diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.md b/.github/ISSUE_TEMPLATE/docs_improvement.md index f2dc170a8185f..4bc84c5fc9eb7 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/book/contributing/code/) 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://bevyengine.org/learn/contribute/helping-out/writing-docs/) instead. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ccc60ae289f2..e0f2450b46b55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ env: CARGO_TERM_COLOR: always # If nightly is breaking CI, modify this variable to target a specific nightly version. NIGHTLY_TOOLCHAIN: nightly + RUSTFLAGS: "-D warnings" concurrency: group: ${{github.workflow}}-${{github.ref}} @@ -195,7 +196,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" + RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings" markdownlint: runs-on: ubuntu-latest @@ -241,7 +242,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.28.3 + uses: crate-ci/typos@v1.29.4 - name: Typos info if: failure() run: | @@ -319,7 +320,7 @@ jobs: run: cargo run -p ci -- doc env: CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-C debuginfo=0" + RUSTFLAGS: "-C debuginfo=0 -D warnings" # This currently report a lot of false positives # Enable it again once it's fixed - https://github.com/bevyengine/bevy/issues/1983 # - name: Installs cargo-deadlinks diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml deleted file mode 100644 index d187165a763e9..0000000000000 --- a/.github/workflows/daily.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: Daily Jobs - -on: - schedule: - - cron: '0 12 * * *' - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - -jobs: - build-for-iOS: - if: github.repository == 'bevyengine/bevy' - runs-on: macos-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Add iOS targets - run: rustup target add aarch64-apple-ios x86_64-apple-ios - - - name: Build app for iOS - run: | - cd examples/mobile - make xcodebuild-iphone - mkdir Payload - mv build/Build/Products/Debug-iphoneos/bevy_mobile_example.app Payload - zip -r bevy_mobile_example.zip Payload - mv bevy_mobile_example.zip bevy_mobile_example.ipa - - - name: Upload to Browser Stack - run: | - curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@examples/mobile/bevy_mobile_example.ipa" \ - -F "custom_id=$GITHUB_RUN_ID" - - build-for-Android: - if: github.repository == 'bevyengine/bevy' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - java-version: '17' - distribution: 'temurin' - - - name: Add Android targets - run: rustup target add aarch64-linux-android - - - name: Install Cargo NDK - run: cargo install --force cargo-ndk - - - name: Build .so file - run: cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build --package bevy_mobile_example - env: - # This will reduce the APK size from 1GB to ~200MB - CARGO_PROFILE_DEV_DEBUG: false - - - name: Build app for Android - run: cd examples/mobile/android_example && chmod +x gradlew && ./gradlew build - - - name: Upload to Browser Stack - run: | - curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ - -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@app/build/outputs/apk/debug/app-debug.apk" \ - -F "custom_id=$GITHUB_RUN_ID" - - nonce: - if: github.repository == 'bevyengine/bevy' - runs-on: ubuntu-latest - timeout-minutes: 30 - outputs: - result: ${{ steps.nonce.outputs.result }} - steps: - - id: nonce - run: echo "result=${{ github.run_id }}-$(date +%s)" >> $GITHUB_OUTPUT - - run: - if: github.repository == 'bevyengine/bevy' - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [nonce, build-for-iOS, build-for-Android] - env: - PERCY_PARALLEL_NONCE: ${{ needs.nonce.outputs.result }} - PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }} - strategy: - matrix: - include: - - device: "iPhone 13" - os_version: "15" - - device: "iPhone 14" - os_version: "16" - - device: "iPhone 15" - os_version: "17" - - device: "Xiaomi Redmi Note 11" - os_version: "11.0" - - device: "Google Pixel 6" - os_version: "12.0" - - device: "Samsung Galaxy S23" - os_version: "13.0" - - device: "Google Pixel 8" - os_version: "14.0" - steps: - - uses: actions/checkout@v4 - - - name: Run Example - run: | - cd .github/start-mobile-example - npm install - npm install -g @percy/cli@latest - npx percy app:exec --parallel -- npm run mobile - env: - BROWSERSTACK_APP_ID: ${{ github.run_id }} - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} - DEVICE: ${{ matrix.device }} - OS_VERSION: ${{ matrix.os_version }} - - - name: Save screenshots - if: ${{ always() }} - uses: actions/upload-artifact@v4 - with: - name: screenshots-${{ matrix.device }}-${{ matrix.os_version }} - path: .github/start-mobile-example/*.png - - check-result: - if: github.repository == 'bevyengine/bevy' - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [run] - steps: - - name: Wait for screenshots comparison - run: | - npm install -g @percy/cli@latest - npx percy build:wait --project dede4209/Bevy-Mobile-Example --commit ${{ github.sha }} --fail-on-changes --pass-if-approved - env: - PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 7902584a9fdb9..91a98f3ea7acc 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -8,9 +8,12 @@ env: CARGO_TERM_COLOR: always jobs: - ci: + bump: if: github.repository == 'bevyengine/bevy' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 32e481b23047c..0000000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Release - -# how to trigger: https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow -on: - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always - -jobs: - ci: - if: github.repository == 'bevyengine/bevy' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install cargo-release - run: cargo install cargo-release - - - name: Setup release - run: | - # Set the commit author to the github-actions bot. See discussion here for more information: - # https://github.com/actions/checkout/issues/13#issuecomment-724415212 - # https://github.community/t/github-actions-bot-email-address/17204/6 - git config user.name 'Bevy Auto Releaser' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - # release: remove the dev suffix, like going from 0.X.0-dev to 0.X.0 - # --workspace: updating all crates in the workspace - # --no-publish: do not publish to crates.io - # --execute: not a dry run - # --no-tag: do not push tag for each new version - # --no-push: do not push the update commits - # --dependent-version upgrade: change 0.X.0-dev in internal dependencies to 0.X.0 - # --exclude: ignore those packages - cargo release release \ - --workspace \ - --no-publish \ - --execute \ - --no-tag \ - --no-confirm \ - --no-push \ - --dependent-version upgrade \ - --exclude ci \ - --exclude errors \ - --exclude bevy_mobile_example \ - --exclude build-wasm-example - - - name: Create PR - uses: peter-evans/create-pull-request@v7 - with: - delete-branch: true - base: "main" - title: "Preparing Next Release" - body: | - Preparing next release. This PR has been auto-generated. diff --git a/.github/workflows/send-screenshots-to-pixeleagle.yml b/.github/workflows/send-screenshots-to-pixeleagle.yml index 4372d75ec865a..b43a316f2593e 100644 --- a/.github/workflows/send-screenshots-to-pixeleagle.yml +++ b/.github/workflows/send-screenshots-to-pixeleagle.yml @@ -34,13 +34,13 @@ jobs: if: ${{ ! fromJSON(env.PIXELEAGLE_TOKEN_EXISTS) }} run: | echo "The PIXELEAGLE_TOKEN secret does not exist, so uploading screenshots to Pixel Eagle was skipped." >> $GITHUB_STEP_SUMMARY - + - name: Download artifact if: ${{ fromJSON(env.PIXELEAGLE_TOKEN_EXISTS) }} uses: actions/download-artifact@v4 with: pattern: ${{ inputs.artifact }} - + - name: Send to Pixel Eagle if: ${{ fromJSON(env.PIXELEAGLE_TOKEN_EXISTS) }} env: @@ -49,11 +49,11 @@ jobs: # Create a new run with its associated metadata metadata='{"os":"${{ inputs.os }}", "commit": "${{ inputs.commit }}", "branch": "${{ inputs.branch }}"}' run=`curl https://pixel-eagle.vleue.com/$project/runs --json "$metadata" --oauth2-bearer ${{ secrets.PIXELEAGLE_TOKEN }} | jq '.id'` - + SAVEIFS=$IFS - + cd ${{ inputs.artifact }} - + # Read the hashes of the screenshot for fast comparison when they are equal IFS=$'\n' # Build a json array of screenshots and their hashes @@ -67,7 +67,7 @@ jobs: done hashes=`echo $hashes | rev | cut -c 2- | rev` hashes="$hashes]" - + IFS=$SAVEIFS # Upload screenshots with unknown hashes @@ -78,7 +78,7 @@ jobs: curl https://pixel-eagle.vleue.com/$project/runs/$run/screenshots -F "data=@./screenshots-$name" -F "screenshot=$name" --oauth2-bearer ${{ secrets.PIXELEAGLE_TOKEN }} echo done - + IFS=$SAVEIFS cd .. @@ -93,17 +93,17 @@ jobs: missing=`cat pixeleagle.json | jq '.missing | length'` if [ ! $missing -eq 0 ]; then echo "There are $missing missing screenshots" - echo "::warning title=$missing missing screenshots on ${{ inputs.os }}::https://pixel-eagle.vleue.com/$project/runs/$run/compare/$compared_with" + echo "::warning title=$missing missing screenshots on ${{ inputs.os }}::https://pixel-eagle.com/project/$project/run/$run/compare/$compared_with" status=1 fi diff=`cat pixeleagle.json | jq '.diff | length'` if [ ! $diff -eq 0 ]; then echo "There are $diff screenshots with a difference" - echo "::warning title=$diff different screenshots on ${{ inputs.os }}::https://pixel-eagle.vleue.com/$project/runs/$run/compare/$compared_with" + echo "::warning title=$diff different screenshots on ${{ inputs.os }}::https://pixel-eagle.com/project/$project/run/$run/compare/$compared_with" status=1 fi - echo "created run $run: https://pixel-eagle.vleue.com/$project/runs/$run/compare/$compared_with" + echo "created run $run: https://pixel-eagle.com/project/$project/run/$run/compare/$compared_with" exit $status diff --git a/Cargo.toml b/Cargo.toml index 56aac74f53cb9..d6d5aedacd086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" categories = ["game-engines", "graphics", "gui", "rendering"] description = "A refreshingly simple data-driven game engine and app framework" @@ -13,20 +13,20 @@ documentation = "https://docs.rs/bevy" rust-version = "1.83.0" [workspace] -exclude = [ - "benches", - "crates/bevy_derive/compile_fail", - "crates/bevy_ecs/compile_fail", - "crates/bevy_reflect/compile_fail", - "tools/compile_fail_utils", -] +resolver = "2" members = [ + # All of Bevy's official crates are within the `crates` folder! "crates/*", + # Several crates with macros have "compile fail" tests nested inside them, also known as UI + # tests, that verify diagnostic output does not accidentally change. + "crates/*/compile_fail", + # Examples of compiling Bevy for mobile platforms. "examples/mobile", - "tools/ci", - "tools/build-templated-pages", - "tools/build-wasm-example", - "tools/example-showcase", + # Benchmarks + "benches", + # Internal tools that are not published. + "tools/*", + # Bevy's error codes. This is a crate so we can automatically check all of the code blocks. "errors", ] @@ -41,6 +41,7 @@ type_complexity = "allow" undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" needless_lifetimes = "allow" +too_many_arguments = "allow" ptr_as_ptr = "warn" ptr_cast_constness = "warn" @@ -53,6 +54,9 @@ std_instead_of_core = "warn" std_instead_of_alloc = "warn" alloc_instead_of_core = "warn" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" + [workspace.lints.rust] missing_docs = "warn" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } @@ -82,6 +86,7 @@ type_complexity = "allow" undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" needless_lifetimes = "allow" +too_many_arguments = "allow" ptr_as_ptr = "warn" ptr_cast_constness = "warn" @@ -93,6 +98,9 @@ std_instead_of_core = "allow" std_instead_of_alloc = "allow" alloc_instead_of_core = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" + [lints.rust] missing_docs = "warn" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } @@ -213,7 +221,6 @@ bevy_sprite = [ "bevy_render", "bevy_core_pipeline", "bevy_color", - "bevy_sprite_picking_backend", ] # Provides text functionality @@ -226,14 +233,16 @@ bevy_ui = [ "bevy_text", "bevy_sprite", "bevy_color", - "bevy_ui_picking_backend", ] # Windowing layer bevy_window = ["bevy_internal/bevy_window"] # winit window and input backend -bevy_winit = ["bevy_internal/bevy_winit"] +bevy_winit = ["bevy_internal/bevy_winit", "bevy_window"] + +# Load and access image data. Usually added by an image format +bevy_image = ["bevy_internal/bevy_image"] # Adds support for rendering gizmos bevy_gizmos = ["bevy_internal/bevy_gizmos", "bevy_color"] @@ -452,7 +461,7 @@ ios_simulator = ["bevy_internal/ios_simulator"] bevy_state = ["bevy_internal/bevy_state"] # Enables source location tracking for change detection and spawning/despawning, which can assist with debugging -track_change_detection = ["bevy_internal/track_change_detection"] +track_location = ["bevy_internal/track_location"] # Enable function reflection reflect_functions = ["bevy_internal/reflect_functions"] @@ -464,11 +473,11 @@ custom_cursor = ["bevy_internal/custom_cursor"] ghost_nodes = ["bevy_internal/ghost_nodes"] [dependencies] -bevy_internal = { path = "crates/bevy_internal", version = "0.15.0-dev", default-features = false } +bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } # Wasm does not support dynamic linking. [target.'cfg(not(target_family = "wasm"))'.dependencies] -bevy_dylib = { path = "crates/bevy_dylib", version = "0.15.0-dev", default-features = false, optional = true } +bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-features = false, optional = true } [dev-dependencies] rand = "0.8.0" @@ -478,7 +487,7 @@ flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1" bytemuck = "1.7" -bevy_render = { path = "crates/bevy_render", version = "0.15.0-dev", default-features = false } +bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" async-std = "1.13" @@ -491,6 +500,7 @@ http-body-util = "0.1" anyhow = "1" macro_rules_attribute = "0.2" accesskit = "0.17" +nonmax = "0.5" [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] smol = "2" @@ -942,6 +952,17 @@ description = "Illustrates bloom configuration using HDR and emissive materials" category = "3D Rendering" wasm = true +[[example]] +name = "decal" +path = "examples/3d/decal.rs" +doc-scrape-examples = true + +[package.metadata.example.decal] +name = "Decal" +description = "Decal rendering" +category = "3D Rendering" +wasm = true + [[example]] name = "deferred_rendering" path = "examples/3d/deferred_rendering.rs" @@ -1233,7 +1254,7 @@ setup = [ "curl", "-o", "assets/models/bunny.meshlet_mesh", - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/defbd9b32072624d40d57de7d345c66a9edf5d0b/bunny.meshlet_mesh", + "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/7a7c14138021f63904b584d5f7b73b695c7f4bbf/bunny.meshlet_mesh", ], ] @@ -1280,13 +1301,35 @@ category = "Animation" wasm = true [[example]] -name = "animated_fox" -path = "examples/animation/animated_fox.rs" +name = "animated_mesh" +path = "examples/animation/animated_mesh.rs" +doc-scrape-examples = true + +[package.metadata.example.animated_mesh] +name = "Animated Mesh" +description = "Plays an animation on a skinned glTF model of a fox" +category = "Animation" +wasm = true + +[[example]] +name = "animated_mesh_control" +path = "examples/animation/animated_mesh_control.rs" +doc-scrape-examples = true + +[package.metadata.example.animated_mesh_control] +name = "Animated Mesh Control" +description = "Plays an animation from a skinned glTF with keyboard controls" +category = "Animation" +wasm = true + +[[example]] +name = "animated_mesh_events" +path = "examples/animation/animated_mesh_events.rs" doc-scrape-examples = true -[package.metadata.example.animated_fox] -name = "Animated Fox" -description = "Plays an animation from a skinned glTF" +[package.metadata.example.animated_mesh_events] +name = "Animated Mesh Events" +description = "Plays an animation from a skinned glTF with events" category = "Animation" wasm = true @@ -1850,7 +1893,7 @@ wasm = false name = "change_detection" path = "examples/ecs/change_detection.rs" doc-scrape-examples = true -required-features = ["track_change_detection"] +required-features = ["track_location"] [package.metadata.example.change_detection] name = "Change Detection" @@ -2608,6 +2651,18 @@ description = "A shader that renders a mesh multiple times in one draw call usin category = "Shaders" wasm = true +[[example]] +name = "custom_render_phase" +path = "examples/shader/custom_render_phase.rs" +doc-scrape-examples = true + +[package.metadata.example.custom_render_phase] +name = "Custom Render Phase" +description = "Shows how to make a complete render phase" +category = "Shaders" +wasm = true + + [[example]] name = "automatic_instancing" path = "examples/shader/automatic_instancing.rs" @@ -2830,6 +2885,17 @@ description = "Displays many sprites in a grid arrangement! Used for performance category = "Stress Tests" wasm = true +[[example]] +name = "many_text2d" +path = "examples/stress_tests/many_text2d.rs" +doc-scrape-examples = true + +[package.metadata.example.many_text2d] +name = "Many Text2d" +description = "Displays many Text2d! Used for performance testing." +category = "Stress Tests" +wasm = true + [[example]] name = "transform_hierarchy" path = "examples/stress_tests/transform_hierarchy.rs" @@ -3275,6 +3341,18 @@ description = "Creates a solid color window" category = "Window" wasm = true +[[example]] +name = "custom_cursor_image" +path = "examples/window/custom_cursor_image.rs" +doc-scrape-examples = true +required-features = ["custom_cursor"] + +[package.metadata.example.custom_cursor_image] +name = "Custom Cursor Image" +description = "Demonstrates creating an animated custom cursor from an image" +category = "Window" +wasm = true + [[example]] name = "custom_user_event" path = "examples/window/custom_user_event.rs" @@ -3557,6 +3635,17 @@ description = "A 2D top-down camera smoothly following player movements" category = "Camera" wasm = true +[[example]] +name = "custom_projection" +path = "examples/camera/custom_projection.rs" +doc-scrape-examples = true + +[package.metadata.example.custom_projection] +name = "Custom Projection" +description = "Shows how to create custom camera projections." +category = "Camera" +wasm = true + [[example]] name = "first_person_view_model" path = "examples/camera/first_person_view_model.rs" @@ -3826,6 +3915,18 @@ category = "Picking" wasm = true required-features = ["bevy_sprite_picking_backend"] +[[example]] +name = "debug_picking" +path = "examples/picking/debug_picking.rs" +doc-scrape-examples = true +required-features = ["bevy_dev_tools"] + +[package.metadata.example.debug_picking] +name = "Picking Debug Tools" +description = "Demonstrates picking debug overlay" +category = "Picking" +wasm = true + [[example]] name = "animation_masks" path = "examples/animation/animation_masks.rs" @@ -3955,6 +4056,17 @@ doc-scrape-examples = true [package.metadata.example.tab_navigation] name = "Tab Navigation" -description = "Demonstration of Tab Navigation" +description = "Demonstration of Tab Navigation between UI elements" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "directional_navigation" +path = "examples/ui/directional_navigation.rs" +doc-scrape-examples = true + +[package.metadata.example.directional_navigation] +name = "Directional Navigation" +description = "Demonstration of Directional Navigation between UI elements" category = "UI (User Interface)" wasm = true diff --git a/assets/cursors/kenney_crosshairPack/License.txt b/assets/cursors/kenney_crosshairPack/License.txt new file mode 100644 index 0000000000000..d6eaa6cb6b7d6 --- /dev/null +++ b/assets/cursors/kenney_crosshairPack/License.txt @@ -0,0 +1,19 @@ + + + Crosshair Pack + + by Kenney Vleugels (Kenney.nl) + + ------------------------------ + + License (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You may use these assets in personal and commercial projects. + Credit (Kenney or www.kenney.nl) would be nice but is not mandatory. + + ------------------------------ + + Donate: http://support.kenney.nl + + Follow on Twitter for updates: @KenneyNL (www.twitter.com/kenneynl) diff --git a/assets/cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png b/assets/cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png new file mode 100644 index 0000000000000..76c8b2f851414 Binary files /dev/null and b/assets/cursors/kenney_crosshairPack/Tilesheet/crosshairs_tilesheet_white.png differ diff --git a/assets/shaders/custom_stencil.wgsl b/assets/shaders/custom_stencil.wgsl new file mode 100644 index 0000000000000..6f2fa2da4f977 --- /dev/null +++ b/assets/shaders/custom_stencil.wgsl @@ -0,0 +1,41 @@ +//! A shader showing how to use the vertex position data to output the +//! stencil in the right position + +// First we import everything we need from bevy_pbr +// A 2d shader would be vevry similar but import from bevy_sprite instead +#import bevy_pbr::{ + mesh_functions, + view_transformations::position_world_to_clip +} + +struct Vertex { + // This is needed if you are using batching and/or gpu preprocessing + // It's a built in so you don't need to define it in the vertex layout + @builtin(instance_index) instance_index: u32, + // Like we defined for the vertex layout + // position is at location 0 + @location(0) position: vec3, +}; + +// This is the output of the vertex shader and we also use it as the input for the fragment shader +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) world_position: vec4, +}; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + // This is how bevy computes the world position + // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing + var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); + out.clip_position = position_world_to_clip(out.world_position.xyz); + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + // Output a red color to represent the stencil of the mesh + return vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/assets/shaders/custom_ui_material.wgsl b/assets/shaders/custom_ui_material.wgsl index ef000aef7e87e..ae53b528d8cb2 100644 --- a/assets/shaders/custom_ui_material.wgsl +++ b/assets/shaders/custom_ui_material.wgsl @@ -10,16 +10,24 @@ @fragment fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + // normalized position relative to the center of the UI node let r = in.uv - 0.5; + + // normalized size of the border closest to the current position let b = vec2( - select(in.border_widths.x, in.border_widths.y, r.x < 0.), - select(in.border_widths.z, in.border_widths.w, r.y < 0.) + select(in.border_widths.x, in.border_widths.y, 0. < r.x), + select(in.border_widths.z, in.border_widths.w, 0. < r.y) ); + // if the distance to the edge from the current position on any axis + // is less than the border width on that axis then the position is within + // the border and we return the border color if any(0.5 - b < abs(r)) { return border_color; } + // sample the texture at this position if it's to the left of the slider value + // otherwise return a fully transparent color if in.uv.x < slider { let output_color = textureSample(material_color_texture, material_color_sampler, in.uv) * color; return output_color; diff --git a/assets/shaders/specialized_mesh_pipeline.wgsl b/assets/shaders/specialized_mesh_pipeline.wgsl index 82b5cea911658..e307a7c48c406 100644 --- a/assets/shaders/specialized_mesh_pipeline.wgsl +++ b/assets/shaders/specialized_mesh_pipeline.wgsl @@ -30,7 +30,7 @@ struct VertexOutput { fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; // This is how bevy computes the world position - // The vertex.instance_index is very important. Esepecially if you are using batching and gpu preprocessing + // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); out.clip_position = position_world_to_clip(out.world_position.xyz); diff --git a/assets/shaders/storage_buffer.wgsl b/assets/shaders/storage_buffer.wgsl index c052411e3f198..1859e8dde2755 100644 --- a/assets/shaders/storage_buffer.wgsl +++ b/assets/shaders/storage_buffer.wgsl @@ -4,6 +4,7 @@ } @group(2) @binding(0) var colors: array, 5>; +@group(2) @binding(1) var color_id: u32; struct Vertex { @builtin(instance_index) instance_index: u32, @@ -23,10 +24,7 @@ fn vertex(vertex: Vertex) -> VertexOutput { out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); out.clip_position = position_world_to_clip(out.world_position.xyz); - // We have 5 colors in the storage buffer, but potentially many instances of the mesh, so - // we use the instance index to select a color from the storage buffer. - out.color = colors[vertex.instance_index % 5]; - + out.color = colors[color_id]; return out; } diff --git a/benches/Cargo.toml b/benches/Cargo.toml index b05e8bba351f5..1e55f712a7a79 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -7,6 +7,11 @@ license = "MIT OR Apache-2.0" # Do not automatically discover benchmarks, we specify them manually instead. 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"] } + [dev-dependencies] # Bevy crates bevy_app = { path = "../crates/bevy_app" } @@ -22,7 +27,6 @@ bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } # Other crates -criterion = { version = "0.5.1", features = ["html_reports"] } glam = "0.29" rand = "0.8" rand_chacha = "0.3" @@ -32,10 +36,6 @@ rand_chacha = "0.3" [target.'cfg(target_os = "linux")'.dev-dependencies] bevy_winit = { path = "../crates/bevy_winit", features = ["x11"] } -[profile.release] -opt-level = 3 -lto = true - [lints.clippy] doc_markdown = "warn" manual_let_else = "warn" @@ -47,6 +47,7 @@ type_complexity = "allow" undocumented_unsafe_blocks = "warn" unwrap_or_default = "warn" needless_lifetimes = "allow" +too_many_arguments = "allow" ptr_as_ptr = "warn" ptr_cast_constness = "warn" @@ -55,15 +56,20 @@ ref_as_ptr = "warn" # see: https://github.com/bevyengine/bevy/pull/15375#issuecomment-2366966219 too_long_first_doc_paragraph = "allow" +allow_attributes = "warn" +allow_attributes_without_reason = "warn" + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_op_in_unsafe_fn = "warn" unused_qualifications = "warn" -[[bench]] -name = "entity_cloning" -path = "benches/bevy_ecs/entity_cloning.rs" -harness = false +[lib] +# This fixes the "Unrecognized Option" error when running commands like +# `cargo bench -- --save-baseline before` by disabling the default benchmark harness. +# See +# for more information. +bench = false [[bench]] name = "ecs" diff --git a/benches/README.md b/benches/README.md index 2641ab027aa72..2e91916e481f1 100644 --- a/benches/README.md +++ b/benches/README.md @@ -1,27 +1,35 @@ # Bevy Benchmarks -This is a crate with a collection of benchmarks for Bevy, separate from the rest of the Bevy crates. +This is a crate with a collection of benchmarks for Bevy. -## Running the benchmarks +## Running benchmarks -1. Setup everything you need for Bevy with the [setup guide](https://bevyengine.org/learn/book/getting-started/setup/). -2. Move into the `benches` directory (where this README is located). +Benchmarks can be run through Cargo: - ```sh - bevy $ cd benches - ``` +```sh +# Run all benchmarks. (This will take a while!) +cargo bench -p benches -3. Run the benchmarks with cargo (This will take a while) +# Just compile the benchmarks, do not run them. +cargo bench -p benches --no-run - ```sh - bevy/benches $ cargo bench - ``` +# Run the benchmarks for a specific crate. (See `Cargo.toml` for a complete list of crates +# tracked.) +cargo bench -p benches --bench ecs - If you'd like to only compile the benchmarks (without running them), you can do that like this: +# Filter which benchmarks are run based on the name. This will only run benchmarks whose name +# contains "name_fragment". +cargo bench -p benches -- name_fragment - ```sh - bevy/benches $ cargo bench --no-run - ``` +# List all available benchmarks. +cargo bench -p benches -- --list + +# Save a baseline to be compared against later. +cargo bench -p benches --save-baseline before + +# Compare the current benchmarks against a baseline to find performance gains and regressions. +cargo bench -p benches --baseline before +``` ## Criterion diff --git a/benches/benches/bevy_ecs/change_detection.rs b/benches/benches/bevy_ecs/change_detection.rs index ca57da93fabe6..8d49c2b8c7e1c 100644 --- a/benches/benches/bevy_ecs/change_detection.rs +++ b/benches/benches/bevy_ecs/change_detection.rs @@ -1,3 +1,5 @@ +use core::hint::black_box; + use bevy_ecs::{ component::{Component, Mutable}, entity::Entity, @@ -5,7 +7,7 @@ use bevy_ecs::{ query::QueryFilter, world::World, }; -use criterion::{black_box, criterion_group, Criterion}; +use criterion::{criterion_group, Criterion}; use rand::{prelude::SliceRandom, SeedableRng}; use rand_chacha::ChaCha8Rng; diff --git a/benches/benches/bevy_ecs/components/add_remove_very_big_table.rs b/benches/benches/bevy_ecs/components/add_remove_very_big_table.rs index 1a4f238cd32c2..72555be8c28b0 100644 --- a/benches/benches/bevy_ecs/components/add_remove_very_big_table.rs +++ b/benches/benches/bevy_ecs/components/add_remove_very_big_table.rs @@ -1,4 +1,7 @@ -#![allow(dead_code)] +#![expect( + dead_code, + reason = "The `Mat4`s in the structs are used to bloat the size of the structs for benchmarking purposes." +)] use bevy_ecs::prelude::*; use glam::*; diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index daec970b74a87..91c2b5427a068 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -1,5 +1,7 @@ +use core::hint::black_box; + use bevy_ecs::{component::Component, prelude::*, schedule::ExecutorKind, world::World}; -use criterion::{black_box, criterion_group, BenchmarkId, Criterion}; +use criterion::{criterion_group, BenchmarkId, Criterion}; criterion_group!(benches, empty_archetypes); diff --git a/benches/benches/bevy_ecs/entity_cloning.rs b/benches/benches/bevy_ecs/entity_cloning.rs index d0c71c7c2ae8e..80577b9a9d0b5 100644 --- a/benches/benches/bevy_ecs/entity_cloning.rs +++ b/benches/benches/bevy_ecs/entity_cloning.rs @@ -1,171 +1,236 @@ +use core::hint::black_box; + +use benches::bench; use bevy_ecs::bundle::Bundle; +use bevy_ecs::component::ComponentCloneHandler; use bevy_ecs::reflect::AppTypeRegistry; -use bevy_ecs::{component::Component, reflect::ReflectComponent, world::World}; +use bevy_ecs::{component::Component, world::World}; use bevy_hierarchy::{BuildChildren, CloneEntityHierarchyExt}; use bevy_math::Mat4; use bevy_reflect::{GetTypeRegistration, Reflect}; -use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use criterion::{criterion_group, Bencher, Criterion, Throughput}; -criterion_group!(benches, reflect_benches, clone_benches); -criterion_main!(benches); +criterion_group!( + benches, + single, + hierarchy_tall, + hierarchy_wide, + hierarchy_many, +); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C1(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C2(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C3(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C4(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C5(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C6(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C7(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C8(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C9(Mat4); #[derive(Component, Reflect, Default, Clone)] -#[reflect(Component)] struct C10(Mat4); type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10); -fn hierarchy( +/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to +/// use the [`Reflect`] trait instead of [`Clone`]. +fn set_reflect_clone_handler(world: &mut World) { + // Get mutable access to the type registry, creating it if it does not exist yet. + let registry = world.get_resource_or_init::(); + + // Recursively register all components in the bundle to the reflection type registry. + { + let mut r = registry.write(); + r.register::(); + } + + // Recursively register all components in the bundle, then save the component IDs to a list. + // This uses `contributed_components()`, meaning both explicit and required component IDs in + // this bundle are saved. + let component_ids: Vec<_> = world.register_bundle::().contributed_components().into(); + + let clone_handlers = world.get_component_clone_handlers_mut(); + + // Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`. + for component in component_ids { + clone_handlers.set_component_handler(component, ComponentCloneHandler::reflect_handler()); + } +} + +/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on 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` +/// is true, it will overwrite the handler for all components in the bundle to be +/// [`ComponentCloneHandler::reflect_handler()`]. +fn bench_clone( b: &mut Bencher, - width: usize, - height: usize, clone_via_reflect: bool, ) { let mut world = World::default(); - let registry = AppTypeRegistry::default(); - { - let mut r = registry.write(); - r.register::(); + + if clone_via_reflect { + set_reflect_clone_handler::(&mut world); } - world.insert_resource(registry); - world.register_bundle::(); + + // Spawn the first entity, which will be cloned in the benchmark routine. + let id = world.spawn(B::default()).id(); + + b.iter(|| { + // Queue the command to clone the entity. + world.commands().entity(black_box(id)).clone_and_spawn(); + + // Run the command. + world.flush(); + }); +} + +/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on 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 +/// specified number of `children`. +/// +/// For example, setting `height` to 5 and `children` to 1 creates a single chain of entities with +/// no siblings. Alternatively, setting `height` to 1 and `children` to 5 will spawn 5 direct +/// children of the root entity. +fn bench_clone_hierarchy( + b: &mut Bencher, + height: usize, + children: usize, + clone_via_reflect: bool, +) { + let mut world = World::default(); + if clone_via_reflect { - let mut components = Vec::new(); - C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap())); - for component in components { - world - .get_component_clone_handlers_mut() - .set_component_handler( - component, - bevy_ecs::component::ComponentCloneHandler::reflect_handler(), - ); - } + set_reflect_clone_handler::(&mut world); } - let id = world.spawn(black_box(C::default())).id(); + // Spawn the first entity, which will be cloned in the benchmark routine. + let id = world.spawn(B::default()).id(); let mut hierarchy_level = vec![id]; + // Set up the hierarchy tree by spawning all children. for _ in 0..height { let current_hierarchy_level = hierarchy_level.clone(); + hierarchy_level.clear(); + for parent_id in current_hierarchy_level { - for _ in 0..width { - let child_id = world - .spawn(black_box(C::default())) - .set_parent(parent_id) - .id(); - hierarchy_level.push(child_id) + for _ in 0..children { + let child_id = world.spawn(B::default()).set_parent(parent_id).id(); + + hierarchy_level.push(child_id); } } } + + // Flush all `set_parent()` commands. world.flush(); - b.iter(move || { - world.commands().entity(id).clone_and_spawn_with(|builder| { - builder.recursive(true); - }); + b.iter(|| { + world + .commands() + .entity(black_box(id)) + .clone_and_spawn_with(|builder| { + // Make the clone command recursive, so children are cloned as well. + builder.recursive(true); + }); + world.flush(); }); } -fn simple(b: &mut Bencher, clone_via_reflect: bool) { - let mut world = World::default(); - let registry = AppTypeRegistry::default(); - { - let mut r = registry.write(); - r.register::(); +// 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)]; + +/// Benchmarks cloning a single entity with 10 components and no children. +fn single(c: &mut Criterion) { + let mut group = c.benchmark_group(bench!("single")); + + // We're cloning 1 entity. + group.throughput(Throughput::Elements(1)); + + for (id, clone_via_reflect) in SCENARIOS { + group.bench_function(id, |b| { + bench_clone::(b, clone_via_reflect); + }); } - world.insert_resource(registry); - world.register_bundle::(); - if clone_via_reflect { - let mut components = Vec::new(); - C::get_component_ids(world.components(), &mut |id| components.push(id.unwrap())); - for component in components { - world - .get_component_clone_handlers_mut() - .set_component_handler( - component, - bevy_ecs::component::ComponentCloneHandler::reflect_handler(), - ); - } + + group.finish(); +} + +/// Benchmarks cloning an an entity and its 50 descendents, each with only 1 component. +fn hierarchy_tall(c: &mut Criterion) { + let mut group = c.benchmark_group(bench!("hierarchy_tall")); + + // We're cloning both the root entity and its 50 descendents. + group.throughput(Throughput::Elements(51)); + + for (id, clone_via_reflect) in SCENARIOS { + group.bench_function(id, |b| { + bench_clone_hierarchy::(b, 50, 1, clone_via_reflect); + }); } - let id = world.spawn(black_box(C::default())).id(); - b.iter(move || { - world.commands().entity(id).clone_and_spawn(); - world.flush(); - }); + group.finish(); } -fn reflect_benches(c: &mut Criterion) { - c.bench_function("many components reflect", |b| { - simple::(b, true); - }); +/// Benchmarks cloning an an entity and its 50 direct children, each with only 1 component. +fn hierarchy_wide(c: &mut Criterion) { + let mut group = c.benchmark_group(bench!("hierarchy_wide")); - c.bench_function("hierarchy wide reflect", |b| { - hierarchy::(b, 10, 4, true); - }); + // We're cloning both the root entity and its 50 direct children. + group.throughput(Throughput::Elements(51)); - c.bench_function("hierarchy tall reflect", |b| { - hierarchy::(b, 1, 50, true); - }); + for (id, clone_via_reflect) in SCENARIOS { + group.bench_function(id, |b| { + bench_clone_hierarchy::(b, 1, 50, clone_via_reflect); + }); + } - c.bench_function("hierarchy many reflect", |b| { - hierarchy::(b, 5, 5, true); - }); + group.finish(); } -fn clone_benches(c: &mut Criterion) { - c.bench_function("many components clone", |b| { - simple::(b, false); - }); +/// Benchmarks cloning a large hierarchy of entities with several children each. Each entity has 10 +/// components. +fn hierarchy_many(c: &mut Criterion) { + let mut group = c.benchmark_group(bench!("hierarchy_many")); - c.bench_function("hierarchy wide clone", |b| { - hierarchy::(b, 10, 4, false); - }); + // We're cloning 364 entities total. This number was calculated by manually counting the number + // of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :) + group.throughput(Throughput::Elements(364)); - c.bench_function("hierarchy tall clone", |b| { - hierarchy::(b, 1, 50, false); - }); + for (id, clone_via_reflect) in SCENARIOS { + group.bench_function(id, |b| { + bench_clone_hierarchy::(b, 5, 3, clone_via_reflect); + }); + } - c.bench_function("hierarchy many clone", |b| { - hierarchy::(b, 5, 5, false); - }); + group.finish(); } diff --git a/benches/benches/bevy_ecs/main.rs b/benches/benches/bevy_ecs/main.rs index 83f0cde0286d6..4a025ab829369 100644 --- a/benches/benches/bevy_ecs/main.rs +++ b/benches/benches/bevy_ecs/main.rs @@ -2,13 +2,13 @@ dead_code, reason = "Many fields are unused/unread as they are just for benchmarking purposes." )] -#![expect(clippy::type_complexity)] use criterion::criterion_main; mod change_detection; mod components; mod empty_archetypes; +mod entity_cloning; mod events; mod fragmentation; mod iteration; @@ -21,6 +21,7 @@ criterion_main!( change_detection::benches, components::benches, empty_archetypes::benches, + entity_cloning::benches, events::benches, iteration::benches, fragmentation::benches, diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 5de85bc3269b2..1989c05fc62f7 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -1,9 +1,11 @@ +use core::hint::black_box; + use bevy_ecs::{ component::Component, entity::Entity, event::Event, observer::Trigger, world::World, }; use bevy_hierarchy::{BuildChildren, Parent}; -use criterion::{black_box, Criterion}; +use criterion::Criterion; use rand::SeedableRng; use rand::{seq::IteratorRandom, Rng}; use rand_chacha::ChaCha8Rng; diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 81dd8e021e8ce..bf2dd236d60ea 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,6 +1,8 @@ +use core::hint::black_box; + use bevy_ecs::{entity::Entity, event::Event, observer::Trigger, world::World}; -use criterion::{black_box, Criterion}; +use criterion::Criterion; use rand::{prelude::SliceRandom, SeedableRng}; use rand_chacha::ChaCha8Rng; fn deterministic_rand() -> ChaCha8Rng { diff --git a/benches/benches/bevy_ecs/param/dyn_param.rs b/benches/benches/bevy_ecs/param/dyn_param.rs index 33de52bf13560..b88370272eb1f 100644 --- a/benches/benches/bevy_ecs/param/dyn_param.rs +++ b/benches/benches/bevy_ecs/param/dyn_param.rs @@ -14,6 +14,8 @@ pub fn dyn_param(criterion: &mut Criterion) { #[derive(Resource)] struct R; + world.insert_resource(R); + let mut schedule = Schedule::default(); let system = ( DynParamBuilder::new::>(ParamBuilder), diff --git a/benches/benches/bevy_ecs/param/param_set.rs b/benches/benches/bevy_ecs/param/param_set.rs index 0521561b6b804..3f967a8de174e 100644 --- a/benches/benches/bevy_ecs/param/param_set.rs +++ b/benches/benches/bevy_ecs/param/param_set.rs @@ -11,6 +11,8 @@ pub fn param_set(criterion: &mut Criterion) { #[derive(Resource)] struct R; + world.insert_resource(R); + let mut schedule = Schedule::default(); schedule.add_systems( |_: ParamSet<( diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index a1d7cdb09e382..6ff63b2e20416 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -1,9 +1,11 @@ +use core::hint::black_box; + use bevy_ecs::{ component::Component, - system::Commands, - world::{Command, CommandQueue, World}, + system::{Command, Commands}, + world::{CommandQueue, World}, }; -use criterion::{black_box, Criterion}; +use criterion::Criterion; #[derive(Component)] struct A; diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 190402fbadb27..fcb9b0116bb95 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -1,3 +1,5 @@ +use core::hint::black_box; + use bevy_ecs::{ bundle::Bundle, component::Component, @@ -5,7 +7,7 @@ use bevy_ecs::{ system::{Query, SystemState}, world::World, }; -use criterion::{black_box, Criterion}; +use criterion::Criterion; use rand::{prelude::SliceRandom, SeedableRng}; use rand_chacha::ChaCha8Rng; diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index 404ab08a63eb2..27affcaa71317 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -1,75 +1,83 @@ -use criterion::{black_box, criterion_group, Criterion}; +use benches::bench; +use bevy_math::{prelude::*, VectorSpace}; +use core::hint::black_box; +use criterion::{ + criterion_group, measurement::Measurement, BatchSize, BenchmarkGroup, BenchmarkId, Criterion, +}; -use bevy_math::prelude::*; +criterion_group!(benches, segment_ease, curve_position, curve_iter_positions); -fn easing(c: &mut Criterion) { - let cubic_bezier = CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0)); - c.bench_function("easing_1000", |b| { - b.iter(|| { - (0..1000).map(|i| i as f32 / 1000.0).for_each(|t| { - black_box(cubic_bezier.ease(black_box(t))); - }); - }); +fn segment_ease(c: &mut Criterion) { + let segment = black_box(CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0))); + + c.bench_function(bench!("segment_ease"), |b| { + let mut t = 0; + + b.iter_batched( + || { + // Increment `t` by 1, but use modulo to constrain it to `0..=1000`. + t = (t + 1) % 1001; + + // Return time as a decimal between 0 and 1, inclusive. + t as f32 / 1000.0 + }, + |t| segment.ease(t), + BatchSize::SmallInput, + ); }); } -fn cubic_2d(c: &mut Criterion) { - let bezier = CubicBezier::new([[ +fn curve_position(c: &mut Criterion) { + /// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`]. + fn bench_curve( + group: &mut BenchmarkGroup, + name: &str, + curve: CubicCurve

, + ) { + group.bench_with_input(BenchmarkId::from_parameter(name), &curve, |b, curve| { + b.iter(|| curve.position(black_box(0.5))); + }); + } + + let mut group = c.benchmark_group(bench!("curve_position")); + + let bezier_2 = CubicBezier::new([[ vec2(0.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 0.0), vec2(1.0, 1.0), ]]) .to_curve() - .expect("Unable to build a curve from this data"); - c.bench_function("cubic_position_Vec2", |b| { - b.iter(|| black_box(bezier.position(black_box(0.5)))); - }); -} + .unwrap(); -fn cubic(c: &mut Criterion) { - let bezier = CubicBezier::new([[ - vec3a(0.0, 0.0, 0.0), - vec3a(0.0, 1.0, 0.0), - vec3a(1.0, 0.0, 0.0), - vec3a(1.0, 1.0, 1.0), - ]]) - .to_curve() - .expect("Unable to build a curve from this data"); - c.bench_function("cubic_position_Vec3A", |b| { - b.iter(|| black_box(bezier.position(black_box(0.5)))); - }); -} + bench_curve(&mut group, "vec2", bezier_2); -fn cubic_vec3(c: &mut Criterion) { - let bezier = CubicBezier::new([[ + let bezier_3 = CubicBezier::new([[ vec3(0.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0), ]]) .to_curve() - .expect("Unable to build a curve from this data"); - c.bench_function("cubic_position_Vec3", |b| { - b.iter(|| black_box(bezier.position(black_box(0.5)))); - }); -} + .unwrap(); -fn build_pos_cubic(c: &mut Criterion) { - let bezier = CubicBezier::new([[ + bench_curve(&mut group, "vec3", bezier_3); + + let bezier_3a = CubicBezier::new([[ vec3a(0.0, 0.0, 0.0), vec3a(0.0, 1.0, 0.0), vec3a(1.0, 0.0, 0.0), vec3a(1.0, 1.0, 1.0), ]]) .to_curve() - .expect("Unable to build a curve from this data"); - c.bench_function("build_pos_cubic_100_points", |b| { - b.iter(|| black_box(bezier.iter_positions(black_box(100)).collect::>())); - }); + .unwrap(); + + bench_curve(&mut group, "vec3a", bezier_3a); + + group.finish(); } -fn build_accel_cubic(c: &mut Criterion) { +fn curve_iter_positions(c: &mut Criterion) { let bezier = CubicBezier::new([[ vec3a(0.0, 0.0, 0.0), vec3a(0.0, 1.0, 0.0), @@ -77,18 +85,15 @@ fn build_accel_cubic(c: &mut Criterion) { vec3a(1.0, 1.0, 1.0), ]]) .to_curve() - .expect("Unable to build a curve from this data"); - c.bench_function("build_accel_cubic_100_points", |b| { - b.iter(|| black_box(bezier.iter_positions(black_box(100)).collect::>())); + .unwrap(); + + c.bench_function(bench!("curve_iter_positions"), |b| { + b.iter(|| { + for x in bezier.iter_positions(black_box(100)) { + // Discard `x`, since we just care about `iter_positions()` being consumed, but make + // the compiler believe `x` is being used so it doesn't eliminate the iterator. + black_box(x); + } + }); }); } - -criterion_group!( - benches, - easing, - cubic_2d, - cubic_vec3, - cubic, - build_pos_cubic, - build_accel_cubic, -); diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index 1d019d43ee37f..ee81f1ac7fd87 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -1,31 +1,53 @@ +use core::hint::black_box; +use std::time::Duration; + +use benches::bench; use bevy_math::{Dir3, Mat4, Ray3d, Vec3}; -use bevy_picking::mesh_picking::ray_cast; -use criterion::{black_box, criterion_group, Criterion}; +use bevy_picking::mesh_picking::ray_cast::{self, Backfaces}; +use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; -fn ptoxznorm(p: u32, size: u32) -> (f32, f32) { - let ij = (p / (size), p % (size)); - (ij.0 as f32 / size as f32, ij.1 as f32 / size as f32) -} +criterion_group!(benches, bench); +/// A mesh that can be passed to [`ray_cast::ray_mesh_intersection()`]. struct SimpleMesh { positions: Vec<[f32; 3]>, normals: Vec<[f32; 3]>, indices: Vec, } -fn mesh_creation(vertices_per_side: u32) -> SimpleMesh { +/// Selects a point within a normal square. +/// +/// `p` is an index within `0..vertices_per_side.pow(2)`. The returned value is a coordinate where +/// both `x` and `z` are within `0..1`. +fn p_to_xz_norm(p: u32, vertices_per_side: u32) -> (f32, f32) { + let x = (p / vertices_per_side) as f32; + let z = (p % vertices_per_side) as f32; + + let vertices_per_side = vertices_per_side as f32; + + // Scale `x` and `z` to be between 0 and 1. + (x / vertices_per_side, z / vertices_per_side) +} + +fn create_mesh(vertices_per_side: u32) -> SimpleMesh { let mut positions = Vec::new(); let mut normals = Vec::new(); + let mut indices = Vec::new(); + for p in 0..vertices_per_side.pow(2) { - let xz = ptoxznorm(p, vertices_per_side); - positions.push([xz.0 - 0.5, 0.0, xz.1 - 0.5]); + let (x, z) = p_to_xz_norm(p, vertices_per_side); + + // Push a new vertice to the mesh. We translate all vertices so the final square is + // centered at (0, 0), instead of (0.5, 0.5). + positions.push([x - 0.5, 0.0, z - 0.5]); + + // All vertices have the same normal. normals.push([0.0, 1.0, 0.0]); - } - let mut indices = vec![]; - for p in 0..vertices_per_side.pow(2) { - if p % (vertices_per_side) != vertices_per_side - 1 - && p / (vertices_per_side) != vertices_per_side - 1 + // Extend the indices for for all vertices except for the final row and column, since + // indices are "between" points. + if p % vertices_per_side != vertices_per_side - 1 + && p / vertices_per_side != vertices_per_side - 1 { indices.extend_from_slice(&[p, p + 1, p + vertices_per_side]); indices.extend_from_slice(&[p + vertices_per_side, p + 1, p + vertices_per_side + 1]); @@ -39,81 +61,110 @@ fn mesh_creation(vertices_per_side: u32) -> SimpleMesh { } } -fn ray_mesh_intersection(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection"); - group.warm_up_time(std::time::Duration::from_millis(500)); - - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); - - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Cull, - )); - }); - }); - } +/// An enum that represents the configuration for all variations of the ray mesh intersection +/// benchmarks. +enum Benchmarks { + /// The ray intersects the mesh, and culling is enabled. + CullHit, + + /// The ray intersects the mesh, and culling is disabled. + NoCullHit, + + /// The ray does not intersect the mesh, and culling is enabled. + CullMiss, } -fn ray_mesh_intersection_no_cull(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection_no_cull"); - group.warm_up_time(std::time::Duration::from_millis(500)); - - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", vertices_per_side.pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::NEG_Y); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); - - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Include, - )); - }); - }); +impl Benchmarks { + const WARM_UP_TIME: Duration = Duration::from_millis(500); + const VERTICES_PER_SIDE: [u32; 3] = [10, 100, 1000]; + + /// Returns an iterator over every variant in this enum. + fn iter() -> impl Iterator { + [Self::CullHit, Self::NoCullHit, Self::CullMiss].into_iter() } -} -fn ray_mesh_intersection_no_intersection(c: &mut Criterion) { - let mut group = c.benchmark_group("ray_mesh_intersection_no_intersection"); - group.warm_up_time(std::time::Duration::from_millis(500)); - - for vertices_per_side in [10_u32, 100, 1000] { - group.bench_function(format!("{}_vertices", (vertices_per_side).pow(2)), |b| { - let ray = Ray3d::new(Vec3::new(0.0, 1.0, 0.0), Dir3::X); - let mesh_to_world = Mat4::IDENTITY; - let mesh = mesh_creation(vertices_per_side); - - b.iter(|| { - black_box(ray_cast::ray_mesh_intersection( - ray, - &mesh_to_world, - &mesh.positions, - Some(&mesh.normals), - Some(&mesh.indices), - ray_cast::Backfaces::Cull, - )); - }); - }); + /// Returns the benchmark group name. + fn name(&self) -> &'static str { + match *self { + Self::CullHit => bench!("cull_intersect"), + Self::NoCullHit => bench!("no_cull_intersect"), + Self::CullMiss => bench!("cull_no_intersect"), + } + } + + fn ray(&self) -> Ray3d { + Ray3d::new( + Vec3::new(0.0, 1.0, 0.0), + match *self { + Self::CullHit | Self::NoCullHit => Dir3::NEG_Y, + // `NoIntersection` should not hit the mesh, so it goes an orthogonal direction. + Self::CullMiss => Dir3::X, + }, + ) + } + + fn mesh_to_world(&self) -> Mat4 { + Mat4::IDENTITY + } + + fn backface_culling(&self) -> Backfaces { + match *self { + Self::CullHit | Self::CullMiss => Backfaces::Cull, + Self::NoCullHit => Backfaces::Include, + } + } + + /// Returns whether the ray should intersect with the mesh. + #[cfg(test)] + fn should_intersect(&self) -> bool { + match *self { + Self::CullHit | Self::NoCullHit => true, + Self::CullMiss => false, + } } } -criterion_group!( - benches, - ray_mesh_intersection, - ray_mesh_intersection_no_cull, - ray_mesh_intersection_no_intersection -); +/// A benchmark that times [`ray_cast::ray_mesh_intersection()`]. +/// +/// There are multiple different scenarios that are tracked, which are described by the +/// [`Benchmarks`] enum. Each scenario has its own benchmark group, where individual benchmarks +/// track a ray intersecting a square mesh of an increasing amount of vertices. +fn bench(c: &mut Criterion) { + for benchmark in Benchmarks::iter() { + let mut group = c.benchmark_group(benchmark.name()); + + group + .warm_up_time(Benchmarks::WARM_UP_TIME) + // Make the scale logarithmic, to match `VERTICES_PER_SIDE`. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + for vertices_per_side in Benchmarks::VERTICES_PER_SIDE { + group.bench_with_input( + BenchmarkId::from_parameter(format!("{}_vertices", vertices_per_side.pow(2))), + &vertices_per_side, + |b, &vertices_per_side| { + let ray = black_box(benchmark.ray()); + let mesh_to_world = black_box(benchmark.mesh_to_world()); + let mesh = black_box(create_mesh(vertices_per_side)); + let backface_culling = black_box(benchmark.backface_culling()); + + b.iter(|| { + let intersected = ray_cast::ray_mesh_intersection( + ray, + &mesh_to_world, + &mesh.positions, + Some(&mesh.normals), + Some(&mesh.indices), + backface_culling, + ); + + #[cfg(test)] + assert_eq!(intersected.is_some(), benchmark.should_intersect()); + + intersected + }); + }, + ); + } + } +} diff --git a/benches/benches/bevy_reflect/function.rs b/benches/benches/bevy_reflect/function.rs index 5398cc3da91e2..0ee1126b8ed9f 100644 --- a/benches/benches/bevy_reflect/function.rs +++ b/benches/benches/bevy_reflect/function.rs @@ -1,14 +1,25 @@ +use core::hint::black_box; + +use benches::bench; use bevy_reflect::func::{ArgList, IntoFunction, IntoFunctionMut, TypedFunction}; -use criterion::{criterion_group, BatchSize, Criterion}; +use criterion::{criterion_group, BatchSize, BenchmarkId, Criterion}; -criterion_group!(benches, typed, into, call, overload, clone); +criterion_group!( + benches, + typed, + into, + call, + clone, + with_overload, + call_overload, +); fn add(a: i32, b: i32) -> i32 { a + b } fn typed(c: &mut Criterion) { - c.benchmark_group("typed") + c.benchmark_group(bench!("typed")) .bench_function("function", |b| { b.iter(|| add.get_function_info()); }) @@ -25,7 +36,7 @@ fn typed(c: &mut Criterion) { } fn into(c: &mut Criterion) { - c.benchmark_group("into") + c.benchmark_group(bench!("into")) .bench_function("function", |b| { b.iter(|| add.into_function()); }) @@ -36,17 +47,18 @@ fn into(c: &mut Criterion) { }) .bench_function("closure_mut", |b| { let mut _capture = 25; + // `move` is required here because `into_function_mut()` takes ownership of `self`. let closure = move |a: i32| _capture += a; b.iter(|| closure.into_function_mut()); }); } fn call(c: &mut Criterion) { - c.benchmark_group("call") + c.benchmark_group(bench!("call")) .bench_function("trait_object", |b| { b.iter_batched( || Box::new(add) as Box i32>, - |func| func(75, 25), + |func| func(black_box(75), black_box(25)), BatchSize::SmallInput, ); }) @@ -78,35 +90,42 @@ fn call(c: &mut Criterion) { }); } -fn overload(c: &mut Criterion) { - fn add>(a: T, b: T) -> T { - a + b - } +fn clone(c: &mut Criterion) { + c.benchmark_group(bench!("clone")) + .bench_function("function", |b| { + let add = add.into_function(); + b.iter(|| add.clone()); + }); +} + +fn simple>(a: T, b: T) -> T { + a + b +} - #[expect(clippy::too_many_arguments)] - fn complex( - _: T0, - _: T1, - _: T2, - _: T3, - _: T4, - _: T5, - _: T6, - _: T7, - _: T8, - _: T9, - ) { - } +fn complex( + _: T0, + _: T1, + _: T2, + _: T3, + _: T4, + _: T5, + _: T6, + _: T7, + _: T8, + _: T9, +) { +} - c.benchmark_group("with_overload") - .bench_function("01_simple_overload", |b| { +fn with_overload(c: &mut Criterion) { + c.benchmark_group(bench!("with_overload")) + .bench_function(BenchmarkId::new("simple_overload", 1), |b| { b.iter_batched( - || add::.into_function(), - |func| func.with_overload(add::), + || simple::.into_function(), + |func| func.with_overload(simple::), BatchSize::SmallInput, ); }) - .bench_function("01_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 1), |b| { b.iter_batched( || complex::.into_function(), |func| { @@ -115,18 +134,18 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("03_simple_overload", |b| { + .bench_function(BenchmarkId::new("simple_overload", 3), |b| { b.iter_batched( - || add::.into_function(), + || simple::.into_function(), |func| { - func.with_overload(add::) - .with_overload(add::) - .with_overload(add::) + func.with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) }, BatchSize::SmallInput, ); }) - .bench_function("03_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 3), |b| { b.iter_batched( || complex::.into_function(), |func| { @@ -137,24 +156,24 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("10_simple_overload", |b| { + .bench_function(BenchmarkId::new("simple_overload", 10), |b| { b.iter_batched( - || add::.into_function(), + || simple::.into_function(), |func| { - func.with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) + func.with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) }, BatchSize::SmallInput, ); }) - .bench_function("10_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 10), |b| { b.iter_batched( || complex::.into_function(), |func| { @@ -171,41 +190,41 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("01_nested_simple_overload", |b| { + .bench_function(BenchmarkId::new("nested_simple_overload", 1), |b| { b.iter_batched( - || add::.into_function(), - |func| func.with_overload(add::), + || simple::.into_function(), + |func| func.with_overload(simple::), BatchSize::SmallInput, ); }) - .bench_function("03_nested_simple_overload", |b| { + .bench_function(BenchmarkId::new("nested_simple_overload", 3), |b| { b.iter_batched( - || add::.into_function(), + || simple::.into_function(), |func| { func.with_overload( - add:: - .into_function() - .with_overload(add::.into_function().with_overload(add::)), + simple::.into_function().with_overload( + simple::.into_function().with_overload(simple::), + ), ) }, BatchSize::SmallInput, ); }) - .bench_function("10_nested_simple_overload", |b| { + .bench_function(BenchmarkId::new("nested_simple_overload", 10), |b| { b.iter_batched( - || add::.into_function(), + || simple::.into_function(), |func| { func.with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add::.into_function().with_overload( - add:: + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple::.into_function().with_overload( + simple:: .into_function() - .with_overload(add::), + .with_overload(simple::), ), ), ), @@ -218,13 +237,15 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }); +} - c.benchmark_group("call_overload") - .bench_function("01_simple_overload", |b| { +fn call_overload(c: &mut Criterion) { + c.benchmark_group(bench!("call_overload")) + .bench_function(BenchmarkId::new("simple_overload", 1), |b| { b.iter_batched( || { ( - add::.into_function().with_overload(add::), + simple::.into_function().with_overload(simple::), ArgList::new().push_owned(75_i8).push_owned(25_i8), ) }, @@ -232,7 +253,7 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("01_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 1), |b| { b.iter_batched( || { ( @@ -258,15 +279,15 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("03_simple_overload", |b| { + .bench_function(BenchmarkId::new("simple_overload", 3), |b| { b.iter_batched( || { ( - add:: + simple:: .into_function() - .with_overload(add::) - .with_overload(add::) - .with_overload(add::), + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::), ArgList::new().push_owned(75_i32).push_owned(25_i32), ) }, @@ -274,7 +295,7 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("03_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 3), |b| { b.iter_batched( || { ( @@ -306,21 +327,21 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("10_simple_overload", |b| { + .bench_function(BenchmarkId::new("simple_overload", 10), |b| { b.iter_batched( || { ( - add:: + simple:: .into_function() - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::) - .with_overload(add::), + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::) + .with_overload(simple::), ArgList::new().push_owned(75_u8).push_owned(25_u8), ) }, @@ -328,7 +349,7 @@ fn overload(c: &mut Criterion) { BatchSize::SmallInput, ); }) - .bench_function("10_complex_overload", |b| { + .bench_function(BenchmarkId::new("complex_overload", 10), |b| { b.iter_batched( || { ( @@ -379,10 +400,3 @@ fn overload(c: &mut Criterion) { ); }); } - -fn clone(c: &mut Criterion) { - c.benchmark_group("clone").bench_function("function", |b| { - let add = add.into_function(); - b.iter(|| add.clone()); - }); -} diff --git a/benches/benches/bevy_reflect/list.rs b/benches/benches/bevy_reflect/list.rs index d9c92dd03ef06..872c2dd0cb898 100644 --- a/benches/benches/bevy_reflect/list.rs +++ b/benches/benches/bevy_reflect/list.rs @@ -1,9 +1,10 @@ -use core::{iter, time::Duration}; +use core::{hint::black_box, iter, time::Duration}; +use benches::bench; use bevy_reflect::{DynamicList, List}; use criterion::{ - black_box, criterion_group, measurement::Measurement, BatchSize, BenchmarkGroup, BenchmarkId, - Criterion, Throughput, + criterion_group, measurement::Measurement, AxisScale, BatchSize, BenchmarkGroup, BenchmarkId, + Criterion, PlotConfiguration, Throughput, }; criterion_group!( @@ -14,11 +15,29 @@ criterion_group!( dynamic_list_push ); +// Use a shorter warm-up time (from 3 to 0.5 seconds) and measurement time (from 5 to 4) because we +// have so many combinations (>50) to benchmark. const WARM_UP_TIME: Duration = Duration::from_millis(500); const MEASUREMENT_TIME: Duration = Duration::from_secs(4); -// log10 scaling -const SIZES: [usize; 5] = [100_usize, 316, 1000, 3162, 10000]; +/// An array of list sizes used in benchmarks. +/// +/// This scales logarithmically. +const SIZES: [usize; 5] = [100, 316, 1000, 3162, 10000]; + +/// Creates a [`BenchmarkGroup`] with common configuration shared by all benchmarks within this +/// module. +fn create_group<'a, M: Measurement>(c: &'a mut Criterion, name: &str) -> BenchmarkGroup<'a, M> { + let mut group = c.benchmark_group(name); + + group + .warm_up_time(WARM_UP_TIME) + .measurement_time(MEASUREMENT_TIME) + // Make the plots logarithmic, matching `SIZES`' scale. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + group +} fn list_apply( group: &mut BenchmarkGroup, @@ -53,9 +72,7 @@ fn list_apply( } fn concrete_list_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_list_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_list_apply")); let empty_base = |_: usize| Vec::::new; let full_base = |size: usize| move || iter::repeat(0).take(size).collect::>(); @@ -77,9 +94,7 @@ fn concrete_list_apply(criterion: &mut Criterion) { } fn concrete_list_clone_dynamic(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_list_clone_dynamic"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_list_clone_dynamic")); for size in SIZES { group.throughput(Throughput::Elements(size as u64)); @@ -99,9 +114,7 @@ fn concrete_list_clone_dynamic(criterion: &mut Criterion) { } fn dynamic_list_push(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_list_push"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_list_push")); for size in SIZES { group.throughput(Throughput::Elements(size as u64)); @@ -130,9 +143,7 @@ fn dynamic_list_push(criterion: &mut Criterion) { } fn dynamic_list_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_list_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_list_apply")); let empty_base = |_: usize| || Vec::::new().clone_dynamic(); let full_base = |size: usize| move || iter::repeat(0).take(size).collect::>(); diff --git a/benches/benches/bevy_reflect/main.rs b/benches/benches/bevy_reflect/main.rs index d347baccd0fa3..3785652295318 100644 --- a/benches/benches/bevy_reflect/main.rs +++ b/benches/benches/bevy_reflect/main.rs @@ -1,5 +1,3 @@ -#![expect(clippy::type_complexity)] - use criterion::criterion_main; mod function; diff --git a/benches/benches/bevy_reflect/map.rs b/benches/benches/bevy_reflect/map.rs index 054dcf9570da0..d1d9d836039fc 100644 --- a/benches/benches/bevy_reflect/map.rs +++ b/benches/benches/bevy_reflect/map.rs @@ -1,10 +1,11 @@ -use core::{fmt::Write, iter, time::Duration}; +use core::{fmt::Write, hint::black_box, iter, time::Duration}; +use benches::bench; use bevy_reflect::{DynamicMap, Map}; use bevy_utils::HashMap; use criterion::{ - black_box, criterion_group, measurement::Measurement, BatchSize, BenchmarkGroup, BenchmarkId, - Criterion, Throughput, + criterion_group, measurement::Measurement, AxisScale, BatchSize, BenchmarkGroup, BenchmarkId, + Criterion, PlotConfiguration, Throughput, }; criterion_group!( @@ -15,10 +16,30 @@ criterion_group!( dynamic_map_insert ); +// Use a shorter warm-up time (from 3 to 0.5 seconds) and measurement time (from 5 to 4) because we +// have so many combinations (>50) to benchmark. const WARM_UP_TIME: Duration = Duration::from_millis(500); const MEASUREMENT_TIME: Duration = Duration::from_secs(4); + +/// An array of list sizes used in benchmarks. +/// +/// This scales logarithmically. const SIZES: [usize; 5] = [100, 316, 1000, 3162, 10000]; +/// Creates a [`BenchmarkGroup`] with common configuration shared by all benchmarks within this +/// module. +fn create_group<'a, M: Measurement>(c: &'a mut Criterion, name: &str) -> BenchmarkGroup<'a, M> { + let mut group = c.benchmark_group(name); + + group + .warm_up_time(WARM_UP_TIME) + .measurement_time(MEASUREMENT_TIME) + // Make the plots logarithmic, matching `SIZES`' scale. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + group +} + /// Generic benchmark for applying one `Map` to another. /// /// `f_base` is a function which takes an input size and produces a generator @@ -55,9 +76,7 @@ fn map_apply( } fn concrete_map_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_map_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_map_apply")); let empty_base = |_: usize| HashMap::::default; @@ -131,9 +150,7 @@ fn u64_to_n_byte_key(k: u64, n: usize) -> String { } fn dynamic_map_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_map_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_map_apply")); let empty_base = |_: usize| DynamicMap::default; @@ -199,9 +216,7 @@ fn dynamic_map_apply(criterion: &mut Criterion) { } fn dynamic_map_get(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_map_get"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_map_get")); for size in SIZES { group.throughput(Throughput::Elements(size as u64)); @@ -217,7 +232,7 @@ fn dynamic_map_get(criterion: &mut Criterion) { bencher.iter(|| { for i in 0..size as u64 { let key = black_box(i); - black_box(assert!(map.get(&key).is_some())); + black_box(map.get(&key)); } }); }, @@ -250,9 +265,7 @@ fn dynamic_map_get(criterion: &mut Criterion) { } fn dynamic_map_insert(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_map_insert"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_map_insert")); for size in SIZES { group.throughput(Throughput::Elements(size as u64)); diff --git a/benches/benches/bevy_reflect/path.rs b/benches/benches/bevy_reflect/path.rs index 2cca245239e89..c0d8bfe0da732 100644 --- a/benches/benches/bevy_reflect/path.rs +++ b/benches/benches/bevy_reflect/path.rs @@ -1,7 +1,8 @@ -use core::{fmt::Write, str, time::Duration}; +use core::{fmt::Write, hint::black_box, str, time::Duration}; +use benches::bench; use bevy_reflect::ParsedPath; -use criterion::{black_box, criterion_group, BatchSize, BenchmarkId, Criterion, Throughput}; +use criterion::{criterion_group, BatchSize, BenchmarkId, Criterion, Throughput}; use rand::{distributions::Uniform, Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -11,7 +12,7 @@ const WARM_UP_TIME: Duration = Duration::from_millis(500); const MEASUREMENT_TIME: Duration = Duration::from_secs(2); const SAMPLE_SIZE: usize = 500; const NOISE_THRESHOLD: f64 = 0.03; -const SIZES: [usize; 6] = [100, 3160, 1000, 3_162, 10_000, 24_000]; +const SIZES: [usize; 6] = [100, 316, 1_000, 3_162, 10_000, 24_000]; fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) @@ -66,23 +67,32 @@ fn mk_paths(size: usize) -> impl FnMut() -> String { } fn parse_reflect_path(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("parse_reflect_path"); + let mut group = criterion.benchmark_group(bench!("parse_reflect_path")); + group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); group.sample_size(SAMPLE_SIZE); group.noise_threshold(NOISE_THRESHOLD); - let group = &mut group; for size in SIZES { group.throughput(Throughput::Elements(size as u64)); + group.bench_with_input( - BenchmarkId::new("parse_reflect_path", size), + BenchmarkId::from_parameter(size), &size, |bencher, &size| { let mk_paths = mk_paths(size); bencher.iter_batched( mk_paths, - |path| assert!(ParsedPath::parse(black_box(&path)).is_ok()), + |path| { + let parsed_path = black_box(ParsedPath::parse(black_box(&path))); + + // When `cargo test --benches` is run, each benchmark is run once. This + // verifies that we are benchmarking a successful parse without it + // affecting the recorded time. + #[cfg(test)] + assert!(parsed_path.is_ok()); + }, BatchSize::SmallInput, ); }, diff --git a/benches/benches/bevy_reflect/struct.rs b/benches/benches/bevy_reflect/struct.rs index dfd324e7053e6..d8f25554c36ba 100644 --- a/benches/benches/bevy_reflect/struct.rs +++ b/benches/benches/bevy_reflect/struct.rs @@ -1,7 +1,11 @@ -use core::time::Duration; +use core::{hint::black_box, time::Duration}; +use benches::bench; use bevy_reflect::{DynamicStruct, GetField, PartialReflect, Reflect, Struct}; -use criterion::{black_box, criterion_group, BatchSize, BenchmarkId, Criterion, Throughput}; +use criterion::{ + criterion_group, measurement::Measurement, AxisScale, BatchSize, BenchmarkGroup, BenchmarkId, + Criterion, PlotConfiguration, Throughput, +}; criterion_group!( benches, @@ -19,10 +23,22 @@ const WARM_UP_TIME: Duration = Duration::from_millis(500); const MEASUREMENT_TIME: Duration = Duration::from_secs(4); const SIZES: [usize; 4] = [16, 32, 64, 128]; +/// Creates a [`BenchmarkGroup`] with common configuration shared by all benchmarks within this +/// module. +fn create_group<'a, M: Measurement>(c: &'a mut Criterion, name: &str) -> BenchmarkGroup<'a, M> { + let mut group = c.benchmark_group(name); + + group + .warm_up_time(WARM_UP_TIME) + .measurement_time(MEASUREMENT_TIME) + // Make the plots logarithmic, matching `SIZES`' scale. + .plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); + + group +} + fn concrete_struct_field(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_struct_field"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_struct_field")); let structs: [Box; 4] = [ Box::new(Struct16::default()), @@ -44,7 +60,7 @@ fn concrete_struct_field(criterion: &mut Criterion) { bencher.iter(|| { for name in &field_names { - s.field(black_box(name)); + black_box(s.field(black_box(name))); } }); }, @@ -53,9 +69,7 @@ fn concrete_struct_field(criterion: &mut Criterion) { } fn concrete_struct_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_struct_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_struct_apply")); // Use functions that produce trait objects of varying concrete types as the // input to the benchmark. @@ -111,9 +125,7 @@ fn concrete_struct_apply(criterion: &mut Criterion) { } fn concrete_struct_type_info(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_struct_type_info"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_struct_type_info")); let structs: [(Box, Box); 5] = [ ( @@ -145,23 +157,21 @@ fn concrete_struct_type_info(criterion: &mut Criterion) { BenchmarkId::new("NonGeneric", field_count), &standard, |bencher, s| { - bencher.iter(|| black_box(s.get_represented_type_info())); + bencher.iter(|| s.get_represented_type_info()); }, ); group.bench_with_input( BenchmarkId::new("Generic", field_count), &generic, |bencher, s| { - bencher.iter(|| black_box(s.get_represented_type_info())); + bencher.iter(|| s.get_represented_type_info()); }, ); } } fn concrete_struct_clone(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("concrete_struct_clone"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("concrete_struct_clone")); let structs: [(Box, Box); 5] = [ ( @@ -193,23 +203,21 @@ fn concrete_struct_clone(criterion: &mut Criterion) { BenchmarkId::new("NonGeneric", field_count), &standard, |bencher, s| { - bencher.iter(|| black_box(s.clone_dynamic())); + bencher.iter(|| s.clone_dynamic()); }, ); group.bench_with_input( BenchmarkId::new("Generic", field_count), &generic, |bencher, s| { - bencher.iter(|| black_box(s.clone_dynamic())); + bencher.iter(|| s.clone_dynamic()); }, ); } } fn dynamic_struct_clone(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_struct_clone"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_struct_clone")); let structs: [Box; 5] = [ Box::new(Struct1::default().clone_dynamic()), @@ -226,16 +234,14 @@ fn dynamic_struct_clone(criterion: &mut Criterion) { BenchmarkId::from_parameter(field_count), &s, |bencher, s| { - bencher.iter(|| black_box(s.clone_dynamic())); + bencher.iter(|| s.clone_dynamic()); }, ); } } fn dynamic_struct_apply(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_struct_apply"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_struct_apply")); let patches: &[(fn() -> Box, usize)] = &[ (|| Box::new(Struct16::default()), 16), @@ -293,9 +299,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) { } fn dynamic_struct_insert(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_struct_insert"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_struct_insert")); for field_count in SIZES { group.throughput(Throughput::Elements(field_count as u64)); @@ -325,9 +329,7 @@ fn dynamic_struct_insert(criterion: &mut Criterion) { } fn dynamic_struct_get_field(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("dynamic_struct_get"); - group.warm_up_time(WARM_UP_TIME); - group.measurement_time(MEASUREMENT_TIME); + let mut group = create_group(criterion, bench!("dynamic_struct_get_field")); for field_count in SIZES { group.throughput(Throughput::Elements(field_count as u64)); @@ -342,9 +344,7 @@ fn dynamic_struct_get_field(criterion: &mut Criterion) { } let field = black_box("field_63"); - bencher.iter(|| { - black_box(s.get_field::<()>(field)); - }); + bencher.iter(|| s.get_field::<()>(field)); }, ); } diff --git a/benches/benches/bevy_render/render_layers.rs b/benches/benches/bevy_render/render_layers.rs index 42dd5356b55ed..d460a7bc96c48 100644 --- a/benches/benches/bevy_render/render_layers.rs +++ b/benches/benches/bevy_render/render_layers.rs @@ -1,4 +1,6 @@ -use criterion::{black_box, criterion_group, Criterion}; +use core::hint::black_box; + +use criterion::{criterion_group, Criterion}; use bevy_render::view::RenderLayers; diff --git a/benches/benches/bevy_render/torus.rs b/benches/benches/bevy_render/torus.rs index a5ef753bc8ccb..dcadd09180f9d 100644 --- a/benches/benches/bevy_render/torus.rs +++ b/benches/benches/bevy_render/torus.rs @@ -1,4 +1,6 @@ -use criterion::{black_box, criterion_group, Criterion}; +use core::hint::black_box; + +use criterion::{criterion_group, Criterion}; use bevy_render::mesh::TorusMeshBuilder; diff --git a/benches/benches/bevy_tasks/iter.rs b/benches/benches/bevy_tasks/iter.rs index 4f8f75c8ed0e8..7fe00ecb794db 100644 --- a/benches/benches/bevy_tasks/iter.rs +++ b/benches/benches/bevy_tasks/iter.rs @@ -1,5 +1,7 @@ +use core::hint::black_box; + use bevy_tasks::{ParallelIterator, TaskPoolBuilder}; -use criterion::{black_box, criterion_group, BenchmarkId, Criterion}; +use criterion::{criterion_group, BenchmarkId, Criterion}; struct ParChunks<'a, T>(core::slice::Chunks<'a, T>); impl<'a, T> ParallelIterator> for ParChunks<'a, T> @@ -61,7 +63,7 @@ fn bench_for_each(c: &mut Criterion) { b.iter(|| { v.iter_mut().for_each(|x| { busy_work(10000); - *x *= *x; + *x = x.wrapping_mul(*x); }); }); }); @@ -77,7 +79,7 @@ fn bench_for_each(c: &mut Criterion) { b.iter(|| { ParChunksMut(v.chunks_mut(100)).for_each(&pool, |x| { busy_work(10000); - *x *= *x; + *x = x.wrapping_mul(*x); }); }); }, diff --git a/benches/src/lib.rs b/benches/src/lib.rs new file mode 100644 index 0000000000000..699ab13e86461 --- /dev/null +++ b/benches/src/lib.rs @@ -0,0 +1,44 @@ +/// Automatically generates the qualified name of a benchmark given its function name and module +/// path. +/// +/// This macro takes a single string literal as input and returns a [`&'static str`](str). Its +/// result is determined at compile-time. If you need to create variations of a benchmark name +/// based on its input, use this in combination with [`BenchmarkId`](criterion::BenchmarkId). +/// +/// # When to use this +/// +/// Use this macro to name benchmarks that are not within a group and benchmark groups themselves. +/// You'll most commonly use this macro with: +/// +/// - [`Criterion::bench_function()`](criterion::Criterion::bench_function) +/// - [`Criterion::bench_with_input()`](criterion::Criterion::bench_with_input) +/// - [`Criterion::benchmark_group()`](criterion::Criterion::benchmark_group) +/// +/// You do not want to use this macro with +/// [`BenchmarkGroup::bench_function()`](criterion::BenchmarkGroup::bench_function) or +/// [`BenchmarkGroup::bench_with_input()`](criterion::BenchmarkGroup::bench_with_input), because +/// the group they are in already has the qualified path in it. +/// +/// # Example +/// +/// ``` +/// mod ecs { +/// mod query { +/// use criterion::Criterion; +/// use benches::bench; +/// +/// fn iter(c: &mut Criterion) { +/// // Benchmark name ends in `ecs::query::iter`. +/// c.bench_function(bench!("iter"), |b| { +/// // ... +/// }); +/// } +/// } +/// } +/// ``` +#[macro_export] +macro_rules! bench { + ($name:literal) => { + concat!(module_path!(), "::", $name) + }; +} diff --git a/clippy.toml b/clippy.toml index d1d234817a913..26b39b4e841e8 100644 --- a/clippy.toml +++ b/clippy.toml @@ -41,4 +41,5 @@ 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" }, ] diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml index 73464d568ed4b..601f93ddbbf96 100644 --- a/crates/bevy_a11y/Cargo.toml +++ b/crates/bevy_a11y/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_a11y" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides accessibility support for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,11 +10,11 @@ keywords = ["bevy", "accessibility", "a11y"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.15.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_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } accesskit = "0.17" diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index b3312aa0ce2d7..8dfa704b8d0f5 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_animation" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides animation functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,35 +10,36 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", "petgraph", ] } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } +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_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" } # other petgraph = { version = "0.6", features = ["serde-1"] } ron = "0.8" serde = "1" blake3 = { version = "1.0" } -downcast-rs = "1.2.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"] } either = "1.13" thread_local = "1" uuid = { version = "1.7", features = ["v4"] } smallvec = "1" +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 6af653d3077b4..a345c5fce4f0a 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -18,7 +18,7 @@ pub struct BlendInput { /// An animatable value type. pub trait Animatable: Reflect + Sized + Send + Sync + 'static { - /// Interpolates between `a` and `b` with a interpolation factor of `time`. + /// Interpolates between `a` and `b` with an interpolation factor of `time`. /// /// The `time` parameter here may not be clamped to the range `[0.0, 1.0]`. fn interpolate(a: &Self, b: &Self, time: f32) -> Self; @@ -125,9 +125,8 @@ impl Animatable for bool { #[inline] fn blend(inputs: impl Iterator>) -> Self { inputs - .max_by(|a, b| FloatOrd(a.weight).cmp(&FloatOrd(b.weight))) - .map(|input| input.value) - .unwrap_or(false) + .max_by_key(|x| FloatOrd(x.weight)) + .is_some_and(|input| input.value) } } diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 28069c1af4928..f31f54f8da778 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -117,22 +117,27 @@ use downcast_rs::{impl_downcast, Downcast}; /// # use bevy_animation::{prelude::AnimatableProperty, AnimationEntityMut, AnimationEvaluationError, animation_curves::EvaluatorId}; /// # use bevy_reflect::Reflect; /// # use std::any::TypeId; -/// # use bevy_render::camera::PerspectiveProjection; +/// # use bevy_render::camera::{Projection, PerspectiveProjection}; /// #[derive(Reflect)] /// struct FieldOfViewProperty; /// /// impl AnimatableProperty for FieldOfViewProperty { /// type Property = f32; /// fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> { -/// let component = entity -/// .get_mut::() -/// .ok_or( -/// AnimationEvaluationError::ComponentNotPresent( -/// TypeId::of::() -/// ) -/// )? +/// let component = entity +/// .get_mut::() +/// .ok_or(AnimationEvaluationError::ComponentNotPresent(TypeId::of::< +/// Projection, +/// >( +/// )))? /// .into_inner(); -/// Ok(&mut component.fov) +/// match component { +/// Projection::Perspective(perspective) => Ok(&mut perspective.fov), +/// _ => Err(AnimationEvaluationError::PropertyNotPresent(TypeId::of::< +/// PerspectiveProjection, +/// >( +/// ))), +/// } /// } /// /// fn evaluator_id(&self) -> EvaluatorId { @@ -146,7 +151,7 @@ use downcast_rs::{impl_downcast, Downcast}; /// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; /// # use bevy_ecs::name::Name; /// # use bevy_reflect::Reflect; -/// # use bevy_render::camera::PerspectiveProjection; +/// # use bevy_render::camera::{Projection, PerspectiveProjection}; /// # use std::any::TypeId; /// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); /// # #[derive(Reflect, Clone)] @@ -154,15 +159,20 @@ use downcast_rs::{impl_downcast, Downcast}; /// # impl AnimatableProperty for FieldOfViewProperty { /// # type Property = f32; /// # fn get_mut<'a>(&self, entity: &'a mut AnimationEntityMut) -> Result<&'a mut Self::Property, AnimationEvaluationError> { -/// # let component = entity -/// # .get_mut::() -/// # .ok_or( -/// # AnimationEvaluationError::ComponentNotPresent( -/// # TypeId::of::() -/// # ) -/// # )? +/// # let component = entity +/// # .get_mut::() +/// # .ok_or(AnimationEvaluationError::ComponentNotPresent(TypeId::of::< +/// # Projection, +/// # >( +/// # )))? /// # .into_inner(); -/// # Ok(&mut component.fov) +/// # match component { +/// # Projection::Perspective(perspective) => Ok(&mut perspective.fov), +/// # _ => Err(AnimationEvaluationError::PropertyNotPresent(TypeId::of::< +/// # PerspectiveProjection, +/// # >( +/// # ))), +/// # } /// # } /// # fn evaluator_id(&self) -> EvaluatorId { /// # EvaluatorId::Type(TypeId::of::()) @@ -974,6 +984,7 @@ where /// /// ``` /// # use bevy_animation::{animation_curves::AnimatedField, animated_field}; +/// # use bevy_color::Srgba; /// # use bevy_ecs::component::Component; /// # use bevy_math::Vec3; /// # use bevy_reflect::Reflect; @@ -983,10 +994,15 @@ where /// } /// /// let field = animated_field!(Transform::translation); +/// +/// #[derive(Component, Reflect)] +/// struct Color(Srgba); +/// +/// let tuple_field = animated_field!(Color::0); /// ``` #[macro_export] macro_rules! animated_field { - ($component:ident::$field:ident) => { + ($component:ident::$field:tt) => { AnimatedField::new_unchecked(stringify!($field), |component: &mut $component| { &mut component.$field }) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 04033447912d0..f50fddd711918 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -43,13 +43,11 @@ use bevy_math::FloatOrd; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use bevy_time::Time; use bevy_transform::TransformSystem; -use bevy_utils::{ - tracing::{trace, warn}, - HashMap, NoOpHash, PreHashMap, PreHashMapExt, TypeIdMap, -}; +use bevy_utils::{HashMap, NoOpHash, PreHashMap, PreHashMapExt, TypeIdMap}; use petgraph::graph::NodeIndex; use serde::{Deserialize, Serialize}; use thread_local::ThreadLocal; +use tracing::{trace, warn}; use uuid::Uuid; /// The animation prelude. @@ -466,7 +464,7 @@ pub enum AnimationEvaluationError { /// An animation that an [`AnimationPlayer`] is currently either playing or was /// playing, but is presently paused. /// -/// An stopped animation is considered no longer active. +/// A stopped animation is considered no longer active. #[derive(Debug, Clone, Copy, Reflect)] pub struct ActiveAnimation { /// The factor by which the weight from the [`AnimationGraph`] is multiplied. @@ -938,13 +936,6 @@ impl AnimationPlayer { pub fn animation_mut(&mut self, animation: AnimationNodeIndex) -> Option<&mut ActiveAnimation> { self.active_animations.get_mut(&animation) } - - #[deprecated = "Use `is_playing_animation` instead"] - /// Returns true if the animation is currently playing or paused, or false - /// if the animation is stopped. - pub fn animation_is_playing(&self, animation: AnimationNodeIndex) -> bool { - self.active_animations.contains_key(&animation) - } } /// A system that triggers untargeted animation events for the currently-playing animations. @@ -1057,8 +1048,8 @@ pub fn animate_targets( (player, graph_handle.id()) } else { trace!( - "Either an animation player {:?} or a graph was missing for the target \ - entity {:?} ({:?}); no animations will play this frame", + "Either an animation player {} or a graph was missing for the target \ + entity {} ({:?}); no animations will play this frame", player_id, entity_mut.id(), entity_mut.get::(), @@ -1262,7 +1253,7 @@ impl Plugin for AnimationPlugin { // `PostUpdate`. For now, we just disable ambiguity testing // for this system. animate_targets - .after(bevy_render::mesh::inherit_weights) + .before(bevy_render::mesh::inherit_weights) .ambiguous_with_all(), trigger_untargeted_animation_events, expire_completed_transitions, diff --git a/crates/bevy_animation/src/transition.rs b/crates/bevy_animation/src/transition.rs index 679c63bec3ffb..c94378208b3a7 100644 --- a/crates/bevy_animation/src/transition.rs +++ b/crates/bevy_animation/src/transition.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_time::Time; -use bevy_utils::Duration; +use core::time::Duration; use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer}; diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 416e8cf16553f..09b92d5cbc9ac 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_app" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides core App functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["std", "bevy_reflect", "bevy_tasks", "bevy_ecs/default", "downcast"] +default = ["std", "bevy_reflect", "bevy_tasks", "bevy_ecs/default"] # Functionality @@ -26,9 +26,6 @@ reflect_functions = [ ## Adds support for running async background tasks bevy_tasks = ["dep:bevy_tasks"] -## Adds `downcast-rs` integration for `Plugin` -downcast = ["dep:downcast-rs"] - # Debugging Features ## Enables `tracing` integration, allowing spans and other metrics to be reported @@ -48,7 +45,7 @@ std = [ "bevy_reflect?/std", "bevy_ecs/std", "dep:ctrlc", - "downcast-rs?/std", + "downcast-rs/std", "bevy_utils/std", "bevy_tasks?/std", ] @@ -72,16 +69,16 @@ portable-atomic = [ [dependencies] # bevy -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false, optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false, features = [ +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.15.0-dev", default-features = false, optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, optional = true } # other -downcast-rs = { version = "1.2.0", default-features = false, optional = true } +downcast-rs = { version = "2", default-features = false } thiserror = { version = "2", default-features = false } variadics_please = "1.1" tracing = { version = "0.1", default-features = false, optional = true } @@ -93,7 +90,7 @@ portable-atomic-util = { version = "0.2.4", features = [ "alloc", ], optional = true } -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +[target.'cfg(any(unix, 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 a755b978ed081..6ff5b9993dab1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -5,6 +5,7 @@ use crate::{ use alloc::{ boxed::Box, string::{String, ToString}, + vec::Vec, }; pub use bevy_derive::AppLabel; use bevy_ecs::{ @@ -29,9 +30,6 @@ use std::{ process::{ExitCode, Termination}, }; -#[cfg(feature = "downcast")] -use alloc::vec::Vec; - bevy_ecs::define_label!( /// A strongly-typed class of labels used to identify an [`App`]. AppLabel, @@ -522,7 +520,6 @@ impl App { /// # app.add_plugins(ImagePlugin::default()); /// let default_sampler = app.get_added_plugins::()[0].default_sampler; /// ``` - #[cfg(feature = "downcast")] pub fn get_added_plugins(&self) -> Vec<&T> where T: Plugin, @@ -1058,6 +1055,16 @@ impl App { &mut self.sub_apps.main } + /// Returns a reference to the [`SubApps`] collection. + pub fn sub_apps(&self) -> &SubApps { + &self.sub_apps + } + + /// Returns a mutable reference to the [`SubApps`] collection. + pub fn sub_apps_mut(&mut self) -> &mut SubApps { + &mut self.sub_apps + } + /// Returns a reference to the [`SubApp`] with the given label. /// /// # Panics @@ -1350,7 +1357,7 @@ pub enum AppExit { } impl AppExit { - /// Creates a [`AppExit::Error`] with a error code of 1. + /// Creates a [`AppExit::Error`] with an error code of 1. #[must_use] pub const fn error() -> Self { Self::Error(NonZero::::MIN) @@ -1726,7 +1733,7 @@ mod tests { #[test] fn app_exit_size() { - // There wont be many of them so the size isn't a issue but + // There wont be many of them so the size isn't an issue but // it's nice they're so small let's keep it that way. assert_eq!(size_of::(), size_of::()); } diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index ff680e4f04ac6..13021924d033e 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -11,10 +11,13 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] //! This crate is about everything concerning the highest-level, application layer of a Bevy app. +#[cfg(feature = "std")] +extern crate std; + extern crate alloc; mod app; @@ -26,7 +29,7 @@ mod schedule_runner; mod sub_app; #[cfg(feature = "bevy_tasks")] mod task_pool_plugin; -#[cfg(all(not(target_arch = "wasm32"), feature = "std"))] +#[cfg(all(any(unix, windows), feature = "std"))] mod terminal_ctrl_c_handler; pub use app::*; @@ -38,7 +41,7 @@ pub use schedule_runner::*; pub use sub_app::*; #[cfg(feature = "bevy_tasks")] pub use task_pool_plugin::*; -#[cfg(all(not(target_arch = "wasm32"), feature = "std"))] +#[cfg(all(any(unix, windows), feature = "std"))] pub use terminal_ctrl_c_handler::*; /// The app prelude. diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index 724dbfde93e27..dc26e273f42c7 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -1,20 +1,6 @@ -// TODO: Upstream `portable-atomic` support to `downcast_rs` and unconditionally -// include it as a dependency. -// See https://github.com/marcianx/downcast-rs/pull/22 for details -#[cfg(feature = "downcast")] -use downcast_rs::{impl_downcast, Downcast}; - use crate::App; use core::any::Any; - -/// Dummy trait with the same name as `downcast_rs::Downcast`. This is to ensure -/// the `Plugin: Downcast` bound can remain even when `downcast` isn't enabled. -#[cfg(not(feature = "downcast"))] -#[doc(hidden)] -pub trait Downcast {} - -#[cfg(not(feature = "downcast"))] -impl Downcast for T {} +use downcast_rs::{impl_downcast, Downcast}; /// A collection of Bevy app logic and configuration. /// @@ -105,7 +91,6 @@ pub trait Plugin: Downcast + Any + Send + Sync { } } -#[cfg(feature = "downcast")] impl_downcast!(Plugin); impl Plugin for T { @@ -183,7 +168,10 @@ mod sealed { where $($plugins: Plugins<$param>),* { - // We use `allow` instead of `expect` here because the lint is not generated for all cases. + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro, and as such, may not trigger in all cases." + )] #[allow(non_snake_case, reason = "`all_tuples!()` generates non-snake-case variable names.")] #[allow(unused_variables, reason = "`app` is unused when implemented for the unit type `()`.")] #[track_caller] diff --git a/crates/bevy_app/src/plugin_group.rs b/crates/bevy_app/src/plugin_group.rs index ce78f52315c62..49d05f32290ba 100644 --- a/crates/bevy_app/src/plugin_group.rs +++ b/crates/bevy_app/src/plugin_group.rs @@ -4,7 +4,7 @@ use alloc::{ string::{String, ToString}, vec::Vec, }; -use bevy_utils::TypeIdMap; +use bevy_utils::{hashbrown::hash_map::Entry, TypeIdMap}; use core::any::TypeId; use log::{debug, warn}; @@ -224,11 +224,6 @@ impl PluginGroup for PluginGroupBuilder { } } -/// Helper method to get the [`TypeId`] of a value without having to name its type. -fn type_id_of_val(_: &T) -> TypeId { - TypeId::of::() -} - /// Facilitates the creation and configuration of a [`PluginGroup`]. /// /// Provides a build ordering to ensure that [`Plugin`]s which produce/require a [`Resource`](bevy_ecs::system::Resource) @@ -250,20 +245,23 @@ impl PluginGroupBuilder { } } - /// Finds the index of a target [`Plugin`]. Panics if the target's [`TypeId`] is not found. - fn index_of(&self) -> usize { - let index = self - .order + /// Checks if the [`PluginGroupBuilder`] contains the given [`Plugin`]. + pub fn contains(&self) -> bool { + self.plugins.contains_key(&TypeId::of::()) + } + + /// Returns `true` if the [`PluginGroupBuilder`] contains the given [`Plugin`] and it's enabled. + pub fn enabled(&self) -> bool { + self.plugins + .get(&TypeId::of::()) + .is_some_and(|e| e.enabled) + } + + /// Finds the index of a target [`Plugin`]. + fn index_of(&self) -> Option { + self.order .iter() - .position(|&ty| ty == TypeId::of::()); - - match index { - Some(i) => i, - None => panic!( - "Plugin does not exist in group: {}.", - core::any::type_name::() - ), - } + .position(|&ty| ty == TypeId::of::()) } // Insert the new plugin as enabled, and removes its previous ordering if it was @@ -311,15 +309,27 @@ impl PluginGroupBuilder { /// # Panics /// /// Panics if the [`Plugin`] does not exist. - pub fn set(mut self, plugin: T) -> Self { - let entry = self.plugins.get_mut(&TypeId::of::()).unwrap_or_else(|| { + pub fn set(self, plugin: T) -> Self { + self.try_set(plugin).unwrap_or_else(|_| { panic!( "{} does not exist in this PluginGroup", core::any::type_name::(), ) - }); - entry.plugin = Box::new(plugin); - self + }) + } + + /// Tries to set the value of the given [`Plugin`], if it exists. + /// + /// If the given plugin doesn't exist returns self and the passed in [`Plugin`]. + pub fn try_set(mut self, plugin: T) -> Result { + match self.plugins.entry(TypeId::of::()) { + Entry::Occupied(mut entry) => { + entry.get_mut().plugin = Box::new(plugin); + + Ok(self) + } + Entry::Vacant(_) => Err((self, plugin)), + } } /// Adds the plugin [`Plugin`] at the end of this [`PluginGroupBuilder`]. If the plugin was @@ -336,6 +346,17 @@ impl PluginGroupBuilder { self } + /// Attempts to add the plugin [`Plugin`] at the end of this [`PluginGroupBuilder`]. + /// + /// If the plugin was already in the group the addition fails. + pub fn try_add(self, plugin: T) -> Result { + if self.contains::() { + return Err((self, plugin)); + } + + Ok(self.add(plugin)) + } + /// Adds a [`PluginGroup`] at the end of this [`PluginGroupBuilder`]. If the plugin was /// already in the group, it is removed from its previous place. pub fn add_group(mut self, group: impl PluginGroup) -> Self { @@ -357,23 +378,105 @@ impl PluginGroupBuilder { } /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] before the plugin of type `Target`. - /// If the plugin was already the group, it is removed from its previous place. There must - /// be a plugin of type `Target` in the group or it will panic. - pub fn add_before(mut self, plugin: impl Plugin) -> Self { - let target_index = self.index_of::(); - self.order.insert(target_index, type_id_of_val(&plugin)); + /// + /// If the plugin was already the group, it is removed from its previous place. + /// + /// # Panics + /// + /// Panics if `Target` is not already in this [`PluginGroupBuilder`]. + pub fn add_before(self, plugin: impl Plugin) -> Self { + self.try_add_before_overwrite::(plugin) + .unwrap_or_else(|_| { + panic!( + "Plugin does not exist in group: {}.", + core::any::type_name::() + ) + }) + } + + /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] before the plugin of type `Target`. + /// + /// If the plugin was already in the group the add fails. If there isn't a plugin + /// of type `Target` in the group the plugin we're trying to insert is returned. + pub fn try_add_before( + self, + plugin: Insert, + ) -> Result { + if self.contains::() { + return Err((self, plugin)); + } + + self.try_add_before_overwrite::(plugin) + } + + /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] before the plugin of type `Target`. + /// + /// If the plugin was already in the group, it is removed from its previous places. + /// If there isn't a plugin of type `Target` in the group the plugin we're trying to insert + /// is returned. + pub fn try_add_before_overwrite( + mut self, + plugin: Insert, + ) -> Result { + let Some(target_index) = self.index_of::() else { + return Err((self, plugin)); + }; + + self.order.insert(target_index, TypeId::of::()); self.upsert_plugin_state(plugin, target_index); - self + Ok(self) } /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] after the plugin of type `Target`. - /// If the plugin was already the group, it is removed from its previous place. There must - /// be a plugin of type `Target` in the group or it will panic. - pub fn add_after(mut self, plugin: impl Plugin) -> Self { - let target_index = self.index_of::() + 1; - self.order.insert(target_index, type_id_of_val(&plugin)); + /// + /// If the plugin was already the group, it is removed from its previous place. + /// + /// # Panics + /// + /// Panics if `Target` is not already in this [`PluginGroupBuilder`]. + pub fn add_after(self, plugin: impl Plugin) -> Self { + self.try_add_after_overwrite::(plugin) + .unwrap_or_else(|_| { + panic!( + "Plugin does not exist in group: {}.", + core::any::type_name::() + ) + }) + } + + /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] after the plugin of type `Target`. + /// + /// If the plugin was already in the group the add fails. If there isn't a plugin + /// of type `Target` in the group the plugin we're trying to insert is returned. + pub fn try_add_after( + self, + plugin: Insert, + ) -> Result { + if self.contains::() { + return Err((self, plugin)); + } + + self.try_add_after_overwrite::(plugin) + } + + /// Adds a [`Plugin`] in this [`PluginGroupBuilder`] after the plugin of type `Target`. + /// + /// If the plugin was already in the group, it is removed from its previous places. + /// If there isn't a plugin of type `Target` in the group the plugin we're trying to insert + /// is returned. + pub fn try_add_after_overwrite( + mut self, + plugin: Insert, + ) -> Result { + let Some(target_index) = self.index_of::() else { + return Err((self, plugin)); + }; + + let target_index = target_index + 1; + + self.order.insert(target_index, TypeId::of::()); self.upsert_plugin_state(plugin, target_index); - self + Ok(self) } /// Enables a [`Plugin`]. @@ -451,6 +554,9 @@ impl PluginGroup for NoopPluginGroup { #[cfg(test)] mod tests { + use alloc::vec; + use core::{any::TypeId, fmt::Debug}; + use super::PluginGroupBuilder; use crate::{App, NoopPluginGroup, Plugin}; @@ -469,6 +575,35 @@ mod tests { fn build(&self, _: &mut App) {} } + #[derive(PartialEq, Debug)] + struct PluginWithData(u32); + impl Plugin for PluginWithData { + fn build(&self, _: &mut App) {} + } + + fn get_plugin(group: &PluginGroupBuilder, id: TypeId) -> &T { + group.plugins[&id] + .plugin + .as_any() + .downcast_ref::() + .unwrap() + } + + #[test] + fn contains() { + let group = PluginGroupBuilder::start::() + .add(PluginA) + .add(PluginB); + + assert!(group.contains::()); + assert!(!group.contains::()); + + let group = group.disable::(); + + assert!(group.enabled::()); + assert!(!group.enabled::()); + } + #[test] fn basic_ordering() { let group = PluginGroupBuilder::start::() @@ -479,13 +614,56 @@ mod tests { assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ] + ); + } + + #[test] + fn add_before() { + let group = PluginGroupBuilder::start::() + .add(PluginA) + .add(PluginB) + .add_before::(PluginC); + + assert_eq!( + group.order, + vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } + #[test] + fn try_add_before() { + let group = PluginGroupBuilder::start::().add(PluginA); + + let Ok(group) = group.try_add_before::(PluginC) else { + panic!("PluginA wasn't in group"); + }; + + assert_eq!( + group.order, + vec![TypeId::of::(), TypeId::of::(),] + ); + + assert!(group.try_add_before::(PluginC).is_err()); + } + + #[test] + #[should_panic( + expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB." + )] + fn add_before_nonexistent() { + PluginGroupBuilder::start::() + .add(PluginA) + .add_before::(PluginC); + } + #[test] fn add_after() { let group = PluginGroupBuilder::start::() @@ -496,26 +674,103 @@ mod tests { assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } #[test] - fn add_before() { + fn try_add_after() { let group = PluginGroupBuilder::start::() .add(PluginA) - .add(PluginB) - .add_before::(PluginC); + .add(PluginB); + + let Ok(group) = group.try_add_after::(PluginC) else { + panic!("PluginA wasn't in group"); + }; assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ] + ); + + assert!(group.try_add_after::(PluginC).is_err()); + } + + #[test] + #[should_panic( + expected = "Plugin does not exist in group: bevy_app::plugin_group::tests::PluginB." + )] + fn add_after_nonexistent() { + PluginGroupBuilder::start::() + .add(PluginA) + .add_after::(PluginC); + } + + #[test] + fn add_overwrite() { + let group = PluginGroupBuilder::start::() + .add(PluginA) + .add(PluginWithData(0x0F)) + .add(PluginC); + + let id = TypeId::of::(); + assert_eq!( + get_plugin::(&group, id), + &PluginWithData(0x0F) + ); + + let group = group.add(PluginWithData(0xA0)); + + assert_eq!( + get_plugin::(&group, id), + &PluginWithData(0xA0) + ); + assert_eq!( + group.order, + vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ] + ); + + let Ok(group) = group.try_add_before_overwrite::(PluginWithData(0x01)) else { + panic!("PluginA wasn't in group"); + }; + assert_eq!( + get_plugin::(&group, id), + &PluginWithData(0x01) + ); + assert_eq!( + group.order, + vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ] + ); + + let Ok(group) = group.try_add_after_overwrite::(PluginWithData(0xdeadbeef)) + else { + panic!("PluginA wasn't in group"); + }; + assert_eq!( + get_plugin::(&group, id), + &PluginWithData(0xdeadbeef) + ); + assert_eq!( + group.order, + vec![ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } @@ -531,45 +786,45 @@ mod tests { assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } #[test] - fn readd_after() { + fn readd_before() { let group = PluginGroupBuilder::start::() .add(PluginA) .add(PluginB) .add(PluginC) - .add_after::(PluginC); + .add_before::(PluginC); assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } #[test] - fn readd_before() { + fn readd_after() { let group = PluginGroupBuilder::start::() .add(PluginA) .add(PluginB) .add(PluginC) - .add_before::(PluginC); + .add_after::(PluginC); assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } @@ -587,9 +842,9 @@ mod tests { assert_eq!( group_b.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } @@ -611,9 +866,9 @@ mod tests { assert_eq!( group.order, vec![ - core::any::TypeId::of::(), - core::any::TypeId::of::(), - core::any::TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), ] ); } diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index d1e3865b52ac8..21ff6669ef710 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -3,14 +3,14 @@ use crate::{ plugin::Plugin, PluginsState, }; -use bevy_utils::Duration; +use core::time::Duration; #[cfg(any(target_arch = "wasm32", feature = "std"))] use bevy_utils::Instant; #[cfg(target_arch = "wasm32")] use { - alloc::rc::Rc, + alloc::{boxed::Box, rc::Rc}, core::cell::RefCell, wasm_bindgen::{prelude::*, JsCast}, }; diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index b921956a6d317..b2a783ec3ed10 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -166,6 +166,35 @@ impl SubApp { self } + /// Take the function that will be called by [`extract`](Self::extract) out of the app, if any was set, + /// and replace it with `None`. + /// + /// If you use Bevy, `bevy_render` will set a default extract function used to extract data from + /// the main world into the render world as part of the Extract phase. In that case, you cannot replace + /// it with your own function. Instead, take the Bevy default function with this, and install your own + /// instead which calls the Bevy default. + /// + /// ``` + /// # use bevy_app::SubApp; + /// # let mut app = SubApp::new(); + /// let 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() { + /// f(main, render); + /// } + /// + /// // Do post-extract custom logic + /// // [...] + /// }); + /// ``` + pub fn take_extract(&mut self) -> Option { + self.extract.take() + } + /// See [`App::insert_resource`]. pub fn insert_resource(&mut self, resource: R) -> &mut Self { self.world.insert_resource(resource); @@ -333,7 +362,6 @@ impl SubApp { } /// See [`App::get_added_plugins`]. - #[cfg(feature = "downcast")] pub fn get_added_plugins(&self) -> Vec<&T> where T: Plugin, diff --git a/crates/bevy_app/src/task_pool_plugin.rs b/crates/bevy_app/src/task_pool_plugin.rs index 5623371dad58f..a4bf9d12dc9a8 100644 --- a/crates/bevy_app/src/task_pool_plugin.rs +++ b/crates/bevy_app/src/task_pool_plugin.rs @@ -6,14 +6,16 @@ ) )] -use crate::{App, Last, Plugin}; +use crate::{App, Plugin}; use alloc::string::ToString; -use bevy_ecs::prelude::*; use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder}; use core::{fmt::Debug, marker::PhantomData}; use log::trace; +#[cfg(not(target_arch = "wasm32"))] +use {crate::Last, bevy_ecs::prelude::NonSend}; + #[cfg(feature = "portable-atomic")] use portable_atomic_util::Arc; @@ -187,6 +189,7 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(io_threads); IoTaskPool::get_or_init(|| { + #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let mut builder = TaskPoolBuilder::default() .num_threads(io_threads) .thread_name("IO Task Pool".to_string()); @@ -215,6 +218,7 @@ impl TaskPoolOptions { remaining_threads = remaining_threads.saturating_sub(async_compute_threads); AsyncComputeTaskPool::get_or_init(|| { + #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let mut builder = TaskPoolBuilder::default() .num_threads(async_compute_threads) .thread_name("Async Compute Task Pool".to_string()); @@ -243,6 +247,7 @@ impl TaskPoolOptions { trace!("Compute Threads: {}", compute_threads); ComputeTaskPool::get_or_init(|| { + #[cfg_attr(target_arch = "wasm32", expect(unused_mut))] let mut builder = TaskPoolBuilder::default() .num_threads(compute_threads) .thread_name("Compute Task Pool".to_string()); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index de7094a63663e..5ae415c1f123d 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides asset functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -19,14 +19,14 @@ watch = [] trace = [] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset_macros = { path = "macros", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset_macros = { path = "macros", 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", features = [ "uuid", ] } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } stackfuture = "0.3" atomicow = "1.0" @@ -35,7 +35,7 @@ async-fs = "2.0" async-lock = "3.0" bitflags = { version = "2.3", features = ["serde"] } crossbeam-channel = "0.5" -downcast-rs = "1.2" +downcast-rs = { version = "2", default-features = false, features = ["std"] } disqualified = "1.0" either = "1.13" futures-io = "0.3" @@ -47,9 +47,10 @@ serde = { version = "1", features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } uuid = { version = "1.0", features = ["v4"] } +tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_os = "android")'.dependencies] -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } @@ -65,7 +66,7 @@ js-sys = "0.3" notify-debouncer-full = { version = "0.4.0", optional = true } [dev-dependencies] -bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } [lints] workspace = true diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml index a210d535299f6..9b6c4f56a19ad 100644 --- a/crates/bevy_asset/macros/Cargo.toml +++ b/crates/bevy_asset/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_asset_macros" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Derive implementations for bevy_asset" homepage = "https://bevyengine.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index b7902587d420b..cb9f95defae9c 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -13,10 +13,10 @@ use bevy_ecs::{ storage::{Table, TableRow}, world::unsafe_world_cell::UnsafeWorldCell, }; -use bevy_utils::tracing::error; use bevy_utils::HashMap; use core::marker::PhantomData; use disqualified::ShortName; +use tracing::error; /// A resource that stores the last tick an asset was changed. This is used by /// the [`AssetChanged`] filter to determine if an asset has changed since the last time @@ -86,7 +86,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { } } -/// Filter that selects entities with a `A` for an asset that changed +/// Filter that selects entities with an `A` for an asset that changed /// after the system last ran, where `A` is a component that implements /// [`AsAssetId`]. /// @@ -114,8 +114,8 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// # Performance /// /// When at least one `A` is updated, this will -/// read a hashmap once per entity with a `A` component. The -/// runtime of the query is proportional to how many entities with a `A` +/// read a hashmap once per entity with an `A` component. The +/// runtime of the query is proportional to how many entities with an `A` /// it matches. /// /// If no `A` asset updated since the last time the system ran, then no lookups occur. @@ -148,7 +148,7 @@ pub struct AssetChangedState { _asset: PhantomData, } -#[allow(unsafe_code)] +#[expect(unsafe_code, reason = "WorldQuery is an unsafe trait.")] /// SAFETY: `ROQueryFetch` is the same as `QueryFetch` unsafe impl WorldQuery for AssetChanged { type Item<'w> = (); @@ -233,11 +233,6 @@ unsafe impl WorldQuery for AssetChanged { #[inline] fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { <&A>::update_component_access(&state.asset_id, access); - assert!( - !access.access().has_resource_write(state.resource_id), - "AssetChanged<{ty}> requires read-only access to AssetChanges<{ty}>", - ty = ShortName::of::() - ); access.add_resource_read(state.resource_id); } @@ -269,7 +264,7 @@ unsafe impl WorldQuery for AssetChanged { } } -#[allow(unsafe_code)] +#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")] /// SAFETY: read-only access unsafe impl QueryFilter for AssetChanged { const IS_ARCHETYPAL: bool = false; @@ -280,7 +275,7 @@ unsafe impl QueryFilter for AssetChanged { entity: Entity, table_row: TableRow, ) -> bool { - fetch.inner.as_mut().map_or(false, |inner| { + 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); diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 9c61ff0f88c7b..19065ae6e84fa 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -551,8 +551,11 @@ mod tests { } /// Typed and Untyped `Handles` should be orderable amongst each other and themselves - #[allow(clippy::cmp_owned)] #[test] + #[expect( + clippy::cmp_owned, + reason = "This lints on the assertion that a typed handle converted to an untyped handle maintains its ordering compared to an untyped handle. While the conversion would normally be useless, we need to ensure that converted handles maintain their ordering, making the conversion necessary here." + )] fn ordering() { assert!(UUID_1 < UUID_2); diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index b8b78a9681637..d8414a539fe06 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -1,7 +1,7 @@ use crate::io::{get_meta_path, AssetReader, AssetReaderError, PathStream, Reader, VecReader}; -use bevy_utils::tracing::error; use futures_lite::stream; use std::{ffi::CString, path::Path}; +use tracing::error; /// [`AssetReader`] implementation for Android devices, built on top of Android's [`AssetManager`]. /// diff --git a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs index cc97eb3cda83c..d568aa6052b73 100644 --- a/crates/bevy_asset/src/io/embedded/embedded_watcher.rs +++ b/crates/bevy_asset/src/io/embedded/embedded_watcher.rs @@ -4,7 +4,8 @@ use crate::io::{ AssetSourceEvent, AssetWatcher, }; use alloc::sync::Arc; -use bevy_utils::{tracing::warn, Duration, HashMap}; +use bevy_utils::HashMap; +use core::time::Duration; use notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, RecommendedCache}; use parking_lot::RwLock; use std::{ @@ -12,6 +13,7 @@ use std::{ io::{BufReader, Read}, path::{Path, PathBuf}, }; +use tracing::warn; /// A watcher for assets stored in the `embedded` asset source. Embedded assets are assets whose /// bytes have been embedded into the Rust binary using the [`embedded_asset`](crate::embedded_asset) macro. diff --git a/crates/bevy_asset/src/io/file/file_watcher.rs b/crates/bevy_asset/src/io/file/file_watcher.rs index bb4cf109c32c1..80a36a82407f9 100644 --- a/crates/bevy_asset/src/io/file/file_watcher.rs +++ b/crates/bevy_asset/src/io/file/file_watcher.rs @@ -2,7 +2,7 @@ use crate::{ io::{AssetSourceEvent, AssetWatcher}, path::normalize_path, }; -use bevy_utils::{tracing::error, Duration}; +use core::time::Duration; use crossbeam_channel::Sender; use notify_debouncer_full::{ new_debouncer, @@ -14,6 +14,7 @@ use notify_debouncer_full::{ DebounceEventResult, Debouncer, RecommendedCache, }; use std::path::{Path, PathBuf}; +use tracing::error; /// An [`AssetWatcher`] that watches the filesystem for changes to asset files in a given root folder and emits [`AssetSourceEvent`] /// for each relevant change. This uses [`notify_debouncer_full`] to retrieve "debounced" filesystem events. @@ -26,13 +27,13 @@ pub struct FileWatcher { impl FileWatcher { pub fn new( - root: PathBuf, + path: PathBuf, sender: Sender, debounce_wait_time: Duration, ) -> Result { - let root = normalize_path(super::get_base_path().join(root).as_path()); + let root = normalize_path(&path); let watcher = new_asset_event_debouncer( - root.clone(), + path.clone(), debounce_wait_time, FileEventHandler { root, @@ -49,15 +50,12 @@ impl AssetWatcher for FileWatcher {} pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) { let relative_path = absolute_path.strip_prefix(root).unwrap_or_else(|_| { panic!( - "FileWatcher::get_asset_path() failed to strip prefix from absolute path: absolute_path={:?}, root={:?}", - absolute_path, - root + "FileWatcher::get_asset_path() failed to strip prefix from absolute path: absolute_path={}, root={}", + absolute_path.display(), + root.display() ) }); - let is_meta = relative_path - .extension() - .map(|e| e == "meta") - .unwrap_or(false); + let is_meta = relative_path.extension().is_some_and(|e| e == "meta"); let asset_path = if is_meta { relative_path.with_extension("") } else { diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 387924001f5fd..dc04349868d78 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -6,9 +6,9 @@ mod file_asset; #[cfg(not(feature = "multi_threaded"))] mod sync_file_asset; -use bevy_utils::tracing::{debug, error}; #[cfg(feature = "file_watcher")] pub use file_watcher::*; +use tracing::{debug, error}; use std::{ env, @@ -78,8 +78,9 @@ impl FileAssetWriter { if create_root { if let Err(e) = std::fs::create_dir_all(&root_path) { error!( - "Failed to create root directory {:?} for file asset writer: {:?}", - root_path, e + "Failed to create root directory {} for file asset writer: {}", + root_path.display(), + e ); } } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index 0c4c0b1f00356..f6d567e78906a 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -22,7 +22,7 @@ pub use futures_lite::AsyncWriteExt; pub use source::*; use alloc::sync::Arc; -use bevy_utils::{BoxedFuture, ConditionalSendFuture}; +use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::future::Future; use core::{ mem::size_of, diff --git a/crates/bevy_asset/src/io/processor_gated.rs b/crates/bevy_asset/src/io/processor_gated.rs index 2179379070ce1..1472e7ad136be 100644 --- a/crates/bevy_asset/src/io/processor_gated.rs +++ b/crates/bevy_asset/src/io/processor_gated.rs @@ -5,10 +5,10 @@ use crate::{ }; use alloc::sync::Arc; use async_lock::RwLockReadGuardArc; -use bevy_utils::tracing::trace; use core::{pin::Pin, task::Poll}; use futures_io::AsyncRead; use std::path::Path; +use tracing::trace; use super::{AsyncSeekForward, ErasedAssetReader}; diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index c0bab2037f8e3..4b109a124f47f 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -5,12 +5,10 @@ use crate::{ use alloc::sync::Arc; use atomicow::CowArc; use bevy_ecs::system::Resource; -use bevy_utils::{ - tracing::{error, warn}, - Duration, HashMap, -}; -use core::{fmt::Display, hash::Hash}; +use bevy_utils::HashMap; +use core::{fmt::Display, hash::Hash, time::Duration}; use thiserror::Error; +use tracing::{error, warn}; use super::{ErasedAssetReader, ErasedAssetWriter}; @@ -531,7 +529,7 @@ impl AssetSource { not(target_os = "android") ))] { - let path = std::path::PathBuf::from(path.clone()); + let path = super::file::get_base_path().join(path.clone()); if path.exists() { Some(Box::new( super::file::FileWatcher::new( diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 25a5d223cbb0b..34e8d6a2cb44b 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -1,9 +1,9 @@ use crate::io::{ get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, }; -use bevy_utils::tracing::error; use js_sys::{Uint8Array, JSON}; use std::path::{Path, PathBuf}; +use tracing::error; use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue}; use wasm_bindgen_futures::JsFuture; use web_sys::Response; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index cb14a190a335f..10a1bcd8f6d4b 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -215,8 +215,9 @@ use bevy_ecs::{ world::FromWorld, }; use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; -use bevy_utils::{tracing::error, HashSet}; +use bevy_utils::HashSet; use core::any::TypeId; +use tracing::error; #[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))] compile_error!( @@ -641,7 +642,8 @@ mod tests { }; use bevy_log::LogPlugin; use bevy_reflect::TypePath; - use bevy_utils::{Duration, HashMap}; + use bevy_utils::HashMap; + use core::time::Duration; use serde::{Deserialize, Serialize}; use std::path::Path; use thiserror::Error; @@ -1684,9 +1686,9 @@ mod tests { ); } } - _ => panic!("Unexpected error type {:?}", read_error), + _ => panic!("Unexpected error type {}", read_error), }, - _ => panic!("Unexpected error type {:?}", error.error), + _ => panic!("Unexpected error type {}", error.error), } } } @@ -1765,8 +1767,11 @@ mod tests { #[derive(Asset, TypePath)] pub struct TestAsset; - #[allow(dead_code)] #[derive(Asset, TypePath)] + #[expect( + dead_code, + reason = "This exists to ensure that `#[derive(Asset)]` works on enums. The inner variants are known not to be used." + )] pub enum EnumTestAsset { Unnamed(#[dependency] Handle), Named { @@ -1781,7 +1786,6 @@ mod tests { Empty, } - #[allow(dead_code)] #[derive(Asset, TypePath)] pub struct StructTestAsset { #[dependency] @@ -1790,7 +1794,6 @@ mod tests { embedded: TestAsset, } - #[allow(dead_code)] #[derive(Asset, TypePath)] pub struct TupleTestAsset(#[dependency] Handle); } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index bfd5064138929..96515460e494a 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -8,7 +8,8 @@ use crate::{ }; use atomicow::CowArc; use bevy_ecs::world::World; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap, HashSet}; +use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; +use bevy_utils::{HashMap, HashSet}; use core::any::{Any, TypeId}; use downcast_rs::{impl_downcast, Downcast}; use ron::error::SpannedError; diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index bad3a4be729f6..e15de6dd0c83a 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -2,10 +2,10 @@ use crate::{ self as bevy_asset, loader::AssetLoader, processor::Process, Asset, AssetPath, DeserializeMetaError, VisitAssetDependencies, }; -use bevy_utils::tracing::error; use downcast_rs::{impl_downcast, Downcast}; use ron::ser::PrettyConfig; use serde::{Deserialize, Serialize}; +use tracing::error; pub const META_FORMAT_VERSION: &str = "1.0"; pub type MetaTransform = Box; diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index 3bbb643650b6f..c88e624635452 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -454,7 +454,7 @@ impl<'a> AssetPath<'a> { pub fn get_full_extension(&self) -> Option { let file_name = self.path().file_name()?.to_str()?; let index = file_name.find('.')?; - let mut extension = file_name[index + 1..].to_lowercase(); + let mut extension = file_name[index + 1..].to_owned(); // Strip off any query parameters let query = extension.find('?'); @@ -972,5 +972,8 @@ mod tests { let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz"); assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string())); + + let result = AssetPath::from("asset.Custom"); + assert_eq!(result.get_full_extension(), Some("Custom".to_string())); } } diff --git a/crates/bevy_asset/src/processor/log.rs b/crates/bevy_asset/src/processor/log.rs index 2649b815d8519..dbbab8a9adec4 100644 --- a/crates/bevy_asset/src/processor/log.rs +++ b/crates/bevy_asset/src/processor/log.rs @@ -1,9 +1,10 @@ use crate::AssetPath; use async_fs::File; -use bevy_utils::{tracing::error, HashSet}; +use bevy_utils::HashSet; use futures_lite::{AsyncReadExt, AsyncWriteExt}; use std::path::PathBuf; use thiserror::Error; +use tracing::error; /// An in-memory representation of a single [`ProcessorTransactionLog`] entry. #[derive(Debug)] diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index c74fd80b5673d..d32b98dcbe46d 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -58,21 +58,18 @@ use crate::{ }; use alloc::{collections::VecDeque, sync::Arc}; use bevy_ecs::prelude::*; -use bevy_tasks::IoTaskPool; -use bevy_utils::{ - tracing::{debug, error, trace, warn}, - HashMap, HashSet, -}; #[cfg(feature = "trace")] -use bevy_utils::{ - tracing::{info_span, instrument::Instrument}, - ConditionalSendFuture, -}; +use bevy_tasks::ConditionalSendFuture; +use bevy_tasks::IoTaskPool; +use bevy_utils::{HashMap, HashSet}; use futures_io::ErrorKind; use futures_lite::{AsyncReadExt, AsyncWriteExt, StreamExt}; use parking_lot::RwLock; use std::path::{Path, PathBuf}; use thiserror::Error; +use tracing::{debug, error, trace, warn}; +#[cfg(feature = "trace")] +use tracing::{info_span, instrument::Instrument}; /// A "background" asset processor that reads asset values from a source [`AssetSource`] (which corresponds to an [`AssetReader`](crate::io::AssetReader) / [`AssetWriter`](crate::io::AssetWriter) pair), /// processes them in some way, and writes them to a destination [`AssetSource`]. @@ -381,7 +378,7 @@ impl AssetProcessor { // Therefore, we shouldn't automatically delete the asset ... that is a // user-initiated action. debug!( - "Meta for asset {:?} was removed. Attempting to re-process", + "Meta for asset {} was removed. Attempting to re-process", AssetPath::from_path(&path).with_source(source.id()) ); self.process_asset(source, path).await; @@ -389,7 +386,10 @@ impl AssetProcessor { /// Removes all processed assets stored at the given path (respecting transactionality), then removes the folder itself. async fn handle_removed_folder(&self, source: &AssetSource, path: &Path) { - debug!("Removing folder {:?} because source was removed", path); + debug!( + "Removing folder {} because source was removed", + path.display() + ); let processed_reader = source.processed_reader().unwrap(); match processed_reader.read_directory(path).await { Ok(mut path_stream) => { @@ -478,7 +478,6 @@ impl AssetProcessor { self.set_state(ProcessorState::Finished).await; } - #[allow(unused)] #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] async fn process_assets_internal<'scope>( &'scope self, @@ -739,7 +738,7 @@ impl AssetProcessor { ) -> Result { // TODO: The extension check was removed now that AssetPath is the input. is that ok? // TODO: check if already processing to protect against duplicate hot-reload events - debug!("Processing {:?}", asset_path); + debug!("Processing {}", asset_path); let server = &self.server; let path = asset_path.path(); let reader = source.reader(); @@ -1237,7 +1236,7 @@ impl ProcessorAssetInfos { ) { match result { Ok(ProcessResult::Processed(processed_info)) => { - debug!("Finished processing \"{:?}\"", asset_path); + debug!("Finished processing \"{}\"", asset_path); // clean up old dependents let old_processed_info = self .infos @@ -1260,7 +1259,7 @@ impl ProcessorAssetInfos { } } Ok(ProcessResult::SkippedNotChanged) => { - debug!("Skipping processing (unchanged) \"{:?}\"", asset_path); + debug!("Skipping processing (unchanged) \"{}\"", asset_path); let info = self.get_mut(&asset_path).expect("info should exist"); // NOTE: skipping an asset on a given pass doesn't mean it won't change in the future as a result // of a dependency being re-processed. This means apps might receive an "old" (but valid) asset first. @@ -1271,7 +1270,7 @@ impl ProcessorAssetInfos { info.update_status(ProcessStatus::Processed).await; } Ok(ProcessResult::Ignored) => { - debug!("Skipping processing (ignored) \"{:?}\"", asset_path); + debug!("Skipping processing (ignored) \"{}\"", asset_path); } Err(ProcessError::ExtensionRequired) => { // Skip assets without extensions diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 64370824dc630..3dd3b5127168c 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -10,7 +10,7 @@ use crate::{ AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset, MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError, }; -use bevy_utils::{BoxedFuture, ConditionalSendFuture}; +use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::marker::PhantomData; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -105,26 +105,6 @@ impl< } } -/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then -/// saves that `L` asset using the `S` [`AssetSaver`]. -/// -/// This is a specialized use case of [`LoadTransformAndSave`] and is useful where there is no asset manipulation -/// such as when compressing assets. -/// -/// This uses [`LoadAndSaveSettings`] to configure the processor. -/// -/// [`Asset`]: crate::Asset -#[deprecated = "Use `LoadTransformAndSave::Asset>, S>` instead"] -pub type LoadAndSave = - LoadTransformAndSave::Asset>, S>; - -/// Settings for the [`LoadAndSave`] [`Process::Settings`] implementation. -/// -/// `LoaderSettings` corresponds to [`AssetLoader::Settings`] and `SaverSettings` corresponds to [`AssetSaver::Settings`]. -#[deprecated = "Use `LoadTransformAndSaveSettings` instead"] -pub type LoadAndSaveSettings = - LoadTransformAndSaveSettings; - /// An error that is encountered during [`Process::process`]. #[derive(Error, Debug)] pub enum ProcessError { diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index f2cb5bb9330f9..d1f907e07e86e 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -3,7 +3,8 @@ use crate::{ ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle, }; use atomicow::CowArc; -use bevy_utils::{BoxedFuture, ConditionalSendFuture, HashMap}; +use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; +use bevy_utils::HashMap; use core::{borrow::Borrow, hash::Hash, ops::Deref}; use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 898b3a76ec46f..33645d8b1e87c 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -7,11 +7,12 @@ use crate::{ use alloc::sync::{Arc, Weak}; use bevy_ecs::world::World; use bevy_tasks::Task; -use bevy_utils::{tracing::warn, Entry, HashMap, HashSet, TypeIdMap}; +use bevy_utils::{Entry, HashMap, HashSet, TypeIdMap}; use core::{any::TypeId, task::Waker}; use crossbeam_channel::Sender; use either::Either; use thiserror::Error; +use tracing::warn; #[derive(Debug)] pub(crate) struct AssetInfo { @@ -112,10 +113,6 @@ impl AssetInfos { .unwrap() } - #[expect( - clippy::too_many_arguments, - reason = "Arguments needed so that both `create_loading_handle_untyped()` and `get_or_create_path_handle_internal()` may share code." - )] fn create_handle_internal( infos: &mut HashMap, handle_providers: &TypeIdMap, @@ -443,7 +440,7 @@ impl AssetInfos { } else { // the dependency id does not exist, which implies it was manually removed or never existed in the first place warn!( - "Dependency {:?} from asset {:?} is unknown. This asset's dependency load status will not switch to 'Loaded' until the unknown dependency is loaded.", + "Dependency {} from asset {} is unknown. This asset's dependency load status will not switch to 'Loaded' until the unknown dependency is loaded.", dep_id, loaded_asset_id ); true diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 2442de389ae3d..6bedfe2446132 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -4,15 +4,15 @@ use crate::{ }; use alloc::sync::Arc; use async_broadcast::RecvError; -use bevy_tasks::IoTaskPool; -use bevy_utils::{tracing::warn, HashMap, TypeIdMap}; #[cfg(feature = "trace")] -use bevy_utils::{ - tracing::{info_span, instrument::Instrument}, - ConditionalSendFuture, -}; +use bevy_tasks::ConditionalSendFuture; +use bevy_tasks::IoTaskPool; +use bevy_utils::{HashMap, TypeIdMap}; use core::any::TypeId; use thiserror::Error; +use tracing::warn; +#[cfg(feature = "trace")] +use tracing::{info_span, instrument::Instrument}; #[derive(Default)] pub(crate) struct AssetLoaders { @@ -47,6 +47,7 @@ impl AssetLoaders { }; if is_new { + let existing_loaders_for_type_id = self.type_id_to_loaders.get(&loader_asset_type); let mut duplicate_extensions = Vec::new(); for extension in AssetLoader::extensions(&*loader) { let list = self @@ -55,26 +56,29 @@ impl AssetLoaders { .or_default(); if !list.is_empty() { - duplicate_extensions.push(extension); + if let Some(existing_loaders_for_type_id) = existing_loaders_for_type_id { + if list + .iter() + .any(|index| existing_loaders_for_type_id.contains(index)) + { + duplicate_extensions.push(extension); + } + } } list.push(loader_index); } - - self.type_name_to_loader.insert(type_name, loader_index); - - let list = self - .type_id_to_loaders - .entry(loader_asset_type) - .or_default(); - - let duplicate_asset_registration = !list.is_empty(); - if !duplicate_extensions.is_empty() && duplicate_asset_registration { + if !duplicate_extensions.is_empty() { warn!("Duplicate AssetLoader registered for Asset type `{loader_asset_type_name}` with extensions `{duplicate_extensions:?}`. \ Loader must be specified in a .meta file in order to load assets of this type with these extensions."); } - list.push(loader_index); + self.type_name_to_loader.insert(type_name, loader_index); + + self.type_id_to_loaders + .entry(loader_asset_type) + .or_default() + .push(loader_index); self.loaders.push(MaybeAssetLoader::Ready(loader)); } else { @@ -108,6 +112,8 @@ impl AssetLoaders { self.preregistered_loaders.insert(type_name, loader_index); self.type_name_to_loader.insert(type_name, loader_index); + + let existing_loaders_for_type_id = self.type_id_to_loaders.get(&loader_asset_type); let mut duplicate_extensions = Vec::new(); for extension in extensions { let list = self @@ -116,24 +122,27 @@ impl AssetLoaders { .or_default(); if !list.is_empty() { - duplicate_extensions.push(extension); + if let Some(existing_loaders_for_type_id) = existing_loaders_for_type_id { + if list + .iter() + .any(|index| existing_loaders_for_type_id.contains(index)) + { + duplicate_extensions.push(extension); + } + } } list.push(loader_index); } - - let list = self - .type_id_to_loaders - .entry(loader_asset_type) - .or_default(); - - let duplicate_asset_registration = !list.is_empty(); - if !duplicate_extensions.is_empty() && duplicate_asset_registration { + if !duplicate_extensions.is_empty() { warn!("Duplicate AssetLoader preregistered for Asset type `{loader_asset_type_name}` with extensions `{duplicate_extensions:?}`. \ Loader must be specified in a .meta file in order to load assets of this type with these extensions."); } - list.push(loader_index); + self.type_id_to_loaders + .entry(loader_asset_type) + .or_default() + .push(loader_index); let (mut sender, receiver) = async_broadcast::broadcast(1); sender.set_overflow(true); @@ -342,18 +351,14 @@ mod tests { use super::*; - // The compiler notices these fields are never read and raises a dead_code lint which kill CI. - #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] - struct A(usize); + struct A; - #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] - struct B(usize); + struct B; - #[allow(dead_code)] #[derive(Asset, TypePath, Debug)] - struct C(usize); + struct C; struct Loader { sender: Sender<()>, diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index bfeb7a40f99f2..af751c3a12b67 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -21,10 +21,7 @@ use alloc::sync::Arc; use atomicow::CowArc; use bevy_ecs::prelude::*; use bevy_tasks::IoTaskPool; -use bevy_utils::{ - tracing::{error, info}, - HashSet, -}; +use bevy_utils::HashSet; use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll}; use crossbeam_channel::{Receiver, Sender}; use either::Either; @@ -34,6 +31,7 @@ use loaders::*; use parking_lot::{RwLock, RwLockWriteGuard}; use std::path::{Path, PathBuf}; use thiserror::Error; +use tracing::{error, info}; /// Loads and tracks the state of [`Asset`] values from a configured [`AssetReader`](crate::io::AssetReader). This can be used to kick off new asset loads and /// retrieve their current load states. @@ -906,7 +904,7 @@ impl AssetServer { .spawn(async move { let Ok(source) = server.get_source(path.source()) else { error!( - "Failed to load {path}. AssetSource {:?} does not exist", + "Failed to load {path}. AssetSource {} does not exist", path.source() ); return; @@ -918,7 +916,7 @@ impl AssetServer { Ok(reader) => reader, Err(_) => { error!( - "Failed to load {path}. AssetSource {:?} does not have a processed AssetReader", + "Failed to load {path}. AssetSource {} does not have a processed AssetReader", path.source() ); return; diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index 484e02003f644..ac894ea69fcad 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -1,6 +1,7 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, UntypedHandle}; use atomicow::CowArc; -use bevy_utils::{ConditionalSendFuture, HashMap}; +use bevy_tasks::ConditionalSendFuture; +use bevy_utils::HashMap; use core::{ borrow::Borrow, convert::Infallible, diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index 7df10a1bcbd57..71c7efe65ba08 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_audio" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides audio functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,20 +10,20 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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_hierarchy = { path = "../bevy_hierarchy", 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", features = [ "bevy", ] } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } # other 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 } diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 25f2e07df95df..1d0149381bd57 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - use crate::{AudioSource, Decodable, Volume}; use bevy_asset::{Asset, Handle}; use bevy_ecs::prelude::*; @@ -207,13 +205,6 @@ impl Default for SpatialScale { #[reflect(Resource, Default)] pub struct DefaultSpatialScale(pub SpatialScale); -/// Bundle for playing a standard bevy audio asset -#[deprecated( - since = "0.15.0", - note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." -)] -pub type AudioBundle = AudioSourceBundle; - /// A component for playing a sound. /// /// Insert this component onto an entity to trigger an audio source to begin playing. @@ -252,48 +243,3 @@ impl AudioPlayer { Self(source) } } - -/// Bundle for playing a sound. -/// -/// Insert this bundle onto an entity to trigger a sound source to begin playing. -/// -/// 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 -/// added to the entity. You can use that component to control the audio settings during playback. -#[derive(Bundle)] -#[deprecated( - since = "0.15.0", - note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." -)] -pub struct AudioSourceBundle -where - Source: Asset + Decodable, -{ - /// Asset containing the audio data to play. - pub source: AudioPlayer, - /// Initial settings that the audio starts playing with. - /// If you would like to control the audio while it is playing, - /// query for the [`AudioSink`][crate::AudioSink] component. - /// Changes to this component will *not* be applied to already-playing audio. - pub settings: PlaybackSettings, -} - -impl Clone for AudioSourceBundle { - fn clone(&self) -> Self { - Self { - source: self.source.clone(), - settings: self.settings, - } - } -} - -impl Default for AudioSourceBundle { - fn default() -> Self { - Self { - source: AudioPlayer(Handle::default()), - settings: Default::default(), - } - } -} diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 45896c81ba7ac..90bdc38499974 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -7,8 +7,8 @@ use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_hierarchy::DespawnRecursiveExt; use bevy_math::Vec3; use bevy_transform::prelude::GlobalTransform; -use bevy_utils::tracing::warn; use rodio::{OutputStream, OutputStreamHandle, Sink, Source, SpatialSink}; +use tracing::warn; use crate::{AudioSink, AudioSinkPlayback}; @@ -47,11 +47,11 @@ impl Default for AudioOutput { } /// Marker for internal use, to despawn entities when playback finishes. -#[derive(Component)] +#[derive(Component, Default)] pub struct PlaybackDespawnMarker; /// Marker for internal use, to remove audio components when playback finishes. -#[derive(Component)] +#[derive(Component, Default)] pub struct PlaybackRemoveMarker; #[derive(SystemParam)] @@ -130,7 +130,7 @@ pub(crate) fn play_queued_audio_system( // the user may have made a mistake. if ear_positions.multiple_listeners() { warn!( - "Multiple SpatialListeners found. Using {:?}.", + "Multiple SpatialListeners found. Using {}.", ear_positions.query.iter().next().unwrap().0 ); } diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index a8de9393d15e1..babae2f8a9be9 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -39,13 +39,11 @@ mod volume; /// The audio prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ - AudioBundle, AudioPlayer, AudioSink, AudioSinkPlayback, AudioSource, AudioSourceBundle, - Decodable, GlobalVolume, Pitch, PitchBundle, PlaybackSettings, SpatialAudioSink, - SpatialListener, + AudioPlayer, AudioSink, AudioSinkPlayback, AudioSource, Decodable, GlobalVolume, Pitch, + PlaybackSettings, SpatialAudioSink, SpatialListener, }; } diff --git a/crates/bevy_audio/src/pitch.rs b/crates/bevy_audio/src/pitch.rs index 02863d6c62781..d85b9b31cf071 100644 --- a/crates/bevy_audio/src/pitch.rs +++ b/crates/bevy_audio/src/pitch.rs @@ -1,6 +1,4 @@ -#![expect(deprecated)] - -use crate::{AudioSourceBundle, Decodable}; +use crate::Decodable; use bevy_asset::Asset; use bevy_reflect::TypePath; use rodio::{ @@ -35,10 +33,3 @@ impl Decodable for Pitch { SineWave::new(self.frequency).take_duration(self.duration) } } - -/// Bundle for playing a bevy note sound -#[deprecated( - since = "0.15.0", - note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." -)] -pub type PitchBundle = AudioSourceBundle; diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index cb53472950161..324579cecb02f 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "bevy_color" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Types for representing and manipulating color values" homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "color"] -rust-version = "1.82.0" +rust-version = "1.83.0" [dependencies] -bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false, features = [ +bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ "curve", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", ], optional = true } bytemuck = { version = "1", features = ["derive"] } diff --git a/crates/bevy_color/src/color.rs b/crates/bevy_color/src/color.rs index d2e4cb792187c..29a1b4853cc77 100644 --- a/crates/bevy_color/src/color.rs +++ b/crates/bevy_color/src/color.rs @@ -84,12 +84,6 @@ impl Color { (*self).into() } - #[deprecated = "Use `Color::srgba` instead"] - /// Creates a new [`Color`] object storing a [`Srgba`] color. - pub const fn rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self { - Self::srgba(red, green, blue, alpha) - } - /// Creates a new [`Color`] object storing a [`Srgba`] color. pub const fn srgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self { Self::Srgba(Srgba { @@ -100,12 +94,6 @@ impl Color { }) } - #[deprecated = "Use `Color::srgb` instead"] - /// Creates a new [`Color`] object storing a [`Srgba`] color with an alpha of 1.0. - pub const fn rgb(red: f32, green: f32, blue: f32) -> Self { - Self::srgb(red, green, blue) - } - /// Creates a new [`Color`] object storing a [`Srgba`] color with an alpha of 1.0. pub const fn srgb(red: f32, green: f32, blue: f32) -> Self { Self::Srgba(Srgba { @@ -116,12 +104,6 @@ impl Color { }) } - #[deprecated = "Use `Color::srgb_from_array` instead"] - /// Reads an array of floats to creates a new [`Color`] object storing a [`Srgba`] color with an alpha of 1.0. - pub fn rgb_from_array([r, g, b]: [f32; 3]) -> Self { - Self::Srgba(Srgba::rgb(r, g, b)) - } - /// Reads an array of floats to creates a new [`Color`] object storing a [`Srgba`] color with an alpha of 1.0. pub const fn srgb_from_array(array: [f32; 3]) -> Self { Self::Srgba(Srgba { @@ -132,14 +114,6 @@ impl Color { }) } - #[deprecated = "Use `Color::srgba_u8` instead"] - /// Creates a new [`Color`] object storing a [`Srgba`] color from [`u8`] values. - /// - /// A value of 0 is interpreted as 0.0, and a value of 255 is interpreted as 1.0. - pub fn rgba_u8(red: u8, green: u8, blue: u8, alpha: u8) -> Self { - Self::srgba_u8(red, green, blue, alpha) - } - /// Creates a new [`Color`] object storing a [`Srgba`] color from [`u8`] values. /// /// A value of 0 is interpreted as 0.0, and a value of 255 is interpreted as 1.0. @@ -152,14 +126,6 @@ impl Color { }) } - #[deprecated = "Use `Color::srgb_u8` instead"] - /// Creates a new [`Color`] object storing a [`Srgba`] color from [`u8`] values with an alpha of 1.0. - /// - /// A value of 0 is interpreted as 0.0, and a value of 255 is interpreted as 1.0. - pub fn rgb_u8(red: u8, green: u8, blue: u8) -> Self { - Self::srgb_u8(red, green, blue) - } - /// Creates a new [`Color`] object storing a [`Srgba`] color from [`u8`] values with an alpha of 1.0. /// /// A value of 0 is interpreted as 0.0, and a value of 255 is interpreted as 1.0. @@ -172,12 +138,6 @@ impl Color { }) } - #[deprecated = "Use Color::linear_rgba instead."] - /// Creates a new [`Color`] object storing a [`LinearRgba`] color. - pub const fn rbga_linear(red: f32, green: f32, blue: f32, alpha: f32) -> Self { - Self::linear_rgba(red, green, blue, alpha) - } - /// Creates a new [`Color`] object storing a [`LinearRgba`] color. pub const fn linear_rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self { Self::LinearRgba(LinearRgba { @@ -188,12 +148,6 @@ impl Color { }) } - #[deprecated = "Use Color::linear_rgb instead."] - /// Creates a new [`Color`] object storing a [`LinearRgba`] color with an alpha of 1.0. - pub const fn rgb_linear(red: f32, green: f32, blue: f32) -> Self { - Self::linear_rgb(red, green, blue) - } - /// Creates a new [`Color`] object storing a [`LinearRgba`] color with an alpha of 1.0. pub const fn linear_rgb(red: f32, green: f32, blue: f32) -> Self { Self::LinearRgba(LinearRgba { diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index 759b33bf93e77..b087205bb6b49 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -76,6 +76,7 @@ where mod tests { use super::*; use crate::{palettes::basic, Srgba}; + use bevy_math::curve::{Curve, CurveExt}; #[test] fn test_color_curve() { diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs index 4a4a9596d545d..e1ee1fbe38cd0 100644 --- a/crates/bevy_color/src/lib.rs +++ b/crates/bevy_color/src/lib.rs @@ -4,7 +4,7 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] //! Representations of colors in various color spaces. //! @@ -90,6 +90,9 @@ //! println!("Hsla: {:?}", hsla); //! ``` +#[cfg(feature = "std")] +extern crate std; + #[cfg(feature = "alloc")] extern crate alloc; @@ -142,7 +145,14 @@ pub use srgba::*; pub use xyza::*; /// Describes the traits that a color should implement for consistency. -#[allow(dead_code)] // This is an internal marker trait used to ensure that our color types impl the required traits +#[expect( + clippy::allow_attributes, + reason = "If the below attribute on `dead_code` is removed, then rustc complains that `StandardColor` is dead code. However, if we `expect` the `dead_code` lint, then rustc complains of an unfulfilled expectation." +)] +#[allow( + dead_code, + reason = "This is an internal marker trait used to ensure that our color types impl the required traits" +)] pub(crate) trait StandardColor where Self: core::fmt::Debug, diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 0ffb35ddd33db..1281109d02a96 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -216,7 +216,6 @@ impl ColorToComponents for Oklaba { } } -#[allow(clippy::excessive_precision)] impl From for Oklaba { fn from(value: LinearRgba) -> Self { let LinearRgba { @@ -225,21 +224,21 @@ impl From for Oklaba { blue, alpha, } = value; - // From https://github.com/DougLau/pix - let l = 0.4122214708 * red + 0.5363325363 * green + 0.0514459929 * blue; - let m = 0.2119034982 * red + 0.6806995451 * green + 0.1073969566 * blue; - let s = 0.0883024619 * red + 0.2817188376 * green + 0.6299787005 * blue; + // From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab + // Float literals are truncated to avoid excessive precision. + let l = 0.41222146 * red + 0.53633255 * green + 0.051445995 * blue; + let m = 0.2119035 * red + 0.6806995 * green + 0.10739696 * blue; + let s = 0.08830246 * red + 0.28171885 * green + 0.6299787 * blue; let l_ = ops::cbrt(l); let m_ = ops::cbrt(m); let s_ = ops::cbrt(s); - let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; - let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; - let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; + let l = 0.21045426 * l_ + 0.7936178 * m_ - 0.004072047 * s_; + let a = 1.9779985 * l_ - 2.4285922 * m_ + 0.4505937 * s_; + let b = 0.025904037 * l_ + 0.78277177 * m_ - 0.80867577 * s_; Oklaba::new(l, a, b, alpha) } } -#[allow(clippy::excessive_precision)] impl From for LinearRgba { fn from(value: Oklaba) -> Self { let Oklaba { @@ -249,18 +248,19 @@ impl From for LinearRgba { alpha, } = value; - // From https://github.com/Ogeon/palette/blob/e75eab2fb21af579353f51f6229a510d0d50a311/palette/src/oklab.rs#L312-L332 - let l_ = lightness + 0.3963377774 * a + 0.2158037573 * b; - let m_ = lightness - 0.1055613458 * a - 0.0638541728 * b; - let s_ = lightness - 0.0894841775 * a - 1.2914855480 * b; + // From https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab + // Float literals are truncated to avoid excessive precision. + let l_ = lightness + 0.39633778 * a + 0.21580376 * b; + let m_ = lightness - 0.105561346 * a - 0.06385417 * b; + let s_ = lightness - 0.08948418 * a - 1.2914855 * b; let l = l_ * l_ * l_; let m = m_ * m_ * m_; let s = s_ * s_ * s_; - let red = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s; - let green = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s; - let blue = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s; + let red = 4.0767417 * l - 3.3077116 * m + 0.23096994 * s; + let green = -1.268438 * l + 2.6097574 * m - 0.34131938 * s; + let blue = -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s; Self { red, diff --git a/crates/bevy_color/src/palettes/css.rs b/crates/bevy_color/src/palettes/css.rs index 0c1e073bec4b6..9b0dd7fe7e18f 100644 --- a/crates/bevy_color/src/palettes/css.rs +++ b/crates/bevy_color/src/palettes/css.rs @@ -4,7 +4,6 @@ use crate::Srgba; // The CSS4 colors are a superset of the CSS1 colors, so we can just re-export the CSS1 colors. -#[allow(unused_imports)] pub use crate::palettes::basic::*; ///

diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 8f4549df3b563..49e40792b7d3e 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -141,17 +141,17 @@ impl Srgba { 3 => { let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes(); let (r, g, b) = (l & 0x0F, (b & 0xF0) >> 4, b & 0x0F); - Ok(Self::rgb_u8(r << 4 | r, g << 4 | g, b << 4 | b)) + Ok(Self::rgb_u8((r << 4) | r, (g << 4) | g, (b << 4) | b)) } // RGBA 4 => { let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes(); let (r, g, b, a) = ((l & 0xF0) >> 4, l & 0xF, (b & 0xF0) >> 4, b & 0x0F); Ok(Self::rgba_u8( - r << 4 | r, - g << 4 | g, - b << 4 | b, - a << 4 | a, + (r << 4) | r, + (g << 4) | g, + (b << 4) | b, + (a << 4) | a, )) } // RRGGBB diff --git a/crates/bevy_color/src/testing.rs b/crates/bevy_color/src/testing.rs index 0c87fe226c749..6c7747e2a540b 100644 --- a/crates/bevy_color/src/testing.rs +++ b/crates/bevy_color/src/testing.rs @@ -4,7 +4,7 @@ macro_rules! assert_approx_eq { if ($x - $y).abs() >= $d { panic!( "assertion failed: `(left !== right)` \ - (left: `{:?}`, right: `{:?}`, tolerance: `{:?}`)", + (left: `{}`, right: `{}`, tolerance: `{}`)", $x, $y, $d ); } diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 3993b031e9e70..0042b77180deb 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_core_pipeline" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" authors = [ "Bevy Contributors ", @@ -22,19 +22,19 @@ smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } +bevy_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" } serde = { version = "1", features = ["derive"] } bitflags = "2.3" @@ -42,6 +42,7 @@ radsort = "0.1" nonmax = "0.5" smallvec = "1" thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } [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 25ec27cee4df2..7a89de331c19c 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -136,7 +136,10 @@ impl AutoExposureCompensationCurve { let lut_inv_range = 1.0 / (lut_end - lut_begin); // Iterate over all LUT entries whose pixel centers fall within the current segment. - #[allow(clippy::needless_range_loop)] + #[expect( + clippy::needless_range_loop, + reason = "This for-loop also uses `i` to calculate a value `t`." + )] for i in lut_begin.ceil() as usize..=lut_end.floor() as usize { let t = (i as f32 - lut_begin) * lut_inv_range; lut[i] = previous.y.lerp(current.y, t); diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs index 59f314d12e1ab..f94a61d09be16 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/mod.rs @@ -24,8 +24,7 @@ use node::AutoExposureNode; use pipeline::{ AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline, METERING_SHADER_HANDLE, }; -#[allow(deprecated)] -pub use settings::{AutoExposure, AutoExposureSettings}; +pub use settings::AutoExposure; use crate::{ auto_exposure::compensation_curve::GpuAutoExposureCompensationCurve, diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs index 91bdf836eebee..b5039030ac969 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/settings.rs @@ -88,9 +88,6 @@ pub struct AutoExposure { pub compensation_curve: Handle, } -#[deprecated(since = "0.15.0", note = "Renamed to `AutoExposure`")] -pub type AutoExposureSettings = AutoExposure; - impl Default for AutoExposure { fn default() -> Self { Self { diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl index 0b10333192382..aa4a2f94c46a2 100644 --- a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -9,8 +9,8 @@ struct BloomUniforms { threshold_precomputations: vec4, viewport: vec4, + scale: vec2, aspect: f32, - uv_offset: f32 }; @group(0) @binding(0) var input_texture: texture_2d; @@ -51,6 +51,14 @@ fn karis_average(color: vec3) -> f32 { // [COD] slide 153 fn sample_input_13_tap(uv: vec2) -> vec3 { +#ifdef UNIFORM_SCALE + // This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be + // expressed with constant offsets. + // + // It's possible that this isn't meaningfully faster than the "slow" path. However, because it + // is hard to test performance on all platforms, and uniform bloom is the most common case, this + // path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero, + // cost to maintainability, but it does help me sleep at night. let a = textureSample(input_texture, s, uv, vec2(-2, 2)).rgb; let b = textureSample(input_texture, s, uv, vec2(0, 2)).rgb; let c = textureSample(input_texture, s, uv, vec2(2, 2)).rgb; @@ -64,6 +72,35 @@ fn sample_input_13_tap(uv: vec2) -> vec3 { let k = textureSample(input_texture, s, uv, vec2(1, 1)).rgb; let l = textureSample(input_texture, s, uv, vec2(-1, -1)).rgb; let m = textureSample(input_texture, s, uv, vec2(1, -1)).rgb; +#else + // This is the flexible, but potentially slower, path for non-uniform sampling. Because the + // sample is not a constant, and it can fall outside of the limits imposed on constant sample + // offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the + // texture. + // + // It isn't clear if this is meaningfully slower than using the offset syntax, the spec doesn't + // mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset + // syntax uses a const-expr implies that it allows some compiler optimizations - maybe more + // impactful on mobile? + let scale = uniforms.scale; + let ps = scale / vec2(textureDimensions(input_texture)); + let pl = 2.0 * ps; + let ns = -1.0 * ps; + let nl = -2.0 * ps; + let a = textureSample(input_texture, s, uv + vec2(nl.x, pl.y)).rgb; + let b = textureSample(input_texture, s, uv + vec2(0.00, pl.y)).rgb; + let c = textureSample(input_texture, s, uv + vec2(pl.x, pl.y)).rgb; + let d = textureSample(input_texture, s, uv + vec2(nl.x, 0.00)).rgb; + let e = textureSample(input_texture, s, uv).rgb; + let f = textureSample(input_texture, s, uv + vec2(pl.x, 0.00)).rgb; + let g = textureSample(input_texture, s, uv + vec2(nl.x, nl.y)).rgb; + let h = textureSample(input_texture, s, uv + vec2(0.00, nl.y)).rgb; + let i = textureSample(input_texture, s, uv + vec2(pl.x, nl.y)).rgb; + let j = textureSample(input_texture, s, uv + vec2(ns.x, ps.y)).rgb; + let k = textureSample(input_texture, s, uv + vec2(ps.x, ps.y)).rgb; + let l = textureSample(input_texture, s, uv + vec2(ns.x, ns.y)).rgb; + let m = textureSample(input_texture, s, uv + vec2(ps.x, ns.y)).rgb; +#endif #ifdef FIRST_DOWNSAMPLE // [COD] slide 168 @@ -95,9 +132,11 @@ fn sample_input_13_tap(uv: vec2) -> vec3 { // [COD] slide 162 fn sample_input_3x3_tent(uv: vec2) -> vec3 { - // UV offsets configured from uniforms. - let x = uniforms.uv_offset / uniforms.aspect; - let y = uniforms.uv_offset; + // While this is probably technically incorrect, it makes nonuniform bloom smoother, without + // having any impact on uniform bloom, which simply evaluates to 1.0 here. + let frag_size = uniforms.scale / vec2(textureDimensions(input_texture)); + let x = frag_size.x; + let y = frag_size.y; let a = textureSample(input_texture, s, vec2(uv.x - x, uv.y + y)).rgb; let b = textureSample(input_texture, s, vec2(uv.x, uv.y + y)).rgb; diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index e3efe5cad8946..642791568acc1 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; -use bevy_math::Vec4; +use bevy_math::{Vec2, Vec4}; use bevy_render::{ render_resource::{ binding_types::{sampler, texture_2d, uniform_buffer}, @@ -31,6 +31,7 @@ pub struct BloomDownsamplingPipeline { pub struct BloomDownsamplingPipelineKeys { prefilter: bool, first_downsample: bool, + uniform_scale: bool, } /// The uniform struct extracted from [`Bloom`] attached to a Camera. @@ -40,8 +41,8 @@ pub struct BloomUniforms { // Precomputed values used when thresholding, see https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4 pub threshold_precomputations: Vec4, pub viewport: Vec4, + pub scale: Vec2, pub aspect: f32, - pub uv_offset: f32, } impl FromWorld for BloomDownsamplingPipeline { @@ -102,6 +103,10 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { shader_defs.push("USE_THRESHOLD".into()); } + if key.uniform_scale { + shader_defs.push("UNIFORM_SCALE".into()); + } + RenderPipelineDescriptor { label: Some( if key.first_downsample { @@ -148,6 +153,7 @@ pub fn prepare_downsampling_pipeline( BloomDownsamplingPipelineKeys { prefilter, first_downsample: false, + uniform_scale: bloom.scale == Vec2::ONE, }, ); @@ -157,6 +163,7 @@ pub fn prepare_downsampling_pipeline( BloomDownsamplingPipelineKeys { prefilter, first_downsample: true, + uniform_scale: bloom.scale == Vec2::ONE, }, ); diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index bfd7ee22dbd10..8f242931d71cf 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -3,10 +3,7 @@ mod settings; mod upsampling_pipeline; use bevy_color::{Gray, LinearRgba}; -#[allow(deprecated)] -pub use settings::{ - Bloom, BloomCompositeMode, BloomPrefilter, BloomPrefilterSettings, BloomSettings, -}; +pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter}; use crate::{ core_2d::graph::{Core2d, Node2d}, diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index effa135677f3b..2e22875a35f12 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -1,6 +1,6 @@ use super::downsampling_pipeline::BloomUniforms; use bevy_ecs::{prelude::Component, query::QueryItem, reflect::ReflectComponent}; -use bevy_math::{AspectRatio, URect, UVec4, Vec4}; +use bevy_math::{AspectRatio, URect, UVec4, Vec2, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; @@ -113,17 +113,14 @@ pub struct Bloom { /// Only tweak if you are seeing visual artifacts. pub max_mip_dimension: u32, - /// UV offset for bloom shader. Ideally close to 2.0 / `max_mip_dimension`. - /// Only tweak if you are seeing visual artifacts. - pub uv_offset: f32, + /// Amount to stretch the bloom on each axis. Artistic control, can be used to emulate + /// anamorphic blur by using a large x-value. For large values, you may need to increase + /// [`Bloom::max_mip_dimension`] to reduce sampling artifacts. + pub scale: Vec2, } -#[deprecated(since = "0.15.0", note = "Renamed to `Bloom`")] -pub type BloomSettings = Bloom; - impl Bloom { const DEFAULT_MAX_MIP_DIMENSION: u32 = 512; - const DEFAULT_UV_OFFSET: f32 = 0.004; /// The default bloom preset. /// @@ -139,7 +136,15 @@ impl Bloom { }, composite_mode: BloomCompositeMode::EnergyConserving, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, + }; + + /// Emulates the look of stylized anamorphic bloom, stretched horizontally. + pub const ANAMORPHIC: Self = Self { + // The larger scale necessitates a larger resolution to reduce artifacts: + max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION * 2, + scale: Vec2::new(4.0, 1.0), + ..Self::NATURAL }; /// A preset that's similar to how older games did bloom. @@ -154,7 +159,7 @@ impl Bloom { }, composite_mode: BloomCompositeMode::Additive, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, }; /// A preset that applies a very strong bloom, and blurs the whole screen. @@ -169,7 +174,7 @@ impl Bloom { }, composite_mode: BloomCompositeMode::EnergyConserving, max_mip_dimension: Self::DEFAULT_MAX_MIP_DIMENSION, - uv_offset: Self::DEFAULT_UV_OFFSET, + scale: Vec2::ONE, }; } @@ -203,9 +208,6 @@ pub struct BloomPrefilter { pub threshold_softness: f32, } -#[deprecated(since = "0.15.0", note = "Renamed to `BloomPrefilter`")] -pub type BloomPrefilterSettings = BloomPrefilter; - #[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, Copy)] pub enum BloomCompositeMode { EnergyConserving, @@ -246,7 +248,7 @@ impl ExtractComponent for Bloom { aspect: AspectRatio::try_from_pixels(size.x, size.y) .expect("Valid screen size values for Bloom settings") .ratio(), - uv_offset: bloom.uv_offset, + scale: bloom.scale, }; Some((bloom.clone(), uniform)) diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs index fbc3ecfec3f75..7067058833c7c 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/mod.rs @@ -54,9 +54,6 @@ pub struct ContrastAdaptiveSharpening { pub denoise: bool, } -#[deprecated(since = "0.15.0", note = "Renamed to `ContrastAdaptiveSharpening`")] -pub type ContrastAdaptiveSharpeningSettings = ContrastAdaptiveSharpening; - impl Default for ContrastAdaptiveSharpening { fn default() -> Self { ContrastAdaptiveSharpening { diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 9f8073e3f51df..9780ddd31f63e 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -1,21 +1,13 @@ -#![expect(deprecated)] - use crate::{ core_2d::graph::Core2d, tonemapping::{DebandDither, Tonemapping}, }; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::sync_world::SyncToRenderWorld; use bevy_render::{ - camera::{ - Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, - OrthographicProjection, - }, + camera::{Camera, CameraProjection, CameraRenderGraph, OrthographicProjection, Projection}, extract_component::ExtractComponent, - prelude::Msaa, primitives::Frustum, - view::VisibleEntities, }; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -27,87 +19,8 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; Camera, DebandDither, CameraRenderGraph(|| CameraRenderGraph::new(Core2d)), - OrthographicProjection(OrthographicProjection::default_2d), + Projection(|| Projection::Orthographic(OrthographicProjection::default_2d())), Frustum(|| OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default()))), Tonemapping(|| Tonemapping::None), )] pub struct Camera2d; - -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `Camera2d` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct Camera2dBundle { - pub camera: Camera, - pub camera_render_graph: CameraRenderGraph, - pub projection: OrthographicProjection, - pub visible_entities: VisibleEntities, - pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, - pub camera_2d: Camera2d, - pub tonemapping: Tonemapping, - pub deband_dither: DebandDither, - pub main_texture_usages: CameraMainTextureUsages, - pub msaa: Msaa, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} - -impl Default for Camera2dBundle { - fn default() -> Self { - let projection = OrthographicProjection::default_2d(); - let transform = Transform::default(); - let frustum = projection.compute_frustum(&GlobalTransform::from(transform)); - Self { - camera_render_graph: CameraRenderGraph::new(Core2d), - projection, - visible_entities: VisibleEntities::default(), - frustum, - transform, - global_transform: Default::default(), - camera: Camera::default(), - camera_2d: Camera2d, - tonemapping: Tonemapping::None, - deband_dither: DebandDither::Disabled, - main_texture_usages: Default::default(), - msaa: Default::default(), - sync: Default::default(), - } - } -} - -impl Camera2dBundle { - /// Create an orthographic projection camera with a custom `Z` position. - /// - /// The camera is placed at `Z=far-0.1`, looking toward the world origin `(0,0,0)`. - /// Its orthographic projection extends from `0.0` to `-far` in camera view space, - /// corresponding to `Z=far-0.1` (closest to camera) to `Z=-0.1` (furthest away from - /// camera) in world space. - pub fn new_with_far(far: f32) -> Self { - // we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset - // the camera's translation by far and use a right handed coordinate system - let projection = OrthographicProjection { - far, - ..OrthographicProjection::default_2d() - }; - let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); - let frustum = projection.compute_frustum(&GlobalTransform::from(transform)); - Self { - camera_render_graph: CameraRenderGraph::new(Core2d), - projection, - visible_entities: VisibleEntities::default(), - frustum, - transform, - global_transform: Default::default(), - camera: Camera::default(), - camera_2d: Camera2d, - tonemapping: Tonemapping::None, - deband_dither: DebandDither::Disabled, - main_texture_usages: Default::default(), - msaa: Default::default(), - sync: Default::default(), - } - } -} 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 91093d0da5c94..60f355c1153db 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 @@ -7,11 +7,11 @@ use bevy_render::{ render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewTarget}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; use super::AlphaMask2d; @@ -22,6 +22,7 @@ pub struct MainOpaquePass2dNode; impl ViewNode for MainOpaquePass2dNode { type ViewQuery = ( &'static ExtractedCamera, + &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, ); @@ -30,7 +31,7 @@ impl ViewNode for MainOpaquePass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, 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)) = ( @@ -47,8 +48,8 @@ impl ViewNode for MainOpaquePass2dNode { let view_entity = graph.view_entity(); let (Some(opaque_phase), Some(alpha_mask_phase)) = ( - opaque_phases.get(&view_entity), - alpha_mask_phases.get(&view_entity), + opaque_phases.get(&view.retained_view_entity), + alpha_mask_phases.get(&view.retained_view_entity), ) else { return Ok(()); }; 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 e365be954775b..bce42cead700e 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 @@ -7,11 +7,11 @@ use bevy_render::{ render_phase::ViewSortedRenderPhases, render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewTarget}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; #[derive(Default)] pub struct MainTransparentPass2dNode {} @@ -19,6 +19,7 @@ pub struct MainTransparentPass2dNode {} impl ViewNode for MainTransparentPass2dNode { type ViewQuery = ( &'static ExtractedCamera, + &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, ); @@ -27,7 +28,7 @@ impl ViewNode for MainTransparentPass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, 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) = @@ -37,7 +38,7 @@ impl ViewNode for MainTransparentPass2dNode { }; let view_entity = graph.view_entity(); - let Some(transparent_phase) = transparent_phases.get(&view_entity) else { + let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else { return Ok(()); }; diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index d57134aa3ec07..ec0fa58d73f60 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -34,16 +34,18 @@ use core::ops::Range; use bevy_asset::UntypedAssetId; use bevy_render::{ - batching::gpu_preprocessing::GpuPreprocessingMode, render_phase::PhaseItemBinKey, + batching::gpu_preprocessing::GpuPreprocessingMode, + render_phase::PhaseItemBatchSetKey, + view::{ExtractedView, RetainedViewEntity}, }; -use bevy_utils::HashMap; +use bevy_utils::{HashMap, HashSet}; 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 bevy_app::{App, Plugin}; -use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_ecs::prelude::*; use bevy_math::FloatOrd; use bevy_render::{ camera::{Camera, ExtractedCamera}, @@ -59,7 +61,7 @@ use bevy_render::{ TextureFormat, TextureUsages, }, renderer::RenderDevice, - sync_world::{MainEntity, RenderEntity}, + sync_world::MainEntity, texture::TextureCache, view::{Msaa, ViewDepthTexture}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, @@ -127,8 +129,13 @@ impl Plugin for Core2dPlugin { /// Opaque 2D [`BinnedPhaseItem`]s. pub struct Opaque2d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: BatchSetKey2d, /// The key, which determines which can be batched. - pub key: Opaque2dBinKey, + pub bin_key: Opaque2dBinKey, /// An entity from which data will be fetched, including the mesh if /// applicable. pub representative_entity: (Entity, MainEntity), @@ -155,14 +162,6 @@ pub struct Opaque2dBinKey { pub material_bind_group_id: Option, } -impl PhaseItemBinKey for Opaque2dBinKey { - type BatchSetKey = (); - - fn get_batch_set_key(&self) -> Option { - None - } -} - impl PhaseItem for Opaque2d { #[inline] fn entity(&self) -> Entity { @@ -175,7 +174,7 @@ impl PhaseItem for Opaque2d { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.draw_function + self.bin_key.draw_function } #[inline] @@ -198,16 +197,22 @@ impl PhaseItem for Opaque2d { } impl BinnedPhaseItem for Opaque2d { + // Since 2D meshes presently can't be multidrawn, the batch set key is + // irrelevant. + type BatchSetKey = BatchSetKey2d; + type BinKey = Opaque2dBinKey; fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Opaque2d { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -215,17 +220,36 @@ impl BinnedPhaseItem for Opaque2d { } } +/// 2D meshes aren't currently multi-drawn together, so this batch set key only +/// stores whether the mesh is indexed. +#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] +pub struct BatchSetKey2d { + /// True if the mesh is indexed. + pub indexed: bool, +} + +impl PhaseItemBatchSetKey for BatchSetKey2d { + fn indexed(&self) -> bool { + self.indexed + } +} + impl CachedRenderPipelinePhaseItem for Opaque2d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.pipeline + self.bin_key.pipeline } } /// Alpha mask 2D [`BinnedPhaseItem`]s. pub struct AlphaMask2d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: BatchSetKey2d, /// The key, which determines which can be batched. - pub key: AlphaMask2dBinKey, + pub bin_key: AlphaMask2dBinKey, /// An entity from which data will be fetched, including the mesh if /// applicable. pub representative_entity: (Entity, MainEntity), @@ -265,7 +289,7 @@ impl PhaseItem for AlphaMask2d { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.draw_function + self.bin_key.draw_function } #[inline] @@ -288,16 +312,20 @@ impl PhaseItem for AlphaMask2d { } impl BinnedPhaseItem for AlphaMask2d { + type BatchSetKey = BatchSetKey2d; + type BinKey = AlphaMask2dBinKey; fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { AlphaMask2d { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -305,18 +333,10 @@ impl BinnedPhaseItem for AlphaMask2d { } } -impl PhaseItemBinKey for AlphaMask2dBinKey { - type BatchSetKey = (); - - fn get_batch_set_key(&self) -> Option { - None - } -} - impl CachedRenderPipelinePhaseItem for AlphaMask2d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.pipeline + self.bin_key.pipeline } } @@ -328,6 +348,9 @@ pub struct Transparent2d { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, } impl PhaseItem for Transparent2d { @@ -380,6 +403,10 @@ impl SortedPhaseItem for Transparent2d { // radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. radsort::sort_by_key(items, |item| item.sort_key().0); } + + fn indexed(&self) -> bool { + self.indexed + } } impl CachedRenderPipelinePhaseItem for Transparent2d { @@ -393,20 +420,24 @@ pub fn extract_core_2d_camera_phases( mut transparent_2d_phases: ResMut>, mut opaque_2d_phases: ResMut>, mut alpha_mask_2d_phases: ResMut>, - cameras_2d: Extract>>, - mut live_entities: Local, + cameras_2d: Extract>>, + mut live_entities: Local>, ) { live_entities.clear(); - for (entity, camera) in &cameras_2d { + for (main_entity, camera) in &cameras_2d { if !camera.is_active { continue; } - transparent_2d_phases.insert_or_clear(entity); - opaque_2d_phases.insert_or_clear(entity, GpuPreprocessingMode::None); - alpha_mask_2d_phases.insert_or_clear(entity, GpuPreprocessingMode::None); - live_entities.insert(entity); + // This is the main 2D camera, so we use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + + transparent_2d_phases.insert_or_clear(retained_view_entity); + opaque_2d_phases.insert_or_clear(retained_view_entity, GpuPreprocessingMode::None); + alpha_mask_2d_phases.insert_or_clear(retained_view_entity, GpuPreprocessingMode::None); + + live_entities.insert(retained_view_entity); } // Clear out all dead views. @@ -421,11 +452,13 @@ pub fn prepare_core_2d_depth_textures( render_device: Res, transparent_2d_phases: Res>, opaque_2d_phases: Res>, - views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With,)>, + views_2d: Query<(Entity, &ExtractedCamera, &ExtractedView, &Msaa), (With,)>, ) { let mut textures = >::default(); - for (view, camera, msaa) in &views_2d { - if !opaque_2d_phases.contains_key(&view) || !transparent_2d_phases.contains_key(&view) { + for (view, camera, extracted_view, msaa) in &views_2d { + if !opaque_2d_phases.contains_key(&extracted_view.retained_view_entity) + || !transparent_2d_phases.contains_key(&extracted_view.retained_view_entity) + { continue; }; diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 2053b96882817..418d3b8d482a3 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - use crate::{ core_3d::graph::Core3d, tonemapping::{DebandDither, Tonemapping}, @@ -7,14 +5,11 @@ use crate::{ use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render::{ - camera::{Camera, CameraMainTextureUsages, CameraRenderGraph, Exposure, Projection}, + camera::{Camera, CameraRenderGraph, Exposure, Projection}, extract_component::ExtractComponent, - primitives::Frustum, render_resource::{LoadOp, TextureUsages}, - sync_world::SyncToRenderWorld, - view::{ColorGrading, Msaa, VisibleEntities}, + view::ColorGrading, }; -use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; /// A 3D camera component. Enables the main 3D render graph for a [`Camera`]. @@ -147,52 +142,3 @@ pub enum ScreenSpaceTransmissionQuality { /// `num_taps` = 32 Ultra, } - -/// The camera coordinate space is right-handed x-right, y-up, z-back. -/// This means "forward" is -Z. -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `Camera3d` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct Camera3dBundle { - pub camera: Camera, - pub camera_render_graph: CameraRenderGraph, - pub projection: Projection, - pub visible_entities: VisibleEntities, - pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, - pub camera_3d: Camera3d, - pub tonemapping: Tonemapping, - pub deband_dither: DebandDither, - pub color_grading: ColorGrading, - pub exposure: Exposure, - pub main_texture_usages: CameraMainTextureUsages, - pub msaa: Msaa, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} - -// NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference -impl Default for Camera3dBundle { - fn default() -> Self { - Self { - camera_render_graph: CameraRenderGraph::new(Core3d), - camera: Default::default(), - projection: Default::default(), - visible_entities: Default::default(), - frustum: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - camera_3d: Default::default(), - tonemapping: Default::default(), - color_grading: Default::default(), - exposure: Default::default(), - main_texture_usages: Default::default(), - deband_dither: DebandDither::Enabled, - msaa: Default::default(), - sync: Default::default(), - } - } -} 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 b51f36354340a..3b1bc96c9014d 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 @@ -2,7 +2,7 @@ use crate::{ core_3d::Opaque3d, skybox::{SkyboxBindGroup, SkyboxPipelineId}, }; -use bevy_ecs::{entity::Entity, prelude::World, query::QueryItem}; +use bevy_ecs::{prelude::World, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, @@ -10,11 +10,11 @@ use bevy_render::{ render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewTarget, ViewUniformOffset}, + view::{ExtractedView, ViewDepthTexture, ViewTarget, ViewUniformOffset}, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; use super::AlphaMask3d; @@ -24,8 +24,8 @@ use super::AlphaMask3d; pub struct MainOpaquePass3dNode; impl ViewNode for MainOpaquePass3dNode { type ViewQuery = ( - Entity, &'static ExtractedCamera, + &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, Option<&'static SkyboxPipelineId>, @@ -38,8 +38,8 @@ impl ViewNode for MainOpaquePass3dNode { graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, ( - view, camera, + extracted_view, target, depth, skybox_pipeline, @@ -55,9 +55,10 @@ impl ViewNode for MainOpaquePass3dNode { return Ok(()); }; - let (Some(opaque_phase), Some(alpha_mask_phase)) = - (opaque_phases.get(&view), alpha_mask_phases.get(&view)) - else { + let (Some(opaque_phase), Some(alpha_mask_phase)) = ( + opaque_phases.get(&extracted_view.retained_view_entity), + alpha_mask_phases.get(&extracted_view.retained_view_entity), + ) else { return Ok(()); }; 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 225ce81da6c3a..0a2e98f0bf9ac 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 @@ -7,12 +7,12 @@ use bevy_render::{ render_phase::ViewSortedRenderPhases, render_resource::{Extent3d, RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewTarget}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; -use bevy_utils::tracing::error; -#[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; use core::ops::Range; +use tracing::error; +#[cfg(feature = "trace")] +use tracing::info_span; /// A [`bevy_render::render_graph::Node`] that runs the [`Transmissive3d`] /// [`ViewSortedRenderPhases`]. @@ -22,6 +22,7 @@ pub struct MainTransmissivePass3dNode; impl ViewNode for MainTransmissivePass3dNode { type ViewQuery = ( &'static ExtractedCamera, + &'static ExtractedView, &'static Camera3d, &'static ViewTarget, Option<&'static ViewTransmissionTexture>, @@ -32,7 +33,7 @@ impl ViewNode for MainTransmissivePass3dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, camera_3d, target, transmission, depth): QueryItem, + (camera, view, camera_3d, target, transmission, depth): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -43,7 +44,7 @@ impl ViewNode for MainTransmissivePass3dNode { return Ok(()); }; - let Some(transmissive_phase) = transmissive_phases.get(&view_entity) else { + let Some(transmissive_phase) = transmissive_phases.get(&view.retained_view_entity) else { return Ok(()); }; 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 4f0d3d0722f0e..36fe8417c4de2 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 @@ -7,11 +7,11 @@ use bevy_render::{ render_phase::ViewSortedRenderPhases, render_resource::{RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewTarget}, + view::{ExtractedView, ViewDepthTexture, ViewTarget}, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; /// A [`bevy_render::render_graph::Node`] that runs the [`Transparent3d`] /// [`ViewSortedRenderPhases`]. @@ -21,6 +21,7 @@ pub struct MainTransparentPass3dNode; impl ViewNode for MainTransparentPass3dNode { type ViewQuery = ( &'static ExtractedCamera, + &'static ExtractedView, &'static ViewTarget, &'static ViewDepthTexture, ); @@ -28,7 +29,7 @@ impl ViewNode for MainTransparentPass3dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext, - (camera, target, depth): QueryItem, + (camera, view, target, depth): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); @@ -39,7 +40,7 @@ impl ViewNode for MainTransparentPass3dNode { return Ok(()); }; - let Some(transparent_phase) = transparent_phases.get(&view_entity) else { + let Some(transparent_phase) = transparent_phases.get(&view.retained_view_entity) else { return Ok(()); }; diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index f70ad1391473f..393508047a017 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -68,8 +68,8 @@ use core::ops::Range; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, mesh::allocator::SlabId, - render_phase::PhaseItemBinKey, - view::NoIndirectDrawing, + render_phase::PhaseItemBatchSetKey, + view::{NoIndirectDrawing, RetainedViewEntity}, }; pub use camera_3d::*; pub use main_opaque_pass_3d_node::*; @@ -78,7 +78,7 @@ pub use main_transparent_pass_3d_node::*; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::UntypedAssetId; use bevy_color::LinearRgba; -use bevy_ecs::{entity::EntityHashSet, prelude::*}; +use bevy_ecs::prelude::*; use bevy_image::BevyDefault; use bevy_math::FloatOrd; use bevy_render::{ @@ -101,8 +101,9 @@ use bevy_render::{ view::{ExtractedView, ViewDepthTexture, ViewTarget}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{tracing::warn, HashMap}; +use bevy_utils::{HashMap, HashSet}; use nonmax::NonMaxU32; +use tracing::warn; use crate::{ core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode, @@ -114,8 +115,8 @@ use crate::{ dof::DepthOfFieldNode, prepass::{ node::PrepassNode, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, - NormalPrepass, Opaque3dPrepass, OpaqueNoLightmap3dBinKey, ViewPrepassTextures, - MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, + NormalPrepass, Opaque3dPrepass, OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey, + ViewPrepassTextures, MOTION_VECTOR_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, }, skybox::SkyboxPlugin, tonemapping::TonemappingNode, @@ -218,8 +219,13 @@ impl Plugin for Core3dPlugin { /// Opaque 3D [`BinnedPhaseItem`]s. pub struct Opaque3d { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: Opaque3dBatchSetKey, /// The key, which determines which can be batched. - pub key: Opaque3dBinKey, + pub bin_key: Opaque3dBinKey, /// An entity from which data will be fetched, including the mesh if /// applicable. pub representative_entity: (Entity, MainEntity), @@ -264,17 +270,17 @@ pub struct Opaque3dBatchSetKey { pub lightmap_slab: Option, } +impl PhaseItemBatchSetKey for Opaque3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + /// Data that must be identical in order to *batch* phase items together. /// /// Note that a *batch set* (if multi-draw is in use) contains multiple batches. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Opaque3dBinKey { - /// The key of the *batch set*. - /// - /// As batches belong to a batch set, meshes in a batch must obviously be - /// able to be placed in a single batch set. - pub batch_set_key: Opaque3dBatchSetKey, - /// The asset that this phase item is associated with. /// /// Normally, this is the ID of the mesh, but for non-mesh items it might be @@ -282,14 +288,6 @@ pub struct Opaque3dBinKey { pub asset_id: UntypedAssetId, } -impl PhaseItemBinKey for Opaque3dBinKey { - type BatchSetKey = Opaque3dBatchSetKey; - - fn get_batch_set_key(&self) -> Option { - Some(self.batch_set_key.clone()) - } -} - impl PhaseItem for Opaque3d { #[inline] fn entity(&self) -> Entity { @@ -303,7 +301,7 @@ impl PhaseItem for Opaque3d { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -326,17 +324,20 @@ impl PhaseItem for Opaque3d { } impl BinnedPhaseItem for Opaque3d { + type BatchSetKey = Opaque3dBatchSetKey; type BinKey = Opaque3dBinKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Opaque3d { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -347,12 +348,18 @@ impl BinnedPhaseItem for Opaque3d { impl CachedRenderPipelinePhaseItem for Opaque3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } pub struct AlphaMask3d { - pub key: OpaqueNoLightmap3dBinKey, + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// The key, which determines which can be batched. + pub bin_key: OpaqueNoLightmap3dBinKey, pub representative_entity: (Entity, MainEntity), pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, @@ -370,7 +377,7 @@ impl PhaseItem for AlphaMask3d { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -396,16 +403,19 @@ impl PhaseItem for AlphaMask3d { impl BinnedPhaseItem for AlphaMask3d { type BinKey = OpaqueNoLightmap3dBinKey; + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Self { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -416,7 +426,7 @@ impl BinnedPhaseItem for AlphaMask3d { impl CachedRenderPipelinePhaseItem for AlphaMask3d { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } @@ -427,6 +437,9 @@ pub struct Transmissive3d { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, } impl PhaseItem for Transmissive3d { @@ -490,6 +503,11 @@ impl SortedPhaseItem for Transmissive3d { fn sort(items: &mut [Self]) { radsort::sort_by_key(items, |item| item.distance); } + + #[inline] + fn indexed(&self) -> bool { + self.indexed + } } impl CachedRenderPipelinePhaseItem for Transmissive3d { @@ -506,6 +524,9 @@ pub struct Transparent3d { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, + /// Whether the mesh in question is indexed (uses an index buffer in + /// addition to its vertex buffer). + pub indexed: bool, } impl PhaseItem for Transparent3d { @@ -557,6 +578,11 @@ impl SortedPhaseItem for Transparent3d { fn sort(items: &mut [Self]) { radsort::sort_by_key(items, |item| item.distance); } + + #[inline] + fn indexed(&self) -> bool { + self.indexed + } } impl CachedRenderPipelinePhaseItem for Transparent3d { @@ -571,13 +597,13 @@ pub fn extract_core_3d_camera_phases( mut alpha_mask_3d_phases: ResMut>, mut transmissive_3d_phases: ResMut>, mut transparent_3d_phases: ResMut>, - cameras_3d: Extract), With>>, - mut live_entities: Local, + cameras_3d: Extract), With>>, + mut live_entities: Local>, gpu_preprocessing_support: Res, ) { live_entities.clear(); - for (entity, camera, no_indirect_drawing) in &cameras_3d { + for (main_entity, camera, no_indirect_drawing) in &cameras_3d { if !camera.is_active { continue; } @@ -590,23 +616,25 @@ pub fn extract_core_3d_camera_phases( GpuPreprocessingMode::PreprocessingOnly }); - opaque_3d_phases.insert_or_clear(entity, gpu_preprocessing_mode); - alpha_mask_3d_phases.insert_or_clear(entity, gpu_preprocessing_mode); - transmissive_3d_phases.insert_or_clear(entity); - transparent_3d_phases.insert_or_clear(entity); + // This is the main 3D camera, so use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + + opaque_3d_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + transmissive_3d_phases.insert_or_clear(retained_view_entity); + transparent_3d_phases.insert_or_clear(retained_view_entity); - live_entities.insert(entity); + live_entities.insert(retained_view_entity); } - opaque_3d_phases.retain(|entity, _| live_entities.contains(entity)); - alpha_mask_3d_phases.retain(|entity, _| live_entities.contains(entity)); - transmissive_3d_phases.retain(|entity, _| live_entities.contains(entity)); - transparent_3d_phases.retain(|entity, _| live_entities.contains(entity)); + opaque_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + transmissive_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + transparent_3d_phases.retain(|view_entity, _| live_entities.contains(view_entity)); } // Extract the render phases for the prepass -#[allow(clippy::too_many_arguments)] pub fn extract_camera_prepass_phase( mut commands: Commands, mut opaque_3d_prepass_phases: ResMut>, @@ -616,6 +644,7 @@ pub fn extract_camera_prepass_phase( cameras_3d: Extract< Query< ( + Entity, RenderEntity, &Camera, Has, @@ -627,12 +656,13 @@ pub fn extract_camera_prepass_phase( With, >, >, - mut live_entities: Local, + mut live_entities: Local>, gpu_preprocessing_support: Res, ) { live_entities.clear(); for ( + main_entity, entity, camera, no_indirect_drawing, @@ -654,22 +684,27 @@ pub fn extract_camera_prepass_phase( GpuPreprocessingMode::PreprocessingOnly }); + // This is the main 3D camera, so we use the first subview index (0). + let retained_view_entity = RetainedViewEntity::new(main_entity.into(), None, 0); + if depth_prepass || normal_prepass || motion_vector_prepass { - opaque_3d_prepass_phases.insert_or_clear(entity, gpu_preprocessing_mode); - alpha_mask_3d_prepass_phases.insert_or_clear(entity, gpu_preprocessing_mode); + opaque_3d_prepass_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_prepass_phases + .insert_or_clear(retained_view_entity, gpu_preprocessing_mode); } else { - opaque_3d_prepass_phases.remove(&entity); - alpha_mask_3d_prepass_phases.remove(&entity); + opaque_3d_prepass_phases.remove(&retained_view_entity); + alpha_mask_3d_prepass_phases.remove(&retained_view_entity); } if deferred_prepass { - opaque_3d_deferred_phases.insert_or_clear(entity, gpu_preprocessing_mode); - alpha_mask_3d_deferred_phases.insert_or_clear(entity, gpu_preprocessing_mode); + opaque_3d_deferred_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + alpha_mask_3d_deferred_phases + .insert_or_clear(retained_view_entity, gpu_preprocessing_mode); } else { - opaque_3d_deferred_phases.remove(&entity); - alpha_mask_3d_deferred_phases.remove(&entity); + opaque_3d_deferred_phases.remove(&retained_view_entity); + alpha_mask_3d_deferred_phases.remove(&retained_view_entity); } - live_entities.insert(entity); + live_entities.insert(retained_view_entity); commands .get_entity(entity) @@ -680,13 +715,12 @@ pub fn extract_camera_prepass_phase( .insert_if(DeferredPrepass, || deferred_prepass); } - opaque_3d_prepass_phases.retain(|entity, _| live_entities.contains(entity)); - alpha_mask_3d_prepass_phases.retain(|entity, _| live_entities.contains(entity)); - opaque_3d_deferred_phases.retain(|entity, _| live_entities.contains(entity)); - alpha_mask_3d_deferred_phases.retain(|entity, _| live_entities.contains(entity)); + opaque_3d_prepass_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_prepass_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + opaque_3d_deferred_phases.retain(|view_entity, _| live_entities.contains(view_entity)); + alpha_mask_3d_deferred_phases.retain(|view_entity, _| live_entities.contains(view_entity)); } -#[allow(clippy::too_many_arguments)] pub fn prepare_core_3d_depth_textures( mut commands: Commands, mut texture_cache: ResMut, @@ -698,17 +732,18 @@ pub fn prepare_core_3d_depth_textures( views_3d: Query<( Entity, &ExtractedCamera, + &ExtractedView, Option<&DepthPrepass>, &Camera3d, &Msaa, )>, ) { let mut render_target_usage = >::default(); - for (view, camera, depth_prepass, camera_3d, _msaa) in &views_3d { - if !opaque_3d_phases.contains_key(&view) - || !alpha_mask_3d_phases.contains_key(&view) - || !transmissive_3d_phases.contains_key(&view) - || !transparent_3d_phases.contains_key(&view) + for (_, camera, extracted_view, depth_prepass, camera_3d, _msaa) in &views_3d { + if !opaque_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !alpha_mask_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !transmissive_3d_phases.contains_key(&extracted_view.retained_view_entity) + || !transparent_3d_phases.contains_key(&extracted_view.retained_view_entity) { continue; }; @@ -726,7 +761,7 @@ pub fn prepare_core_3d_depth_textures( } let mut textures = >::default(); - for (entity, camera, _, camera_3d, msaa) in &views_3d { + for (entity, camera, _, _, camera_3d, msaa) in &views_3d { let Some(physical_target_size) = camera.physical_target_size else { continue; }; @@ -777,7 +812,6 @@ pub struct ViewTransmissionTexture { pub sampler: Sampler, } -#[allow(clippy::too_many_arguments)] pub fn prepare_core_3d_transmission_textures( mut commands: Commands, mut texture_cache: ResMut, @@ -790,14 +824,15 @@ pub fn prepare_core_3d_transmission_textures( ) { let mut textures = >::default(); for (entity, camera, camera_3d, view) in &views_3d { - if !opaque_3d_phases.contains_key(&entity) - || !alpha_mask_3d_phases.contains_key(&entity) - || !transparent_3d_phases.contains_key(&entity) + if !opaque_3d_phases.contains_key(&view.retained_view_entity) + || !alpha_mask_3d_phases.contains_key(&view.retained_view_entity) + || !transparent_3d_phases.contains_key(&view.retained_view_entity) { continue; }; - let Some(transmissive_3d_phase) = transmissive_3d_phases.get(&entity) else { + let Some(transmissive_3d_phase) = transmissive_3d_phases.get(&view.retained_view_entity) + else { continue; }; @@ -877,7 +912,6 @@ pub fn check_msaa(mut deferred_views: Query<&mut Msaa, (With, With, @@ -889,6 +923,7 @@ pub fn prepare_prepass_textures( views_3d: Query<( Entity, &ExtractedCamera, + &ExtractedView, &Msaa, Has, Has, @@ -904,6 +939,7 @@ pub fn prepare_prepass_textures( for ( entity, camera, + view, msaa, depth_prepass, normal_prepass, @@ -911,10 +947,10 @@ pub fn prepare_prepass_textures( deferred_prepass, ) in &views_3d { - if !opaque_3d_prepass_phases.contains_key(&entity) - && !alpha_mask_3d_prepass_phases.contains_key(&entity) - && !opaque_3d_deferred_phases.contains_key(&entity) - && !alpha_mask_3d_deferred_phases.contains_key(&entity) + if !opaque_3d_prepass_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_3d_prepass_phases.contains_key(&view.retained_view_entity) + && !opaque_3d_deferred_phases.contains_key(&view.retained_view_entity) + && !alpha_mask_3d_deferred_phases.contains_key(&view.retained_view_entity) { continue; }; diff --git a/crates/bevy_core_pipeline/src/deferred/mod.rs b/crates/bevy_core_pipeline/src/deferred/mod.rs index 1ddc66a285c20..b9f5169b48f32 100644 --- a/crates/bevy_core_pipeline/src/deferred/mod.rs +++ b/crates/bevy_core_pipeline/src/deferred/mod.rs @@ -3,7 +3,7 @@ pub mod node; use core::ops::Range; -use crate::prepass::OpaqueNoLightmap3dBinKey; +use crate::prepass::{OpaqueNoLightmap3dBatchSetKey, OpaqueNoLightmap3dBinKey}; use bevy_ecs::prelude::*; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +25,13 @@ pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat: /// Used to render all 3D meshes with materials that have no transparency. #[derive(PartialEq, Eq, Hash)] pub struct Opaque3dDeferred { - pub key: OpaqueNoLightmap3dBinKey, + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, pub representative_entity: (Entity, MainEntity), pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, @@ -43,7 +49,7 @@ impl PhaseItem for Opaque3dDeferred { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -68,17 +74,20 @@ impl PhaseItem for Opaque3dDeferred { } impl BinnedPhaseItem for Opaque3dDeferred { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; type BinKey = OpaqueNoLightmap3dBinKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Self { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -89,7 +98,7 @@ impl BinnedPhaseItem for Opaque3dDeferred { impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } @@ -99,7 +108,13 @@ impl CachedRenderPipelinePhaseItem for Opaque3dDeferred { /// /// Used to render all meshes with a material with an alpha mask. pub struct AlphaMask3dDeferred { - pub key: OpaqueNoLightmap3dBinKey, + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, pub representative_entity: (Entity, MainEntity), pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, @@ -118,7 +133,7 @@ impl PhaseItem for AlphaMask3dDeferred { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -143,16 +158,19 @@ impl PhaseItem for AlphaMask3dDeferred { } impl BinnedPhaseItem for AlphaMask3dDeferred { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; type BinKey = OpaqueNoLightmap3dBinKey; fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Self { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -163,6 +181,6 @@ impl BinnedPhaseItem for AlphaMask3dDeferred { impl CachedRenderPipelinePhaseItem for AlphaMask3dDeferred { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index 5aa89a8e94a0d..5485e8fc0034d 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -1,6 +1,7 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::render_graph::ViewNode; +use bevy_render::view::ExtractedView; use bevy_render::{ camera::ExtractedCamera, render_graph::{NodeRunError, RenderGraphContext}, @@ -9,9 +10,9 @@ use bevy_render::{ renderer::RenderContext, view::ViewDepthTexture, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; use crate::prepass::ViewPrepassTextures; @@ -25,8 +26,8 @@ pub struct DeferredGBufferPrepassNode; impl ViewNode for DeferredGBufferPrepassNode { type ViewQuery = ( - Entity, &'static ExtractedCamera, + &'static ExtractedView, &'static ViewDepthTexture, &'static ViewPrepassTextures, ); @@ -35,7 +36,10 @@ impl ViewNode for DeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (view, camera, view_depth_texture, view_prepass_textures): QueryItem<'w, Self::ViewQuery>, + (camera, extracted_view, view_depth_texture, view_prepass_textures): QueryItem< + 'w, + Self::ViewQuery, + >, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = ( @@ -46,8 +50,8 @@ impl ViewNode for DeferredGBufferPrepassNode { }; let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = ( - opaque_deferred_phases.get(&view), - alpha_mask_deferred_phases.get(&view), + opaque_deferred_phases.get(&extracted_view.retained_view_entity), + alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity), ) else { return Ok(()); }; @@ -143,7 +147,8 @@ impl ViewNode for DeferredGBufferPrepassNode { } // Opaque draws - if !opaque_deferred_phase.batchable_mesh_keys.is_empty() + if !opaque_deferred_phase.multidrawable_mesh_keys.is_empty() + || !opaque_deferred_phase.batchable_mesh_keys.is_empty() || !opaque_deferred_phase.unbatchable_mesh_keys.is_empty() { #[cfg(feature = "trace")] diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 06cbbe3e9d312..ca5eac082b4fa 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -56,8 +56,9 @@ use bevy_render::{ }, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{info_once, prelude::default, warn_once}; +use bevy_utils::{default, once}; use smallvec::SmallVec; +use tracing::{info, warn}; use crate::{ core_3d::{ @@ -119,9 +120,6 @@ pub struct DepthOfField { pub max_depth: f32, } -#[deprecated(since = "0.15.0", note = "Renamed to `DepthOfField`")] -pub type DepthOfFieldSettings = DepthOfField; - /// Controls the appearance of the effect. #[derive(Clone, Copy, Default, PartialEq, Debug, Reflect)] #[reflect(Default, PartialEq)] @@ -384,7 +382,9 @@ impl ViewNode for DepthOfFieldNode { auxiliary_dof_texture, view_bind_group_layouts.dual_input.as_ref(), ) else { - warn_once!("Should have created the auxiliary depth of field texture by now"); + once!(warn!( + "Should have created the auxiliary depth of field texture by now" + )); continue; }; render_context.render_device().create_bind_group( @@ -426,7 +426,9 @@ impl ViewNode for DepthOfFieldNode { // `prepare_auxiliary_depth_of_field_textures``. if pipeline_render_info.is_dual_output { let Some(auxiliary_dof_texture) = auxiliary_dof_texture else { - warn_once!("Should have created the auxiliary depth of field texture by now"); + once!(warn!( + "Should have created the auxiliary depth of field texture by now" + )); continue; }; color_attachments.push(Some(RenderPassColorAttachment { @@ -818,9 +820,9 @@ fn extract_depth_of_field_settings( mut query: Extract>, ) { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { - info_once!( + once!(info!( "Disabling depth of field on this platform because depth textures aren't supported correctly" - ); + )); return; } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index e94daa90f4bdc..6c2ee5bec489b 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -33,11 +33,9 @@ pub use skybox::Skybox; /// /// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. pub mod experimental { - #[expect(deprecated)] pub mod taa { pub use crate::taa::{ - TemporalAntiAliasBundle, TemporalAntiAliasNode, TemporalAntiAliasPlugin, - TemporalAntiAliasSettings, TemporalAntiAliasing, + TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing, }; } } @@ -45,13 +43,9 @@ pub mod experimental { /// The core pipeline prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] - pub use crate::{ - core_2d::{Camera2d, Camera2dBundle}, - core_3d::{Camera3d, Camera3dBundle}, - }; + pub use crate::{core_2d::Camera2d, core_3d::Camera3d}; } use crate::{ diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index c6eb8524ca3ef..e4839b25b44c9 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -2,8 +2,6 @@ //! //! Add the [`MotionBlur`] component to a camera to enable motion blur. -#![expect(deprecated)] - use crate::{ core_3d::graph::{Core3d, Node3d}, prepass::{DepthPrepass, MotionVectorPrepass}, @@ -11,7 +9,6 @@ use crate::{ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_ecs::{ - bundle::Bundle, component::{require, Component}, query::With, reflect::ReflectComponent, @@ -29,18 +26,6 @@ use bevy_render::{ pub mod node; pub mod pipeline; -/// Adds [`MotionBlur`] and the required depth and motion vector prepasses to a camera entity. -#[derive(Bundle, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `MotionBlur` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct MotionBlurBundle { - pub motion_blur: MotionBlur, - pub depth_prepass: DepthPrepass, - pub motion_vector_prepass: MotionVectorPrepass, -} - /// A component that enables and configures motion blur when added to a camera. /// /// Motion blur is an effect that simulates how moving objects blur as they change position during diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 14e8b8d4e36e3..29c5660785f6d 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -16,15 +16,13 @@ use bevy_render::{ view::Msaa, Render, RenderApp, RenderSet, }; -use bevy_utils::{ - tracing::{trace, warn}, - HashSet, Instant, -}; +use bevy_utils::{HashSet, Instant}; use bevy_window::PrimaryWindow; use resolve::{ node::{OitResolveNode, OitResolvePass}, OitResolvePlugin, }; +use tracing::{trace, warn}; use crate::core_3d::{ graph::{Core3d, Node3d}, @@ -235,7 +233,6 @@ pub struct OrderIndependentTransparencySettingsOffset { /// This creates or resizes the oit buffers for each camera. /// It will always create one big buffer that's as big as the biggest buffer needed. /// Cameras with smaller viewports or less layers will simply use the big buffer and ignore the rest. -#[allow(clippy::type_complexity)] pub fn prepare_oit_buffers( mut commands: Commands, render_device: Res, diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index 101f7b1ed941e..a0e97e0770b17 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -22,7 +22,7 @@ use bevy_render::{ view::{ExtractedView, ViewTarget, ViewUniform, ViewUniforms}, Render, RenderApp, RenderSet, }; -use bevy_utils::tracing::warn; +use tracing::warn; use super::OitBuffers; @@ -124,7 +124,6 @@ pub struct OitResolvePipelineKey { layer_count: i32, } -#[allow(clippy::too_many_arguments)] pub fn queue_oit_resolve_pipeline( mut commands: Commands, pipeline_cache: Res, diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index a633134b276d2..f1719c1384222 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -112,7 +112,7 @@ pub struct ChromaticAberration { /// The size of the streaks around the edges of objects, as a fraction of /// the window size. /// - /// The default value is 0.2. + /// The default value is 0.02. pub intensity: f32, /// A cap on the number of texture samples that will be performed. diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index 78bac66df0f0f..7fb2dfcea961b 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -34,7 +34,8 @@ use bevy_asset::UntypedAssetId; use bevy_ecs::prelude::*; use bevy_math::Mat4; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::render_phase::PhaseItemBinKey; +use bevy_render::mesh::allocator::SlabId; +use bevy_render::render_phase::PhaseItemBatchSetKey; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_phase::{ @@ -139,8 +140,13 @@ impl ViewPrepassTextures { /// /// Used to render all 3D meshes with materials that have no transparency. pub struct Opaque3dPrepass { + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, /// Information that separates items into bins. - pub key: OpaqueNoLightmap3dBinKey, + pub bin_key: OpaqueNoLightmap3dBinKey, /// An entity from which Bevy fetches data common to all instances in this /// batch, such as the mesh. @@ -166,30 +172,33 @@ pub struct OpaqueNoLightmap3dBatchSetKey { /// /// In the case of PBR, this is the `MaterialBindGroupIndex`. pub material_bind_group_index: Option, + + /// The ID of the slab of GPU memory that contains vertex data. + /// + /// For non-mesh items, you can fill this with 0 if your items can be + /// multi-drawn, or with a unique value if they can't. + pub vertex_slab: SlabId, + + /// The ID of the slab of GPU memory that contains index data, if present. + /// + /// For non-mesh items, you can safely fill this with `None`. + pub index_slab: Option, +} + +impl PhaseItemBatchSetKey for OpaqueNoLightmap3dBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } } // TODO: Try interning these. /// The data used to bin each opaque 3D object in the prepass and deferred pass. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct OpaqueNoLightmap3dBinKey { - /// The key of the *batch set*. - /// - /// As batches belong to a batch set, meshes in a batch must obviously be - /// able to be placed in a single batch set. - pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, - /// The ID of the asset. pub asset_id: UntypedAssetId, } -impl PhaseItemBinKey for OpaqueNoLightmap3dBinKey { - type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; - - fn get_batch_set_key(&self) -> Option { - Some(self.batch_set_key.clone()) - } -} - impl PhaseItem for Opaque3dPrepass { #[inline] fn entity(&self) -> Entity { @@ -202,7 +211,7 @@ impl PhaseItem for Opaque3dPrepass { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -227,17 +236,20 @@ impl PhaseItem for Opaque3dPrepass { } impl BinnedPhaseItem for Opaque3dPrepass { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; type BinKey = OpaqueNoLightmap3dBinKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Opaque3dPrepass { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -248,7 +260,7 @@ impl BinnedPhaseItem for Opaque3dPrepass { impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } @@ -258,7 +270,13 @@ impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { /// /// Used to render all meshes with a material with an alpha mask. pub struct AlphaMask3dPrepass { - pub key: OpaqueNoLightmap3dBinKey, + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: OpaqueNoLightmap3dBatchSetKey, + /// Information that separates items into bins. + pub bin_key: OpaqueNoLightmap3dBinKey, pub representative_entity: (Entity, MainEntity), pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, @@ -276,7 +294,7 @@ impl PhaseItem for AlphaMask3dPrepass { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -301,17 +319,20 @@ impl PhaseItem for AlphaMask3dPrepass { } impl BinnedPhaseItem for AlphaMask3dPrepass { + type BatchSetKey = OpaqueNoLightmap3dBatchSetKey; type BinKey = OpaqueNoLightmap3dBinKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Self { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -322,7 +343,7 @@ impl BinnedPhaseItem for AlphaMask3dPrepass { impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 17f5dfb2cfe84..9019890d7ed86 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -6,11 +6,11 @@ use bevy_render::{ render_phase::{TrackedRenderPass, ViewBinnedRenderPhases}, render_resource::{CommandEncoderDescriptor, PipelineCache, RenderPassDescriptor, StoreOp}, renderer::RenderContext, - view::{ViewDepthTexture, ViewUniformOffset}, + view::{ExtractedView, ViewDepthTexture, ViewUniformOffset}, }; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; +use tracing::info_span; use crate::skybox::prepass::{RenderSkyboxPrepassPipeline, SkyboxPrepassBindGroup}; @@ -27,8 +27,8 @@ pub struct PrepassNode; impl ViewNode for PrepassNode { type ViewQuery = ( - Entity, &'static ExtractedCamera, + &'static ExtractedView, &'static ViewDepthTexture, &'static ViewPrepassTextures, &'static ViewUniformOffset, @@ -43,8 +43,8 @@ impl ViewNode for PrepassNode { graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, ( - view, camera, + extracted_view, view_depth_texture, view_prepass_textures, view_uniform_offset, @@ -63,8 +63,8 @@ impl ViewNode for PrepassNode { }; let (Some(opaque_prepass_phase), Some(alpha_mask_prepass_phase)) = ( - opaque_prepass_phases.get(&view), - alpha_mask_prepass_phases.get(&view), + opaque_prepass_phases.get(&extracted_view.retained_view_entity), + alpha_mask_prepass_phases.get(&extracted_view.retained_view_entity), ) else { return Ok(()); }; @@ -120,7 +120,8 @@ impl ViewNode for PrepassNode { } // Opaque draws - if !opaque_prepass_phase.batchable_mesh_keys.is_empty() + if !opaque_prepass_phase.multidrawable_mesh_keys.is_empty() + || !opaque_prepass_phase.batchable_mesh_keys.is_empty() || !opaque_prepass_phase.unbatchable_mesh_keys.is_empty() { #[cfg(feature = "trace")] diff --git a/crates/bevy_core_pipeline/src/smaa/mod.rs b/crates/bevy_core_pipeline/src/smaa/mod.rs index 7471cdb09ab76..12fc473061acf 100644 --- a/crates/bevy_core_pipeline/src/smaa/mod.rs +++ b/crates/bevy_core_pipeline/src/smaa/mod.rs @@ -101,9 +101,6 @@ pub struct Smaa { pub preset: SmaaPreset, } -#[deprecated(since = "0.15.0", note = "Renamed to `Smaa`")] -pub type SmaaSettings = Smaa; - /// A preset quality level for SMAA. /// /// Higher values are slower but result in a higher-quality image. @@ -914,7 +911,6 @@ impl ViewNode for SmaaNode { /// writes to the two-channel RG edges texture. Additionally, it ensures that /// all pixels it didn't touch are stenciled out so that phase 2 won't have to /// examine them. -#[allow(clippy::too_many_arguments)] fn perform_edge_detection( render_context: &mut RenderContext, smaa_pipelines: &SmaaPipelines, @@ -968,7 +964,6 @@ fn perform_edge_detection( /// This runs as part of the [`SmaaNode`]. It reads the edges texture and writes /// to the blend weight texture, using the stencil buffer to avoid processing /// pixels it doesn't need to examine. -#[allow(clippy::too_many_arguments)] fn perform_blending_weight_calculation( render_context: &mut RenderContext, smaa_pipelines: &SmaaPipelines, @@ -1027,7 +1022,6 @@ fn perform_blending_weight_calculation( /// /// This runs as part of the [`SmaaNode`]. It reads from the blend weight /// texture. It's the only phase that writes to the postprocessing destination. -#[allow(clippy::too_many_arguments)] fn perform_neighborhood_blending( render_context: &mut RenderContext, smaa_pipelines: &SmaaPipelines, diff --git a/crates/bevy_core_pipeline/src/smaa/smaa.wgsl b/crates/bevy_core_pipeline/src/smaa/smaa.wgsl index 5c95c18c2602f..08723254483c6 100644 --- a/crates/bevy_core_pipeline/src/smaa/smaa.wgsl +++ b/crates/bevy_core_pipeline/src/smaa/smaa.wgsl @@ -44,7 +44,7 @@ * Here you'll find instructions to get the shader up and running as fast as * possible. * - * IMPORTANTE NOTICE: when updating, remember to update both this file and the + * IMPORTANT NOTICE: when updating, remember to update both this file and the * precomputed textures! They may change from version to version. * * The shader has three passes, chained together as follows: @@ -429,7 +429,7 @@ const SMAA_CORNER_ROUNDING: u32 = 25u; // "SMAA Presets".) /** - * If there is an neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times + * If there is a neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times * bigger contrast than current edge, current edge will be discarded. * * This allows to eliminate spurious crossing edges, and is based on the fact diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 559ce4e3a55bc..aadc26cbc68b1 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - use crate::{ core_3d::graph::{Core3d, Node3d}, fullscreen_vertex_shader::fullscreen_shader_vertex_state, @@ -10,7 +8,7 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_diagnostic::FrameCount; use bevy_ecs::{ - prelude::{require, Bundle, Component, Entity, ReflectComponent}, + prelude::{require, Component, Entity, ReflectComponent}, query::{QueryItem, With}, schedule::IntoSystemConfigs, system::{Commands, Query, Res, ResMut, Resource}, @@ -39,7 +37,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; -use bevy_utils::tracing::warn; +use tracing::warn; const TAA_SHADER_HANDLE: Handle = Handle::weak_from_u128(656865235226276); @@ -92,19 +90,6 @@ impl Plugin for TemporalAntiAliasPlugin { } } -/// Bundle to apply temporal anti-aliasing. -#[derive(Bundle, Default, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `TemporalAntiAlias` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct TemporalAntiAliasBundle { - pub settings: TemporalAntiAliasing, - pub jitter: TemporalJitter, - pub depth_prepass: DepthPrepass, - pub motion_vector_prepass: MotionVectorPrepass, -} - /// Component to apply temporal anti-aliasing to a 3D perspective camera. /// /// Temporal anti-aliasing (TAA) is a form of image smoothing/filtering, like @@ -159,9 +144,6 @@ pub struct TemporalAntiAliasing { pub reset: bool, } -#[deprecated(since = "0.15.0", note = "Renamed to `TemporalAntiAliasing`")] -pub type TemporalAntiAliasSettings = TemporalAntiAliasing; - impl Default for TemporalAntiAliasing { fn default() -> Self { Self { reset: true } diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index c6fb3217253f9..e932639b7f420 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -18,9 +18,9 @@ use bevy_render::{ view::{ExtractedView, ViewTarget, ViewUniform}, Render, RenderApp, RenderSet, }; -#[cfg(not(feature = "tonemapping_luts"))] -use bevy_utils::tracing::error; use bitflags::bitflags; +#[cfg(not(feature = "tonemapping_luts"))] +use tracing::error; mod node; @@ -431,8 +431,11 @@ pub fn get_lut_bind_group_layout_entries() -> [BindGroupLayoutEntryBuilder; 2] { ] } -// allow(dead_code) so it doesn't complain when the tonemapping_luts feature is disabled -#[allow(dead_code)] +#[expect(clippy::allow_attributes, reason = "`dead_code` is not always linted.")] +#[allow( + dead_code, + reason = "There is unused code when the `tonemapping_luts` feature is disabled." +)] fn setup_tonemapping_lut_image(bytes: &[u8], image_type: ImageType) -> Image { let image_sampler = ImageSampler::Descriptor(bevy_image::ImageSamplerDescriptor { label: Some("Tonemapping LUT sampler".to_string()), diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 52369fca59abc..1145e17d185bd 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -55,7 +55,7 @@ fn prepare_view_upscaling_pipelines( match blend_state { None => { - // If we've already seen this output for a camera and it doesn't have a output blend + // If we've already seen this output for a camera and it doesn't have an output blend // mode configured, default to alpha blend so that we don't accidentally overwrite // the output texture if already_seen { diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml index 385ba359ba6d3..3cac10ce09f08 100644 --- a/crates/bevy_derive/Cargo.toml +++ b/crates/bevy_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_derive" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides derive implementations for Bevy Engine" homepage = "https://bevyengine.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_derive/compile_fail/tests/derive.rs b/crates/bevy_derive/compile_fail/tests/derive.rs index b918abe2733de..1349ca060df26 100644 --- a/crates/bevy_derive/compile_fail/tests/derive.rs +++ b/crates/bevy_derive/compile_fail/tests/derive.rs @@ -1,4 +1,6 @@ fn main() -> compile_fail_utils::ui_test::Result<()> { - compile_fail_utils::test_multiple("derive_deref", ["tests/deref_derive", "tests/deref_mut_derive"]) + compile_fail_utils::test_multiple( + "derive_deref", + ["tests/deref_derive", "tests/deref_mut_derive"], + ) } - diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 1d426853cee80..223474a0f0493 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dev_tools" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Collection of developer tools for the Bevy Engine" homepage = "https://bevyengine.org" @@ -13,24 +13,27 @@ bevy_ci_testing = ["serde", "ron"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_state = { path = "../bevy_state", version = "0.15.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } +bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } +bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.16.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } +bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } ron = { version = "0.8.0", optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 9f31db2140892..aa4311111b97e 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -62,7 +62,7 @@ impl Plugin for CiTestingPlugin { // The offending system does not exist in the wasm32 target. // As a result, we must conditionally order the two systems using a system set. - #[cfg(not(target_arch = "wasm32"))] + #[cfg(any(unix, windows))] app.configure_sets( Update, SendEvents.before(bevy_app::TerminalCtrlCHandlerPlugin::exit_on_flag), diff --git a/crates/bevy_dev_tools/src/ci_testing/systems.rs b/crates/bevy_dev_tools/src/ci_testing/systems.rs index 20c758a91cdfc..70991fc2acda9 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -2,7 +2,7 @@ use super::config::*; use bevy_app::AppExit; use bevy_ecs::prelude::*; use bevy_render::view::screenshot::{save_to_disk, Screenshot}; -use bevy_utils::tracing::{debug, info}; +use tracing::{debug, info}; pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { let mut config = world.resource_mut::(); diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index f970fc8f434c8..a6368cf92d41b 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -19,7 +19,6 @@ use bevy_ui::{ widget::{Text, TextUiWriter}, GlobalZIndex, Node, PositionType, }; -use bevy_utils::default; /// [`GlobalZIndex`] used to render the fps overlay. /// @@ -43,7 +42,7 @@ impl Plugin for FpsOverlayPlugin { fn build(&self, app: &mut bevy_app::App) { // TODO: Use plugin dependencies, see https://github.com/bevyengine/bevy/issues/69 if !app.is_plugin_added::() { - app.add_plugins(FrameTimeDiagnosticsPlugin); + app.add_plugins(FrameTimeDiagnosticsPlugin::default()); } app.insert_resource(self.config.clone()) .add_systems(Startup, setup) @@ -74,7 +73,7 @@ impl Default for FpsOverlayConfig { text_config: TextFont { font: Handle::::default(), font_size: 32.0, - ..default() + ..Default::default() }, text_color: Color::WHITE, enabled: true, @@ -91,7 +90,7 @@ fn setup(mut commands: Commands, overlay_config: Res) { Node { // We need to make sure the overlay doesn't affect the position of other UI nodes position_type: PositionType::Absolute, - ..default() + ..Default::default() }, // Render overlay on top of everything GlobalZIndex(FPS_OVERLAY_ZINDEX), diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index b49604e6c885d..0f9dc75611326 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -15,6 +15,8 @@ pub mod ci_testing; pub mod fps_overlay; +pub mod picking_debug; + pub mod states; /// Enables developer tools in an [`App`]. This plugin is added automatically with `bevy_dev_tools` diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs new file mode 100644 index 0000000000000..766b065913a39 --- /dev/null +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -0,0 +1,300 @@ +//! 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::prelude::*; +use bevy_picking::{pointer, PickSet}; +use bevy_reflect::prelude::*; +use bevy_render::prelude::*; +use bevy_text::prelude::*; +use bevy_ui::prelude::*; +use core::cmp::Ordering; +use core::fmt::{Debug, Display, Formatter, Result}; +use tracing::{debug, trace}; + +/// This resource determines the runtime behavior of the debug plugin. +#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, Resource)] +pub enum DebugPickingMode { + /// Only log non-noisy events, show the debug overlay. + Normal, + /// Log all events, including noisy events like `Move` and `Drag`, show the debug overlay. + Noisy, + /// Do not show the debug overlay or log any messages. + #[default] + Disabled, +} + +impl DebugPickingMode { + /// A condition indicating the plugin is enabled + pub fn is_enabled(this: Res) -> bool { + matches!(*this, Self::Normal | Self::Noisy) + } + /// A condition indicating the plugin is disabled + pub fn is_disabled(this: Res) -> bool { + matches!(*this, Self::Disabled) + } + /// A condition indicating the plugin is enabled and in noisy mode + pub fn is_noisy(this: Res) -> bool { + matches!(*this, Self::Noisy) + } +} + +/// Logs events for debugging +/// +/// "Normal" events are logged at the `debug` level. "Noisy" events are logged at the `trace` level. +/// See [Bevy's LogPlugin](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) and [Bevy +/// Cheatbook: Logging, Console Messages](https://bevy-cheatbook.github.io/features/log.html) for +/// details. +/// +/// Usually, the default level printed is `info`, so debug and trace messages will not be displayed +/// even when this plugin is active. You can set `RUST_LOG` to change this. +/// +/// You can also change the log filter at runtime in your code. The [LogPlugin +/// docs](https://docs.rs/bevy/latest/bevy/log/struct.LogPlugin.html) give an example. +/// +/// Use the [`DebugPickingMode`] state resource to control this plugin. Example: +/// +/// ```ignore +/// use DebugPickingMode::{Normal, Disabled}; +/// app.insert_resource(DebugPickingMode::Normal) +/// .add_systems( +/// PreUpdate, +/// (|mut mode: ResMut| { +/// *mode = match *mode { +/// DebugPickingMode::Disabled => DebugPickingMode::Normal, +/// _ => DebugPickingMode::Disabled, +/// }; +/// }) +/// .distributive_run_if(bevy::input::common_conditions::input_just_pressed( +/// KeyCode::F3, +/// )), +/// ) +/// ``` +/// This sets the starting mode of the plugin to [`DebugPickingMode::Disabled`] and binds the F3 key +/// to toggle it. +#[derive(Debug, Default, Clone)] +pub struct DebugPickingPlugin; + +impl Plugin for DebugPickingPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems( + PreUpdate, + pointer_debug_visibility.in_set(PickSet::PostHover), + ) + .add_systems( + PreUpdate, + ( + // 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_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::, + log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), + log_pointer_event_debug::, + log_pointer_event_debug::, + ) + .distributive_run_if(DebugPickingMode::is_enabled) + .in_set(PickSet::Last), + ); + + app.add_systems( + PreUpdate, + (add_pointer_debug, update_debug_data, debug_draw) + .chain() + .distributive_run_if(DebugPickingMode::is_enabled) + .in_set(PickSet::Last), + ); + } +} + +/// Listen for any event and logs it at the debug level +pub fn log_event_debug(mut events: EventReader) { + for event in events.read() { + debug!("{event:?}"); + } +} + +/// Listens for pointer events of type `E` and logs them at "debug" level +pub fn log_pointer_event_debug( + mut pointer_events: EventReader>, +) { + for event in pointer_events.read() { + debug!("{event}"); + } +} + +/// Listens for pointer events of type `E` and logs them at "trace" level +pub fn log_pointer_event_trace( + mut pointer_events: EventReader>, +) { + for event in pointer_events.read() { + trace!("{event}"); + } +} + +/// Adds [`PointerDebug`] to pointers automatically. +pub fn add_pointer_debug( + mut commands: Commands, + pointers: Query, Without)>, +) { + for entity in &pointers { + commands.entity(entity).insert(PointerDebug::default()); + } +} + +/// Hide text from pointers. +pub fn pointer_debug_visibility( + debug: Res, + mut pointers: Query<&mut Visibility, With>, +) { + let visible = match *debug { + DebugPickingMode::Disabled => Visibility::Hidden, + _ => Visibility::Visible, + }; + for mut vis in &mut pointers { + *vis = visible; + } +} + +/// Storage for per-pointer debug information. +#[derive(Debug, Component, Clone, Default)] +pub struct PointerDebug { + /// The pointer location. + pub location: Option, + + /// Representation of the different pointer button states. + pub press: PointerPress, + + /// List of hit elements to be displayed. + pub hits: Vec<(String, HitData)>, +} + +fn bool_to_icon(f: &mut Formatter, prefix: &str, input: bool) -> Result { + write!(f, "{prefix}{}", if input { "[X]" } else { "[ ]" }) +} + +impl Display for PointerDebug { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + if let Some(location) = &self.location { + writeln!(f, "Location: {:.2?}", location.position)?; + } + bool_to_icon(f, "Pressed: ", self.press.is_primary_pressed())?; + bool_to_icon(f, " ", self.press.is_middle_pressed())?; + bool_to_icon(f, " ", self.press.is_secondary_pressed())?; + let mut sorted_hits = self.hits.clone(); + sorted_hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal)); + for (entity, hit) in sorted_hits.iter() { + write!(f, "\nEntity: {entity:?}")?; + if let Some((position, normal)) = hit.position.zip(hit.normal) { + write!(f, ", Position: {position:.2?}, Normal: {normal:.2?}")?; + } + write!(f, ", Depth: {:.2?}", hit.depth)?; + } + + Ok(()) + } +} + +/// Update typed debug data used to draw overlays +pub fn update_debug_data( + hover_map: Res, + entity_names: Query, + mut pointers: Query<( + &PointerId, + &pointer::PointerLocation, + &PointerPress, + &mut PointerDebug, + )>, +) { + for (id, location, press, mut debug) in &mut pointers { + *debug = PointerDebug { + location: location.location().cloned(), + press: press.to_owned(), + hits: hover_map + .get(id) + .iter() + .flat_map(|h| h.iter()) + .filter_map(|(e, h)| { + if let Ok(entity_name) = entity_names.get(*e) { + Some((entity_name.to_string(), h.to_owned())) + } else { + None + } + }) + .collect(), + }; + } +} + +/// Draw text on each cursor with debug info +pub fn debug_draw( + mut commands: Commands, + camera_query: Query<(Entity, &Camera)>, + primary_window: Query>, + pointers: Query<(Entity, &PointerId, &PointerDebug)>, + scale: Res, +) { + let font_handle: Handle = Default::default(); + for (entity, id, debug) in pointers.iter() { + 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.get_single().ok()), + ) + }) + .filter_map(|(entity, target)| Some(entity).zip(target)) + .filter(|(_entity, target)| target == &pointer_location.target) + .map(|(cam_entity, _target)| cam_entity) + { + let mut pointer_pos = pointer_location.position; + if let Some(viewport) = camera_query + .get(camera) + .ok() + .and_then(|(_, camera)| camera.logical_viewport_rect()) + { + pointer_pos -= viewport.min; + } + + commands + .entity(entity) + .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, + ..Default::default() + }, + )) + .insert(Pickable::IGNORE) + .insert(TargetCamera(camera)); + } + } +} diff --git a/crates/bevy_dev_tools/src/states.rs b/crates/bevy_dev_tools/src/states.rs index 21d5aedc03232..8e43f80844881 100644 --- a/crates/bevy_dev_tools/src/states.rs +++ b/crates/bevy_dev_tools/src/states.rs @@ -2,7 +2,7 @@ use bevy_ecs::event::EventReader; use bevy_state::state::{StateTransitionEvent, States}; -use bevy_utils::tracing::info; +use tracing::info; /// Logs state transitions into console. /// diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 5d45853997de9..9423b08880aff 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_diagnostic" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides diagnostic functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -16,14 +16,16 @@ serialize = ["dep:serde"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", 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_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } +# other const-fnv1a-hash = "1.1.0" serde = { version = "1.0", optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } # macOS [target.'cfg(all(target_os="macos"))'.dependencies] diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index c9fbe5d600eea..1c641c88a837a 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -1,9 +1,12 @@ use alloc::{borrow::Cow, collections::VecDeque}; -use core::hash::{Hash, Hasher}; +use core::{ + hash::{Hash, Hasher}, + time::Duration, +}; use bevy_app::{App, SubApp}; use bevy_ecs::system::{Deferred, Res, Resource, SystemBuffer, SystemParam}; -use bevy_utils::{Duration, HashMap, Instant, PassHash}; +use bevy_utils::{HashMap, Instant, PassHash}; use const_fnv1a_hash::fnv1a_hash_str_64; use crate::DEFAULT_MAX_HISTORY_LENGTH; @@ -351,8 +354,7 @@ impl<'w, 's> Diagnostics<'w, 's> { if self .store .get(path) - .filter(|diagnostic| diagnostic.is_enabled) - .is_some() + .is_some_and(|diagnostic| diagnostic.is_enabled) { let measurement = DiagnosticMeasurement { time: Instant::now(), diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 03cb08f85312a..22b6176fa2856 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -1,4 +1,7 @@ -use crate::{Diagnostic, DiagnosticPath, Diagnostics, FrameCount, RegisterDiagnostic}; +use crate::{ + Diagnostic, DiagnosticPath, Diagnostics, FrameCount, RegisterDiagnostic, + DEFAULT_MAX_HISTORY_LENGTH, +}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_time::{Real, Time}; @@ -8,15 +11,49 @@ use bevy_time::{Real, Time}; /// # See also /// /// [`LogDiagnosticsPlugin`](crate::LogDiagnosticsPlugin) to output diagnostics to the console. -#[derive(Default)] -pub struct FrameTimeDiagnosticsPlugin; +pub struct FrameTimeDiagnosticsPlugin { + /// The total number of values to keep for averaging. + pub max_history_length: usize, + /// 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`. + pub fn new(max_history_length: usize) -> Self { + Self { + max_history_length, + smoothing_factor: 2.0 / (max_history_length as f64 + 1.0), + } + } +} impl Plugin for FrameTimeDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.register_diagnostic(Diagnostic::new(Self::FRAME_TIME).with_suffix("ms")) - .register_diagnostic(Diagnostic::new(Self::FPS)) - .register_diagnostic(Diagnostic::new(Self::FRAME_COUNT).with_smoothing_factor(0.0)) - .add_systems(Update, Self::diagnostic_system); + app.register_diagnostic( + Diagnostic::new(Self::FRAME_TIME) + .with_suffix("ms") + .with_max_history_length(self.max_history_length) + .with_smoothing_factor(self.smoothing_factor), + ) + .register_diagnostic( + Diagnostic::new(Self::FPS) + .with_max_history_length(self.max_history_length) + .with_smoothing_factor(self.smoothing_factor), + ) + // An average frame count would be nonsensical, so we set the max history length + // to zero and disable smoothing. + .register_diagnostic( + Diagnostic::new(Self::FRAME_COUNT) + .with_smoothing_factor(0.0) + .with_max_history_length(0), + ) + .add_systems(Update, Self::diagnostic_system); } } diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index d6e7a2e0b6a75..bf53fe520cb71 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -2,10 +2,8 @@ use super::{Diagnostic, DiagnosticPath, DiagnosticsStore}; use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_time::{Real, Time, Timer, TimerMode}; -use bevy_utils::{ - tracing::{debug, info}, - Duration, -}; +use core::time::Duration; +use tracing::{debug, info}; /// An App Plugin that logs diagnostics to the console. /// diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 44e4d4698bede..cd390506cece3 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -66,8 +66,8 @@ pub mod internal { use bevy_app::{App, First, Startup, Update}; use bevy_ecs::system::Resource; use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task}; - use bevy_utils::tracing::info; use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System}; + use tracing::info; use crate::{Diagnostic, Diagnostics, DiagnosticsStore}; @@ -210,7 +210,7 @@ pub mod internal { } fn setup_system() { - bevy_utils::tracing::warn!("This platform and/or configuration is not supported!"); + tracing::warn!("This platform and/or configuration is not supported!"); } impl Default for super::SystemInfo { diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml index 33d2c697f76ef..de96856f92b48 100644 --- a/crates/bevy_dylib/Cargo.toml +++ b/crates/bevy_dylib/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_dylib" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Force the Bevy Engine to be dynamically linked for faster linking" homepage = "https://bevyengine.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] crate-type = ["dylib"] [dependencies] -bevy_internal = { path = "../bevy_internal", version = "0.15.0-dev", default-features = false } +bevy_internal = { path = "../bevy_internal", version = "0.16.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 c37cff70c36a1..1ff40ce3e8bb8 100644 --- a/crates/bevy_dylib/src/lib.rs +++ b/crates/bevy_dylib/src/lib.rs @@ -54,6 +54,9 @@ //! ``` // Force linking of the main bevy crate -#[allow(unused_imports)] -#[allow(clippy::single_component_path_imports)] +#[expect( + unused_imports, + clippy::single_component_path_imports, + reason = "This links the main bevy crate when using dynamic linking, and as such cannot be removed or changed without affecting dynamic linking." +)] use bevy_internal; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index add6ff18615f7..3f52188d1cee6 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ecs" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Bevy Engine's entity component system" homepage = "https://bevyengine.org" @@ -8,7 +8,7 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["ecs", "game", "bevy"] categories = ["game-engines", "data-structures"] -rust-version = "1.81.0" +rust-version = "1.83.0" [features] default = ["std", "bevy_reflect", "async_executor"] @@ -28,11 +28,14 @@ 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 +configurable_error_handler = [] + # Debugging Features ## Enables `tracing` integration, allowing spans and other metrics to be reported ## through that framework. -trace = ["std", "dep:tracing", "bevy_utils/tracing"] +trace = ["std", "dep:tracing"] ## Enables a more detailed set of traces which may be noisy if left on by default. detailed_trace = ["trace"] @@ -43,7 +46,7 @@ bevy_debug_stepping = [] ## Provides more detailed tracking of the cause of various effects within the ECS. ## This will often provide more detailed error messages. -track_change_detection = [] +track_location = [] # Executor Backend @@ -84,7 +87,7 @@ critical-section = [ ] ## `portable-atomic` provides additional platform support for atomic types and -## operations, even on targets without native support. +## operations, even on targets without native support. portable-atomic = [ "dep:portable-atomic", "dep:portable-atomic-util", @@ -94,13 +97,13 @@ portable-atomic = [ ] [dependencies] -bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false, optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", default-features = false, optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false, features = [ +bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ "alloc", ] } -bevy_ecs_macros = { path = "macros", version = "0.15.0-dev" } +bevy_ecs_macros = { path = "macros", version = "0.16.0-dev" } bitflags = { version = "2.3", default-features = false } concurrent-queue = { version = "2.5.0", default-features = false } diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index b947644bcd346..8614dbc5e35e1 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -80,7 +80,7 @@ struct Position { x: f32, y: f32 } fn print_position(query: Query<(Entity, &Position)>) { for (entity, position) in &query { - println!("Entity {:?} is at position: x {}, y {}", entity, position.x, position.y); + println!("Entity {} is at position: x {}, y {}", entity, position.x, position.y); } } ``` @@ -172,7 +172,7 @@ struct Player; struct Alive; // Gets the Position component of all Entities with Player component and without the Alive -// component. +// component. fn system(query: Query<&Position, (With, Without)>) { for position in &query { } @@ -340,7 +340,7 @@ let mut world = World::new(); let entity = world.spawn_empty().id(); world.add_observer(|trigger: Trigger, mut commands: Commands| { - println!("Entity {:?} goes BOOM!", trigger.target()); + println!("Entity {} goes BOOM!", trigger.target()); commands.entity(trigger.target()).despawn(); }); diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 41300653ba6fb..23420b5e88038 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -6,7 +6,10 @@ //! To demonstrate change detection, there are some console outputs based on changes in //! the `EntityCounter` resource and updated Age components -#![expect(clippy::std_instead_of_core)] +#![expect( + clippy::std_instead_of_core, + reason = "Examples should not follow this lint" +)] use bevy_ecs::prelude::*; use rand::Rng; @@ -80,10 +83,10 @@ 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 it's first birthday!"); } for (entity, value) in &entity_with_mutated_component { - println!(" {entity:?} is now {value:?} frames old"); + println!(" {entity} is now {value:?} frames old"); } } @@ -98,7 +101,7 @@ fn age_all_entities(mut entities: Query<&mut Age>) { fn remove_old_entities(mut commands: Commands, entities: Query<(Entity, &Age)>) { for (entity, age) in &entities { if age.frames > 2 { - println!(" despawning {entity:?} due to age > 2"); + println!(" despawning {entity} due to age > 2"); commands.entity(entity).despawn(); } } diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index a7894ad938e23..aac9dc38bc860 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -57,7 +57,7 @@ fn sending_system(mut event_writer: EventWriter) { fn receiving_system(mut event_reader: EventReader) { for my_event in event_reader.read() { println!( - " Received message {:?}, with random value of {}", + " Received message {}, with random value of {}", my_event.message, my_event.random_value ); } diff --git a/crates/bevy_ecs/examples/resources.rs b/crates/bevy_ecs/examples/resources.rs index f5de6f7b3d30e..43eddf7ce2bf1 100644 --- a/crates/bevy_ecs/examples/resources.rs +++ b/crates/bevy_ecs/examples/resources.rs @@ -1,7 +1,10 @@ //! In this example we add a counter resource and increase its value in one system, //! while a different system prints the current count to the console. -#![expect(clippy::std_instead_of_core)] +#![expect( + clippy::std_instead_of_core, + reason = "Examples should not follow this lint" +)] use bevy_ecs::prelude::*; use rand::Rng; diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index 85d051af28de8..8368e2a9dc23e 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.15.0-dev" +version = "0.16.0-dev" description = "Bevy ECS Macros" edition = "2021" 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.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } syn = { version = "2.0", features = ["full"] } quote = "1.0" diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index cd7cd52bf6570..e89436e42622a 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -29,11 +29,6 @@ pub fn derive_event(input: TokenStream) -> TokenStream { type Traversal = (); const AUTO_PROPAGATE: bool = false; } - - impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause { - const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet; - type Mutability = #bevy_ecs_path::component::Mutable; - } }) } diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 3f198b1ad1b18..972f7e33b8ae0 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -36,10 +36,10 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let mut attributes = QueryDataAttributes::default(); for attr in &ast.attrs { - if !attr + if attr .path() .get_ident() - .map_or(false, |ident| ident == QUERY_DATA_ATTRIBUTE_NAME) + .is_none_or(|ident| ident != QUERY_DATA_ATTRIBUTE_NAME) { continue; } @@ -382,7 +382,7 @@ fn read_world_query_field_info(field: &Field) -> syn::Result if attr .path() .get_ident() - .map_or(false, |ident| ident == QUERY_DATA_ATTRIBUTE_NAME) + .is_some_and(|ident| ident == QUERY_DATA_ATTRIBUTE_NAME) { return Err(syn::Error::new_spanned( attr, diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 9d97f185dede9..f008247657328 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -2,10 +2,6 @@ use proc_macro2::Ident; use quote::quote; use syn::{Attribute, Fields, ImplGenerics, TypeGenerics, Visibility, WhereClause}; -#[expect( - clippy::too_many_arguments, - reason = "Required to generate the entire item structure." -)] pub(crate) fn item_struct( path: &syn::Path, fields: &Fields, @@ -55,10 +51,6 @@ pub(crate) fn item_struct( } } -#[expect( - clippy::too_many_arguments, - reason = "Required to generate the entire world query implementation." -)] pub(crate) fn world_query_impl( path: &syn::Path, struct_name: &Ident, diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index e0d85242f9206..e3b8b8dac545d 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -789,7 +789,10 @@ pub struct Archetypes { pub struct ArchetypeRecord { /// Index of the component in the archetype's [`Table`](crate::storage::Table), /// or None if the component is a sparse set component. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "Currently unused, but planned to be used to implement a component index to improve performance of fragmenting relations." + )] pub(crate) column: Option, } @@ -827,7 +830,10 @@ impl Archetypes { /// Fetches the total number of [`Archetype`]s within the world. #[inline] - #[allow(clippy::len_without_is_empty)] // the internal vec is never empty. + #[expect( + clippy::len_without_is_empty, + reason = "The internal vec is never empty" + )] pub fn len(&self) -> usize { self.archetypes.len() } diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 18044fcc8cd5b..6b4d9da2811bb 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -23,7 +23,7 @@ use crate::{ use alloc::{boxed::Box, vec, vec::Vec}; use bevy_ptr::{ConstNonNull, OwningPtr}; use bevy_utils::{HashMap, HashSet, TypeIdMap}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{any::TypeId, ptr::NonNull}; use variadics_please::all_tuples; @@ -246,6 +246,15 @@ impl DynamicBundle for C { macro_rules! tuple_impl { ($(#[$meta:meta])* $($name: ident),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + #[allow( + unused_mut, + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] $(#[$meta])* // SAFETY: // - `Bundle::component_ids` calls `ids` for each component type in the @@ -254,43 +263,57 @@ macro_rules! tuple_impl { // - `Bundle::get_components` is called exactly once for each member. Relies on the above implementation to pass the correct // `StorageType` into the callback. unsafe impl<$($name: Bundle),*> Bundle for ($($name,)*) { - #[allow(unused_variables)] fn component_ids(components: &mut Components, storages: &mut Storages, ids: &mut impl FnMut(ComponentId)){ $(<$name as Bundle>::component_ids(components, storages, ids);)* } - #[allow(unused_variables)] fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option)){ $(<$name as Bundle>::get_component_ids(components, ids);)* } - #[allow(unused_variables, unused_mut)] - #[allow(clippy::unused_unit)] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate a function body 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." + )] unsafe fn from_components(ctx: &mut T, func: &mut F) -> Self where F: FnMut(&mut T) -> OwningPtr<'_> { - #[allow(unused_unsafe)] + #[allow( + unused_unsafe, + reason = "Zero-length tuples will not run anything in the unsafe block. Additionally, rewriting this to move the () outside of the unsafe would require putting the safety comment inside the tuple, hurting readability of the code." + )] // SAFETY: Rust guarantees that tuple calls are evaluated 'left to right'. // https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands unsafe { ($(<$name as Bundle>::from_components(ctx, func),)*) } } fn register_required_components( - _components: &mut Components, - _storages: &mut Storages, - _required_components: &mut RequiredComponents, + components: &mut Components, + storages: &mut Storages, + required_components: &mut RequiredComponents, ) { - $(<$name as Bundle>::register_required_components(_components, _storages, _required_components);)* + $(<$name as Bundle>::register_required_components(components, storages, required_components);)* } } + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such, the lints below may not always apply." + )] + #[allow( + unused_mut, + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] $(#[$meta])* impl<$($name: Bundle),*> DynamicBundle for ($($name,)*) { - #[allow(unused_variables, unused_mut)] #[inline(always)] fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { - #[allow(non_snake_case)] + #[allow( + non_snake_case, + reason = "The names of these variables are provided by the caller, not by us." + )] let ($(mut $name,)*) = self; $( $name.get_components(&mut *func); @@ -336,12 +359,12 @@ impl SparseSetIndex for BundleId { } } -// What to do on insertion if component already exists +/// What to do on insertion if a component already exists. #[derive(Clone, Copy, Eq, PartialEq)] -pub(crate) enum InsertMode { +pub enum InsertMode { /// Any existing components of a matching type will be overwritten. Replace, - /// Any existing components of a matching type will kept unchanged. + /// Any existing components of a matching type will be left unchanged. Keep, } @@ -504,7 +527,6 @@ impl BundleInfo { /// `table` must be the "new" table for `entity`. `table_row` must have space allocated for the /// `entity`, `bundle` must match this [`BundleInfo`]'s type #[inline] - #[allow(clippy::too_many_arguments)] unsafe fn write_components<'a, T: DynamicBundle, S: BundleComponentStatus>( &self, table: &mut Table, @@ -516,7 +538,7 @@ impl BundleInfo { change_tick: Tick, bundle: T, insert_mode: InsertMode, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { // NOTE: get_components calls this closure on each component in "bundle order". // bundle_info.component_ids are also in "bundle order" @@ -535,14 +557,14 @@ impl BundleInfo { table_row, component_ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ), (ComponentStatus::Existing, InsertMode::Replace) => column.replace( table_row, component_ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ), (ComponentStatus::Existing, InsertMode::Keep) => { @@ -561,7 +583,7 @@ impl BundleInfo { entity, component_ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -576,7 +598,7 @@ impl BundleInfo { change_tick, table_row, entity, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -594,7 +616,6 @@ impl BundleInfo { /// This method _should not_ be called outside of [`BundleInfo::write_components`]. /// For more information, read the [`BundleInfo::write_components`] safety docs. /// This function inherits the safety requirements defined there. - #[allow(clippy::too_many_arguments)] pub(crate) unsafe fn initialize_required_component( table: &mut Table, sparse_sets: &mut SparseSets, @@ -604,7 +625,7 @@ impl BundleInfo { component_id: ComponentId, storage_type: StorageType, component_ptr: OwningPtr, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { { match storage_type { @@ -617,7 +638,7 @@ impl BundleInfo { table_row, component_ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -630,7 +651,7 @@ impl BundleInfo { entity, component_ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1019,7 +1040,7 @@ impl<'w> BundleInserter<'w> { location: EntityLocation, bundle: T, insert_mode: InsertMode, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> EntityLocation { let bundle_info = self.bundle_info.as_ref(); let archetype_after_insert = self.archetype_after_insert.as_ref(); @@ -1070,7 +1091,7 @@ impl<'w> BundleInserter<'w> { self.change_tick, bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); @@ -1112,7 +1133,7 @@ impl<'w> BundleInserter<'w> { self.change_tick, bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); @@ -1195,7 +1216,7 @@ impl<'w> BundleInserter<'w> { self.change_tick, bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); @@ -1330,7 +1351,7 @@ impl<'w> BundleSpawner<'w> { &mut self, entity: Entity, bundle: T, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> EntityLocation { // SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid let bundle_info = self.bundle_info.as_ref(); @@ -1355,7 +1376,7 @@ impl<'w> BundleSpawner<'w> { self.change_tick, bundle, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); entities.set(entity.index(), location); @@ -1404,7 +1425,7 @@ impl<'w> BundleSpawner<'w> { pub unsafe fn spawn( &mut self, bundle: T, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> Entity { let entity = self.entities().alloc(); // SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type @@ -1412,7 +1433,7 @@ impl<'w> BundleSpawner<'w> { self.spawn_non_existent( entity, bundle, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1643,6 +1664,7 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { mod tests { use crate as bevy_ecs; use crate::{component::ComponentId, prelude::*, world::DeferredWorld}; + use alloc::vec; #[derive(Component)] struct A; diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 025c3804e73e2..e238287a7ec3d 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -5,12 +5,13 @@ use crate::{ ptr::PtrMut, system::Resource, }; +use alloc::borrow::ToOwned; use bevy_ptr::{Ptr, UnsafeCellDeref}; use core::{ mem, ops::{Deref, DerefMut}, }; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use { bevy_ptr::ThinSlicePtr, core::{cell::UnsafeCell, panic::Location}, @@ -72,7 +73,7 @@ pub trait DetectChanges { fn last_changed(&self) -> Tick; /// The location that last caused this to change. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] fn changed_by(&self) -> &'static Location<'static>; } @@ -268,6 +269,55 @@ pub trait DetectChangesMut: DetectChanges { None } } + + /// Overwrites this smart pointer with a clone of the given value, if and only if `*self != value`. + /// Returns `true` if the value was overwritten, and returns `false` if it was not. + /// + /// This method is useful when the caller only has a borrowed form of `Inner`, + /// e.g. when writing a `&str` into a `Mut`. + /// + /// # Examples + /// ``` + /// # extern crate alloc; + /// # use alloc::borrow::ToOwned; + /// # use bevy_ecs::{prelude::*, schedule::common_conditions::resource_changed}; + /// #[derive(Resource)] + /// pub struct Message(String); + /// + /// fn update_message(mut message: ResMut) { + /// // Set the score to zero, unless it is already zero. + /// ResMut::map_unchanged(message, |Message(msg)| msg).clone_from_if_neq("another string"); + /// } + /// # let mut world = World::new(); + /// # world.insert_resource(Message("initial string".into())); + /// # let mut message_changed = IntoSystem::into_system(resource_changed::); + /// # message_changed.initialize(&mut world); + /// # message_changed.run((), &mut world); + /// # + /// # let mut schedule = Schedule::default(); + /// # schedule.add_systems(update_message); + /// # + /// # // first time `reset_score` runs, the score is changed. + /// # schedule.run(&mut world); + /// # assert!(message_changed.run((), &mut world)); + /// # // second time `reset_score` runs, the score is not changed. + /// # schedule.run(&mut world); + /// # assert!(!message_changed.run((), &mut world)); + /// ``` + fn clone_from_if_neq(&mut self, value: &T) -> bool + where + T: ToOwned + ?Sized, + Self::Inner: PartialEq, + { + let old = self.bypass_change_detection(); + if old != value { + value.clone_into(old); + self.set_changed(); + true + } else { + false + } + } } macro_rules! change_detection_impl { @@ -293,7 +343,7 @@ macro_rules! change_detection_impl { } #[inline] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] fn changed_by(&self) -> &'static Location<'static> { self.changed_by } @@ -326,7 +376,7 @@ macro_rules! change_detection_mut_impl { #[track_caller] fn set_changed(&mut self) { *self.ticks.changed = self.ticks.this_run; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by = Location::caller(); } @@ -336,7 +386,7 @@ macro_rules! change_detection_mut_impl { #[track_caller] fn set_last_changed(&mut self, last_changed: Tick) { *self.ticks.changed = last_changed; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by = Location::caller(); } @@ -353,7 +403,7 @@ macro_rules! change_detection_mut_impl { #[track_caller] fn deref_mut(&mut self) -> &mut Self::Target { self.set_changed(); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by = Location::caller(); } @@ -394,7 +444,7 @@ macro_rules! impl_methods { last_run: self.ticks.last_run, this_run: self.ticks.this_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -425,7 +475,7 @@ macro_rules! impl_methods { Mut { value: f(self.value), ticks: self.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -439,7 +489,7 @@ macro_rules! impl_methods { value.map(|value| Mut { value, ticks: self.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, }) } @@ -544,13 +594,13 @@ impl<'w> From> for Ticks<'w> { /// If you need a unique mutable borrow, use [`ResMut`] instead. /// /// This [`SystemParam`](crate::system::SystemParam) fails validation if resource doesn't exist. -/// This will cause systems that use this parameter to be skipped. +/// This will cause a panic, but can be configured to do nothing or warn once. /// /// Use [`Option>`] instead if the resource might not always exist. pub struct Res<'w, T: ?Sized + Resource> { pub(crate) value: &'w T, pub(crate) ticks: Ticks<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'static Location<'static>, } @@ -559,12 +609,15 @@ impl<'w, T: Resource> Res<'w, T> { /// /// Note that unless you actually need an instance of `Res`, you should /// prefer to just convert it to `&T` which can be freely copied. - #[allow(clippy::should_implement_trait)] + #[expect( + clippy::should_implement_trait, + reason = "As this struct derefs to the inner resource, a `Clone` trait implementation would interfere with the common case of cloning the inner content. (A similar case of this happening can be found with `std::cell::Ref::clone()`.)" + )] pub fn clone(this: &Self) -> Self { Self { value: this.value, ticks: this.ticks.clone(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: this.changed_by, } } @@ -582,7 +635,7 @@ impl<'w, T: Resource> From> for Res<'w, T> { Self { value: res.value, ticks: res.ticks.into(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: res.changed_by, } } @@ -595,7 +648,7 @@ impl<'w, T: Resource> From> for Ref<'w, T> { Self { value: res.value, ticks: res.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: res.changed_by, } } @@ -622,13 +675,13 @@ impl_debug!(Res<'w, T>, Resource); /// If you need a shared borrow, use [`Res`] instead. /// /// This [`SystemParam`](crate::system::SystemParam) fails validation if resource doesn't exist. -/// This will cause systems that use this parameter to be skipped. +/// /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Use [`Option>`] instead if the resource might not always exist. pub struct ResMut<'w, T: ?Sized + Resource> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'w mut &'static Location<'static>, } @@ -669,7 +722,7 @@ impl<'w, T: Resource> From> for Mut<'w, T> { Mut { value: other.value, ticks: other.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: other.changed_by, } } @@ -683,13 +736,13 @@ impl<'w, T: Resource> From> for Mut<'w, T> { /// over to another thread. /// /// This [`SystemParam`](crate::system::SystemParam) fails validation if non-send resource doesn't exist. -/// This will cause systems that use this parameter to be skipped. +/// /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Use [`Option>`] instead if the resource might not always exist. pub struct NonSendMut<'w, T: ?Sized + 'static> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'w mut &'static Location<'static>, } @@ -705,7 +758,7 @@ impl<'w, T: 'static> From> for Mut<'w, T> { Mut { value: other.value, ticks: other.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: other.changed_by, } } @@ -738,7 +791,7 @@ impl<'w, T: 'static> From> for Mut<'w, T> { pub struct Ref<'w, T: ?Sized> { pub(crate) value: &'w T, pub(crate) ticks: Ticks<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'static Location<'static>, } @@ -756,7 +809,7 @@ impl<'w, T: ?Sized> Ref<'w, T> { Ref { value: f(self.value), ticks: self.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -778,7 +831,7 @@ impl<'w, T: ?Sized> Ref<'w, T> { changed: &'w Tick, last_run: Tick, this_run: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> Ref<'w, T> { Ref { value, @@ -788,7 +841,7 @@ impl<'w, T: ?Sized> Ref<'w, T> { last_run, this_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: caller, } } @@ -871,7 +924,7 @@ impl_debug!(Ref<'w, T>,); pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, pub(crate) ticks: TicksMut<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'w mut &'static Location<'static>, } @@ -897,7 +950,7 @@ impl<'w, T: ?Sized> Mut<'w, T> { last_changed: &'w mut Tick, last_run: Tick, this_run: Tick, - #[cfg(feature = "track_change_detection")] caller: &'w mut &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'w mut &'static Location<'static>, ) -> Self { Self { value, @@ -907,7 +960,7 @@ impl<'w, T: ?Sized> Mut<'w, T> { last_run, this_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: caller, } } @@ -918,7 +971,7 @@ impl<'w, T: ?Sized> From> for Ref<'w, T> { Self { value: mut_ref.value, ticks: mut_ref.ticks.into(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: mut_ref.changed_by, } } @@ -965,7 +1018,7 @@ impl_debug!(Mut<'w, T>,); pub struct MutUntyped<'w> { pub(crate) value: PtrMut<'w>, pub(crate) ticks: TicksMut<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(crate) changed_by: &'w mut &'static Location<'static>, } @@ -991,7 +1044,7 @@ impl<'w> MutUntyped<'w> { last_run: self.ticks.last_run, this_run: self.ticks.this_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -1043,7 +1096,7 @@ impl<'w> MutUntyped<'w> { Mut { value: f(self.value), ticks: self.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -1058,7 +1111,7 @@ impl<'w> MutUntyped<'w> { value: unsafe { self.value.deref_mut() }, ticks: self.ticks, // SAFETY: `caller` is `Aligned`. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: self.changed_by, } } @@ -1085,7 +1138,7 @@ impl<'w> DetectChanges for MutUntyped<'w> { } #[inline] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] fn changed_by(&self) -> &'static Location<'static> { self.changed_by } @@ -1098,7 +1151,7 @@ impl<'w> DetectChangesMut for MutUntyped<'w> { #[track_caller] fn set_changed(&mut self) { *self.ticks.changed = self.ticks.this_run; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by = Location::caller(); } @@ -1108,7 +1161,7 @@ impl<'w> DetectChangesMut for MutUntyped<'w> { #[track_caller] fn set_last_changed(&mut self, last_changed: Tick) { *self.ticks.changed = last_changed; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by = Location::caller(); } @@ -1134,13 +1187,13 @@ impl<'w, T> From> for MutUntyped<'w> { MutUntyped { value: value.value.into(), ticks: value.ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: value.changed_by, } } } -/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_change_detection` feature is +/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_location` feature is /// enabled, and the unit type `()` when it is not. /// /// This is primarily used in places where `#[cfg(...)]` attributes are not allowed, such as @@ -1148,10 +1201,10 @@ impl<'w, T> From> for MutUntyped<'w> { /// `Location` at all. /// /// Please use this type sparingly: prefer normal `#[cfg(...)]` attributes when possible. -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] pub(crate) type MaybeLocation = &'static Location<'static>; -/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_change_detection` feature is +/// A type alias to [`&'static Location<'static>`](std::panic::Location) when the `track_location` feature is /// enabled, and the unit type `()` when it is not. /// /// This is primarily used in places where `#[cfg(...)]` attributes are not allowed, such as @@ -1159,36 +1212,36 @@ pub(crate) type MaybeLocation = &'static Location<'static>; /// `Location` at all. /// /// Please use this type sparingly: prefer normal `#[cfg(...)]` attributes when possible. -#[cfg(not(feature = "track_change_detection"))] +#[cfg(not(feature = "track_location"))] pub(crate) type MaybeLocation = (); -/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_change_detection` +/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_location` /// feature is enabled, and the unit type `()` when it is not. /// /// See [`MaybeLocation`] for further information. -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] pub(crate) type MaybeUnsafeCellLocation<'a> = &'a UnsafeCell<&'static Location<'static>>; -/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_change_detection` +/// A type alias to `&UnsafeCell<&'static Location<'static>>` when the `track_location` /// feature is enabled, and the unit type `()` when it is not. /// /// See [`MaybeLocation`] for further information. -#[cfg(not(feature = "track_change_detection"))] +#[cfg(not(feature = "track_location"))] pub(crate) type MaybeUnsafeCellLocation<'a> = (); /// A type alias to `ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>` when the -/// `track_change_detection` feature is enabled, and the unit type `()` when it is not. +/// `track_location` feature is enabled, and the unit type `()` when it is not. /// /// See [`MaybeLocation`] for further information. -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] pub(crate) type MaybeThinSlicePtrLocation<'w> = ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>; /// A type alias to `ThinSlicePtr<'w, UnsafeCell<&'static Location<'static>>>` when the -/// `track_change_detection` feature is enabled, and the unit type `()` when it is not. +/// `track_location` feature is enabled, and the unit type `()` when it is not. /// /// See [`MaybeLocation`] for further information. -#[cfg(not(feature = "track_change_detection"))] +#[cfg(not(feature = "track_location"))] pub(crate) type MaybeThinSlicePtrLocation<'w> = (); #[cfg(test)] @@ -1197,7 +1250,7 @@ mod tests { use bevy_ptr::PtrMut; use bevy_reflect::{FromType, ReflectFromPtr}; use core::ops::{Deref, DerefMut}; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] use core::panic::Location; use crate::{ @@ -1329,13 +1382,13 @@ mod tests { this_run: Tick::new(4), }; let mut res = R {}; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let res_mut = ResMut { value: &mut res, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut caller, }; @@ -1353,7 +1406,7 @@ mod tests { changed: Tick::new(3), }; let mut res = R {}; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let val = Mut::new( @@ -1362,7 +1415,7 @@ mod tests { &mut component_ticks.changed, Tick::new(2), // last_run Tick::new(4), // this_run - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] &mut caller, ); @@ -1383,13 +1436,13 @@ mod tests { this_run: Tick::new(4), }; let mut res = R {}; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let non_send_mut = NonSendMut { value: &mut res, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut caller, }; @@ -1419,13 +1472,13 @@ mod tests { }; let mut outer = Outer(0); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let ptr = Mut { value: &mut outer, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut caller, }; assert!(!ptr.is_changed()); @@ -1509,13 +1562,13 @@ mod tests { }; let mut value: i32 = 5; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let value = MutUntyped { value: PtrMut::from(&mut value), ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut caller, }; @@ -1547,13 +1600,13 @@ mod tests { this_run: Tick::new(4), }; let mut c = C {}; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let mut caller = Location::caller(); let mut_typed = Mut { value: &mut c, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut caller, }; diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index f0cd6a6b5bcaf..66d5db98f8fcb 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -19,7 +19,7 @@ use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use bevy_utils::{HashMap, HashSet, TypeIdMap}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{ alloc::Layout, @@ -1901,14 +1901,14 @@ pub enum RequiredComponentsError { } /// A Required Component constructor. See [`Component`] for details. -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] #[derive(Clone)] pub struct RequiredComponentConstructor( pub Arc)>, ); /// A Required Component constructor. See [`Component`] for details. -#[cfg(not(feature = "track_change_detection"))] +#[cfg(not(feature = "track_location"))] #[derive(Clone)] pub struct RequiredComponentConstructor( pub Arc, @@ -1931,7 +1931,7 @@ impl RequiredComponentConstructor { change_tick: Tick, table_row: TableRow, entity: Entity, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { (self.0)( table, @@ -1939,7 +1939,7 @@ impl RequiredComponentConstructor { change_tick, table_row, entity, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -2046,7 +2046,7 @@ impl RequiredComponents { #[cfg(feature = "portable-atomic")] use alloc::boxed::Box; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] type Constructor = dyn for<'a, 'b> Fn( &'a mut Table, &'b mut SparseSets, @@ -2056,7 +2056,7 @@ impl RequiredComponents { &'static Location<'static>, ); - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] type Constructor = dyn for<'a, 'b> Fn(&'a mut Table, &'b mut SparseSets, Tick, TableRow, Entity); @@ -2072,7 +2072,7 @@ impl RequiredComponents { change_tick, table_row, entity, - #[cfg(feature = "track_change_detection")] caller| { + #[cfg(feature = "track_location")] caller| { OwningPtr::make(constructor(), |ptr| { // SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will // pass in a valid table_row and entity requiring a C constructor @@ -2088,7 +2088,7 @@ impl RequiredComponents { component_id, C::STORAGE_TYPE, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index b15723aa57419..2b51c1f937b34 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -147,10 +147,10 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { if self.target_component_written { panic!("Trying to write component '{short_name}' multiple times") } - if !self + if self .component_info .type_id() - .is_some_and(|id| id == TypeId::of::()) + .is_none_or(|id| id != TypeId::of::()) { panic!("TypeId of component '{short_name}' does not match source component TypeId") }; @@ -671,6 +671,7 @@ mod tests { entity::EntityCloneBuilder, world::{DeferredWorld, World}, }; + use alloc::vec::Vec; use bevy_ecs_macros::require; use bevy_ptr::OwningPtr; use core::alloc::Layout; @@ -679,6 +680,7 @@ mod tests { mod reflect { use super::*; use crate::reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}; + use alloc::vec; use bevy_reflect::{std_traits::ReflectDefault, FromType, Reflect, ReflectFromPtr}; #[test] diff --git a/crates/bevy_ecs/src/entity/entity_set.rs b/crates/bevy_ecs/src/entity/entity_set.rs index 34e48551fde7b..e2d77a98fd46a 100644 --- a/crates/bevy_ecs/src/entity/entity_set.rs +++ b/crates/bevy_ecs/src/entity/entity_set.rs @@ -135,6 +135,7 @@ unsafe impl TrustedEntityBorrow for Arc {} /// [`into_iter()`]: IntoIterator::into_iter /// [`iter_many_unique`]: crate::system::Query::iter_many_unique /// [`iter_many_unique_mut`]: crate::system::Query::iter_many_unique_mut +/// [`Vec`]: alloc::vec::Vec pub trait EntitySet: IntoIterator {} impl> EntitySet for T {} @@ -379,25 +380,26 @@ impl + Debug> Debug for UniqueEntityIter< #[cfg(test)] mod tests { - #[allow(unused_imports)] + use alloc::{vec, vec::Vec}; + use crate::prelude::{Schedule, World}; - #[allow(unused_imports)] use crate::component::Component; + use crate::entity::Entity; use crate::query::{QueryState, With}; use crate::system::Query; use crate::world::Mut; - #[allow(unused_imports)] use crate::{self as bevy_ecs}; - #[allow(unused_imports)] - use crate::{entity::Entity, world::unsafe_world_cell}; use super::UniqueEntityIter; #[derive(Component, Clone)] pub struct Thing; - #[allow(clippy::iter_skip_zero)] + #[expect( + clippy::iter_skip_zero, + reason = "The `skip(0)` is used to ensure that the `Skip` iterator implements `EntitySet`, which is needed to pass the iterator as the `entities` parameter." + )] #[test] fn preserving_uniqueness() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/entity/hash.rs b/crates/bevy_ecs/src/entity/hash.rs index 2e7c8ff2a3fc6..b7d4dcae54586 100644 --- a/crates/bevy_ecs/src/entity/hash.rs +++ b/crates/bevy_ecs/src/entity/hash.rs @@ -2,12 +2,9 @@ use core::hash::{BuildHasher, Hasher}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -use bevy_utils::hashbrown; - -use super::Entity; /// A [`BuildHasher`] that results in a [`EntityHasher`]. -#[derive(Default, Clone)] +#[derive(Debug, Default, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct EntityHash; @@ -20,7 +17,7 @@ impl BuildHasher for EntityHash { } /// A very fast hash that is only designed to work on generational indices -/// like [`Entity`]. It will panic if attempting to hash a type containing +/// like [`Entity`](super::Entity). It will panic if attempting to hash a type containing /// non-u64 fields. /// /// This is heavily optimized for typical cases, where you have mostly live @@ -78,21 +75,3 @@ impl Hasher for EntityHasher { self.hash = bits.wrapping_mul(UPPER_PHI); } } - -/// A [`HashMap`](hashbrown::HashMap) pre-configured to use [`EntityHash`] hashing. -pub type EntityHashMap = hashbrown::HashMap; - -/// A [`HashSet`](hashbrown::HashSet) pre-configured to use [`EntityHash`] hashing. -pub type EntityHashSet = hashbrown::HashSet; - -#[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!(EntityHashMap::: Clone); - // EntityHashMap should implement Reflect - #[cfg(feature = "bevy_reflect")] - assert_impl_all!(EntityHashMap::: Reflect); -} diff --git a/crates/bevy_ecs/src/entity/hash_map.rs b/crates/bevy_ecs/src/entity/hash_map.rs new file mode 100644 index 0000000000000..20ec6767baa46 --- /dev/null +++ b/crates/bevy_ecs/src/entity/hash_map.rs @@ -0,0 +1,279 @@ +use core::{ + fmt::{self, Debug, Formatter}, + iter::FusedIterator, + marker::PhantomData, + ops::{Deref, DerefMut, Index}, +}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use bevy_utils::hashbrown::hash_map::{self, HashMap}; + +use super::{Entity, EntityHash, EntitySetIterator, TrustedEntityBorrow}; + +/// A [`HashMap`] pre-configured to use [`EntityHash`] hashing. +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EntityHashMap(pub(crate) HashMap); + +impl EntityHashMap { + /// Creates an empty `EntityHashMap`. + /// + /// Equivalent to [`HashMap::with_hasher(EntityHash)`]. + /// + /// [`HashMap::with_hasher(EntityHash)`]: HashMap::with_hasher + pub fn new() -> Self { + Self(HashMap::with_hasher(EntityHash)) + } + + /// Creates an empty `EntityHashMap` with the specified capacity. + /// + /// Equivalent to [`HashMap::with_capacity_and_hasher(n, EntityHash)`]. + /// + /// [`HashMap:with_capacity_and_hasher(n, EntityHash)`]: HashMap::with_capacity_and_hasher + pub fn with_capacity(n: usize) -> Self { + Self(HashMap::with_capacity_and_hasher(n, EntityHash)) + } + + /// Returns the inner [`HashMap`]. + pub fn into_inner(self) -> HashMap { + self.0 + } + + /// An iterator visiting all keys in arbitrary order. + /// The iterator element type is `&'a Entity`. + /// + /// Equivalent to [`HashMap::keys`]. + pub fn keys(&self) -> Keys<'_, V> { + Keys(self.0.keys(), PhantomData) + } + + /// Creates a consuming iterator visiting all the keys in arbitrary order. + /// The map cannot be used after calling this. + /// The iterator element type is [`Entity`]. + /// + /// Equivalent to [`HashMap::into_keys`]. + pub fn into_keys(self) -> IntoKeys { + IntoKeys(self.0.into_keys(), PhantomData) + } +} + +impl Default for EntityHashMap { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Deref for EntityHashMap { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EntityHashMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a, V: Copy> Extend<&'a (Entity, V)> for EntityHashMap { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl<'a, V: Copy> Extend<(&'a Entity, &'a V)> for EntityHashMap { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl Extend<(Entity, V)> for EntityHashMap { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl From<[(Entity, V); N]> for EntityHashMap { + fn from(value: [(Entity, V); N]) -> Self { + Self(HashMap::from_iter(value)) + } +} + +impl FromIterator<(Entity, V)> for EntityHashMap { + fn from_iter>(iterable: I) -> Self { + Self(HashMap::from_iter(iterable)) + } +} + +impl Index<&Q> for EntityHashMap { + type Output = V; + fn index(&self, key: &Q) -> &V { + self.0.index(&key.entity()) + } +} + +impl<'a, V> IntoIterator for &'a EntityHashMap { + type Item = (&'a Entity, &'a V); + type IntoIter = hash_map::Iter<'a, Entity, V>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'a, V> IntoIterator for &'a mut EntityHashMap { + type Item = (&'a Entity, &'a mut V); + type IntoIter = hash_map::IterMut<'a, Entity, V>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + +impl IntoIterator for EntityHashMap { + type Item = (Entity, V); + type IntoIter = hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +/// An iterator over the keys of a [`EntityHashMap`] in arbitrary order. +/// The iterator element type is `&'a Entity`. +/// +/// /// This struct is created by the [`keys`] method on [`EntityHashMap`]. See its documentation for more. +/// +/// [`keys`]: EntityHashMap::keys +pub struct Keys<'a, V, S = EntityHash>(hash_map::Keys<'a, Entity, V>, PhantomData); + +impl<'a, V> Keys<'a, V> { + /// Returns the inner [`Keys`](hash_map::Keys). + pub fn into_inner(self) -> hash_map::Keys<'a, Entity, V> { + self.0 + } +} + +impl<'a, V> Deref for Keys<'a, V> { + type Target = hash_map::Keys<'a, Entity, V>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Keys<'_, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a, V> Iterator for Keys<'a, V> { + type Item = &'a Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for Keys<'_, V> {} + +impl FusedIterator for Keys<'_, V> {} + +impl Clone for Keys<'_, V> { + fn clone(&self) -> Self { + Self(self.0.clone(), PhantomData) + } +} + +impl Debug for Keys<'_, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Keys").field(&self.0).field(&self.1).finish() + } +} + +impl Default for Keys<'_, V> { + fn default() -> Self { + Self(Default::default(), PhantomData) + } +} + +// SAFETY: Keys stems from a correctly behaving `HashMap`. +unsafe impl EntitySetIterator for Keys<'_, V> {} + +/// An owning iterator over the keys of a [`EntityHashMap`] in arbitrary order. +/// The iterator element type is [`Entity`]. +/// +/// This struct is created by the [`into_keys`] method on [`EntityHashMap`]. +/// See its documentation for more. +/// The map cannot be used after calling that method. +/// +/// [`into_keys`]: EntityHashMap::into_keys +pub struct IntoKeys(hash_map::IntoKeys, PhantomData); + +impl IntoKeys { + /// Returns the inner [`IntoKeys`](hash_map::IntoKeys). + pub fn into_inner(self) -> hash_map::IntoKeys { + self.0 + } +} + +impl Deref for IntoKeys { + type Target = hash_map::IntoKeys; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for IntoKeys { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Iterator for IntoKeys { + type Item = Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for IntoKeys {} + +impl FusedIterator for IntoKeys {} + +impl Debug for IntoKeys { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("IntoKeys") + .field(&self.0) + .field(&self.1) + .finish() + } +} + +impl Default for IntoKeys { + fn default() -> Self { + Self(Default::default(), PhantomData) + } +} + +// SAFETY: IntoKeys stems from a correctly behaving `HashMap`. +unsafe impl EntitySetIterator for IntoKeys {} + +#[cfg(test)] +mod tests { + use super::*; + use bevy_reflect::Reflect; + use static_assertions::assert_impl_all; + + // Check that the HashMaps are Clone if the key/values are Clone + assert_impl_all!(EntityHashMap::: Clone); + // EntityHashMap should implement Reflect + #[cfg(feature = "bevy_reflect")] + assert_impl_all!(EntityHashMap::: Reflect); +} diff --git a/crates/bevy_ecs/src/entity/hash_set.rs b/crates/bevy_ecs/src/entity/hash_set.rs new file mode 100644 index 0000000000000..12538d873b5c9 --- /dev/null +++ b/crates/bevy_ecs/src/entity/hash_set.rs @@ -0,0 +1,415 @@ +use core::{ + fmt::{self, Debug, Formatter}, + iter::FusedIterator, + marker::PhantomData, + ops::{ + BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Deref, DerefMut, Sub, + SubAssign, + }, +}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use bevy_utils::hashbrown::hash_set::{self, HashSet}; + +use super::{Entity, EntityHash, EntitySetIterator}; + +/// A [`HashSet`] pre-configured to use [`EntityHash`] hashing. +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct EntityHashSet(pub(crate) HashSet); + +impl EntityHashSet { + /// Creates an empty `EntityHashSet`. + /// + /// Equivalent to [`HashSet::with_hasher(EntityHash)`]. + /// + /// [`HashSet::with_hasher(EntityHash)`]: HashSet::with_hasher + pub fn new() -> Self { + Self(HashSet::with_hasher(EntityHash)) + } + + /// Creates an empty `EntityHashSet` with the specified capacity. + /// + /// Equivalent to [`HashSet::with_capacity_and_hasher(n, EntityHash)`]. + /// + /// [`HashSet::with_capacity_and_hasher(n, EntityHash)`]: HashSet::with_capacity_and_hasher + pub fn with_capacity(n: usize) -> Self { + Self(HashSet::with_capacity_and_hasher(n, EntityHash)) + } + + /// Returns the inner [`HashSet`]. + pub fn into_inner(self) -> HashSet { + self.0 + } + + /// Clears the set, returning all elements in an iterator. + /// + /// Equivalent to [`HashSet::drain`]. + pub fn drain(&mut self) -> Drain<'_> { + Drain(self.0.drain(), PhantomData) + } + + /// An iterator visiting all elements in arbitrary order. + /// The iterator element type is `&'a Entity`. + /// + /// Equivalent to [`HashSet::iter`]. + pub fn iter(&self) -> Iter<'_> { + Iter(self.0.iter(), PhantomData) + } + + /// Drains elements which are true under the given predicate, + /// and returns an iterator over the removed items. + /// + /// Equivalent to [`HashSet::extract_if`]. + pub fn extract_if bool>(&mut self, f: F) -> ExtractIf<'_, F> { + ExtractIf(self.0.extract_if(f), PhantomData) + } +} + +impl Deref for EntityHashSet { + type Target = HashSet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EntityHashSet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> IntoIterator for &'a EntityHashSet { + type Item = &'a Entity; + + type IntoIter = Iter<'a>; + + fn into_iter(self) -> Self::IntoIter { + Iter((&self.0).into_iter(), PhantomData) + } +} + +impl IntoIterator for EntityHashSet { + type Item = Entity; + + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter(self.0.into_iter(), PhantomData) + } +} + +impl BitAnd for &EntityHashSet { + type Output = EntityHashSet; + + fn bitand(self, rhs: Self) -> Self::Output { + EntityHashSet(self.0.bitand(&rhs.0)) + } +} + +impl BitAndAssign<&EntityHashSet> for EntityHashSet { + fn bitand_assign(&mut self, rhs: &Self) { + self.0.bitand_assign(&rhs.0); + } +} + +impl BitOr for &EntityHashSet { + type Output = EntityHashSet; + + fn bitor(self, rhs: Self) -> Self::Output { + EntityHashSet(self.0.bitor(&rhs.0)) + } +} + +impl BitOrAssign<&EntityHashSet> for EntityHashSet { + fn bitor_assign(&mut self, rhs: &Self) { + self.0.bitor_assign(&rhs.0); + } +} + +impl BitXor for &EntityHashSet { + type Output = EntityHashSet; + + fn bitxor(self, rhs: Self) -> Self::Output { + EntityHashSet(self.0.bitxor(&rhs.0)) + } +} + +impl BitXorAssign<&EntityHashSet> for EntityHashSet { + fn bitxor_assign(&mut self, rhs: &Self) { + self.0.bitxor_assign(&rhs.0); + } +} + +impl Sub for &EntityHashSet { + type Output = EntityHashSet; + + fn sub(self, rhs: Self) -> Self::Output { + EntityHashSet(self.0.sub(&rhs.0)) + } +} + +impl SubAssign<&EntityHashSet> for EntityHashSet { + fn sub_assign(&mut self, rhs: &Self) { + self.0.sub_assign(&rhs.0); + } +} + +impl<'a> Extend<&'a Entity> for EntityHashSet { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl Extend for EntityHashSet { + fn extend>(&mut self, iter: T) { + self.0.extend(iter); + } +} + +impl From<[Entity; N]> for EntityHashSet { + fn from(value: [Entity; N]) -> Self { + Self(HashSet::from_iter(value)) + } +} + +impl FromIterator for EntityHashSet { + fn from_iter>(iterable: I) -> Self { + Self(HashSet::from_iter(iterable)) + } +} + +/// An iterator over the items of an [`EntityHashSet`]. +/// +/// This struct is created by the [`iter`] method on [`EntityHashSet`]. See its documentation for more. +/// +/// [`iter`]: EntityHashSet::iter +pub struct Iter<'a, S = EntityHash>(hash_set::Iter<'a, Entity>, PhantomData); + +impl<'a> Iter<'a> { + /// Returns the inner [`Iter`](hash_set::Iter). + pub fn into_inner(self) -> hash_set::Iter<'a, Entity> { + self.0 + } +} + +impl<'a> Deref for Iter<'a> { + type Target = hash_set::Iter<'a, Entity>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Iter<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = &'a Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for Iter<'_> {} + +impl FusedIterator for Iter<'_> {} + +impl Clone for Iter<'_> { + fn clone(&self) -> Self { + Self(self.0.clone(), PhantomData) + } +} + +impl Debug for Iter<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Iter").field(&self.0).field(&self.1).finish() + } +} + +impl Default for Iter<'_> { + fn default() -> Self { + Self(Default::default(), PhantomData) + } +} + +// SAFETY: Iter stems from a correctly behaving `HashSet`. +unsafe impl EntitySetIterator for Iter<'_> {} + +/// Owning iterator over the items of an [`EntityHashSet`]. +/// +/// This struct is created by the [`into_iter`] method on [`EntityHashSet`] (provided by the [`IntoIterator`] trait). See its documentation for more. +/// +/// [`into_iter`]: EntityHashSet::into_iter +pub struct IntoIter(hash_set::IntoIter, PhantomData); + +impl IntoIter { + /// Returns the inner [`IntoIter`](hash_set::IntoIter). + pub fn into_inner(self) -> hash_set::IntoIter { + self.0 + } +} + +impl Deref for IntoIter { + type Target = hash_set::IntoIter; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for IntoIter { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Iterator for IntoIter { + type Item = Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for IntoIter {} + +impl FusedIterator for IntoIter {} + +impl Debug for IntoIter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("IntoIter") + .field(&self.0) + .field(&self.1) + .finish() + } +} + +impl Default for IntoIter { + fn default() -> Self { + Self(Default::default(), PhantomData) + } +} + +// SAFETY: IntoIter stems from a correctly behaving `HashSet`. +unsafe impl EntitySetIterator for IntoIter {} + +/// A draining iterator over the items of an [`EntityHashSet`]. +/// +/// This struct is created by the [`drain`] method on [`EntityHashSet`]. See its documentation for more. +/// +/// [`drain`]: EntityHashSet::drain +pub struct Drain<'a, S = EntityHash>(hash_set::Drain<'a, Entity>, PhantomData); + +impl<'a> Drain<'a> { + /// Returns the inner [`Drain`](hash_set::Drain). + pub fn into_inner(self) -> hash_set::Drain<'a, Entity> { + self.0 + } +} + +impl<'a> Deref for Drain<'a> { + type Target = hash_set::Drain<'a, Entity>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Drain<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> Iterator for Drain<'a> { + type Item = Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl ExactSizeIterator for Drain<'_> {} + +impl FusedIterator for Drain<'_> {} + +impl Debug for Drain<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("Drain") + .field(&self.0) + .field(&self.1) + .finish() + } +} + +// SAFETY: Drain stems from a correctly behaving `HashSet`. +unsafe impl EntitySetIterator for Drain<'_> {} + +/// A draining iterator over entries of a [`EntityHashSet`] which don't satisfy the predicate `f`. +/// +/// This struct is created by the [`extract_if`] method on [`EntityHashSet`]. See its documentation for more. +/// +/// [`extract_if`]: EntityHashSet::extract_if +pub struct ExtractIf<'a, F: FnMut(&Entity) -> bool, S = EntityHash>( + hash_set::ExtractIf<'a, Entity, F>, + PhantomData, +); + +impl<'a, F: FnMut(&Entity) -> bool> ExtractIf<'a, F> { + /// Returns the inner [`ExtractIf`](hash_set::ExtractIf). + pub fn into_inner(self) -> hash_set::ExtractIf<'a, Entity, F> { + self.0 + } +} + +impl<'a, F: FnMut(&Entity) -> bool> Deref for ExtractIf<'a, F> { + type Target = hash_set::ExtractIf<'a, Entity, F>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl bool> DerefMut for ExtractIf<'_, F> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a, F: FnMut(&Entity) -> bool> Iterator for ExtractIf<'a, F> { + type Item = Entity; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl bool> FusedIterator for ExtractIf<'_, F> {} + +impl bool> Debug for ExtractIf<'_, F> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("ExtractIf").finish() + } +} + +// SAFETY: ExtractIf stems from a correctly behaving `HashSet`. +unsafe impl bool> EntitySetIterator for ExtractIf<'_, F> {} + +// SAFETY: Difference stems from two correctly behaving `HashSet`s. +unsafe impl EntitySetIterator for hash_set::Difference<'_, Entity, EntityHash> {} + +// SAFETY: Intersection stems from two correctly behaving `HashSet`s. +unsafe impl EntitySetIterator for hash_set::Intersection<'_, Entity, EntityHash> {} + +// SAFETY: SymmetricDifference stems from two correctly behaving `HashSet`s. +unsafe impl EntitySetIterator for hash_set::SymmetricDifference<'_, Entity, EntityHash> {} + +// SAFETY: Union stems from two correctly behaving `HashSet`s. +unsafe impl EntitySetIterator for hash_set::Union<'_, Entity, EntityHash> {} diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 4932c5fc110bf..946b6821a4e40 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -53,6 +53,12 @@ pub use visit_entities::*; mod hash; pub use hash::*; +mod hash_map; +mod hash_set; + +pub use hash_map::EntityHashMap; +pub use hash_set::EntityHashSet; + use crate::{ archetype::{ArchetypeId, ArchetypeRow}, identifier::{ @@ -63,11 +69,11 @@ use crate::{ }, storage::{SparseSetIndex, TableId, TableRow}, }; -use alloc::{borrow::ToOwned, string::String, vec::Vec}; +use alloc::vec::Vec; use core::{fmt, hash::Hash, mem, num::NonZero}; use log::warn; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; #[cfg(feature = "serialize")] @@ -586,7 +592,14 @@ impl Entities { /// Reserve entity IDs concurrently. /// /// Storage for entity generation and location is lazily allocated by calling [`flush`](Entities::flush). - #[allow(clippy::unnecessary_fallible_conversions)] // Because `IdCursor::try_from` may fail on 32-bit platforms. + #[expect( + clippy::allow_attributes, + reason = "`clippy::unnecessary_fallible_conversions` may not always lint." + )] + #[allow( + clippy::unnecessary_fallible_conversions, + reason = "`IdCursor::try_from` may fail on 32-bit platforms." + )] pub fn reserve_entities(&self, count: u32) -> ReserveEntitiesIterator { // Use one atomic subtract to grab a range of new IDs. The range might be // entirely nonnegative, meaning all IDs come from the freelist, or entirely @@ -780,7 +793,14 @@ impl Entities { } /// Ensure at least `n` allocations can succeed without reallocating. - #[allow(clippy::unnecessary_fallible_conversions)] // Because `IdCursor::try_from` may fail on 32-bit platforms. + #[expect( + clippy::allow_attributes, + reason = "`clippy::unnecessary_fallible_conversions` may not always lint." + )] + #[allow( + clippy::unnecessary_fallible_conversions, + reason = "`IdCursor::try_from` may fail on 32-bit platforms." + )] pub fn reserve(&mut self, additional: u32) { self.verify_flushed(); @@ -798,7 +818,7 @@ impl Entities { // not reallocated since the generation is incremented in `free` pub fn contains(&self, entity: Entity) -> bool { self.resolve_from_id(entity.index()) - .map_or(false, |e| e.generation() == entity.generation()) + .is_some_and(|e| e.generation() == entity.generation()) } /// Clears all [`Entity`] from the World. @@ -962,7 +982,7 @@ impl Entities { /// Sets the source code location from which this entity has last been spawned /// or despawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] #[inline] pub(crate) fn set_spawned_or_despawned_by(&mut self, index: u32, caller: &'static Location) { let meta = self @@ -973,31 +993,59 @@ impl Entities { } /// Returns the source code location from which this entity has last been spawned - /// or despawned. Returns `None` if this entity has never existed. - #[cfg(feature = "track_change_detection")] + /// or despawned. Returns `None` if its index has been reused by another entity + /// or if this entity has never existed. + #[cfg(feature = "track_location")] pub fn entity_get_spawned_or_despawned_by( &self, entity: Entity, ) -> Option<&'static Location<'static>> { self.meta .get(entity.index() as usize) + .filter(|meta| + // Generation is incremented immediately upon despawn + (meta.generation == entity.generation) + || (meta.location.archetype_id == ArchetypeId::INVALID) + && (meta.generation == IdentifierMask::inc_masked_high_by(entity.generation, 1))) .and_then(|meta| meta.spawned_or_despawned_by) } /// Constructs a message explaining why an entity does not exists, if known. - pub(crate) fn entity_does_not_exist_error_details_message(&self, _entity: Entity) -> String { - #[cfg(feature = "track_change_detection")] - { - if let Some(location) = self.entity_get_spawned_or_despawned_by(_entity) { - format!("was despawned by {location}",) - } else { - "was never spawned".to_owned() - } + pub(crate) fn entity_does_not_exist_error_details( + &self, + _entity: Entity, + ) -> EntityDoesNotExistDetails { + EntityDoesNotExistDetails { + #[cfg(feature = "track_location")] + location: self.entity_get_spawned_or_despawned_by(_entity), } - #[cfg(not(feature = "track_change_detection"))] - { - "does not exist (enable `track_change_detection` feature for more details)".to_owned() + } +} + +/// Helper struct that, when printed, will write the appropriate details +/// regarding an entity that did not exist. +#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub struct EntityDoesNotExistDetails { + #[cfg(feature = "track_location")] + location: Option<&'static Location<'static>>, +} + +impl fmt::Display for EntityDoesNotExistDetails { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[cfg(feature = "track_location")] + if let Some(location) = self.location { + write!(f, "was despawned by {location}") + } else { + write!( + f, + "does not exist (index has been reused or was never spawned)" + ) } + #[cfg(not(feature = "track_location"))] + write!( + f, + "does not exist (enable `track_location` feature for more details)" + ) } } @@ -1008,7 +1056,7 @@ struct EntityMeta { /// The current location of the [`Entity`] pub location: EntityLocation, /// Location of the last spawn or despawn of this entity - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] spawned_or_despawned_by: Option<&'static Location<'static>>, } @@ -1017,7 +1065,7 @@ impl EntityMeta { const EMPTY: EntityMeta = EntityMeta { generation: NonZero::::MIN, location: EntityLocation::INVALID, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] spawned_or_despawned_by: None, }; } @@ -1059,6 +1107,7 @@ impl EntityLocation { #[cfg(test)] mod tests { use super::*; + use alloc::format; #[test] fn entity_niche_optimization() { @@ -1143,7 +1192,10 @@ mod tests { } #[test] - #[allow(clippy::nonminimal_bool)] // This is intentionally testing `lt` and `ge` as separate functions. + #[expect( + clippy::nonminimal_bool, + reason = "This intentionally tests all possible comparison operators as separate functions; thus, we don't want to rewrite these comparisons to use different operators." + )] fn entity_comparison() { assert_eq!( Entity::from_raw_and_generation(123, NonZero::::new(456).unwrap()), diff --git a/crates/bevy_ecs/src/entity/visit_entities.rs b/crates/bevy_ecs/src/entity/visit_entities.rs index abce76853d403..0896d7208100a 100644 --- a/crates/bevy_ecs/src/entity/visit_entities.rs +++ b/crates/bevy_ecs/src/entity/visit_entities.rs @@ -61,6 +61,7 @@ mod tests { entity::{EntityHashMap, MapEntities, SceneEntityMapper}, world::World, }; + use alloc::{string::String, vec, vec::Vec}; use bevy_utils::HashSet; use super::*; @@ -70,7 +71,6 @@ mod tests { ordered: Vec, unordered: HashSet, single: Entity, - #[allow(dead_code)] #[visit_entities(ignore)] not_an_entity: String, } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 3106009d6be3b..b9f7ec8d05579 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -1,7 +1,10 @@ +use crate as bevy_ecs; +use crate::component::ComponentId; +use crate::world::World; use crate::{component::Component, traversal::Traversal}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{ cmp::Ordering, @@ -19,10 +22,6 @@ use core::{ /// /// This trait can be derived. /// -/// Events implement the [`Component`] type (and they automatically do when they are derived). Events are (generally) -/// not directly inserted as components. More often, the [`ComponentId`] is used to identify the event type within the -/// context of the ECS. -/// /// Events must be thread-safe. /// /// [`World`]: crate::world::World @@ -36,7 +35,7 @@ use core::{ label = "invalid `Event`", note = "consider annotating `{Self}` with `#[derive(Event)]`" )] -pub trait Event: Component { +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 @@ -48,8 +47,53 @@ pub trait Event: Component { /// [triggered]: crate::system::Commands::trigger_targets /// [`Trigger::propagate`]: crate::observer::Trigger::propagate const AUTO_PROPAGATE: bool = false; + + /// Generates the [`ComponentId`] for this event type. + /// + /// If this type has already been registered, + /// this will return the existing [`ComponentId`]. + /// + /// 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::>() + } + + /// Fetches the [`ComponentId`] for this event type, + /// if it has already been generated. + /// + /// 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 [`register_component_id`](Event::register_component_id). + fn component_id(world: &World) -> Option { + world.component_id::>() + } } +/// 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, +/// without requiring that [`Event`] types implement [`Component`] directly. +/// [`ComponentId`] is used internally as a unique identitifier for events because they are: +/// +/// - Unique to each event type. +/// - Can be quickly generated and looked up. +/// - Are compatible with dynamic event types, which aren't backed by a Rust type. +/// +/// This type is an implementation detail and should never be made public. +// TODO: refactor events to store their metadata on distinct entities, rather than using `ComponentId` +#[derive(Component)] +struct EventWrapperComponent(PhantomData); + /// An `EventId` uniquely identifies an event stored in a specific [`World`]. /// /// An `EventId` can among other things be used to trace the flow of an event from the point it was @@ -62,7 +106,7 @@ pub struct EventId { // This value corresponds to the order in which each event was added to the world. pub id: usize, /// The source code location that triggered this event. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub caller: &'static Location<'static>, #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] pub(super) _marker: PhantomData, diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index e5c3e43452d56..d35c4743ee648 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -4,7 +4,7 @@ use bevy_ecs::{ event::{Event, EventCursor, EventId, EventInstance}, system::Resource, }; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{ marker::PhantomData, @@ -126,7 +126,7 @@ impl Events { pub fn send(&mut self, event: E) -> EventId { self.send_with_caller( event, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -134,11 +134,11 @@ impl Events { pub(crate) fn send_with_caller( &mut self, event: E, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) -> EventId { let event_id = EventId { id: self.event_count, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, _marker: PhantomData, }; @@ -308,7 +308,7 @@ impl Extend for Events { let events = iter.into_iter().map(|event| { let event_id = EventId { id: event_count, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller: Location::caller(), _marker: PhantomData, }; @@ -379,7 +379,7 @@ impl Iterator for SendBatchIds { let result = Some(EventId { id: self.last_count, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller: Location::caller(), _marker: PhantomData, }); diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ca1be152e5caa..262d4e3636adc 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -74,7 +74,6 @@ impl Clone for EventCursor { } } -#[allow(clippy::len_without_is_empty)] // Check fails since the is_empty implementation has a signature other than `(&self) -> bool` impl EventCursor { /// See [`EventReader::read`](super::EventReader::read) pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { diff --git a/crates/bevy_ecs/src/event/iterators.rs b/crates/bevy_ecs/src/event/iterators.rs index 956072715c74a..b2f9421aa7d0c 100644 --- a/crates/bevy_ecs/src/event/iterators.rs +++ b/crates/bevy_ecs/src/event/iterators.rs @@ -145,6 +145,7 @@ pub struct EventParIter<'a, E: Event> { reader: &'a mut EventCursor, slices: [&'a [EventInstance]; 2], batching_strategy: BatchingStrategy, + #[cfg(not(target_arch = "wasm32"))] unread: usize, } @@ -170,6 +171,7 @@ impl<'a, E: Event> EventParIter<'a, E> { reader, slices: [a, b], batching_strategy: BatchingStrategy::default(), + #[cfg(not(target_arch = "wasm32"))] unread: unread_count, } } @@ -206,6 +208,10 @@ impl<'a, E: Event> EventParIter<'a, E> { /// initialized and run from the ECS scheduler, this should never panic. /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool + #[cfg_attr( + target_arch = "wasm32", + expect(unused_mut, reason = "not mutated on this target") + )] pub fn for_each_with_id) + Send + Sync + Clone>(mut self, func: FN) { #[cfg(target_arch = "wasm32")] { diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index f7f46af258a3c..c8bd24dcdde5f 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -7,7 +7,6 @@ mod mut_iterators; mod mutator; mod reader; mod registry; -mod send_event; mod update; mod writer; @@ -25,7 +24,6 @@ pub use mut_iterators::{EventMutIterator, EventMutIteratorWithId}; pub use mutator::EventMutator; pub use reader::EventReader; pub use registry::{EventRegistry, ShouldUpdateEvents}; -pub use send_event::SendEvent; pub use update::{ event_update_condition, event_update_system, signal_event_update_system, EventUpdates, }; @@ -34,6 +32,7 @@ pub use writer::EventWriter; #[cfg(test)] mod tests { use crate as bevy_ecs; + use alloc::{vec, vec::Vec}; use bevy_ecs::{event::*, system::assert_is_read_only_system}; use bevy_ecs_macros::Event; @@ -569,7 +568,6 @@ mod tests { assert!(last.is_none(), "EventMutator should be empty"); } - #[allow(clippy::iter_nth_zero)] #[test] fn test_event_reader_iter_nth() { use bevy_ecs::prelude::*; @@ -596,7 +594,6 @@ mod tests { schedule.run(&mut world); } - #[allow(clippy::iter_nth_zero)] #[test] fn test_event_mutator_iter_nth() { use bevy_ecs::prelude::*; diff --git a/crates/bevy_ecs/src/event/mut_iterators.rs b/crates/bevy_ecs/src/event/mut_iterators.rs index f8f32236ea8e6..f1434db062583 100644 --- a/crates/bevy_ecs/src/event/mut_iterators.rs +++ b/crates/bevy_ecs/src/event/mut_iterators.rs @@ -148,6 +148,7 @@ pub struct EventMutParIter<'a, E: Event> { mutator: &'a mut EventCursor, slices: [&'a mut [EventInstance]; 2], batching_strategy: BatchingStrategy, + #[cfg(not(target_arch = "wasm32"))] unread: usize, } @@ -171,6 +172,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { mutator, slices: [a, b], batching_strategy: BatchingStrategy::default(), + #[cfg(not(target_arch = "wasm32"))] unread: unread_count, } } @@ -207,6 +209,10 @@ impl<'a, E: Event> EventMutParIter<'a, E> { /// initialized and run from the ECS scheduler, this should never panic. /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool + #[cfg_attr( + target_arch = "wasm32", + expect(unused_mut, reason = "not mutated on this target") + )] pub fn for_each_with_id) + Send + Sync + Clone>( mut self, func: FN, diff --git a/crates/bevy_ecs/src/event/send_event.rs b/crates/bevy_ecs/src/event/send_event.rs deleted file mode 100644 index 0d5f61cadcc4f..0000000000000 --- a/crates/bevy_ecs/src/event/send_event.rs +++ /dev/null @@ -1,37 +0,0 @@ -#[cfg(feature = "track_change_detection")] -use core::panic::Location; - -use super::{Event, Events}; -use crate::world::{Command, World}; - -/// A command to send an arbitrary [`Event`], used by [`Commands::send_event`](crate::system::Commands::send_event). -pub struct SendEvent { - /// The event to send. - pub event: E, - /// The source code location that triggered this command. - #[cfg(feature = "track_change_detection")] - pub caller: &'static Location<'static>, -} - -// This does not use `From`, as the resulting `Into` is not track_caller -impl SendEvent { - /// Constructs a new `SendEvent` tracking the caller. - pub fn new(event: E) -> Self { - Self { - event, - #[cfg(feature = "track_change_detection")] - caller: Location::caller(), - } - } -} - -impl Command for SendEvent { - fn apply(self, world: &mut World) { - let mut events = world.resource_mut::>(); - events.send_with_caller( - self.event, - #[cfg(feature = "track_change_detection")] - self.caller, - ); - } -} diff --git a/crates/bevy_ecs/src/identifier/mod.rs b/crates/bevy_ecs/src/identifier/mod.rs index 6134e472427e2..cf467da505dfc 100644 --- a/crates/bevy_ecs/src/identifier/mod.rs +++ b/crates/bevy_ecs/src/identifier/mod.rs @@ -216,7 +216,10 @@ mod tests { #[rustfmt::skip] #[test] - #[allow(clippy::nonminimal_bool)] // This is intentionally testing `lt` and `ge` as separate functions. + #[expect( + clippy::nonminimal_bool, + reason = "This intentionally tests all possible comparison operators as separate functions; thus, we don't want to rewrite these comparisons to use different operators." + )] fn id_comparison() { assert!(Identifier::new(123, 456, IdKind::Entity).unwrap() == Identifier::new(123, 456, IdKind::Entity).unwrap()); assert!(Identifier::new(123, 456, IdKind::Placeholder).unwrap() == Identifier::new(123, 456, IdKind::Placeholder).unwrap()); diff --git a/crates/bevy_ecs/src/intern.rs b/crates/bevy_ecs/src/intern.rs index e606b0d546315..18e866e87d953 100644 --- a/crates/bevy_ecs/src/intern.rs +++ b/crates/bevy_ecs/src/intern.rs @@ -180,6 +180,7 @@ impl Default for Interner { #[cfg(test)] mod tests { + use alloc::{boxed::Box, string::ToString}; use bevy_utils::FixedHasher; use core::hash::{BuildHasher, Hash, Hasher}; diff --git a/crates/bevy_ecs/src/label.rs b/crates/bevy_ecs/src/label.rs index e3f5078b22f71..10a23cfb898c3 100644 --- a/crates/bevy_ecs/src/label.rs +++ b/crates/bevy_ecs/src/label.rs @@ -22,6 +22,9 @@ pub trait DynEq: Any { fn dyn_eq(&self, other: &dyn DynEq) -> bool; } +// Tests that this trait is dyn-compatible +const _: Option> = None; + impl DynEq for T where T: Any + Eq, @@ -48,6 +51,9 @@ pub trait DynHash: DynEq { fn dyn_hash(&self, state: &mut dyn Hasher); } +// Tests that this trait is dyn-compatible +const _: Option> = None; + impl DynHash for T where T: DynEq + Hash, @@ -201,19 +207,3 @@ macro_rules! define_label { $crate::intern::Interner::new(); }; } - -#[cfg(test)] -mod tests { - use super::{DynEq, DynHash}; - use bevy_utils::assert_object_safe; - - #[test] - fn dyn_eq_object_safe() { - assert_object_safe::(); - } - - #[test] - fn dyn_hash_object_safe() { - assert_object_safe::(); - } -} diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 419972fdfff79..606d1b9e9113f 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1,7 +1,14 @@ -// FIXME(11590): remove this once the lint is fixed -#![allow(unsafe_op_in_unsafe_fn)] -// TODO: remove once Edition 2024 is released -#![allow(dependency_on_unit_never_type_fallback)] +#![expect( + unsafe_op_in_unsafe_fn, + reason = "See #11590. To be removed once all applicable unsafe code has an unsafe block with a safety comment." +)] +#![cfg_attr( + test, + expect( + dependency_on_unit_never_type_fallback, + reason = "See #17340. To be removed once Edition 2024 is released" + ) +)] #![doc = include_str!("../README.md")] #![cfg_attr( any(docsrs, docsrs_dep), @@ -11,12 +18,15 @@ ) )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] -#![allow(unsafe_code)] +#![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" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; #[cfg(target_pointer_width = "16")] compile_error!("bevy_ecs cannot safely compile for a 16-bit platform."); @@ -52,13 +62,16 @@ pub use bevy_ptr as ptr; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - #[expect(deprecated)] + #[expect( + deprecated, + reason = "`crate::schedule::apply_deferred` is considered deprecated; however, it may still be used by crates which consume `bevy_ecs`, so its removal here may cause confusion. It is intended to be removed in the Bevy 0.17 cycle." + )] #[doc(hidden)] pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, component::{require, Component}, - entity::{Entity, EntityMapper}, + entity::{Entity, EntityBorrow, EntityMapper}, event::{Event, EventMutator, EventReader, EventWriter, Events}, name::{Name, NameOrEntity}, observer::{CloneEntityWithObserversExt, Observer, Trigger}, @@ -70,13 +83,13 @@ pub mod prelude { IntoSystemSet, IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ - Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, - NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, Res, ResMut, Resource, - Single, System, SystemIn, SystemInput, SystemParamBuilder, SystemParamFunction, - WithParamWarnPolicy, + Command, Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, + IntoSystem, Local, NonSend, NonSendMut, ParamSet, Populated, Query, ReadOnlySystem, + Res, ResMut, Resource, Single, System, SystemIn, SystemInput, SystemParamBuilder, + SystemParamFunction, WithParamWarnPolicy, }, world::{ - Command, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, + EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, }, }; @@ -120,7 +133,12 @@ mod tests { system::Resource, world::{EntityMut, EntityRef, Mut, World}, }; - use alloc::{sync::Arc, vec}; + use alloc::{ + string::{String, ToString}, + sync::Arc, + vec, + vec::Vec, + }; use bevy_ecs_macros::{VisitEntities, VisitEntitiesMut}; use bevy_tasks::{ComputeTaskPool, TaskPool}; use bevy_utils::HashSet; @@ -139,9 +157,8 @@ mod tests { #[derive(Component, Debug, PartialEq, Eq, Clone, Copy)] struct C; - #[allow(dead_code)] #[derive(Default)] - struct NonSendA(usize, PhantomData<*mut ()>); + struct NonSendA(PhantomData<*mut ()>); #[derive(Component, Clone, Debug)] struct DropCk(Arc); @@ -158,8 +175,10 @@ mod tests { } } - // TODO: The compiler says the Debug and Clone are removed during dead code analysis. Investigate. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to test how `Drop` behavior works in regards to SparseSet storage, and as such is solely a wrapper around `DropCk` to make it use the SparseSet storage. Because of this, the inner field is intentionally never read." + )] #[derive(Component, Clone, Debug)] #[component(storage = "SparseSet")] struct DropCkSparse(DropCk); @@ -2633,42 +2652,49 @@ mod tests { World::new().register_component::(); } - // These structs are primarily compilation tests to test the derive macros. Because they are - // never constructed, we have to manually silence the `dead_code` lint. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed." + )] #[derive(Component)] struct ComponentA(u32); - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed." + )] #[derive(Component)] struct ComponentB(u32); - #[allow(dead_code)] #[derive(Bundle)] struct Simple(ComponentA); - #[allow(dead_code)] #[derive(Bundle)] struct Tuple(Simple, ComponentB); - #[allow(dead_code)] #[derive(Bundle)] struct Record { field0: Simple, field1: ComponentB, } - #[allow(dead_code)] #[derive(Component, VisitEntities, VisitEntitiesMut)] struct MyEntities { entities: Vec, another_one: Entity, maybe_entity: Option, + #[expect( + dead_code, + reason = "This struct is used as a compilation test to test the derive macros, and as such this field is intentionally never used." + )] #[visit_entities(ignore)] something_else: String, } - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed." + )] #[derive(Component, VisitEntities, VisitEntitiesMut)] struct MyEntitiesTuple(Vec, Entity, #[visit_entities(ignore)] usize); } diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index 3ae68798a17bf..05bc0e8c9cd8e 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -13,9 +13,12 @@ use core::{ }; #[cfg(feature = "serialize")] -use serde::{ - de::{Error, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, +use { + alloc::string::ToString, + serde::{ + de::{Error, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, + }, }; #[cfg(feature = "bevy_reflect")] @@ -261,6 +264,7 @@ impl<'de> Visitor<'de> for NameVisitor { mod tests { use super::*; use crate::world::World; + use alloc::string::ToString; #[test] fn test_display_of_debug_name() { diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index ee94cfa62a73e..9fb6beb2a206e 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -8,7 +8,7 @@ use alloc::vec::Vec; /// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. #[derive(Default)] -pub(crate) struct ObservedBy(pub(crate) Vec); +pub struct ObservedBy(pub(crate) Vec); impl Component for ObservedBy { const STORAGE_TYPE: StorageType = StorageType::SparseSet; @@ -50,7 +50,7 @@ impl Component for ObservedBy { /// Trait that holds functions for configuring interaction with observers during entity cloning. pub trait CloneEntityWithObserversExt { - /// Sets the option to automatically add cloned entities to the obsevers targeting source entity. + /// Sets the option to automatically add cloned entities to the observers targeting source entity. fn add_observers(&mut self, add_observers: bool) -> &mut Self; } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index c78484c6a6ff5..36c74a3b9825b 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -2,17 +2,14 @@ mod entity_observer; mod runner; -mod trigger_event; -pub use entity_observer::CloneEntityWithObserversExt; +pub use entity_observer::{CloneEntityWithObserversExt, ObservedBy}; pub use runner::*; -pub use trigger_event::*; use crate::{ archetype::ArchetypeFlags, component::ComponentId, entity::EntityHashMap, - observer::entity_observer::ObservedBy, prelude::*, system::IntoObserverSystem, world::{DeferredWorld, *}, @@ -168,6 +165,98 @@ impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { } } +/// 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) -> &[ComponentId]; + + /// The entities the trigger should target. + fn entities(&self) -> &[Entity]; +} + +impl TriggerTargets for () { + fn components(&self) -> &[ComponentId] { + &[] + } + + fn entities(&self) -> &[Entity] { + &[] + } +} + +impl TriggerTargets for Entity { + fn components(&self) -> &[ComponentId] { + &[] + } + + fn entities(&self) -> &[Entity] { + core::slice::from_ref(self) + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> &[ComponentId] { + &[] + } + + fn entities(&self) -> &[Entity] { + self.as_slice() + } +} + +impl TriggerTargets for [Entity; N] { + fn components(&self) -> &[ComponentId] { + &[] + } + + fn entities(&self) -> &[Entity] { + self.as_slice() + } +} + +impl TriggerTargets for ComponentId { + fn components(&self) -> &[ComponentId] { + core::slice::from_ref(self) + } + + fn entities(&self) -> &[Entity] { + &[] + } +} + +impl TriggerTargets for Vec { + fn components(&self) -> &[ComponentId] { + self.as_slice() + } + + fn entities(&self) -> &[Entity] { + &[] + } +} + +impl TriggerTargets for [ComponentId; N] { + fn components(&self) -> &[ComponentId] { + self.as_slice() + } + + fn entities(&self) -> &[Entity] { + &[] + } +} + +impl TriggerTargets for &Vec { + fn components(&self) -> &[ComponentId] { + &[] + } + + fn entities(&self) -> &[Entity] { + self.as_slice() + } +} + /// A description of what an [`Observer`] observes. #[derive(Default, Clone)] pub struct ObserverDescriptor { @@ -431,16 +520,20 @@ impl World { /// 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_ref`] instead. - pub fn trigger(&mut self, event: impl Event) { - TriggerEvent { event, targets: () }.trigger(self); + pub fn trigger(&mut self, mut event: 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, &mut event, ()) }; } /// Triggers the given [`Event`] as a mutable reference, which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. - pub fn trigger_ref(&mut self, event: &mut impl Event) { - TriggerEvent { event, targets: () }.trigger_ref(self); + 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, ()) }; } /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. @@ -448,8 +541,10 @@ impl World { /// 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. - pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) { - TriggerEvent { event, targets }.trigger(self); + pub fn trigger_targets(&mut self, mut event: 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, &mut event, targets) }; } /// Triggers the given [`Event`] as a mutable reference for the given `targets`, @@ -457,8 +552,74 @@ impl World { /// /// 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. - pub fn trigger_targets_ref(&mut self, event: &mut impl Event, targets: impl TriggerTargets) { - TriggerEvent { event, targets }.trigger_ref(self); + pub fn trigger_targets_ref(&mut self, event: &mut E, targets: impl TriggerTargets) { + let event_id = E::register_component_id(self); + // SAFETY: We just registered `event_id` with the type of `event` + unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) }; + } + + /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + /// + /// 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_dynamic_ref`] instead. + /// + /// # Safety + /// + /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. + pub unsafe fn trigger_targets_dynamic( + &mut self, + event_id: ComponentId, + mut event_data: E, + targets: Targets, + ) { + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + self.trigger_targets_dynamic_ref(event_id, &mut event_data, targets); + }; + } + + /// Triggers the given [`Event`] 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 + /// or use the event after it has been modified by observers. + /// + /// # Safety + /// + /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. + pub unsafe fn trigger_targets_dynamic_ref( + &mut self, + event_id: ComponentId, + event_data: &mut E, + targets: Targets, + ) { + let mut world = DeferredWorld::from(self); + if targets.entities().is_empty() { + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, E::Traversal>( + event_id, + Entity::PLACEHOLDER, + targets.components(), + event_data, + false, + ); + }; + } else { + for target in targets.entities() { + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, E::Traversal>( + event_id, + *target, + targets.components(), + event_data, + E::AUTO_PROPAGATE, + ); + }; + } + } } /// Register an observer to the cache, called when an observer is created @@ -582,7 +743,7 @@ impl World { #[cfg(test)] mod tests { - use alloc::vec; + use alloc::{vec, vec::Vec}; use bevy_ptr::OwningPtr; use bevy_utils::HashMap; @@ -590,7 +751,7 @@ mod tests { use crate as bevy_ecs; use crate::component::ComponentId; use crate::{ - observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState, OnReplace}, + observer::{Observer, ObserverDescriptor, ObserverState, OnReplace}, prelude::*, traversal::Traversal, }; @@ -834,7 +995,7 @@ mod tests { fn observer_multiple_events() { let mut world = World::new(); world.init_resource::(); - let on_remove = world.register_component::(); + let on_remove = OnRemove::register_component_id(&mut world); world.spawn( // SAFETY: OnAdd and OnRemove are both unit types, so this is safe unsafe { @@ -993,10 +1154,10 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = world.register_component::(); + let event_a = OnRemove::register_component_id(&mut world); world.spawn(ObserverState { - // SAFETY: we registered `event_a` above and it matches the type of TriggerA + // SAFETY: we registered `event_a` above and it matches the type of EventA descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) }, runner: |mut world, _trigger, _ptr, _propagate| { world.resource_mut::().observed("event_a"); @@ -1004,10 +1165,10 @@ mod tests { ..Default::default() }); - world.commands().queue( - // SAFETY: we registered `event_a` above and it matches the type of TriggerA - unsafe { EmitDynamicTrigger::new_with_id(event_a, EventA, ()) }, - ); + world.commands().queue(move |world: &mut World| { + // SAFETY: we registered `event_a` above and it matches the type of EventA + unsafe { world.trigger_targets_dynamic(event_a, EventA, ()) }; + }); world.flush(); assert_eq!(vec!["event_a"], world.resource::().0); } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index b63f4f34a5d00..0814e4462686b 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -198,7 +198,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// struct Explode; /// /// world.add_observer(|trigger: Trigger, mut commands: Commands| { -/// println!("Entity {:?} goes BOOM!", trigger.target()); +/// println!("Entity {} goes BOOM!", trigger.target()); /// commands.entity(trigger.target()).despawn(); /// }); /// @@ -398,13 +398,13 @@ fn hook_on_add>( _: ComponentId, ) { world.commands().queue(move |world: &mut World| { - let event_type = world.register_component::(); + let event_id = E::register_component_id(world); let mut components = Vec::new(); B::component_ids(&mut world.components, &mut world.storages, &mut |id| { components.push(id); }); let mut descriptor = ObserverDescriptor { - events: vec![event_type], + events: vec![event_id], components, ..Default::default() }; diff --git a/crates/bevy_ecs/src/observer/trigger_event.rs b/crates/bevy_ecs/src/observer/trigger_event.rs deleted file mode 100644 index bf84e57bae301..0000000000000 --- a/crates/bevy_ecs/src/observer/trigger_event.rs +++ /dev/null @@ -1,196 +0,0 @@ -use crate::{ - component::ComponentId, - entity::Entity, - event::Event, - world::{Command, DeferredWorld, World}, -}; -use alloc::vec::Vec; - -/// A [`Command`] that emits a given trigger for a given set of targets. -pub struct TriggerEvent { - /// The event to trigger. - pub event: E, - - /// The targets to trigger the event for. - pub targets: Targets, -} - -impl TriggerEvent { - pub(super) fn trigger(mut self, world: &mut World) { - let event_type = world.register_component::(); - trigger_event(world, event_type, &mut self.event, self.targets); - } -} - -impl TriggerEvent<&mut E, Targets> { - pub(super) fn trigger_ref(self, world: &mut World) { - let event_type = world.register_component::(); - trigger_event(world, event_type, self.event, self.targets); - } -} - -impl Command - for TriggerEvent -{ - fn apply(self, world: &mut World) { - self.trigger(world); - } -} - -/// Emit a trigger for a dynamic component id. This is unsafe and must be verified manually. -pub struct EmitDynamicTrigger { - event_type: ComponentId, - event_data: T, - targets: Targets, -} - -impl EmitDynamicTrigger { - /// Sets the event type of the resulting trigger, used for dynamic triggers - /// # Safety - /// Caller must ensure that the component associated with `event_type` is accessible as E - pub unsafe fn new_with_id(event_type: ComponentId, event_data: E, targets: Targets) -> Self { - Self { - event_type, - event_data, - targets, - } - } -} - -impl Command - for EmitDynamicTrigger -{ - fn apply(mut self, world: &mut World) { - trigger_event(world, self.event_type, &mut self.event_data, self.targets); - } -} - -#[inline] -fn trigger_event( - world: &mut World, - event_type: ComponentId, - event_data: &mut E, - targets: Targets, -) { - let mut world = DeferredWorld::from(world); - if targets.entities().is_empty() { - // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` - unsafe { - world.trigger_observers_with_data::<_, E::Traversal>( - event_type, - Entity::PLACEHOLDER, - targets.components(), - event_data, - false, - ); - }; - } else { - for target in targets.entities() { - // SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new` - unsafe { - world.trigger_observers_with_data::<_, E::Traversal>( - event_type, - *target, - targets.components(), - event_data, - E::AUTO_PROPAGATE, - ); - }; - } - } -} - -/// 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. -/// -/// [`Trigger`]: crate::observer::Trigger -/// [`Observer`]: crate::observer::Observer -pub trait TriggerTargets { - /// The components the trigger should target. - fn components(&self) -> &[ComponentId]; - - /// The entities the trigger should target. - fn entities(&self) -> &[Entity]; -} - -impl TriggerTargets for () { - fn components(&self) -> &[ComponentId] { - &[] - } - - fn entities(&self) -> &[Entity] { - &[] - } -} - -impl TriggerTargets for Entity { - fn components(&self) -> &[ComponentId] { - &[] - } - - fn entities(&self) -> &[Entity] { - core::slice::from_ref(self) - } -} - -impl TriggerTargets for Vec { - fn components(&self) -> &[ComponentId] { - &[] - } - - fn entities(&self) -> &[Entity] { - self.as_slice() - } -} - -impl TriggerTargets for [Entity; N] { - fn components(&self) -> &[ComponentId] { - &[] - } - - fn entities(&self) -> &[Entity] { - self.as_slice() - } -} - -impl TriggerTargets for ComponentId { - fn components(&self) -> &[ComponentId] { - core::slice::from_ref(self) - } - - fn entities(&self) -> &[Entity] { - &[] - } -} - -impl TriggerTargets for Vec { - fn components(&self) -> &[ComponentId] { - self.as_slice() - } - - fn entities(&self) -> &[Entity] { - &[] - } -} - -impl TriggerTargets for [ComponentId; N] { - fn components(&self) -> &[ComponentId] { - self.as_slice() - } - - fn entities(&self) -> &[Entity] { - &[] - } -} - -impl TriggerTargets for &Vec { - fn components(&self) -> &[ComponentId] { - &[] - } - - fn entities(&self) -> &[Entity] { - self.as_slice() - } -} diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 1ee7c188775c1..be97d19c6845c 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1272,28 +1272,28 @@ impl FilteredAccessSet { } /// Adds a read access to a resource to the set. - pub(crate) fn add_unfiltered_resource_read(&mut self, index: T) { + pub fn add_unfiltered_resource_read(&mut self, index: T) { let mut filter = FilteredAccess::default(); filter.add_resource_read(index); self.add(filter); } /// Adds a write access to a resource to the set. - pub(crate) fn add_unfiltered_resource_write(&mut self, index: T) { + pub fn add_unfiltered_resource_write(&mut self, index: T) { let mut filter = FilteredAccess::default(); filter.add_resource_write(index); self.add(filter); } /// Adds read access to all resources to the set. - pub(crate) fn add_unfiltered_read_all_resources(&mut self) { + pub fn add_unfiltered_read_all_resources(&mut self) { let mut filter = FilteredAccess::default(); filter.access.read_all_resources(); self.add(filter); } /// Adds write access to all resources to the set. - pub(crate) fn add_unfiltered_write_all_resources(&mut self) { + pub fn add_unfiltered_write_all_resources(&mut self) { let mut filter = FilteredAccess::default(); filter.access.write_all_resources(); self.add(filter); @@ -1338,6 +1338,7 @@ mod tests { use crate::query::{ access::AccessFilters, Access, AccessConflicts, FilteredAccess, FilteredAccessSet, }; + use alloc::vec; use core::marker::PhantomData; use fixedbitset::FixedBitSet; diff --git a/crates/bevy_ecs/src/query/builder.rs b/crates/bevy_ecs/src/query/builder.rs index af1af7749e89b..d699cb3aea4ac 100644 --- a/crates/bevy_ecs/src/query/builder.rs +++ b/crates/bevy_ecs/src/query/builder.rs @@ -78,10 +78,9 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { self.world() .components() .get_info(component_id) - .map_or(false, |info| info.storage_type() == StorageType::Table) + .is_some_and(|info| info.storage_type() == StorageType::Table) }; - #[allow(deprecated)] let (mut component_reads_and_writes, component_reads_and_writes_inverted) = self.access.access().component_reads_and_writes(); if component_reads_and_writes_inverted { @@ -278,6 +277,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> { mod tests { use crate as bevy_ecs; use crate::{prelude::*, world::FilteredEntityRef}; + use std::dbg; #[derive(Component, PartialEq, Debug)] struct A(usize); diff --git a/crates/bevy_ecs/src/query/error.rs b/crates/bevy_ecs/src/query/error.rs index 9746471c66852..2031f390009cc 100644 --- a/crates/bevy_ecs/src/query/error.rs +++ b/crates/bevy_ecs/src/query/error.rs @@ -1,6 +1,9 @@ use thiserror::Error; -use crate::{entity::Entity, world::unsafe_world_cell::UnsafeWorldCell}; +use crate::{ + entity::{Entity, EntityDoesNotExistDetails}, + world::unsafe_world_cell::UnsafeWorldCell, +}; /// An error that occurs when retrieving a specific [`Entity`]'s query result from [`Query`](crate::system::Query) or [`QueryState`](crate::query::QueryState). // TODO: return the type_name as part of this error @@ -11,7 +14,7 @@ pub enum QueryEntityError<'w> { /// Either it does not have a requested component, or it has a component which the query filters out. QueryDoesNotMatch(Entity, UnsafeWorldCell<'w>), /// The given [`Entity`] does not exist. - NoSuchEntity(Entity, UnsafeWorldCell<'w>), + NoSuchEntity(Entity, EntityDoesNotExistDetails), /// The [`Entity`] was requested mutably more than once. /// /// See [`QueryState::get_many_mut`](crate::query::QueryState::get_many_mut) for an example. @@ -30,18 +33,15 @@ impl<'w> core::fmt::Display for QueryEntityError<'w> { )?; format_archetype(f, world, entity) } - Self::NoSuchEntity(entity, world) => { + Self::NoSuchEntity(entity, details) => { + write!(f, "The entity with ID {entity} {details}") + } + Self::AliasedMutability(entity) => { write!( f, - "Entity {entity} {}", - world - .entities() - .entity_does_not_exist_error_details_message(entity) + "The entity with ID {entity} was requested mutably more than once" ) } - Self::AliasedMutability(entity) => { - write!(f, "Entity {entity} was requested mutably more than once") - } } } } @@ -54,14 +54,8 @@ impl<'w> core::fmt::Debug for QueryEntityError<'w> { format_archetype(f, world, entity)?; write!(f, ")") } - Self::NoSuchEntity(entity, world) => { - write!( - f, - "NoSuchEntity({entity} {})", - world - .entities() - .entity_does_not_exist_error_details_message(entity) - ) + Self::NoSuchEntity(entity, details) => { + write!(f, "NoSuchEntity({entity} {details})") } Self::AliasedMutability(entity) => write!(f, "AliasedMutability({entity})"), } @@ -119,6 +113,7 @@ pub enum QuerySingleError { mod test { use crate as bevy_ecs; use crate::prelude::World; + use alloc::format; use bevy_ecs_macros::Component; #[test] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 13ad4655b895d..afb56206454e4 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1322,9 +1322,9 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { 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(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] column.get_changed_by_slice(table.entity_count()).into(), - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] (), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1350,7 +1350,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { // SAFETY: The caller ensures `table_row` is in range. let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; // SAFETY: The caller ensures `table_row` is in range. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = unsafe { _callers.get(table_row.as_usize()) }; Ref { @@ -1361,7 +1361,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { this_run: fetch.this_run, last_run: fetch.last_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: caller.deref(), } }, @@ -1373,7 +1373,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { Ref { value: component.deref(), ticks: Ticks::from_tick_cells(ticks, fetch.last_run, fetch.this_run), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), } }, @@ -1521,9 +1521,9 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { 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(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] column.get_changed_by_slice(table.entity_count()).into(), - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] (), )); // SAFETY: set_table is only called when T::STORAGE_TYPE = StorageType::Table @@ -1549,7 +1549,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { // SAFETY: The caller ensures `table_row` is in range. let changed = unsafe { changed_ticks.get(table_row.as_usize()) }; // SAFETY: The caller ensures `table_row` is in range. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = unsafe { _callers.get(table_row.as_usize()) }; Mut { @@ -1560,7 +1560,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { this_run: fetch.this_run, last_run: fetch.last_run, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: caller.deref_mut(), } }, @@ -1572,7 +1572,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { Mut { value: component.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, fetch.last_run, fetch.this_run), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref_mut(), } }, @@ -2015,8 +2015,6 @@ pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] $(#[$meta])* // SAFETY: defers to soundness `$name: WorldQuery` impl unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { @@ -2033,8 +2031,22 @@ macro_rules! impl_tuple_query_data { macro_rules! impl_anytuple_fetch { ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { $(#[$meta])* - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[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." + )] /// 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. @@ -2059,7 +2071,6 @@ macro_rules! impl_anytuple_fetch { } #[inline] - #[allow(clippy::unused_unit)] unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are uphold by the caller. @@ -2100,7 +2111,6 @@ macro_rules! impl_anytuple_fetch { } #[inline(always)] - #[allow(clippy::unused_unit)] unsafe fn fetch<'w>( _fetch: &mut Self::Fetch<'w>, _entity: Entity, @@ -2139,11 +2149,9 @@ macro_rules! impl_anytuple_fetch { <($(Option<$name>,)*)>::update_component_access(state, access); } - #[allow(unused_variables)] fn init_state(world: &mut World) -> Self::State { ($($name::init_state(world),)*) } - #[allow(unused_variables)] fn get_state(components: &Components) -> Option { Some(($($name::get_state(components)?,)*)) } @@ -2155,8 +2163,6 @@ macro_rules! impl_anytuple_fetch { } $(#[$meta])* - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] // SAFETY: defers to soundness of `$name: WorldQuery` impl unsafe impl<$($name: QueryData),*> QueryData for AnyOf<($($name,)*)> { type ReadOnly = AnyOf<($($name::ReadOnly,)*)>; diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index b096e801f4cc2..3a3d9a0162769 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -98,7 +98,6 @@ 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. - #[allow(unused_variables)] unsafe fn filter_fetch( fetch: &mut Self::Fetch<'_>, entity: Entity, @@ -356,7 +355,7 @@ unsafe impl QueryFilter for Without { /// # /// fn print_cool_entity_system(query: Query, Changed)>>) { /// for entity in &query { -/// println!("Entity {:?} got a new style or color", entity); +/// println!("Entity {} got a new style or color", entity); /// } /// } /// # bevy_ecs::system::assert_is_system(print_cool_entity_system); @@ -381,9 +380,22 @@ impl Clone for OrFetch<'_, T> { macro_rules! impl_or_query_filter { ($(#[$meta:meta])* $(($filter: ident, $state: ident)),*) => { $(#[$meta])* - #[allow(unused_variables)] - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[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." + )] /// SAFETY: /// `fetch` accesses are a subset of the subqueries' accesses /// This is sound because `update_component_access` adds accesses according to the implementations of all the subqueries. @@ -454,34 +466,34 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn fetch<'w>( fetch: &mut Self::Fetch<'w>, - _entity: Entity, - _table_row: TableRow + entity: Entity, + table_row: TableRow ) -> Self::Item<'w> { let ($($filter,)*) = fetch; // SAFETY: The invariants are uphold by the caller. - false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, _entity, _table_row) }))* + false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, entity, table_row) }))* } fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { let ($($filter,)*) = state; - let mut _new_access = FilteredAccess::matches_nothing(); + let mut new_access = FilteredAccess::matches_nothing(); $( // Create an intermediate because `access`'s value needs to be preserved // for the next filter, and `_new_access` has to be modified only by `append_or` to it. let mut intermediate = access.clone(); $filter::update_component_access($filter, &mut intermediate); - _new_access.append_or(&intermediate); + new_access.append_or(&intermediate); // Also extend the accesses required to compute the filter. This is required because // otherwise a `Query<(), Or<(Changed,)>` won't conflict with `Query<&mut Foo>`. - _new_access.extend_access(&intermediate); + new_access.extend_access(&intermediate); )* // The required components remain the same as the original `access`. - _new_access.required = core::mem::take(&mut access.required); + new_access.required = core::mem::take(&mut access.required); - *access = _new_access; + *access = new_access; } fn init_state(world: &mut World) -> Self::State { @@ -492,15 +504,15 @@ macro_rules! impl_or_query_filter { Some(($($filter::get_state(components)?,)*)) } - fn matches_component_set(_state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { - let ($($filter,)*) = _state; - false $(|| $filter::matches_component_set($filter, _set_contains_id))* + fn matches_component_set(state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + let ($($filter,)*) = state; + false $(|| $filter::matches_component_set($filter, set_contains_id))* } } - $(#[$meta])* - // SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access. - unsafe impl<$($filter: QueryFilter),*> QueryFilter for Or<($($filter,)*)> { + $(#[$meta])* + // SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access. + unsafe impl<$($filter: QueryFilter),*> QueryFilter for Or<($($filter,)*)> { const IS_ARCHETYPAL: bool = true $(&& $filter::IS_ARCHETYPAL)*; #[inline(always)] @@ -518,9 +530,18 @@ macro_rules! impl_or_query_filter { macro_rules! impl_tuple_query_filter { ($(#[$meta:meta])* $($name: ident),*) => { - #[allow(unused_variables)] - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] $(#[$meta])* // SAFETY: This only performs access that subqueries perform, and they impl `QueryFilter` and so perform no mutable access. unsafe impl<$($name: QueryFilter),*> QueryFilter for ($($name,)*) { @@ -529,12 +550,12 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( fetch: &mut Self::Fetch<'_>, - _entity: Entity, - _table_row: TableRow + entity: Entity, + table_row: TableRow ) -> bool { let ($($name,)*) = fetch; // SAFETY: The invariants are uphold by the caller. - true $(&& unsafe { $name::filter_fetch($name, _entity, _table_row) })* + true $(&& unsafe { $name::filter_fetch($name, entity, table_row) })* } } diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index 816c92e76ca14..f17d1a1490c93 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1,11 +1,15 @@ use super::{QueryData, QueryFilter, ReadOnlyQueryData}; use crate::{ archetype::{Archetype, ArchetypeEntity, Archetypes}, + bundle::Bundle, component::Tick, entity::{Entities, Entity, EntityBorrow, EntitySet, EntitySetIterator}, query::{ArchetypeFilter, DebugCheckedUnwrap, QueryState, StorageId}, storage::{Table, TableRow, Tables}, - world::unsafe_world_cell::UnsafeWorldCell, + world::{ + unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + FilteredEntityMut, FilteredEntityRef, + }, }; use alloc::vec::Vec; use core::{ @@ -122,7 +126,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment - /// from an storage. + /// from a storage. /// /// # Safety /// - `range` must be in `[0, storage::entity_count)` or None. @@ -183,7 +187,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } /// Executes the equivalent of [`Iterator::fold`] over a contiguous segment - /// from an table. + /// from a table. /// /// # Safety /// - all `rows` must be in `[0, table.entity_count)`. @@ -1105,12 +1109,48 @@ impl<'w, 's, D: QueryData, F: QueryFilter> FusedIterator for QueryIter<'w, 's, D // SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator for QueryIter<'w, 's, Entity, F> {} +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator for QueryIter<'w, 's, EntityRef<'_>, F> {} + +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator for QueryIter<'w, 's, EntityMut<'_>, F> {} + +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator + for QueryIter<'w, 's, FilteredEntityRef<'_>, F> +{ +} + +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter> EntitySetIterator + for QueryIter<'w, 's, FilteredEntityMut<'_>, F> +{ +} + +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter, B: Bundle> EntitySetIterator + for QueryIter<'w, 's, EntityRefExcept<'_, B>, F> +{ +} + +// SAFETY: [`QueryIter`] is guaranteed to return every matching entity once and only once. +unsafe impl<'w, 's, F: QueryFilter, B: Bundle> EntitySetIterator + for QueryIter<'w, 's, EntityMutExcept<'_, B>, F> +{ +} + impl<'w, 's, D: QueryData, F: QueryFilter> Debug for QueryIter<'w, 's, D, F> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("QueryIter").finish() } } +impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter> Clone for QueryIter<'w, 's, D, F> { + fn clone(&self) -> Self { + self.remaining() + } +} + /// An [`Iterator`] over sorted query results of a [`Query`](crate::system::Query). /// /// This struct is created by the [`QueryIter::sort`], [`QueryIter::sort_unstable`], @@ -2901,7 +2941,10 @@ impl PartialEq for NeutralOrd { impl Eq for NeutralOrd {} -#[allow(clippy::non_canonical_partial_ord_impl)] +#[expect( + clippy::non_canonical_partial_ord_impl, + reason = "`PartialOrd` and `Ord` on this struct must only ever return `Ordering::Equal`, so we prefer clarity" +)] impl PartialOrd for NeutralOrd { fn partial_cmp(&self, _other: &Self) -> Option { Some(Ordering::Equal) @@ -2916,13 +2959,12 @@ impl Ord for NeutralOrd { #[cfg(test)] mod tests { - #[allow(unused_imports)] + use alloc::vec::Vec; + use std::println; + use crate::component::Component; - #[allow(unused_imports)] use crate::entity::Entity; - #[allow(unused_imports)] use crate::prelude::World; - #[allow(unused_imports)] use crate::{self as bevy_ecs}; #[derive(Component, Debug, PartialEq, PartialOrd, Clone, Copy)] @@ -2931,7 +2973,6 @@ mod tests { #[component(storage = "SparseSet")] struct Sparse(usize); - #[allow(clippy::unnecessary_sort_by)] #[test] fn query_iter_sorts() { let mut world = World::new(); @@ -3119,7 +3160,6 @@ mod tests { } } - #[allow(clippy::unnecessary_sort_by)] #[test] fn query_iter_many_sorts() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index c6c1383ceb7b9..6104d0a543abb 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -117,9 +117,10 @@ mod tests { system::{assert_is_system, IntoSystem, Query, System, SystemState}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; + use alloc::{vec, vec::Vec}; use bevy_ecs_macros::QueryFilter; use core::{any::type_name, fmt::Debug, hash::Hash}; - use std::collections::HashSet; + use std::{collections::HashSet, println}; #[derive(Component, Debug, Hash, Eq, PartialEq, Clone, Copy, PartialOrd, Ord)] struct A(usize); @@ -804,9 +805,6 @@ mod tests { /// `QueryData` that performs read access on R to test that resource access is tracked struct ReadsRData; - /// `QueryData` that performs write access on R to test that resource access is tracked - struct WritesRData; - /// SAFETY: /// `update_component_access` adds resource read access for `R`. /// `update_archetype_component_access` does nothing, as this accesses no components. @@ -889,85 +887,6 @@ mod tests { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for ReadsRData {} - /// 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 WritesRData { - type Item<'w> = (); - type Fetch<'w> = (); - type State = ComponentId; - - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} - - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - - unsafe fn init_fetch<'w>( - _world: UnsafeWorldCell<'w>, - _state: &Self::State, - _last_run: Tick, - _this_run: Tick, - ) -> Self::Fetch<'w> { - } - - const IS_DENSE: bool = true; - - #[inline] - unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, - _archetype: &'w Archetype, - _table: &Table, - ) { - } - - #[inline] - unsafe fn set_table<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, - _table: &'w Table, - ) { - } - - #[inline(always)] - unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, - _entity: Entity, - _table_row: TableRow, - ) -> Self::Item<'w> { - } - - fn update_component_access( - &component_id: &Self::State, - access: &mut FilteredAccess, - ) { - assert!( - !access.access().has_resource_read(component_id), - "WritesRData conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - ); - access.add_resource_write(component_id); - } - - fn init_state(world: &mut World) -> Self::State { - world.components.register_resource::() - } - - fn get_state(components: &Components) -> Option { - components.resource_id::() - } - - 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 WritesRData { - type ReadOnly = ReadsRData; - } - #[test] fn read_res_read_res_no_conflict() { fn system(_q1: Query>, _q2: Query>) {} @@ -975,38 +894,13 @@ mod tests { } #[test] - #[should_panic] - fn read_res_write_res_conflict() { - fn system(_q1: Query>, _q2: Query>) {} - assert_is_system(system); - } - - #[test] - #[should_panic] - fn write_res_read_res_conflict() { - fn system(_q1: Query>, _q2: Query>) {} - assert_is_system(system); - } - - #[test] - #[should_panic] - fn write_res_write_res_conflict() { - fn system(_q1: Query>, _q2: Query>) {} - assert_is_system(system); - } - - #[test] - fn read_write_res_sets_archetype_component_access() { + 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 write_query(_q: Query>) {} - let mut write_query = IntoSystem::into_system(write_query); - write_query.initialize(&mut world); - fn read_res(_r: Res) {} let mut read_res = IntoSystem::into_system(read_res); read_res.initialize(&mut world); @@ -1018,14 +912,8 @@ mod tests { assert!(read_query .archetype_component_access() .is_compatible(read_res.archetype_component_access())); - assert!(!write_query - .archetype_component_access() - .is_compatible(read_res.archetype_component_access())); assert!(!read_query .archetype_component_access() .is_compatible(write_res.archetype_component_access())); - assert!(!write_query - .archetype_component_access() - .is_compatible(write_res.archetype_component_access())); } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 36d82772d4783..2785ea44a049f 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -13,11 +13,11 @@ use crate::{ }; use alloc::vec::Vec; -#[cfg(feature = "trace")] -use bevy_utils::tracing::Span; use core::{fmt, mem::MaybeUninit, ptr}; use fixedbitset::FixedBitSet; use log::warn; +#[cfg(feature = "trace")] +use tracing::Span; use super::{ NopWorldQuery, QueryBuilder, QueryData, QueryEntityError, QueryFilter, QueryManyIter, @@ -199,13 +199,10 @@ impl QueryState { } } - if state.component_access.access().has_write_all_resources() { - access.write_all_resources(); - } else { - for component_id in state.component_access.access().resource_writes() { - access.add_resource_write(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 } @@ -566,7 +563,6 @@ impl QueryState { ) { // As a fast path, we can iterate directly over the components involved // if the `access` isn't inverted. - #[allow(deprecated)] let (component_reads_and_writes, component_reads_and_writes_inverted) = self.component_access.access.component_reads_and_writes(); let (component_writes, component_writes_inverted) = @@ -1020,7 +1016,10 @@ impl QueryState { let location = world .entities() .get(entity) - .ok_or(QueryEntityError::NoSuchEntity(entity, world))?; + .ok_or(QueryEntityError::NoSuchEntity( + entity, + world.entities().entity_does_not_exist_error_details(entity), + ))?; if !self .matched_archetypes .contains(location.archetype_id.index()) @@ -1883,6 +1882,7 @@ mod tests { use crate::{ component::Component, prelude::*, query::QueryEntityError, world::FilteredEntityRef, }; + use alloc::vec::Vec; #[test] fn get_many_unchecked_manual_uniqueness() { diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index c805e8dec7213..88164e3398634 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -14,7 +14,7 @@ use variadics_please::all_tuples; /// # Safety /// /// Implementor must ensure that -/// [`update_component_access`], [`matches_component_set`], and [`fetch`] +/// [`update_component_access`], [`matches_component_set`], [`fetch`] and [`init_fetch`] /// obey the following: /// /// - For each component mutably accessed by [`fetch`], [`update_component_access`] should add write access unless read or write access has already been added, in which case it should panic. @@ -26,8 +26,8 @@ use variadics_please::all_tuples; /// - [`matches_component_set`] must be a disjunction of the element's implementations /// - [`update_component_access`] must replace the filters with a disjunction of filters /// - Each filter in that disjunction must be a conjunction of the corresponding element's filter with the previous `access` -/// - For each resource mutably accessed by [`init_fetch`], [`update_component_access`] should add write access unless read or write access has already been added, in which case it should panic. -/// - For each resource readonly accessed by [`init_fetch`], [`update_component_access`] should add read access unless write access has already been added, in which case it should panic. +/// - For each resource readonly accessed by [`init_fetch`], [`update_component_access`] should add read access. +/// - Mutable resource access is not allowed. /// /// When implementing [`update_component_access`], note that `add_read` and `add_write` both also add a `With` filter, whereas `extend_access` does not change the filters. /// @@ -60,11 +60,14 @@ pub unsafe trait WorldQuery { fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort>; /// Creates a new instance of this fetch. + /// Readonly accesses resources registered in [`WorldQuery::update_component_access`]. /// /// # Safety /// /// - `state` must have been initialized (via [`WorldQuery::init_state`]) using the same `world` passed /// 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>( world: UnsafeWorldCell<'w>, state: &Self::State, @@ -113,12 +116,14 @@ pub unsafe trait WorldQuery { /// Fetch [`Self::Item`](`WorldQuery::Item`) for either the given `entity` in the current [`Table`], /// or for the given `entity` in the current [`Archetype`]. This must always be called after /// [`WorldQuery::set_table`] with a `table_row` in the range of the current [`Table`] or after - /// [`WorldQuery::set_archetype`] with a `entity` in the current archetype. + /// [`WorldQuery::set_archetype`] with an `entity` in the current archetype. + /// Accesses components registered in [`WorldQuery::update_component_access`]. /// /// # Safety /// - /// 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. + /// - 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>( fetch: &mut Self::Fetch<'w>, entity: Entity, @@ -152,8 +157,22 @@ pub unsafe trait WorldQuery { macro_rules! impl_tuple_world_query { ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + non_snake_case, + reason = "The names of some variables are provided by the macro's caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[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." + )] $(#[$meta])* /// SAFETY: /// `fetch` accesses are the conjunction of the subqueries' accesses @@ -180,64 +199,60 @@ macro_rules! impl_tuple_world_query { } #[inline] - #[allow(clippy::unused_unit)] - 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>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are uphold by the caller. - ($(unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) },)*) + ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) } const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] unsafe fn set_archetype<'w>( - _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, - _archetype: &'w Archetype, - _table: &'w Table + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + archetype: &'w Archetype, + table: &'w Table ) { - let ($($name,)*) = _fetch; - let ($($state,)*) = _state; + let ($($name,)*) = fetch; + let ($($state,)*) = state; // SAFETY: The invariants are uphold by the caller. - $(unsafe { $name::set_archetype($name, $state, _archetype, _table); })* + $(unsafe { $name::set_archetype($name, $state, archetype, table); })* } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { - let ($($name,)*) = _fetch; - let ($($state,)*) = _state; + unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + let ($($name,)*) = fetch; + let ($($state,)*) = state; // SAFETY: The invariants are uphold by the caller. - $(unsafe { $name::set_table($name, $state, _table); })* + $(unsafe { $name::set_table($name, $state, table); })* } #[inline(always)] - #[allow(clippy::unused_unit)] unsafe fn fetch<'w>( - _fetch: &mut Self::Fetch<'w>, - _entity: Entity, - _table_row: TableRow + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow ) -> Self::Item<'w> { - let ($($name,)*) = _fetch; + let ($($name,)*) = fetch; // SAFETY: The invariants are uphold by the caller. - ($(unsafe { $name::fetch($name, _entity, _table_row) },)*) + ($(unsafe { $name::fetch($name, entity, table_row) },)*) } - fn update_component_access(state: &Self::State, _access: &mut FilteredAccess) { + fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { let ($($name,)*) = state; - $($name::update_component_access($name, _access);)* + $($name::update_component_access($name, access);)* } - #[allow(unused_variables)] fn init_state(world: &mut World) -> Self::State { ($($name::init_state(world),)*) } - #[allow(unused_variables)] fn get_state(components: &Components) -> Option { Some(($($name::get_state(components)?,)*)) } - fn matches_component_set(state: &Self::State, _set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { + fn matches_component_set(state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool) -> bool { let ($($name,)*) = state; - true $(&& $name::matches_component_set($name, _set_contains_id))* + true $(&& $name::matches_component_set($name, set_contains_id))* } } }; diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index 4bb77d9d8fc95..15dedd8574cb4 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -3,11 +3,10 @@ use crate::{ prelude::Mut, reflect::{AppTypeRegistry, ReflectBundle, ReflectComponent}, system::{EntityCommands, Resource}, - world::{Command, World}, + world::{EntityWorldMut, World}, }; use alloc::{borrow::Cow, boxed::Box}; use bevy_reflect::{PartialReflect, TypeRegistry}; -use core::marker::PhantomData; /// An extension trait for [`EntityCommands`] for reflection related functions pub trait ReflectCommandExt { @@ -170,48 +169,175 @@ pub trait ReflectCommandExt { impl ReflectCommandExt for EntityCommands<'_> { fn insert_reflect(&mut self, component: Box) -> &mut Self { - self.commands.queue(InsertReflect { - entity: self.entity, - component, + self.queue(move |mut entity: EntityWorldMut| { + entity.insert_reflect(component); + }) + } + + fn insert_reflect_with_registry>( + &mut self, + component: Box, + ) -> &mut Self { + self.queue(move |mut entity: EntityWorldMut| { + entity.insert_reflect_with_registry::(component); + }) + } + + fn remove_reflect(&mut self, component_type_path: impl Into>) -> &mut Self { + let component_type_path: Cow<'static, str> = component_type_path.into(); + self.queue(move |mut entity: EntityWorldMut| { + entity.remove_reflect(component_type_path); + }) + } + + fn remove_reflect_with_registry>( + &mut self, + component_type_path: impl Into>, + ) -> &mut Self { + let component_type_path: Cow<'static, str> = component_type_path.into(); + self.queue(move |mut entity: EntityWorldMut| { + entity.remove_reflect_with_registry::(component_type_path); + }) + } +} + +impl<'w> EntityWorldMut<'w> { + /// Adds the given boxed reflect component or bundle to the entity using the reflection data in + /// [`AppTypeRegistry`]. + /// + /// This will overwrite any previous component(s) of the same type. + /// + /// # Panics + /// + /// - If the entity has been despawned while this `EntityWorldMut` is still alive. + /// - If [`AppTypeRegistry`] does not have the reflection data for the given + /// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle). + /// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details. + /// - If [`AppTypeRegistry`] is not present in the [`World`]. + /// + /// # Note + /// + /// Prefer to use the typed [`EntityWorldMut::insert`] if possible. Adding a reflected component + /// is much slower. + pub fn insert_reflect(&mut self, component: Box) -> &mut Self { + self.assert_not_despawned(); + let entity_id = self.id(); + self.world_scope(|world| { + world.resource_scope(|world, registry: Mut| { + let type_registry = ®istry.as_ref().read(); + insert_reflect_with_registry_ref(world, entity_id, type_registry, component); + }); + world.flush(); }); + self.update_location(); self } - fn insert_reflect_with_registry>( + /// Same as [`insert_reflect`](EntityWorldMut::insert_reflect), but using + /// the `T` resource as type registry instead of [`AppTypeRegistry`]. + /// + /// This will overwrite any previous component(s) of the same type. + /// + /// # Panics + /// + /// - If the entity has been despawned while this `EntityWorldMut` is still alive. + /// - If the given [`Resource`] does not have the reflection data for the given + /// [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle). + /// - If the component or bundle data is invalid. See [`PartialReflect::apply`] for further details. + /// - If the given [`Resource`] is not present in the [`World`]. + pub fn insert_reflect_with_registry>( &mut self, component: Box, ) -> &mut Self { - self.commands.queue(InsertReflectWithRegistry:: { - entity: self.entity, - _t: PhantomData, - component, + self.assert_not_despawned(); + let entity_id = self.id(); + self.world_scope(|world| { + world.resource_scope(|world, registry: Mut| { + let type_registry = registry.as_ref().as_ref(); + insert_reflect_with_registry_ref(world, entity_id, type_registry, component); + }); + world.flush(); }); + self.update_location(); self } - fn remove_reflect(&mut self, component_type_path: impl Into>) -> &mut Self { - self.commands.queue(RemoveReflect { - entity: self.entity, - component_type_path: component_type_path.into(), + /// Removes from the entity the component or bundle with the given type name 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. + /// + /// Does nothing if the type is a component and the entity does not have a component of the same type, + /// if the type is a bundle and the entity does not contain any of the components in the bundle, + /// or if [`AppTypeRegistry`] does not contain the reflection data for the given component. + /// + /// # Panics + /// + /// - If the entity has been despawned while this `EntityWorldMut` is still alive. + /// - If [`AppTypeRegistry`] is not present in the [`World`]. + /// + /// # Note + /// + /// Prefer to use the typed [`EntityCommands::remove`] if possible. Removing a reflected component + /// is much slower. + pub fn remove_reflect(&mut self, component_type_path: Cow<'static, str>) -> &mut Self { + self.assert_not_despawned(); + let entity_id = self.id(); + self.world_scope(|world| { + world.resource_scope(|world, registry: Mut| { + let type_registry = ®istry.as_ref().read(); + remove_reflect_with_registry_ref( + world, + entity_id, + type_registry, + component_type_path, + ); + }); + world.flush(); }); + self.update_location(); self } - fn remove_reflect_with_registry>( + /// Same as [`remove_reflect`](EntityWorldMut::remove_reflect), but using + /// the `T` resource as type registry instead of `AppTypeRegistry`. + /// + /// If the given type is a bundle, it will remove any components in that bundle regardless if the entity + /// contains all the components. + /// + /// Does nothing if the type is a component and the entity does not have a component of the same type, + /// if the type is a bundle and the entity does not contain any of the components in the bundle, + /// or if [`AppTypeRegistry`] does not contain the reflection data for the given component. + /// + /// # Panics + /// + /// - If the entity has been despawned while this `EntityWorldMut` is still alive. + /// - If [`AppTypeRegistry`] is not present in the [`World`]. + pub fn remove_reflect_with_registry>( &mut self, - component_type_name: impl Into>, + component_type_path: Cow<'static, str>, ) -> &mut Self { - self.commands.queue(RemoveReflectWithRegistry:: { - entity: self.entity, - _t: PhantomData, - component_type_name: component_type_name.into(), + self.assert_not_despawned(); + let entity_id = self.id(); + self.world_scope(|world| { + world.resource_scope(|world, registry: Mut| { + let type_registry = registry.as_ref().as_ref(); + remove_reflect_with_registry_ref( + world, + entity_id, + type_registry, + component_type_path, + ); + }); + world.flush(); }); + self.update_location(); self } } /// Helper function to add a reflect component or bundle to a given entity -fn insert_reflect( +fn insert_reflect_with_registry_ref( world: &mut World, entity: Entity, type_registry: &TypeRegistry, @@ -223,7 +349,7 @@ fn insert_reflect( 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", - world.entities().entity_does_not_exist_error_details_message(entity)); + world.entities().entity_does_not_exist_error_details(entity)); }; let Some(type_registration) = type_registry.get(type_info.type_id()) else { panic!("`{type_path}` should be registered in type registry via `App::register_type<{type_path}>`"); @@ -238,48 +364,8 @@ fn insert_reflect( } } -/// A [`Command`] that adds the boxed reflect component or bundle to an entity using the data in -/// [`AppTypeRegistry`]. -/// -/// See [`ReflectCommandExt::insert_reflect`] for details. -pub struct InsertReflect { - /// The entity on which the component will be inserted. - pub entity: Entity, - /// The reflect [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle) - /// that will be added to the entity. - pub component: Box, -} - -impl Command for InsertReflect { - fn apply(self, world: &mut World) { - let registry = world.get_resource::().unwrap().clone(); - insert_reflect(world, self.entity, ®istry.read(), self.component); - } -} - -/// A [`Command`] that adds the boxed reflect component or bundle to an entity using the data in the provided -/// [`Resource`] that implements [`AsRef`]. -/// -/// See [`ReflectCommandExt::insert_reflect_with_registry`] for details. -pub struct InsertReflectWithRegistry> { - /// The entity on which the component will be inserted. - pub entity: Entity, - pub _t: PhantomData, - /// The reflect [`Component`](crate::component::Component) that will be added to the entity. - pub component: Box, -} - -impl> Command for InsertReflectWithRegistry { - fn apply(self, world: &mut World) { - world.resource_scope(|world, registry: Mut| { - let registry: &TypeRegistry = registry.as_ref().as_ref(); - insert_reflect(world, self.entity, registry, self.component); - }); - } -} - /// Helper function to remove a reflect component or bundle from a given entity -fn remove_reflect( +fn remove_reflect_with_registry_ref( world: &mut World, entity: Entity, type_registry: &TypeRegistry, @@ -298,54 +384,6 @@ fn remove_reflect( } } -/// A [`Command`] that removes the component or bundle of the same type as the given type name from -/// the provided entity. -/// -/// See [`ReflectCommandExt::remove_reflect`] for details. -pub struct RemoveReflect { - /// The entity from which the component will be removed. - pub entity: Entity, - /// The [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle) - /// type name that will be used to remove a component - /// of the same type from the entity. - pub component_type_path: Cow<'static, str>, -} - -impl Command for RemoveReflect { - fn apply(self, world: &mut World) { - let registry = world.get_resource::().unwrap().clone(); - remove_reflect( - world, - self.entity, - ®istry.read(), - self.component_type_path, - ); - } -} - -/// A [`Command`] that removes the component or bundle of the same type as the given type name from -/// the provided entity using the provided [`Resource`] that implements [`AsRef`]. -/// -/// See [`ReflectCommandExt::remove_reflect_with_registry`] for details. -pub struct RemoveReflectWithRegistry> { - /// The entity from which the component will be removed. - pub entity: Entity, - pub _t: PhantomData, - /// The [`Component`](crate::component::Component) or [`Bundle`](crate::bundle::Bundle) - /// type name that will be used to remove a component - /// of the same type from the entity. - pub component_type_name: Cow<'static, str>, -} - -impl> Command for RemoveReflectWithRegistry { - fn apply(self, world: &mut World) { - world.resource_scope(|world, registry: Mut| { - let registry: &TypeRegistry = registry.as_ref().as_ref(); - remove_reflect(world, self.entity, registry, self.component_type_name); - }); - } -} - #[cfg(test)] mod tests { use crate::{ @@ -357,6 +395,7 @@ mod tests { system::{Commands, SystemState}, world::World, }; + use alloc::{borrow::ToOwned, boxed::Box}; use bevy_ecs_macros::Resource; use bevy_reflect::{PartialReflect, Reflect, TypeRegistry}; diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs index 7df072ab2d39f..c81a5cfa1eb2c 100644 --- a/crates/bevy_ecs/src/removal_detection.rs +++ b/crates/bevy_ecs/src/removal_detection.rs @@ -134,7 +134,7 @@ impl RemovedComponentEvents { /// # #[derive(Component)] /// # struct MyComponent; /// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{:?}", removed_entity)); +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); /// } /// # bevy_ecs::system::assert_is_system(react_on_removal); /// ``` @@ -233,8 +233,7 @@ impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { /// Returns `true` if there are no events available to read. pub fn is_empty(&self) -> bool { self.events() - .map(|events| self.reader.is_empty(events)) - .unwrap_or(true) + .is_none_or(|events| self.reader.is_empty(events)) } /// Consumes all available events. diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 4f7745b1d687e..112d6d9481f4e 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -80,7 +80,7 @@ pub trait Condition: sealed::Condition /// /// # Examples /// - /// ``` + /// ```should_panic /// use bevy_ecs::prelude::*; /// /// #[derive(Resource, PartialEq)] @@ -90,7 +90,7 @@ pub trait Condition: sealed::Condition /// # let mut world = World::new(); /// # fn my_system() {} /// app.add_systems( - /// // The `resource_equals` run condition will fail since we don't initialize `R`, + /// // The `resource_equals` run condition will panic since we don't initialize `R`, /// // just like if we used `Res` in a system. /// my_system.run_if(resource_equals(R(0))), /// ); @@ -398,6 +398,7 @@ pub mod common_conditions { change_detection::DetectChanges, event::{Event, EventReader}, prelude::{Component, Query, With}, + query::QueryFilter, removal_detection::RemovedComponents, system::{In, IntoSystem, Local, Res, Resource, System, SystemInput}, }; @@ -937,6 +938,12 @@ pub mod common_conditions { removals.read().count() > 0 } + /// A [`Condition`]-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. /// /// # Example @@ -1256,6 +1263,7 @@ where mod tests { use super::{common_conditions::*, Condition}; use crate as bevy_ecs; + use crate::query::With; use crate::{ change_detection::ResMut, component::Component, @@ -1396,6 +1404,7 @@ mod tests { .distributive_run_if(resource_removed::) .distributive_run_if(on_event::) .distributive_run_if(any_with_component::) + .distributive_run_if(any_match_filter::>) .distributive_run_if(not(run_once)), ); } diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index a4518c0255773..20308578a4726 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -9,7 +9,7 @@ use crate::{ set::{InternedSystemSet, IntoSystemSet, SystemSet}, Chain, }, - system::{BoxedSystem, IntoSystem, ScheduleSystem, System}, + system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, }; fn new_condition(condition: impl Condition) -> BoxedCondition { @@ -431,7 +431,7 @@ where /// /// Ordering constraints will be applied between the successive elements. /// - /// If the preceding node on a edge has deferred parameters, a [`ApplyDeferred`](crate::schedule::ApplyDeferred) + /// If the preceding node on an edge has deferred parameters, an [`ApplyDeferred`](crate::schedule::ApplyDeferred) /// will be inserted on the edge. If this behavior is not desired consider using /// [`chain_ignore_deferred`](Self::chain_ignore_deferred) instead. fn chain(self) -> SystemConfigs { @@ -519,6 +519,7 @@ impl IntoSystemConfigs<()> for SystemConfigs { } } +/// Marker component to allow for conflicting implementations of [`IntoSystemConfigs`] #[doc(hidden)] pub struct Infallible; @@ -527,17 +528,12 @@ where F: IntoSystem<(), (), Marker>, { fn into_configs(self) -> SystemConfigs { - let boxed_system = Box::new(IntoSystem::into_system(self)); - SystemConfigs::new_system(ScheduleSystem::Infallible(boxed_system)) - } -} - -impl IntoSystemConfigs<()> for BoxedSystem<(), ()> { - fn into_configs(self) -> SystemConfigs { - SystemConfigs::new_system(ScheduleSystem::Infallible(self)) + let wrapper = InfallibleSystemWrapper::new(IntoSystem::into_system(self)); + SystemConfigs::new_system(Box::new(wrapper)) } } +/// Marker component to allow for conflicting implementations of [`IntoSystemConfigs`] #[doc(hidden)] pub struct Fallible; @@ -547,13 +543,13 @@ where { fn into_configs(self) -> SystemConfigs { let boxed_system = Box::new(IntoSystem::into_system(self)); - SystemConfigs::new_system(ScheduleSystem::Fallible(boxed_system)) + SystemConfigs::new_system(boxed_system) } } impl IntoSystemConfigs<()> for BoxedSystem<(), Result> { fn into_configs(self) -> SystemConfigs { - SystemConfigs::new_system(ScheduleSystem::Fallible(self)) + SystemConfigs::new_system(self) } } @@ -567,7 +563,14 @@ macro_rules! impl_system_collection { where $($sys: IntoSystemConfigs<$param>),* { - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "We are inside a macro, and as such, `non_snake_case` is not guaranteed to apply." + )] + #[allow( + non_snake_case, + reason = "Variable names are provided by the macro caller, not by us." + )] fn into_configs(self) -> SystemConfigs { let ($($sys,)*) = self; SystemConfigs::Configs { @@ -641,7 +644,7 @@ where self.into_configs().before(set) } - /// Runs before all systems in `set`. If `set` has any systems that produce [`Commands`](crate::system::Commands) + /// Runs after all systems in `set`. If `set` has any systems that produce [`Commands`](crate::system::Commands) /// or other [`Deferred`](crate::system::Deferred) operations, all systems in `self` will see their effect. /// /// If automatically inserting [`ApplyDeferred`](crate::schedule::ApplyDeferred) like @@ -792,7 +795,14 @@ macro_rules! impl_system_set_collection { $(#[$meta])* impl<$($set: IntoSystemSetConfigs),*> IntoSystemSetConfigs for ($($set,)*) { - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "We are inside a macro, and as such, `non_snake_case` is not guaranteed to apply." + )] + #[allow( + non_snake_case, + reason = "Variable names are provided by the macro caller, not by us." + )] fn into_configs(self) -> SystemSetConfigs { let ($($set,)*) = self; SystemSetConfigs::Configs { diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 9218fe33fe0da..8b549a15ea6d3 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -18,6 +18,7 @@ use crate::{ component::{ComponentId, Tick}, prelude::{IntoSystemSet, SystemSet}, query::Access, + result::Result, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, system::{ScheduleSystem, System, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -121,7 +122,10 @@ impl SystemSchedule { since = "0.16.0", note = "Use `ApplyDeferred` instead. This was previously a function but is now a marker struct System." )] -#[expect(non_upper_case_globals)] +#[expect( + non_upper_case_globals, + reason = "This item is deprecated; as such, its previous name needs to stay." +)] pub const apply_deferred: ApplyDeferred = ApplyDeferred; /// A special [`System`] that instructs the executor to call @@ -158,7 +162,7 @@ pub(super) fn is_apply_deferred(system: &ScheduleSystem) -> bool { impl System for ApplyDeferred { type In = (); - type Out = (); + type Out = Result<()>; fn name(&self) -> Cow<'static, str> { Cow::Borrowed("bevy_ecs::apply_deferred") @@ -203,11 +207,13 @@ impl System for ApplyDeferred { ) -> Self::Out { // 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 { // This system does nothing on its own. The executor will apply deferred // commands from other systems instead of running this system. + Ok(()) } fn apply_deferred(&mut self, _world: &mut World) {} @@ -259,7 +265,7 @@ mod __rust_begin_short_backtrace { use crate::{ result::Result, - system::{ReadOnlySystem, ScheduleSystem, System}, + system::{ReadOnlySystem, ScheduleSystem}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -308,7 +314,7 @@ mod tests { self as bevy_ecs, prelude::{IntoSystemConfigs, IntoSystemSetConfigs, Resource, Schedule, SystemSet}, schedule::ExecutorKind, - system::{Commands, In, IntoSystem, Res}, + system::{Commands, Res, WithParamWarnPolicy}, world::World, }; @@ -337,15 +343,11 @@ mod tests { schedule.set_executor_kind(executor); schedule.add_systems( ( - // Combined systems get skipped together. - (|mut commands: Commands| { - commands.insert_resource(R1); - }) - .pipe(|_: In<()>, _: Res| {}), // This system depends on a system that is always skipped. - |mut commands: Commands| { + (|mut commands: Commands| { commands.insert_resource(R2); - }, + }) + .warn_param_missing(), ) .chain(), ); @@ -368,18 +370,20 @@ mod tests { let mut world = World::new(); let mut schedule = Schedule::default(); schedule.set_executor_kind(executor); - schedule.configure_sets(S1.run_if(|_: Res| true)); + schedule.configure_sets(S1.run_if((|_: Res| true).warn_param_missing())); schedule.add_systems(( // System gets skipped if system set run conditions fail validation. (|mut commands: Commands| { commands.insert_resource(R1); }) + .warn_param_missing() .in_set(S1), // System gets skipped if run conditions fail validation. (|mut commands: Commands| { commands.insert_resource(R2); }) - .run_if(|_: Res| true), + .warn_param_missing() + .run_if((|_: Res| true).warn_param_missing()), )); schedule.run(&mut world); assert!(world.get_resource::().is_none()); diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 2d687ed510833..a9eaea7311789 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -4,7 +4,10 @@ use bevy_utils::{default, syncunsafecell::SyncUnsafeCell}; use concurrent_queue::ConcurrentQueue; use core::{any::Any, panic::AssertUnwindSafe}; use fixedbitset::FixedBitSet; -use std::sync::{Mutex, MutexGuard}; +use std::{ + eprintln, + sync::{Mutex, MutexGuard}, +}; #[cfg(feature = "trace")] use tracing::{info_span, Span}; @@ -20,7 +23,7 @@ use crate::{ prelude::Resource, query::Access, schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, - system::{ScheduleSystem, System}, + system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -605,11 +608,17 @@ impl ExecutorState { // access the world data used by the system. // - `update_archetype_component_access` has been called. unsafe { - // TODO: implement an error-handling API instead of suppressing a possible failure. - let _ = __rust_begin_short_backtrace::run_unsafe( + // TODO: implement an error-handling API instead of panicking. + if let Err(err) = __rust_begin_short_backtrace::run_unsafe( system, context.environment.world_cell, - ); + ) { + panic!( + "Encountered an error in system `{}`: {:?}", + &*system.name(), + err + ); + }; }; })); context.system_completed(system_index, res, system); @@ -653,8 +662,14 @@ 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(|| { - // TODO: implement an error-handling API instead of suppressing a possible failure. - let _ = __rust_begin_short_backtrace::run(system, world); + // TODO: implement an error-handling API instead of panicking. + if let Err(err) = __rust_begin_short_backtrace::run(system, world) { + panic!( + "Encountered an error in system `{}`: {:?}", + &*system.name(), + err + ); + }; })); context.system_completed(system_index, res, system); }; @@ -743,8 +758,10 @@ unsafe fn evaluate_and_fold_conditions( conditions: &mut [BoxedCondition], world: UnsafeWorldCell, ) -> bool { - // not short-circuiting is intentional - #[allow(clippy::unnecessary_fold)] + #[expect( + clippy::unnecessary_fold, + reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." + )] conditions .iter_mut() .map(|condition| { diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 5cf03e2088276..81f7deab3a302 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -1,13 +1,16 @@ use core::panic::AssertUnwindSafe; use fixedbitset::FixedBitSet; + #[cfg(feature = "trace")] use tracing::info_span; +#[cfg(feature = "std")] +use std::eprintln; + use crate::{ schedule::{ executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, }, - system::System, world::World, }; @@ -101,8 +104,14 @@ impl SystemExecutor for SimpleExecutor { } let f = AssertUnwindSafe(|| { - // TODO: implement an error-handling API instead of suppressing a possible failure. - let _ = __rust_begin_short_backtrace::run(system, world); + // TODO: implement an error-handling API instead of panicking. + if let Err(err) = __rust_begin_short_backtrace::run(system, world) { + panic!( + "Encountered an error in system `{}`: {:?}", + &*system.name(), + err + ); + } }); #[cfg(feature = "std")] @@ -130,7 +139,7 @@ impl SystemExecutor for SimpleExecutor { impl SimpleExecutor { /// Creates a new simple executor for use in a [`Schedule`](crate::schedule::Schedule). - /// This calls each system in order and immediately calls [`System::apply_deferred`]. + /// This calls each system in order and immediately calls [`System::apply_deferred`](crate::system::System). pub const fn new() -> Self { Self { evaluated_sets: FixedBitSet::new(), @@ -140,8 +149,10 @@ impl SimpleExecutor { } fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - // not short-circuiting is intentional - #[allow(clippy::unnecessary_fold)] + #[expect( + clippy::unnecessary_fold, + reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." + )] conditions .iter_mut() .map(|condition| { diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index d19a222d3011b..8c5a7e0261bbe 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -1,11 +1,14 @@ use core::panic::AssertUnwindSafe; use fixedbitset::FixedBitSet; + #[cfg(feature = "trace")] use tracing::info_span; +#[cfg(feature = "std")] +use std::eprintln; + use crate::{ schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, - system::System, world::World, }; @@ -109,8 +112,14 @@ impl SystemExecutor for SingleThreadedExecutor { let f = AssertUnwindSafe(|| { if system.is_exclusive() { - // TODO: implement an error-handling API instead of suppressing a possible failure. - let _ = __rust_begin_short_backtrace::run(system, world); + // TODO: implement an error-handling API instead of panicking. + if let Err(err) = __rust_begin_short_backtrace::run(system, world) { + panic!( + "Encountered an error in system `{}`: {:?}", + &*system.name(), + err + ); + } } else { // Use run_unsafe to avoid immediately applying deferred buffers let world = world.as_unsafe_world_cell(); @@ -118,8 +127,14 @@ impl SystemExecutor for SingleThreadedExecutor { // SAFETY: We have exclusive, single-threaded access to the world and // update_archetype_component_access is being called immediately before this. unsafe { - // TODO: implement an error-handling API instead of suppressing a possible failure. - let _ = __rust_begin_short_backtrace::run_unsafe(system, world); + // TODO: implement an error-handling API instead of panicking. + if let Err(err) = __rust_begin_short_backtrace::run_unsafe(system, world) { + panic!( + "Encountered an error in system `{}`: {:?}", + &*system.name(), + err + ); + } }; } }); @@ -176,8 +191,10 @@ impl SingleThreadedExecutor { } fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { - // not short-circuiting is intentional - #[allow(clippy::unnecessary_fold)] + #[expect( + clippy::unnecessary_fold, + reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." + )] conditions .iter_mut() .map(|condition| { diff --git a/crates/bevy_ecs/src/schedule/graph/graph_map.rs b/crates/bevy_ecs/src/schedule/graph/graph_map.rs index b20c763543181..35f3260d3e5a7 100644 --- a/crates/bevy_ecs/src/schedule/graph/graph_map.rs +++ b/crates/bevy_ecs/src/schedule/graph/graph_map.rs @@ -32,7 +32,7 @@ pub type DiGraph = Graph; /// `Graph` is a graph datastructure using an associative array /// of its node weights `NodeId`. /// -/// It uses an combined adjacency list and sparse adjacency matrix +/// It uses a combined adjacency list and sparse adjacency matrix /// representation, using **O(|N| + |E|)** space, and allows testing for edge /// existence in constant time. /// @@ -125,7 +125,7 @@ where /// For a directed graph, the edge is directed from `a` to `b`. /// /// Inserts nodes `a` and/or `b` if they aren't already part of the graph. - pub(crate) fn add_edge(&mut self, a: NodeId, b: NodeId) { + pub fn add_edge(&mut self, a: NodeId, b: NodeId) { if self.edges.insert(Self::edge_key(a, b)) { // insert in the adjacency list if it's a new edge self.nodes @@ -393,6 +393,7 @@ impl CompactNodeIdPair { #[cfg(test)] mod tests { use super::*; + use alloc::vec; /// 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 @@ -436,7 +437,7 @@ mod tests { assert_eq!(graph.nodes().collect::>(), vec![]); } - /// Nodes that have bidrectional edges (or any edge in the case of undirected graphs) are + /// Nodes that have bidirectional edges (or any edge in the case of undirected graphs) are /// considered strongly connected. A strongly connected component is a collection of /// nodes where there exists a path from any node to any other node in the collection. #[test] diff --git a/crates/bevy_ecs/src/schedule/graph/mod.rs b/crates/bevy_ecs/src/schedule/graph/mod.rs index 1532184f1761e..eb55d7db0d86c 100644 --- a/crates/bevy_ecs/src/schedule/graph/mod.rs +++ b/crates/bevy_ecs/src/schedule/graph/mod.rs @@ -86,7 +86,7 @@ pub(crate) struct CheckGraphResults { pub(crate) transitive_reduction: DiGraph, /// Variant of the graph with all possible transitive edges. // TODO: this will very likely be used by "if-needed" ordering - #[allow(dead_code)] + #[expect(dead_code, reason = "See the TODO above this attribute.")] pub(crate) transitive_closure: DiGraph, } diff --git a/crates/bevy_ecs/src/schedule/graph/node.rs b/crates/bevy_ecs/src/schedule/graph/node.rs index 3ee2c97824fb4..bf44af73aec5d 100644 --- a/crates/bevy_ecs/src/schedule/graph/node.rs +++ b/crates/bevy_ecs/src/schedule/graph/node.rs @@ -3,7 +3,7 @@ use core::fmt::Debug; /// Unique identifier for a system or system set stored in a [`ScheduleGraph`]. /// /// [`ScheduleGraph`]: crate::schedule::ScheduleGraph -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum NodeId { /// Identifier for a system. System(usize), @@ -11,18 +11,6 @@ pub enum NodeId { Set(usize), } -impl PartialOrd for NodeId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(Ord::cmp(self, other)) - } -} - -impl Ord for NodeId { - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - self.cmp(other) - } -} - impl NodeId { /// Returns the internal integer value. pub(crate) const fn index(&self) -> usize { diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index dc2a907949bd1..59340f0d9228d 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -4,7 +4,6 @@ mod condition; mod config; mod executor; mod graph; -#[allow(clippy::module_inception)] mod schedule; mod set; mod stepping; @@ -17,6 +16,7 @@ pub use self::graph::NodeId; #[cfg(test)] mod tests { use super::*; + use alloc::{string::ToString, vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; pub use crate as bevy_ecs; diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index ff1d8283e8d6c..33568ec09f5f5 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1,3 +1,7 @@ +#![expect( + clippy::module_inception, + reason = "This instance of module inception is being discussed; see #17344." +)] use alloc::{ boxed::Box, collections::BTreeSet, @@ -21,7 +25,7 @@ use crate::{ prelude::Component, result::Result, schedule::*, - system::{IntoSystem, Resource, ScheduleSystem, System}, + system::{IntoSystem, Resource, ScheduleSystem}, world::World, }; @@ -108,8 +112,13 @@ impl Schedules { pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); - // label used when trace feature is enabled - #[allow(unused_variables)] + #[cfg_attr( + not(feature = "trace"), + expect( + unused_variables, + reason = "The `label` variable goes unused if the `trace` feature isn't active" + ) + )] for (label, schedule) in &mut self.inner { #[cfg(feature = "trace")] let name = format!("{label:?}"); @@ -1053,7 +1062,7 @@ impl ScheduleGraph { Ok(()) } - /// Initializes any newly-added systems and conditions by calling [`System::initialize`] + /// 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(..) { match id { @@ -1200,8 +1209,8 @@ impl ScheduleGraph { let id = NodeId::System(self.systems.len()); self.systems - .push(SystemNode::new(ScheduleSystem::Infallible(Box::new( - IntoSystem::into_system(ApplyDeferred), + .push(SystemNode::new(Box::new(IntoSystem::into_system( + ApplyDeferred, )))); self.system_conditions.push(Vec::new()); diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index ed796c29e9e4e..7855053de6fb7 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -1,6 +1,6 @@ use crate::{ schedule::{InternedScheduleLabel, NodeId, Schedule, ScheduleLabel}, - system::{IntoSystem, ResMut, Resource, System}, + system::{IntoSystem, ResMut, Resource}, }; use alloc::vec::Vec; use bevy_utils::{HashMap, TypeIdMap}; @@ -168,14 +168,8 @@ impl Stepping { if self.action == Action::RunAll { return None; } - let label = match self.schedule_order.get(self.cursor.schedule) { - None => return None, - Some(label) => label, - }; - let state = match self.schedule_states.get(label) { - None => return None, - Some(state) => state, - }; + let label = self.schedule_order.get(self.cursor.schedule)?; + let state = self.schedule_states.get(label)?; state .node_ids .get(self.cursor.system) @@ -420,7 +414,10 @@ impl Stepping { // transitions, and add debugging messages for permitted // transitions. Any action transition that falls through // this match block will be performed. - #[expect(clippy::match_same_arms)] + #[expect( + clippy::match_same_arms, + reason = "Readability would be negatively impacted by combining the `(Waiting, RunAll)` and `(Continue, RunAll)` match arms." + )] match (self.action, action) { // ignore non-transition updates, and prevent a call to // enable() from overwriting a step or continue call @@ -829,6 +826,8 @@ impl ScheduleState { mod tests { use super::*; use crate::{prelude::*, schedule::ScheduleLabel}; + use alloc::{format, vec}; + use std::println; pub use crate as bevy_ecs; diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index c508b78c9853c..86315386a8d1f 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -77,6 +77,8 @@ impl BlobArray { /// # Safety /// - The element at index `index` is safe to access. /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `index` < `len`) + /// + /// [`Vec::len`]: alloc::vec::Vec::len #[inline] pub unsafe fn get_unchecked(&self, index: usize) -> Ptr<'_> { #[cfg(debug_assertions)] @@ -98,6 +100,8 @@ impl BlobArray { /// # Safety /// - The element with at index `index` is safe to access. /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `index` < `len`) + /// + /// [`Vec::len`]: alloc::vec::Vec::len #[inline] pub unsafe fn get_unchecked_mut(&mut self, index: usize) -> PtrMut<'_> { #[cfg(debug_assertions)] @@ -134,6 +138,8 @@ impl BlobArray { /// # Safety /// - The type `T` must be the type of the items in this [`BlobArray`]. /// - `slice_len` <= `len` + /// + /// [`Vec::len`]: alloc::vec::Vec::len pub unsafe fn get_sub_slice(&self, slice_len: usize) -> &[UnsafeCell] { #[cfg(debug_assertions)] debug_assert!(slice_len <= self.capacity); @@ -151,6 +157,8 @@ impl BlobArray { /// # Safety /// - For every element with index `i`, if `i` < `len`: It must be safe to call [`Self::get_unchecked_mut`] with `i`. /// (If the safety requirements of every method that has been used on `Self` have been fulfilled, the caller just needs to ensure that `len` is correct.) + /// + /// [`Vec::clear`]: alloc::vec::Vec::clear pub unsafe fn clear(&mut self, len: usize) { #[cfg(debug_assertions)] debug_assert!(self.capacity >= len); diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index d42c63a6f1605..51a3d49e3c08d 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -497,11 +497,13 @@ const fn padding_needed_for(layout: &Layout, align: usize) -> usize { #[cfg(test)] mod tests { + use super::BlobVec; use crate as bevy_ecs; // required for derive macros use crate::{component::Component, ptr::OwningPtr, world::World}; - - use super::BlobVec; - use alloc::rc::Rc; + use alloc::{ + rc::Rc, + string::{String, ToString}, + }; use core::{alloc::Layout, cell::RefCell}; /// # Safety diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index 76f14b3e10ef2..501c6e80a535b 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -6,7 +6,7 @@ use crate::{ }; use alloc::string::String; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{cell::UnsafeCell, mem::ManuallyDrop}; @@ -30,7 +30,7 @@ pub struct ResourceData { id: ArchetypeComponentId, #[cfg(feature = "std")] origin_thread_id: Option, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: UnsafeCell<&'static Location<'static>>, } @@ -146,9 +146,9 @@ impl ResourceData { added: &self.added_ticks, changed: &self.changed_ticks, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] &self.changed_by, - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] (), ) }) @@ -166,7 +166,7 @@ impl ResourceData { value: unsafe { ptr.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. ticks: unsafe { TicksMut::from_tick_cells(ticks, last_run, this_run) }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: We have exclusive access to the underlying storage. changed_by: unsafe { _caller.deref_mut() }, }) @@ -186,7 +186,7 @@ impl ResourceData { &mut self, value: OwningPtr<'_>, change_tick: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { if self.is_present() { self.validate_access(); @@ -205,7 +205,7 @@ impl ResourceData { *self.added_ticks.deref_mut() = change_tick; } *self.changed_ticks.deref_mut() = change_tick; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by.deref_mut() = caller; } @@ -225,7 +225,7 @@ impl ResourceData { &mut self, value: OwningPtr<'_>, change_ticks: ComponentTicks, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { if self.is_present() { self.validate_access(); @@ -244,7 +244,7 @@ impl ResourceData { } *self.added_ticks.deref_mut() = change_ticks.added; *self.changed_ticks.deref_mut() = change_ticks.changed; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by.deref_mut() = caller; } @@ -268,9 +268,9 @@ impl ResourceData { let res = unsafe { self.data.swap_remove_and_forget_unchecked(Self::ROW) }; // SAFETY: This function is being called through an exclusive mutable reference to Self - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = unsafe { *self.changed_by.deref_mut() }; - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] let caller = (); // SAFETY: This function is being called through an exclusive mutable reference to Self, which @@ -392,7 +392,7 @@ impl Resources { id: f(), #[cfg(feature = "std")] origin_thread_id: None, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: UnsafeCell::new(Location::caller()) } }) diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 14b135fd04241..518333fa272d8 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -6,7 +6,7 @@ use crate::{ }; use alloc::{boxed::Box, vec::Vec}; use bevy_ptr::{OwningPtr, Ptr}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{cell::UnsafeCell, hash::Hash, marker::PhantomData}; use nonmax::NonMaxUsize; @@ -50,7 +50,7 @@ macro_rules! impl_sparse_array { #[inline] pub fn contains(&self, index: I) -> bool { let index = index.sparse_set_index(); - self.values.get(index).map(|v| v.is_some()).unwrap_or(false) + self.values.get(index).is_some_and(Option::is_some) } /// Returns a reference to the value at `index`. @@ -59,7 +59,7 @@ macro_rules! impl_sparse_array { #[inline] pub fn get(&self, index: I) -> Option<&V> { let index = index.sparse_set_index(); - self.values.get(index).map(|v| v.as_ref()).unwrap_or(None) + self.values.get(index).and_then(Option::as_ref) } } }; @@ -87,10 +87,7 @@ impl SparseArray { #[inline] pub fn get_mut(&mut self, index: I) -> Option<&mut V> { let index = index.sparse_set_index(); - self.values - .get_mut(index) - .map(|v| v.as_mut()) - .unwrap_or(None) + self.values.get_mut(index).and_then(Option::as_mut) } /// Removes and returns the value stored at `index`. @@ -173,7 +170,7 @@ impl ComponentSparseSet { entity: Entity, value: OwningPtr<'_>, change_tick: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { if let Some(&dense_index) = self.sparse.get(entity.index()) { #[cfg(debug_assertions)] @@ -182,7 +179,7 @@ impl ComponentSparseSet { dense_index, value, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } else { @@ -190,7 +187,7 @@ impl ComponentSparseSet { self.dense.push( value, ComponentTicks::new(change_tick), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); self.sparse @@ -253,9 +250,9 @@ impl ComponentSparseSet { added: self.dense.get_added_tick_unchecked(dense_index), changed: self.dense.get_changed_tick_unchecked(dense_index), }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.dense.get_changed_by_unchecked(dense_index), - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] (), )) } @@ -301,7 +298,7 @@ impl ComponentSparseSet { /// /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn get_changed_by( &self, entity: Entity, @@ -669,6 +666,7 @@ mod tests { entity::Entity, storage::SparseSet, }; + use alloc::{vec, vec::Vec}; #[derive(Debug, Eq, PartialEq)] struct Foo(usize); diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index f7ea1683e458f..4054b5c15fd54 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -17,7 +17,7 @@ pub struct ThinColumn { pub(super) data: BlobArray, pub(super) added_ticks: ThinArrayPtr>, pub(super) changed_ticks: ThinArrayPtr>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub(super) changed_by: ThinArrayPtr>>, } @@ -31,7 +31,7 @@ impl ThinColumn { }, added_ticks: ThinArrayPtr::with_capacity(capacity), changed_ticks: ThinArrayPtr::with_capacity(capacity), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: ThinArrayPtr::with_capacity(capacity), } } @@ -54,7 +54,7 @@ impl ThinColumn { .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); self.changed_ticks .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by .swap_remove_unchecked_nonoverlapping(row.as_usize(), last_element_index); } @@ -76,7 +76,7 @@ impl ThinColumn { .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); self.changed_ticks .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by .swap_remove_and_drop_unchecked(row.as_usize(), last_element_index); } @@ -99,7 +99,7 @@ impl ThinColumn { .swap_remove_unchecked(row.as_usize(), last_element_index); self.changed_ticks .swap_remove_unchecked(row.as_usize(), last_element_index); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by .swap_remove_unchecked(row.as_usize(), last_element_index); } @@ -117,7 +117,7 @@ impl ThinColumn { self.data.realloc(current_capacity, new_capacity); self.added_ticks.realloc(current_capacity, new_capacity); self.changed_ticks.realloc(current_capacity, new_capacity); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.realloc(current_capacity, new_capacity); } @@ -127,7 +127,7 @@ impl ThinColumn { self.data.alloc(new_capacity); self.added_ticks.alloc(new_capacity); self.changed_ticks.alloc(new_capacity); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.alloc(new_capacity); } @@ -144,7 +144,7 @@ impl ThinColumn { row: TableRow, data: OwningPtr<'_>, tick: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { self.data.initialize_unchecked(row.as_usize(), data); *self.added_ticks.get_unchecked_mut(row.as_usize()).get_mut() = tick; @@ -152,7 +152,7 @@ impl ThinColumn { .changed_ticks .get_unchecked_mut(row.as_usize()) .get_mut() = tick; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by.get_unchecked_mut(row.as_usize()).get_mut() = caller; } @@ -169,14 +169,14 @@ impl ThinColumn { row: TableRow, data: OwningPtr<'_>, change_tick: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { self.data.replace_unchecked(row.as_usize(), data); *self .changed_ticks .get_unchecked_mut(row.as_usize()) .get_mut() = change_tick; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by.get_unchecked_mut(row.as_usize()).get_mut() = caller; } @@ -218,11 +218,11 @@ impl ThinColumn { .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); self.changed_ticks .initialize_unchecked(dst_row.as_usize(), changed_tick); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let changed_by = other .changed_by .swap_remove_unchecked(src_row.as_usize(), other_last_element_index); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by .initialize_unchecked(dst_row.as_usize(), changed_by); } @@ -258,7 +258,7 @@ impl ThinColumn { self.added_ticks.clear_elements(len); self.changed_ticks.clear_elements(len); self.data.clear(len); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.clear_elements(len); } @@ -273,7 +273,7 @@ impl ThinColumn { self.added_ticks.drop(cap, len); self.changed_ticks.drop(cap, len); self.data.drop(cap, len); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.drop(cap, len); } @@ -285,7 +285,7 @@ impl ThinColumn { pub(crate) unsafe fn drop_last_component(&mut self, last_element_index: usize) { core::ptr::drop_in_place(self.added_ticks.get_unchecked_raw(last_element_index)); core::ptr::drop_in_place(self.changed_ticks.get_unchecked_raw(last_element_index)); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] core::ptr::drop_in_place(self.changed_by.get_unchecked_raw(last_element_index)); self.data.drop_last_element(last_element_index); } @@ -319,7 +319,7 @@ impl ThinColumn { /// /// # Safety /// - `len` must match the actual length of this column (number of elements stored) - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub unsafe fn get_changed_by_slice( &self, len: usize, @@ -343,7 +343,7 @@ pub struct Column { pub(super) data: BlobVec, pub(super) added_ticks: Vec>, pub(super) changed_ticks: Vec>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: Vec>>, } @@ -356,7 +356,7 @@ impl Column { data: unsafe { BlobVec::new(component_info.layout(), component_info.drop(), capacity) }, added_ticks: Vec::with_capacity(capacity), changed_ticks: Vec::with_capacity(capacity), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: Vec::with_capacity(capacity), } } @@ -378,7 +378,7 @@ impl Column { row: TableRow, data: OwningPtr<'_>, change_tick: Tick, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { debug_assert!(row.as_usize() < self.len()); self.data.replace_unchecked(row.as_usize(), data); @@ -386,7 +386,7 @@ impl Column { .changed_ticks .get_unchecked_mut(row.as_usize()) .get_mut() = change_tick; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { *self.changed_by.get_unchecked_mut(row.as_usize()).get_mut() = caller; } @@ -418,7 +418,7 @@ impl Column { 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()); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.swap_remove(row.as_usize()); } @@ -442,9 +442,9 @@ impl Column { 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(); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = self.changed_by.swap_remove(row.as_usize()).into_inner(); - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] let caller = (); (data, ComponentTicks { added, changed }, caller) } @@ -457,12 +457,12 @@ impl Column { &mut self, ptr: OwningPtr<'_>, ticks: ComponentTicks, - #[cfg(feature = "track_change_detection")] caller: &'static Location<'static>, + #[cfg(feature = "track_location")] caller: &'static Location<'static>, ) { self.data.push(ptr); self.added_ticks.push(UnsafeCell::new(ticks.added)); self.changed_ticks.push(UnsafeCell::new(ticks.changed)); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.push(UnsafeCell::new(caller)); } @@ -644,7 +644,7 @@ impl Column { self.data.clear(); self.added_ticks.clear(); self.changed_ticks.clear(); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.changed_by.clear(); } @@ -666,7 +666,7 @@ impl Column { /// Users of this API must ensure that accesses to each individual element /// adhere to the safety invariants of [`UnsafeCell`]. #[inline] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn get_changed_by(&self, row: TableRow) -> Option<&UnsafeCell<&'static Location<'static>>> { self.changed_by.get(row.as_usize()) } @@ -678,7 +678,7 @@ impl Column { /// # Safety /// `row` must be within the range `[0, self.len())`. #[inline] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub unsafe fn get_changed_by_unchecked( &self, row: TableRow, diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 1f82642c07d6b..25c343a531bfc 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -9,7 +9,7 @@ use alloc::{boxed::Box, vec, vec::Vec}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; use bevy_utils::HashMap; pub use column::*; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{ alloc::Layout, @@ -85,11 +85,11 @@ impl TableId { } } -/// A opaque newtype for rows in [`Table`]s. Specifies a single row in a specific table. +/// An opaque newtype for rows in [`Table`]s. Specifies a single row in a specific table. /// /// Values of this type are retrievable from [`Archetype::entity_table_row`] and can be /// used alongside [`Archetype::table_id`] to fetch the exact table and row where an -/// [`Entity`]'s +/// [`Entity`]'s components are stored. /// /// Values of this type are only valid so long as entities have not moved around. /// Adding and removing components from an entity, or despawning it will invalidate @@ -183,7 +183,7 @@ impl TableBuilder { /// A column-oriented [structure-of-arrays] based storage for [`Component`]s of entities /// in a [`World`]. /// -/// Conceptually, a `Table` can be thought of as an `HashMap`, where +/// Conceptually, a `Table` can be thought of as a `HashMap`, where /// each [`ThinColumn`] is a type-erased `Vec`. Each row corresponds to a single entity /// (i.e. index 3 in Column A and index 3 in Column B point to different components on the same /// entity). Fetching components from a table involves fetching the associated column for a @@ -390,7 +390,7 @@ impl Table { } /// Fetches the calling locations that last changed the each component - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn get_changed_by_slice_for( &self, component_id: ComponentId, @@ -433,7 +433,7 @@ impl Table { } /// Get the specific calling location that changed the component matching `component_id` in `row` - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn get_changed_by( &self, component_id: ComponentId, @@ -571,7 +571,7 @@ impl Table { .initialize_unchecked(len, UnsafeCell::new(Tick::new(0))); col.changed_ticks .initialize_unchecked(len, UnsafeCell::new(Tick::new(0))); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] col.changed_by .initialize_unchecked(len, UnsafeCell::new(Location::caller())); } @@ -743,6 +743,10 @@ impl Tables { component_ids: &[ComponentId], components: &Components, ) -> TableId { + if component_ids.is_empty() { + return TableId::empty(); + } + let tables = &mut self.tables; let (_key, value) = self .table_ids @@ -816,14 +820,28 @@ mod tests { component::{Component, Components, Tick}, entity::Entity, ptr::OwningPtr, - storage::{Storages, TableBuilder, TableRow}, + storage::{Storages, TableBuilder, TableId, TableRow, Tables}, }; - #[cfg(feature = "track_change_detection")] + use alloc::vec::Vec; + + #[cfg(feature = "track_location")] use core::panic::Location; #[derive(Component)] struct W(T); + #[test] + fn only_one_empty_table() { + let components = Components::default(); + let mut tables = Tables::default(); + + let component_ids = &[]; + // SAFETY: component_ids is empty, so we know it cannot reference invalid component IDs + let table_id = unsafe { tables.get_id_or_insert(component_ids, &components) }; + + assert_eq!(table_id, TableId::empty()); + } + #[test] fn table() { let mut components = Components::default(); @@ -844,7 +862,7 @@ mod tests { row, value_ptr, Tick::new(0), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); }); diff --git a/crates/bevy_ecs/src/storage/thin_array_ptr.rs b/crates/bevy_ecs/src/storage/thin_array_ptr.rs index 9c073324559d3..5654b6da67944 100644 --- a/crates/bevy_ecs/src/storage/thin_array_ptr.rs +++ b/crates/bevy_ecs/src/storage/thin_array_ptr.rs @@ -14,6 +14,8 @@ use core::{ /// /// This type can be treated as a `ManuallyDrop>` without a built in length. To avoid /// memory leaks, [`drop`](Self::drop) must be called when no longer in use. +/// +/// [`Vec`]: alloc::vec::Vec pub struct ThinArrayPtr { data: NonNull, #[cfg(debug_assertions)] diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index fe68ea6bfa6a7..89ec9f25189fc 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -162,7 +162,7 @@ pub unsafe trait SystemParamBuilder: Sized { /// .build_state(&mut world) /// .build_system(my_system); /// ``` -#[derive(Default, Debug, Copy, Clone)] +#[derive(Default, Debug, Clone)] pub struct ParamBuilder; // SAFETY: Calls `SystemParam::init_state` @@ -240,7 +240,7 @@ unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> /// .build_state(&mut world) /// .build_system(|query: Query<()>| { /// for _ in &query { -/// // This only includes entities with an `Player` component. +/// // This only includes entities with a `Player` component. /// } /// }); /// @@ -257,6 +257,7 @@ unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> /// .build_state(&mut world) /// .build_system(|query: Vec>| {}); /// ``` +#[derive(Clone)] pub struct QueryParamBuilder(T); impl QueryParamBuilder { @@ -299,14 +300,28 @@ unsafe impl< macro_rules! impl_system_param_builder_tuple { ($(#[$meta:meta])* $(($param: ident, $builder: ident)),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is in a macro; as such, the below lints may not always apply." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[allow( + non_snake_case, + reason = "The variable names are provided by the macro caller, not by us." + )] $(#[$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 { - #[allow(non_snake_case)] + fn build(self, world: &mut World, meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { let ($($builder,)*) = self; - #[allow(clippy::unused_unit)] - ($($builder.build(_world, _meta),)*) + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't generate any calls to the system parameter builders." + )] + ($($builder.build(world, meta),)*) } } }; @@ -401,14 +416,26 @@ unsafe impl> SystemParamBuilder> /// set.for_each(|mut query| for mut health in query.iter_mut() {}); /// } /// ``` +#[derive(Debug, Default, Clone)] pub struct ParamSetBuilder(pub T); macro_rules! impl_param_set_builder_tuple { ($(($param: ident, $builder: ident, $meta: ident)),*) => { + #[expect( + clippy::allow_attributes, + reason = "This is in a macro; as such, the below lints may not always apply." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] + #[allow( + non_snake_case, + reason = "The variable names are provided by the macro caller, not by us." + )] // 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,)*)> { - #[allow(non_snake_case)] - fn build(self, _world: &mut World, _system_meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> <($($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. @@ -416,22 +443,25 @@ macro_rules! impl_param_set_builder_tuple { // 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); + 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.set_non_send(); } $( - _system_meta + system_meta .component_access_set .extend($meta.component_access_set); - _system_meta + system_meta .archetype_component_access .extend(&$meta.archetype_component_access); )* - #[allow(clippy::unused_unit)] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't generate any calls to the system parameter builders." + )] ($($param,)*) } } @@ -520,6 +550,7 @@ unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamB /// }); /// # world.run_system_once(system); /// ``` +#[derive(Default, Debug, Clone)] pub struct LocalBuilder(pub T); // SAFETY: `Local` performs no world access. @@ -537,6 +568,7 @@ unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> /// A [`SystemParamBuilder`] for a [`FilteredResources`]. /// See the [`FilteredResources`] docs for examples. +#[derive(Clone)] pub struct FilteredResourcesParamBuilder(T); impl FilteredResourcesParamBuilder { @@ -600,6 +632,7 @@ unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesBuilder)> /// A [`SystemParamBuilder`] for a [`FilteredResourcesMut`]. /// See the [`FilteredResourcesMut`] docs for examples. +#[derive(Clone)] pub struct FilteredResourcesMutParamBuilder(T); impl FilteredResourcesMutParamBuilder { @@ -684,6 +717,7 @@ mod tests { prelude::{Component, Query}, system::{Local, RunSystemOnce}, }; + use alloc::vec; use super::*; diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 9d8652e9dbc00..f6e696a106a96 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -194,7 +194,7 @@ where // be called in parallel. Since mutable access to `world` only exists within // the scope of either closure, we can be sure they will never alias one another. |input| self.a.run(input, unsafe { world.world_mut() }), - #[allow(clippy::undocumented_unsafe_blocks)] + // SAFETY: See the above safety comment. |input| self.b.run(input, unsafe { world.world_mut() }), ) } @@ -214,7 +214,7 @@ where #[inline] unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { // SAFETY: Delegate to other `System` implementations. - unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) } + unsafe { self.a.validate_param_unsafe(world) } } fn initialize(&mut self, world: &mut World) { @@ -433,7 +433,7 @@ where unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { // SAFETY: Delegate to other `System` implementations. - unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) } + unsafe { self.a.validate_param_unsafe(world) } } fn validate_param(&mut self, world: &World) -> bool { diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs new file mode 100644 index 0000000000000..bbff4240684d4 --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -0,0 +1,315 @@ +//! This module contains the definition of the [`Command`] trait, as well as +//! blanket implementations of the trait for closures. +//! +//! It also contains functions that return closures for use with +//! [`Commands`](crate::system::Commands). + +#[cfg(feature = "track_location")] +use core::panic::Location; + +use crate::{ + bundle::{Bundle, InsertMode}, + entity::Entity, + event::{Event, Events}, + observer::TriggerTargets, + result::{Error, Result}, + schedule::ScheduleLabel, + system::{error_handler, IntoSystem, Resource, SystemId, SystemInput}, + world::{FromWorld, SpawnBatchIter, World}, +}; + +/// A [`World`] mutation. +/// +/// Should be used with [`Commands::queue`](crate::system::Commands::queue). +/// +/// The `Out` generic parameter is the returned "output" of the command. +/// +/// # Usage +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// // Our world resource +/// #[derive(Resource, Default)] +/// struct Counter(u64); +/// +/// // Our custom command +/// struct AddToCounter(u64); +/// +/// impl Command for AddToCounter { +/// fn apply(self, world: &mut World) { +/// let mut counter = world.get_resource_or_insert_with(Counter::default); +/// counter.0 += self.0; +/// } +/// } +/// +/// fn some_system(mut commands: Commands) { +/// commands.queue(AddToCounter(42)); +/// } +/// ``` +pub trait Command: Send + 'static { + /// Applies this command, causing it to mutate the provided `world`. + /// + /// This method is used to define what a command "does" when it is ultimately applied. + /// Because this method takes `self`, you can store data or settings on the type that implements this trait. + /// This data is set by the system or other source of the command, and then ultimately read in this method. + fn apply(self, world: &mut World) -> Out; +} + +impl Command for F +where + F: FnOnce(&mut World) -> Out + Send + 'static, +{ + fn apply(self, world: &mut World) -> Out { + self(world) + } +} + +/// 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 `()`. +pub trait HandleError { + /// 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(&mut World, Error)) -> 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(error_handler::default()) + } +} + +impl>, T, E: Into> HandleError> for C { + fn handle_error_with(self, error_handler: fn(&mut World, Error)) -> impl Command { + move |world: &mut World| match self.apply(world) { + Ok(_) => {} + Err(err) => (error_handler)(world, err.into()), + } + } +} + +impl HandleError for C { + #[inline] + fn handle_error_with(self, _error_handler: fn(&mut World, Error)) -> impl Command { + self + } + #[inline] + fn handle_error(self) -> impl Command + where + Self: Sized, + { + self + } +} + +/// A [`Command`] that consumes an iterator of [`Bundles`](Bundle) to spawn a series of entities. +/// +/// This is more efficient than spawning the entities individually. +#[track_caller] +pub fn spawn_batch(bundles_iter: I) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + I::Item: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + SpawnBatchIter::new( + world, + bundles_iter.into_iter(), + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will panic. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +pub fn insert_batch(batch: I, mode: InsertMode) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_batch_with_caller( + batch, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. +/// If any entities do not exist in the world, this command will ignore them. +/// +/// This is more efficient than inserting the bundles individually. +#[track_caller] +pub fn try_insert_batch(batch: I, mode: InsertMode) -> impl Command +where + I: IntoIterator + Send + Sync + 'static, + B: Bundle, +{ + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.try_insert_batch_with_caller( + batch, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that inserts a [`Resource`] into the world using a value +/// created with the [`FromWorld`] trait. +#[track_caller] +pub fn init_resource() -> impl Command { + move |world: &mut World| { + world.init_resource::(); + } +} + +/// A [`Command`] that inserts a [`Resource`] into the world. +#[track_caller] +pub fn insert_resource(resource: R) -> impl Command { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + world.insert_resource_with_caller( + resource, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// A [`Command`] that removes a [`Resource`] from the world. +pub fn remove_resource() -> impl Command { + move |world: &mut World| { + world.remove_resource::(); + } +} + +/// A [`Command`] that runs the system corresponding to the given [`SystemId`]. +pub fn run_system(id: SystemId<(), O>) -> impl Command { + move |world: &mut World| -> Result { + world.run_system(id)?; + Ok(()) + } +} + +/// A [`Command`] that runs the system corresponding to the given [`SystemId`] +/// and provides the given input value. +pub fn run_system_with(id: SystemId, input: I::Inner<'static>) -> impl Command +where + I: SystemInput: Send> + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_with(id, input)?; + Ok(()) + } +} + +/// A [`Command`] that runs the given system, +/// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. +pub fn run_system_cached(system: S) -> impl Command +where + M: 'static, + S: IntoSystem<(), (), M> + Send + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_cached(system)?; + Ok(()) + } +} + +/// A [`Command`] that runs the given system with the given input value, +/// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. +pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +where + I: SystemInput: Send> + Send + 'static, + M: 'static, + S: IntoSystem + Send + 'static, +{ + move |world: &mut World| -> Result { + world.run_system_cached_with(system, input)?; + Ok(()) + } +} + +/// A [`Command`] that removes a system previously registered with +/// [`Commands::register_system`](crate::system::Commands::register_system) or +/// [`World::register_system`]. +pub fn unregister_system(system_id: SystemId) -> impl Command +where + I: SystemInput + Send + 'static, + O: Send + 'static, +{ + move |world: &mut World| -> Result { + world.unregister_system(system_id)?; + Ok(()) + } +} + +/// A [`Command`] that removes a system previously registered with +/// [`World::register_system_cached`]. +pub fn unregister_system_cached(system: S) -> impl Command +where + I: SystemInput + Send + 'static, + O: 'static, + M: 'static, + S: IntoSystem + Send + 'static, +{ + move |world: &mut World| -> Result { + world.unregister_system_cached(system)?; + Ok(()) + } +} + +/// A [`Command`] that runs the schedule corresponding to the given [`ScheduleLabel`]. +pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { + move |world: &mut World| -> Result { + world.try_run_schedule(label)?; + Ok(()) + } +} + +/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +pub fn trigger(event: impl Event) -> impl Command { + move |world: &mut World| { + world.trigger(event); + } +} + +/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +pub fn trigger_targets( + event: impl Event, + targets: impl TriggerTargets + Send + Sync + 'static, +) -> impl Command { + move |world: &mut World| { + world.trigger_targets(event, targets); + } +} + +/// A [`Command`] that sends an arbitrary [`Event`]. +#[track_caller] +pub fn send_event(event: E) -> impl Command { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |world: &mut World| { + let mut events = world.resource_mut::>(); + events.send_with_caller( + event, + #[cfg(feature = "track_location")] + caller, + ); + } +} diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs new file mode 100644 index 0000000000000..c81f657084ec0 --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -0,0 +1,317 @@ +//! This module contains the definition of the [`EntityCommand`] trait, as well as +//! blanket implementations of the trait for closures. +//! +//! It also contains functions that return closures for use with +//! [`EntityCommands`](crate::system::EntityCommands). + +use alloc::vec::Vec; +use log::info; + +#[cfg(feature = "track_location")] +use core::panic::Location; + +use crate::{ + bundle::{Bundle, InsertMode}, + component::{Component, ComponentId, ComponentInfo}, + entity::{Entity, EntityCloneBuilder}, + event::Event, + result::Result, + system::{command::HandleError, Command, IntoObserverSystem}, + world::{error::EntityFetchError, EntityWorldMut, FromWorld, World}, +}; +use bevy_ptr::OwningPtr; + +/// A command which gets executed for a given [`Entity`]. +/// +/// Should be used with [`EntityCommands::queue`](crate::system::EntityCommands::queue). +/// +/// The `Out` generic parameter is the returned "output" of the command. +/// +/// # Examples +/// +/// ``` +/// # use std::collections::HashSet; +/// # use bevy_ecs::prelude::*; +/// use bevy_ecs::system::EntityCommand; +/// # +/// # #[derive(Component, PartialEq)] +/// # struct Name(String); +/// # impl Name { +/// # fn new(s: String) -> Self { Name(s) } +/// # fn as_str(&self) -> &str { &self.0 } +/// # } +/// +/// #[derive(Resource, Default)] +/// struct Counter(i64); +/// +/// /// A `Command` which names an entity based on a global counter. +/// fn count_name(mut entity: EntityWorldMut) { +/// // Get the current value of the counter, and increment it for next time. +/// let i = { +/// let mut counter = entity.resource_mut::(); +/// let i = counter.0; +/// counter.0 += 1; +/// i +/// }; +/// // Name the entity after the value of the counter. +/// entity.insert(Name::new(format!("Entity #{i}"))); +/// } +/// +/// // App creation boilerplate omitted... +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # +/// # let mut setup_schedule = Schedule::default(); +/// # setup_schedule.add_systems(setup); +/// # let mut assert_schedule = Schedule::default(); +/// # assert_schedule.add_systems(assert_names); +/// # +/// # setup_schedule.run(&mut world); +/// # assert_schedule.run(&mut world); +/// +/// fn setup(mut commands: Commands) { +/// commands.spawn_empty().queue(count_name); +/// commands.spawn_empty().queue(count_name); +/// } +/// +/// fn assert_names(named: Query<&Name>) { +/// // We use a HashSet because we do not care about the order. +/// let names: HashSet<_> = named.iter().map(Name::as_str).collect(); +/// assert_eq!(names, HashSet::from_iter(["Entity #0", "Entity #1"])); +/// } +/// ``` +pub trait EntityCommand: Send + 'static { + /// Executes this command for the given [`Entity`] and + /// returns a [`Result`] for error handling. + fn apply(self, entity: EntityWorldMut) -> Out; +} +/// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that +/// internally runs the [`EntityCommand`] on that entity. +/// +// NOTE: This is a separate trait from `EntityCommand` because "result-returning entity commands" and +// "non-result returning entity commands" require different implementations, so they cannot be automatically +// implemented. And this isn't the type of implementation that we want to thrust on people implementing +// EntityCommand. +pub trait CommandWithEntity { + /// Passes in a specific entity to an [`EntityCommand`], resulting in a [`Command`] that + /// internally runs the [`EntityCommand`] on that entity. + fn with_entity(self, entity: Entity) -> impl Command + HandleError; +} + +impl CommandWithEntity> for C { + fn with_entity( + self, + entity: Entity, + ) -> impl Command> + HandleError> + { + move |world: &mut World| -> Result<(), EntityFetchError> { + let entity = world.get_entity_mut(entity)?; + self.apply(entity); + Ok(()) + } + } +} + +impl< + C: EntityCommand>, + T, + Err: core::fmt::Debug + core::fmt::Display + Send + Sync + 'static, + > CommandWithEntity>> for C +{ + fn with_entity( + self, + entity: Entity, + ) -> impl Command>> + HandleError>> + { + move |world: &mut World| { + let entity = world.get_entity_mut(entity)?; + self.apply(entity) + .map_err(EntityCommandError::CommandFailed) + } + } +} + +/// An error that occurs when running an [`EntityCommand`] on a specific entity. +#[derive(thiserror::Error, Debug)] +pub enum EntityCommandError { + /// The entity this [`EntityCommand`] tried to run on could not be fetched. + #[error(transparent)] + EntityFetchError(#[from] EntityFetchError), + /// An error that occurred while running the [`EntityCommand`]. + #[error("{0}")] + CommandFailed(E), +} + +impl EntityCommand for F +where + F: FnOnce(EntityWorldMut) -> Out + Send + 'static, +{ + fn apply(self, entity: EntityWorldMut) -> Out { + self(entity) + } +} + +/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity, +/// replacing any that were already present. +#[track_caller] +pub fn insert(bundle: impl Bundle) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |mut entity: EntityWorldMut| { + entity.insert_with_caller( + bundle, + InsertMode::Replace, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity, +/// except for any that were already present. +#[track_caller] +pub fn insert_if_new(bundle: impl Bundle) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |mut entity: EntityWorldMut| { + entity.insert_with_caller( + bundle, + InsertMode::Keep, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that adds a dynamic component to an entity. +#[track_caller] +pub fn insert_by_id(component_id: ComponentId, value: T) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + // SAFETY: + // - `component_id` safety is ensured by the caller + // - `ptr` is valid within the `make` block + OwningPtr::make(value, |ptr| unsafe { + entity.insert_by_id(component_id, ptr); + }); + } +} + +/// An [`EntityCommand`] that adds a component to an entity using +/// the component's [`FromWorld`] implementation. +#[track_caller] +pub fn insert_from_world(mode: InsertMode) -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |mut entity: EntityWorldMut| { + let value = entity.world_scope(|world| T::from_world(world)); + entity.insert_with_caller( + value, + mode, + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity. +pub fn remove() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove::(); + } +} + +/// An [`EntityCommand`] that removes the components in a [`Bundle`] from an entity, +/// as well as the required components for each component removed. +pub fn remove_with_requires() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove_with_requires::(); + } +} + +/// An [`EntityCommand`] that removes a dynamic component from an entity. +pub fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.remove_by_id(component_id); + } +} + +/// An [`EntityCommand`] that removes all components from an entity. +pub fn clear() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clear(); + } +} + +/// An [`EntityCommand`] that removes all components from an entity, +/// except for those in the given [`Bundle`]. +pub fn retain() -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.retain::(); + } +} + +/// An [`EntityCommand`] that despawns an entity. +/// +/// # Note +/// +/// This won't clean up external references to the entity (such as parent-child relationships +/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. +pub fn despawn() -> impl EntityCommand { + #[cfg(feature = "track_location")] + let caller = Location::caller(); + move |entity: EntityWorldMut| { + entity.despawn_with_caller( + #[cfg(feature = "track_location")] + caller, + ); + } +} + +/// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) +/// listening for events of type `E` targeting an entity +pub fn observe( + observer: impl IntoObserverSystem, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.observe(observer); + } +} + +/// An [`EntityCommand`] that clones parts of an entity onto another entity, +/// configured through [`EntityCloneBuilder`]. +pub fn clone_with( + target: Entity, + config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_with(target, config); + } +} + +/// An [`EntityCommand`] that clones the specified components of an entity +/// and inserts them into another entity. +pub fn clone_components(target: Entity) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_components::(target); + } +} + +/// An [`EntityCommand`] that clones the specified components of an entity +/// and inserts them into another entity, then removes them from the original entity. +pub fn move_components(target: Entity) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.move_components::(target); + } +} + +/// An [`EntityCommand`] that logs the components of an entity. +pub fn log_components() -> impl EntityCommand { + move |entity: EntityWorldMut| { + let debug_infos: Vec<_> = entity + .world() + .inspect_entity(entity.id()) + .map(ComponentInfo::name) + .collect(); + info!("Entity {}: {debug_infos:?}", entity.id()); + } +} diff --git a/crates/bevy_ecs/src/system/commands/error_handler.rs b/crates/bevy_ecs/src/system/commands/error_handler.rs new file mode 100644 index 0000000000000..231df9ec7387e --- /dev/null +++ b/crates/bevy_ecs/src/system/commands/error_handler.rs @@ -0,0 +1,61 @@ +//! This module contains convenience functions that return simple error handlers +//! for use with [`Commands::queue_handled`](super::Commands::queue_handled) and [`EntityCommands::queue_handled`](super::EntityCommands::queue_handled). + +use crate::{result::Error, world::World}; +use log::{error, warn}; + +/// An error handler that does nothing. +pub fn silent() -> fn(&mut World, Error) { + |_, _| {} +} + +/// An error handler that accepts an error and logs it with [`warn!`]. +pub fn warn() -> fn(&mut World, Error) { + |_, error| warn!("{error}") +} + +/// An error handler that accepts an error and logs it with [`error!`]. +pub fn error() -> fn(&mut World, Error) { + |_, error| error!("{error}") +} + +/// An error handler that accepts an error and panics with the error in +/// the panic message. +pub fn panic() -> fn(&mut World, Error) { + |_, error| panic!("{error}") +} + +/// The default error handler. This defaults to [`panic()`]. If the +/// `configurable_error_handler` cargo feature is enabled, then +/// `GLOBAL_ERROR_HANDLER` will be used instead, enabling error handler customization. +#[cfg(not(feature = "configurable_error_handler"))] +#[inline] +pub fn default() -> fn(&mut World, Error) { + panic() +} + +/// 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. +/// +/// If the `configurable_error_handler` cargo feature is enabled, this will be used +/// by default. +/// +/// This should be set in the following way: +/// +/// ``` +/// # use bevy_ecs::system::error_handler::{GLOBAL_ERROR_HANDLER, warn}; +/// GLOBAL_ERROR_HANDLER.set(warn()); +/// // initialize Bevy App here +/// ``` +#[cfg(feature = "configurable_error_handler")] +pub static GLOBAL_ERROR_HANDLER: std::sync::OnceLock = + std::sync::OnceLock::new(); + +/// The default error handler. This defaults to [`panic()`]. If the +/// `configurable_error_handler` cargo feature is enabled, then +/// [`GLOBAL_ERROR_HANDLER`] will be used instead, enabling error handler customization. +#[cfg(feature = "configurable_error_handler")] +#[inline] +pub fn default() -> fn(&mut World, Error) { + *GLOBAL_ERROR_HANDLER.get_or_init(|| panic()) +} diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4fb880592ab80..52f0417ee2ec1 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1,33 +1,42 @@ +pub mod command; +pub mod entity_command; +pub mod error_handler; + #[cfg(feature = "std")] mod parallel_scope; -use alloc::vec::Vec; -use core::{marker::PhantomData, panic::Location}; +pub use command::Command; +pub use entity_command::EntityCommand; + +#[cfg(feature = "std")] +pub use parallel_scope::*; + +use alloc::boxed::Box; +use core::marker::PhantomData; +use log::error; + +#[cfg(feature = "track_location")] +use core::panic::Location; -use super::{ - Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith, - UnregisterSystem, UnregisterSystemCached, -}; use crate::{ self as bevy_ecs, bundle::{Bundle, InsertMode}, change_detection::Mut, - component::{Component, ComponentId, ComponentInfo, Mutable}, + component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityCloneBuilder}, - event::{Event, SendEvent}, - observer::{Observer, TriggerEvent, TriggerTargets}, + event::Event, + observer::{Observer, TriggerTargets}, + result::Error, schedule::ScheduleLabel, - system::{input::SystemInput, RunSystemWith, SystemId}, + system::{ + command::HandleError, entity_command::CommandWithEntity, input::SystemInput, Deferred, + IntoObserverSystem, IntoSystem, RegisteredSystem, Resource, SystemId, + }, world::{ - command_queue::RawCommandQueue, unsafe_world_cell::UnsafeWorldCell, Command, CommandQueue, - EntityWorldMut, FromWorld, SpawnBatchIter, World, + command_queue::RawCommandQueue, unsafe_world_cell::UnsafeWorldCell, CommandQueue, + EntityWorldMut, FromWorld, World, }, }; -use bevy_ptr::OwningPtr; -use log::{error, info}; - -#[cfg(feature = "std")] -pub use parallel_scope::*; /// A [`Command`] queue to perform structural changes to the [`World`]. /// @@ -80,6 +89,19 @@ pub use parallel_scope::*; /// # } /// ``` /// +/// # Error handling +/// +/// Commands can return a [`Result`](crate::result::Result), which can be passed to +/// an error handler. Error handlers are functions/closures of the form +/// `fn(&mut World, CommandError)`. +/// +/// The default error handler panics. It can be configured by enabling the `configurable_error_handler` +/// cargo feature, then setting the `GLOBAL_ERROR_HANDLER`. +/// +/// Alternatively, you can customize the error handler for a specific command by calling [`Commands::queue_handled`]. +/// +/// The [`error_handler`] module provides some simple error handlers for convenience. +/// /// [`ApplyDeferred`]: crate::schedule::ApplyDeferred pub struct Commands<'w, 's> { queue: InternalQueue<'s>, @@ -318,38 +340,6 @@ impl<'w, 's> Commands<'w, 's> { } } - /// Pushes a [`Command`] to the queue for creating a new [`Entity`] if the given one does not exists, - /// and returns its corresponding [`EntityCommands`]. - /// - /// This method silently fails by returning [`EntityCommands`] - /// even if the given `Entity` cannot be spawned. - /// - /// See [`World::get_or_spawn`] for more details. - /// - /// # Note - /// - /// Spawning a specific `entity` value is rarely the right choice. Most apps should favor - /// [`Commands::spawn`]. This method should generally only be used for sharing entities across - /// apps, and only when they have a scheme worked out to share an ID space (which doesn't happen - /// by default). - #[deprecated(since = "0.15.0", note = "use Commands::spawn instead")] - #[track_caller] - pub fn get_or_spawn(&mut self, entity: Entity) -> EntityCommands { - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - self.queue(move |world: &mut World| { - world.get_or_spawn_with_caller( - entity, - #[cfg(feature = "track_change_detection")] - caller, - ); - }); - EntityCommands { - entity, - commands: self.reborrow(), - } - } - /// Pushes a [`Command`] to the queue for creating a new entity with the given [`Bundle`]'s components, /// and returns its corresponding [`EntityCommands`]. /// @@ -412,6 +402,9 @@ impl<'w, 's> Commands<'w, 's> { /// Returns the [`EntityCommands`] for the requested [`Entity`]. /// + /// This method does not guarantee that commands queued by the `EntityCommands` + /// will be successful, since the entity could be despawned before they are executed. + /// /// # Panics /// /// This method panics if the requested entity does not exist. @@ -452,8 +445,8 @@ impl<'w, 's> Commands<'w, 's> { #[track_caller] fn panic_no_entity(entities: &Entities, entity: Entity) -> ! { panic!( - "Attempting to create an EntityCommands for entity {entity:?}, which {}", - entities.entity_does_not_exist_error_details_message(entity) + "Attempting to create an EntityCommands for entity {entity}, which {}", + entities.entity_does_not_exist_error_details(entity) ); } @@ -471,8 +464,8 @@ impl<'w, 's> Commands<'w, 's> { /// /// Returns `None` if the entity does not exist. /// - /// This method does not guarantee that `EntityCommands` will be successfully applied, - /// since another command in the queue may delete the entity before them. + /// This method does not guarantee that commands queued by the `EntityCommands` + /// will be successful, since the entity could be despawned before they are executed. /// /// # Example /// @@ -550,31 +543,87 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, I::Item: Bundle, { - self.queue(spawn_batch(bundles_iter)); + self.queue(command::spawn_batch(bundles_iter)); } /// Pushes a generic [`Command`] to the command queue. /// - /// `command` can be a built-in command, custom struct that implements [`Command`] or a closure - /// that takes [`&mut World`](World) as an argument. + /// If the [`Command`] returns a [`Result`], it will be handled using the [default error handler](error_handler::default). + /// + /// To use a custom error handler, see [`Commands::queue_handled`]. + /// + /// The command can be: + /// - A custom struct that implements [`Command`]. + /// - A closure or function that matches one of the following signatures: + /// - [`(&mut World)`](World) + /// - A built-in command from the [`command`] module. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Resource, Default)] + /// struct Counter(u64); + /// + /// struct AddToCounter(String); + /// + /// impl Command for AddToCounter { + /// fn apply(self, world: &mut World) -> Result { + /// let mut counter = world.get_resource_or_insert_with(Counter::default); + /// let amount: u64 = self.0.parse()?; + /// counter.0 += amount; + /// Ok(()) + /// } + /// } + /// + /// fn add_three_to_counter_system(mut commands: Commands) { + /// commands.queue(AddToCounter("3".to_string())); + /// } + /// fn add_twenty_five_to_counter_system(mut commands: Commands) { + /// commands.queue(|world: &mut World| { + /// let mut counter = world.get_resource_or_insert_with(Counter::default); + /// counter.0 += 25; + /// }); + /// } + /// # bevy_ecs::system::assert_is_system(add_three_to_counter_system); + /// # bevy_ecs::system::assert_is_system(add_twenty_five_to_counter_system); + /// ``` + pub fn queue + HandleError, T>(&mut self, command: C) { + self.queue_internal(command.handle_error()); + } + /// Pushes a generic [`Command`] to the command queue. If the command returns a [`Result`] the given + /// `error_handler` will be used to handle error cases. + /// + /// To implicitly use the default error handler, see [`Commands::queue`]. + /// + /// The command can be: + /// - A custom struct that implements [`Command`]. + /// - A closure or function that matches one of the following signatures: + /// - [`(&mut World)`](World) + /// - [`(&mut World)`](World) `->` [`Result`] + /// - A built-in command from the [`command`] module. + /// /// # Example /// /// ``` - /// # use bevy_ecs::{world::Command, prelude::*}; + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::system::error_handler; /// #[derive(Resource, Default)] /// struct Counter(u64); /// - /// struct AddToCounter(u64); + /// struct AddToCounter(String); /// - /// impl Command for AddToCounter { - /// fn apply(self, world: &mut World) { + /// impl Command for AddToCounter { + /// fn apply(self, world: &mut World) -> Result { /// let mut counter = world.get_resource_or_insert_with(Counter::default); - /// counter.0 += self.0; + /// let amount: u64 = self.0.parse()?; + /// counter.0 += amount; + /// Ok(()) /// } /// } /// /// fn add_three_to_counter_system(mut commands: Commands) { - /// commands.queue(AddToCounter(3)); + /// commands.queue_handled(AddToCounter("3".to_string()), error_handler::warn()); /// } /// fn add_twenty_five_to_counter_system(mut commands: Commands) { /// commands.queue(|world: &mut World| { @@ -585,7 +634,15 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(add_three_to_counter_system); /// # bevy_ecs::system::assert_is_system(add_twenty_five_to_counter_system); /// ``` - pub fn queue(&mut self, command: C) { + pub fn queue_handled + HandleError, T>( + &mut self, + command: C, + error_handler: fn(&mut World, Error), + ) { + self.queue_internal(command.handle_error_with(error_handler)); + } + + fn queue_internal(&mut self, command: impl Command) { match &mut self.queue { InternalQueue::CommandQueue(queue) => { queue.push(command); @@ -612,7 +669,7 @@ impl<'w, 's> Commands<'w, 's> { /// Then, the `Bundle` is added to the entity. /// /// This method is equivalent to iterating `bundles_iter`, - /// calling [`get_or_spawn`](Self::get_or_spawn) for each bundle, + /// calling [`spawn`](Self::spawn) for each bundle, /// and passing it to [`insert`](EntityCommands::insert), /// but it is faster due to memory pre-allocation. /// @@ -627,7 +684,21 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(insert_or_spawn_batch(bundles_iter)); + #[cfg(feature = "track_location")] + let caller = Location::caller(); + self.queue(move |world: &mut World| { + if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller( + bundles_iter, + #[cfg(feature = "track_location")] + caller, + ) { + error!( + "Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", + core::any::type_name::(), + invalid_entities + ); + } + }); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -654,7 +725,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(insert_batch(batch)); + self.queue(command::insert_batch(batch, InsertMode::Replace)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -681,7 +752,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(insert_batch_if_new(batch)); + self.queue(command::insert_batch(batch, InsertMode::Keep)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -706,7 +777,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(try_insert_batch(batch)); + self.queue(command::try_insert_batch(batch, InsertMode::Replace)); } /// Pushes a [`Command`] to the queue for adding a [`Bundle`] type to a batch of [`Entities`](Entity). @@ -731,7 +802,7 @@ impl<'w, 's> Commands<'w, 's> { I: IntoIterator + Send + Sync + 'static, B: Bundle, { - self.queue(try_insert_batch_if_new(batch)); + self.queue(command::try_insert_batch(batch, InsertMode::Keep)); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with an inferred value. @@ -760,7 +831,7 @@ impl<'w, 's> Commands<'w, 's> { /// ``` #[track_caller] pub fn init_resource(&mut self) { - self.queue(init_resource::); + self.queue(command::init_resource::()); } /// Pushes a [`Command`] to the queue for inserting a [`Resource`] in the [`World`] with a specific value. @@ -790,7 +861,7 @@ impl<'w, 's> Commands<'w, 's> { /// ``` #[track_caller] pub fn insert_resource(&mut self, resource: R) { - self.queue(insert_resource(resource)); + self.queue(command::insert_resource(resource)); } /// Pushes a [`Command`] to the queue for removing a [`Resource`] from the [`World`]. @@ -814,7 +885,7 @@ impl<'w, 's> Commands<'w, 's> { /// # bevy_ecs::system::assert_is_system(system); /// ``` pub fn remove_resource(&mut self) { - self.queue(remove_resource::); + self.queue(command::remove_resource::()); } /// Runs the system corresponding to the given [`SystemId`]. @@ -827,7 +898,7 @@ impl<'w, 's> Commands<'w, 's> { /// execution of the system happens later. To get the output of a system, use /// [`World::run_system`] or [`World::run_system_with`] instead of running the system as a command. pub fn run_system(&mut self, id: SystemId) { - self.run_system_with(id, ()); + self.queue(command::run_system(id).handle_error_with(error_handler::warn())); } /// Runs the system corresponding to the given [`SystemId`]. @@ -843,7 +914,7 @@ impl<'w, 's> Commands<'w, 's> { where I: SystemInput: Send> + 'static, { - self.queue(RunSystemWith::new_with_input(id, input)); + self.queue(command::run_system_with(id, input).handle_error_with(error_handler::warn())); } /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`]. @@ -904,7 +975,8 @@ impl<'w, 's> Commands<'w, 's> { O: Send + 'static, { let entity = self.spawn_empty().id(); - self.queue(RegisterSystem::new(system, entity)); + let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); + self.entity(entity).insert(system); SystemId::from_entity(entity) } @@ -916,7 +988,7 @@ impl<'w, 's> Commands<'w, 's> { I: SystemInput + Send + 'static, O: Send + 'static, { - self.queue(UnregisterSystem::new(system_id)); + self.queue(command::unregister_system(system_id).handle_error_with(error_handler::warn())); } /// Removes a system previously registered with [`World::register_system_cached`]. @@ -931,7 +1003,9 @@ impl<'w, 's> Commands<'w, 's> { &mut self, system: S, ) { - self.queue(UnregisterSystemCached::new(system)); + self.queue( + command::unregister_system_cached(system).handle_error_with(error_handler::warn()), + ); } /// Similar to [`Self::run_system`], but caching the [`SystemId`] in a @@ -942,7 +1016,7 @@ impl<'w, 's> Commands<'w, 's> { &mut self, system: S, ) { - self.run_system_cached_with(system, ()); + self.queue(command::run_system_cached(system).handle_error_with(error_handler::warn())); } /// Similar to [`Self::run_system_with`], but caching the [`SystemId`] in a @@ -955,7 +1029,9 @@ impl<'w, 's> Commands<'w, 's> { M: 'static, S: IntoSystem + Send + 'static, { - self.queue(RunSystemCachedWith::new(system, input)); + self.queue( + command::run_system_cached_with(system, input).handle_error_with(error_handler::warn()), + ); } /// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that @@ -963,7 +1039,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// [`Trigger`]: crate::observer::Trigger pub fn trigger(&mut self, event: impl Event) { - self.queue(TriggerEvent { event, targets: () }); + self.queue(command::trigger(event)); } /// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that @@ -975,7 +1051,7 @@ impl<'w, 's> Commands<'w, 's> { event: impl Event, targets: impl TriggerTargets + Send + Sync + 'static, ) { - self.queue(TriggerEvent { event, targets }); + self.queue(command::trigger_targets(event, targets)); } /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated @@ -1003,11 +1079,7 @@ impl<'w, 's> Commands<'w, 's> { /// [`EventWriter`]: crate::event::EventWriter #[track_caller] pub fn send_event(&mut self, event: E) -> &mut Self { - self.queue(SendEvent { - event, - #[cfg(feature = "track_change_detection")] - caller: Location::caller(), - }); + self.queue(command::send_event(event)); self } @@ -1051,87 +1123,38 @@ impl<'w, 's> Commands<'w, 's> { /// # assert_eq!(world.resource::().0, 1); /// ``` pub fn run_schedule(&mut self, label: impl ScheduleLabel) { - self.queue(|world: &mut World| { - if let Err(error) = world.try_run_schedule(label) { - panic!("Failed to run schedule: {error}"); - } - }); + self.queue(command::run_schedule(label).handle_error_with(error_handler::warn())); } } -/// A [`Command`] which gets executed for a given [`Entity`]. +/// A list of commands that will be run to modify an [`Entity`]. /// -/// # Examples +/// # Note /// -/// ``` -/// # use std::collections::HashSet; -/// # use bevy_ecs::prelude::*; -/// use bevy_ecs::system::EntityCommand; -/// # -/// # #[derive(Component, PartialEq)] -/// # struct Name(String); -/// # impl Name { -/// # fn new(s: String) -> Self { Name(s) } -/// # fn as_str(&self) -> &str { &self.0 } -/// # } +/// Most [`Commands`] (and thereby [`EntityCommands`]) are deferred: when you call the command, +/// if it requires mutable access to the [`World`] (that is, if it removes, adds, or changes something), +/// it's not executed immediately. Instead, the command is added to a "command queue." +/// The command queue is applied between [`Schedules`](bevy_ecs::schedule::Schedule), one by one, +/// so that each command can have exclusive access to the World. /// -/// #[derive(Resource, Default)] -/// struct Counter(i64); +/// # Fallible /// -/// /// A `Command` which names an entity based on a global counter. -/// fn count_name(entity: Entity, world: &mut World) { -/// // Get the current value of the counter, and increment it for next time. -/// let mut counter = world.resource_mut::(); -/// let i = counter.0; -/// counter.0 += 1; +/// Due to their deferred nature, an entity you're trying to change with an [`EntityCommand`] can be +/// despawned by the time the command is executed. All deferred entity commands will check if the +/// entity exists at the time of execution and will return an error if it doesn't. /// -/// // Name the entity after the value of the counter. -/// world.entity_mut(entity).insert(Name::new(format!("Entity #{i}"))); -/// } +/// # Error handling /// -/// // App creation boilerplate omitted... -/// # let mut world = World::new(); -/// # world.init_resource::(); -/// # -/// # let mut setup_schedule = Schedule::default(); -/// # setup_schedule.add_systems(setup); -/// # let mut assert_schedule = Schedule::default(); -/// # assert_schedule.add_systems(assert_names); -/// # -/// # setup_schedule.run(&mut world); -/// # assert_schedule.run(&mut world); +/// [`EntityCommands`] can return a [`Result`](crate::result::Result), which can be passed to +/// an error handler. Error handlers are functions/closures of the form +/// `fn(&mut World, CommandError)`. /// -/// fn setup(mut commands: Commands) { -/// commands.spawn_empty().queue(count_name); -/// commands.spawn_empty().queue(count_name); -/// } +/// The default error handler panics. It can be configured by enabling the `configurable_error_handler` +/// cargo feature, then setting the `GLOBAL_ERROR_HANDLER`. /// -/// fn assert_names(named: Query<&Name>) { -/// // We use a HashSet because we do not care about the order. -/// let names: HashSet<_> = named.iter().map(Name::as_str).collect(); -/// assert_eq!(names, HashSet::from_iter(["Entity #0", "Entity #1"])); -/// } -/// ``` -pub trait EntityCommand: Send + 'static { - /// Executes this command for the given [`Entity`]. - fn apply(self, entity: Entity, world: &mut World); - - /// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`]. - /// - /// This method is called when adding an [`EntityCommand`] to a command queue via [`Commands`]. - /// You can override the provided implementation if you can return a `Command` with a smaller memory - /// footprint than `(Entity, Self)`. - /// In most cases the provided implementation is sufficient. - #[must_use = "commands do nothing unless applied to a `World`"] - fn with_entity(self, entity: Entity) -> impl Command - where - Self: Sized, - { - move |world: &mut World| self.apply(entity, world) - } -} - -/// A list of commands that will be run to modify an [entity](crate::entity). +/// Alternatively, you can customize the error handler for a specific command by calling [`EntityCommands::queue_handled`]. +/// +/// The [`error_handler`] module provides some simple error handlers for convenience. pub struct EntityCommands<'a> { pub(crate) entity: Entity, pub(crate) commands: Commands<'a, 'a>, @@ -1251,7 +1274,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(insert(bundle, InsertMode::Replace)) + self.queue(entity_command::insert(bundle)) } /// Similar to [`Self::insert`] but will only insert if the predicate returns true. @@ -1289,7 +1312,7 @@ impl<'a> EntityCommands<'a> { F: FnOnce() -> bool, { if condition() { - self.queue(insert(bundle, InsertMode::Replace)) + self.insert(bundle) } else { self } @@ -1309,8 +1332,9 @@ impl<'a> EntityCommands<'a> { /// The command will panic when applied if the associated entity does not exist. /// /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] instead. + #[track_caller] pub fn insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(insert(bundle, InsertMode::Keep)) + self.queue(entity_command::insert_if_new(bundle)) } /// Adds a [`Bundle`] of components to the entity without overwriting if the @@ -1327,6 +1351,7 @@ impl<'a> EntityCommands<'a> { /// /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] /// instead. + #[track_caller] pub fn insert_if_new_and(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, @@ -1358,11 +1383,7 @@ impl<'a> EntityCommands<'a> { component_id: ComponentId, value: T, ) -> &mut Self { - let caller = Location::caller(); - // SAFETY: same invariants as parent call - self.queue(unsafe {insert_by_id(component_id, value, move |world, entity| { - panic!("error[B0003]: {caller}: Could not insert a component {component_id:?} (with type {}) for entity {entity:?}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity)); - })}) + self.queue(entity_command::insert_by_id(component_id, value)) } /// Attempts to add a dynamic component to an entity. @@ -1373,13 +1394,16 @@ impl<'a> EntityCommands<'a> { /// /// - [`ComponentId`] must be from the same world as `self`. /// - `T` must have the same layout as the one passed during `component_id` creation. + #[track_caller] pub unsafe fn try_insert_by_id( &mut self, component_id: ComponentId, value: T, ) -> &mut Self { - // SAFETY: same invariants as parent call - self.queue(unsafe { insert_by_id(component_id, value, |_, _| {}) }) + self.queue_handled( + entity_command::insert_by_id(component_id, value), + error_handler::silent(), + ) } /// Tries to add a [`Bundle`] of components to the entity. @@ -1432,7 +1456,7 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(try_insert(bundle, InsertMode::Replace)) + self.queue_handled(entity_command::insert(bundle), error_handler::silent()) } /// Similar to [`Self::try_insert`] but will only try to insert if the predicate returns true. @@ -1467,7 +1491,7 @@ impl<'a> EntityCommands<'a> { F: FnOnce() -> bool, { if condition() { - self.queue(try_insert(bundle, InsertMode::Replace)) + self.try_insert(bundle) } else { self } @@ -1508,6 +1532,7 @@ impl<'a> EntityCommands<'a> { /// } /// # bevy_ecs::system::assert_is_system(add_health_system); /// ``` + #[track_caller] pub fn try_insert_if_new_and(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, @@ -1528,8 +1553,12 @@ impl<'a> EntityCommands<'a> { /// # Note /// /// Unlike [`Self::insert_if_new`], this will not panic if the associated entity does not exist. + #[track_caller] pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { - self.queue(try_insert(bundle, InsertMode::Keep)) + self.queue_handled( + entity_command::insert_if_new(bundle), + error_handler::silent(), + ) } /// Removes a [`Bundle`] of components from the entity. @@ -1571,7 +1600,53 @@ impl<'a> EntityCommands<'a> { where T: Bundle, { - self.queue(remove::) + self.queue_handled(entity_command::remove::(), error_handler::warn()) + } + + /// Removes a [`Bundle`] of components from the entity. + /// + /// # Note + /// + /// Unlike [`Self::remove`], this will not panic if the associated entity does not exist. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Resource)] + /// # struct PlayerEntity { entity: Entity } + /// #[derive(Component)] + /// struct Health(u32); + /// #[derive(Component)] + /// struct Strength(u32); + /// #[derive(Component)] + /// struct Defense(u32); + /// + /// #[derive(Bundle)] + /// struct CombatBundle { + /// health: Health, + /// strength: Strength, + /// } + /// + /// fn remove_combat_stats_system(mut commands: Commands, player: Res) { + /// commands + /// .entity(player.entity) + /// // You can remove individual components: + /// .try_remove::() + /// // You can also remove pre-defined Bundles of components: + /// .try_remove::() + /// // You can also remove tuples of components and bundles. + /// // This is equivalent to the calls above: + /// .try_remove::<(Defense, CombatBundle)>(); + /// } + /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); + /// ``` + pub fn try_remove(&mut self) -> &mut Self + where + T: Bundle, + { + self.queue_handled(entity_command::remove::(), error_handler::silent()) } /// Removes all components in the [`Bundle`] components and remove all required components for each component in the [`Bundle`] from entity. @@ -1599,20 +1674,25 @@ impl<'a> EntityCommands<'a> { /// # bevy_ecs::system::assert_is_system(remove_with_requires_system); /// ``` pub fn remove_with_requires(&mut self) -> &mut Self { - self.queue(remove_with_requires::) + self.queue(entity_command::remove_with_requires::()) } - /// Removes a component from the entity. + /// Removes a dynamic [`Component`] from the entity if it exists. + /// + /// # Panics + /// + /// Panics if the provided [`ComponentId`] does not exist in the [`World`]. pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { - self.queue(remove_by_id(component_id)) + self.queue(entity_command::remove_by_id(component_id)) } /// Removes all components associated with the entity. pub fn clear(&mut self) -> &mut Self { - self.queue(clear()) + self.queue(entity_command::clear()) } /// Despawns the entity. + /// /// This will emit a warning if the entity does not exist. /// /// See [`World::despawn`] for more details. @@ -1641,19 +1721,30 @@ impl<'a> EntityCommands<'a> { /// ``` #[track_caller] pub fn despawn(&mut self) { - self.queue(despawn()); + self.queue_handled(entity_command::despawn(), error_handler::warn()); } /// Despawns the entity. + /// /// This will not emit a warning if the entity does not exist, essentially performing /// the same function as [`Self::despawn`] without emitting warnings. - #[track_caller] pub fn try_despawn(&mut self) { - self.queue(try_despawn()); + self.queue_handled(entity_command::despawn(), error_handler::silent()); } /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. /// + /// If the [`EntityCommand`] returns a [`Result`], it will be handled using the [default error handler](error_handler::default). + /// + /// To use a custom error handler, see [`EntityCommands::queue_handled`]. + /// + /// The command can be: + /// - A custom struct that implements [`EntityCommand`]. + /// - A closure or function that matches the following signature: + /// - [`(EntityWorldMut)`](EntityWorldMut) + /// - [`(EntityWorldMut)`](EntityWorldMut) `->` [`Result`] + /// - A built-in command from the [`entity_command`] module. + /// /// # Examples /// /// ``` @@ -1663,16 +1754,61 @@ impl<'a> EntityCommands<'a> { /// .spawn_empty() /// // Closures with this signature implement `EntityCommand`. /// .queue(|entity: EntityWorldMut| { - /// println!("Executed an EntityCommand for {:?}", entity.id()); + /// println!("Executed an EntityCommand for {}", entity.id()); /// }); /// # } /// # bevy_ecs::system::assert_is_system(my_system); /// ``` - pub fn queue(&mut self, command: impl EntityCommand) -> &mut Self { + pub fn queue + CommandWithEntity, T, M>( + &mut self, + command: C, + ) -> &mut Self { self.commands.queue(command.with_entity(self.entity)); self } + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. + /// If the command returns a [`Result`] the given `error_handler` will be used to handle error cases. + /// + /// To implicitly use the default error handler, see [`EntityCommands::queue`]. + /// + /// The command can be: + /// - A custom struct that implements [`EntityCommand`]. + /// - A closure or function that matches the following signature: + /// - [`(EntityWorldMut)`](EntityWorldMut) + /// - [`(EntityWorldMut)`](EntityWorldMut) `->` [`Result`] + /// - A built-in command from the [`entity_command`] module. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::system::error_handler; + /// # fn my_system(mut commands: Commands) { + /// commands + /// .spawn_empty() + /// // Closures with this signature implement `EntityCommand`. + /// .queue_handled( + /// |entity: EntityWorldMut| -> Result { + /// let value: usize = "100".parse()?; + /// println!("Successfully parsed the value {} for entity {}", value, entity.id()); + /// Ok(()) + /// }, + /// error_handler::warn() + /// ); + /// # } + /// # bevy_ecs::system::assert_is_system(my_system); + /// ``` + pub fn queue_handled + CommandWithEntity, T, M>( + &mut self, + command: C, + error_handler: fn(&mut World, Error), + ) -> &mut Self { + self.commands + .queue_handled(command.with_entity(self.entity), error_handler); + self + } + /// Removes all components except the given [`Bundle`] from the entity. /// /// This can also be used to remove all the components from the entity by passing it an empty Bundle. @@ -1714,7 +1850,7 @@ impl<'a> EntityCommands<'a> { where T: Bundle, { - self.queue(retain::) + self.queue(entity_command::retain::()) } /// Logs the components of the entity at the info level. @@ -1723,7 +1859,7 @@ impl<'a> EntityCommands<'a> { /// /// The command will panic when applied if the associated entity does not exist. pub fn log_components(&mut self) -> &mut Self { - self.queue(log_components) + self.queue(entity_command::log_components()) } /// Returns the underlying [`Commands`]. @@ -1745,12 +1881,12 @@ impl<'a> EntityCommands<'a> { self } - /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity. + /// Creates an [`Observer`] listening for events of type `E` targeting this entity. pub fn observe( &mut self, - system: impl IntoObserverSystem, + observer: impl IntoObserverSystem, ) -> &mut Self { - self.queue(observe(system)) + self.queue(entity_command::observe(observer)) } /// Clones parts of an entity (components, observers, etc.) onto another entity, @@ -1759,6 +1895,12 @@ impl<'a> EntityCommands<'a> { /// By default, the other entity will receive all the components of the original that implement /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). /// + /// # Panics + /// + /// The command will panic when applied if the target entity does not exist. + /// + /// # Example + /// /// Configure through [`EntityCloneBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; @@ -1787,16 +1929,12 @@ impl<'a> EntityCommands<'a> { /// - [`EntityCloneBuilder`] /// - [`CloneEntityWithObserversExt`](crate::observer::CloneEntityWithObserversExt) /// - `CloneEntityHierarchyExt` - /// - /// # Panics - /// - /// The command will panic when applied if either of the entities do not exist. pub fn clone_with( &mut self, target: Entity, config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, ) -> &mut Self { - self.queue(clone_with(target, config)) + self.queue(entity_command::clone_with(target, config)) } /// Spawns a clone of this entity and returns the [`EntityCommands`] of the clone. @@ -1807,9 +1945,10 @@ impl<'a> EntityCommands<'a> { /// To configure cloning behavior (such as only cloning certain components), /// use [`EntityCommands::clone_and_spawn_with`]. /// - /// # Panics + /// # Note /// - /// The command will panic when applied if the original entity does not exist. + /// If the original entity does not exist when this command is applied, + /// the returned entity will have no components. /// /// # Example /// @@ -1845,9 +1984,10 @@ impl<'a> EntityCommands<'a> { /// /// See the methods on [`EntityCloneBuilder`] for more options. /// - /// # Panics + /// # Note /// - /// The command will panic when applied if the original entity does not exist. + /// If the original entity does not exist when this command is applied, + /// the returned entity will have no components. /// /// # Example /// @@ -1874,7 +2014,7 @@ impl<'a> EntityCommands<'a> { config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, ) -> EntityCommands<'_> { let entity_clone = self.commands().spawn_empty().id(); - self.queue(clone_with(entity_clone, config)); + self.clone_with(entity_clone, config); EntityCommands { commands: self.commands_mut().reborrow(), entity: entity_clone, @@ -1888,9 +2028,9 @@ impl<'a> EntityCommands<'a> { /// /// # Panics /// - /// The command will panic when applied if either of the entities do not exist. + /// The command will panic when applied if the target entity does not exist. pub fn clone_components(&mut self, target: Entity) -> &mut Self { - self.queue(clone_components::(target)) + self.queue(entity_command::clone_components::(target)) } /// Clones the specified components of this entity and inserts them into another entity, @@ -1901,9 +2041,9 @@ impl<'a> EntityCommands<'a> { /// /// # Panics /// - /// The command will panic when applied if either of the entities do not exist. + /// The command will panic when applied if the target entity does not exist. pub fn move_components(&mut self, target: Entity) -> &mut Self { - self.queue(move_components::(target)) + self.queue(entity_command::move_components::(target)) } } @@ -1937,8 +2077,7 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// See [`or_try_insert`](Self::or_try_insert) for a non-panicking version. #[track_caller] pub fn or_insert(&mut self, default: T) -> &mut Self { - self.entity_commands - .queue(insert(default, InsertMode::Keep)); + self.entity_commands.insert_if_new(default); self } @@ -1949,8 +2088,7 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// See also [`or_insert_with`](Self::or_insert_with). #[track_caller] pub fn or_try_insert(&mut self, default: T) -> &mut Self { - self.entity_commands - .queue(try_insert(default, InsertMode::Keep)); + self.entity_commands.try_insert_if_new(default); self } @@ -1989,8 +2127,6 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { where T: Default, { - #[allow(clippy::unwrap_or_default)] - // FIXME: use `expect` once stable self.or_insert(T::default()) } @@ -2007,411 +2143,12 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { T: FromWorld, { self.entity_commands - .queue(insert_from_world::(InsertMode::Keep)); + .queue(entity_command::insert_from_world::(InsertMode::Keep)); self } } -impl Command for F -where - F: FnOnce(&mut World) + Send + 'static, -{ - fn apply(self, world: &mut World) { - self(world); - } -} - -impl EntityCommand for F -where - F: FnOnce(EntityWorldMut) + Send + 'static, -{ - fn apply(self, id: Entity, world: &mut World) { - self(world.entity_mut(id)); - } -} - -impl EntityCommand for F -where - F: FnOnce(Entity, &mut World) + Send + 'static, -{ - fn apply(self, id: Entity, world: &mut World) { - self(id, world); - } -} - -/// A [`Command`] that consumes an iterator of [`Bundle`]s to spawn a series of entities. -/// -/// This is more efficient than spawning the entities individually. -#[track_caller] -fn spawn_batch(bundles_iter: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - SpawnBatchIter::new( - world, - bundles_iter.into_iter(), - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundle`]s to a set of entities. -/// If any entities do not already exist in the world, they will be spawned. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn insert_or_spawn_batch(bundles_iter: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - if let Err(invalid_entities) = world.insert_or_spawn_batch_with_caller( - bundles_iter, - #[cfg(feature = "track_change_detection")] - caller, - ) { - error!( - "Failed to 'insert or spawn' bundle of type {} into the following invalid entities: {:?}", - core::any::type_name::(), - invalid_entities - ); - } - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will panic. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn insert_batch(batch: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - world.insert_batch_with_caller( - batch, - InsertMode::Replace, - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will panic. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn insert_batch_if_new(batch: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - world.insert_batch_with_caller( - batch, - InsertMode::Keep, - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will ignore them. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn try_insert_batch(batch: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - world.try_insert_batch_with_caller( - batch, - InsertMode::Replace, - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// A [`Command`] that consumes an iterator to add a series of [`Bundles`](Bundle) to a set of entities. -/// If any entities do not exist in the world, this command will ignore them. -/// -/// This is more efficient than inserting the bundles individually. -#[track_caller] -fn try_insert_batch_if_new(batch: I) -> impl Command -where - I: IntoIterator + Send + Sync + 'static, - B: Bundle, -{ - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - world.try_insert_batch_with_caller( - batch, - InsertMode::Keep, - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// A [`Command`] that despawns a specific entity. -/// This will emit a warning if the entity does not exist. -/// -/// # Note -/// -/// This won't clean up external references to the entity (such as parent-child relationships -/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. -#[track_caller] -fn despawn() -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - world.despawn_with_caller(entity, caller, true); - } -} - -/// A [`Command`] that despawns a specific entity. -/// This will not emit a warning if the entity does not exist. -/// -/// # Note -/// -/// This won't clean up external references to the entity (such as parent-child relationships -/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. -#[track_caller] -fn try_despawn() -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - world.despawn_with_caller(entity, caller, false); - } -} - -/// An [`EntityCommand`] that adds the components in a [`Bundle`] to an entity. -#[track_caller] -fn insert(bundle: T, mode: InsertMode) -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - bundle, - mode, - #[cfg(feature = "track_change_detection")] - caller, - ); - } else { - panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for entity {entity:?}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity)); - } - } -} - -/// An [`EntityCommand`] that adds the component using its `FromWorld` implementation. -#[track_caller] -fn insert_from_world(mode: InsertMode) -> impl EntityCommand { - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - let value = T::from_world(world); - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - value, - mode, - #[cfg(feature = "track_change_detection")] - caller, - ); - } else { - panic!("error[B0003]: {caller}: Could not insert a bundle (of type `{}`) for {entity:?}, which {}. See: https://bevyengine.org/learn/errors/b0003", core::any::type_name::(), world.entities().entity_does_not_exist_error_details_message(entity) ); - } - } -} - -/// An [`EntityCommand`] that attempts to add the components in a [`Bundle`] to an entity. -/// Does nothing if the entity does not exist. -#[track_caller] -fn try_insert(bundle: impl Bundle, mode: InsertMode) -> impl EntityCommand { - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.insert_with_caller( - bundle, - mode, - #[cfg(feature = "track_change_detection")] - caller, - ); - } - } -} - -/// An [`EntityCommand`] that attempts to add the dynamic component to an entity. -/// -/// # Safety -/// -/// - The returned `EntityCommand` must be queued for the world where `component_id` was created. -/// - `T` must be the type represented by `component_id`. -unsafe fn insert_by_id( - component_id: ComponentId, - value: T, - on_none_entity: impl FnOnce(&mut World, Entity) + Send + 'static, -) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - // SAFETY: - // - `component_id` safety is ensured by the caller - // - `ptr` is valid within the `make` block; - OwningPtr::make(value, |ptr| unsafe { - entity.insert_by_id(component_id, ptr); - }); - } else { - on_none_entity(world, entity); - } - } -} - -/// An [`EntityCommand`] that removes components from an entity. -/// -/// For a [`Bundle`] type `T`, this will remove any components in the bundle. -/// Any components in the bundle that aren't found on the entity will be ignored. -fn remove(entity: Entity, world: &mut World) { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove::(); - } -} - -/// An [`EntityCommand`] that removes components with a provided [`ComponentId`] from an entity. -/// # Panics -/// -/// Panics if the provided [`ComponentId`] does not exist in the [`World`]. -fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_by_id(component_id); - } - } -} - -/// An [`EntityCommand`] that remove all components in the bundle and remove all required components for each component in the bundle. -fn remove_with_requires(entity: Entity, world: &mut World) { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.remove_with_requires::(); - } -} - -/// An [`EntityCommand`] that removes all components associated with a provided entity. -fn clear() -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clear(); - } - } -} - -/// An [`EntityCommand`] that removes components from an entity. -/// -/// For a [`Bundle`] type `T`, this will remove all components except those in the bundle. -/// Any components in the bundle that aren't found on the entity will be ignored. -fn retain(entity: Entity, world: &mut World) { - if let Ok(mut entity_mut) = world.get_entity_mut(entity) { - entity_mut.retain::(); - } -} - -/// A [`Command`] that inserts a [`Resource`] into the world using a value -/// created with the [`FromWorld`] trait. -#[track_caller] -fn init_resource(world: &mut World) { - world.init_resource::(); -} - -/// A [`Command`] that removes the [resource](Resource) `R` from the world. -#[track_caller] -fn remove_resource(world: &mut World) { - world.remove_resource::(); -} - -/// A [`Command`] that inserts a [`Resource`] into the world. -#[track_caller] -fn insert_resource(resource: R) -> impl Command { - #[cfg(feature = "track_change_detection")] - let caller = Location::caller(); - move |world: &mut World| { - world.insert_resource_with_caller( - resource, - #[cfg(feature = "track_change_detection")] - caller, - ); - } -} - -/// [`EntityCommand`] to log the components of a given entity. See [`EntityCommands::log_components`]. -fn log_components(entity: Entity, world: &mut World) { - let debug_infos: Vec<_> = world - .inspect_entity(entity) - .map(ComponentInfo::name) - .collect(); - info!("Entity {entity}: {debug_infos:?}"); -} - -fn observe( - observer: impl IntoObserverSystem, -) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.observe(observer); - } - } -} - -/// An [`EntityCommand`] that clones an entity with configurable cloning behavior. -fn clone_with( - target: Entity, - config: impl FnOnce(&mut EntityCloneBuilder) + Send + Sync + 'static, -) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clone_with(target, config); - } - } -} - -/// An [`EntityCommand`] that clones the specified components into another entity. -fn clone_components(target: Entity) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.clone_components::(target); - } - } -} - -/// An [`EntityCommand`] that clones the specified components into another entity -/// and removes them from the original entity. -fn move_components(target: Entity) -> impl EntityCommand { - move |entity: Entity, world: &mut World| { - if let Ok(mut entity) = world.get_entity_mut(entity) { - entity.move_components::(target); - } - } -} - #[cfg(test)] -#[allow(clippy::float_cmp, clippy::approx_constant)] mod tests { use crate::{ self as bevy_ecs, @@ -2419,13 +2156,16 @@ mod tests { system::{Commands, Resource}, world::{CommandQueue, FromWorld, World}, }; - use alloc::sync::Arc; + use alloc::{string::String, sync::Arc, vec, vec::Vec}; use core::{ any::TypeId, sync::atomic::{AtomicUsize, Ordering}, }; - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to test how `Drop` behavior works in regards to SparseSet storage, and as such is solely a wrapper around `DropCk` to make it use the SparseSet storage. Because of this, the inner field is intentionally never read." + )] #[derive(Component)] #[component(storage = "SparseSet")] struct SparseDropCk(DropCk); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 99f3d1d0299af..2b1c081d51db0 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -219,7 +219,14 @@ pub struct HasExclusiveSystemInput; macro_rules! impl_exclusive_system_function { ($($param: ident),*) => { - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is within a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] impl ExclusiveSystemParamFunction Out> for Func where Func: Send + Sync + 'static, @@ -236,7 +243,6 @@ macro_rules! impl_exclusive_system_function { // Yes, this is strange, but `rustc` fails to compile this impl // without using this function. It fails to recognize that `func` // is a function, potentially because of the multiple impls of `FnMut` - #[allow(clippy::too_many_arguments)] fn call_inner( mut f: impl FnMut(&mut World, $($param,)*) -> Out, world: &mut World, @@ -249,7 +255,14 @@ macro_rules! impl_exclusive_system_function { } } - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is within a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] impl ExclusiveSystemParamFunction<(HasExclusiveSystemInput, fn(In, $($param,)*) -> Out)> for Func where Func: Send + Sync + 'static, @@ -267,7 +280,6 @@ macro_rules! impl_exclusive_system_function { // Yes, this is strange, but `rustc` fails to compile this impl // without using this function. It fails to recognize that `func` // is a function, potentially because of the multiple impls of `FnMut` - #[allow(clippy::too_many_arguments)] fn call_inner( mut f: impl FnMut(In::Param<'_>, &mut World, $($param,)*) -> Out, input: In::Inner<'_>, diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index cc24cb7904304..e36b7ea759bdd 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -88,26 +88,38 @@ impl ExclusiveSystemParam for PhantomData { macro_rules! impl_exclusive_system_param_tuple { ($(#[$meta:meta])* $($param: ident),*) => { - #[allow(unused_variables)] - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is within a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use any of the parameters." + )] $(#[$meta])* impl<$($param: ExclusiveSystemParam),*> ExclusiveSystemParam for ($($param,)*) { type State = ($($param::State,)*); type Item<'s> = ($($param::Item<'s>,)*); #[inline] - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { - (($($param::init(_world, _system_meta),)*)) + fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + (($($param::init(world, system_meta),)*)) } #[inline] - #[allow(clippy::unused_unit)] fn get_param<'s>( state: &'s mut Self::State, system_meta: &SystemMeta, ) -> Self::Item<'s> { - let ($($param,)*) = state; + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't have any params to get." + )] ($($param::get_param($param, system_meta),)*) } } @@ -126,6 +138,7 @@ all_tuples!( mod tests { use crate as bevy_ecs; use crate::{schedule::Schedule, system::Local, world::World}; + use alloc::vec::Vec; use bevy_ecs_macros::Resource; use core::marker::PhantomData; diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 3d029d74a9577..593d94e5a0917 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -18,7 +18,7 @@ use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{In, IntoSystem, ReadOnlySystem, SystemParamBuilder}; +use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder}; /// The metadata of a [`System`]. #[derive(Clone)] @@ -60,7 +60,7 @@ impl SystemMeta { is_send: true, has_deferred: false, last_run: Tick::new(0), - param_warn_policy: ParamWarnPolicy::Once, + param_warn_policy: ParamWarnPolicy::Panic, #[cfg(feature = "trace")] system_span: info_span!("system", name = name), #[cfg(feature = "trace")] @@ -190,16 +190,19 @@ impl SystemMeta { /// State machine for emitting warnings when [system params are invalid](System::validate_param). #[derive(Clone, Copy)] pub enum ParamWarnPolicy { + /// Stop app with a panic. + Panic, /// No warning should ever be emitted. Never, /// The warning will be emitted once and status will update to [`Self::Never`]. - Once, + Warn, } impl ParamWarnPolicy { /// Advances the warn policy after validation failed. #[inline] fn advance(&mut self) { + // Ignore `Panic` case, because it stops execution before this function gets called. *self = Self::Never; } @@ -209,15 +212,21 @@ impl ParamWarnPolicy { where P: SystemParam, { - if matches!(self, Self::Never) { - return; + match self { + Self::Panic => panic!( + "{0} could not access system parameter {1}", + name, + disqualified::ShortName::of::

() + ), + Self::Warn => { + log::warn!( + "{0} did not run because it requested inaccessible system parameter {1}", + name, + disqualified::ShortName::of::

() + ); + } + Self::Never => {} } - - log::warn!( - "{0} did not run because it requested inaccessible system parameter {1}", - name, - disqualified::ShortName::of::

() - ); } } @@ -232,8 +241,13 @@ where /// Set warn policy. fn with_param_warn_policy(self, warn_policy: ParamWarnPolicy) -> FunctionSystem; - /// Disable all param warnings. - fn never_param_warn(self) -> FunctionSystem { + /// Warn and ignore systems with invalid parameters. + fn warn_param_missing(self) -> FunctionSystem { + self.with_param_warn_policy(ParamWarnPolicy::Warn) + } + + /// Silently ignore systems with invalid parameters. + fn ignore_param_missing(self) -> FunctionSystem { self.with_param_warn_policy(ParamWarnPolicy::Never) } } @@ -382,7 +396,7 @@ macro_rules! impl_build_system { Input: SystemInput, Out: 'static, Marker, - F: FnMut(In, $(SystemParamItem<$param>),*) -> Out + F: FnMut(Input, $(SystemParamItem<$param>),*) -> Out + SystemParamFunction, >( self, @@ -460,6 +474,12 @@ impl SystemState { &self.meta } + /// Gets the metadata for this instance. + #[inline] + pub fn meta_mut(&mut self) -> &mut SystemMeta { + &mut self.meta + } + /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. #[inline] pub fn get<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> @@ -630,6 +650,25 @@ impl SystemState { self.meta.last_run = change_tick; param } + + /// Returns a reference to the current system param states. + pub fn param_state(&self) -> &Param::State { + &self.param_state + } + + /// Returns a mutable reference to the current system param states. + /// Marked as unsafe because modifying the system states may result in violation to certain + /// assumptions made by the [`SystemParam`]. Use with care. + /// + /// # Safety + /// 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. + pub unsafe fn param_state_mut(&mut self) -> &mut Param::State { + &mut self.param_state + } } impl FromWorld for SystemState { @@ -642,7 +681,7 @@ impl FromWorld for SystemState { /// /// You get this by calling [`IntoSystem::into_system`] on a function that only accepts /// [`SystemParam`]s. The output of the system becomes the functions return type, while the input -/// becomes the functions [`In`] tagged parameter or `()` if no such parameter exists. +/// becomes the functions first parameter or `()` if no such parameter exists. /// /// [`FunctionSystem`] must be `.initialized` before they can be run. /// @@ -973,7 +1012,14 @@ pub struct HasSystemInput; macro_rules! impl_system_function { ($($param: ident),*) => { - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is within a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] impl SystemParamFunction Out> for Func where Func: Send + Sync + 'static, @@ -990,7 +1036,6 @@ macro_rules! impl_system_function { // Yes, this is strange, but `rustc` fails to compile this impl // without using this function. It fails to recognize that `func` // is a function, potentially because of the multiple impls of `FnMut` - #[allow(clippy::too_many_arguments)] fn call_inner( mut f: impl FnMut($($param,)*)->Out, $($param: $param,)* @@ -1002,7 +1047,14 @@ macro_rules! impl_system_function { } } - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is within a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] impl SystemParamFunction<(HasSystemInput, fn(In, $($param,)*) -> Out)> for Func where Func: Send + Sync + 'static, @@ -1017,7 +1069,6 @@ macro_rules! impl_system_function { type Param = ($($param,)*); #[inline] fn run(&mut self, input: In::Inner<'_>, param_value: SystemParamItem< ($($param,)*)>) -> Out { - #[allow(clippy::too_many_arguments)] fn call_inner( mut f: impl FnMut(In::Param<'_>, $($param,)*)->Out, input: In::Inner<'_>, diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 469403bc7345c..12087fdf6a64a 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -259,8 +259,18 @@ macro_rules! impl_system_input_tuple { type Param<'i> = ($($name::Param<'i>,)*); type Inner<'i> = ($($name::Inner<'i>,)*); - #[allow(non_snake_case)] - #[allow(clippy::unused_unit)] + #[expect( + clippy::allow_attributes, + reason = "This is in a macro; as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't have anything to wrap." + )] fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { let ($($name,)*) = this; ($($name::wrap($name),)*) diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 4a4c9e48af230..360ed03c9b1b8 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -116,6 +116,8 @@ //! - [`DynSystemParam`] //! - [`Vec

`] where `P: SystemParam` //! - [`ParamSet>`] where `P: SystemParam` +//! +//! [`Vec

`]: alloc::vec::Vec mod adapter_system; mod builder; @@ -128,7 +130,6 @@ mod input; mod observer_system; mod query; mod schedule_system; -#[allow(clippy::module_inception)] mod system; mod system_name; mod system_param; @@ -317,8 +318,10 @@ pub fn assert_system_does_not_conflict); - #[allow(dead_code)] struct NotSend2(alloc::rc::Rc); world.insert_non_send_resource(NotSend1(alloc::rc::Rc::new(0))); @@ -919,13 +926,15 @@ mod tests { } #[test] + #[expect( + dead_code, + reason = "The `NotSend1` and `NotSend2` structs are used to verify that a system will run, even if the system params include a non-Send resource. As such, the inner value doesn't matter." + )] fn non_send_system() { let mut world = World::default(); world.insert_resource(SystemRan::No); - #[allow(dead_code)] struct NotSend1(alloc::rc::Rc); - #[allow(dead_code)] struct NotSend2(alloc::rc::Rc); world.insert_non_send_resource(NotSend1(alloc::rc::Rc::new(1))); @@ -1107,7 +1116,6 @@ mod tests { } #[test] - #[allow(clippy::too_many_arguments)] fn can_have_16_parameters() { fn sys_x( _: Res, @@ -1275,9 +1283,11 @@ mod tests { } } - /// this test exists to show that read-only world-only queries can return data that lives as long as 'world #[test] - #[allow(unused)] + #[expect( + dead_code, + reason = "This test exists to show that read-only world-only queries can return data that lives as long as `'world`." + )] fn long_life_test() { struct Holder<'w> { value: &'w A, @@ -1652,9 +1662,10 @@ mod tests { assert_is_system(returning::<&str>.map(u64::from_str).map(Result::unwrap)); assert_is_system(static_system_param); assert_is_system( - exclusive_in_out::<(), Result<(), std::io::Error>>.map(|result| { - if let Err(error) = result { - log::error!("{:?}", error); + exclusive_in_out::<(), Result<(), std::io::Error>>.map(|_out| { + #[cfg(feature = "trace")] + if let Err(error) = _out { + tracing::error!("{}", error); } }), ); @@ -1766,6 +1777,7 @@ mod tests { } #[test] + #[should_panic] fn simple_fallible_system() { fn sys() -> Result { Err("error")?; diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 6fc2a4caa5d47..1680eea8497cd 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -202,7 +202,7 @@ use core::{ /// ## Whole Entity Access /// /// [`EntityRef`]s can be fetched from a query. This will give read-only access to any component on the entity, -/// and can be use to dynamically fetch any component without baking it into the query type. Due to this global +/// and can be used to dynamically fetch any component without baking it into the query type. Due to this global /// access to the entity, this will block any other system from parallelizing with it. As such these queries /// should be sparingly used. /// @@ -620,7 +620,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// ) { /// for friends in &friends_query { /// for counter in counter_query.iter_many(&friends.list) { - /// println!("Friend's counter: {:?}", counter.value); + /// println!("Friend's counter: {}", counter.value); /// } /// } /// } @@ -674,7 +674,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// for friends in &friends_query { /// let mut iter = counter_query.iter_many_mut(&friends.list); /// while let Some(mut counter) = iter.fetch_next() { - /// println!("Friend's counter: {:?}", counter.value); + /// println!("Friend's counter: {}", counter.value); /// counter.value += 1; /// } /// } @@ -1890,7 +1890,7 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> /// [System parameter] that provides access to single entity's components, much like [`Query::single`]/[`Query::single_mut`]. /// /// This [`SystemParam`](crate::system::SystemParam) fails validation if zero or more than one matching entity exists. -/// This will cause systems that use this parameter to be skipped. +/// /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Use [`Option>`] instead if zero or one matching entities can exist. /// @@ -1926,7 +1926,7 @@ impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { /// [System parameter] that works very much like [`Query`] except it always contains at least one matching entity. /// /// This [`SystemParam`](crate::system::SystemParam) fails validation if no matching entities exist. -/// This will cause systems that use this parameter to be skipped. +/// /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Much like [`Query::is_empty`] the worst case runtime will be `O(n)` where `n` is the number of *potential* matches. /// This can be notably expensive for queries that rely on non-archetypal filters such as [`Added`](crate::query::Added) or [`Changed`](crate::query::Changed) diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 042e69b675955..e0005f06f46d9 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,183 +1,120 @@ use alloc::{borrow::Cow, vec::Vec}; -use core::any::TypeId; use crate::{ archetype::ArchetypeComponentId, component::{ComponentId, Tick}, query::Access, result::Result, - schedule::InternedSystemSet, system::{input::SystemIn, BoxedSystem, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -/// A type which wraps and unifies the different sorts of systems that can be added to a schedule. -pub enum ScheduleSystem { - /// A system that does not return a result. - Infallible(BoxedSystem<(), ()>), - /// A system that does return a result. - Fallible(BoxedSystem<(), Result>), +use super::IntoSystem; + +/// 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 ScheduleSystem { +impl> System for InfallibleSystemWrapper { type In = (); type Out = Result; - #[inline(always)] + #[inline] fn name(&self) -> Cow<'static, str> { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.name(), - ScheduleSystem::Fallible(inner_system) => inner_system.name(), - } - } - - #[inline(always)] - fn type_id(&self) -> TypeId { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.type_id(), - ScheduleSystem::Fallible(inner_system) => inner_system.type_id(), - } + self.0.name() } - #[inline(always)] + #[inline] fn component_access(&self) -> &Access { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.component_access(), - ScheduleSystem::Fallible(inner_system) => inner_system.component_access(), - } + self.0.component_access() } - #[inline(always)] + #[inline] fn archetype_component_access(&self) -> &Access { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.archetype_component_access(), - ScheduleSystem::Fallible(inner_system) => inner_system.archetype_component_access(), - } + self.0.archetype_component_access() + } + + #[inline] + fn is_send(&self) -> bool { + self.0.is_send() } - #[inline(always)] + #[inline] fn is_exclusive(&self) -> bool { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.is_exclusive(), - ScheduleSystem::Fallible(inner_system) => inner_system.is_exclusive(), - } + self.0.is_exclusive() } - #[inline(always)] + #[inline] fn has_deferred(&self) -> bool { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.has_deferred(), - ScheduleSystem::Fallible(inner_system) => inner_system.has_deferred(), - } + self.0.has_deferred() } - #[inline(always)] + #[inline] unsafe fn run_unsafe( &mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell, ) -> Self::Out { - match self { - ScheduleSystem::Infallible(inner_system) => { - inner_system.run_unsafe(input, world); - Ok(()) - } - ScheduleSystem::Fallible(inner_system) => inner_system.run_unsafe(input, world), - } + self.0.run_unsafe(input, world); + Ok(()) } - #[inline(always)] + #[inline] fn run(&mut self, input: SystemIn<'_, Self>, world: &mut World) -> Self::Out { - match self { - ScheduleSystem::Infallible(inner_system) => { - inner_system.run(input, world); - Ok(()) - } - ScheduleSystem::Fallible(inner_system) => inner_system.run(input, world), - } + self.0.run(input, world); + Ok(()) } - #[inline(always)] + #[inline] fn apply_deferred(&mut self, world: &mut World) { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.apply_deferred(world), - ScheduleSystem::Fallible(inner_system) => inner_system.apply_deferred(world), - } + self.0.apply_deferred(world); } - #[inline(always)] + #[inline] fn queue_deferred(&mut self, world: DeferredWorld) { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.queue_deferred(world), - ScheduleSystem::Fallible(inner_system) => inner_system.queue_deferred(world), - } + self.0.queue_deferred(world); } - #[inline(always)] - fn is_send(&self) -> bool { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.is_send(), - ScheduleSystem::Fallible(inner_system) => inner_system.is_send(), - } - } - - #[inline(always)] + #[inline] unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.validate_param_unsafe(world), - ScheduleSystem::Fallible(inner_system) => inner_system.validate_param_unsafe(world), - } + self.0.validate_param_unsafe(world) } - #[inline(always)] + #[inline] fn initialize(&mut self, world: &mut World) { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.initialize(world), - ScheduleSystem::Fallible(inner_system) => inner_system.initialize(world), - } + self.0.initialize(world); } - #[inline(always)] + #[inline] fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - match self { - ScheduleSystem::Infallible(inner_system) => { - inner_system.update_archetype_component_access(world); - } - ScheduleSystem::Fallible(inner_system) => { - inner_system.update_archetype_component_access(world); - } - } + self.0.update_archetype_component_access(world); } - #[inline(always)] + #[inline] fn check_change_tick(&mut self, change_tick: Tick) { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.check_change_tick(change_tick), - ScheduleSystem::Fallible(inner_system) => inner_system.check_change_tick(change_tick), - } - } - - #[inline(always)] - fn default_system_sets(&self) -> Vec { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.default_system_sets(), - ScheduleSystem::Fallible(inner_system) => inner_system.default_system_sets(), - } + self.0.check_change_tick(change_tick); } - #[inline(always)] + #[inline] fn get_last_run(&self) -> Tick { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.get_last_run(), - ScheduleSystem::Fallible(inner_system) => inner_system.get_last_run(), - } + self.0.get_last_run() } - #[inline(always)] + #[inline] fn set_last_run(&mut self, last_run: Tick) { - match self { - ScheduleSystem::Infallible(inner_system) => inner_system.set_last_run(last_run), - ScheduleSystem::Fallible(inner_system) => inner_system.set_last_run(last_run), - } + self.0.set_last_run(last_run); + } + + fn default_system_sets(&self) -> Vec { + self.0.default_system_sets() } } + +/// Type alias for a `BoxedSystem` that a `Schedule` can store. +pub type ScheduleSystem = BoxedSystem<(), Result>; diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 6cef4da705399..4f22340bff8cc 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -1,3 +1,7 @@ +#![expect( + clippy::module_inception, + reason = "This instance of module inception is being discussed; see #17353." +)] use core::fmt::Debug; use log::warn; use thiserror::Error; @@ -400,7 +404,6 @@ mod tests { #[derive(Resource, Default, PartialEq, Debug)] struct Counter(u8); - #[allow(dead_code)] fn count_up(mut counter: ResMut) { counter.0 += 1; } @@ -416,7 +419,6 @@ mod tests { assert_eq!(*world.resource::(), Counter(2)); } - #[allow(dead_code)] fn spawn_entity(mut commands: Commands) { commands.spawn_empty(); } @@ -450,7 +452,7 @@ mod tests { let mut world = World::default(); // This fails because `T` has not been added to the world yet. - let result = world.run_system_once(system); + let result = world.run_system_once(system.warn_param_missing()); assert!(matches!(result, Err(RunSystemError::InvalidParams(_)))); } diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index 3ecc901baae2b..b28ddd89f6658 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -94,6 +94,7 @@ mod tests { system::{IntoSystem, RunSystemOnce, SystemName}, world::World, }; + use alloc::{borrow::ToOwned, string::String}; #[test] fn test_system_name_regular_param() { diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 2b38fc6201146..8358bc66af827 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -20,7 +20,7 @@ use alloc::{borrow::ToOwned, boxed::Box, vec::Vec}; pub use bevy_ecs_macros::{Resource, SystemParam}; use bevy_ptr::UnsafeCellDeref; use bevy_utils::synccell::SyncCell; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{ any::Any, @@ -201,7 +201,10 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// `archetype` must be from the [`World`] used to initialize `state` in [`SystemParam::init_state`]. #[inline] - #[allow(unused_variables)] + #[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, @@ -214,12 +217,18 @@ pub unsafe trait SystemParam: Sized { /// /// [`Commands`]: crate::prelude::Commands #[inline] - #[allow(unused_variables)] + #[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 apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {} /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). #[inline] - #[allow(unused_variables)] + #[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 queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {} /// Validates that the param can be acquired by the [`get_param`](SystemParam::get_param). @@ -249,10 +258,14 @@ pub unsafe trait SystemParam: Sized { /// registered in [`init_state`](SystemParam::init_state). /// - `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, - _system_meta: &SystemMeta, - _world: UnsafeWorldCell, + state: &Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell, ) -> bool { // By default we allow panics in [`SystemParam::get_param`] and return `true`. // Preventing panics is an optional feature. @@ -645,12 +658,16 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// # } /// fn event_system( /// mut set: ParamSet<( -/// // `EventReader`s and `EventWriter`s conflict with each other, -/// // since they both access the event queue resource for `MyEvent`. +/// // PROBLEM: `EventReader` and `EventWriter` cannot be used together normally, +/// // because they both need access to the same event queue. +/// // SOLUTION: `ParamSet` allows these conflicting parameters to be used safely +/// // by ensuring only one is accessed at a time. /// EventReader, /// EventWriter, -/// // `&World` reads the entire world, so a `ParamSet` is the only way -/// // that it can be used in the same system as any mutable accesses. +/// // PROBLEM: `&World` needs read access to everything, which conflicts with +/// // any mutable access in the same system. +/// // SOLUTION: `ParamSet` ensures `&World` is only accessed when we're not +/// // using the other mutable parameters. /// &World, /// )>, /// ) { @@ -687,8 +704,14 @@ macro_rules! impl_param_set { type State = ($($param::State,)*); type Item<'w, 's> = ParamSet<'w, 's, ($($param,)*)>; - // Note: We allow non snake case so the compiler don't complain about the creation of non_snake_case variables - #[allow(non_snake_case)] + #[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_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { $( // Pretend to add each param to the system alone, see if it conflicts @@ -914,7 +937,7 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { last_run: system_meta.last_run, this_run: change_tick, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), } } @@ -949,7 +972,7 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { last_run: system_meta.last_run, this_run: change_tick, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), }) } @@ -1027,7 +1050,7 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { last_run: system_meta.last_run, this_run: change_tick, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: value.changed_by, } } @@ -1059,7 +1082,7 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { last_run: system_meta.last_run, this_run: change_tick, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: value.changed_by, }) } @@ -1440,7 +1463,7 @@ unsafe impl SystemParam for Deferred<'_, T> { /// over to another thread. /// /// This [`SystemParam`] fails validation if non-send resource doesn't exist. -/// This will cause systems that use this parameter to be skipped. +/// /// This will cause a panic, but can be configured to do nothing or warn once. /// /// Use [`Option>`] instead if the resource might not always exist. pub struct NonSend<'w, T: 'static> { @@ -1448,7 +1471,7 @@ pub struct NonSend<'w, T: 'static> { ticks: ComponentTicks, last_run: Tick, this_run: Tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &'static Location<'static>, } @@ -1476,7 +1499,7 @@ impl<'w, T: 'static> NonSend<'w, T> { } /// The location that last caused this to change. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn changed_by(&self) -> &'static Location<'static> { self.changed_by } @@ -1499,7 +1522,7 @@ impl<'a, T> From> for NonSend<'a, T> { }, this_run: nsm.ticks.this_run, last_run: nsm.ticks.last_run, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: nsm.changed_by, } } @@ -1575,7 +1598,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { ticks: ticks.read(), last_run: system_meta.last_run, this_run: change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), } } @@ -1607,7 +1630,7 @@ unsafe impl SystemParam for Option> { ticks: ticks.read(), last_run: system_meta.last_run, this_run: change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), }) } @@ -1683,7 +1706,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { NonSendMut { value: ptr.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref_mut(), } } @@ -1710,7 +1733,7 @@ unsafe impl<'a, T: 'static> SystemParam for Option> { .map(|(ptr, ticks, _caller)| NonSendMut { value: ptr.assert_unique().deref_mut(), ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref_mut(), }) } @@ -2005,56 +2028,76 @@ macro_rules! impl_system_param_tuple { // SAFETY: tuple consists only of ReadOnlySystemParams unsafe impl<$($param: ReadOnlySystemParam),*> ReadOnlySystemParam for ($($param,)*) {} - // SAFETY: implementors of each `SystemParam` in the tuple have validated their impls - #[allow(clippy::undocumented_unsafe_blocks)] // false positive by clippy - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "This is in a macro, and as such, the below lints may not always apply." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + #[allow( + unused_variables, + reason = "Zero-length tuples won't use some of the parameters." + )] $(#[$meta])* + // SAFETY: implementors of each `SystemParam` in the tuple have validated their impls unsafe impl<$($param: SystemParam),*> SystemParam for ($($param,)*) { type State = ($($param::State,)*); 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, system_meta: &mut SystemMeta) -> Self::State { + (($($param::init_state(world, system_meta),)*)) } #[inline] - #[allow(unused_unsafe)] - unsafe fn new_archetype(($($param,)*): &mut Self::State, _archetype: &Archetype, _system_meta: &mut SystemMeta) { + 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);)* } + unsafe { $($param::new_archetype($param, archetype, system_meta);)* } } #[inline] - fn apply(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, _world: &mut World) { - $($param::apply($param, _system_meta, _world);)* + fn apply(($($param,)*): &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + $($param::apply($param, system_meta, world);)* } #[inline] - fn queue(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, mut _world: DeferredWorld) { - $($param::queue($param, _system_meta, _world.reborrow());)* + #[allow( + unused_mut, + reason = "The `world` parameter is unused for zero-length tuples; however, it must be mutable for other lengths of tuples." + )] + fn queue(($($param,)*): &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + $($param::queue($param, system_meta, world.reborrow());)* } #[inline] unsafe fn validate_param( state: &Self::State, - _system_meta: &SystemMeta, - _world: UnsafeWorldCell, + system_meta: &SystemMeta, + world: UnsafeWorldCell, ) -> bool { let ($($param,)*) = state; - $($param::validate_param($param, _system_meta, _world)&&)* true + $($param::validate_param($param, system_meta, world)&&)* true } #[inline] - #[allow(clippy::unused_unit)] unsafe fn get_param<'w, 's>( state: &'s mut Self::State, - _system_meta: &SystemMeta, - _world: UnsafeWorldCell<'w>, - _change_tick: Tick, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + change_tick: Tick, ) -> Self::Item<'w, 's> { let ($($param,)*) = state; - ($($param::get_param($param, _system_meta, _world, _change_tick),)*) + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples won't have any params to get." + )] + ($($param::get_param($param, system_meta, world, change_tick),)*) } } }; @@ -2643,7 +2686,10 @@ mod tests { // Compile test for https://github.com/bevyengine/bevy/pull/7001. #[test] fn system_param_const_generics() { - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to ensure that const generics are supported as a SystemParam; thus, the inner value never needs to be read." + )] #[derive(SystemParam)] pub struct ConstGenericParam<'w, const I: usize>(Res<'w, R>); @@ -2701,7 +2747,10 @@ mod tests { #[derive(SystemParam)] pub struct UnitParam; - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to ensure that tuple structs are supported as a SystemParam; thus, the inner values never need to be read." + )] #[derive(SystemParam)] pub struct TupleParam<'w, 's, R: Resource, L: FromWorld + Send + 'static>( Res<'w, R>, @@ -2718,7 +2767,10 @@ mod tests { #[derive(Resource)] struct PrivateResource; - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to ensure that SystemParam's derive can't leak private fields; thus, the inner values never need to be read." + )] #[derive(SystemParam)] pub struct EncapsulatedParam<'w>(Res<'w, PrivateResource>); diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 6cc7556bfda2b..2a85afd12adad 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -2,14 +2,13 @@ use crate::reflect::ReflectComponent; use crate::{ self as bevy_ecs, - bundle::Bundle, change_detection::Mut, entity::Entity, system::{input::SystemInput, BoxedSystem, IntoSystem, System}, - world::{Command, World}, + world::World, }; use alloc::boxed::Box; -use bevy_ecs_macros::{Component, Resource}; +use bevy_ecs_macros::{require, Component, Resource}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use core::marker::PhantomData; @@ -17,13 +16,23 @@ use thiserror::Error; /// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized. #[derive(Component)] -struct RegisteredSystem { +#[require(SystemIdMarker)] +pub(crate) struct RegisteredSystem { initialized: bool, system: BoxedSystem, } +impl RegisteredSystem { + pub fn new(system: BoxedSystem) -> Self { + RegisteredSystem { + initialized: false, + system, + } + } +} + /// Marker [`Component`](bevy_ecs::component::Component) for identifying [`SystemId`] [`Entity`]s. -#[derive(Component)] +#[derive(Component, Default)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Component))] pub struct SystemIdMarker; @@ -120,17 +129,6 @@ impl core::fmt::Debug for SystemId { #[derive(Resource)] pub struct CachedSystemId(pub SystemId); -/// Creates a [`Bundle`] for a one-shot system entity. -fn system_bundle(system: BoxedSystem) -> impl Bundle { - ( - RegisteredSystem { - initialized: false, - system, - }, - SystemIdMarker, - ) -} - impl World { /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`]. /// @@ -163,7 +161,7 @@ impl World { I: SystemInput + 'static, O: 'static, { - let entity = self.spawn(system_bundle(system)).id(); + let entity = self.spawn(RegisteredSystem::new(system)).id(); SystemId::from_entity(entity) } @@ -401,7 +399,9 @@ impl World { self.resource_scope(|world, mut id: Mut>| { if let Ok(mut entity) = world.get_entity_mut(id.0.entity()) { if !entity.contains::>() { - entity.insert(system_bundle(Box::new(IntoSystem::into_system(system)))); + entity.insert(RegisteredSystem::new(Box::new(IntoSystem::into_system( + system, + )))); } } else { id.0 = world.register_system(system); @@ -456,199 +456,6 @@ impl World { } } -/// The [`Command`] type for [`World::run_system`] or [`World::run_system_with`]. -/// -/// This command runs systems in an exclusive and single threaded way. -/// Running slow systems can become a bottleneck. -/// -/// If the system needs an [`In<_>`](crate::system::In) input value to run, it must -/// be provided as part of the command. -/// -/// There is no way to get the output of a system when run as a command, because the -/// execution of the system happens later. To get the output of a system, use -/// [`World::run_system`] or [`World::run_system_with`] instead of running the system as a command. -#[derive(Debug, Clone)] -pub struct RunSystemWith { - system_id: SystemId, - input: I::Inner<'static>, -} - -/// The [`Command`] type for [`World::run_system`]. -/// -/// This command runs systems in an exclusive and single threaded way. -/// Running slow systems can become a bottleneck. -/// -/// If the system needs an [`In<_>`](crate::system::In) input value to run, use the -/// [`RunSystemWith`] type instead. -/// -/// There is no way to get the output of a system when run as a command, because the -/// execution of the system happens later. To get the output of a system, use -/// [`World::run_system`] or [`World::run_system_with`] instead of running the system as a command. -pub type RunSystem = RunSystemWith<()>; - -impl RunSystem { - /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands). - pub fn new(system_id: SystemId) -> Self { - Self::new_with_input(system_id, ()) - } -} - -impl RunSystemWith { - /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands) - /// in order to run the specified system with the provided [`In<_>`](crate::system::In) input value. - pub fn new_with_input(system_id: SystemId, input: I::Inner<'static>) -> Self { - Self { system_id, input } - } -} - -impl Command for RunSystemWith -where - I: SystemInput: Send> + 'static, -{ - #[inline] - fn apply(self, world: &mut World) { - _ = world.run_system_with(self.system_id, self.input); - } -} - -/// The [`Command`] type for registering one shot systems from [`Commands`](crate::system::Commands). -/// -/// This command needs an already boxed system to register, and an already spawned entity. -pub struct RegisterSystem { - system: BoxedSystem, - entity: Entity, -} - -impl RegisterSystem -where - I: SystemInput + 'static, - O: 'static, -{ - /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands). - pub fn new + 'static>(system: S, entity: Entity) -> Self { - Self { - system: Box::new(IntoSystem::into_system(system)), - entity, - } - } -} - -impl Command for RegisterSystem -where - I: SystemInput + Send + 'static, - O: Send + 'static, -{ - fn apply(self, world: &mut World) { - if let Ok(mut entity) = world.get_entity_mut(self.entity) { - entity.insert(system_bundle(self.system)); - } - } -} - -/// The [`Command`] type for unregistering one-shot systems from [`Commands`](crate::system::Commands). -pub struct UnregisterSystem { - system_id: SystemId, -} - -impl UnregisterSystem -where - I: SystemInput + 'static, - O: 'static, -{ - /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands). - pub fn new(system_id: SystemId) -> Self { - Self { system_id } - } -} - -impl Command for UnregisterSystem -where - I: SystemInput + 'static, - O: 'static, -{ - fn apply(self, world: &mut World) { - let _ = world.unregister_system(self.system_id); - } -} - -/// The [`Command`] type for unregistering one-shot systems from [`Commands`](crate::system::Commands). -pub struct UnregisterSystemCached -where - I: SystemInput + 'static, - S: IntoSystem + Send + 'static, -{ - system: S, - _phantom: PhantomData (I, O, M)>, -} - -impl UnregisterSystemCached -where - I: SystemInput + 'static, - S: IntoSystem + Send + 'static, -{ - /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands). - pub fn new(system: S) -> Self { - Self { - system, - _phantom: PhantomData, - } - } -} - -impl Command for UnregisterSystemCached -where - I: SystemInput + 'static, - O: 'static, - M: 'static, - S: IntoSystem + Send + 'static, -{ - fn apply(self, world: &mut World) { - let _ = world.unregister_system_cached(self.system); - } -} - -/// The [`Command`] type for running a cached one-shot system from -/// [`Commands`](crate::system::Commands). -/// -/// See [`World::register_system_cached`] for more information. -pub struct RunSystemCachedWith -where - I: SystemInput, - S: IntoSystem, -{ - system: S, - input: I::Inner<'static>, - _phantom: PhantomData<(fn() -> O, fn() -> M)>, -} - -impl RunSystemCachedWith -where - I: SystemInput, - S: IntoSystem, -{ - /// Creates a new [`Command`] struct, which can be added to - /// [`Commands`](crate::system::Commands). - pub fn new(system: S, input: I::Inner<'static>) -> Self { - Self { - system, - input, - _phantom: PhantomData, - } - } -} - -impl Command for RunSystemCachedWith -where - I: SystemInput: Send> + Send + 'static, - O: Send + 'static, - S: IntoSystem + Send + 'static, - M: 'static, -{ - fn apply(self, world: &mut World) { - let _ = world.run_system_cached_with(self.system, self.input); - } -} - /// An operation with stored systems failed. #[derive(Error)] pub enum RegisteredSystemError { @@ -998,7 +805,7 @@ mod tests { fn system(_: Res) {} let mut world = World::new(); - let id = world.register_system_cached(system); + let id = world.register_system(system.warn_param_missing()); // This fails because `T` has not been added to the world yet. let result = world.run_system(id); diff --git a/crates/bevy_ecs/src/world/command_queue.rs b/crates/bevy_ecs/src/world/command_queue.rs index 014c5b21c5895..5b78b7a974e3f 100644 --- a/crates/bevy_ecs/src/world/command_queue.rs +++ b/crates/bevy_ecs/src/world/command_queue.rs @@ -1,20 +1,17 @@ -use crate::system::{SystemBuffer, SystemMeta}; - +use crate::{ + system::{Command, SystemBuffer, SystemMeta}, + world::{DeferredWorld, World}, +}; +use alloc::{boxed::Box, vec::Vec}; +use bevy_ptr::{OwningPtr, Unaligned}; use core::{ fmt::Debug, mem::{size_of, MaybeUninit}, panic::AssertUnwindSafe, ptr::{addr_of_mut, NonNull}, }; - -use alloc::{boxed::Box, vec::Vec}; -use bevy_ptr::{OwningPtr, Unaligned}; use log::warn; -use crate::world::{Command, World}; - -use super::DeferredWorld; - struct CommandMeta { /// SAFETY: The `value` must point to a value of type `T: Command`, /// where `T` is some specific type that was used to produce this metadata. @@ -75,10 +72,7 @@ unsafe impl Sync for CommandQueue {} impl CommandQueue { /// Push a [`Command`] onto the queue. #[inline] - pub fn push(&mut self, command: C) - where - C: Command, - { + pub fn push(&mut self, command: impl Command) { // SAFETY: self is guaranteed to live for the lifetime of this method unsafe { self.get_raw().push(command); @@ -154,17 +148,14 @@ impl RawCommandQueue { /// /// * Caller ensures that `self` has not outlived the underlying queue #[inline] - pub unsafe fn push(&mut self, command: C) - where - C: Command, - { + pub unsafe fn push(&mut self, command: C) { // Stores a command alongside its metadata. // `repr(C)` prevents the compiler from reordering the fields, // while `repr(packed)` prevents the compiler from inserting padding bytes. #[repr(C, packed)] - struct Packed { + struct Packed { meta: CommandMeta, - command: T, + command: C, } let meta = CommandMeta { @@ -344,14 +335,16 @@ impl SystemBuffer for CommandQueue { #[cfg(test)] mod test { use super::*; - use crate as bevy_ecs; - use crate::system::Resource; - use alloc::sync::Arc; + use crate::{self as bevy_ecs, system::Resource}; + use alloc::{borrow::ToOwned, string::String, sync::Arc}; use core::{ panic::AssertUnwindSafe, sync::atomic::{AtomicU32, Ordering}, }; + #[cfg(miri)] + use alloc::format; + struct DropCheck(Arc); impl DropCheck { @@ -438,10 +431,10 @@ mod test { assert_eq!(world.entities().len(), 2); } - // This has an arbitrary value `String` stored to ensure - // when then command gets pushed, the `bytes` vector gets - // some data added to it. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "The inner string is used to ensure that, when the PanicCommand gets pushed to the queue, some data is written to the `bytes` vector." + )] struct PanicCommand(String); impl Command for PanicCommand { fn apply(self, _: &mut World) { @@ -517,7 +510,10 @@ mod test { assert_is_send(SpawnCommand); } - #[allow(dead_code)] + #[expect( + dead_code, + reason = "This struct is used to test how the CommandQueue reacts to padding added by rust's compiler." + )] struct CommandWithPadding(u8, u16); impl Command for CommandWithPadding { fn apply(self, _: &mut World) {} diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index c66bcc53c869a..1d28051fc516e 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -14,7 +14,7 @@ use crate::{ world::{error::EntityFetchError, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. @@ -75,11 +75,92 @@ impl<'w> DeferredWorld<'w> { &mut self, entity: Entity, ) -> Option> { + self.get_entity_mut(entity).ok()?.into_mut() + } + + /// 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 + /// causing an archetype move. + /// + /// This is most useful with immutable components, where removal and reinsertion + /// is the only way to modify a value. + /// + /// 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( + &mut self, + entity: Entity, + f: impl FnOnce(&mut T) -> R, + ) -> Result, EntityFetchError> { + // If the component is not registered, then it doesn't exist on this entity, so no action required. + let Some(component_id) = self.component_id::() else { + return Ok(None); + }; + + let entity_cell = match self.get_entity_mut(entity) { + Ok(cell) => cell, + Err(EntityFetchError::AliasedMutability(..)) => { + return Err(EntityFetchError::AliasedMutability(entity)) + } + Err(EntityFetchError::NoSuchEntity(..)) => { + return Err(EntityFetchError::NoSuchEntity( + entity, + self.entities().entity_does_not_exist_error_details(entity), + )) + } + }; + + if !entity_cell.contains::() { + return Ok(None); + } + + let archetype = &raw const *entity_cell.archetype(); + + // SAFETY: + // - 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 + unsafe { + let archetype = &*archetype; + self.trigger_on_replace(archetype, entity, [component_id].into_iter()); + if archetype.has_replace_observer() { + self.trigger_observers(ON_REPLACE, entity, [component_id].into_iter()); + } + } + + let mut entity_cell = self + .get_entity_mut(entity) + .expect("entity access confirmed above"); + + // SAFETY: we will run the required hooks to simulate removal/replacement. + let mut component = unsafe { + entity_cell + .get_mut_assume_mutable::() + .expect("component access confirmed above") + }; + + let result = f(&mut component); + + // Simulate adding this component by updating the relevant ticks + *component.ticks.added = *component.ticks.changed; + // SAFETY: - // - `as_unsafe_world_cell` is the only thing that is borrowing world - // - `as_unsafe_world_cell` provides mutable permission to everything - // - `&mut self` ensures no other borrows on world data - unsafe { self.world.get_entity(entity)?.get_mut() } + // - 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 + unsafe { + let archetype = &*archetype; + self.trigger_on_insert(archetype, entity, [component_id].into_iter()); + if archetype.has_insert_observer() { + self.trigger_observers(ON_INSERT, entity, [component_id].into_iter()); + } + } + + Ok(Some(result)) } /// Returns [`EntityMut`]s that expose read and write operations for the @@ -110,6 +191,7 @@ impl<'w> DeferredWorld<'w> { /// [`EntityMut`]: crate::world::EntityMut /// [`&EntityHashSet`]: crate::entity::EntityHashSet /// [`EntityHashMap`]: crate::entity::EntityHashMap + /// [`Vec`]: alloc::vec::Vec #[inline] pub fn get_entity_mut( &mut self, @@ -241,6 +323,7 @@ impl<'w> DeferredWorld<'w> { /// [`EntityMut`]: crate::world::EntityMut /// [`&EntityHashSet`]: crate::entity::EntityHashSet /// [`EntityHashMap`]: crate::entity::EntityHashMap + /// [`Vec`]: alloc::vec::Vec #[inline] pub fn entity_mut(&mut self, entities: F) -> F::DeferredMut<'_> { self.get_entity_mut(entities).unwrap() @@ -403,13 +486,10 @@ impl<'w> DeferredWorld<'w> { entity: Entity, component_id: ComponentId, ) -> Option> { - // SAFETY: &mut self ensure that there are no outstanding accesses to the resource - unsafe { - self.world - .get_entity(entity)? - .get_mut_by_id(component_id) - .ok() - } + self.get_entity_mut(entity) + .ok()? + .into_mut_by_id(component_id) + .ok() } /// Triggers all `on_add` hooks for [`ComponentId`] in target. @@ -562,7 +642,7 @@ impl<'w> DeferredWorld<'w> { } /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. - pub fn trigger(&mut self, trigger: impl Event) { + pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } diff --git a/crates/bevy_ecs/src/world/entity_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs index 32fbca97864c1..05a5c51aff67e 100644 --- a/crates/bevy_ecs/src/world/entity_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use core::mem::MaybeUninit; use crate::{ - entity::{Entity, EntityHash, EntityHashMap, EntityHashSet}, + entity::{Entity, EntityHashMap, EntityHashSet}, world::{ error::EntityFetchError, unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, EntityWorldMut, @@ -21,10 +21,8 @@ use crate::{ /// /// # Performance /// -/// - The slice and array implementations perform an aliased mutabiltiy check +/// - The slice and array implementations perform an aliased mutability check /// in [`WorldEntityFetch::fetch_mut`] that is `O(N^2)`. -/// - The [`EntityHashSet`] implementation performs no such check as the type -/// itself guarantees no duplicates. /// - The single [`Entity`] implementation performs no such check as only one /// reference is returned. /// @@ -124,7 +122,10 @@ unsafe impl WorldEntityFetch for Entity { let location = cell .entities() .get(self) - .ok_or(EntityFetchError::NoSuchEntity(self, cell))?; + .ok_or(EntityFetchError::NoSuchEntity( + self, + cell.entities().entity_does_not_exist_error_details(self), + ))?; // 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`. @@ -135,9 +136,10 @@ unsafe impl WorldEntityFetch for Entity { self, cell: UnsafeWorldCell<'_>, ) -> Result, EntityFetchError> { - let ecell = cell - .get_entity(self) - .ok_or(EntityFetchError::NoSuchEntity(self, cell))?; + let ecell = cell.get_entity(self).ok_or(EntityFetchError::NoSuchEntity( + self, + cell.entities().entity_does_not_exist_error_details(self), + ))?; // SAFETY: caller ensures that the world cell has mutable access to the entity. Ok(unsafe { EntityMut::new(ecell) }) } @@ -209,9 +211,10 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { let mut refs = [const { MaybeUninit::uninit() }; N]; for (r, &id) in core::iter::zip(&mut refs, self) { - let ecell = cell - .get_entity(id) - .ok_or(EntityFetchError::NoSuchEntity(id, cell))?; + let ecell = cell.get_entity(id).ok_or(EntityFetchError::NoSuchEntity( + id, + cell.entities().entity_does_not_exist_error_details(id), + ))?; // SAFETY: caller ensures that the world cell has mutable access to the entity. *r = MaybeUninit::new(unsafe { EntityMut::new(ecell) }); } @@ -267,9 +270,10 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { let mut refs = Vec::with_capacity(self.len()); for &id in self { - let ecell = cell - .get_entity(id) - .ok_or(EntityFetchError::NoSuchEntity(id, cell))?; + let ecell = cell.get_entity(id).ok_or(EntityFetchError::NoSuchEntity( + id, + cell.entities().entity_does_not_exist_error_details(id), + ))?; // SAFETY: caller ensures that the world cell has mutable access to the entity. refs.push(unsafe { EntityMut::new(ecell) }); } @@ -297,7 +301,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { type DeferredMut<'w> = EntityHashMap>; unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { - let mut refs = EntityHashMap::with_capacity_and_hasher(self.len(), EntityHash); + let mut refs = EntityHashMap::with_capacity(self.len()); for &id in self { let ecell = cell.get_entity(id).ok_or(id)?; // SAFETY: caller ensures that the world cell has read-only access to the entity. @@ -310,11 +314,12 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { self, cell: UnsafeWorldCell<'_>, ) -> Result, EntityFetchError> { - let mut refs = EntityHashMap::with_capacity_and_hasher(self.len(), EntityHash); + let mut refs = EntityHashMap::with_capacity(self.len()); for &id in self { - let ecell = cell - .get_entity(id) - .ok_or(EntityFetchError::NoSuchEntity(id, cell))?; + let ecell = cell.get_entity(id).ok_or(EntityFetchError::NoSuchEntity( + id, + cell.entities().entity_does_not_exist_error_details(id), + ))?; // SAFETY: caller ensures that the world cell has mutable access to the entity. refs.insert(id, unsafe { EntityMut::new(ecell) }); } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 64dd16149a0ca..ddbfa81e627a6 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -3,21 +3,29 @@ use crate::{ bundle::{Bundle, BundleId, BundleInfo, BundleInserter, DynamicBundle, InsertMode}, change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, Mutable, StorageType}, - entity::{Entities, Entity, EntityCloneBuilder, EntityLocation}, + entity::{ + Entities, Entity, EntityBorrow, EntityCloneBuilder, EntityLocation, TrustedEntityBorrow, + }, event::Event, observer::Observer, query::{Access, ReadOnlyQueryData}, removal_detection::RemovedComponentEvents, storage::Storages, - system::IntoObserverSystem, + system::{IntoObserverSystem, Resource}, world::{error::EntityComponentError, DeferredWorld, Mut, World}, }; use alloc::vec::Vec; use bevy_ptr::{OwningPtr, Ptr}; use bevy_utils::{HashMap, HashSet}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; -use core::{any::TypeId, marker::PhantomData, mem::MaybeUninit}; +use core::{ + any::TypeId, + cmp::Ordering, + hash::{Hash, Hasher}, + marker::PhantomData, + mem::MaybeUninit, +}; use thiserror::Error; use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE, ON_REPLACE}; @@ -41,7 +49,9 @@ use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE, ON_REPLACE}; /// # bevy_ecs::system::assert_is_system(disjoint_system); /// ``` #[derive(Copy, Clone)] -pub struct EntityRef<'w>(UnsafeEntityCell<'w>); +pub struct EntityRef<'w> { + cell: UnsafeEntityCell<'w>, +} impl<'w> EntityRef<'w> { /// # Safety @@ -50,26 +60,26 @@ impl<'w> EntityRef<'w> { /// at the same time as the returned [`EntityRef`]. #[inline] pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { - Self(cell) + Self { cell } } /// Returns the [ID](Entity) of the current entity. #[inline] #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] pub fn id(&self) -> Entity { - self.0.id() + self.cell.id() } /// Gets metadata indicating the location where the current entity is stored. #[inline] pub fn location(&self) -> EntityLocation { - self.0.location() + self.cell.location() } /// Returns the archetype that the current entity belongs to. #[inline] pub fn archetype(&self) -> &Archetype { - self.0.archetype() + self.cell.archetype() } /// Returns `true` if the current entity has a component of type `T`. @@ -94,7 +104,7 @@ impl<'w> EntityRef<'w> { /// [`Self::contains_type_id`]. #[inline] pub fn contains_id(&self, component_id: ComponentId) -> bool { - self.0.contains_id(component_id) + self.cell.contains_id(component_id) } /// Returns `true` if the current entity has a component with the type identified by `type_id`. @@ -106,7 +116,7 @@ impl<'w> EntityRef<'w> { /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. #[inline] pub fn contains_type_id(&self, type_id: TypeId) -> bool { - self.0.contains_type_id(type_id) + self.cell.contains_type_id(type_id) } /// Gets access to the component of type `T` for the current entity. @@ -114,7 +124,7 @@ impl<'w> EntityRef<'w> { #[inline] pub fn get(&self) -> Option<&'w T> { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get::() } + unsafe { self.cell.get::() } } /// Gets access to the component of type `T` for the current entity, @@ -124,7 +134,7 @@ impl<'w> EntityRef<'w> { #[inline] pub fn get_ref(&self) -> Option> { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_ref::() } + unsafe { self.cell.get_ref::() } } /// Retrieves the change ticks for the given component. This can be useful for implementing change @@ -132,7 +142,7 @@ impl<'w> EntityRef<'w> { #[inline] pub fn get_change_ticks(&self) -> Option { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_change_ticks::() } + unsafe { self.cell.get_change_ticks::() } } /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change @@ -144,7 +154,7 @@ impl<'w> EntityRef<'w> { #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_change_ticks_by_id(component_id) } + unsafe { self.cell.get_change_ticks_by_id(component_id) } } /// Returns [untyped read-only reference(s)](Ptr) to component(s) for the @@ -257,7 +267,7 @@ impl<'w> EntityRef<'w> { component_ids: F, ) -> Result, EntityComponentError> { // SAFETY: We have read-only access to all components of this entity. - unsafe { component_ids.fetch_ref(self.0) } + unsafe { component_ids.fetch_ref(self.cell) } } /// Returns read-only components for the current entity that match the query `Q`. @@ -266,66 +276,67 @@ impl<'w> EntityRef<'w> { /// /// If the entity does not have the components required by the query `Q`. pub fn components(&self) -> Q::Item<'w> { - self.get_components::().expect(QUERY_MISMATCH_ERROR) + 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> { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_components::() } + unsafe { self.cell.get_components::() } } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { - self.0.spawned_by() + self.cell.spawned_by() } } impl<'w> From> for EntityRef<'w> { - fn from(entity_mut: EntityWorldMut<'w>) -> EntityRef<'w> { + fn from(entity: EntityWorldMut<'w>) -> EntityRef<'w> { // SAFETY: // - `EntityWorldMut` guarantees exclusive access to the entire world. - unsafe { EntityRef::new(entity_mut.into_unsafe_entity_cell()) } + unsafe { EntityRef::new(entity.into_unsafe_entity_cell()) } } } impl<'a> From<&'a EntityWorldMut<'_>> for EntityRef<'a> { - fn from(value: &'a EntityWorldMut<'_>) -> Self { + fn from(entity: &'a EntityWorldMut<'_>) -> Self { // SAFETY: // - `EntityWorldMut` guarantees exclusive access to the entire world. - // - `&value` ensures no mutable accesses are active. - unsafe { EntityRef::new(value.as_unsafe_entity_cell_readonly()) } + // - `&entity` ensures no mutable accesses are active. + unsafe { EntityRef::new(entity.as_unsafe_entity_cell_readonly()) } } } impl<'w> From> for EntityRef<'w> { - fn from(value: EntityMut<'w>) -> Self { + fn from(entity: EntityMut<'w>) -> Self { // SAFETY: // - `EntityMut` guarantees exclusive access to all of the entity's components. - unsafe { EntityRef::new(value.0) } + unsafe { EntityRef::new(entity.cell) } } } impl<'a> From<&'a EntityMut<'_>> for EntityRef<'a> { - fn from(value: &'a EntityMut<'_>) -> Self { + fn from(entity: &'a EntityMut<'_>) -> Self { // SAFETY: // - `EntityMut` guarantees exclusive access to all of the entity's components. - // - `&value` ensures there are no mutable accesses. - unsafe { EntityRef::new(value.0) } + // - `&entity` ensures there are no mutable accesses. + unsafe { EntityRef::new(entity.cell) } } } impl<'a> TryFrom> for EntityRef<'a> { type Error = TryFromFilteredError; - fn try_from(value: FilteredEntityRef<'a>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: FilteredEntityRef<'a>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) } else { // SAFETY: check above guarantees read-only access to all components of the entity. - Ok(unsafe { EntityRef::new(value.entity) }) + Ok(unsafe { EntityRef::new(entity.entity) }) } } } @@ -333,12 +344,12 @@ impl<'a> TryFrom> for EntityRef<'a> { impl<'a> TryFrom<&'a FilteredEntityRef<'_>> for EntityRef<'a> { type Error = TryFromFilteredError; - fn try_from(value: &'a FilteredEntityRef<'_>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: &'a FilteredEntityRef<'_>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) } else { // SAFETY: check above guarantees read-only access to all components of the entity. - Ok(unsafe { EntityRef::new(value.entity) }) + Ok(unsafe { EntityRef::new(entity.entity) }) } } } @@ -346,12 +357,12 @@ impl<'a> TryFrom<&'a FilteredEntityRef<'_>> for EntityRef<'a> { impl<'a> TryFrom> for EntityRef<'a> { type Error = TryFromFilteredError; - fn try_from(value: FilteredEntityMut<'a>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: FilteredEntityMut<'a>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) } else { // SAFETY: check above guarantees read-only access to all components of the entity. - Ok(unsafe { EntityRef::new(value.entity) }) + Ok(unsafe { EntityRef::new(entity.entity) }) } } } @@ -359,16 +370,53 @@ impl<'a> TryFrom> for EntityRef<'a> { impl<'a> TryFrom<&'a FilteredEntityMut<'_>> for EntityRef<'a> { type Error = TryFromFilteredError; - fn try_from(value: &'a FilteredEntityMut<'_>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: &'a FilteredEntityMut<'_>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) } else { // SAFETY: check above guarantees read-only access to all components of the entity. - Ok(unsafe { EntityRef::new(value.entity) }) + Ok(unsafe { EntityRef::new(entity.entity) }) } } } +impl PartialEq for EntityRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for EntityRef<'_> {} + +impl PartialOrd for EntityRef<'_> { + /// [`EntityRef`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EntityRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for EntityRef<'_> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for EntityRef<'_> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for EntityRef<'_> {} + /// Provides mutable access to a single entity and all of its components. /// /// Contrast with [`EntityWorldMut`], which allows adding and removing components, @@ -390,7 +438,9 @@ impl<'a> TryFrom<&'a FilteredEntityMut<'_>> for EntityRef<'a> { /// } /// # bevy_ecs::system::assert_is_system(disjoint_system); /// ``` -pub struct EntityMut<'w>(UnsafeEntityCell<'w>); +pub struct EntityMut<'w> { + cell: UnsafeEntityCell<'w>, +} impl<'w> EntityMut<'w> { /// # Safety @@ -398,14 +448,20 @@ impl<'w> EntityMut<'w> { /// - No accesses to any of the entity's components may exist /// at the same time as the returned [`EntityMut`]. pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { - Self(cell) + Self { cell } } /// Returns a new instance with a shorter lifetime. /// This is useful if you have `&mut EntityMut`, but you need `EntityMut`. pub fn reborrow(&mut self) -> EntityMut<'_> { // SAFETY: We have exclusive access to the entire entity and its components. - unsafe { Self::new(self.0) } + unsafe { Self::new(self.cell) } + } + + /// Consumes `self` and returns read-only access to all of the entity's + /// components, with the world `'w` lifetime. + pub fn into_readonly(self) -> EntityRef<'w> { + EntityRef::from(self) } /// Gets read-only access to all of the entity's components. @@ -417,19 +473,19 @@ impl<'w> EntityMut<'w> { #[inline] #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] pub fn id(&self) -> Entity { - self.0.id() + self.cell.id() } /// Gets metadata indicating the location where the current entity is stored. #[inline] pub fn location(&self) -> EntityLocation { - self.0.location() + self.cell.location() } /// Returns the archetype that the current entity belongs to. #[inline] pub fn archetype(&self) -> &Archetype { - self.0.archetype() + self.cell.archetype() } /// Returns `true` if the current entity has a component of type `T`. @@ -454,7 +510,7 @@ impl<'w> EntityMut<'w> { /// [`Self::contains_type_id`]. #[inline] pub fn contains_id(&self, component_id: ComponentId) -> bool { - self.0.contains_id(component_id) + self.cell.contains_id(component_id) } /// Returns `true` if the current entity has a component with the type identified by `type_id`. @@ -466,7 +522,7 @@ impl<'w> EntityMut<'w> { /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. #[inline] pub fn contains_type_id(&self, type_id: TypeId) -> bool { - self.0.contains_type_id(type_id) + self.cell.contains_type_id(type_id) } /// Gets access to the component of type `T` for the current entity. @@ -482,14 +538,13 @@ impl<'w> EntityMut<'w> { /// /// If the entity does not have the components required by the query `Q`. pub fn components(&self) -> Q::Item<'_> { - self.get_components::().expect(QUERY_MISMATCH_ERROR) + 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> { - // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_components::() } + self.as_readonly().get_components::() } /// Consumes `self` and gets access to the component of type `T` with the @@ -498,8 +553,7 @@ impl<'w> EntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn into_borrow(self) -> Option<&'w T> { - // SAFETY: consuming `self` implies exclusive access - unsafe { self.0.get() } + self.into_readonly().get() } /// Gets access to the component of type `T` for the current entity, @@ -518,8 +572,7 @@ impl<'w> EntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn into_ref(self) -> Option> { - // SAFETY: consuming `self` implies exclusive access - unsafe { self.0.get_ref() } + self.into_readonly().get_ref() } /// Gets mutable access to the component of type `T` for the current entity. @@ -527,7 +580,7 @@ impl<'w> EntityMut<'w> { #[inline] pub fn get_mut>(&mut self) -> Option> { // SAFETY: &mut self implies exclusive access for duration of returned value - unsafe { self.0.get_mut() } + unsafe { self.cell.get_mut() } } /// Gets mutable access to the component of type `T` for the current entity. @@ -538,8 +591,10 @@ impl<'w> EntityMut<'w> { /// - `T` must be a mutable component #[inline] pub unsafe fn get_mut_assume_mutable(&mut self) -> Option> { - // SAFETY: &mut self implies exclusive access for duration of returned value - unsafe { self.0.get_mut_assume_mutable() } + // SAFETY: + // - &mut self implies exclusive access for duration of returned value + // - Caller ensures `T` is a mutable component + unsafe { self.cell.get_mut_assume_mutable() } } /// Consumes self and gets mutable access to the component of type `T` @@ -548,7 +603,21 @@ impl<'w> EntityMut<'w> { #[inline] pub fn into_mut>(self) -> Option> { // SAFETY: consuming `self` implies exclusive access - unsafe { self.0.get_mut() } + unsafe { self.cell.get_mut() } + } + + /// Gets mutable access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + /// + /// # Safety + /// + /// - `T` must be a mutable component + #[inline] + pub unsafe fn into_mut_assume_mutable(self) -> Option> { + // SAFETY: + // - Consuming `self` implies exclusive access + // - Caller ensures `T` is a mutable component + unsafe { self.cell.get_mut_assume_mutable() } } /// Retrieves the change ticks for the given component. This can be useful for implementing change @@ -621,10 +690,7 @@ impl<'w> EntityMut<'w> { self, component_ids: F, ) -> Result, EntityComponentError> { - // SAFETY: - // - We have read-only access to all components of this entity. - // - consuming `self` ensures that no references exist to this entity's components. - unsafe { component_ids.fetch_ref(self.0) } + self.into_readonly().get_by_id(component_ids) } /// Returns [untyped mutable reference(s)](MutUntyped) to component(s) for @@ -748,7 +814,7 @@ impl<'w> EntityMut<'w> { // SAFETY: // - `&mut self` ensures that no references exist to this entity's components. // - We have exclusive access to all components of this entity. - unsafe { component_ids.fetch_mut(self.0) } + unsafe { component_ids.fetch_mut(self.cell) } } /// Returns [untyped mutable reference](MutUntyped) to component for @@ -776,7 +842,7 @@ impl<'w> EntityMut<'w> { // SAFETY: // - The caller must ensure simultaneous access is limited // - to components that are mutually independent. - unsafe { component_ids.fetch_mut(self.0) } + unsafe { component_ids.fetch_mut(self.cell) } } /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) @@ -809,47 +875,47 @@ impl<'w> EntityMut<'w> { // SAFETY: // - consuming `self` ensures that no references exist to this entity's components. // - We have exclusive access to all components of this entity. - unsafe { component_ids.fetch_mut(self.0) } + unsafe { component_ids.fetch_mut(self.cell) } } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { - self.0.spawned_by() + self.cell.spawned_by() } } impl<'w> From<&'w mut EntityMut<'_>> for EntityMut<'w> { - fn from(value: &'w mut EntityMut<'_>) -> Self { - value.reborrow() + fn from(entity: &'w mut EntityMut<'_>) -> Self { + entity.reborrow() } } impl<'w> From> for EntityMut<'w> { - fn from(value: EntityWorldMut<'w>) -> Self { + fn from(entity: EntityWorldMut<'w>) -> Self { // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. - unsafe { EntityMut::new(value.into_unsafe_entity_cell()) } + unsafe { EntityMut::new(entity.into_unsafe_entity_cell()) } } } impl<'a> From<&'a mut EntityWorldMut<'_>> for EntityMut<'a> { - fn from(value: &'a mut EntityWorldMut<'_>) -> Self { + fn from(entity: &'a mut EntityWorldMut<'_>) -> Self { // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. - unsafe { EntityMut::new(value.as_unsafe_entity_cell()) } + unsafe { EntityMut::new(entity.as_unsafe_entity_cell()) } } } impl<'a> TryFrom> for EntityMut<'a> { type Error = TryFromFilteredError; - fn try_from(value: FilteredEntityMut<'a>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: FilteredEntityMut<'a>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) - } else if !value.access.has_write_all() { + } else if !entity.access.has_write_all() { Err(TryFromFilteredError::MissingWriteAllAccess) } else { // SAFETY: check above guarantees exclusive access to all components of the entity. - Ok(unsafe { EntityMut::new(value.entity) }) + Ok(unsafe { EntityMut::new(entity.entity) }) } } } @@ -857,18 +923,55 @@ impl<'a> TryFrom> for EntityMut<'a> { impl<'a> TryFrom<&'a mut FilteredEntityMut<'_>> for EntityMut<'a> { type Error = TryFromFilteredError; - fn try_from(value: &'a mut FilteredEntityMut<'_>) -> Result { - if !value.access.has_read_all() { + fn try_from(entity: &'a mut FilteredEntityMut<'_>) -> Result { + if !entity.access.has_read_all() { Err(TryFromFilteredError::MissingReadAllAccess) - } else if !value.access.has_write_all() { + } else if !entity.access.has_write_all() { Err(TryFromFilteredError::MissingWriteAllAccess) } else { // SAFETY: check above guarantees exclusive access to all components of the entity. - Ok(unsafe { EntityMut::new(value.entity) }) + Ok(unsafe { EntityMut::new(entity.entity) }) } } } +impl PartialEq for EntityMut<'_> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for EntityMut<'_> {} + +impl PartialOrd for EntityMut<'_> { + /// [`EntityMut`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EntityMut<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for EntityMut<'_> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for EntityMut<'_> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for EntityMut<'_> {} + /// A mutable reference to a particular [`Entity`], and the entire world. /// /// This is essentially a performance-optimized `(Entity, &mut World)` tuple, @@ -896,13 +999,13 @@ impl<'w> EntityWorldMut<'w> { self.entity, self.world .entities() - .entity_does_not_exist_error_details_message(self.entity) + .entity_does_not_exist_error_details(self.entity) ); } #[inline(always)] #[track_caller] - fn assert_not_despawned(&self) { + pub(crate) fn assert_not_despawned(&self) { if self.location.archetype_id == ArchetypeId::INVALID { self.panic_despawned(); } @@ -955,6 +1058,28 @@ impl<'w> EntityWorldMut<'w> { } } + /// Consumes `self` and returns read-only access to all of the entity's + /// components, with the world `'w` lifetime. + pub fn into_readonly(self) -> EntityRef<'w> { + EntityRef::from(self) + } + + /// Gets read-only access to all of the entity's components. + pub fn as_readonly(&self) -> EntityRef<'_> { + EntityRef::from(self) + } + + /// Consumes `self` and returns non-structural mutable access to all of the + /// entity's components, with the world `'w` lifetime. + pub fn into_mutable(self) -> EntityMut<'w> { + EntityMut::from(self) + } + + /// Gets non-structural mutable access to all of the entity's components. + pub fn as_mutable(&mut self) -> EntityMut<'_> { + EntityMut::from(self) + } + /// Returns the [ID](Entity) of the current entity. #[inline] #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] @@ -1043,7 +1168,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get(&self) -> Option<&'_ T> { - EntityRef::from(self).get() + self.as_readonly().get() } /// Returns read-only components for the current entity that match the query `Q`. @@ -1054,7 +1179,7 @@ impl<'w> EntityWorldMut<'w> { /// has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn components(&self) -> Q::Item<'_> { - EntityRef::from(self).components::() + self.as_readonly().components::() } /// Returns read-only components for the current entity that match the query `Q`, @@ -1065,7 +1190,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get_components(&self) -> Option> { - EntityRef::from(self).get_components::() + self.as_readonly().get_components::() } /// Consumes `self` and gets access to the component of type `T` with @@ -1077,8 +1202,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn into_borrow(self) -> Option<&'w T> { - // SAFETY: consuming `self` implies exclusive access - unsafe { self.into_unsafe_entity_cell().get() } + self.into_readonly().get() } /// Gets access to the component of type `T` for the current entity, @@ -1091,7 +1215,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get_ref(&self) -> Option> { - EntityRef::from(self).get_ref() + self.as_readonly().get_ref() } /// Consumes `self` and gets access to the component of type `T` @@ -1105,7 +1229,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn into_ref(self) -> Option> { - EntityRef::from(self).get_ref() + self.into_readonly().get_ref() } /// Gets mutable access to the component of type `T` for the current entity. @@ -1116,8 +1240,60 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get_mut>(&mut self) -> Option> { - // SAFETY: trait bound `Mutability = Mutable` ensures `T` is mutable - unsafe { self.get_mut_assume_mutable() } + self.as_mutable().into_mut() + } + + /// 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 + /// causing an archetype move. + /// + /// This is most useful with immutable components, where removal and reinsertion + /// is the only way to modify a value. + /// + /// If you do not need to ensure the above hooks are triggered, and your component + /// is mutable, prefer using [`get_mut`](EntityWorldMut::get_mut). + /// + /// # Examples + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component, PartialEq, Eq, Debug)] + /// #[component(immutable)] + /// struct Foo(bool); + /// + /// # let mut world = World::default(); + /// # world.register_component::(); + /// # + /// # let entity = world.spawn(Foo(false)).id(); + /// # + /// # let mut entity = world.entity_mut(entity); + /// # + /// # assert_eq!(entity.get::(), Some(&Foo(false))); + /// # + /// entity.modify_component(|foo: &mut Foo| { + /// foo.0 = true; + /// }); + /// # + /// # assert_eq!(entity.get::(), Some(&Foo(true))); + /// ``` + /// + /// # Panics + /// + /// If the entity has been despawned while this `EntityWorldMut` is still alive. + #[inline] + pub fn modify_component(&mut self, f: impl FnOnce(&mut T) -> R) -> Option { + self.assert_not_despawned(); + + let result = self + .world + .modify_component(self.entity, f) + .expect("entity access must be valid")?; + + self.update_location(); + + Some(result) } /// Gets mutable access to the component of type `T` for the current entity. @@ -1128,10 +1304,7 @@ impl<'w> EntityWorldMut<'w> { /// - `T` must be a mutable component #[inline] pub unsafe fn get_mut_assume_mutable(&mut self) -> Option> { - // SAFETY: - // - &mut self implies exclusive access for duration of returned value - // - caller ensures T is mutable - unsafe { self.as_unsafe_entity_cell().get_mut_assume_mutable() } + self.as_mutable().into_mut_assume_mutable() } /// Consumes `self` and gets mutable access to the component of type `T` @@ -1147,6 +1320,45 @@ impl<'w> EntityWorldMut<'w> { unsafe { self.into_unsafe_entity_cell().get_mut() } } + /// Gets a reference to the resource of the given type + /// + /// # Panics + /// + /// Panics if the resource does not exist. + /// Use [`get_resource`](EntityWorldMut::get_resource) instead if you want to handle this case. + #[inline] + #[track_caller] + pub fn resource(&self) -> &R { + self.world.resource::() + } + + /// Gets a mutable reference to the resource of the given type + /// + /// # Panics + /// + /// Panics if the resource does not exist. + /// Use [`get_resource_mut`](World::get_resource_mut) instead if you want to handle this case. + /// + /// If you want to instead insert a value if the resource does not exist, + /// use [`get_resource_or_insert_with`](World::get_resource_or_insert_with). + #[inline] + #[track_caller] + pub fn resource_mut(&mut self) -> Mut<'_, R> { + self.world.resource_mut::() + } + + /// Gets a reference to the resource of the given type if it exists + #[inline] + pub fn get_resource(&self) -> Option<&R> { + self.world.get_resource() + } + + /// Gets a mutable reference to the resource of the given type if it exists + #[inline] + pub fn get_resource_mut(&mut self) -> Option> { + self.world.get_resource_mut() + } + /// Retrieves the change ticks for the given component. This can be useful for implementing change /// detection in custom runtimes. /// @@ -1155,7 +1367,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get_change_ticks(&self) -> Option { - EntityRef::from(self).get_change_ticks::() + self.as_readonly().get_change_ticks::() } /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change @@ -1170,7 +1382,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { - EntityRef::from(self).get_change_ticks_by_id(component_id) + self.as_readonly().get_change_ticks_by_id(component_id) } /// Returns [untyped read-only reference(s)](Ptr) to component(s) for the @@ -1201,7 +1413,7 @@ impl<'w> EntityWorldMut<'w> { &self, component_ids: F, ) -> Result, EntityComponentError> { - EntityRef::from(self).get_by_id(component_ids) + self.as_readonly().get_by_id(component_ids) } /// Consumes `self` and returns [untyped read-only reference(s)](Ptr) to @@ -1233,10 +1445,7 @@ impl<'w> EntityWorldMut<'w> { self, component_ids: F, ) -> Result, EntityComponentError> { - // SAFETY: - // - We have read-only access to all components of this entity. - // - consuming `self` ensures that no references exist to this entity's components. - unsafe { component_ids.fetch_ref(self.into_unsafe_entity_cell()) } + self.into_readonly().get_by_id(component_ids) } /// Returns [untyped mutable reference(s)](MutUntyped) to component(s) for @@ -1269,10 +1478,7 @@ impl<'w> EntityWorldMut<'w> { &mut self, component_ids: F, ) -> Result, EntityComponentError> { - // SAFETY: - // - `&mut self` ensures that no references exist to this entity's components. - // - We have exclusive access to all components of this entity. - unsafe { component_ids.fetch_mut(self.as_unsafe_entity_cell()) } + self.as_mutable().into_mut_by_id(component_ids) } /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) @@ -1306,10 +1512,7 @@ impl<'w> EntityWorldMut<'w> { self, component_ids: F, ) -> Result, EntityComponentError> { - // SAFETY: - // - consuming `self` ensures that no references exist to this entity's components. - // - We have exclusive access to all components of this entity. - unsafe { component_ids.fetch_mut(self.into_unsafe_entity_cell()) } + self.into_mutable().into_mut_by_id(component_ids) } /// Adds a [`Bundle`] of components to the entity. @@ -1324,7 +1527,7 @@ impl<'w> EntityWorldMut<'w> { self.insert_with_caller( bundle, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -1342,7 +1545,7 @@ impl<'w> EntityWorldMut<'w> { self.insert_with_caller( bundle, InsertMode::Keep, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -1354,7 +1557,7 @@ impl<'w> EntityWorldMut<'w> { &mut self, bundle: T, mode: InsertMode, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) -> &mut Self { self.assert_not_despawned(); let change_tick = self.world.change_tick(); @@ -1363,7 +1566,7 @@ impl<'w> EntityWorldMut<'w> { self.location = // SAFETY: location matches current entity. `T` matches `bundle_info` unsafe { - bundle_inserter.insert(self.entity, self.location, bundle, mode, #[cfg(feature = "track_change_detection")] caller) + bundle_inserter.insert(self.entity, self.location, bundle, mode, #[cfg(feature = "track_location")] caller) }; self.world.flush(); self.update_location(); @@ -1553,7 +1756,10 @@ impl<'w> EntityWorldMut<'w> { }) }; - #[allow(clippy::undocumented_unsafe_blocks)] // TODO: document why this is safe + #[expect( + clippy::undocumented_unsafe_blocks, + reason = "Needs to be documented; see #17345." + )] unsafe { Self::move_entity_from_remove::( entity, @@ -1580,7 +1786,6 @@ impl<'w> EntityWorldMut<'w> { /// when DROP is true removed components will be dropped otherwise they will be forgotten // We use a const generic here so that we are less reliant on // inlining for rustc to optimize out the `match DROP` - #[allow(clippy::too_many_arguments)] unsafe fn move_entity_from_remove( entity: Entity, self_location: &mut EntityLocation, @@ -1660,7 +1865,6 @@ impl<'w> EntityWorldMut<'w> { /// /// # Safety /// - A `BundleInfo` with the corresponding `BundleId` must have been initialized. - #[allow(clippy::too_many_arguments)] unsafe fn remove_bundle(&mut self, bundle: BundleId) -> EntityLocation { let entity = self.entity; let world = &mut self.world; @@ -1897,14 +2101,14 @@ impl<'w> EntityWorldMut<'w> { #[track_caller] pub fn despawn(self) { self.despawn_with_caller( - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } pub(crate) fn despawn_with_caller( self, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { self.assert_not_despawned(); let world = self.world; @@ -1995,7 +2199,7 @@ impl<'w> EntityWorldMut<'w> { } world.flush(); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] { // SAFETY: No structural changes unsafe { @@ -2331,7 +2535,7 @@ impl<'w> EntityWorldMut<'w> { } /// Returns the source code location from which this entity has last been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { self.world() .entities() @@ -2362,8 +2566,6 @@ unsafe fn trigger_on_replace_and_on_remove_hooks_and_observers( deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_explicit_components()); } -const QUERY_MISMATCH_ERROR: &str = "Query does not match the current entity"; - /// A view into a single entity and component in a world, which may either be vacant or occupied. /// /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. @@ -2873,7 +3075,7 @@ impl<'w> FilteredEntityRef<'w> { } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { self.entity.spawned_by() } @@ -2881,19 +3083,19 @@ impl<'w> FilteredEntityRef<'w> { impl<'w> From> for FilteredEntityRef<'w> { #[inline] - fn from(entity_mut: FilteredEntityMut<'w>) -> Self { + fn from(entity: FilteredEntityMut<'w>) -> Self { // SAFETY: // - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`. - unsafe { FilteredEntityRef::new(entity_mut.entity, entity_mut.access) } + unsafe { FilteredEntityRef::new(entity.entity, entity.access) } } } impl<'a> From<&'a FilteredEntityMut<'_>> for FilteredEntityRef<'a> { #[inline] - fn from(entity_mut: &'a FilteredEntityMut<'_>) -> Self { + fn from(entity: &'a FilteredEntityMut<'_>) -> Self { // SAFETY: // - `FilteredEntityMut` guarantees exclusive access to all components in the new `FilteredEntityRef`. - unsafe { FilteredEntityRef::new(entity_mut.entity, entity_mut.access.clone()) } + unsafe { FilteredEntityRef::new(entity.entity, entity.access.clone()) } } } @@ -2904,7 +3106,7 @@ impl<'a> From> for FilteredEntityRef<'a> { unsafe { let mut access = Access::default(); access.read_all(); - FilteredEntityRef::new(entity.0, access) + FilteredEntityRef::new(entity.cell, access) } } } @@ -2916,7 +3118,7 @@ impl<'a> From<&'a EntityRef<'_>> for FilteredEntityRef<'a> { unsafe { let mut access = Access::default(); access.read_all(); - FilteredEntityRef::new(entity.0, access) + FilteredEntityRef::new(entity.cell, access) } } } @@ -2928,7 +3130,7 @@ impl<'a> From> for FilteredEntityRef<'a> { unsafe { let mut access = Access::default(); access.read_all(); - FilteredEntityRef::new(entity.0, access) + FilteredEntityRef::new(entity.cell, access) } } } @@ -2940,7 +3142,7 @@ impl<'a> From<&'a EntityMut<'_>> for FilteredEntityRef<'a> { unsafe { let mut access = Access::default(); access.read_all(); - FilteredEntityRef::new(entity.0, access) + FilteredEntityRef::new(entity.cell, access) } } } @@ -2969,6 +3171,43 @@ impl<'a> From<&'a EntityWorldMut<'_>> for FilteredEntityRef<'a> { } } +impl PartialEq for FilteredEntityRef<'_> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for FilteredEntityRef<'_> {} + +impl PartialOrd for FilteredEntityRef<'_> { + /// [`FilteredEntityRef`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FilteredEntityRef<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for FilteredEntityRef<'_> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for FilteredEntityRef<'_> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for FilteredEntityRef<'_> {} + /// Provides mutable access to a single entity and some of its components defined by the contained [`Access`]. /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), @@ -3200,7 +3439,7 @@ impl<'w> FilteredEntityMut<'w> { } /// Returns the source code location from which this entity has last been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { self.entity.spawned_by() } @@ -3214,7 +3453,7 @@ impl<'a> From> for FilteredEntityMut<'a> { let mut access = Access::default(); access.read_all(); access.write_all(); - FilteredEntityMut::new(entity.0, access) + FilteredEntityMut::new(entity.cell, access) } } } @@ -3227,7 +3466,7 @@ impl<'a> From<&'a mut EntityMut<'_>> for FilteredEntityMut<'a> { let mut access = Access::default(); access.read_all(); access.write_all(); - FilteredEntityMut::new(entity.0, access) + FilteredEntityMut::new(entity.cell, access) } } } @@ -3258,6 +3497,43 @@ impl<'a> From<&'a mut EntityWorldMut<'_>> for FilteredEntityMut<'a> { } } +impl PartialEq for FilteredEntityMut<'_> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for FilteredEntityMut<'_> {} + +impl PartialOrd for FilteredEntityMut<'_> { + /// [`FilteredEntityMut`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for FilteredEntityMut<'_> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for FilteredEntityMut<'_> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for FilteredEntityMut<'_> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for FilteredEntityMut<'_> {} + /// Error type returned by [`TryFrom`] conversions from filtered entity types /// ([`FilteredEntityRef`]/[`FilteredEntityMut`]) to full-access entity types /// ([`EntityRef`]/[`EntityMut`]). @@ -3275,7 +3551,6 @@ pub enum TryFromFilteredError { /// Provides read-only access to a single entity and all its components, save /// for an explicitly-enumerated set. -#[derive(Clone)] pub struct EntityRefExcept<'w, B> where B: Bundle, @@ -3344,7 +3619,7 @@ where } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { self.entity.spawned_by() } @@ -3354,13 +3629,58 @@ impl<'a, B> From<&'a EntityMutExcept<'_, B>> for EntityRefExcept<'a, B> where B: Bundle, { - fn from(entity_mut: &'a EntityMutExcept<'_, B>) -> Self { + fn from(entity: &'a EntityMutExcept<'_, B>) -> Self { // SAFETY: All accesses that `EntityRefExcept` provides are also // accesses that `EntityMutExcept` provides. - unsafe { EntityRefExcept::new(entity_mut.entity) } + unsafe { EntityRefExcept::new(entity.entity) } + } +} + +impl Clone for EntityRefExcept<'_, B> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for EntityRefExcept<'_, B> {} + +impl PartialEq for EntityRefExcept<'_, B> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for EntityRefExcept<'_, B> {} + +impl PartialOrd for EntityRefExcept<'_, B> { + /// [`EntityRefExcept`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) } } +impl Ord for EntityRefExcept<'_, B> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for EntityRefExcept<'_, B> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for EntityRefExcept<'_, B> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for EntityRefExcept<'_, B> {} + /// Provides mutable access to all components of an entity, with the exception /// of an explicit set. /// @@ -3369,7 +3689,6 @@ where /// queries that might match entities that this query also matches. If you don't /// need access to all components, prefer a standard query with a /// [`crate::query::Without`] filter. -#[derive(Clone)] pub struct EntityMutExcept<'w, B> where B: Bundle, @@ -3458,12 +3777,49 @@ where } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(&self) -> &'static Location<'static> { self.entity.spawned_by() } } +impl PartialEq for EntityMutExcept<'_, B> { + fn eq(&self, other: &Self) -> bool { + self.entity() == other.entity() + } +} + +impl Eq for EntityMutExcept<'_, B> {} + +impl PartialOrd for EntityMutExcept<'_, B> { + /// [`EntityMutExcept`]'s comparison trait implementations match the underlying [`Entity`], + /// and cannot discern between different worlds. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for EntityMutExcept<'_, B> { + fn cmp(&self, other: &Self) -> Ordering { + self.entity().cmp(&other.entity()) + } +} + +impl Hash for EntityMutExcept<'_, B> { + fn hash(&self, state: &mut H) { + self.entity().hash(state); + } +} + +impl EntityBorrow for EntityMutExcept<'_, B> { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: This type represents one Entity. We implement the comparison traits based on that Entity. +unsafe impl TrustedEntityBorrow for EntityMutExcept<'_, B> {} + fn bundle_contains_component(components: &Components, query_id: ComponentId) -> bool where B: Bundle, @@ -3519,7 +3875,7 @@ unsafe fn insert_dynamic_bundle< location, bundle, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -3820,11 +4176,13 @@ unsafe impl DynamicComponentFetch for &'_ HashSet { #[cfg(test)] mod tests { + use alloc::{vec, vec::Vec}; use bevy_ptr::{OwningPtr, Ptr}; use core::panic::AssertUnwindSafe; - #[cfg(feature = "track_change_detection")] + + #[cfg(feature = "track_location")] use core::panic::Location; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] use std::sync::OnceLock; use crate::{ @@ -5106,7 +5464,7 @@ mod tests { } #[test] - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] fn update_despawned_by_after_observers() { let mut world = World::new(); @@ -5155,4 +5513,87 @@ mod tests { .unwrap() ); } + + #[test] + fn with_component_activates_hooks() { + use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + + #[derive(Component, PartialEq, Eq, Debug)] + #[component(immutable)] + struct Foo(bool); + + static EXPECTED_VALUE: AtomicBool = AtomicBool::new(false); + + static ADD_COUNT: AtomicU8 = AtomicU8::new(0); + static REMOVE_COUNT: AtomicU8 = AtomicU8::new(0); + static REPLACE_COUNT: AtomicU8 = AtomicU8::new(0); + static INSERT_COUNT: AtomicU8 = AtomicU8::new(0); + + let mut world = World::default(); + + world.register_component::(); + world + .register_component_hooks::() + .on_add(|world, entity, _| { + ADD_COUNT.fetch_add(1, Ordering::Relaxed); + + assert_eq!( + world.get(entity), + Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed))) + ); + }) + .on_remove(|world, entity, _| { + REMOVE_COUNT.fetch_add(1, Ordering::Relaxed); + + assert_eq!( + world.get(entity), + Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed))) + ); + }) + .on_replace(|world, entity, _| { + REPLACE_COUNT.fetch_add(1, Ordering::Relaxed); + + assert_eq!( + world.get(entity), + Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed))) + ); + }) + .on_insert(|world, entity, _| { + INSERT_COUNT.fetch_add(1, Ordering::Relaxed); + + assert_eq!( + world.get(entity), + Some(&Foo(EXPECTED_VALUE.load(Ordering::Relaxed))) + ); + }); + + let entity = world.spawn(Foo(false)).id(); + + assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(REMOVE_COUNT.load(Ordering::Relaxed), 0); + assert_eq!(REPLACE_COUNT.load(Ordering::Relaxed), 0); + assert_eq!(INSERT_COUNT.load(Ordering::Relaxed), 1); + + let mut entity = world.entity_mut(entity); + + let archetype_pointer_before = &raw const *entity.archetype(); + + assert_eq!(entity.get::(), Some(&Foo(false))); + + entity.modify_component(|foo: &mut Foo| { + foo.0 = true; + EXPECTED_VALUE.store(foo.0, Ordering::Relaxed); + }); + + let archetype_pointer_after = &raw const *entity.archetype(); + + assert_eq!(entity.get::(), Some(&Foo(true))); + + assert_eq!(ADD_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(REMOVE_COUNT.load(Ordering::Relaxed), 0); + assert_eq!(REPLACE_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(INSERT_COUNT.load(Ordering::Relaxed), 2); + + assert_eq!(archetype_pointer_before, archetype_pointer_after); + } } diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 7f137fa012fee..1c6b5043bcea8 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -2,9 +2,11 @@ use thiserror::Error; -use crate::{component::ComponentId, entity::Entity, schedule::InternedScheduleLabel}; - -use super::unsafe_world_cell::UnsafeWorldCell; +use crate::{ + component::ComponentId, + entity::{Entity, EntityDoesNotExistDetails}, + schedule::InternedScheduleLabel, +}; /// The error type returned by [`World::try_run_schedule`] if the provided schedule does not exist. /// @@ -25,53 +27,17 @@ pub enum EntityComponentError { } /// An error that occurs when fetching entities mutably from a world. -#[derive(Clone, Copy)] -pub enum EntityFetchError<'w> { +#[derive(Error, Debug, Clone, Copy)] +pub enum EntityFetchError { /// The entity with the given ID does not exist. - NoSuchEntity(Entity, UnsafeWorldCell<'w>), + #[error("The entity with ID {0} {1}")] + NoSuchEntity(Entity, EntityDoesNotExistDetails), /// The entity with the given ID was requested mutably more than once. + #[error("The entity with ID {0} was requested mutably more than once")] AliasedMutability(Entity), } -impl<'w> core::error::Error for EntityFetchError<'w> {} - -impl<'w> core::fmt::Display for EntityFetchError<'w> { - fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - match *self { - Self::NoSuchEntity(entity, world) => { - write!( - f, - "Entity {entity} {}", - world - .entities() - .entity_does_not_exist_error_details_message(entity) - ) - } - Self::AliasedMutability(entity) => { - write!(f, "Entity {entity} was requested mutably more than once") - } - } - } -} - -impl<'w> core::fmt::Debug for EntityFetchError<'w> { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match *self { - Self::NoSuchEntity(entity, world) => { - write!( - f, - "NoSuchEntity({entity} {})", - world - .entities() - .entity_does_not_exist_error_details_message(entity) - ) - } - Self::AliasedMutability(entity) => write!(f, "AliasedMutability({entity})"), - } - } -} - -impl<'w> PartialEq for EntityFetchError<'w> { +impl PartialEq for EntityFetchError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::NoSuchEntity(e1, _), Self::NoSuchEntity(e2, _)) if e1 == e2 => true, @@ -81,4 +47,4 @@ impl<'w> PartialEq for EntityFetchError<'w> { } } -impl<'w> Eq for EntityFetchError<'w> {} +impl Eq for EntityFetchError {} diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index 66eac2fdb9f95..b2fe54478b6bb 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -6,7 +6,7 @@ use crate::{ world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_ptr::Ptr; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use bevy_ptr::UnsafeCellDeref; /// Provides read-only access to a set of [`Resource`]s defined by the contained [`Access`]. @@ -165,7 +165,7 @@ impl<'w, 's> FilteredResources<'w, 's> { value: unsafe { value.deref() }, // SAFETY: We have read access to the resource, so no mutable reference can exist. ticks: unsafe { Ticks::from_tick_cells(ticks, self.last_run, self.this_run) }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: We have read access to the resource, so no mutable reference can exist. changed_by: unsafe { _caller.deref() }, }, @@ -483,7 +483,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { value: unsafe { value.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. ticks: unsafe { TicksMut::from_tick_cells(ticks, self.last_run, self.this_run) }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: We have exclusive access to the underlying storage. changed_by: unsafe { _caller.deref_mut() }, }, diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index b1342e04dcc35..4ab38a2cdbb93 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -99,6 +99,7 @@ impl SparseSetIndex for WorldId { #[cfg(test)] mod tests { use super::*; + use alloc::vec::Vec; #[test] fn world_ids_unique() { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 997d9fcee7bd0..4b8be6cc1f3e1 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -43,6 +43,7 @@ use crate::{ observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, removal_detection::RemovedComponentEvents, + result::Result, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::{Commands, Resource}, @@ -62,49 +63,13 @@ use core::sync::atomic::{AtomicU32, Ordering}; #[cfg(feature = "portable-atomic")] use portable_atomic::{AtomicU32, Ordering}; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use bevy_ptr::UnsafeCellDeref; use core::panic::Location; use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; -/// A [`World`] mutation. -/// -/// Should be used with [`Commands::queue`]. -/// -/// # Usage -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::world::Command; -/// // Our world resource -/// #[derive(Resource, Default)] -/// struct Counter(u64); -/// -/// // Our custom command -/// struct AddToCounter(u64); -/// -/// impl Command for AddToCounter { -/// fn apply(self, world: &mut World) { -/// let mut counter = world.get_resource_or_insert_with(Counter::default); -/// counter.0 += self.0; -/// } -/// } -/// -/// fn some_system(mut commands: Commands) { -/// commands.queue(AddToCounter(42)); -/// } -/// ``` -pub trait Command: Send + 'static { - /// Applies this command, causing it to mutate the provided `world`. - /// - /// This method is used to define what a command "does" when it is ultimately applied. - /// Because this method takes `self`, you can store data or settings on the type that implements this trait. - /// This data is set by the system or other source of the command, and then ultimately read in this method. - fn apply(self, world: &mut World); -} - /// Stores and exposes operations on [entities](Entity), [components](Component), resources, /// and their associated metadata. /// @@ -182,10 +147,18 @@ impl World { /// This _must_ be run as part of constructing a [`World`], before it is returned to the caller. #[inline] fn bootstrap(&mut self) { - assert_eq!(ON_ADD, self.register_component::()); - assert_eq!(ON_INSERT, self.register_component::()); - assert_eq!(ON_REPLACE, self.register_component::()); - assert_eq!(ON_REMOVE, self.register_component::()); + // 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_insert = OnInsert::register_component_id(self); + assert_eq!(ON_INSERT, on_insert); + + let on_replace = OnReplace::register_component_id(self); + assert_eq!(ON_REPLACE, on_replace); + + let on_remove = OnRemove::register_component_id(self); + assert_eq!(ON_REMOVE, on_remove); } /// Creates a new empty [`World`]. /// @@ -612,8 +585,6 @@ impl World { /// - Pass an [`Entity`] to receive a single [`EntityRef`]. /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityRef`]s. - /// - Pass a reference to a [`EntityHashSet`] to receive an - /// [`EntityHashMap`](crate::entity::EntityHashMap). /// /// # Panics /// @@ -710,10 +681,8 @@ impl World { #[track_caller] fn panic_no_entity(world: &World, entity: Entity) -> ! { panic!( - "Entity {entity:?} {}", - world - .entities - .entity_does_not_exist_error_details_message(entity) + "Entity {entity} {}", + world.entities.entity_does_not_exist_error_details(entity) ); } @@ -862,7 +831,7 @@ impl World { let entity_location = self .entities() .get(entity) - .unwrap_or_else(|| panic!("Entity {entity:?} does not exist")); + .unwrap_or_else(|| panic!("Entity {entity} does not exist")); let archetype = self .archetypes() @@ -879,49 +848,6 @@ impl World { .filter_map(|id| self.components().get_info(id)) } - /// Returns an [`EntityWorldMut`] for the given `entity` (if it exists) or spawns one if it doesn't exist. - /// This will return [`None`] if the `entity` exists with a different generation. - /// - /// # Note - /// Spawning a specific `entity` value is rarely the right choice. Most apps should favor [`World::spawn`]. - /// This method should generally only be used for sharing entities across apps, and only when they have a - /// scheme worked out to share an ID space (which doesn't happen by default). - #[inline] - #[deprecated(since = "0.15.0", note = "use `World::spawn` instead")] - pub fn get_or_spawn(&mut self, entity: Entity) -> Option { - self.get_or_spawn_with_caller( - entity, - #[cfg(feature = "track_change_detection")] - Location::caller(), - ) - } - - #[inline] - pub(crate) fn get_or_spawn_with_caller( - &mut self, - entity: Entity, - #[cfg(feature = "track_change_detection")] caller: &'static Location, - ) -> Option { - self.flush(); - match self.entities.alloc_at_without_replacement(entity) { - AllocAtWithoutReplacement::Exists(location) => { - // SAFETY: `entity` exists and `location` is that entity's location - Some(unsafe { EntityWorldMut::new(self, entity, location) }) - } - AllocAtWithoutReplacement::DidNotExist => { - // SAFETY: entity was just allocated - Some(unsafe { - self.spawn_at_empty_internal( - entity, - #[cfg(feature = "track_change_detection")] - caller, - ) - }) - } - AllocAtWithoutReplacement::ExistsWithWrongGeneration => None, - } - } - /// Returns [`EntityRef`]s that expose read-only operations for the given /// `entities`, returning [`Err`] if any of the given entities do not exist. /// Instead of immediately unwrapping the value returned from this function, @@ -1083,7 +1009,7 @@ impl World { unsafe { self.spawn_at_empty_internal( entity, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -1161,13 +1087,13 @@ impl World { bundle_spawner.spawn_non_existent( entity, bundle, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } }; - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.entities .set_spawned_or_despawned_by(entity.index(), Location::caller()); @@ -1180,7 +1106,7 @@ impl World { unsafe fn spawn_at_empty_internal( &mut self, entity: Entity, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) -> EntityWorldMut { let archetype = self.archetypes.empty_mut(); // PERF: consider avoiding allocating entities in the empty archetype unless needed @@ -1190,7 +1116,7 @@ impl World { let location = unsafe { archetype.allocate(entity, table_row) }; self.entities.set(entity.index(), location); - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.entities .set_spawned_or_despawned_by(entity.index(), caller); @@ -1228,7 +1154,7 @@ impl World { SpawnBatchIter::new( self, iter.into_iter(), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -1275,11 +1201,63 @@ impl World { &mut self, entity: Entity, ) -> Option> { - // SAFETY: - // - `as_unsafe_world_cell` is the only thing that is borrowing world - // - `as_unsafe_world_cell` provides mutable permission to everything - // - `&mut self` ensures no other borrows on world data - unsafe { self.as_unsafe_world_cell().get_entity(entity)?.get_mut() } + self.get_entity_mut(entity).ok()?.into_mut() + } + + /// 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 + /// causing an archetype move. + /// + /// This is most useful with immutable components, where removal and reinsertion + /// is the only way to modify a value. + /// + /// If you do not need to ensure the above hooks are triggered, and your component + /// is mutable, prefer using [`get_mut`](World::get_mut). + /// + /// # Examples + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// # + /// #[derive(Component, PartialEq, Eq, Debug)] + /// #[component(immutable)] + /// struct Foo(bool); + /// + /// # let mut world = World::default(); + /// # world.register_component::(); + /// # + /// # let entity = world.spawn(Foo(false)).id(); + /// # + /// world.modify_component(entity, |foo: &mut Foo| { + /// foo.0 = true; + /// }); + /// # + /// # assert_eq!(world.get::(entity), Some(&Foo(true))); + /// ``` + #[inline] + pub fn modify_component( + &mut self, + entity: Entity, + f: impl FnOnce(&mut T) -> R, + ) -> Result, EntityFetchError> { + let mut world = DeferredWorld::from(&mut *self); + + let result = match world.modify_component(entity, f) { + Ok(result) => result, + Err(EntityFetchError::AliasedMutability(..)) => { + return Err(EntityFetchError::AliasedMutability(entity)) + } + Err(EntityFetchError::NoSuchEntity(..)) => { + return Err(EntityFetchError::NoSuchEntity( + entity, + self.entities().entity_does_not_exist_error_details(entity), + )) + } + }; + + self.flush(); + Ok(result) } /// Despawns the given `entity`, if it exists. This will also remove all of the entity's @@ -1330,13 +1308,13 @@ impl World { self.flush(); if let Ok(entity) = self.get_entity_mut(entity) { entity.despawn_with_caller( - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); true } else { if log_warning { - warn!("error[B0003]: {caller}: Could not despawn entity {entity:?}, which {}. See: https://bevyengine.org/learn/errors/b0003", self.entities.entity_does_not_exist_error_details_message(entity)); + warn!("error[B0003]: {caller}: Could not despawn entity {entity}, which {}. See: https://bevyengine.org/learn/errors/b0003", self.entities.entity_does_not_exist_error_details(entity)); } false } @@ -1603,14 +1581,14 @@ impl World { #[inline] #[track_caller] pub fn init_resource(&mut self) -> ComponentId { - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = Location::caller(); let component_id = self.components.register_resource::(); if self .storages .resources .get(component_id) - .map_or(true, |data| !data.is_present()) + .is_none_or(|data| !data.is_present()) { let value = R::from_world(self); OwningPtr::make(value, |ptr| { @@ -1619,7 +1597,7 @@ impl World { self.insert_resource_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1638,7 +1616,7 @@ impl World { pub fn insert_resource(&mut self, value: R) { self.insert_resource_with_caller( value, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } @@ -1649,7 +1627,7 @@ impl World { pub(crate) fn insert_resource_with_caller( &mut self, value: R, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { let component_id = self.components.register_resource::(); OwningPtr::make(value, |ptr| { @@ -1658,7 +1636,7 @@ impl World { self.insert_resource_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1679,14 +1657,14 @@ impl World { #[inline] #[track_caller] pub fn init_non_send_resource(&mut self) -> ComponentId { - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = Location::caller(); let component_id = self.components.register_non_send::(); if self .storages .non_send_resources .get(component_id) - .map_or(true, |data| !data.is_present()) + .is_none_or(|data| !data.is_present()) { let value = R::from_world(self); OwningPtr::make(value, |ptr| { @@ -1695,7 +1673,7 @@ impl World { self.insert_non_send_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1716,7 +1694,7 @@ impl World { #[inline] #[track_caller] pub fn insert_non_send_resource(&mut self, value: R) { - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = Location::caller(); let component_id = self.components.register_non_send::(); OwningPtr::make(value, |ptr| { @@ -1725,7 +1703,7 @@ impl World { self.insert_non_send_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -1824,12 +1802,11 @@ impl World { self.storages .resources .get(component_id) - .and_then(|resource| { - resource - .get_ticks() - .map(|ticks| ticks.is_added(self.last_change_tick(), self.read_change_tick())) + .is_some_and(|resource| { + resource.get_ticks().is_some_and(|ticks| { + ticks.is_added(self.last_change_tick(), self.read_change_tick()) + }) }) - .unwrap_or(false) } /// Returns `true` if a resource of type `R` exists and was modified since the world's @@ -1842,8 +1819,7 @@ impl World { pub fn is_resource_changed(&self) -> bool { self.components .get_resource_id(TypeId::of::()) - .map(|component_id| self.is_resource_changed_by_id(component_id)) - .unwrap_or(false) + .is_some_and(|component_id| self.is_resource_changed_by_id(component_id)) } /// Returns `true` if a resource with id `component_id` exists and was modified since the world's @@ -1857,12 +1833,11 @@ impl World { self.storages .resources .get(component_id) - .and_then(|resource| { - resource - .get_ticks() - .map(|ticks| ticks.is_changed(self.last_change_tick(), self.read_change_tick())) + .is_some_and(|resource| { + resource.get_ticks().is_some_and(|ticks| { + ticks.is_changed(self.last_change_tick(), self.read_change_tick()) + }) }) - .unwrap_or(false) } /// Retrieves the change ticks for the given resource. @@ -2005,7 +1980,7 @@ impl World { &mut self, func: impl FnOnce() -> R, ) -> Mut<'_, R> { - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = Location::caller(); let change_tick = self.change_tick(); let last_change_tick = self.last_change_tick(); @@ -2019,7 +1994,7 @@ impl World { data.insert( ptr, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -2069,7 +2044,7 @@ impl World { /// ``` #[track_caller] pub fn get_resource_or_init(&mut self) -> Mut<'_, R> { - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = Location::caller(); let change_tick = self.change_tick(); let last_change_tick = self.last_change_tick(); @@ -2079,7 +2054,7 @@ impl World { .storages .resources .get(component_id) - .map_or(true, |data| !data.is_present()) + .is_none_or(|data| !data.is_present()) { let value = R::from_world(self); OwningPtr::make(value, |ptr| { @@ -2088,7 +2063,7 @@ impl World { self.insert_resource_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -2219,7 +2194,7 @@ impl World { { self.insert_or_spawn_batch_with_caller( iter, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ) } @@ -2230,7 +2205,7 @@ impl World { pub(crate) fn insert_or_spawn_batch_with_caller( &mut self, iter: I, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) -> Result<(), Vec> where I: IntoIterator, @@ -2280,7 +2255,7 @@ impl World { location, bundle, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2302,7 +2277,7 @@ impl World { location, bundle, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2318,7 +2293,7 @@ impl World { spawner.spawn_non_existent( entity, bundle, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2331,7 +2306,7 @@ impl World { spawner.spawn_non_existent( entity, bundle, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2376,7 +2351,7 @@ impl World { self.insert_batch_with_caller( batch, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } @@ -2406,7 +2381,7 @@ impl World { self.insert_batch_with_caller( batch, InsertMode::Keep, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } @@ -2423,7 +2398,7 @@ impl World { &mut self, iter: I, insert_mode: InsertMode, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) where I: IntoIterator, I::IntoIter: Iterator, @@ -2465,7 +2440,7 @@ impl World { first_location, first_bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2493,16 +2468,16 @@ impl World { location, bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; } 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_message(entity)); + 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)); } } } 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_message(first_entity)); + 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)); } } } @@ -2530,7 +2505,7 @@ impl World { self.try_insert_batch_with_caller( batch, InsertMode::Replace, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } @@ -2557,7 +2532,7 @@ impl World { self.try_insert_batch_with_caller( batch, InsertMode::Keep, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] Location::caller(), ); } @@ -2574,7 +2549,7 @@ impl World { &mut self, iter: I, insert_mode: InsertMode, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) where I: IntoIterator, I::IntoIter: Iterator, @@ -2616,7 +2591,7 @@ impl World { first_location, first_bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2644,7 +2619,7 @@ impl World { location, bundle, insert_mode, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ) }; @@ -2716,7 +2691,7 @@ impl World { last_run: last_change_tick, this_run: change_tick, }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: &mut _caller, }; let result = f(self, value_mut); @@ -2732,7 +2707,7 @@ impl World { info.insert_with_ticks( ptr, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] _caller, ); }) @@ -2789,7 +2764,7 @@ impl World { &mut self, component_id: ComponentId, value: OwningPtr<'_>, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { let change_tick = self.change_tick(); @@ -2799,7 +2774,7 @@ impl World { resource.insert( value, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -2823,7 +2798,7 @@ impl World { &mut self, component_id: ComponentId, value: OwningPtr<'_>, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) { let change_tick = self.change_tick(); @@ -2833,7 +2808,7 @@ impl World { resource.insert( value, change_tick, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, ); } @@ -3412,7 +3387,7 @@ impl World { // - We iterate one resource at a time, and we let go of each `PtrMut` before getting the next one value: unsafe { ptr.assert_unique() }, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: // - We have exclusive access to the world, so no other code can be aliasing the `Ptr` // - We iterate one resource at a time, and we let go of each `PtrMut` before getting the next one @@ -3500,14 +3475,7 @@ impl World { /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] pub fn get_by_id(&self, entity: Entity, component_id: ComponentId) -> Option> { - // SAFETY: - // - `&self` ensures that all accessed data is not mutably aliased - // - `as_unsafe_world_cell_readonly` provides shared/readonly permission to the whole world - unsafe { - self.as_unsafe_world_cell_readonly() - .get_entity(entity)? - .get_by_id(component_id) - } + self.get_entity(entity).ok()?.get_by_id(component_id).ok() } /// Retrieves a mutable untyped reference to the given `entity`'s [`Component`] of the given [`ComponentId`]. @@ -3521,15 +3489,10 @@ impl World { entity: Entity, component_id: ComponentId, ) -> Option> { - // SAFETY: - // - `&mut self` ensures that all accessed data is unaliased - // - `as_unsafe_world_cell` provides mutable permission to the whole world - unsafe { - self.as_unsafe_world_cell() - .get_entity(entity)? - .get_mut_by_id(component_id) - .ok() - } + self.get_entity_mut(entity) + .ok()? + .into_mut_by_id(component_id) + .ok() } } @@ -3716,7 +3679,13 @@ mod tests { system::Resource, world::error::EntityFetchError, }; - use alloc::sync::Arc; + use alloc::{ + borrow::ToOwned, + string::{String, ToString}, + sync::Arc, + vec, + vec::Vec, + }; use bevy_ecs_macros::Component; use bevy_utils::{HashMap, HashSet}; use core::{ @@ -3724,7 +3693,7 @@ mod tests { panic, sync::atomic::{AtomicBool, AtomicU32, Ordering}, }; - use std::sync::Mutex; + use std::{println, sync::Mutex}; // For bevy_ecs_macros use crate as bevy_ecs; @@ -3967,7 +3936,7 @@ mod tests { world.insert_resource_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] panic::Location::caller(), ); } @@ -4015,7 +3984,7 @@ mod tests { world.insert_resource_by_id( component_id, ptr, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] panic::Location::caller(), ); } @@ -4357,4 +4326,34 @@ mod tests { .map(|_| {}), Err(EntityFetchError::NoSuchEntity(e, ..)) if e == e1)); } + + #[cfg(feature = "track_location")] + #[test] + #[track_caller] + fn entity_spawn_despawn_tracking() { + use core::panic::Location; + + let mut world = World::new(); + let entity = world.spawn_empty().id(); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_by(entity), + Some(Location::caller()) + ); + world.despawn(entity); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_by(entity), + Some(Location::caller()) + ); + let new = world.spawn_empty().id(); + assert_eq!(entity.index(), new.index()); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_by(entity), + None + ); + world.despawn(new); + assert_eq!( + world.entities.entity_get_spawned_or_despawned_by(entity), + None + ); + } } diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index afa2015f4ef38..882cf9b74a78a 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -213,7 +213,7 @@ pub enum GetComponentReflectError { NoCorrespondingComponentId(TypeId), /// The given [`Entity`] does not have a [`Component`] corresponding to the given [`TypeId`]. - #[error("The given `Entity` {entity:?} does not have a `{component_name:?}` component ({component_id:?}, which corresponds to {type_id:?})")] + #[error("The given `Entity` {entity} does not have a `{component_name:?}` component ({component_id:?}, which corresponds to {type_id:?})")] EntityDoesNotHaveComponent { /// The given [`Entity`]. entity: Entity, diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index 6be86136953c3..eaa8cf7b9c889 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -4,7 +4,7 @@ use crate::{ world::World, }; use core::iter::FusedIterator; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; /// An iterator that spawns a series of entities and returns the [ID](Entity) of @@ -18,7 +18,7 @@ where { inner: I, spawner: BundleSpawner<'w>, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller: &'static Location<'static>, } @@ -32,7 +32,7 @@ where pub(crate) fn new( world: &'w mut World, iter: I, - #[cfg(feature = "track_change_detection")] caller: &'static Location, + #[cfg(feature = "track_location")] caller: &'static Location, ) -> Self { // Ensure all entity allocations are accounted for so `self.entities` can realloc if // necessary @@ -50,7 +50,7 @@ where Self { inner: iter, spawner, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] caller, } } @@ -83,7 +83,7 @@ where unsafe { Some(self.spawner.spawn( bundle, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] self.caller, )) } diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 82593961c51c8..94d36a3dc17fd 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -1,14 +1,12 @@ //! Contains types that allow disjoint mutable access to a [`World`]. -#![warn(unsafe_op_in_unsafe_fn)] - use super::{Mut, Ref, World, WorldId}; use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, change_detection::{MaybeUnsafeCellLocation, MutUntyped, Ticks, TicksMut}, component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, - entity::{Entities, Entity, EntityLocation}, + entity::{Entities, Entity, EntityBorrow, EntityLocation}, observer::Observers, prelude::Component, query::{DebugCheckedUnwrap, ReadOnlyQueryData}, @@ -18,9 +16,9 @@ use crate::{ world::RawCommandQueue, }; use bevy_ptr::Ptr; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use bevy_ptr::UnsafeCellDeref; -#[cfg(feature = "track_change_detection")] +#[cfg(feature = "track_location")] use core::panic::Location; use core::{any::TypeId, cell::UnsafeCell, fmt::Debug, marker::PhantomData, ptr}; use thiserror::Error; @@ -377,13 +375,13 @@ impl<'w> UnsafeWorldCell<'w> { unsafe { Ticks::from_tick_cells(ticks, self.last_change_tick(), self.change_tick()) }; // SAFETY: caller ensures that no mutable reference to the resource exists - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] let caller = unsafe { _caller.deref() }; Some(Ref { value, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: caller, }) } @@ -506,7 +504,7 @@ impl<'w> UnsafeWorldCell<'w> { // - caller ensures that the resource is unaliased value: unsafe { ptr.assert_unique() }, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: // - caller ensures that `self` has permission to access the resource // - caller ensures that the resource is unaliased @@ -570,7 +568,7 @@ impl<'w> UnsafeWorldCell<'w> { // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. value: unsafe { ptr.assert_unique() }, ticks, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] // SAFETY: This function has exclusive access to the world changed_by: unsafe { _caller.deref_mut() }, }) @@ -784,7 +782,7 @@ impl<'w> UnsafeEntityCell<'w> { // SAFETY: returned component is of type T value: value.deref::(), ticks: Ticks::from_tick_cells(cells, last_change_tick, change_tick), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref(), }) } @@ -904,7 +902,7 @@ impl<'w> UnsafeEntityCell<'w> { // SAFETY: returned component is of type T value: value.assert_unique().deref_mut::(), ticks: TicksMut::from_tick_cells(cells, last_change_tick, change_tick), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref_mut(), }) } @@ -1030,7 +1028,7 @@ impl<'w> UnsafeEntityCell<'w> { self.world.last_change_tick(), self.world.change_tick(), ), - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] changed_by: _caller.deref_mut(), }) .ok_or(GetEntityMutByIdError::ComponentNotFound) @@ -1038,7 +1036,7 @@ impl<'w> UnsafeEntityCell<'w> { } /// Returns the source code location from which this entity has been spawned. - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] pub fn spawned_by(self) -> &'static Location<'static> { self.world() .entities() @@ -1095,7 +1093,6 @@ impl<'w> UnsafeWorldCell<'w> { /// - `storage_type` must accurately reflect where the components for `component_id` are stored. /// - the caller must ensure that no aliasing rules are violated #[inline] -#[allow(unsafe_op_in_unsafe_fn)] unsafe fn get_component( world: UnsafeWorldCell<'_>, component_id: ComponentId, @@ -1122,7 +1119,6 @@ unsafe fn get_component( /// - `storage_type` must accurately reflect where the components for `component_id` are stored. /// - the caller must ensure that no aliasing rules are violated #[inline] -#[allow(unsafe_op_in_unsafe_fn)] unsafe fn get_component_and_ticks( world: UnsafeWorldCell<'_>, component_id: ComponentId, @@ -1145,11 +1141,11 @@ unsafe fn get_component_and_ticks( .get_changed_tick(component_id, location.table_row) .debug_checked_unwrap(), }, - #[cfg(feature = "track_change_detection")] + #[cfg(feature = "track_location")] table .get_changed_by(component_id, location.table_row) .debug_checked_unwrap(), - #[cfg(not(feature = "track_change_detection"))] + #[cfg(not(feature = "track_location"))] (), )) } @@ -1166,7 +1162,6 @@ unsafe fn get_component_and_ticks( /// - `storage_type` must accurately reflect where the components for `component_id` are stored. /// - the caller must ensure that no aliasing rules are violated #[inline] -#[allow(unsafe_op_in_unsafe_fn)] unsafe fn get_ticks( world: UnsafeWorldCell<'_>, component_id: ComponentId, @@ -1183,3 +1178,9 @@ unsafe fn get_ticks( StorageType::SparseSet => world.fetch_sparse_set(component_id)?.get_ticks(entity), } } + +impl EntityBorrow for UnsafeEntityCell<'_> { + fn entity(&self) -> Entity { + self.id() + } +} diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index 9184bddd25418..f35c44db3d43c 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_encase_derive" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Bevy derive macro for encase" homepage = "https://bevyengine.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.16.0-dev" } encase_derive_impl = "0.10" [lints] diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index cd20696e77ba3..65e25d5c90837 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gilrs" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Gamepad system made using Gilrs for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,15 +10,16 @@ keywords = ["bevy"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } +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" } # other gilrs = "0.11.0" thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index f9e9bc8dfd176..b8d83adbf3592 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -18,10 +18,11 @@ use bevy_app::{App, Plugin, PostUpdate, PreStartup, PreUpdate}; use bevy_ecs::entity::EntityHashMap; use bevy_ecs::prelude::*; use bevy_input::InputSystem; -use bevy_utils::{synccell::SyncCell, tracing::error, HashMap}; +use bevy_utils::{synccell::SyncCell, HashMap}; use gilrs::GilrsBuilder; use gilrs_system::{gilrs_event_startup_system, gilrs_event_system}; use rumble::{play_gilrs_rumble, RunningRumbleEffects}; +use tracing::error; #[cfg_attr(not(target_arch = "wasm32"), derive(Resource))] pub(crate) struct Gilrs(pub SyncCell); diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 62c6b0dc7d639..e0fdff62d8cf2 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -5,16 +5,14 @@ use bevy_ecs::prelude::{EventReader, Res, ResMut, Resource}; use bevy_ecs::system::NonSendMut; use bevy_input::gamepad::{GamepadRumbleIntensity, GamepadRumbleRequest}; use bevy_time::{Real, Time}; -use bevy_utils::{ - synccell::SyncCell, - tracing::{debug, warn}, - Duration, HashMap, -}; +use bevy_utils::{synccell::SyncCell, HashMap}; +use core::time::Duration; use gilrs::{ ff::{self, BaseEffect, BaseEffectType, Repeat, Replay}, GamepadId, }; use thiserror::Error; +use tracing::{debug, warn}; /// A rumble effect that is currently in effect. struct RunningRumble { @@ -23,7 +21,10 @@ struct RunningRumble { /// A ref-counted handle to the specific force-feedback effect /// /// Dropping it will cause the effect to stop - #[allow(dead_code)] + #[expect( + dead_code, + reason = "We don't need to read this field, as its purpose is to keep the rumble effect going until the field is dropped." + )] effect: SyncCell, } diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index e880e985fff52..6e51c609442a7 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides gizmos for Bevy Engine" homepage = "https://bevyengine.org" @@ -15,23 +15,25 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] [dependencies] # Bevy -bevy_pbr = { path = "../bevy_pbr", version = "0.15.0-dev", optional = true } -bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev", optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_gizmos_macros = { path = "macros", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } +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" } +# other bytemuck = "1.0" +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index 97aebb4d894ba..3862914d7218c 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gizmos_macros" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Derive implementations for bevy_gizmos" homepage = "https://bevyengine.org" @@ -13,7 +13,7 @@ proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } syn = "2.0" proc-macro2 = "1.0" diff --git a/crates/bevy_gizmos/src/curves.rs b/crates/bevy_gizmos/src/curves.rs index 522bf8ebd5246..2d7a350ca29dc 100644 --- a/crates/bevy_gizmos/src/curves.rs +++ b/crates/bevy_gizmos/src/curves.rs @@ -4,7 +4,10 @@ //! [`GizmoBuffer::curve_3d`] and assorted support items. use bevy_color::Color; -use bevy_math::{curve::Curve, Vec2, Vec3}; +use bevy_math::{ + curve::{Curve, CurveExt}, + Vec2, Vec3, +}; use crate::{gizmos::GizmoBuffer, prelude::GizmoConfigGroup}; diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 3580b41b61f43..04f5404c8d20a 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -182,7 +182,10 @@ where state: as SystemParam>::State, } -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "We cannot implement SystemParam without using unsafe code." +)] // SAFETY: All methods are delegated to existing `SystemParam` implementations unsafe impl SystemParam for Gizmos<'_, '_, Config, Clear> where @@ -254,7 +257,10 @@ where } } -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "We cannot implement ReadOnlySystemParam without using unsafe code." +)] // Safety: Each field is `ReadOnlySystemParam`, and Gizmos SystemParam does not mutate world unsafe impl<'w, 's, Config, Clear> ReadOnlySystemParam for Gizmos<'w, 's, Config, Clear> where diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index 03ee5c665445c..42742e196c77c 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -347,7 +347,6 @@ where } } -#[allow(clippy::too_many_arguments)] fn draw_grid( gizmos: &mut GizmoBuffer, isometry: Isometry3d, diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 242f19bc6bd5d..a7bca0769a352 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -189,16 +189,16 @@ impl Plugin for GizmoPlugin { if app.is_plugin_added::() { app.add_plugins(pipeline_2d::LineGizmo2dPlugin); } else { - bevy_utils::tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); + tracing::warn!("bevy_sprite feature is enabled but bevy_sprite::SpritePlugin was not detected. Are you sure you loaded GizmoPlugin after SpritePlugin?"); } #[cfg(feature = "bevy_pbr")] if app.is_plugin_added::() { app.add_plugins(pipeline_3d::LineGizmo3dPlugin); } else { - bevy_utils::tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); + tracing::warn!("bevy_pbr feature is enabled but bevy_pbr::PbrPlugin was not detected. Are you sure you loaded GizmoPlugin after PbrPlugin?"); } } else { - bevy_utils::tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); + tracing::warn!("bevy_render feature is enabled but RenderApp was not detected. Are you sure you loaded GizmoPlugin after RenderPlugin?"); } } @@ -419,8 +419,9 @@ fn extract_gizmo_data( handles: Extract>, config: Extract>, ) { - use bevy_utils::warn_once; + use bevy_utils::once; use config::GizmoLineStyle; + use tracing::warn; for (group_type_id, handle) in &handles.handles { let Some((config, _)) = config.get_config_dyn(group_type_id) else { @@ -447,10 +448,10 @@ fn extract_gizmo_data( } = config.line.style { if gap_scale <= 0.0 { - warn_once!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero."); + once!(warn!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero.")); } if line_scale <= 0.0 { - warn_once!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero."); + once!(warn!("When using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero.")); } (gap_scale, line_scale) } else { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 89d6cec6260b6..13c9b89dd98ff 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -27,7 +27,7 @@ use bevy_render::{ Render, RenderApp, RenderSet, }; use bevy_sprite::{Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup}; -use bevy_utils::tracing::error; +use tracing::error; pub struct LineGizmo2dPlugin; @@ -142,8 +142,8 @@ impl SpecializedRenderPipeline for LineGizmoPipeline { primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::GreaterEqual, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, stencil: StencilState { front: StencilFaceState::IGNORE, back: StencilFaceState::IGNORE, @@ -243,8 +243,8 @@ impl SpecializedRenderPipeline for LineJointGizmoPipeline { primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_2D_DEPTH_FORMAT, - depth_write_enabled: true, - depth_compare: CompareFunction::GreaterEqual, + depth_write_enabled: false, + depth_compare: CompareFunction::Always, stencil: StencilState { front: StencilFaceState::IGNORE, back: StencilFaceState::IGNORE, @@ -288,7 +288,6 @@ type DrawLineJointGizmo2d = ( DrawLineJointGizmo, ); -#[allow(clippy::too_many_arguments)] fn queue_line_gizmos_2d( draw_functions: Res>, pipeline: Res, @@ -297,7 +296,7 @@ fn queue_line_gizmos_2d( line_gizmos: Query<(Entity, &MainEntity, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, &Msaa, Option<&RenderLayers>)>, + mut views: Query<(&ExtractedView, &Msaa, Option<&RenderLayers>)>, ) { let draw_function = draw_functions.read().get_id::().unwrap(); let draw_function_strip = draw_functions @@ -305,8 +304,9 @@ fn queue_line_gizmos_2d( .get_id::() .unwrap(); - for (view_entity, view, msaa, render_layers) in &mut views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + for (view, msaa, render_layers) in &mut views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -340,6 +340,7 @@ fn queue_line_gizmos_2d( sort_key: FloatOrd(f32::INFINITY), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: false, }); } @@ -360,13 +361,12 @@ fn queue_line_gizmos_2d( sort_key: FloatOrd(f32::INFINITY), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: false, }); } } } } - -#[allow(clippy::too_many_arguments)] fn queue_line_joint_gizmos_2d( draw_functions: Res>, pipeline: Res, @@ -375,15 +375,16 @@ fn queue_line_joint_gizmos_2d( line_gizmos: Query<(Entity, &MainEntity, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<(Entity, &ExtractedView, &Msaa, Option<&RenderLayers>)>, + mut views: Query<(&ExtractedView, &Msaa, Option<&RenderLayers>)>, ) { let draw_function = draw_functions .read() .get_id::() .unwrap(); - for (view_entity, view, msaa, render_layers) in &mut views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + for (view, msaa, render_layers) in &mut views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -419,6 +420,7 @@ fn queue_line_joint_gizmos_2d( sort_key: FloatOrd(f32::INFINITY), batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: false, }); } } diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 025cc4c7c033b..aac6358d638bf 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -30,7 +30,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, RenderLayers, ViewTarget}, Render, RenderApp, RenderSet, }; -use bevy_utils::tracing::error; +use tracing::error; pub struct LineGizmo3dPlugin; impl Plugin for LineGizmo3dPlugin { @@ -283,7 +283,6 @@ type DrawLineJointGizmo3d = ( DrawLineJointGizmo, ); -#[allow(clippy::too_many_arguments)] fn queue_line_gizmos_3d( draw_functions: Res>, pipeline: Res, @@ -292,8 +291,7 @@ fn queue_line_gizmos_3d( line_gizmos: Query<(Entity, &MainEntity, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<( - Entity, + views: Query<( &ExtractedView, &Msaa, Option<&RenderLayers>, @@ -312,14 +310,14 @@ fn queue_line_gizmos_3d( .unwrap(); for ( - view_entity, view, msaa, render_layers, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), - ) in &mut views + ) in &views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -371,6 +369,7 @@ fn queue_line_gizmos_3d( distance: 0., batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: true, }); } @@ -392,13 +391,13 @@ fn queue_line_gizmos_3d( distance: 0., batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: true, }); } } } } -#[allow(clippy::too_many_arguments)] fn queue_line_joint_gizmos_3d( draw_functions: Res>, pipeline: Res, @@ -407,8 +406,7 @@ fn queue_line_joint_gizmos_3d( line_gizmos: Query<(Entity, &MainEntity, &GizmoMeshConfig)>, line_gizmo_assets: Res>, mut transparent_render_phases: ResMut>, - mut views: Query<( - Entity, + views: Query<( &ExtractedView, &Msaa, Option<&RenderLayers>, @@ -426,14 +424,14 @@ fn queue_line_joint_gizmos_3d( .unwrap(); for ( - view_entity, view, msaa, render_layers, (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), - ) in &mut views + ) in &views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -488,6 +486,7 @@ fn queue_line_joint_gizmos_3d( distance: 0., batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: true, }); } } diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 1af21869a9781..b99ab9a31a793 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -413,7 +413,7 @@ where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, { - /// Set the number of lines used to approximate the top an bottom of the cylinder geometry. + /// Set the number of lines used to approximate the top and bottom of the cylinder geometry. pub fn resolution(mut self, resolution: u32) -> Self { self.resolution = resolution; self diff --git a/crates/bevy_gizmos/src/primitives/helpers.rs b/crates/bevy_gizmos/src/primitives/helpers.rs index f6cdfcf0d3cd4..37253b14a9ac9 100644 --- a/crates/bevy_gizmos/src/primitives/helpers.rs +++ b/crates/bevy_gizmos/src/primitives/helpers.rs @@ -15,7 +15,7 @@ pub(crate) fn single_circle_coordinate(radius: f32, resolution: u32, nth_point: /// Generates an iterator over the coordinates of a circle. /// -/// The coordinates form a open circle, meaning the first and last points aren't the same. +/// The coordinates form an open circle, meaning the first and last points aren't the same. /// /// This function creates an iterator that yields the positions of points approximating a /// circle with the given radius, divided into linear segments. The iterator produces `resolution` diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 9cb6791aca182..435f417552463 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -106,7 +106,8 @@ pub(crate) fn extract_linegizmos( ) { use bevy_math::Affine3; use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; - use bevy_utils::warn_once; + use bevy_utils::once; + use tracing::warn; use crate::config::GizmoLineStyle; @@ -124,10 +125,10 @@ pub(crate) fn extract_linegizmos( } = gizmo.line_config.style { if gap_scale <= 0.0 { - warn_once!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero"); + once!(warn!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the gap scale should be greater than zero")); } if line_scale <= 0.0 { - warn_once!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero"); + once!(warn!("when using gizmos with the line style `GizmoLineStyle::Dashed{{..}}` the line scale should be greater than zero")); } (gap_scale, line_scale) } else { diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 9bc1ca3e7d047..167ed3c19d4d0 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_gltf" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Bevy Engine GLTF loading" homepage = "https://bevyengine.org" @@ -18,26 +18,26 @@ pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] [dependencies] # bevy -bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_pbr = { path = "../bevy_pbr", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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_hierarchy = { path = "../bevy_hierarchy", 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_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", ] } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_scene = { path = "../bevy_scene", version = "0.15.0-dev", features = [ +bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = [ "bevy_render", ] } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +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" } # other gltf = { version = "1.4.0", default-features = false, features = [ @@ -59,9 +59,10 @@ percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" smallvec = "1.11" +tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] -bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } [lints] workspace = true diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index c1f6a5d2eaa43..e83ed9351f09c 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -41,10 +41,7 @@ use bevy_scene::Scene; #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::IoTaskPool; use bevy_transform::components::Transform; -use bevy_utils::{ - tracing::{error, info_span, warn}, - HashMap, HashSet, -}; +use bevy_utils::{HashMap, HashSet}; use gltf::{ accessor::Iter, image::Source, @@ -60,6 +57,7 @@ use std::{ path::{Path, PathBuf}, }; use thiserror::Error; +use tracing::{error, info_span, warn}; #[cfg(feature = "bevy_animation")] use { bevy_animation::{prelude::*, AnimationTarget, AnimationTargetId}, @@ -456,7 +454,10 @@ async fn load_gltf<'a, 'b, 'c>( ReadOutputs::MorphTargetWeights(weights) => { let weights: Vec = weights.into_f32().collect(); if keyframe_timestamps.len() == 1 { - #[allow(clippy::unnecessary_map_on_constructor)] + #[expect( + clippy::unnecessary_map_on_constructor, + reason = "While the mapping is unnecessary, it is much more readable at this level of indentation. Additionally, mapping makes it more consistent with the other branches." + )] Some(ConstantCurve::new(Interval::EVERYWHERE, weights)) .map(WeightsCurve) .map(VariableCurve::new) @@ -646,13 +647,13 @@ async fn load_gltf<'a, 'b, 'c>( if [Semantic::Joints(0), Semantic::Weights(0)].contains(&semantic) { if !meshes_on_skinned_nodes.contains(&gltf_mesh.index()) { warn!( - "Ignoring attribute {:?} for skinned mesh {:?} used on non skinned nodes (NODE_SKINNED_MESH_WITHOUT_SKIN)", + "Ignoring attribute {:?} for skinned mesh {} used on non skinned nodes (NODE_SKINNED_MESH_WITHOUT_SKIN)", semantic, primitive_label ); continue; } else if meshes_on_non_skinned_nodes.contains(&gltf_mesh.index()) { - error!("Skinned mesh {:?} used on both skinned and non skin nodes, this is likely to cause an error (NODE_SKINNED_MESH_WITHOUT_SKIN)", primitive_label); + error!("Skinned mesh {} used on both skinned and non skin nodes, this is likely to cause an error (NODE_SKINNED_MESH_WITHOUT_SKIN)", primitive_label); } } match convert_attribute( @@ -704,17 +705,15 @@ async fn load_gltf<'a, 'b, 'c>( if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() && matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList) { - bevy_utils::tracing::debug!( - "Automatically calculating missing vertex normals for geometry." - ); + tracing::debug!("Automatically calculating missing vertex normals for geometry."); let vertex_count_before = mesh.count_vertices(); mesh.duplicate_vertices(); mesh.compute_flat_normals(); let vertex_count_after = mesh.count_vertices(); if vertex_count_before != vertex_count_after { - bevy_utils::tracing::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); + tracing::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); } else { - bevy_utils::tracing::debug!( + tracing::debug!( "Missing vertex normals in indexed geometry, computing them as flat." ); } @@ -728,7 +727,7 @@ async fn load_gltf<'a, 'b, 'c>( } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() && material_needs_tangents(&primitive.material()) { - bevy_utils::tracing::debug!( + tracing::debug!( "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name ); @@ -737,9 +736,9 @@ async fn load_gltf<'a, 'b, 'c>( generate_tangents_span.in_scope(|| { if let Err(err) = mesh.generate_tangents() { warn!( - "Failed to generate vertex tangents using the mikktspace algorithm: {:?}", - err - ); + "Failed to generate vertex tangents using the mikktspace algorithm: {}", + err + ); } }); } @@ -1372,7 +1371,10 @@ fn warn_on_differing_texture_transforms( } /// Loads a glTF node. -#[allow(clippy::too_many_arguments, clippy::result_large_err)] +#[expect( + clippy::result_large_err, + reason = "`GltfError` is only barely past the threshold for large errors." +)] fn load_node( gltf_node: &Node, world_builder: &mut WorldChildBuilder, @@ -1727,7 +1729,10 @@ fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Ha /// /// This is a low-level function only used when the `gltf` crate has no support /// for an extension, forcing us to parse its texture references manually. -#[allow(dead_code)] +#[cfg(any( + feature = "pbr_anisotropy_texture", + feature = "pbr_multi_layer_material_textures" +))] fn texture_handle_from_info( load_context: &mut LoadContext, document: &Document, @@ -1800,7 +1805,7 @@ fn texture_sampler(texture: &gltf::Texture) -> ImageSamplerDescriptor { } } -/// Maps the texture address mode form glTF to wgpu. +/// Maps the texture address mode from glTF to wgpu. fn texture_address_mode(gltf_address_mode: &WrappingMode) -> ImageAddressMode { match gltf_address_mode { WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge, @@ -1809,8 +1814,11 @@ fn texture_address_mode(gltf_address_mode: &WrappingMode) -> ImageAddressMode { } } -/// Maps the `primitive_topology` form glTF to `wgpu`. -#[allow(clippy::result_large_err)] +/// Maps the `primitive_topology` from glTF to `wgpu`. +#[expect( + clippy::result_large_err, + reason = "`GltfError` is only barely past the threshold for large errors." +)] fn get_primitive_topology(mode: Mode) -> Result { match mode { Mode::Points => Ok(PrimitiveTopology::PointList), @@ -1880,7 +1888,10 @@ struct GltfTreeIterator<'a> { } impl<'a> GltfTreeIterator<'a> { - #[allow(clippy::result_large_err)] + #[expect( + clippy::result_large_err, + reason = "`GltfError` is only barely past the threshold for large errors." + )] fn try_new(gltf: &'a gltf::Gltf) -> Result { let nodes = gltf.nodes().collect::>(); @@ -1912,7 +1923,7 @@ impl<'a> GltfTreeIterator<'a> { if skin.joints().len() > MAX_JOINTS && warned_about_max_joints.insert(skin.index()) { warn!( - "The glTF skin {:?} has {} joints, but the maximum supported is {}", + "The glTF skin {} has {} joints, but the maximum supported is {}", skin.name() .map(ToString::to_string) .unwrap_or_else(|| skin.index().to_string()), @@ -2092,7 +2103,14 @@ struct ClearcoatExtension { } impl ClearcoatExtension { - #[allow(unused_variables)] + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` is not always linted" + )] + #[allow( + unused_variables, + reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." + )] fn parse( load_context: &mut LoadContext, document: &Document, @@ -2175,7 +2193,14 @@ struct AnisotropyExtension { } impl AnisotropyExtension { - #[allow(unused_variables)] + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` is not always linted" + )] + #[allow( + unused_variables, + reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." + )] fn parse( load_context: &mut LoadContext, document: &Document, @@ -2309,7 +2334,10 @@ mod test { } fn load_gltf_into_app(gltf_path: &str, gltf: &str) -> App { - #[expect(unused)] + #[expect( + dead_code, + reason = "This struct is used to keep the handle alive. As such, we have no need to handle the handle directly." + )] #[derive(Resource)] struct GltfHandle(Handle); diff --git a/crates/bevy_hierarchy/Cargo.toml b/crates/bevy_hierarchy/Cargo.toml index 093f5f7f88c3a..076428a613f41 100644 --- a/crates/bevy_hierarchy/Cargo.toml +++ b/crates/bevy_hierarchy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_hierarchy" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides hierarchy functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -9,23 +9,54 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["bevy_app"] -trace = [] -bevy_app = ["reflect", "dep:bevy_app"] -reflect = ["bevy_ecs/bevy_reflect", "bevy_reflect"] +default = ["std", "bevy_app", "reflect"] + +# Functionality + +## Adds integration with the `bevy_app` plugin API. +bevy_app = ["dep:bevy_app"] + +## Adds runtime reflection support using `bevy_reflect`. +reflect = ["bevy_ecs/bevy_reflect", "bevy_reflect", "bevy_app?/bevy_reflect"] + +# Debugging Features + +## Enables `tracing` integration, allowing spans and other metrics to be reported +## through that framework. +trace = ["dep:tracing"] + +# Platform Compatibility + +## 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 = [ + "bevy_app?/std", + "bevy_ecs/std", + "bevy_reflect/std", + "bevy_utils/std", + "disqualified/alloc", +] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev", optional = true } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ - "bevy", +bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, optional = true } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "smallvec", -], optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -disqualified = "1.0" +], default-features = false, optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ + "alloc", +] } +disqualified = { version = "1.0", default-features = false } -smallvec = { version = "1.11", features = ["union", "const_generics"] } +# other +smallvec = { version = "1.11", default-features = false, features = [ + "union", + "const_generics", +] } +tracing = { version = "0.1", default-features = false, optional = true } +log = { version = "0.4", default-features = false } [lints] workspace = true diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 769da2b83bb8b..811c4f4eaa8e3 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -2,9 +2,9 @@ use crate::{Children, HierarchyEvent, Parent}; use bevy_ecs::{ bundle::Bundle, entity::Entity, - prelude::Events, - system::{Commands, EntityCommands}, - world::{Command, EntityWorldMut, World}, + event::Events, + system::{Command, Commands, EntityCommands}, + world::{EntityWorldMut, World}, }; use smallvec::{smallvec, SmallVec}; @@ -149,109 +149,6 @@ fn remove_children(parent: Entity, children: &[Entity], world: &mut World) { } } -/// Removes all children from `parent` by removing its [`Children`] component, as well as removing -/// [`Parent`] component from its children. -fn clear_children(parent: Entity, world: &mut World) { - if let Some(children) = world.entity_mut(parent).take::() { - for &child in &children.0 { - world.entity_mut(child).remove::(); - } - } -} - -/// Command that adds a child to an entity. -#[derive(Debug)] -pub struct AddChild { - /// Parent entity to add the child to. - pub parent: Entity, - /// Child entity to add. - pub child: Entity, -} - -impl Command for AddChild { - fn apply(self, world: &mut World) { - world.entity_mut(self.parent).add_child(self.child); - } -} - -/// Command that inserts a child at a given index of a parent's children, shifting following children back. -#[derive(Debug)] -pub struct InsertChildren { - parent: Entity, - children: SmallVec<[Entity; 8]>, - index: usize, -} - -impl Command for InsertChildren { - fn apply(self, world: &mut World) { - world - .entity_mut(self.parent) - .insert_children(self.index, &self.children); - } -} - -/// Command that pushes children to the end of the entity's [`Children`]. -#[derive(Debug)] -pub struct AddChildren { - parent: Entity, - children: SmallVec<[Entity; 8]>, -} - -impl Command for AddChildren { - fn apply(self, world: &mut World) { - world.entity_mut(self.parent).add_children(&self.children); - } -} - -/// Command that removes children from an entity, and removes these children's parent. -pub struct RemoveChildren { - parent: Entity, - children: SmallVec<[Entity; 8]>, -} - -impl Command for RemoveChildren { - fn apply(self, world: &mut World) { - remove_children(self.parent, &self.children, world); - } -} - -/// Command that clears all children from an entity and removes [`Parent`] component from those -/// children. -pub struct ClearChildren { - parent: Entity, -} - -impl Command for ClearChildren { - fn apply(self, world: &mut World) { - clear_children(self.parent, world); - } -} - -/// Command that clear all children from an entity, replacing them with the given children. -pub struct ReplaceChildren { - parent: Entity, - children: SmallVec<[Entity; 8]>, -} - -impl Command for ReplaceChildren { - fn apply(self, world: &mut World) { - clear_children(self.parent, world); - world.entity_mut(self.parent).add_children(&self.children); - } -} - -/// Command that removes the parent of an entity, and removes that entity from the parent's [`Children`]. -pub struct RemoveParent { - /// `Entity` whose parent must be removed. - pub child: Entity, -} - -impl Command for RemoveParent { - fn apply(self, world: &mut World) { - world.entity_mut(self.child).remove_parent(); - } -} - /// Struct for building children entities and adding them to a parent entity. /// /// # Example @@ -276,7 +173,8 @@ impl Command for RemoveParent { /// ``` pub struct ChildBuilder<'a> { commands: Commands<'a, 'a>, - add_children: AddChildren, + children: SmallVec<[Entity; 8]>, + parent: Entity, } /// Trait for building children entities and adding them to a parent entity. This is used in @@ -316,18 +214,18 @@ impl ChildBuild for ChildBuilder<'_> { fn spawn(&mut self, bundle: impl Bundle) -> EntityCommands { let e = self.commands.spawn(bundle); - self.add_children.children.push(e.id()); + self.children.push(e.id()); e } fn spawn_empty(&mut self) -> EntityCommands { let e = self.commands.spawn_empty(); - self.add_children.children.push(e.id()); + self.children.push(e.id()); e } fn parent_entity(&self) -> Entity { - self.add_children.parent + self.parent } fn queue_command(&mut self, command: C) -> &mut Self { @@ -350,6 +248,8 @@ pub trait BuildChildren { /// Spawns the passed bundle and adds it to this entity as a child. /// + /// The bundle's [`Parent`] component will be updated to the new parent. + /// /// For efficient spawning of multiple children, use [`with_children`]. /// /// [`with_children`]: BuildChildren::with_children @@ -358,6 +258,8 @@ pub trait BuildChildren { /// Pushes children to the back of the builder's children. For any entities that are /// already a child of this one, this method does nothing. /// + /// The children's [`Parent`] component will be updated to the new parent. + /// /// If the children were previously children of another parent, that parent's [`Children`] component /// will have those children removed from its list. Removing all children from a parent causes its /// [`Children`] component to be removed from the entity. @@ -369,6 +271,8 @@ pub trait BuildChildren { /// Inserts children at the given index. /// + /// The children's [`Parent`] component will be updated to the new parent. + /// /// If the children were previously children of another parent, that parent's [`Children`] component /// will have those children removed from its list. Removing all children from a parent causes its /// [`Children`] component to be removed from the entity. @@ -378,13 +282,17 @@ pub trait BuildChildren { /// Panics if any of the children are the same as the parent. fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self; - /// Removes the given children + /// Removes the given children. + /// + /// The removed children will have their [`Parent`] component removed. /// /// Removing all children from a parent causes its [`Children`] component to be removed from the entity. fn remove_children(&mut self, children: &[Entity]) -> &mut Self; /// Adds a single child. /// + /// The child's [`Parent`] component will be updated to the new parent. + /// /// If the child was previously the child of another parent, that parent's [`Children`] component /// will have the child removed from its list. Removing all children from a parent causes its /// [`Children`] component to be removed from the entity. @@ -394,11 +302,13 @@ pub trait BuildChildren { /// Panics if the child is the same as the parent. fn add_child(&mut self, child: Entity) -> &mut Self; - /// Removes all children from this entity. The [`Children`] component will be removed if it exists, otherwise this does nothing. + /// Removes all children from this entity. The [`Children`] component and the children's [`Parent`] component will be removed. + /// If the [`Children`] component is not present, this has no effect. fn clear_children(&mut self) -> &mut Self; /// Removes all current children from this entity, replacing them with the specified list of entities. /// + /// The added children's [`Parent`] component will be updated to the new parent. /// The removed children will have their [`Parent`] component removed. /// /// # Panics @@ -431,38 +341,37 @@ impl BuildChildren for EntityCommands<'_> { let parent = self.id(); let mut builder = ChildBuilder { commands: self.commands(), - add_children: AddChildren { - children: SmallVec::default(), - parent, - }, + children: SmallVec::default(), + parent, }; spawn_children(&mut builder); - let children = builder.add_children; - if children.children.contains(&parent) { + + let children = builder.children; + if children.contains(&parent) { panic!("Entity cannot be a child of itself."); } - self.commands().queue(children); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.add_children(&children); + }) } fn with_child(&mut self, bundle: B) -> &mut Self { - let parent = self.id(); let child = self.commands().spawn(bundle).id(); - self.commands().queue(AddChild { parent, child }); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.add_child(child); + }) } fn add_children(&mut self, children: &[Entity]) -> &mut Self { let parent = self.id(); if children.contains(&parent) { - panic!("Cannot push entity as a child of itself."); + panic!("Cannot add entity as a child of itself."); } - self.commands().queue(AddChildren { - children: SmallVec::from(children), - parent, - }); - self + let children = SmallVec::<[Entity; 8]>::from_slice(children); + self.queue(move |mut entity: EntityWorldMut| { + entity.add_children(&children); + }) } fn insert_children(&mut self, index: usize, children: &[Entity]) -> &mut Self { @@ -470,21 +379,17 @@ impl BuildChildren for EntityCommands<'_> { if children.contains(&parent) { panic!("Cannot insert entity as a child of itself."); } - self.commands().queue(InsertChildren { - children: SmallVec::from(children), - index, - parent, - }); - self + let children = SmallVec::<[Entity; 8]>::from_slice(children); + self.queue(move |mut entity: EntityWorldMut| { + entity.insert_children(index, &children); + }) } fn remove_children(&mut self, children: &[Entity]) -> &mut Self { - let parent = self.id(); - self.commands().queue(RemoveChildren { - children: SmallVec::from(children), - parent, - }); - self + let children = SmallVec::<[Entity; 8]>::from_slice(children); + self.queue(move |mut entity: EntityWorldMut| { + entity.remove_children(&children); + }) } fn add_child(&mut self, child: Entity) -> &mut Self { @@ -492,14 +397,15 @@ impl BuildChildren for EntityCommands<'_> { if child == parent { panic!("Cannot add entity as a child of itself."); } - self.commands().queue(AddChild { child, parent }); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.add_child(child); + }) } fn clear_children(&mut self) -> &mut Self { - let parent = self.id(); - self.commands().queue(ClearChildren { parent }); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.clear_children(); + }) } fn replace_children(&mut self, children: &[Entity]) -> &mut Self { @@ -507,11 +413,10 @@ impl BuildChildren for EntityCommands<'_> { if children.contains(&parent) { panic!("Cannot replace entity as a child of itself."); } - self.commands().queue(ReplaceChildren { - children: SmallVec::from(children), - parent, - }); - self + let children = SmallVec::<[Entity; 8]>::from_slice(children); + self.queue(move |mut entity: EntityWorldMut| { + entity.replace_children(&children); + }) } fn set_parent(&mut self, parent: Entity) -> &mut Self { @@ -519,14 +424,17 @@ impl BuildChildren for EntityCommands<'_> { if child == parent { panic!("Cannot set parent to itself"); } - self.commands().queue(AddChild { child, parent }); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.world_scope(|world| { + world.entity_mut(parent).add_child(child); + }); + }) } fn remove_parent(&mut self) -> &mut Self { - let child = self.id(); - self.commands().queue(RemoveParent { child }); - self + self.queue(move |mut entity: EntityWorldMut| { + entity.remove_parent(); + }) } } @@ -557,16 +465,7 @@ impl ChildBuild for WorldChildBuilder<'_> { } fn spawn_empty(&mut self) -> EntityWorldMut { - let entity = self.world.spawn(Parent(self.parent)).id(); - add_child_unchecked(self.world, self.parent, entity); - push_events( - self.world, - [HierarchyEvent::ChildAdded { - child: entity, - parent: self.parent, - }], - ); - self.world.entity_mut(entity) + self.spawn(()) } fn parent_entity(&self) -> Entity { @@ -574,11 +473,19 @@ impl ChildBuild for WorldChildBuilder<'_> { } fn queue_command(&mut self, command: C) -> &mut Self { - command.apply(self.world); + self.world.commands().queue(command); self } } +impl WorldChildBuilder<'_> { + /// Calls the world's [`World::flush`] to apply any commands + /// queued by [`Self::queue_command`]. + pub fn flush_world(&mut self) { + self.world.flush(); + } +} + impl BuildChildren for EntityWorldMut<'_> { type Builder<'a> = WorldChildBuilder<'a>; @@ -691,7 +598,11 @@ impl BuildChildren for EntityWorldMut<'_> { fn clear_children(&mut self) -> &mut Self { let parent = self.id(); self.world_scope(|world| { - clear_children(parent, world); + if let Some(children) = world.entity_mut(parent).take::() { + for &child in &children.0 { + world.entity_mut(child).remove::(); + } + } }); self } @@ -708,6 +619,7 @@ mod tests { components::{Children, Parent}, HierarchyEvent::{self, ChildAdded, ChildMoved, ChildRemoved}, }; + use alloc::{vec, vec::Vec}; use smallvec::{smallvec, SmallVec}; use bevy_ecs::{ @@ -871,7 +783,10 @@ mod tests { ); } - #[allow(dead_code)] + #[expect( + dead_code, + reason = "The inner field is used to differentiate the different instances of this struct." + )] #[derive(Component)] struct C(u32); diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index 37dcb83061f93..5b89149154c0b 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -5,28 +5,10 @@ use crate::{ use bevy_ecs::{ component::ComponentCloneHandler, entity::{ComponentCloneCtx, Entity, EntityCloneBuilder}, - system::EntityCommands, - world::{Command, DeferredWorld, EntityWorldMut, World}, + system::{error_handler, EntityCommands}, + world::{DeferredWorld, EntityWorldMut, World}, }; -use bevy_utils::tracing::debug; - -/// Despawns the given entity and all its children recursively -#[derive(Debug)] -pub struct DespawnRecursive { - /// Target entity - pub entity: Entity, - /// Whether or not this command should output a warning if the entity does not exist - pub warn: bool, -} - -/// Despawns the given entity's children recursively -#[derive(Debug)] -pub struct DespawnChildrenRecursive { - /// Target entity - pub entity: Entity, - /// Whether or not this command should output a warning if the entity does not exist - pub warn: bool, -} +use log::debug; /// Function for despawning an entity and all its children pub fn despawn_with_children_recursive(world: &mut World, entity: Entity, warn: bool) { @@ -41,7 +23,7 @@ pub fn despawn_with_children_recursive(world: &mut World, entity: Entity, warn: despawn_with_children_recursive_inner(world, entity, warn); } -// Should only be called by `despawn_with_children_recursive` and `try_despawn_with_children_recursive`! +// Should only be called by `despawn_with_children_recursive` and `despawn_children_recursive`! fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity, warn: bool) { if let Some(mut children) = world.get_mut::(entity) { for e in core::mem::take(&mut children.0) { @@ -51,10 +33,10 @@ fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity, warn if warn { if !world.despawn(entity) { - debug!("Failed to despawn entity {:?}", entity); + debug!("Failed to despawn entity {}", entity); } } else if !world.try_despawn(entity) { - debug!("Failed to despawn entity {:?}", entity); + debug!("Failed to despawn entity {}", entity); } } @@ -66,35 +48,6 @@ fn despawn_children_recursive(world: &mut World, entity: Entity, warn: bool) { } } -impl Command for DespawnRecursive { - fn apply(self, world: &mut World) { - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( - "command", - name = "DespawnRecursive", - entity = bevy_utils::tracing::field::debug(self.entity), - warn = bevy_utils::tracing::field::debug(self.warn) - ) - .entered(); - despawn_with_children_recursive(world, self.entity, self.warn); - } -} - -impl Command for DespawnChildrenRecursive { - fn apply(self, world: &mut World) { - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( - "command", - name = "DespawnChildrenRecursive", - entity = bevy_utils::tracing::field::debug(self.entity), - warn = bevy_utils::tracing::field::debug(self.warn) - ) - .entered(); - - despawn_children_recursive(world, self.entity, self.warn); - } -} - /// Trait that holds functions for despawning recursively down the transform hierarchy pub trait DespawnRecursiveExt { /// Despawns the provided entity alongside all descendants. @@ -114,34 +67,90 @@ impl DespawnRecursiveExt for EntityCommands<'_> { /// Despawns the provided entity and its children. /// This will emit warnings for any entity that does not exist. fn despawn_recursive(mut self) { - let entity = self.id(); - self.commands() - .queue(DespawnRecursive { entity, warn: true }); + let warn = true; + self.queue_handled( + move |mut entity: EntityWorldMut| { + let id = entity.id(); + #[cfg(feature = "trace")] + let _span = tracing::info_span!( + "command", + name = "DespawnRecursive", + entity = tracing::field::debug(id), + warn = tracing::field::debug(warn) + ) + .entered(); + entity.world_scope(|world| { + despawn_with_children_recursive(world, id, warn); + }); + }, + error_handler::warn(), + ); } fn despawn_descendants(&mut self) -> &mut Self { - let entity = self.id(); - self.commands() - .queue(DespawnChildrenRecursive { entity, warn: true }); + let warn = true; + self.queue_handled( + move |mut entity: EntityWorldMut| { + let id = entity.id(); + #[cfg(feature = "trace")] + let _span = tracing::info_span!( + "command", + name = "DespawnChildrenRecursive", + entity = tracing::field::debug(id), + warn = tracing::field::debug(warn) + ) + .entered(); + entity.world_scope(|world| { + despawn_children_recursive(world, id, warn); + }); + }, + error_handler::warn(), + ); self } /// Despawns the provided entity and its children. /// This will never emit warnings. fn try_despawn_recursive(mut self) { - let entity = self.id(); - self.commands().queue(DespawnRecursive { - entity, - warn: false, - }); + let warn = false; + self.queue_handled( + move |mut entity: EntityWorldMut| { + let id = entity.id(); + #[cfg(feature = "trace")] + let _span = tracing::info_span!( + "command", + name = "TryDespawnRecursive", + entity = tracing::field::debug(id), + warn = tracing::field::debug(warn) + ) + .entered(); + entity.world_scope(|world| { + despawn_with_children_recursive(world, id, warn); + }); + }, + error_handler::silent(), + ); } fn try_despawn_descendants(&mut self) -> &mut Self { - let entity = self.id(); - self.commands().queue(DespawnChildrenRecursive { - entity, - warn: false, - }); + let warn = false; + self.queue_handled( + move |mut entity: EntityWorldMut| { + let id = entity.id(); + #[cfg(feature = "trace")] + let _span = tracing::info_span!( + "command", + name = "TryDespawnChildrenRecursive", + entity = tracing::field::debug(id), + warn = tracing::field::debug(warn) + ) + .entered(); + entity.world_scope(|world| { + despawn_children_recursive(world, id, warn); + }); + }, + error_handler::silent(), + ); self } } @@ -150,10 +159,10 @@ fn despawn_recursive_inner(world: EntityWorldMut, warn: bool) { let entity = world.id(); #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( + let _span = tracing::info_span!( "despawn_recursive", - entity = bevy_utils::tracing::field::debug(entity), - warn = bevy_utils::tracing::field::debug(warn) + entity = tracing::field::debug(entity), + warn = tracing::field::debug(warn) ) .entered(); @@ -167,10 +176,10 @@ fn despawn_descendants_inner<'v, 'w>( let entity = world.id(); #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( + let _span = tracing::info_span!( "despawn_descendants", - entity = bevy_utils::tracing::field::debug(entity), - warn = bevy_utils::tracing::field::debug(warn) + entity = tracing::field::debug(entity), + warn = tracing::field::debug(warn) ) .entered(); @@ -262,6 +271,7 @@ fn component_clone_parent(world: &mut DeferredWorld, ctx: &mut ComponentCloneCtx #[cfg(test)] mod tests { + use alloc::{borrow::ToOwned, string::String, vec, vec::Vec}; use bevy_ecs::{ component::Component, system::Commands, diff --git a/crates/bevy_hierarchy/src/lib.rs b/crates/bevy_hierarchy/src/lib.rs index ced37bd154f64..2e8beea501f7d 100644 --- a/crates/bevy_hierarchy/src/lib.rs +++ b/crates/bevy_hierarchy/src/lib.rs @@ -4,6 +4,7 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] +#![no_std] //! Parent-child relationships for Bevy entities. //! @@ -51,6 +52,9 @@ //! [plugin]: HierarchyPlugin //! [query extension methods]: HierarchyQueryExt +#[cfg(feature = "std")] +extern crate std; + extern crate alloc; mod components; @@ -98,8 +102,9 @@ pub struct HierarchyPlugin; #[cfg(feature = "bevy_app")] impl Plugin for HierarchyPlugin { fn build(&self, app: &mut App) { - app.register_type::() - .register_type::() - .add_event::(); + #[cfg(feature = "reflect")] + app.register_type::().register_type::(); + + app.add_event::(); } } diff --git a/crates/bevy_hierarchy/src/query_extension.rs b/crates/bevy_hierarchy/src/query_extension.rs index 6396ddcfb756f..1ab3ec9385fea 100644 --- a/crates/bevy_hierarchy/src/query_extension.rs +++ b/crates/bevy_hierarchy/src/query_extension.rs @@ -139,10 +139,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> HierarchyQueryExt<'w, 's, D, F> for Q { self.iter_descendants_depth_first(entity).filter(|entity| { self.get(*entity) + .ok() // These are leaf nodes if they have the `Children` component but it's empty - .map(|children| children.is_empty()) // Or if they don't have the `Children` component at all - .unwrap_or(true) + .is_none_or(|children| children.is_empty()) }) } @@ -312,6 +312,7 @@ where #[cfg(test)] mod tests { + use alloc::vec::Vec; use bevy_ecs::{ prelude::Component, system::{Query, SystemState}, diff --git a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs index a05ec586cfdfd..c17059c3f3b83 100644 --- a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs +++ b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use bevy_ecs::prelude::*; #[cfg(feature = "bevy_app")] -use {crate::Parent, bevy_utils::HashSet, disqualified::ShortName}; +use {crate::Parent, alloc::format, bevy_utils::HashSet, disqualified::ShortName}; /// When enabled, runs [`check_hierarchy_component_has_valid_parent`]. /// @@ -63,7 +63,7 @@ pub fn check_hierarchy_component_has_valid_parent( let parent = parent.get(); if !component_query.contains(parent) && !already_diagnosed.contains(&entity) { already_diagnosed.insert(entity); - bevy_utils::tracing::warn!( + log::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", ty_name = ShortName::of::(), diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index df033d64d6700..5db0ad91ee055 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_image" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides image types for Bevy Engine" homepage = "https://bevyengine.org" @@ -9,6 +9,10 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] +default = ["bevy_reflect"] + +bevy_reflect = ["dep:bevy_reflect", "bevy_math/bevy_reflect"] + # Image formats basis-universal = ["dep:basis-universal"] bmp = ["image/bmp"] @@ -26,22 +30,25 @@ qoi = ["image/qoi"] tga = ["image/tga"] tiff = ["image/tiff"] webp = ["image/webp"] +serialize = ["bevy_reflect"] # For ktx2 supercompression zlib = ["flate2"] zstd = ["ruzstd"] [dependencies] -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev", features = [ +# 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 = [ "serialize", "wgpu-types", ] } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", -] } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +], optional = true } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } # rendering image = { version = "0.25.2", default-features = false } @@ -55,6 +62,8 @@ wgpu = { version = "23.0.1", default-features = false } serde = { version = "1", features = ["derive"] } thiserror = { version = "2", default-features = false } futures-lite = "2.0.1" +guillotiere = "0.6.0" +rectangle-pack = "0.4" ddsfile = { version = "0.5.2", optional = true } ktx2 = { version = "0.3.0", optional = true } # For ktx2 supercompression @@ -62,6 +71,11 @@ flate2 = { version = "1.0.22", optional = true } ruzstd = { version = "0.7.0", optional = true } # For transcoding of UASTC/ETC1S universal formats, and for .basis file support basis-universal = { version = "0.3.0", optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[dev-dependencies] +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.16.0-dev" } [lints] workspace = true diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index 220317172626a..4bd68fdafca6c 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -45,6 +45,10 @@ impl AssetSaver for CompressedImageSaver { source_image.init(&image.data, size.x, size.y, 4); let mut compressor = basis_universal::Compressor::new(4); + #[expect( + unsafe_code, + reason = "The basis-universal compressor cannot be interacted with except through unsafe functions" + )] // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal // library bindings note that invalid params might produce undefined behavior. unsafe { diff --git a/crates/bevy_image/src/dds.rs b/crates/bevy_image/src/dds.rs index f36ec25894c02..ffb3ce2c7c118 100644 --- a/crates/bevy_image/src/dds.rs +++ b/crates/bevy_image/src/dds.rs @@ -1,11 +1,11 @@ //! [DirectDraw Surface](https://en.wikipedia.org/wiki/DirectDraw_Surface) functionality. -#[cfg(debug_assertions)] -use bevy_utils::warn_once; use ddsfile::{Caps2, D3DFormat, Dds, DxgiFormat}; use std::io::Cursor; use wgpu::TextureViewDescriptor; use wgpu_types::{Extent3d, TextureDimension, TextureFormat, TextureViewDimension}; +#[cfg(debug_assertions)] +use {bevy_utils::once, tracing::warn}; use super::{CompressedImageFormats, Image, TextureError}; @@ -53,10 +53,10 @@ pub fn dds_buffer_to_image( let mip_map_level = match dds.get_num_mipmap_levels() { 0 => { #[cfg(debug_assertions)] - warn_once!( + once!(warn!( "Mipmap levels for texture {} are 0, bumping them to 1", name - ); + )); 1 } t => t, diff --git a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs similarity index 89% rename from crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs rename to crates/bevy_image/src/dynamic_texture_atlas_builder.rs index 87245d463f6bd..8944e74e74157 100644 --- a/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs @@ -1,10 +1,6 @@ -use crate::TextureAtlasLayout; -use bevy_image::{Image, TextureFormatPixelInfo}; +use crate::{Image, TextureAtlasLayout, TextureFormatPixelInfo as _}; +use bevy_asset::RenderAssetUsages; use bevy_math::{URect, UVec2}; -use bevy_render::{ - render_asset::{RenderAsset, RenderAssetUsages}, - texture::GpuImage, -}; use guillotiere::{size2, Allocation, AtlasAllocator}; /// Helper utility to update [`TextureAtlasLayout`] on the fly. @@ -53,9 +49,8 @@ impl DynamicTextureAtlasBuilder { )); if let Some(allocation) = allocation { assert!( - ::asset_usage(atlas_texture) - .contains(RenderAssetUsages::MAIN_WORLD), - "The asset at atlas_texture_handle must have the RenderAssetUsages::MAIN_WORLD usage flag set" + atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD), + "The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set" ); self.place_texture(atlas_texture, allocation, texture); diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index d9f02379f41fe..9d5adf5e55402 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -4,12 +4,12 @@ use super::basis::*; use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; -use bevy_reflect::std_traits::ReflectDefault; -use bevy_reflect::Reflect; use core::hash::Hash; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -72,7 +72,7 @@ macro_rules! feature_gate { ($feature: tt, $value: ident) => {{ #[cfg(not(feature = $feature))] { - bevy_utils::tracing::warn!("feature \"{}\" is not enabled", $feature); + tracing::warn!("feature \"{}\" is not enabled", $feature); return None; } #[cfg(feature = $feature)] @@ -117,7 +117,14 @@ impl ImageFormat { #[cfg(feature = "webp")] ImageFormat::WebP => &["webp"], // FIXME: https://github.com/rust-lang/rust/issues/129031 - #[allow(unreachable_patterns)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_patterns` may not always lint" + )] + #[allow( + unreachable_patterns, + reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable" + )] _ => &[], } } @@ -165,13 +172,27 @@ impl ImageFormat { #[cfg(feature = "webp")] ImageFormat::WebP => &["image/webp"], // FIXME: https://github.com/rust-lang/rust/issues/129031 - #[allow(unreachable_patterns)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_patterns` may not always lint" + )] + #[allow( + unreachable_patterns, + reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable" + )] _ => &[], } } pub fn from_mime_type(mime_type: &str) -> Option { - #[allow(unreachable_code)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_code` may not always lint" + )] + #[allow( + unreachable_code, + reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed." + )] Some(match mime_type.to_ascii_lowercase().as_str() { // note: farbfeld does not have a MIME type "image/basis" | "image/x-basis" => feature_gate!("basis-universal", Basis), @@ -197,7 +218,14 @@ impl ImageFormat { } pub fn from_extension(extension: &str) -> Option { - #[allow(unreachable_code)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_code` may not always lint" + )] + #[allow( + unreachable_code, + reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed." + )] Some(match extension.to_ascii_lowercase().as_str() { "basis" => feature_gate!("basis-universal", Basis), "bmp" => feature_gate!("bmp", Bmp), @@ -220,7 +248,14 @@ impl ImageFormat { } pub fn as_image_crate_format(&self) -> Option { - #[allow(unreachable_code)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_code` may not always lint" + )] + #[allow( + unreachable_code, + reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed." + )] Some(match self { #[cfg(feature = "bmp")] ImageFormat::Bmp => image::ImageFormat::Bmp, @@ -255,13 +290,27 @@ impl ImageFormat { #[cfg(feature = "ktx2")] ImageFormat::Ktx2 => return None, // FIXME: https://github.com/rust-lang/rust/issues/129031 - #[allow(unreachable_patterns)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_patterns` may not always lint" + )] + #[allow( + unreachable_patterns, + reason = "The wildcard pattern will be unreachable if all formats are enabled; otherwise, it will be reachable" + )] _ => return None, }) } pub fn from_image_crate_format(format: image::ImageFormat) -> Option { - #[allow(unreachable_code)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_code` may not always lint" + )] + #[allow( + unreachable_code, + reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed." + )] Some(match format { image::ImageFormat::Bmp => feature_gate!("bmp", Bmp), image::ImageFormat::Dds => feature_gate!("dds", Dds), @@ -282,9 +331,12 @@ impl ImageFormat { } } -#[derive(Asset, Reflect, Debug, Clone)] -#[reflect(opaque)] -#[reflect(Default, Debug)] +#[derive(Asset, Debug, Clone)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(opaque, Default, Debug) +)] pub struct Image { pub data: Vec, // TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors @@ -874,7 +926,15 @@ impl Image { #[cfg(all(debug_assertions, feature = "dds"))] name: String, buffer: &[u8], image_type: ImageType, - #[allow(unused_variables)] supported_compressed_formats: CompressedImageFormats, + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` may not always lint" + )] + #[allow( + unused_variables, + reason = "`supported_compressed_formats` is needed where the image format is `Basis`, `Dds`, or `Ktx2`; if these are disabled, then `supported_compressed_formats` is unused." + )] + supported_compressed_formats: CompressedImageFormats, is_srgb: bool, image_sampler: ImageSampler, asset_usage: RenderAssetUsages, @@ -904,7 +964,14 @@ impl Image { ImageFormat::Ktx2 => { ktx2_buffer_to_image(buffer, supported_compressed_formats, is_srgb)? } - #[allow(unreachable_patterns)] + #[expect( + clippy::allow_attributes, + reason = "`unreachable_patterns` may not always lint" + )] + #[allow( + unreachable_patterns, + reason = "The wildcard pattern may be unreachable if only the specially-handled formats are enabled; however, the wildcard pattern is needed for any formats not specially handled" + )] _ => { let image_crate_format = format .as_image_crate_format() diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 2952bcec7cfeb..55f74a5f14d35 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -1,8 +1,13 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] -#![allow(unsafe_code)] + +extern crate alloc; pub mod prelude { - pub use crate::{BevyDefault as _, Image, ImageFormat, TextureError}; + pub use crate::{ + dynamic_texture_atlas_builder::DynamicTextureAtlasBuilder, + texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources}, + BevyDefault as _, Image, ImageFormat, TextureAtlasBuilder, TextureError, + }; } mod image; @@ -13,6 +18,7 @@ mod basis; mod compressed_image_saver; #[cfg(feature = "dds")] mod dds; +mod dynamic_texture_atlas_builder; #[cfg(feature = "exr")] mod exr_texture_loader; #[cfg(feature = "hdr")] @@ -20,11 +26,14 @@ mod hdr_texture_loader; mod image_loader; #[cfg(feature = "ktx2")] mod ktx2; +mod texture_atlas; +mod texture_atlas_builder; #[cfg(feature = "basis-universal")] pub use compressed_image_saver::*; #[cfg(feature = "dds")] pub use dds::*; +pub use dynamic_texture_atlas_builder::*; #[cfg(feature = "exr")] pub use exr_texture_loader::*; #[cfg(feature = "hdr")] @@ -32,6 +41,8 @@ pub use hdr_texture_loader::*; pub use image_loader::*; #[cfg(feature = "ktx2")] pub use ktx2::*; +pub use texture_atlas::*; +pub use texture_atlas_builder::*; pub(crate) mod image_texture_conversion; pub use image_texture_conversion::IntoDynamicImageError; diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_image/src/texture_atlas.rs similarity index 87% rename from crates/bevy_sprite/src/texture_atlas.rs rename to crates/bevy_image/src/texture_atlas.rs index 797fb4aa206ff..57aa0c379e6ac 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_image/src/texture_atlas.rs @@ -1,11 +1,27 @@ -use bevy_asset::{Asset, AssetId, Assets, Handle}; -use bevy_image::Image; +use bevy_app::prelude::*; +use bevy_asset::{Asset, AssetApp as _, AssetId, Assets, Handle}; use bevy_math::{URect, UVec2}; +#[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_utils::HashMap; +use crate::Image; + +/// Adds support for texture atlases. +pub struct TextureAtlasPlugin; + +impl Plugin for TextureAtlasPlugin { + fn build(&self, app: &mut App) { + app.init_asset::(); + + #[cfg(feature = "bevy_reflect")] + app.register_asset_reflect::() + .register_type::(); + } +} + /// Stores a mapping from sub texture handles to the related area index. /// /// Generated by [`TextureAtlasBuilder`]. @@ -56,10 +72,13 @@ impl TextureAtlasSources { /// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) /// /// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Asset, Reflect, PartialEq, Eq, Debug, Clone)] -#[reflect(Debug, PartialEq)] -#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))] +#[derive(Asset, PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] pub struct TextureAtlasLayout { /// Total size of texture atlas. pub size: UVec2, @@ -163,8 +182,12 @@ impl TextureAtlasLayout { /// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs) /// - [`sprite animation event example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_animation.rs) /// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs) -#[derive(Default, Debug, Clone, Reflect)] -#[reflect(Default, Debug)] +#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Default, Debug, PartialEq, Hash) +)] pub struct TextureAtlas { /// Texture atlas layout handle pub layout: Handle, diff --git a/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_image/src/texture_atlas_builder.rs similarity index 94% rename from crates/bevy_sprite/src/texture_atlas_builder.rs rename to crates/bevy_image/src/texture_atlas_builder.rs index 695e00f437e0d..0b0889d97c993 100644 --- a/crates/bevy_sprite/src/texture_atlas_builder.rs +++ b/crates/bevy_image/src/texture_atlas_builder.rs @@ -1,20 +1,15 @@ -use bevy_asset::AssetId; -use bevy_image::{Image, TextureFormatPixelInfo}; +use bevy_asset::{AssetId, RenderAssetUsages}; use bevy_math::{URect, UVec2}; -use bevy_render::{ - render_asset::RenderAssetUsages, - render_resource::{Extent3d, TextureDimension, TextureFormat}, -}; -use bevy_utils::{ - tracing::{debug, error, warn}, - HashMap, -}; +use bevy_utils::HashMap; use rectangle_pack::{ contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation, RectToInsert, TargetBin, }; use thiserror::Error; +use tracing::{debug, error, warn}; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; +use crate::{Image, TextureFormatPixelInfo}; use crate::{TextureAtlasLayout, TextureAtlasSources}; #[derive(Debug, Error)] @@ -155,16 +150,6 @@ impl<'a> TextureAtlasBuilder<'a> { } } - #[deprecated( - since = "0.14.0", - note = "TextureAtlasBuilder::finish() was not idiomatic. Use TextureAtlasBuilder::build() instead." - )] - pub fn finish( - &mut self, - ) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> { - self.build() - } - /// Consumes the builder, and returns the newly created texture atlas and /// the associated atlas layout. /// @@ -178,8 +163,7 @@ impl<'a> TextureAtlasBuilder<'a> { /// # use bevy_sprite::prelude::*; /// # use bevy_ecs::prelude::*; /// # use bevy_asset::*; - /// # use bevy_render::prelude::*; - /// # use bevy_image::Image; + /// # use bevy_image::prelude::*; /// /// fn my_system(mut commands: Commands, mut textures: ResMut>, mut layouts: ResMut>) { /// // Declare your builder diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index fa185ea5feed9..aa868911e6ac8 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_input" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides input functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -9,36 +9,72 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["bevy_reflect"] +default = ["std", "bevy_reflect", "bevy_ecs/async_executor", "smol_str"] + +# Functionality + +## Adds runtime reflection support using `bevy_reflect`. bevy_reflect = [ "dep:bevy_reflect", "bevy_app/bevy_reflect", "bevy_ecs/bevy_reflect", "bevy_math/bevy_reflect", ] -serialize = ["serde", "smol_str/serde"] + +## Adds serialization support through `serde`. +serialize = [ + "serde", + "smol_str/serde", + "bevy_ecs/serialize", + "bevy_math/serialize", +] + +## Uses the small-string optimization provided by `smol_str`. +smol_str = ["dep:smol_str", "bevy_reflect/smol_str"] + +# Platform Compatibility + +## 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 = [ + "bevy_app/std", + "bevy_ecs/std", + "bevy_math/std", + "bevy_utils/std", + "bevy_reflect/std", +] + +## `critical-section` provides the building blocks for synchronization primitives +## on all platforms, including `no_std`. +critical-section = ["bevy_app/critical-section", "bevy_ecs/critical-section"] + +## `portable-atomic` provides additional platform support for atomic types and +## operations, even on targets without native support. +portable-atomic = ["bevy_app/portable-atomic", "bevy_ecs/portable-atomic"] + +## Uses the `libm` maths library instead of the one provided in `std` and `core`. +libm = ["bevy_math/libm"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false, features = [ - "serialize", -] } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev", default-features = false, features = [ - "rand", - "serialize", -] } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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 = [ "glam", - "smol_str", -], optional = true } +], default-features = false, optional = true } # other -serde = { version = "1", features = ["derive"], optional = true } +serde = { version = "1", features = [ + "alloc", + "derive", +], default-features = false, optional = true } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } -smol_str = "0.2" +smol_str = { version = "0.2", default-features = false, optional = true } +log = { version = "0.4", default-features = false } [lints] workspace = true diff --git a/crates/bevy_input/src/common_conditions.rs b/crates/bevy_input/src/common_conditions.rs index bc2e161b1b55a..560ea693d982c 100644 --- a/crates/bevy_input/src/common_conditions.rs +++ b/crates/bevy_input/src/common_conditions.rs @@ -2,7 +2,7 @@ use crate::ButtonInput; use bevy_ecs::system::Res; use core::hash::Hash; -/// Stateful run condition that can be toggled via a input press using [`ButtonInput::just_pressed`]. +/// Stateful run condition that can be toggled via an input press using [`ButtonInput::just_pressed`]. /// /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, Update}; diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 5d057381fe11d..d51a087eee1fa 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -1,6 +1,9 @@ //! The gamepad input functionality. +use core::{ops::RangeInclusive, time::Duration}; + use crate::{Axis, ButtonInput, ButtonState}; +use alloc::string::String; #[cfg(feature = "bevy_reflect")] use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::{ @@ -12,16 +15,15 @@ use bevy_ecs::{ prelude::require, system::{Commands, Query}, }; +use bevy_math::ops; use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_utils::{ - tracing::{info, warn}, - Duration, HashMap, -}; +use bevy_utils::HashMap; use derive_more::derive::From; +use log::{info, warn}; use thiserror::Error; /// A gamepad event. @@ -30,7 +32,7 @@ use thiserror::Error; /// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when /// the in-frame relative ordering of events is important. /// -/// This event is produced by `bevy_input` +/// This event is produced by `bevy_input`. #[derive(Event, Debug, Clone, PartialEq, From)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -54,11 +56,11 @@ 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, Reflect, From)] -#[reflect(Debug, PartialEq)] +#[derive(Event, Debug, Clone, PartialEq, From)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub enum RawGamepadEvent { @@ -70,12 +72,12 @@ pub enum RawGamepadEvent { Axis(RawGamepadAxisChangedEvent), } -/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`] -#[derive(Event, Debug, Copy, Clone, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] +/// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. +#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub struct RawGamepadButtonChangedEvent { @@ -98,12 +100,12 @@ impl RawGamepadButtonChangedEvent { } } -/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`] -#[derive(Event, Debug, Copy, Clone, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] +/// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. +#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub struct RawGamepadAxisChangedEvent { @@ -128,11 +130,11 @@ impl RawGamepadAxisChangedEvent { /// A Gamepad connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] +#[derive(Event, Debug, Clone, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub struct GamepadConnectionEvent { @@ -151,23 +153,23 @@ impl GamepadConnectionEvent { } } - /// Is the gamepad connected? + /// Whether the gamepad is connected. pub fn connected(&self) -> bool { matches!(self.connection, GamepadConnection::Connected { .. }) } - /// Is the gamepad disconnected? + /// Whether the gamepad is disconnected. pub fn disconnected(&self) -> bool { !self.connected() } } -/// [`GamepadButton`] event triggered by a digital state change -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq, Reflect)] -#[reflect(Debug, PartialEq)] +/// [`GamepadButton`] event triggered by a digital state change. +#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub struct GamepadButtonStateChangedEvent { @@ -180,7 +182,7 @@ pub struct GamepadButtonStateChangedEvent { } impl GamepadButtonStateChangedEvent { - /// Creates a new [`GamepadButtonStateChangedEvent`] + /// Creates a new [`GamepadButtonStateChangedEvent`]. pub fn new(entity: Entity, button: GamepadButton, state: ButtonState) -> Self { Self { entity, @@ -190,12 +192,12 @@ impl GamepadButtonStateChangedEvent { } } -/// [`GamepadButton`] event triggered by an analog state change -#[derive(Event, Debug, Clone, Copy, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] +/// [`GamepadButton`] event triggered by an analog state change. +#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] pub struct GamepadButtonChangedEvent { @@ -205,12 +207,12 @@ pub struct GamepadButtonChangedEvent { pub button: GamepadButton, /// The pressed state of the button. pub state: ButtonState, - /// The analog value of the button. + /// The analog value of the button (rescaled to be in the 0.0..=1.0 range). pub value: f32, } impl GamepadButtonChangedEvent { - /// Creates a new [`GamepadButtonChangedEvent`] + /// Creates a new [`GamepadButtonChangedEvent`]. pub fn new(entity: Entity, button: GamepadButton, state: ButtonState, value: f32) -> Self { Self { entity, @@ -221,12 +223,12 @@ impl GamepadButtonChangedEvent { } } -/// [`GamepadAxis`] event triggered by an analog state change -#[derive(Event, Debug, Clone, Copy, PartialEq, Reflect)] -#[reflect(Debug, PartialEq)] +/// [`GamepadAxis`] event triggered by an analog state change. +#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + all(feature = "bevy_reflect", feature = "serialize"), reflect(Serialize, Deserialize) )] pub struct GamepadAxisChangedEvent { @@ -234,12 +236,12 @@ pub struct GamepadAxisChangedEvent { pub entity: Entity, /// The gamepad axis assigned to the event. pub axis: GamepadAxis, - /// The value of this axis. + /// The value of this axis (rescaled to account for axis settings). pub value: f32, } impl GamepadAxisChangedEvent { - /// Creates a new [`GamepadAxisChangedEvent`] + /// Creates a new [`GamepadAxisChangedEvent`]. pub fn new(entity: Entity, axis: GamepadAxis, value: f32) -> Self { Self { entity, @@ -339,12 +341,10 @@ pub struct Gamepad { /// The USB vendor ID as assigned by the USB-IF, if available. pub(crate) vendor_id: Option, - /// The USB product ID as assigned by the [vendor], if available. - /// - /// [vendor]: Self::vendor_id + /// The USB product ID as assigned by the [vendor][Self::vendor_id], if available. pub(crate) product_id: Option, - /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state. pub(crate) digital: ButtonInput, /// [`Axis`] of [`GamepadButton`] representing their analog state. @@ -378,7 +378,7 @@ impl Gamepad { self.analog.get_unclamped(input.into()) } - /// Returns the left stick as a [`Vec2`] + /// Returns the left stick as a [`Vec2`]. pub fn left_stick(&self) -> Vec2 { Vec2 { x: self.get(GamepadAxis::LeftStickX).unwrap_or(0.0), @@ -386,7 +386,7 @@ impl Gamepad { } } - /// Returns the right stick as a [`Vec2`] + /// Returns the right stick as a [`Vec2`]. pub fn right_stick(&self) -> Vec2 { Vec2 { x: self.get(GamepadAxis::RightStickX).unwrap_or(0.0), @@ -394,7 +394,7 @@ impl Gamepad { } } - /// Returns the directional pad as a [`Vec2`] + /// Returns the directional pad as a [`Vec2`]. pub fn dpad(&self) -> Vec2 { Vec2 { x: self.get(GamepadButton::DPadRight).unwrap_or(0.0) @@ -480,14 +480,12 @@ impl Gamepad { self.digital.get_just_released() } - /// Returns an iterator over all analog [axes]. - /// - /// [axes]: GamepadInput + /// Returns an iterator over all analog [axes][GamepadInput]. pub fn get_analog_axes(&self) -> impl Iterator { self.analog.all_axes() } - /// [`ButtonInput`] of [`GamepadButton`] representing their digital state + /// [`ButtonInput`] of [`GamepadButton`] representing their digital state. pub fn digital(&self) -> &ButtonInput { &self.digital } @@ -531,7 +529,7 @@ impl Default for Gamepad { /// /// ## Usage /// -/// This is used to determine which button has changed its value when receiving gamepad button events +/// This is used to determine which button has changed its value when receiving gamepad button events. /// It is also used in the [`Gamepad`] component. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[cfg_attr( @@ -593,7 +591,7 @@ pub enum GamepadButton { } impl GamepadButton { - /// Returns an array of all the standard [`GamepadButton`] + /// Returns an array of all the standard [`GamepadButton`]. pub const fn all() -> [GamepadButton; 19] { [ GamepadButton::South, @@ -619,7 +617,7 @@ impl GamepadButton { } } -/// Represents gamepad input types that are mapped in the range [-1.0, 1.0] +/// Represents gamepad input types that are mapped in the range [-1.0, 1.0]. /// /// ## Usage /// @@ -665,14 +663,14 @@ impl GamepadAxis { } } -/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`] +/// Encapsulation over [`GamepadAxis`] and [`GamepadButton`]. // This is done so Gamepad can share a single Axis and simplifies the API by having only one get/get_unclamped method #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq, From)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] pub enum GamepadInput { - /// A [`GamepadAxis`] + /// A [`GamepadAxis`]. Axis(GamepadAxis), - /// A [`GamepadButton`] + /// A [`GamepadButton`]. Button(GamepadButton), } @@ -928,9 +926,9 @@ impl ButtonSettings { /// threshold for an axis. /// Values that are higher than `livezone_upperbound` will be rounded up to 1.0. /// Values that are lower than `livezone_lowerbound` will be rounded down to -1.0. -/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded -/// to 0.0. -/// Otherwise, values will not be rounded. +/// Values that are in-between `deadzone_lowerbound` and `deadzone_upperbound` will be rounded to 0.0. +/// Otherwise, values will be linearly rescaled to fit into the sensitivity range. +/// For example, a value that is one fourth of the way from `deadzone_upperbound` to `livezone_upperbound` will be scaled to 0.25. /// /// The valid range is `[-1.0, 1.0]`. #[derive(Debug, Clone, PartialEq)] @@ -1041,7 +1039,7 @@ impl AxisSettings { /// /// # Errors /// - /// If the value passed is less than the dead zone upper bound, + /// If the value passed is less than the deadzone upper bound, /// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`. /// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`. pub fn try_set_livezone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { @@ -1117,7 +1115,7 @@ impl AxisSettings { /// /// # Errors /// - /// If the value passed is less than the dead zone lower bound, + /// If the value passed is less than the deadzone lower bound, /// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`. /// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`. pub fn try_set_livezone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { @@ -1213,39 +1211,135 @@ impl AxisSettings { } /// Clamps the `raw_value` according to the `AxisSettings`. - pub fn clamp(&self, new_value: f32) -> f32 { - if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound { + pub fn clamp(&self, raw_value: f32) -> f32 { + if self.deadzone_lowerbound <= raw_value && raw_value <= self.deadzone_upperbound { 0.0 - } else if new_value >= self.livezone_upperbound { + } else if raw_value >= self.livezone_upperbound { 1.0 - } else if new_value <= self.livezone_lowerbound { + } else if raw_value <= self.livezone_lowerbound { -1.0 } else { - new_value + raw_value } } - /// Determines whether the change from `old_value` to `new_value` should + /// Determines whether the change from `old_raw_value` to `new_raw_value` should /// be registered as a change, according to the [`AxisSettings`]. - fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { - if old_value.is_none() { - return true; + fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option) -> bool { + match old_raw_value { + None => true, + Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold, } - - f32::abs(new_value - old_value.unwrap()) > self.threshold } - /// Filters the `new_value` based on the `old_value`, according to the [`AxisSettings`]. + /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`AxisSettings`]. /// - /// Returns the clamped `new_value` if the change exceeds the settings threshold, + /// Returns the clamped and scaled `new_raw_value` if the change exceeds the settings threshold, /// and `None` otherwise. - pub fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = self.clamp(new_value); + fn filter( + &self, + new_raw_value: f32, + old_raw_value: Option, + ) -> Option { + let clamped_unscaled = self.clamp(new_raw_value); + match self.should_register_change(clamped_unscaled, old_raw_value) { + true => Some(FilteredAxisPosition { + scaled: self.get_axis_position_from_value(clamped_unscaled), + raw: new_raw_value, + }), + false => None, + } + } - if self.should_register_change(new_value, old_value) { - return Some(new_value); + #[inline(always)] + fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisWithDeadZonePosition { + if value < self.deadzone_upperbound && value > self.deadzone_lowerbound { + ScaledAxisWithDeadZonePosition::Dead + } else if value > self.livezone_upperbound { + ScaledAxisWithDeadZonePosition::AboveHigh + } else if value < self.livezone_lowerbound { + ScaledAxisWithDeadZonePosition::BelowLow + } else if value >= self.deadzone_upperbound { + ScaledAxisWithDeadZonePosition::High(linear_remapping( + value, + self.deadzone_upperbound..=self.livezone_upperbound, + 0.0..=1.0, + )) + } else if value <= self.deadzone_lowerbound { + ScaledAxisWithDeadZonePosition::Low(linear_remapping( + value, + self.livezone_lowerbound..=self.deadzone_lowerbound, + -1.0..=0.0, + )) + } else { + unreachable!(); + } + } +} + +/// A linear remapping of `value` from `old` to `new`. +fn linear_remapping(value: f32, old: RangeInclusive, new: RangeInclusive) -> f32 { + // https://stackoverflow.com/a/929104 + ((value - old.start()) / (old.end() - old.start())) * (new.end() - new.start()) + new.start() +} + +#[derive(Debug, Clone, Copy)] +/// Deadzone-aware axis position. +enum ScaledAxisWithDeadZonePosition { + /// The input clipped below the valid range of the axis. + BelowLow, + /// The input is lower than the deadzone. + Low(f32), + /// The input falls within the deadzone, meaning it is counted as 0. + Dead, + /// The input is higher than the deadzone. + High(f32), + /// The input clipped above the valid range of the axis. + AboveHigh, +} + +struct FilteredAxisPosition { + scaled: ScaledAxisWithDeadZonePosition, + raw: f32, +} + +impl ScaledAxisWithDeadZonePosition { + /// Converts the value into a float in the range [-1, 1]. + fn to_f32(self) -> f32 { + match self { + ScaledAxisWithDeadZonePosition::BelowLow => -1., + ScaledAxisWithDeadZonePosition::Low(scaled) + | ScaledAxisWithDeadZonePosition::High(scaled) => scaled, + ScaledAxisWithDeadZonePosition::Dead => 0., + ScaledAxisWithDeadZonePosition::AboveHigh => 1., + } + } +} + +#[derive(Debug, Clone, Copy)] +/// Low/High-aware axis position. +enum ScaledAxisPosition { + /// The input fell short of the "low" value. + ClampedLow, + /// The input was in the normal range. + Scaled(f32), + /// The input surpassed the "high" value. + ClampedHigh, +} + +struct FilteredButtonAxisPosition { + scaled: ScaledAxisPosition, + raw: f32, +} + +impl ScaledAxisPosition { + /// Converts the value into a float in the range [0, 1]. + fn to_f32(self) -> f32 { + match self { + ScaledAxisPosition::ClampedLow => 0., + ScaledAxisPosition::Scaled(scaled) => scaled, + ScaledAxisPosition::ClampedHigh => 1., } - None } } @@ -1300,27 +1394,48 @@ impl ButtonAxisSettings { raw_value } - /// Determines whether the change from an `old_value` to a `new_value` should + /// Determines whether the change from an `old_raw_value` to a `new_raw_value` should /// be registered as a change event, according to the specified settings. - fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { - if old_value.is_none() { - return true; + fn should_register_change(&self, new_raw_value: f32, old_raw_value: Option) -> bool { + match old_raw_value { + None => true, + Some(old_raw_value) => ops::abs(new_raw_value - old_raw_value) >= self.threshold, } - - f32::abs(new_value - old_value.unwrap()) > self.threshold } - /// Filters the `new_value` based on the `old_value`, according to the [`ButtonAxisSettings`]. + /// Filters the `new_raw_value` based on the `old_raw_value`, according to the [`ButtonAxisSettings`]. /// - /// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change + /// Returns the clamped and scaled `new_raw_value`, according to the [`ButtonAxisSettings`], if the change /// exceeds the settings threshold, and `None` otherwise. - pub fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = self.clamp(new_value); + fn filter( + &self, + new_raw_value: f32, + old_raw_value: Option, + ) -> Option { + let clamped_unscaled = self.clamp(new_raw_value); + match self.should_register_change(clamped_unscaled, old_raw_value) { + true => Some(FilteredButtonAxisPosition { + scaled: self.get_axis_position_from_value(clamped_unscaled), + raw: new_raw_value, + }), + false => None, + } + } - if self.should_register_change(new_value, old_value) { - return Some(new_value); + /// Clamps and scales the `value` according to the specified settings. + /// + /// If the `value` is: + /// - lower than or equal to `low` it will be rounded to 0.0. + /// - higher than or equal to `high` it will be rounded to 1.0. + /// - Otherwise, it will be scaled from (low, high) to (0, 1). + fn get_axis_position_from_value(&self, value: f32) -> ScaledAxisPosition { + if value <= self.low { + ScaledAxisPosition::ClampedLow + } else if value >= self.high { + ScaledAxisPosition::ClampedHigh + } else { + ScaledAxisPosition::Scaled(linear_remapping(value, self.low..=self.high, 0.0..=1.0)) } - None } } @@ -1328,7 +1443,7 @@ impl ButtonAxisSettings { /// /// On connection, adds the components representing a [`Gamepad`] to the entity. /// On disconnection, removes the [`Gamepad`] and other related components. -/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection +/// Entities are left alive and might leave components like [`GamepadSettings`] to preserve state in the case of a reconnection. /// /// ## Note /// @@ -1346,7 +1461,7 @@ pub fn gamepad_connection_system( product_id, } => { let Some(mut gamepad) = commands.get_entity(id) else { - warn!("Gamepad {:} removed before handling connection event.", id); + warn!("Gamepad {} removed before handling connection event.", id); continue; }; gamepad.insert(( @@ -1357,18 +1472,18 @@ pub fn gamepad_connection_system( ..Default::default() }, )); - info!("Gamepad {:?} connected.", id); + info!("Gamepad {} connected.", id); } GamepadConnection::Disconnected => { let Some(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 {} removed before handling disconnection event. You can ignore this if you manually removed it.", id); 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 {} disconnected.", id); } } } @@ -1441,9 +1556,9 @@ pub fn gamepad_event_processing_system( else { continue; }; - - gamepad_axis.analog.set(axis, filtered_value); - let send_event = GamepadAxisChangedEvent::new(gamepad, axis, filtered_value); + gamepad_axis.analog.set(axis, filtered_value.raw); + let send_event = + GamepadAxisChangedEvent::new(gamepad, axis, filtered_value.scaled.to_f32()); processed_axis_events.send(send_event); processed_events.send(GamepadEvent::from(send_event)); } @@ -1463,9 +1578,9 @@ pub fn gamepad_event_processing_system( continue; }; let button_settings = settings.get_button_settings(button); - gamepad_buttons.analog.set(button, filtered_value); + gamepad_buttons.analog.set(button, filtered_value.raw); - if button_settings.is_released(filtered_value) { + if button_settings.is_released(filtered_value.raw) { // Check if button was previously pressed if gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( @@ -1477,7 +1592,7 @@ pub fn gamepad_event_processing_system( // We don't have to check if the button was previously pressed here // because that check is performed within Input::release() gamepad_buttons.digital.release(button); - } else if button_settings.is_pressed(filtered_value) { + } else if button_settings.is_pressed(filtered_value.raw) { // Check if button was previously not pressed if !gamepad_buttons.pressed(button) { processed_digital_events.send(GamepadButtonStateChangedEvent::new( @@ -1494,8 +1609,12 @@ pub fn gamepad_event_processing_system( } else { ButtonState::Released }; - let send_event = - GamepadButtonChangedEvent::new(gamepad, button, button_state, filtered_value); + let send_event = GamepadButtonChangedEvent::new( + gamepad, + button, + button_state, + filtered_value.scaled.to_f32(), + ); processed_analog_events.send(send_event); processed_events.send(GamepadEvent::from(send_event)); } @@ -1574,7 +1693,7 @@ impl GamepadRumbleIntensity { /// ``` /// # use bevy_input::gamepad::{Gamepad, GamepadRumbleRequest, GamepadRumbleIntensity}; /// # use bevy_ecs::prelude::{EventWriter, Res, Query, Entity, With}; -/// # use bevy_utils::Duration; +/// # use core::time::Duration; /// fn rumble_gamepad_system( /// mut rumble_requests: EventWriter, /// gamepads: Query>, @@ -1641,6 +1760,7 @@ mod tests { RawGamepadButtonChangedEvent, RawGamepadEvent, }; use crate::ButtonState; + use alloc::string::ToString; use bevy_app::{App, PreUpdate}; use bevy_ecs::entity::Entity; use bevy_ecs::event::Events; @@ -1648,130 +1768,161 @@ mod tests { fn test_button_axis_settings_filter( settings: ButtonAxisSettings, - new_value: f32, - old_value: Option, + new_raw_value: f32, + old_raw_value: Option, expected: Option, ) { - let actual = settings.filter(new_value, old_value); + let actual = settings + .filter(new_raw_value, old_raw_value) + .map(|f| f.scaled.to_f32()); assert_eq!( expected, actual, - "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", + "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}", ); } #[test] fn test_button_axis_settings_default_filter() { let cases = [ + // clamped (1.0, None, Some(1.0)), (0.99, None, Some(1.0)), (0.96, None, Some(1.0)), (0.95, None, Some(1.0)), - (0.9499, None, Some(0.9499)), - (0.84, None, Some(0.84)), - (0.43, None, Some(0.43)), - (0.05001, None, Some(0.05001)), + // linearly rescaled from 0.05..=0.95 to 0.0..=1.0 + (0.9499, None, Some(0.9998889)), + (0.84, None, Some(0.87777776)), + (0.43, None, Some(0.42222223)), + (0.05001, None, Some(0.000011109644)), + // clamped (0.05, None, Some(0.0)), (0.04, None, Some(0.0)), (0.01, None, Some(0.0)), (0.0, None, Some(0.0)), ]; - for (new_value, old_value, expected) in cases { + for (new_raw_value, old_raw_value, expected) in cases { let settings = ButtonAxisSettings::default(); - test_button_axis_settings_filter(settings, new_value, old_value, expected); + test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } } #[test] - fn test_button_axis_settings_default_filter_with_old_value() { + fn test_button_axis_settings_default_filter_with_old_raw_value() { let cases = [ - (0.43, Some(0.44001), Some(0.43)), + // 0.43 gets rescaled to 0.42222223 (0.05..=0.95 -> 0.0..=1.0) + (0.43, Some(0.44001), Some(0.42222223)), (0.43, Some(0.44), None), (0.43, Some(0.43), None), - (0.43, Some(0.41999), Some(0.43)), - (0.43, Some(0.17), Some(0.43)), - (0.43, Some(0.84), Some(0.43)), + (0.43, Some(0.41999), Some(0.42222223)), + (0.43, Some(0.17), Some(0.42222223)), + (0.43, Some(0.84), Some(0.42222223)), (0.05, Some(0.055), Some(0.0)), (0.95, Some(0.945), Some(1.0)), ]; - for (new_value, old_value, expected) in cases { + for (new_raw_value, old_raw_value, expected) in cases { let settings = ButtonAxisSettings::default(); - test_button_axis_settings_filter(settings, new_value, old_value, expected); + test_button_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } } fn test_axis_settings_filter( settings: AxisSettings, - new_value: f32, - old_value: Option, + new_raw_value: f32, + old_raw_value: Option, expected: Option, ) { - let actual = settings.filter(new_value, old_value); + let actual = settings.filter(new_raw_value, old_raw_value); assert_eq!( - expected, actual, - "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", + expected, actual.map(|f| f.scaled.to_f32()), + "Testing filtering for {settings:?} with new_raw_value = {new_raw_value:?}, old_raw_value = {old_raw_value:?}", ); } #[test] fn test_axis_settings_default_filter() { + // new (raw), expected (rescaled linearly) let cases = [ + // high enough to round to 1.0 (1.0, Some(1.0)), (0.99, Some(1.0)), (0.96, Some(1.0)), (0.95, Some(1.0)), - (0.9499, Some(0.9499)), - (0.84, Some(0.84)), - (0.43, Some(0.43)), - (0.05001, Some(0.05001)), + // for the following, remember that 0.05 is the "low" value and 0.95 is the "high" value + // barely below the high value means barely below 1 after scaling + (0.9499, Some(0.9998889)), // scaled as: (0.9499 - 0.05) / (0.95 - 0.05) + (0.84, Some(0.87777776)), // scaled as: (0.84 - 0.05) / (0.95 - 0.05) + (0.43, Some(0.42222223)), // scaled as: (0.43 - 0.05) / (0.95 - 0.05) + // barely above the low value means barely above 0 after scaling + (0.05001, Some(0.000011109644)), // scaled as: (0.05001 - 0.05) / (0.95 - 0.05) + // low enough to be rounded to 0 (dead zone) (0.05, Some(0.0)), (0.04, Some(0.0)), (0.01, Some(0.0)), (0.0, Some(0.0)), + // same exact tests as above, but below 0 (bottom half of the dead zone and live zone) + // low enough to be rounded to -1 (-1.0, Some(-1.0)), (-0.99, Some(-1.0)), (-0.96, Some(-1.0)), (-0.95, Some(-1.0)), - (-0.9499, Some(-0.9499)), - (-0.84, Some(-0.84)), - (-0.43, Some(-0.43)), - (-0.05001, Some(-0.05001)), + // scaled inputs + (-0.9499, Some(-0.9998889)), // scaled as: (-0.9499 - -0.05) / (-0.95 - -0.05) + (-0.84, Some(-0.87777776)), // scaled as: (-0.84 - -0.05) / (-0.95 - -0.05) + (-0.43, Some(-0.42222226)), // scaled as: (-0.43 - -0.05) / (-0.95 - -0.05) + (-0.05001, Some(-0.000011146069)), // scaled as: (-0.05001 - -0.05) / (-0.95 - -0.05) + // high enough to be rounded to 0 (dead zone) (-0.05, Some(0.0)), (-0.04, Some(0.0)), (-0.01, Some(0.0)), ]; - for (new_value, expected) in cases { + for (new_raw_value, expected) in cases { let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); - test_axis_settings_filter(settings, new_value, None, expected); + test_axis_settings_filter(settings, new_raw_value, None, expected); } } #[test] - fn test_axis_settings_default_filter_with_old_values() { + fn test_axis_settings_default_filter_with_old_raw_values() { + let threshold = 0.01; + // expected values are hardcoded to be rescaled to from 0.05..=0.95 to 0.0..=1.0 + // new (raw), old (raw), expected let cases = [ - (0.43, Some(0.44001), Some(0.43)), - (0.43, Some(0.44), None), - (0.43, Some(0.43), None), - (0.43, Some(0.41999), Some(0.43)), - (0.43, Some(0.17), Some(0.43)), - (0.43, Some(0.84), Some(0.43)), - (0.05, Some(0.055), Some(0.0)), - (0.95, Some(0.945), Some(1.0)), - (-0.43, Some(-0.44001), Some(-0.43)), - (-0.43, Some(-0.44), None), - (-0.43, Some(-0.43), None), - (-0.43, Some(-0.41999), Some(-0.43)), - (-0.43, Some(-0.17), Some(-0.43)), - (-0.43, Some(-0.84), Some(-0.43)), - (-0.05, Some(-0.055), Some(0.0)), - (-0.95, Some(-0.945), Some(-1.0)), + // enough increase to change + (0.43, Some(0.43 + threshold * 1.1), Some(0.42222223)), + // enough decrease to change + (0.43, Some(0.43 - threshold * 1.1), Some(0.42222223)), + // not enough increase to change + (0.43, Some(0.43 + threshold * 0.9), None), + // not enough decrease to change + (0.43, Some(0.43 - threshold * 0.9), None), + // enough increase to change + (-0.43, Some(-0.43 + threshold * 1.1), Some(-0.42222226)), + // enough decrease to change + (-0.43, Some(-0.43 - threshold * 1.1), Some(-0.42222226)), + // not enough increase to change + (-0.43, Some(-0.43 + threshold * 0.9), None), + // not enough decrease to change + (-0.43, Some(-0.43 - threshold * 0.9), None), + // test upper deadzone logic + (0.05, Some(0.0), None), + (0.06, Some(0.0), Some(0.0111111095)), + // test lower deadzone logic + (-0.05, Some(0.0), None), + (-0.06, Some(0.0), Some(-0.011111081)), + // test upper livezone logic + (0.95, Some(1.0), None), + (0.94, Some(1.0), Some(0.9888889)), + // test lower livezone logic + (-0.95, Some(-1.0), None), + (-0.94, Some(-1.0), Some(-0.9888889)), ]; - for (new_value, old_value, expected) in cases { - let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.01).unwrap(); - test_axis_settings_filter(settings, new_value, old_value, expected); + for (new_raw_value, old_raw_value, expected) in cases { + let settings = AxisSettings::new(-0.95, -0.05, 0.05, 0.95, threshold).unwrap(); + test_axis_settings_filter(settings, new_raw_value, old_raw_value, expected); } } diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 0bd6f41a84750..052584706223c 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -72,8 +72,14 @@ use bevy_ecs::{ event::{Event, EventReader}, system::ResMut, }; + #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; + +#[cfg(not(feature = "smol_str"))] +use alloc::string::String as SmolStr; + +#[cfg(feature = "smol_str")] use smol_str::SmolStr; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] @@ -232,7 +238,10 @@ pub enum NativeKeyCode { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[allow(clippy::doc_markdown)] // Clippy doesn't like our use of . +#[expect( + clippy::doc_markdown, + reason = "We use camel-case words inside `` tags to represent keyboard keys, which are not identifiers that we should be putting inside backticks." +)] #[repr(u32)] pub enum KeyCode { /// This variant is used when the key cannot be translated to any other variant. @@ -758,7 +767,10 @@ pub enum NativeKey { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[allow(clippy::doc_markdown)] // Clippy doesn't like our use of . +#[expect( + clippy::doc_markdown, + reason = "We use camel-case words inside `` tags to represent keyboard keys, which are not identifiers that we should be putting inside backticks." +)] pub enum Key { /// A key string that corresponds to the character typed by the user, taking into account the /// user’s current locale setting, and any system-level keyboard mapping overrides that are in diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 7e0225cf7521c..2da2c89cce8ca 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -4,6 +4,7 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] +#![no_std] //! Input functionality for the [Bevy game engine](https://bevyengine.org/). //! @@ -11,6 +12,11 @@ //! //! `bevy` currently supports keyboard, mouse, gamepad, and touch inputs. +#[cfg(feature = "std")] +extern crate std; + +extern crate alloc; + mod axis; mod button_input; /// Common run conditions diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index eb5420225fc5a..628d50e4bf060 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -1,21 +1,40 @@ [package] name = "bevy_input_focus" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Keyboard focus management" homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] -rust-version = "1.76.0" +rust-version = "1.83.0" + +[features] +default = ["bevy_reflect"] + +## Adds runtime reflection support using `bevy_reflect`. +bevy_reflect = [ + "dep:bevy_reflect", + "bevy_app/bevy_reflect", + "bevy_ecs/bevy_reflect", + "bevy_math/bevy_reflect", +] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.15.0-dev", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false } -bevy_input = { path = "../bevy_input", version = "0.15.0-dev", default-features = false } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", default-features = false } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev", default-features = false } +# 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_hierarchy = { path = "../bevy_hierarchy", 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 = [ + "glam", +], default-features = false, optional = true } + +# other +thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] smol_str = "0.2" diff --git a/crates/bevy_input_focus/src/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 10dea5f892461..7de2cbc23e35a 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,8 @@ //! Contains the [`AutoFocus`] component and related machinery. use bevy_ecs::{component::ComponentId, prelude::*, world::DeferredWorld}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{prelude::*, Reflect}; use crate::InputFocus; @@ -12,6 +14,11 @@ use crate::InputFocus; /// The focus is swapped when this component is added /// or an entity with this component is spawned. #[derive(Debug, Default, Component, Copy, Clone)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, Default, Component) +)] #[component(on_add = on_auto_focus_added)] pub struct AutoFocus; diff --git a/crates/bevy_input_focus/src/directional_navigation.rs b/crates/bevy_input_focus/src/directional_navigation.rs new file mode 100644 index 0000000000000..17114ef11a075 --- /dev/null +++ b/crates/bevy_input_focus/src/directional_navigation.rs @@ -0,0 +1,423 @@ +//! A navigation framework for moving between focusable elements based on directional input. +//! +//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!), +//! they are generally both slow and frustrating to use. +//! Instead, directional inputs should provide a direct way to snap between focusable elements. +//! +//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track +//! the current focus. +//! +//! Navigating between focusable entities (commonly UI nodes) is done by +//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method +//! from the [`DirectionalNavigation`] system parameter. +//! +//! Under the hood, the [`DirectionalNavigationMap`] stores a directed graph of focusable entities. +//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing flexibility and required precision. +//! For now, this graph must be built manually, but in the future, it could be generated automatically. + +use bevy_app::prelude::*; +use bevy_ecs::{ + entity::{EntityHashMap, EntityHashSet}, + prelude::*, + system::SystemParam, +}; +use bevy_math::CompassOctant; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{prelude::*, Reflect}; +use thiserror::Error; + +use crate::InputFocus; + +/// A plugin that sets up the directional navigation systems and resources. +#[derive(Default)] +pub struct DirectionalNavigationPlugin; + +impl Plugin for DirectionalNavigationPlugin { + fn build(&self, app: &mut App) { + app.init_resource::(); + + #[cfg(feature = "bevy_reflect")] + app.register_type::() + .register_type::(); + } +} + +/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`]. +#[derive(Default, Debug, Clone, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Default, Debug, PartialEq) +)] +pub struct NavNeighbors { + /// The array of neighbors, one for each [`CompassOctant`]. + /// The mapping between array elements and directions is determined by [`CompassOctant::to_index`]. + /// + /// If no neighbor exists in a given direction, the value will be [`None`]. + /// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`] + /// will be more ergonomic than directly accessing this array. + pub neighbors: [Option; 8], +} + +impl NavNeighbors { + /// An empty set of neighbors. + pub const EMPTY: NavNeighbors = NavNeighbors { + neighbors: [None; 8], + }; + + /// Get the neighbor for a given [`CompassOctant`]. + pub const fn get(&self, octant: CompassOctant) -> Option { + self.neighbors[octant.to_index()] + } + + /// Set the neighbor for a given [`CompassOctant`]. + pub const fn set(&mut self, octant: CompassOctant, entity: Entity) { + self.neighbors[octant.to_index()] = Some(entity); + } +} + +/// A resource that stores the traversable graph of focusable entities. +/// +/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`]. +/// +/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be: +/// +/// - **Connected**: Every focusable entity should be reachable from every other focusable entity. +/// - **Symmetric**: If entity A is a neighbor of entity B, then entity B should be a neighbor of entity A, ideally in the reverse direction. +/// - **Physical**: The direction of navigation should match the layout of the entities when possible, +/// although looping around the edges of the screen is also acceptable. +/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead. +/// +/// For now, this graph must be built manually, and the developer is responsible for ensuring that it meets the above criteria. +#[derive(Resource, Debug, Default, Clone, PartialEq)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Resource, Debug, Default, PartialEq) +)] +pub struct DirectionalNavigationMap { + /// A directed graph of focusable entities. + /// + /// Pass in the current focus as a key, and get back a collection of up to 8 neighbors, + /// each keyed by a [`CompassOctant`]. + pub neighbors: EntityHashMap, +} + +impl DirectionalNavigationMap { + /// Adds a new entity to the navigation map, overwriting any existing neighbors for that entity. + /// + /// Removes an entity from the navigation map, including all connections to and from it. + /// + /// Note that this is an O(n) operation, where n is the number of entities in the map, + /// as we must iterate over each entity to check for connections to the removed entity. + /// + /// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead. + pub fn remove(&mut self, entity: Entity) { + self.neighbors.remove(&entity); + + for node in self.neighbors.values_mut() { + for neighbor in node.neighbors.iter_mut() { + if *neighbor == Some(entity) { + *neighbor = None; + } + } + } + } + + /// Removes a collection of entities from the navigation map. + /// + /// While this is still an O(n) operation, where n is the number of entities in the map, + /// it is more efficient than calling [`remove`](Self::remove) multiple times, + /// as we can check for connections to all removed entities in a single pass. + /// + /// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`]. + pub fn remove_multiple(&mut self, entities: EntityHashSet) { + for entity in &entities { + self.neighbors.remove(entity); + } + + for node in self.neighbors.values_mut() { + for neighbor in node.neighbors.iter_mut() { + if let Some(entity) = *neighbor { + if entities.contains(&entity) { + *neighbor = None; + } + } + } + } + } + + /// Completely clears the navigation map, removing all entities and connections. + pub fn clear(&mut self) { + self.neighbors.clear(); + } + + /// Adds an edge between two entities in the navigation map. + /// Any existing edge from A in the provided direction will be overwritten. + /// + /// The reverse edge will not be added, so navigation will only be possible in one direction. + /// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead. + pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) { + self.neighbors + .entry(a) + .or_insert(NavNeighbors::EMPTY) + .set(direction, b); + } + + /// Adds a symmetrical edge between two entities in the navigation map. + /// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant. + /// + /// Any existing connections between the two entities will be overwritten. + pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) { + self.add_edge(a, b, direction); + self.add_edge(b, a, direction.opposite()); + } + + /// Add symmetrical edges between each consecutive pair of entities in the provided slice. + /// + /// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity. + pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) { + for pair in entities.windows(2) { + self.add_symmetrical_edge(pair[0], pair[1], direction); + } + } + + /// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end. + /// + /// This is useful for creating a circular navigation path between a set of entities, such as a menu. + pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) { + for i in 0..entities.len() { + let a = entities[i]; + let b = entities[(i + 1) % entities.len()]; + self.add_symmetrical_edge(a, b, direction); + } + } + + /// Gets the entity in a given direction from the current focus, if any. + pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option { + self.neighbors + .get(&focus) + .and_then(|neighbors| neighbors.get(octant)) + } + + /// Looks up the neighbors of a given entity. + /// + /// If the entity is not in the map, [`None`] will be returned. + /// Note that the set of neighbors is not guaranteed to be non-empty though! + pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> { + self.neighbors.get(&entity) + } +} + +/// A system parameter for navigating between focusable entities in a directional way. +#[derive(SystemParam, Debug)] +pub struct DirectionalNavigation<'w> { + /// The currently focused entity. + pub focus: ResMut<'w, InputFocus>, + /// The navigation map containing the connections between entities. + pub map: Res<'w, DirectionalNavigationMap>, +} + +impl DirectionalNavigation<'_> { + /// Navigates to the neighbor in a given direction from the current focus, if any. + /// + /// Returns the new focus if successful. + /// Returns an error if there is no focus set or if there is no neighbor in the requested direction. + /// + /// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call. + pub fn navigate( + &mut self, + octant: CompassOctant, + ) -> Result { + if let Some(current_focus) = self.focus.0 { + if let Some(new_focus) = self.map.get_neighbor(current_focus, octant) { + self.focus.set(new_focus); + Ok(new_focus) + } else { + Err(DirectionalNavigationError::NoNeighborInDirection) + } + } else { + Err(DirectionalNavigationError::NoFocus) + } + } +} + +/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation). +#[derive(Debug, PartialEq, Clone, Error)] +pub enum DirectionalNavigationError { + /// No focusable entity is currently set. + #[error("No focusable entity is currently set.")] + NoFocus, + /// No neighbor in the requested direction. + #[error("No neighbor in the requested direction.")] + NoNeighborInDirection, +} + +#[cfg(test)] +mod tests { + use bevy_ecs::system::RunSystemOnce; + + use super::*; + + #[test] + fn setting_and_getting_nav_neighbors() { + let mut neighbors = NavNeighbors::EMPTY; + assert_eq!(neighbors.get(CompassOctant::SouthEast), None); + + neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER); + + for i in 0..8 { + if i == CompassOctant::SouthEast.to_index() { + assert_eq!( + neighbors.get(CompassOctant::SouthEast), + Some(Entity::PLACEHOLDER) + ); + } else { + assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None); + } + } + } + + #[test] + fn simple_set_and_get_navmap() { + let mut world = World::new(); + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + + let mut map = DirectionalNavigationMap::default(); + map.add_edge(a, b, CompassOctant::SouthEast); + + assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b)); + assert_eq!( + map.get_neighbor(b, CompassOctant::SouthEast.opposite()), + None + ); + } + + #[test] + fn symmetrical_edges() { + let mut world = World::new(); + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + + let mut map = DirectionalNavigationMap::default(); + map.add_symmetrical_edge(a, b, CompassOctant::North); + + assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b)); + assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a)); + } + + #[test] + fn remove_nodes() { + let mut world = World::new(); + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + + let mut map = DirectionalNavigationMap::default(); + map.add_edge(a, b, CompassOctant::North); + map.add_edge(b, a, CompassOctant::South); + + assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b)); + assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a)); + + map.remove(b); + + assert_eq!(map.get_neighbor(a, CompassOctant::North), None); + assert_eq!(map.get_neighbor(b, CompassOctant::South), None); + } + + #[test] + fn remove_multiple_nodes() { + 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 map = DirectionalNavigationMap::default(); + map.add_edge(a, b, CompassOctant::North); + map.add_edge(b, a, CompassOctant::South); + map.add_edge(b, c, CompassOctant::East); + map.add_edge(c, b, CompassOctant::West); + + let mut to_remove = EntityHashSet::default(); + to_remove.insert(b); + to_remove.insert(c); + + map.remove_multiple(to_remove); + + assert_eq!(map.get_neighbor(a, CompassOctant::North), None); + assert_eq!(map.get_neighbor(b, CompassOctant::South), None); + assert_eq!(map.get_neighbor(b, CompassOctant::East), None); + assert_eq!(map.get_neighbor(c, CompassOctant::West), None); + } + + #[test] + fn edges() { + 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 map = DirectionalNavigationMap::default(); + map.add_edges(&[a, b, c], CompassOctant::East); + + assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b)); + assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c)); + assert_eq!(map.get_neighbor(c, CompassOctant::East), None); + + assert_eq!(map.get_neighbor(a, CompassOctant::West), None); + assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a)); + assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b)); + } + + #[test] + fn looping_edges() { + 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 map = DirectionalNavigationMap::default(); + map.add_looping_edges(&[a, b, c], CompassOctant::East); + + assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b)); + assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c)); + assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a)); + + assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c)); + assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a)); + assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b)); + } + + #[test] + fn nav_with_system_param() { + 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 map = DirectionalNavigationMap::default(); + map.add_looping_edges(&[a, b, c], CompassOctant::East); + + world.insert_resource(map); + + let mut focus = InputFocus::default(); + focus.set(a); + world.insert_resource(focus); + + assert_eq!(world.resource::().get(), Some(a)); + + fn navigate_east(mut nav: DirectionalNavigation) { + nav.navigate(CompassOctant::East).unwrap(); + } + + world.run_system_once(navigate_east).unwrap(); + assert_eq!(world.resource::().get(), Some(b)); + + world.run_system_once(navigate_east).unwrap(); + assert_eq!(world.resource::().get(), Some(c)); + + world.run_system_once(navigate_east).unwrap(); + assert_eq!(world.resource::().get(), Some(a)); + } +} diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 4b0ec5a763c6f..2e63ad339c830 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -5,16 +5,18 @@ html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -//! Keyboard focus system for Bevy. +//! A UI-centric focus system for Bevy. //! //! This crate provides a system for managing input focus in Bevy applications, including: //! * [`InputFocus`], a resource for tracking which entity has input focus. //! * Methods for getting and setting input focus via [`InputFocus`] and [`IsFocusedHelper`]. //! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity. +//! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`]. //! //! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate, //! which should depend on [`bevy_input_focus`](crate). +pub mod directional_navigation; pub mod tab_navigation; // This module is too small / specific to be exported by the crate, @@ -26,10 +28,12 @@ use bevy_app::{App, Plugin, PreUpdate, Startup}; use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_hierarchy::{HierarchyQueryExt, Parent}; use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{prelude::*, Reflect}; use bevy_window::{PrimaryWindow, Window}; use core::fmt::Debug; -/// Resource representing which entity has input focus, if any. Keyboard events will be +/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be /// dispatched to the current focus entity, or to the primary window if no entity has focus. /// /// Changing the input focus is as easy as modifying this resource. @@ -67,6 +71,11 @@ use core::fmt::Debug; /// } /// ``` #[derive(Clone, Debug, Default, Resource)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, Default, Resource) +)] pub struct InputFocus(pub Option); impl InputFocus { @@ -105,7 +114,10 @@ impl InputFocus { /// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible. /// /// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait. -#[derive(Clone, Debug, Resource)] +/// +/// By default, this resource is set to `false`. +#[derive(Clone, Debug, Resource, Default)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, Resource))] pub struct InputFocusVisible(pub bool); /// A bubble-able user input event that starts at the currently focused entity. @@ -116,6 +128,7 @@ 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 [`InputFocusSet::Dispatch`] system set during [`PreUpdate`]. #[derive(Clone, Debug, Component)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] pub struct FocusedInput { /// The underlying input event. pub input: E, @@ -163,8 +176,8 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, set_initial_focus) - .insert_resource(InputFocus(None)) - .insert_resource(InputFocusVisible(false)) + .init_resource::() + .init_resource::() .add_systems( PreUpdate, ( @@ -174,6 +187,11 @@ impl Plugin for InputDispatchPlugin { ) .in_set(InputFocusSet::Dispatch), ); + + #[cfg(feature = "bevy_reflect")] + app.register_type::() + .register_type::() + .register_type::(); } } diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index eb01df784d052..47a939cd1913d 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -24,6 +24,8 @@ //! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be //! used to navigate between focusable entities. use bevy_app::{App, Plugin, Startup}; +#[cfg(feature = "bevy_reflect")] +use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::{ component::Component, entity::Entity, @@ -36,8 +38,11 @@ use bevy_input::{ keyboard::{KeyCode, KeyboardInput}, ButtonInput, ButtonState, }; -use bevy_utils::tracing::warn; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{prelude::*, Reflect}; use bevy_window::PrimaryWindow; +use thiserror::Error; +use tracing::warn; use crate::{FocusedInput, InputFocus, InputFocusVisible}; @@ -45,11 +50,21 @@ use crate::{FocusedInput, InputFocus, InputFocusVisible}; /// /// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order /// for this component to have any effect. -#[derive(Debug, Default, Component, Copy, Clone)] +#[derive(Debug, Default, Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, Default, Component, PartialEq) +)] pub struct TabIndex(pub i32); /// A component used to mark a tree of entities as containing tabbable elements. #[derive(Debug, Default, Component, Copy, Clone)] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, Default, Component) +)] pub struct TabGroup { /// The order of the tab group relative to other tab groups. pub order: i32, @@ -79,24 +94,58 @@ impl TabGroup { } } -/// A navigation action for tabbing. +/// A navigation action that users might take to navigate your user interface in a cyclic fashion. /// /// These values are consumed by the [`TabNavigation`] system param. pub enum NavAction { /// Navigate to the next focusable entity, wrapping around to the beginning if at the end. + /// + /// This is commonly triggered by pressing the Tab key. Next, /// Navigate to the previous focusable entity, wrapping around to the end if at the beginning. + /// + /// This is commonly triggered by pressing Shift+Tab. Previous, /// Navigate to the first focusable entity. + /// + /// This is commonly triggered by pressing Home. First, /// Navigate to the last focusable entity. + /// + /// This is commonly triggered by pressing End. Last, } +/// An error that can occur during [tab navigation](crate::tab_navigation). +#[derive(Debug, Error, PartialEq, Eq, Clone)] +pub enum TabNavigationError { + /// No tab groups were found. + #[error("No tab groups found")] + NoTabGroups, + /// No focusable entities were found. + #[error("No focusable entities found")] + NoFocusableEntities, + /// Could not navigate to the next focusable entity. + /// + /// This can occur if your tab groups are malformed. + #[error("Failed to navigate to next focusable entity")] + FailedToNavigateToNextFocusableEntity, + /// No tab group for the current focus entity was found. + #[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")] + NoTabGroupForCurrentFocus { + /// The entity that was previously focused, + /// and is missing its tab group. + previous_focus: Entity, + /// The new entity that will be focused. + /// + /// If you want to recover from this error, set [`InputFocus`] to this entity. + new_focus: Entity, + }, +} + /// An injectable helper object that provides tab navigation functionality. #[doc(hidden)] #[derive(SystemParam)] -#[allow(clippy::type_complexity)] pub struct TabNavigation<'w, 's> { // Query for tab groups. tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>, @@ -112,23 +161,23 @@ pub struct TabNavigation<'w, 's> { } impl TabNavigation<'_, '_> { - /// Navigate to the next focusable entity. + /// Navigate to the desired focusable entity. /// + /// Change the [`NavAction`] to navigate in a different direction. /// Focusable entities are determined by the presence of the [`TabIndex`] component. /// - /// Arguments: - /// * `focus`: The current focus entity, or `None` if no entity has focus. - /// * `action`: Whether to select the next, previous, first, or last focusable entity. - /// /// If no focusable entities are found, then this function will return either the first /// or last focusable entity, depending on the direction of navigation. For example, if /// `action` is `Next` and no focusable entities are found, then this function will return /// the first focusable entity. - pub fn navigate(&self, focus: &InputFocus, action: NavAction) -> Option { + pub fn navigate( + &self, + focus: &InputFocus, + action: NavAction, + ) -> Result { // If there are no tab groups, then there are no focusable entities. if self.tabgroup_query.is_empty() { - warn!("No tab groups found"); - return None; + return Err(TabNavigationError::NoTabGroups); } // Start by identifying which tab group we are in. Mainly what we want to know is if @@ -144,11 +193,21 @@ impl TabNavigation<'_, '_> { }) }); - if focus.0.is_some() && tabgroup.is_none() { - warn!("No tab group found for focus entity. Users will not be able to navigate back to this entity."); + let navigation_result = self.navigate_in_group(tabgroup, focus, action); + + match navigation_result { + Ok(entity) => { + if focus.0.is_some() && tabgroup.is_none() { + Err(TabNavigationError::NoTabGroupForCurrentFocus { + previous_focus: focus.0.unwrap(), + new_focus: entity, + }) + } else { + Ok(entity) + } + } + Err(e) => Err(e), } - - self.navigate_in_group(tabgroup, focus, action) } fn navigate_in_group( @@ -156,7 +215,7 @@ impl TabNavigation<'_, '_> { tabgroup: Option<(Entity, &TabGroup)>, focus: &InputFocus, action: NavAction, - ) -> Option { + ) -> Result { // List of all focusable entities found. let mut focusable: Vec<(Entity, TabIndex)> = Vec::with_capacity(self.tabindex_query.iter().len()); @@ -179,7 +238,7 @@ impl TabNavigation<'_, '_> { .map(|(e, tg, _)| (e, *tg)) .collect(); // Stable sort by group order - tab_groups.sort_by(compare_tab_groups); + tab_groups.sort_by_key(|(_, tg)| tg.order); // Search group descendants tab_groups.iter().for_each(|(tg_entity, _)| { @@ -189,12 +248,11 @@ impl TabNavigation<'_, '_> { } if focusable.is_empty() { - warn!("No focusable entities found"); - return None; + return Err(TabNavigationError::NoFocusableEntities); } // Stable sort by tabindex - focusable.sort_by(compare_tab_indices); + focusable.sort_by_key(|(_, idx)| *idx); let index = focusable.iter().position(|e| Some(e.0) == focus.0); let count = focusable.len(); @@ -204,7 +262,10 @@ impl TabNavigation<'_, '_> { (None, NavAction::Next) | (_, NavAction::First) => 0, (None, NavAction::Previous) | (_, NavAction::Last) => count - 1, }; - focusable.get(next).map(|(e, _)| e).copied() + match focusable.get(next) { + Some((entity, _)) => Ok(*entity), + None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity), + } } /// Gather all focusable entities in tree order. @@ -233,21 +294,15 @@ impl TabNavigation<'_, '_> { } } -fn compare_tab_groups(a: &(Entity, TabGroup), b: &(Entity, TabGroup)) -> core::cmp::Ordering { - a.1.order.cmp(&b.1.order) -} - -// Stable sort which compares by tab index -fn compare_tab_indices(a: &(Entity, TabIndex), b: &(Entity, TabIndex)) -> core::cmp::Ordering { - a.1 .0.cmp(&b.1 .0) -} - /// Plugin for navigating between focusable entities using keyboard input. pub struct TabNavigationPlugin; impl Plugin for TabNavigationPlugin { fn build(&self, app: &mut App) { app.add_systems(Startup, setup_tab_navigation); + + #[cfg(feature = "bevy_reflect")] + app.register_type::().register_type::(); } } @@ -261,6 +316,8 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, nav: TabNavigation, @@ -274,7 +331,7 @@ pub fn handle_tab_navigation( && key_event.state == ButtonState::Pressed && !key_event.repeat { - let next = nav.navigate( + let maybe_next = nav.navigate( &focus, if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) { NavAction::Previous @@ -282,10 +339,22 @@ pub fn handle_tab_navigation( NavAction::Next }, ); - if next.is_some() { - trigger.propagate(false); - focus.0 = next; - visible.0 = true; + + match maybe_next { + Ok(next) => { + trigger.propagate(false); + focus.set(next); + visible.0 = true; + } + Err(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); + focus.set(new_focus); + visible.0 = true; + } + } } } } @@ -314,16 +383,16 @@ mod tests { let next_entity = tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next); - assert_eq!(next_entity, Some(tab_entity_2)); + 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, Some(tab_entity_1)); + assert_eq!(prev_entity, Ok(tab_entity_1)); let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First); - assert_eq!(first_entity, Some(tab_entity_1)); + assert_eq!(first_entity, Ok(tab_entity_1)); let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last); - assert_eq!(last_entity, Some(tab_entity_2)); + assert_eq!(last_entity, Ok(tab_entity_2)); } } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index b00b9f6bb4a16..d3c66ee4c5291 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_internal" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature" homepage = "https://bevyengine.org" @@ -89,10 +89,10 @@ shader_format_spirv = ["bevy_render/shader_format_spirv"] serialize = [ "bevy_color?/serialize", "bevy_ecs/serialize", + "bevy_image?/serialize", "bevy_input/serialize", "bevy_math/serialize", "bevy_scene?/serialize", - "bevy_sprite?/serialize", "bevy_time/serialize", "bevy_transform/serialize", "bevy_ui?/serialize", @@ -159,9 +159,14 @@ bevy_ci_testing = ["bevy_dev_tools/bevy_ci_testing", "bevy_render?/ci_limits"] # Enable animation support, and glTF animation loading animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] -bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"] -bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"] +bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite", "bevy_image"] +bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr", "bevy_image"] bevy_window = ["dep:bevy_window", "dep:bevy_a11y"] +bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_image"] +bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"] +bevy_gltf = ["dep:bevy_gltf", "bevy_image"] +bevy_ui = ["dep:bevy_ui", "bevy_image"] +bevy_image = ["dep:bevy_image"] # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -173,12 +178,13 @@ android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"] # screen readers and forks.) accesskit_unix = ["bevy_winit/accesskit_unix"] -bevy_text = ["dep:bevy_text"] +bevy_text = ["dep:bevy_text", "bevy_image"] bevy_render = [ "dep:bevy_render", "bevy_scene?/bevy_render", "bevy_gizmos?/bevy_render", + "bevy_image", ] # Enable assertions to check the validity of parameters passed to glam @@ -214,7 +220,7 @@ meshlet_processor = ["bevy_pbr?/meshlet_processor"] bevy_dev_tools = ["dep:bevy_dev_tools"] # Enable support for the Bevy Remote Protocol -bevy_remote = ["dep:bevy_remote"] +bevy_remote = ["dep:bevy_remote", "serialize"] # Provides picking functionality bevy_picking = ["dep:bevy_picking"] @@ -244,7 +250,7 @@ ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] bevy_state = ["dep:bevy_state"] # Enables source location tracking for change detection, which can assist with debugging -track_change_detection = ["bevy_ecs/track_change_detection"] +track_location = ["bevy_ecs/track_location"] # Enable function reflection reflect_functions = [ @@ -261,46 +267,48 @@ ghost_nodes = ["bevy_ui/ghost_nodes"] [dependencies] # bevy -bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_state = { path = "../bevy_state", optional = true, version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } -bevy_input_focus = { path = "../bevy_input_focus", version = "0.15.0-dev" } -bevy_log = { path = "../bevy_log", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev", optional = true } +bevy_app = { path = "../bevy_app", 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_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", 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", features = [ + "bevy_reflect", +] } +bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", ] } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.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", optional = true } +bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } # bevy (optional) -bevy_animation = { path = "../bevy_animation", optional = true, version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", optional = true, version = "0.15.0-dev" } -bevy_audio = { path = "../bevy_audio", optional = true, version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", optional = true, version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.15.0-dev" } -bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.15.0-dev" } -bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.15.0-dev" } -bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.15.0-dev", default-features = false } -bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", optional = true, version = "0.15.0-dev" } -bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" } -bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" } -bevy_remote = { path = "../bevy_remote", optional = true, version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", optional = true, version = "0.15.0-dev" } -bevy_scene = { path = "../bevy_scene", optional = true, version = "0.15.0-dev" } -bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.15.0-dev" } -bevy_text = { path = "../bevy_text", optional = true, version = "0.15.0-dev" } -bevy_ui = { path = "../bevy_ui", optional = true, version = "0.15.0-dev" } -bevy_winit = { path = "../bevy_winit", optional = true, version = "0.15.0-dev" } +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" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", 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_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_text = { path = "../bevy_text", optional = true, version = "0.16.0-dev" } +bevy_ui = { path = "../bevy_ui", optional = true, version = "0.16.0-dev" } +bevy_winit = { path = "../bevy_winit", optional = true, version = "0.16.0-dev" } [lints] workspace = true diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index bbfa4b1a566bc..fe3cf6f9cf830 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -18,7 +18,7 @@ plugin_group! { bevy_window:::WindowPlugin, #[cfg(feature = "bevy_window")] bevy_a11y:::AccessibilityPlugin, - #[custom(cfg(not(target_arch = "wasm32")))] + #[custom(cfg(any(unix, windows)))] bevy_app:::TerminalCtrlCHandlerPlugin, #[cfg(feature = "bevy_asset")] bevy_asset:::AssetPlugin, @@ -82,7 +82,14 @@ plugin_group! { struct IgnoreAmbiguitiesPlugin; impl Plugin for IgnoreAmbiguitiesPlugin { - #[allow(unused_variables)] // Variables are used depending on enabled features + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` is not always linted" + )] + #[allow( + unused_variables, + reason = "The `app` parameter is used only if a combination of crates that contain ambiguities with each other are enabled." + )] fn build(&self, app: &mut bevy_app::App) { // bevy_ui owns the Transform and cannot be animated #[cfg(all(feature = "bevy_animation", feature = "bevy_ui"))] @@ -119,4 +126,17 @@ plugin_group! { /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) /// to provide functionality that would otherwise be driven by a windowed application's /// *event loop* or *message loop*. + /// + /// By default, this loop will run as fast as possible, which can result in high CPU usage. + /// You can add a delay using [`run_loop`](crate::app::ScheduleRunnerPlugin::run_loop), + /// or remove the loop using [`run_once`](crate::app::ScheduleRunnerPlugin::run_once). + /// # Example: + /// ```rust, no_run + /// # use std::time::Duration; + /// # use bevy_app::{App, PluginGroup, ScheduleRunnerPlugin}; + /// # use bevy_internal::MinimalPlugins; + /// App::new().add_plugins(MinimalPlugins.set(ScheduleRunnerPlugin::run_loop( + /// // Run 60 times per second. + /// Duration::from_secs_f64(1.0 / 60.0), + /// ))).run(); } diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 173d13e6bab76..9a982b4209ced 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_log" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides logging for Bevy Engine" homepage = "https://bevyengine.org" @@ -13,10 +13,12 @@ trace = ["tracing-error"] trace_tracy_memory = ["dep:tracy-client"] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +# 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" } +# other tracing-subscriber = { version = "0.3.1", features = [ "registry", "env-filter", @@ -24,6 +26,7 @@ tracing-subscriber = { version = "0.3.1", features = [ tracing-chrome = { version = "0.7.0", optional = true } tracing-log = "0.2.0" tracing-error = { version = "0.2.0", optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } # Tracy dependency compatibility table: # https://github.com/nagisa/rust_tracy_client diff --git a/crates/bevy_log/src/android_tracing.rs b/crates/bevy_log/src/android_tracing.rs index 3b6649feaf614..ba0b3b7a27a38 100644 --- a/crates/bevy_log/src/android_tracing.rs +++ b/crates/bevy_log/src/android_tracing.rs @@ -1,10 +1,10 @@ use alloc::ffi::CString; -use bevy_utils::tracing::{ +use core::fmt::{Debug, Write}; +use tracing::{ field::Field, span::{Attributes, Record}, Event, Id, Level, Subscriber, }; -use core::fmt::{Debug, Write}; use tracing_subscriber::{field::Visit, layer::Context, registry::LookupSpan, Layer}; #[derive(Default)] diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index 3b98a2c23199f..8328744e5b024 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -22,6 +22,7 @@ use core::error::Error; #[cfg(target_os = "android")] mod android_tracing; +mod once; #[cfg(feature = "trace_tracy_memory")] #[global_allocator] @@ -33,21 +34,21 @@ static GLOBAL: tracy_client::ProfiledAllocator = /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] - pub use bevy_utils::tracing::{ + pub use tracing::{ debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, }; #[doc(hidden)] - pub use bevy_utils::{debug_once, error_once, info_once, once, trace_once, warn_once}; + pub use crate::{debug_once, error_once, info_once, trace_once, warn_once}; + + #[doc(hidden)] + pub use bevy_utils::once; } -pub use bevy_utils::{ - debug_once, error_once, info_once, once, trace_once, - tracing::{ - debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span, - Level, - }, - warn_once, +pub use bevy_utils::once; +pub use tracing::{ + self, debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, + warn_span, Level, }; pub use tracing_subscriber; @@ -89,7 +90,7 @@ pub(crate) struct FlushGuard(SyncCell); /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; /// # use bevy_log::LogPlugin; -/// # use bevy_utils::tracing::Level; +/// # use tracing::Level; /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins.set(LogPlugin { @@ -134,7 +135,70 @@ pub(crate) struct FlushGuard(SyncCell); /// .run(); /// } /// ``` +/// # Example Setup +/// +/// For a quick setup that enables all first-party logging while not showing any of your dependencies' +/// log data, you can configure the plugin as shown below. +/// +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; +/// # use bevy_log::*; +/// App::new() +/// .add_plugins(DefaultPlugins.set(LogPlugin { +/// filter: "warn,my_crate=trace".to_string(), //specific filters +/// level: Level::TRACE,//Change this to be globally change levels +/// ..Default::default() +/// })) +/// .run(); +/// ``` +/// The filter (in this case an `EnvFilter`) chooses whether to print the log. The most specific filters apply with higher priority. +/// Let's start with an example: `filter: "warn".to_string()` will only print logs with level `warn` level or greater. +/// From here, we can change to `filter: "warn,my_crate=trace".to_string()`. Logs will print at level `warn` unless it's in `mycrate`, +/// which will instead print at `trace` level because `my_crate=trace` is more specific. +/// +/// +/// ## Log levels +/// Events can be logged at various levels of importance. +/// Only events at your configured log level and higher will be shown. +/// ```no_run +/// # use bevy_log::*; +/// // here is how you write new logs at each "log level" (in "most important" to +/// // "least important" order) +/// error!("something failed"); +/// warn!("something bad happened that isn't a failure, but that's worth calling out"); +/// info!("helpful information that is worth printing by default"); +/// debug!("helpful for debugging"); +/// trace!("very noisy"); +/// ``` +/// In addition to `format!` style arguments, you can print a variable's debug +/// value by using syntax like: `trace(?my_value)`. +/// +/// ## Per module logging levels +/// Modules can have different logging levels using syntax like `crate_name::module_name=debug`. +/// +/// +/// ```no_run +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup}; +/// # use bevy_log::*; +/// App::new() +/// .add_plugins(DefaultPlugins.set(LogPlugin { +/// filter: "warn,my_crate=trace,my_crate::my_module=debug".to_string(), // Specific filters +/// level: Level::TRACE, // Change this to be globally change levels +/// ..Default::default() +/// })) +/// .run(); +/// ``` +/// The idea is that instead of deleting logs when they are no longer immediately applicable, +/// you just disable them. If you do need to log in the future, then you can enable the logs instead of having to rewrite them. +/// +/// ## Further reading /// +/// The `tracing` crate has much more functionality than these examples can show. +/// Much of this configuration can be done with "layers" in the `log` crate. +/// Check out: +/// - Using spans to add more fine grained filters to logs +/// - Adding instruments to capture more function information +/// - Creating layers to add additional context such as line numbers /// # Panics /// /// This plugin should not be added multiple times in the same process. This plugin @@ -169,7 +233,7 @@ pub struct LogPlugin { /// Because [`BoxedLayer`] takes a `dyn Layer`, `Vec` is also an acceptable return value. /// /// Access to [`App`] is also provided to allow for communication between the - /// [`Subscriber`](bevy_utils::tracing::Subscriber) and the [`App`]. + /// [`Subscriber`](tracing::Subscriber) and the [`App`]. /// /// Please see the `examples/log_layers.rs` for a complete example. pub custom_layer: fn(app: &mut App) -> Option, @@ -301,7 +365,7 @@ impl Plugin for LogPlugin { let logger_already_set = LogTracer::init().is_err(); let subscriber_already_set = - bevy_utils::tracing::subscriber::set_global_default(finished_subscriber).is_err(); + tracing::subscriber::set_global_default(finished_subscriber).is_err(); match (logger_already_set, subscriber_already_set) { (true, true) => error!( diff --git a/crates/bevy_log/src/once.rs b/crates/bevy_log/src/once.rs new file mode 100644 index 0000000000000..ad53b62c6c0aa --- /dev/null +++ b/crates/bevy_log/src/once.rs @@ -0,0 +1,49 @@ +/// Call [`trace!`](crate::trace) once per call site. +/// +/// Useful for logging within systems which are called every frame. +#[macro_export] +macro_rules! trace_once { + ($($arg:tt)+) => ({ + $crate::once!($crate::trace!($($arg)+)) + }); +} + +/// Call [`debug!`](crate::debug) once per call site. +/// +/// Useful for logging within systems which are called every frame. +#[macro_export] +macro_rules! debug_once { + ($($arg:tt)+) => ({ + $crate::once!($crate::debug!($($arg)+)) + }); +} + +/// Call [`info!`](crate::info) once per call site. +/// +/// Useful for logging within systems which are called every frame. +#[macro_export] +macro_rules! info_once { + ($($arg:tt)+) => ({ + $crate::once!($crate::info!($($arg)+)) + }); +} + +/// Call [`warn!`](crate::warn) once per call site. +/// +/// Useful for logging within systems which are called every frame. +#[macro_export] +macro_rules! warn_once { + ($($arg:tt)+) => ({ + $crate::once!($crate::warn!($($arg)+)) + }); +} + +/// Call [`error!`](crate::error) once per call site. +/// +/// Useful for logging within systems which are called every frame. +#[macro_export] +macro_rules! error_once { + ($($arg:tt)+) => ({ + $crate::once!($crate::error!($($arg)+)) + }); +} diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 4b135f205e4f2..91f68913f6dd8 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_macro_utils" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "A collection of utils for Bevy Engine" homepage = "https://bevyengine.org" diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 2e92ea89a857e..3ef041492a8cf 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "bevy_math" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides math functionality for Bevy Engine" homepage = "https://bevyengine.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] -rust-version = "1.81.0" +rust-version = "1.83.0" [dependencies] glam = { version = "0.29", default-features = false, features = ["bytemuck"] } @@ -25,9 +25,10 @@ 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.15.0-dev", features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ "glam", ], optional = true } +variadics_please = "1.1" [dev-dependencies] approx = "0.5" @@ -35,13 +36,13 @@ approx = "0.5" rand = "0.8" rand_chacha = "0.3" # Enable the approx feature when testing. -bevy_math = { path = ".", version = "0.15.0-dev", default-features = false, features = [ +bevy_math = { path = ".", version = "0.16.0-dev", default-features = false, features = [ "approx", ] } glam = { version = "0.29", default-features = false, features = ["approx"] } [features] -default = ["std", "rand", "bevy_reflect", "curve"] +default = ["std", "rand", "curve"] std = [ "alloc", "glam/std", @@ -51,6 +52,7 @@ std = [ "approx?/std", "rand?/std", "rand_distr?/std", + "bevy_reflect?/std", ] alloc = [ "itertools/use_alloc", @@ -75,8 +77,8 @@ debug_glam_assert = ["glam/debug-glam-assert"] rand = ["dep:rand", "dep:rand_distr", "glam/rand"] # Include code related to the Curve trait curve = [] -# Enable bevy_reflect (requires std) -bevy_reflect = ["dep:bevy_reflect", "std"] +# Enable bevy_reflect (requires alloc) +bevy_reflect = ["dep:bevy_reflect", "alloc"] [lints] workspace = true diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index 8f55a6fb0d778..e6c08136881b1 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -51,10 +51,12 @@ fn arc_bounding_points(arc: Arc2d, rotation: impl Into) -> SmallVec<[Vec2; // If inverted = true, then right_angle > left_angle, so we are looking for an angle that is not between them. // There's a chance that this condition fails due to rounding error, if the endpoint angle is juuuust shy of the axis. // But in that case, the endpoint itself is within rounding error of the axis and will define the bounds just fine. - #[allow(clippy::nonminimal_bool)] - if !inverted && angle >= right_angle && angle <= left_angle - || inverted && (angle >= right_angle || angle <= left_angle) - { + let angle_within_parameters = if inverted { + angle >= right_angle || angle <= left_angle + } else { + angle >= right_angle && angle <= left_angle + }; + if angle_within_parameters { bounds.push(extremum * arc.radius); } } @@ -441,6 +443,7 @@ impl Bounded2d for Capsule2d { #[cfg(test)] mod tests { use core::f32::consts::{FRAC_PI_2, FRAC_PI_3, FRAC_PI_4, FRAC_PI_6, TAU}; + use std::println; use approx::assert_abs_diff_eq; use glam::Vec2; @@ -475,7 +478,6 @@ mod tests { // Arcs and circular segments have the same bounding shapes so they share test cases. fn arc_and_segment() { struct TestCase { - #[allow(unused)] name: &'static str, arc: Arc2d, translation: Vec2, @@ -629,7 +631,6 @@ mod tests { #[test] fn circular_sector() { struct TestCase { - #[allow(unused)] name: &'static str, arc: Arc2d, translation: Vec2, @@ -650,7 +651,7 @@ mod tests { let apothem = ops::sqrt(3.0) / 2.0; let inv_sqrt_3 = ops::sqrt(3.0).recip(); let tests = [ - // Test case: An sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center + // Test case: A sector whose arc is minor, but whose bounding circle is not the circumcircle of the endpoints and center TestCase { name: "1/3rd circle", arc: Arc2d::from_radians(1.0, TAU / 3.0), diff --git a/crates/bevy_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 90bc77629e776..a9a8ef910a86e 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -5,6 +5,7 @@ use core::{ fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, }; +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: @@ -393,31 +394,9 @@ impl StableInterpolate for Dir3A { } } -// If you're confused about how #[doc(fake_variadic)] works, -// then the `all_tuples` macro is nicely documented (it can be found in the `bevy_utils` crate). -// tl;dr: `#[doc(fake_variadic)]` goes on the impl of tuple length one. -// the others have to be hidden using `#[doc(hidden)]`. macro_rules! impl_stable_interpolate_tuple { - (($T:ident, $n:tt)) => { - impl_stable_interpolate_tuple! { - @impl - #[cfg_attr(any(docsrs, docsrs_dep), doc(fake_variadic))] - #[cfg_attr( - any(docsrs, docsrs_dep), - doc = "This trait is implemented for tuples up to 11 items long." - )] - ($T, $n) - } - }; - ($(($T:ident, $n:tt)),*) => { - impl_stable_interpolate_tuple! { - @impl - #[cfg_attr(any(docsrs, docsrs_dep), doc(hidden))] - $(($T, $n)),* - } - }; - (@impl $(#[$($meta:meta)*])* $(($T:ident, $n:tt)),*) => { - $(#[$($meta)*])* + ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => { + $(#[$meta])* impl<$($T: StableInterpolate),*> StableInterpolate for ($($T,)*) { fn interpolate_stable(&self, other: &Self, t: f32) -> Self { ( @@ -430,68 +409,12 @@ macro_rules! impl_stable_interpolate_tuple { }; } -// (See `macro_metavar_expr`, which might make this better.) -// This currently implements `StableInterpolate` for tuples of up to 11 elements. -impl_stable_interpolate_tuple!((T, 0)); -impl_stable_interpolate_tuple!((T0, 0), (T1, 1)); -impl_stable_interpolate_tuple!((T0, 0), (T1, 1), (T2, 2)); -impl_stable_interpolate_tuple!((T0, 0), (T1, 1), (T2, 2), (T3, 3)); -impl_stable_interpolate_tuple!((T0, 0), (T1, 1), (T2, 2), (T3, 3), (T4, 4)); -impl_stable_interpolate_tuple!((T0, 0), (T1, 1), (T2, 2), (T3, 3), (T4, 4), (T5, 5)); -impl_stable_interpolate_tuple!( - (T0, 0), - (T1, 1), - (T2, 2), - (T3, 3), - (T4, 4), - (T5, 5), - (T6, 6) -); -impl_stable_interpolate_tuple!( - (T0, 0), - (T1, 1), - (T2, 2), - (T3, 3), - (T4, 4), - (T5, 5), - (T6, 6), - (T7, 7) -); -impl_stable_interpolate_tuple!( - (T0, 0), - (T1, 1), - (T2, 2), - (T3, 3), - (T4, 4), - (T5, 5), - (T6, 6), - (T7, 7), - (T8, 8) -); -impl_stable_interpolate_tuple!( - (T0, 0), - (T1, 1), - (T2, 2), - (T3, 3), - (T4, 4), - (T5, 5), - (T6, 6), - (T7, 7), - (T8, 8), - (T9, 9) -); -impl_stable_interpolate_tuple!( - (T0, 0), - (T1, 1), - (T2, 2), - (T3, 3), - (T4, 4), - (T5, 5), - (T6, 6), - (T7, 7), - (T8, 8), - (T9, 9), - (T10, 10) +all_tuples_enumerated!( + #[doc(fake_variadic)] + impl_stable_interpolate_tuple, + 1, + 11, + T ); /// A type that has tangents. diff --git a/crates/bevy_math/src/compass.rs b/crates/bevy_math/src/compass.rs index 5ee224df4b118..72dd817146905 100644 --- a/crates/bevy_math/src/compass.rs +++ b/crates/bevy_math/src/compass.rs @@ -1,3 +1,5 @@ +use core::ops::Neg; + use crate::Dir2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -34,6 +36,45 @@ pub enum CompassQuadrant { West, } +impl CompassQuadrant { + /// Converts a standard index to a [`CompassQuadrant`]. + /// + /// Starts at 0 for [`CompassQuadrant::North`] and increments clockwise. + pub const fn from_index(index: usize) -> Option { + match index { + 0 => Some(Self::North), + 1 => Some(Self::East), + 2 => Some(Self::South), + 3 => Some(Self::West), + _ => None, + } + } + + /// Converts a [`CompassQuadrant`] to a standard index. + /// + /// Starts at 0 for [`CompassQuadrant::North`] and increments clockwise. + pub const fn to_index(self) -> usize { + match self { + Self::North => 0, + Self::East => 1, + Self::South => 2, + Self::West => 3, + } + } + + /// Returns the opposite [`CompassQuadrant`], located 180 degrees from `self`. + /// + /// This can also be accessed via the `-` operator, using the [`Neg`] trait. + pub const fn opposite(&self) -> CompassQuadrant { + match self { + Self::North => Self::South, + Self::East => Self::West, + Self::South => Self::North, + Self::West => Self::East, + } + } +} + /// A compass enum with 8 directions. /// ```text /// N (North) @@ -72,6 +113,57 @@ pub enum CompassOctant { NorthWest, } +impl CompassOctant { + /// Converts a standard index to a [`CompassOctant`]. + /// + /// Starts at 0 for [`CompassOctant::North`] and increments clockwise. + pub const fn from_index(index: usize) -> Option { + match index { + 0 => Some(Self::North), + 1 => Some(Self::NorthEast), + 2 => Some(Self::East), + 3 => Some(Self::SouthEast), + 4 => Some(Self::South), + 5 => Some(Self::SouthWest), + 6 => Some(Self::West), + 7 => Some(Self::NorthWest), + _ => None, + } + } + + /// Converts a [`CompassOctant`] to a standard index. + /// + /// Starts at 0 for [`CompassOctant::North`] and increments clockwise. + pub const fn to_index(self) -> usize { + match self { + Self::North => 0, + Self::NorthEast => 1, + Self::East => 2, + Self::SouthEast => 3, + Self::South => 4, + Self::SouthWest => 5, + Self::West => 6, + Self::NorthWest => 7, + } + } + + /// Returns the opposite [`CompassOctant`], located 180 degrees from `self`. + /// + /// This can also be accessed via the `-` operator, using the [`Neg`] trait. + pub const fn opposite(&self) -> CompassOctant { + match self { + Self::North => Self::South, + Self::NorthEast => Self::SouthWest, + Self::East => Self::West, + Self::SouthEast => Self::NorthWest, + Self::South => Self::North, + Self::SouthWest => Self::NorthEast, + Self::West => Self::East, + Self::NorthWest => Self::SouthEast, + } + } +} + impl From for Dir2 { fn from(q: CompassQuadrant) -> Self { match q { @@ -134,6 +226,22 @@ impl From for CompassOctant { } } +impl Neg for CompassQuadrant { + type Output = CompassQuadrant; + + fn neg(self) -> Self::Output { + self.opposite() + } +} + +impl Neg for CompassOctant { + type Output = CompassOctant; + + fn neg(self) -> Self::Output { + self.opposite() + } +} + #[cfg(test)] mod test_compass_quadrant { use crate::{CompassQuadrant, Dir2, Vec2}; @@ -235,6 +343,29 @@ mod test_compass_quadrant { assert_eq!(CompassQuadrant::from(dir), expected); } } + + #[test] + fn out_of_bounds_indexes_return_none() { + assert_eq!(CompassQuadrant::from_index(4), None); + assert_eq!(CompassQuadrant::from_index(5), None); + assert_eq!(CompassQuadrant::from_index(usize::MAX), None); + } + + #[test] + fn compass_indexes_are_reversible() { + for i in 0..4 { + let quadrant = CompassQuadrant::from_index(i).unwrap(); + assert_eq!(quadrant.to_index(), i); + } + } + + #[test] + fn opposite_directions_reverse_themselves() { + for i in 0..4 { + let quadrant = CompassQuadrant::from_index(i).unwrap(); + assert_eq!(-(-quadrant), quadrant); + } + } } #[cfg(test)] @@ -420,4 +551,27 @@ mod test_compass_octant { assert_eq!(CompassOctant::from(dir), expected); } } + + #[test] + fn out_of_bounds_indexes_return_none() { + assert_eq!(CompassOctant::from_index(8), None); + assert_eq!(CompassOctant::from_index(9), None); + assert_eq!(CompassOctant::from_index(usize::MAX), None); + } + + #[test] + fn compass_indexes_are_reversible() { + for i in 0..8 { + let octant = CompassOctant::from_index(i).unwrap(); + assert_eq!(octant.to_index(), i); + } + } + + #[test] + fn opposite_directions_reverse_themselves() { + for i in 0..8 { + let octant = CompassOctant::from_index(i).unwrap(); + assert_eq!(-(-octant), octant); + } + } } diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index ecc0f789c6c5c..32e13f6720a92 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -995,7 +995,13 @@ impl CubicSegment

{ } /// Calculate polynomial coefficients for the cubic curve using a characteristic matrix. - #[allow(unused)] + #[cfg_attr( + not(feature = "alloc"), + expect( + dead_code, + reason = "Method only used when `alloc` feature is enabled." + ) + )] #[inline] fn coefficients(p: [P; 4], char_matrix: [[f32; 4]; 4]) -> Self { let [c0, c1, c2, c3] = char_matrix; @@ -1376,7 +1382,13 @@ impl RationalSegment

{ } /// Calculate polynomial coefficients for the cubic polynomials using a characteristic matrix. - #[allow(unused)] + #[cfg_attr( + not(feature = "alloc"), + expect( + dead_code, + reason = "Method only used when `alloc` feature is enabled." + ) + )] #[inline] fn coefficients( control_points: [P; 4], diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 20e0bcd29c937..afc34837d3b13 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -10,7 +10,10 @@ use core::fmt::{self, Debug}; use core::marker::PhantomData; #[cfg(feature = "bevy_reflect")] -use bevy_reflect::{utility::GenericTypePathCell, FromReflect, Reflect, TypePath}; +use { + alloc::format, + bevy_reflect::{utility::GenericTypePathCell, FromReflect, Reflect, TypePath}, +}; #[cfg(feature = "bevy_reflect")] mod paths { @@ -18,6 +21,9 @@ mod paths { pub(super) const THIS_CRATE: &str = "bevy_math"; } +#[expect(unused, reason = "imported just for doc links")] +use super::CurveExt; + // NOTE ON REFLECTION: // // Function members of structs pose an obstacle for reflection, because they don't implement @@ -173,7 +179,7 @@ where } /// A curve whose samples are defined by mapping samples from another curve through a -/// given function. Curves of this type are produced by [`Curve::map`]. +/// given function. Curves of this type are produced by [`CurveExt::map`]. #[derive(Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -269,7 +275,7 @@ where } /// A curve whose sample space is mapped onto that of some base curve's before sampling. -/// Curves of this type are produced by [`Curve::reparametrize`]. +/// Curves of this type are produced by [`CurveExt::reparametrize`]. #[derive(Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -364,7 +370,7 @@ where } /// A curve that has had its domain changed by a linear reparameterization (stretching and scaling). -/// Curves of this type are produced by [`Curve::reparametrize_linear`]. +/// Curves of this type are produced by [`CurveExt::reparametrize_linear`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -399,7 +405,7 @@ where } /// A curve that has been reparametrized by another curve, using that curve to transform the -/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. +/// sample times before sampling. Curves of this type are produced by [`CurveExt::reparametrize_by_curve`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -432,7 +438,7 @@ where } /// A curve that is the graph of another curve over its parameter space. Curves of this type are -/// produced by [`Curve::graph`]. +/// produced by [`CurveExt::graph`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -462,7 +468,7 @@ where } /// A curve that combines the output data from two constituent curves into a tuple output. Curves -/// of this type are produced by [`Curve::zip`]. +/// of this type are produced by [`CurveExt::zip`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -503,7 +509,7 @@ where /// For this to be well-formed, the first curve's domain must be right-finite and the second's /// must be left-finite. /// -/// Curves of this type are produced by [`Curve::chain`]. +/// Curves of this type are produced by [`CurveExt::chain`]. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -549,7 +555,7 @@ where /// The curve that results from reversing another. /// -/// Curves of this type are produced by [`Curve::reverse`]. +/// Curves of this type are produced by [`CurveExt::reverse`]. /// /// # Domain /// @@ -590,7 +596,7 @@ where /// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the /// value at `domain.end()` in the original curve /// -/// Curves of this type are produced by [`Curve::repeat`]. +/// Curves of this type are produced by [`CurveExt::repeat`]. /// /// # Domain /// @@ -649,7 +655,7 @@ where /// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the /// value at `domain.end()` in the original curve /// -/// Curves of this type are produced by [`Curve::forever`]. +/// Curves of this type are produced by [`CurveExt::forever`]. /// /// # Domain /// @@ -703,7 +709,7 @@ where /// The curve that results from chaining a curve with its reversed version. The transition point /// is guaranteed to make no jump. /// -/// Curves of this type are produced by [`Curve::ping_pong`]. +/// Curves of this type are produced by [`CurveExt::ping_pong`]. /// /// # Domain /// @@ -756,7 +762,7 @@ where /// realized by translating the second curve so that its start sample point coincides with the /// first curves' end sample point. /// -/// Curves of this type are produced by [`Curve::chain_continue`]. +/// Curves of this type are produced by [`CurveExt::chain_continue`]. /// /// # Domain /// diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 07330456570fe..838d0d116d440 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -147,7 +147,7 @@ pub enum EvenCoreError { }, /// Unbounded domains are not compatible with `EvenCore`. - #[error("Cannot create a EvenCore over an unbounded domain")] + #[error("Cannot create an EvenCore over an unbounded domain")] UnboundedDomain, } @@ -432,14 +432,14 @@ impl UnevenCore { } /// This core, but with the sample times moved by the map `f`. - /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// In principle, when `f` is monotone, this is equivalent to [`CurveExt::reparametrize`], /// but the function inputs to each are inverses of one another. /// /// The samples are re-sorted by time after mapping and deduplicated by output time, so /// the function `f` should generally be injective over the set of sample times, otherwise /// data will be deleted. /// - /// [`Curve::reparametrize`]: crate::curve::Curve::reparametrize + /// [`CurveExt::reparametrize`]: crate::curve::CurveExt::reparametrize #[must_use] pub fn map_sample_times(mut self, f: impl Fn(f32) -> f32) -> UnevenCore { let mut timed_samples = self @@ -697,6 +697,7 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { mod tests { use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; use crate::curve::{cores::InterpolationDatum, interval}; + use alloc::vec; use approx::{assert_abs_diff_eq, AbsDiffEq}; fn approx_between(datum: InterpolationDatum, start: T, end: T, p: f32) -> bool diff --git a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs index 6a32f1bb20e4f..a499526b78338 100644 --- a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs +++ b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs @@ -453,7 +453,7 @@ mod tests { use super::*; use crate::cubic_splines::{CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator}; - use crate::curve::{Curve, Interval}; + use crate::curve::{Curve, CurveExt, Interval}; use crate::{vec2, Vec2, Vec3}; fn test_curve() -> CubicCurve { diff --git a/crates/bevy_math/src/curve/derivatives/mod.rs b/crates/bevy_math/src/curve/derivatives/mod.rs index e3b9e531dbed2..d819443f0d4f4 100644 --- a/crates/bevy_math/src/curve/derivatives/mod.rs +++ b/crates/bevy_math/src/curve/derivatives/mod.rs @@ -20,7 +20,7 @@ //! counterpart. //! //! [`with_derivative`]: CurveWithDerivative::with_derivative -//! [`by_ref`]: Curve::by_ref +//! [`by_ref`]: crate::curve::CurveExt::by_ref pub mod adaptor_impls; diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 0e5406fa732c7..6081fd1e42f4e 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -4,10 +4,12 @@ //! [easing functions]: EaseFunction use crate::{ - curve::{FunctionCurve, Interval}, - Curve, Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace, + curve::{Curve, CurveExt, FunctionCurve, Interval}, + Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace, }; +use variadics_please::all_tuples_enumerated; + // TODO: Think about merging `Ease` with `StableInterpolate` /// A type whose values can be eased between. @@ -72,6 +74,38 @@ impl Ease for Dir3A { } } +macro_rules! impl_ease_tuple { + ($(#[$meta:meta])* $(($n:tt, $T:ident)),*) => { + $(#[$meta])* + impl<$($T: Ease),*> Ease for ($($T,)*) { + fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { + let curve_tuple = + ( + $( + <$T as Ease>::interpolating_curve_unbounded(start.$n, end.$n), + )* + ); + + FunctionCurve::new(Interval::EVERYWHERE, move |t| + ( + $( + curve_tuple.$n.sample_unchecked(t), + )* + ) + ) + } + } + }; +} + +all_tuples_enumerated!( + #[doc(fake_variadic)] + impl_ease_tuple, + 1, + 11, + T +); + /// A [`Curve`] that is defined by /// /// - an initial `start` sample value at `t = 0` @@ -127,6 +161,7 @@ where /// Curve functions over the [unit interval], commonly used for easing transitions. /// /// [unit interval]: `Interval::UNIT` +#[non_exhaustive] #[derive(Debug, Copy, Clone, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] @@ -135,17 +170,44 @@ pub enum EaseFunction { Linear, /// `f(t) = t²` + /// + /// This is the Hermite interpolator for + /// - f(0) = 0 + /// - f(1) = 1 + /// - f′(0) = 0 QuadraticIn, /// `f(t) = -(t * (t - 2.0))` + /// + /// This is the Hermite interpolator for + /// - f(0) = 0 + /// - f(1) = 1 + /// - f′(1) = 0 QuadraticOut, /// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::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 [`EaseFunction::SmoothStep`]) + /// if you want the acceleration to be continuous. QuadraticInOut, /// `f(t) = t³` + /// + /// This is the Hermite interpolator for + /// - f(0) = 0 + /// - f(1) = 1 + /// - f′(0) = 0 + /// - f″(0) = 0 CubicIn, /// `f(t) = (t - 1.0)³ + 1.0` CubicOut, /// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::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 [`EaseFunction::SmoothStep`] instead, which is also cubic, + /// or [`EaseFunction::SmootherStep`] if you picked this because you wanted + /// the acceleration at the endpoints to also be zero. CubicInOut, /// `f(t) = t⁴` @@ -160,8 +222,53 @@ pub enum EaseFunction { /// `f(t) = (t - 1.0)⁵ + 1.0` QuinticOut, /// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::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 [`EaseFunction::SmootherStep`] instead, which is also quintic. QuinticInOut, + /// Behaves as the first half of [`EaseFunction::SmoothStep`]. + /// + /// This has f″(1) = 0, unlike [`EaseFunction::QuadraticIn`] which starts similarly. + SmoothStepIn, + /// Behaves as the second half of [`EaseFunction::SmoothStep`]. + /// + /// This has f″(0) = 0, unlike [`EaseFunction::QuadraticOut`] which ends similarly. + SmoothStepOut, + /// `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 + SmoothStep, + + /// Behaves as the first half of [`EaseFunction::SmootherStep`]. + /// + /// This has f″(1) = 0, unlike [`EaseFunction::CubicIn`] which starts similarly. + SmootherStepIn, + /// Behaves as the second half of [`EaseFunction::SmootherStep`]. + /// + /// This has f″(0) = 0, unlike [`EaseFunction::CubicOut`] which ends similarly. + SmootherStepOut, + /// `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 + SmootherStep, + /// `f(t) = 1.0 - cos(t * π / 2.0)` SineIn, /// `f(t) = sin(t * π / 2.0)` @@ -176,9 +283,15 @@ pub enum EaseFunction { /// Behaves as `EaseFunction::CircularIn` for t < 0.5 and as `EaseFunction::CircularOut` for t >= 0.5 CircularInOut, - /// `f(t) = 2.0^(10.0 * (t - 1.0))` + /// `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). ExponentialIn, - /// `f(t) = 1.0 - 2.0^(-10.0 * t)` + /// `f(t) ≈ 1.0 - 2.0^(-10.0 * t)` + /// + /// As with `EaseFunction::ExponentialIn`, the precise definition adjusts it slightly + // so it hits both `(0, 0)` and `(1, 1)`. ExponentialOut, /// Behaves as `EaseFunction::ExponentialIn` for t < 0.5 and as `EaseFunction::ExponentialOut` for t >= 0.5 ExponentialInOut, @@ -294,6 +407,36 @@ mod easing_functions { } } + #[inline] + pub(crate) fn smoothstep_in(t: f32) -> f32 { + ((1.5 - 0.5 * t) * t) * t + } + + #[inline] + pub(crate) fn smoothstep_out(t: f32) -> f32 { + (1.5 + (-0.5 * t) * t) * t + } + + #[inline] + pub(crate) fn smoothstep(t: f32) -> f32 { + ((3.0 - 2.0 * t) * t) * t + } + + #[inline] + pub(crate) fn smootherstep_in(t: f32) -> f32 { + (((2.5 + (-1.875 + 0.375 * t) * t) * t) * t) * t + } + + #[inline] + pub(crate) fn smootherstep_out(t: f32) -> f32 { + (1.875 + ((-1.25 + (0.375 * t) * t) * t) * t) * t + } + + #[inline] + pub(crate) fn smootherstep(t: f32) -> f32 { + (((10.0 + (-15.0 + 6.0 * t) * t) * t) * t) * t + } + #[inline] pub(crate) fn sine_in(t: f32) -> f32 { 1.0 - ops::cos(t * FRAC_PI_2) @@ -324,20 +467,36 @@ mod easing_functions { } } + // These are copied from a high precision calculator; I'd rather show them + // with blatantly more digits than needed (since rust will round them to the + // nearest representable value anyway) rather than make it seem like the + // truncated value is somehow carefully chosen. + #[expect( + clippy::excessive_precision, + reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen." + )] + const LOG2_1023: f32 = 9.998590429745328646459226; + #[expect( + clippy::excessive_precision, + reason = "This is deliberately more precise than an f32 will allow, as truncating the value might imply that the value is carefully chosen." + )] + const FRAC_1_1023: f32 = 0.00097751710654936461388074291; #[inline] pub(crate) fn exponential_in(t: f32) -> f32 { - ops::powf(2.0, 10.0 * t - 10.0) + // Derived from a rescaled exponential formula `(2^(10*t) - 1) / (2^10 - 1)` + // See + ops::exp2(10.0 * t - LOG2_1023) - FRAC_1_1023 } #[inline] pub(crate) fn exponential_out(t: f32) -> f32 { - 1.0 - ops::powf(2.0, -10.0 * t) + (FRAC_1_1023 + 1.0) - ops::exp2(-10.0 * t - (LOG2_1023 - 10.0)) } #[inline] pub(crate) fn exponential_in_out(t: f32) -> f32 { if t < 0.5 { - ops::powf(2.0, 20.0 * t - 10.0) / 2.0 + ops::exp2(20.0 * t - (LOG2_1023 + 1.0)) - (FRAC_1_1023 / 2.0) } else { - (2.0 - ops::powf(2.0, -20.0 * t + 10.0)) / 2.0 + (FRAC_1_1023 / 2.0 + 1.0) - ops::exp2(-20.0 * t - (LOG2_1023 - 19.0)) } } @@ -436,6 +595,12 @@ impl EaseFunction { EaseFunction::QuinticIn => easing_functions::quintic_in(t), EaseFunction::QuinticOut => easing_functions::quintic_out(t), EaseFunction::QuinticInOut => easing_functions::quintic_in_out(t), + EaseFunction::SmoothStepIn => easing_functions::smoothstep_in(t), + EaseFunction::SmoothStepOut => easing_functions::smoothstep_out(t), + EaseFunction::SmoothStep => easing_functions::smoothstep(t), + EaseFunction::SmootherStepIn => easing_functions::smootherstep_in(t), + EaseFunction::SmootherStepOut => easing_functions::smootherstep_out(t), + EaseFunction::SmootherStep => easing_functions::smootherstep(t), EaseFunction::SineIn => easing_functions::sine_in(t), EaseFunction::SineOut => easing_functions::sine_out(t), EaseFunction::SineInOut => easing_functions::sine_in_out(t), @@ -459,3 +624,99 @@ impl EaseFunction { } } } + +#[cfg(test)] +mod tests { + use super::*; + const MONOTONIC_IN_OUT_INOUT: &[[EaseFunction; 3]] = { + use EaseFunction::*; + &[ + [QuadraticIn, QuadraticOut, QuadraticInOut], + [CubicIn, CubicOut, CubicInOut], + [QuarticIn, QuarticOut, QuarticInOut], + [QuinticIn, QuinticOut, QuinticInOut], + [SmoothStepIn, SmoothStepOut, SmoothStep], + [SmootherStepIn, SmootherStepOut, SmootherStep], + [SineIn, SineOut, SineInOut], + [CircularIn, CircularOut, CircularInOut], + [ExponentialIn, ExponentialOut, ExponentialInOut], + ] + }; + + // For easing function we don't care if eval(0) is super-tiny like 2.0e-28, + // so add the same amount of error on both ends of the unit interval. + const TOLERANCE: f32 = 1.0e-6; + const _: () = const { + assert!(1.0 - TOLERANCE != 1.0); + }; + + #[test] + fn ease_functions_zero_to_one() { + for ef in MONOTONIC_IN_OUT_INOUT.iter().flatten() { + let start = ef.eval(0.0); + assert!( + (0.0..=TOLERANCE).contains(&start), + "EaseFunction.{ef:?}(0) was {start:?}", + ); + + let finish = ef.eval(1.0); + assert!( + (1.0 - TOLERANCE..=1.0).contains(&finish), + "EaseFunction.{ef:?}(1) was {start:?}", + ); + } + } + + #[test] + fn ease_function_inout_deciles() { + // convexity gives the comparisons against the input built-in tolerances + for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT { + for x in [0.1, 0.2, 0.3, 0.4] { + let y = ef_inout.eval(x); + assert!(y < x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}"); + + let iny = ef_in.eval(2.0 * x) / 2.0; + assert!( + (y - TOLERANCE..y + TOLERANCE).contains(&iny), + "EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \ + EaseFunction.{ef_in:?}(2 * {x:?}) / 2 was {iny:?}", + ); + } + + for x in [0.6, 0.7, 0.8, 0.9] { + let y = ef_inout.eval(x); + assert!(y > x, "EaseFunction.{ef_inout:?}({x:?}) was {y:?}"); + + let outy = ef_out.eval(2.0 * x - 1.0) / 2.0 + 0.5; + assert!( + (y - TOLERANCE..y + TOLERANCE).contains(&outy), + "EaseFunction.{ef_inout:?}({x:?}) was {y:?}, but \ + EaseFunction.{ef_out:?}(2 * {x:?} - 1) / 2 + ½ was {outy:?}", + ); + } + } + } + + #[test] + fn ease_function_midpoints() { + for [ef_in, ef_out, ef_inout] in MONOTONIC_IN_OUT_INOUT { + let mid = ef_in.eval(0.5); + assert!( + mid < 0.5 - TOLERANCE, + "EaseFunction.{ef_in:?}(½) was {mid:?}", + ); + + let mid = ef_out.eval(0.5); + assert!( + mid > 0.5 + TOLERANCE, + "EaseFunction.{ef_out:?}(½) was {mid:?}", + ); + + let mid = ef_inout.eval(0.5); + assert!( + (0.5 - TOLERANCE..=0.5 + TOLERANCE).contains(&mid), + "EaseFunction.{ef_inout:?}(½) was {mid:?}", + ); + } + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index 6e5f4465aed91..007e523c95be6 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -201,6 +201,7 @@ mod tests { use crate::ops; use super::*; + use alloc::vec::Vec; use approx::{assert_abs_diff_eq, AbsDiffEq}; #[test] diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index fa8636338f40f..659f4fee686c5 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -269,18 +269,18 @@ //! //! [domain]: Curve::domain //! [sampled]: Curve::sample -//! [changing parametrizations]: Curve::reparametrize -//! [mapping output]: Curve::map -//! [rasterization]: Curve::resample +//! [changing parametrizations]: CurveExt::reparametrize +//! [mapping output]: CurveExt::map +//! [rasterization]: CurveResampleExt::resample //! [functions]: FunctionCurve //! [sample interpolation]: SampleCurve //! [splines]: crate::cubic_splines //! [easings]: easing //! [spline curves]: crate::cubic_splines //! [easing curves]: easing -//! [`chain`]: Curve::chain -//! [`zip`]: Curve::zip -//! [`resample`]: Curve::resample +//! [`chain`]: CurveExt::chain +//! [`zip`]: CurveExt::zip +//! [`resample`]: CurveResampleExt::resample //! //! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `FunctionCurve::new //! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve. @@ -302,9 +302,7 @@ pub use interval::{interval, Interval}; #[cfg(feature = "alloc")] pub use { - crate::StableInterpolate, cores::{EvenCore, UnevenCore}, - itertools::Itertools, sample_curves::*, }; @@ -313,6 +311,9 @@ use core::{marker::PhantomData, ops::Deref}; use interval::InvalidIntervalError; use thiserror::Error; +#[cfg(feature = "alloc")] +use {crate::StableInterpolate, itertools::Itertools}; + /// A trait for a type that can represent values of type `T` parametrized over a fixed interval. /// /// Typical examples of this are actual geometric curves where `T: VectorSpace`, but other kinds @@ -349,17 +350,41 @@ pub trait Curve { let t = self.domain().clamp(t); self.sample_unchecked(t) } +} + +impl Curve for D +where + C: Curve + ?Sized, + D: Deref, +{ + fn domain(&self) -> Interval { + >::domain(self) + } + fn sample_unchecked(&self, t: f32) -> T { + >::sample_unchecked(self, t) + } +} + +/// Extension trait implemented by [curves], allowing access to a number of adaptors and +/// convenience methods. +/// +/// This trait is automatically implemented for all curves that are `Sized`. In particular, +/// it is implemented for types like `Box>`. `CurveExt` is not dyn-compatible +/// itself. +/// +/// For more information, see the [module-level documentation]. +/// +/// [curves]: Curve +/// [module-level documentation]: self +pub trait CurveExt: Curve + Sized { /// Sample a collection of `n >= 0` points on this curve at the parameter values `t_n`, /// returning `None` if the point is outside of the curve's domain. /// /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> - where - Self: Sized, - { + fn sample_iter(&self, iter: impl IntoIterator) -> impl Iterator> { iter.into_iter().map(|t| self.sample(t)) } @@ -374,10 +399,10 @@ pub trait Curve { /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter_unchecked(&self, iter: impl IntoIterator) -> impl Iterator - where - Self: Sized, - { + fn sample_iter_unchecked( + &self, + iter: impl IntoIterator, + ) -> impl Iterator { iter.into_iter().map(|t| self.sample_unchecked(t)) } @@ -387,10 +412,7 @@ pub trait Curve { /// The samples are returned in the same order as the parameter values `t_n` were provided and /// will include all results. This leaves the responsibility for things like filtering and /// sorting to the user for maximum flexibility. - fn sample_iter_clamped(&self, iter: impl IntoIterator) -> impl Iterator - where - Self: Sized, - { + fn sample_iter_clamped(&self, iter: impl IntoIterator) -> impl Iterator { iter.into_iter().map(|t| self.sample_clamped(t)) } @@ -400,7 +422,6 @@ pub trait Curve { #[must_use] fn map(self, f: F) -> MapCurve where - Self: Sized, F: Fn(T) -> S, { MapCurve { @@ -425,7 +446,7 @@ pub trait Curve { /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// ``` /// This kind of linear remapping is provided by the convenience method - /// [`Curve::reparametrize_linear`], which requires only the desired domain for the new curve. + /// [`CurveExt::reparametrize_linear`], which requires only the desired domain for the new curve. /// /// # Examples /// ``` @@ -443,7 +464,6 @@ pub trait Curve { #[must_use] fn reparametrize(self, domain: Interval, f: F) -> ReparamCurve where - Self: Sized, F: Fn(f32) -> f32, { ReparamCurve { @@ -456,15 +476,15 @@ pub trait Curve { /// Linearly reparametrize this [`Curve`], producing a new curve whose domain is the given /// `domain` instead of the current one. This operation is only valid for curves with bounded - /// domains; if either this curve's domain or the given `domain` is unbounded, an error is - /// returned. + /// domains. + /// + /// # Errors + /// + /// If either this curve's domain or the given `domain` is unbounded, an error is returned. fn reparametrize_linear( self, domain: Interval, - ) -> Result, LinearReparamError> - where - Self: Sized, - { + ) -> Result, LinearReparamError> { if !self.domain().is_bounded() { return Err(LinearReparamError::SourceCurveUnbounded); } @@ -488,7 +508,6 @@ pub trait Curve { #[must_use] fn reparametrize_by_curve(self, other: C) -> CurveReparamCurve where - Self: Sized, C: Curve, { CurveReparamCurve { @@ -505,10 +524,7 @@ pub trait Curve { /// `(t, x)` at time `t`. In particular, if this curve is a `Curve`, the output of this method /// is a `Curve<(f32, T)>`. #[must_use] - fn graph(self) -> GraphCurve - where - Self: Sized, - { + fn graph(self) -> GraphCurve { GraphCurve { base: self, _phantom: PhantomData, @@ -519,11 +535,13 @@ pub trait Curve { /// /// The sample at time `t` in the new curve is `(x, y)`, where `x` is the sample of `self` at /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the - /// intersection of the domains of its constituents. If the domain intersection would be empty, - /// an error is returned. + /// intersection of the domains of its constituents. + /// + /// # Errors + /// + /// If the domain intersection would be empty, an error is returned instead. fn zip(self, other: C) -> Result, InvalidIntervalError> where - Self: Sized, C: Curve + Sized, { let domain = self.domain().intersect(other.domain())?; @@ -545,7 +563,6 @@ pub trait Curve { /// `other`'s domain doesn't have a finite start. fn chain(self, other: C) -> Result, ChainError> where - Self: Sized, C: Curve, { if !self.domain().has_finite_end() { @@ -566,13 +583,10 @@ pub trait Curve { /// and transitioning over to `self.domain().start()`. The domain of the new curve is still the /// same. /// - /// # Error + /// # Errors /// /// A [`ReverseError`] is returned if this curve's domain isn't bounded. - fn reverse(self) -> Result, ReverseError> - where - Self: Sized, - { + fn reverse(self) -> Result, ReverseError> { self.domain() .is_bounded() .then(|| ReverseCurve { @@ -593,13 +607,10 @@ pub trait Curve { /// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the /// value at `domain.end()` in the original curve /// - /// # Error + /// # Errors /// /// A [`RepeatError`] is returned if this curve's domain isn't bounded. - fn repeat(self, count: usize) -> Result, RepeatError> - where - Self: Sized, - { + fn repeat(self, count: usize) -> Result, RepeatError> { self.domain() .is_bounded() .then(|| { @@ -629,13 +640,10 @@ pub trait Curve { /// - the value at the transitioning points (`domain.end() * n` for `n >= 1`) in the results is the /// value at `domain.end()` in the original curve /// - /// # Error + /// # Errors /// /// A [`RepeatError`] is returned if this curve's domain isn't bounded. - fn forever(self) -> Result, RepeatError> - where - Self: Sized, - { + fn forever(self) -> Result, RepeatError> { self.domain() .is_bounded() .then(|| ForeverCurve { @@ -649,13 +657,10 @@ pub trait Curve { /// another curve with outputs of the same type. The domain of the new curve will be twice as /// long. The transition point is guaranteed to not make any jumps. /// - /// # Error + /// # Errors /// /// A [`PingPongError`] is returned if this curve's domain isn't right-finite. - fn ping_pong(self) -> Result, PingPongError> - where - Self: Sized, - { + fn ping_pong(self) -> Result, PingPongError> { self.domain() .has_finite_end() .then(|| PingPongCurve { @@ -676,13 +681,12 @@ pub trait Curve { /// realized by translating the other curve so that its start sample point coincides with the /// current curves' end sample point. /// - /// # Error + /// # Errors /// /// A [`ChainError`] is returned if this curve's domain doesn't have a finite end or if /// `other`'s domain doesn't have a finite start. fn chain_continue(self, other: C) -> Result, ChainError> where - Self: Sized, T: VectorSpace, C: Curve, { @@ -704,17 +708,88 @@ pub trait Curve { }) } + /// Extract an iterator over evenly-spaced samples from this curve. + /// + /// # Errors + /// + /// If `samples` is less than 2 or if this curve has unbounded domain, a [`ResamplingError`] + /// is returned. + fn samples(&self, samples: usize) -> Result, ResamplingError> { + if samples < 2 { + return Err(ResamplingError::NotEnoughSamples(samples)); + } + if !self.domain().is_bounded() { + return Err(ResamplingError::UnboundedDomain); + } + + // Unwrap on `spaced_points` always succeeds because its error conditions are handled + // above. + Ok(self + .domain() + .spaced_points(samples) + .unwrap() + .map(|t| self.sample_unchecked(t))) + } + + /// Borrow this curve rather than taking ownership of it. This is essentially an alias for a + /// prefix `&`; the point is that intermediate operations can be performed while retaining + /// access to the original curve. + /// + /// # Example + /// ``` + /// # use bevy_math::curve::*; + /// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0); + /// + /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes + /// // ownership of its input. + /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); + /// + /// // Do something else with `my_curve` since we retained ownership: + /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); + /// ``` + fn by_ref(&self) -> &Self { + self + } + + /// Flip this curve so that its tuple output is arranged the other way. + #[must_use] + fn flip(self) -> impl Curve<(V, U)> + where + Self: CurveExt<(U, V)>, + { + self.map(|(u, v)| (v, u)) + } +} + +impl CurveExt for C where C: Curve {} + +/// Extension trait implemented by [curves], allowing access to generic resampling methods as +/// well as those based on [stable interpolation]. +/// +/// This trait is automatically implemented for all curves. +/// +/// For more information, see the [module-level documentation]. +/// +/// [curves]: Curve +/// [stable interpolation]: crate::StableInterpolate +/// [module-level documentation]: self +#[cfg(feature = "alloc")] +pub trait CurveResampleExt: Curve { /// Resample this [`Curve`] to produce a new one that is defined by interpolation over equally /// spaced sample values, using the provided `interpolation` to interpolate between adjacent samples. /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1, /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at - /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded - /// domain, then a [`ResamplingError`] is returned. + /// the midpoint is taken as well, and so on. /// /// The interpolation takes two values by reference together with a scalar parameter and /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. /// + /// # Errors + /// + /// If `segments` is zero or if this curve has unbounded domain, then a [`ResamplingError`] is + /// returned. + /// /// # Example /// ``` /// # use bevy_math::*; @@ -723,14 +798,12 @@ pub trait Curve { /// // A curve which only stores three data points and uses `nlerp` to interpolate them: /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t)); /// ``` - #[cfg(feature = "alloc")] fn resample( &self, segments: usize, interpolation: I, ) -> Result, ResamplingError> where - Self: Sized, I: Fn(&T, &T, f32) -> T, { let samples = self.samples(segments + 1)?.collect_vec(); @@ -747,14 +820,15 @@ pub trait Curve { /// spaced sample values, using [automatic interpolation] to interpolate between adjacent samples. /// The curve is interpolated on `segments` segments between samples. For example, if `segments` is 1, /// only the start and end points of the curve are used as samples; if `segments` is 2, a sample at - /// the midpoint is taken as well, and so on. If `segments` is zero, or if this curve has an unbounded - /// domain, then a [`ResamplingError`] is returned. + /// the midpoint is taken as well, and so on. + /// + /// # Errors + /// + /// If `segments` is zero or if this curve has unbounded domain, a [`ResamplingError`] is returned. /// /// [automatic interpolation]: crate::common_traits::StableInterpolate - #[cfg(feature = "alloc")] fn resample_auto(&self, segments: usize) -> Result, ResamplingError> where - Self: Sized, T: StableInterpolate, { let samples = self.samples(segments + 1)?.collect_vec(); @@ -766,35 +840,13 @@ pub trait Curve { }) } - /// Extract an iterator over evenly-spaced samples from this curve. If `samples` is less than 2 - /// or if this curve has unbounded domain, then an error is returned instead. - fn samples(&self, samples: usize) -> Result, ResamplingError> - where - Self: Sized, - { - if samples < 2 { - return Err(ResamplingError::NotEnoughSamples(samples)); - } - if !self.domain().is_bounded() { - return Err(ResamplingError::UnboundedDomain); - } - - // Unwrap on `spaced_points` always succeeds because its error conditions are handled - // above. - Ok(self - .domain() - .spaced_points(samples) - .unwrap() - .map(|t| self.sample_unchecked(t))) - } - /// Resample this [`Curve`] to produce a new one that is defined by interpolation over samples /// taken at a given set of times. The given `interpolation` is used to interpolate adjacent /// samples, and the `sample_times` are expected to contain at least two valid times within the /// curve's domain interval. /// /// Redundant sample times, non-finite sample times, and sample times outside of the domain - /// are simply filtered out. With an insufficient quantity of data, a [`ResamplingError`] is + /// are filtered out. With an insufficient quantity of data, a [`ResamplingError`] is /// returned. /// /// The domain of the produced curve stretches between the first and last sample times of the @@ -803,14 +855,17 @@ pub trait Curve { /// The interpolation takes two values by reference together with a scalar parameter and /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. - #[cfg(feature = "alloc")] + /// + /// # Errors + /// + /// If `sample_times` doesn't contain at least two distinct times after filtering, a + /// [`ResamplingError`] is returned. fn resample_uneven( &self, sample_times: impl IntoIterator, interpolation: I, ) -> Result, ResamplingError> where - Self: Sized, I: Fn(&T, &T, f32) -> T, { let domain = self.domain(); @@ -841,14 +896,17 @@ pub trait Curve { /// The domain of the produced [`UnevenSampleAutoCurve`] stretches between the first and last /// sample times of the iterator. /// + /// # Errors + /// + /// If `sample_times` doesn't contain at least two distinct times after filtering, a + /// [`ResamplingError`] is returned. + /// /// [automatic interpolation]: crate::common_traits::StableInterpolate - #[cfg(feature = "alloc")] fn resample_uneven_auto( &self, sample_times: impl IntoIterator, ) -> Result, ResamplingError> where - Self: Sized, T: StableInterpolate, { let domain = self.domain(); @@ -866,53 +924,10 @@ pub trait Curve { core: UnevenCore { times, samples }, }) } - - /// Borrow this curve rather than taking ownership of it. This is essentially an alias for a - /// prefix `&`; the point is that intermediate operations can be performed while retaining - /// access to the original curve. - /// - /// # Example - /// ``` - /// # use bevy_math::curve::*; - /// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0); - /// - /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes - /// // ownership of its input. - /// let samples = my_curve.by_ref().map(|x| x * 2.0).resample_auto(100).unwrap(); - /// - /// // Do something else with `my_curve` since we retained ownership: - /// let new_curve = my_curve.reparametrize_linear(interval(-1.0, 1.0).unwrap()).unwrap(); - /// ``` - fn by_ref(&self) -> &Self - where - Self: Sized, - { - self - } - - /// Flip this curve so that its tuple output is arranged the other way. - #[must_use] - fn flip(self) -> impl Curve<(V, U)> - where - Self: Sized + Curve<(U, V)>, - { - self.map(|(u, v)| (v, u)) - } } -impl Curve for D -where - C: Curve + ?Sized, - D: Deref, -{ - fn domain(&self) -> Interval { - >::domain(self) - } - - fn sample_unchecked(&self, t: f32) -> T { - >::sample_unchecked(self, t) - } -} +#[cfg(feature = "alloc")] +impl CurveResampleExt for C where C: Curve + ?Sized {} /// An error indicating that a linear reparameterization couldn't be performed because of /// malformed inputs. @@ -990,6 +1005,7 @@ pub enum ResamplingError { mod tests { use super::*; use crate::{ops, Quat}; + use alloc::vec::Vec; use approx::{assert_abs_diff_eq, AbsDiffEq}; use core::f32::consts::TAU; use glam::*; diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index 7a37f55640090..681500328b2da 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -4,6 +4,7 @@ use super::cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use super::{Curve, Interval}; use crate::StableInterpolate; +use alloc::format; use core::any::type_name; use core::fmt::{self, Debug}; @@ -285,11 +286,13 @@ impl UnevenSampleCurve { } /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. - /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// In principle, when `f` is monotone, this is equivalent to [`CurveExt::reparametrize`], /// but the function inputs to each are inverses of one another. /// /// The samples are re-sorted by time after mapping and deduplicated by output time, so /// the function `f` should generally be injective over the sample times of the curve. + /// + /// [`CurveExt::reparametrize`]: super::CurveExt::reparametrize pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { Self { core: self.core.map_sample_times(f), @@ -343,11 +346,13 @@ impl UnevenSampleAutoCurve { } /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. - /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// In principle, when `f` is monotone, this is equivalent to [`CurveExt::reparametrize`], /// but the function inputs to each are inverses of one another. /// /// The samples are re-sorted by time after mapping and deduplicated by output time, so /// the function `f` should generally be injective over the sample times of the curve. + /// + /// [`CurveExt::reparametrize`]: super::CurveExt::reparametrize pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { Self { core: self.core.map_sample_times(f), @@ -365,6 +370,7 @@ mod tests { //! - function pointers use super::{SampleCurve, UnevenSampleCurve}; use crate::{curve::Interval, VectorSpace}; + use alloc::boxed::Box; use bevy_reflect::Reflect; #[test] diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index 5e11d1434bf42..3e32d782babe4 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -8,9 +8,13 @@ use derive_more::derive::Into; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; + #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +#[cfg(all(debug_assertions, feature = "std"))] +use std::eprintln; + /// An error indicating that a direction is invalid. #[derive(Debug, PartialEq)] pub enum InvalidDirectionError { @@ -76,20 +80,6 @@ fn assert_is_normalized(message: &str, length_squared: f32) { } } -/// A normalized vector pointing in a direction in 2D space -#[deprecated( - since = "0.14.0", - note = "`Direction2d` has been renamed. Please use `Dir2` instead." -)] -pub type Direction2d = Dir2; - -/// A normalized vector pointing in a direction in 3D space -#[deprecated( - since = "0.14.0", - note = "`Direction3d` has been renamed. Please use `Dir3` instead." -)] -pub type Direction3d = Dir3; - /// A normalized vector pointing in a direction in 2D space #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] diff --git a/crates/bevy_math/src/float_ord.rs b/crates/bevy_math/src/float_ord.rs index b5f72d1c7cf5e..2369b0f6dc1db 100644 --- a/crates/bevy_math/src/float_ord.rs +++ b/crates/bevy_math/src/float_ord.rs @@ -47,7 +47,10 @@ impl PartialOrd for FloatOrd { } impl Ord for FloatOrd { - #[allow(clippy::comparison_chain)] + #[expect( + clippy::comparison_chain, + reason = "This can't be rewritten with `match` and `cmp`, as this is `cmp` itself." + )] fn cmp(&self, other: &Self) -> Ordering { if self > other { Ordering::Greater @@ -124,7 +127,10 @@ mod tests { } #[test] - #[allow(clippy::nonminimal_bool)] + #[expect( + clippy::nonminimal_bool, + reason = "This tests that all operators work as they should, and in the process requires some non-simplified boolean expressions." + )] fn float_ord_cmp_operators() { assert!(!(NAN < NAN)); assert!(NAN < ZERO); diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index a276111c9d500..20d458db72d23 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -12,7 +12,7 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] //! Provides math types and functionality for the Bevy game engine. //! @@ -20,6 +20,9 @@ //! matrices like [`Mat2`], [`Mat3`] and [`Mat4`] and orientation representations //! like [`Quat`]. +#[cfg(feature = "std")] +extern crate std; + #[cfg(feature = "alloc")] extern crate alloc; diff --git a/crates/bevy_math/src/ops.rs b/crates/bevy_math/src/ops.rs index 42fb55286cc7b..e9d27ac54a83a 100644 --- a/crates/bevy_math/src/ops.rs +++ b/crates/bevy_math/src/ops.rs @@ -8,9 +8,6 @@ //! It also provides `no_std` compatible alternatives to certain floating-point //! operations which are not provided in the [`core`] library. -#![allow(dead_code)] -#![allow(clippy::disallowed_methods)] - // Note: There are some Rust methods with unspecified precision without a `libm` // equivalent: // - `f32::powi` (integer powers) @@ -23,6 +20,10 @@ // - `f32::ln_gamma` #[cfg(not(feature = "libm"))] +#[expect( + clippy::disallowed_methods, + reason = "Many of the disallowed methods are disallowed to force code to use the feature-conditional re-exports from this module, but this module itself is exempt from that rule." +)] mod std_ops { /// Raises a number to a floating point power. @@ -519,6 +520,10 @@ mod libm_ops_for_no_std { } #[cfg(feature = "std")] +#[expect( + clippy::disallowed_methods, + reason = "Many of the disallowed methods are disallowed to force code to use the feature-conditional re-exports from this module, but this module itself is exempt from that rule." +)] mod std_ops_for_no_std { //! Provides standardized names for [`f32`] operations which may not be //! supported on `no_std` platforms. diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index d476fd86077eb..cdd5b805cde3a 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -2242,9 +2242,9 @@ mod tests { let mut rotated_vertices = polygon.vertices(core::f32::consts::FRAC_PI_4).into_iter(); // Distance from the origin to the middle of a side, derived using Pythagorean theorem - let side_sistance = FRAC_1_SQRT_2; + let side_distance = FRAC_1_SQRT_2; assert!( - (rotated_vertices.next().unwrap() - Vec2::new(-side_sistance, side_sistance)).length() + (rotated_vertices.next().unwrap() - Vec2::new(-side_distance, side_distance)).length() < 1e-7, ); } diff --git a/crates/bevy_math/src/primitives/polygon.rs b/crates/bevy_math/src/primitives/polygon.rs index 1167d07981094..20d35b552c827 100644 --- a/crates/bevy_math/src/primitives/polygon.rs +++ b/crates/bevy_math/src/primitives/polygon.rs @@ -1,12 +1,17 @@ #[cfg(feature = "alloc")] -use alloc::{collections::BTreeMap, vec::Vec}; +use { + super::{Measured2d, Triangle2d}, + alloc::{collections::BTreeMap, vec::Vec}, +}; use core::cmp::Ordering; use crate::Vec2; -use super::{Measured2d, Triangle2d}; - +#[cfg_attr( + not(feature = "alloc"), + expect(dead_code, reason = "this type is only used with the alloc feature") +)] #[derive(Debug, Clone, Copy)] enum Endpoint { Left, @@ -20,12 +25,20 @@ enum Endpoint { /// /// This is the order expected by the [`SweepLine`]. #[derive(Debug, Clone, Copy)] +#[cfg_attr( + not(feature = "alloc"), + allow(dead_code, reason = "this type is only used with the alloc feature") +)] struct SweepLineEvent { segment: Segment, /// Type of the vertex (left or right) endpoint: Endpoint, } impl SweepLineEvent { + #[cfg_attr( + not(feature = "alloc"), + allow(dead_code, reason = "this type is only used with the alloc feature") + )] fn position(&self) -> Vec2 { match self.endpoint { Endpoint::Left => self.segment.left, @@ -51,11 +64,12 @@ impl Ord for SweepLineEvent { } /// Orders 2D points according to the order expected by the sweep line and event queue from -X to +X and then -Y to Y. +#[cfg_attr( + not(feature = "alloc"), + allow(dead_code, reason = "this type is only used with the alloc feature") +)] fn xy_order(a: Vec2, b: Vec2) -> Ordering { - match a.x.total_cmp(&b.x) { - Ordering::Equal => a.y.total_cmp(&b.y), - ord => ord, - } + a.x.total_cmp(&b.x).then_with(|| a.y.total_cmp(&b.y)) } /// The event queue holds an ordered list of all events the [`SweepLine`] will encounter when checking the current polygon. @@ -121,6 +135,7 @@ impl PartialEq for Segment { } } impl Eq for Segment {} + impl PartialOrd for Segment { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) @@ -128,14 +143,18 @@ impl PartialOrd for Segment { } impl Ord for Segment { fn cmp(&self, other: &Self) -> Ordering { - match self.left.y.total_cmp(&other.left.y) { - Ordering::Equal => self.right.y.total_cmp(&other.right.y), - ord => ord, - } + self.left + .y + .total_cmp(&other.left.y) + .then_with(|| self.right.y.total_cmp(&other.right.y)) } } /// Holds information about which segment is above and which is below a given [`Segment`] +#[cfg_attr( + not(feature = "alloc"), + expect(dead_code, reason = "this type is only used with the alloc feature") +)] #[derive(Debug, Clone, Copy)] struct SegmentOrder { above: Option, @@ -243,6 +262,13 @@ impl<'a> SweepLine<'a> { /// Test what side of the line through `p1` and `p2` `q` is. /// /// The result will be `0` if the `q` is on the segment, negative for one side and positive for the other. +#[cfg_attr( + not(feature = "alloc"), + expect( + dead_code, + reason = "this function is only used with the alloc feature" + ) +)] #[inline(always)] fn point_side(p1: Vec2, p2: Vec2, q: Vec2) -> f32 { (p2.x - p1.x) * (q.y - p1.y) - (q.x - p1.x) * (p2.y - p1.y) diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs index 5b0bc816bc5ca..40760dcb84273 100644 --- a/crates/bevy_math/src/rotation2d.rs +++ b/crates/bevy_math/src/rotation2d.rs @@ -329,16 +329,6 @@ impl Rot2 { self.cos > 0.0 && ops::abs(self.sin) < threshold_angle_sin } - /// Returns the angle in radians needed to make `self` and `other` coincide. - #[inline] - #[deprecated( - since = "0.15.0", - note = "Use `angle_to` instead, the semantics of `angle_between` will change in the future." - )] - pub fn angle_between(self, other: Self) -> f32 { - self.angle_to(other) - } - /// Returns the angle in radians needed to make `self` and `other` coincide. #[inline] pub fn angle_to(self, other: Self) -> f32 { diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 68d77cd1d7f0b..d1371114bd6d3 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -61,7 +61,7 @@ pub trait ShapeSample { /// let square = Rectangle::new(2.0, 2.0); /// /// // Returns a Vec2 with both x and y between -1 and 1. - /// println!("{:?}", square.sample_interior(&mut rand::thread_rng())); + /// println!("{}", square.sample_interior(&mut rand::thread_rng())); /// ``` fn sample_interior(&self, rng: &mut R) -> Self::Output; @@ -76,7 +76,7 @@ pub trait ShapeSample { /// /// // Returns a Vec2 where one of the coordinates is at ±1, /// // and the other is somewhere between -1 and 1. - /// println!("{:?}", square.sample_boundary(&mut rand::thread_rng())); + /// println!("{}", square.sample_boundary(&mut rand::thread_rng())); /// ``` fn sample_boundary(&self, rng: &mut R) -> Self::Output; @@ -92,7 +92,7 @@ pub trait ShapeSample { /// /// // Iterate over points randomly drawn from `square`'s interior: /// for random_val in square.interior_dist().sample_iter(rng).take(5) { - /// println!("{:?}", random_val); + /// println!("{}", random_val); /// } /// ``` fn interior_dist(self) -> impl Distribution @@ -114,7 +114,7 @@ pub trait ShapeSample { /// /// // Iterate over points randomly drawn from `square`'s boundary: /// for random_val in square.boundary_dist().sample_iter(rng).take(5) { - /// println!("{:?}", random_val); + /// println!("{}", random_val); /// } /// ``` fn boundary_dist(self) -> impl Distribution diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index e5871c3e3c6c3..d98c3a9b46f34 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mesh" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides mesh types for Bevy Engine" homepage = "https://bevyengine.org" @@ -9,25 +9,27 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +# 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", features = [ "bevy", ] } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.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" } -# misc +# other bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } wgpu-types = { version = "23", default-features = false } serde = { version = "1", features = ["derive"] } hexasphere = "15.0" thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index f643cc6674240..fbf70d95fc308 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -13,8 +13,8 @@ use bevy_asset::{Asset, Handle, RenderAssetUsages}; use bevy_image::Image; use bevy_math::{primitives::Triangle3d, *}; use bevy_reflect::Reflect; -use bevy_utils::tracing::warn; use bytemuck::cast_slice; +use tracing::warn; use wgpu_types::{VertexAttribute, VertexFormat, VertexStepMode}; pub const INDEX_BUFFER_ASSET_INDEX: u64 = 0; @@ -491,7 +491,6 @@ impl Mesh { /// /// This can dramatically increase the vertex count, so make sure this is what you want. /// Does nothing if no [Indices] are set. - #[allow(clippy::match_same_arms)] pub fn duplicate_vertices(&mut self) { fn duplicate(values: &[T], indices: impl Iterator) -> Vec { indices.map(|i| values[i]).collect() @@ -503,6 +502,10 @@ impl Mesh { for attributes in self.attributes.values_mut() { let indices = indices.iter(); + #[expect( + clippy::match_same_arms, + reason = "Although the `vec` binding on some match arms may have different types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever." + )] match &mut attributes.values { VertexAttributeValues::Float32(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Sint32(vec) => *vec = duplicate(vec, indices), @@ -789,7 +792,6 @@ impl Mesh { /// /// Panics if the vertex attribute values of `other` are incompatible with `self`. /// For example, [`VertexAttributeValues::Float32`] is incompatible with [`VertexAttributeValues::Float32x3`]. - #[allow(clippy::match_same_arms)] pub fn merge(&mut self, other: &Mesh) { use VertexAttributeValues::*; @@ -800,6 +802,10 @@ impl Mesh { for (attribute, values) in self.attributes_mut() { let enum_variant_name = values.enum_variant_name(); if let Some(other_values) = other.attribute(attribute.id) { + #[expect( + clippy::match_same_arms, + reason = "Although the bindings on some match arms may have different types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever." + )] match (values, other_values) { (Float32(vec1), Float32(vec2)) => vec1.extend(vec2), (Sint32(vec1), Sint32(vec2)) => vec1.extend(vec2), diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 3eda19eed9037..7087ab44bd144 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -399,6 +399,9 @@ impl From for Mesh { } /// A builder used for creating a [`Mesh`] with a [`ConvexPolygon`] shape. +/// +/// You must verify that the `vertices` are not concave when constructing this type. You can +/// guarantee this by creating a [`ConvexPolygon`] first, then calling [`ConvexPolygon::mesh()`]. pub struct ConvexPolygonMeshBuilder { pub vertices: [Vec2; N], } @@ -452,6 +455,28 @@ pub struct RegularPolygonMeshBuilder { circumradius: f32, sides: u32, } + +impl RegularPolygonMeshBuilder { + /// Creates a new [`RegularPolygonMeshBuilder`] from the radius of a circumcircle and a number + /// of sides. + /// + /// # Panics + /// + /// Panics in debug mode if `circumradius` is negative, or if `sides` is less than 3. + pub const fn new(circumradius: f32, sides: u32) -> Self { + debug_assert!( + circumradius.is_sign_positive(), + "polygon has a negative radius" + ); + debug_assert!(sides > 2, "polygon has less than 3 sides"); + + Self { + circumradius, + sides, + } + } +} + impl Meshable for RegularPolygon { type Output = RegularPolygonMeshBuilder; @@ -726,6 +751,28 @@ pub struct RhombusMeshBuilder { half_diagonals: Vec2, } +impl RhombusMeshBuilder { + /// Creates a new [`RhombusMeshBuilder`] from a horizontal and vertical diagonal size. + /// + /// # Panics + /// + /// Panics in debug mode if `horizontal_diagonal` or `vertical_diagonal` is negative. + pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { + debug_assert!( + horizontal_diagonal >= 0.0, + "rhombus has a negative horizontal size", + ); + debug_assert!( + vertical_diagonal >= 0.0, + "rhombus has a negative vertical size" + ); + + Self { + half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), + } + } +} + impl MeshBuilder for RhombusMeshBuilder { fn build(&self) -> Mesh { let [hhd, vhd] = [self.half_diagonals.x, self.half_diagonals.y]; @@ -778,6 +825,16 @@ impl From for Mesh { pub struct Triangle2dMeshBuilder { triangle: Triangle2d, } + +impl Triangle2dMeshBuilder { + /// Creates a new [`Triangle2dMeshBuilder`] from the points `a`, `b`, and `c`. + pub const fn new(a: Vec2, b: Vec2, c: Vec2) -> Self { + Self { + triangle: Triangle2d::new(a, b, c), + } + } +} + impl Meshable for Triangle2d { type Output = Triangle2dMeshBuilder; @@ -785,6 +842,7 @@ impl Meshable for Triangle2d { Self::Output { triangle: *self } } } + impl MeshBuilder for Triangle2dMeshBuilder { fn build(&self) -> Mesh { let vertices_3d = self.triangle.vertices.map(|v| v.extend(0.)); @@ -843,6 +901,22 @@ pub struct RectangleMeshBuilder { half_size: Vec2, } +impl RectangleMeshBuilder { + /// Creates a new [`RectangleMeshBuilder`] from a full width and height. + /// + /// # Panics + /// + /// Panics in debug mode if `width` or `height` is negative. + pub const fn new(width: f32, height: f32) -> Self { + debug_assert!(width >= 0.0, "rectangle has a negative width"); + debug_assert!(height >= 0.0, "rectangle has a negative height"); + + Self { + half_size: Vec2::new(width / 2.0, height / 2.0), + } + } +} + impl MeshBuilder for RectangleMeshBuilder { fn build(&self) -> Mesh { let [hw, hh] = [self.half_size.x, self.half_size.y]; diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 4c6a36fc7698b..8776b99bb4c5c 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -170,45 +170,43 @@ pub trait VertexFormatSize { } impl VertexFormatSize for VertexFormat { - #[allow(clippy::match_same_arms)] fn get_size(self) -> u64 { - match self { - VertexFormat::Uint8x2 => 2, - VertexFormat::Uint8x4 => 4, - VertexFormat::Sint8x2 => 2, - VertexFormat::Sint8x4 => 4, - VertexFormat::Unorm8x2 => 2, - VertexFormat::Unorm8x4 => 4, - VertexFormat::Snorm8x2 => 2, - VertexFormat::Snorm8x4 => 4, - VertexFormat::Unorm10_10_10_2 => 4, - VertexFormat::Uint16x2 => 2 * 2, - VertexFormat::Uint16x4 => 2 * 4, - VertexFormat::Sint16x2 => 2 * 2, - VertexFormat::Sint16x4 => 2 * 4, - VertexFormat::Unorm16x2 => 2 * 2, - VertexFormat::Unorm16x4 => 2 * 4, - VertexFormat::Snorm16x2 => 2 * 2, - VertexFormat::Snorm16x4 => 2 * 4, + use core::mem::size_of; + let size = match self { + VertexFormat::Uint8x2 | VertexFormat::Unorm8x2 => size_of::() * 2, + VertexFormat::Uint8x4 | VertexFormat::Unorm8x4 => size_of::() * 4, + VertexFormat::Sint8x2 | VertexFormat::Snorm8x2 => size_of::() * 2, + VertexFormat::Sint8x4 | VertexFormat::Snorm8x4 => size_of::() * 4, + VertexFormat::Unorm10_10_10_2 => 10 + 10 + 10 + 2, + VertexFormat::Uint16x2 | VertexFormat::Unorm16x2 => size_of::() * 2, + VertexFormat::Uint16x4 | VertexFormat::Unorm16x4 => size_of::() * 4, + VertexFormat::Sint16x2 | VertexFormat::Snorm16x2 => size_of::() * 2, + VertexFormat::Sint16x4 | VertexFormat::Snorm16x4 => size_of::() * 4, + // NOTE: As of the time of writing this code, `f16` is not a stabilized primitive, so we + // can't use `size_of::()` here. VertexFormat::Float16x2 => 2 * 2, VertexFormat::Float16x4 => 2 * 4, - VertexFormat::Float32 => 4, - VertexFormat::Float32x2 => 4 * 2, - VertexFormat::Float32x3 => 4 * 3, - VertexFormat::Float32x4 => 4 * 4, - VertexFormat::Uint32 => 4, - VertexFormat::Uint32x2 => 4 * 2, - VertexFormat::Uint32x3 => 4 * 3, - VertexFormat::Uint32x4 => 4 * 4, - VertexFormat::Sint32 => 4, - VertexFormat::Sint32x2 => 4 * 2, - VertexFormat::Sint32x3 => 4 * 3, - VertexFormat::Sint32x4 => 4 * 4, - VertexFormat::Float64 => 8, - VertexFormat::Float64x2 => 8 * 2, - VertexFormat::Float64x3 => 8 * 3, - VertexFormat::Float64x4 => 8 * 4, - } + VertexFormat::Float32 => size_of::(), + VertexFormat::Float32x2 => size_of::() * 2, + VertexFormat::Float32x3 => size_of::() * 3, + VertexFormat::Float32x4 => size_of::() * 4, + VertexFormat::Uint32 => size_of::(), + VertexFormat::Uint32x2 => size_of::() * 2, + VertexFormat::Uint32x3 => size_of::() * 3, + VertexFormat::Uint32x4 => size_of::() * 4, + VertexFormat::Sint32 => size_of::(), + VertexFormat::Sint32x2 => size_of::() * 2, + VertexFormat::Sint32x3 => size_of::() * 3, + VertexFormat::Sint32x4 => size_of::() * 4, + VertexFormat::Float64 => size_of::(), + VertexFormat::Float64x2 => size_of::() * 2, + VertexFormat::Float64x3 => size_of::() * 3, + VertexFormat::Float64x4 => size_of::() * 4, + }; + + // We can safely cast `size` (a `usize`) into a `u64`, as we don't even reach the limits of + // of a `u8`. + size.try_into().unwrap() } } @@ -249,7 +247,10 @@ pub enum VertexAttributeValues { impl VertexAttributeValues { /// Returns the number of vertices in this [`VertexAttributeValues`]. For a single /// mesh, all of the [`VertexAttributeValues`] must have the same length. - #[allow(clippy::match_same_arms)] + #[expect( + clippy::match_same_arms, + reason = "Although the `values` binding on some match arms may have matching types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever." + )] pub fn len(&self) -> usize { match self { VertexAttributeValues::Float32(values) => values.len(), @@ -299,7 +300,10 @@ impl VertexAttributeValues { // TODO: add vertex format as parameter here and perform type conversions /// Flattens the [`VertexAttributeValues`] into a sequence of bytes. This is /// useful for serialization and sending to the GPU. - #[allow(clippy::match_same_arms)] + #[expect( + clippy::match_same_arms, + reason = "Although the `values` binding on some match arms may have matching types, each variant has different semantics; thus it's not guaranteed that they will use the same type forever." + )] pub fn get_bytes(&self) -> &[u8] { match self { VertexAttributeValues::Float32(values) => cast_slice(values), diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 6145bcf9dde87..0ab431aa8fa9d 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_mikktspace" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" authors = [ "Benjamin Wasty ", diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index fe514e208eb47..ee5f149a8ca19 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -1,3 +1,8 @@ +#![allow( + clippy::allow_attributes, + clippy::allow_attributes_without_reason, + reason = "Much of the code here is still code that's been transpiled from C; we want to save 'fixing' this crate until after it's ported to safe rust." +)] #![allow( unsafe_op_in_unsafe_fn, clippy::all, @@ -11,7 +16,10 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; extern crate alloc; diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index cd42bfebb8d05..f180cdb9fb3ee 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_pbr" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Adds PBR rendering to Bevy Engine" homepage = "https://bevyengine.org" @@ -31,23 +31,22 @@ meshlet_processor = [ [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", ] } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", optional = true } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.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" } # other bitflags = "2.3" @@ -60,7 +59,7 @@ lz4_flex = { version = "0.11", default-features = false, features = [ ], optional = true } range-alloc = { version = "0.1.3", optional = true } half = { version = "2", features = ["bytemuck"], optional = true } -meshopt = { version = "0.4", optional = true } +meshopt = { version = "0.4.1", optional = true } metis = { version = "0.2", optional = true } itertools = { version = "0.13", optional = true } bitvec = { version = "1", optional = true } @@ -70,6 +69,7 @@ radsort = "0.1" smallvec = "1.6" nonmax = "0.5" static_assertions = "1" +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs deleted file mode 100644 index bdfdd695f5904..0000000000000 --- a/crates/bevy_pbr/src/bundle.rs +++ /dev/null @@ -1,214 +0,0 @@ -#![expect(deprecated)] - -use crate::{ - CascadeShadowConfig, Cascades, DirectionalLight, Material, MeshMaterial3d, PointLight, - SpotLight, StandardMaterial, -}; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - bundle::Bundle, - component::Component, - entity::{Entity, EntityHashMap}, - reflect::ReflectComponent, -}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::sync_world::MainEntity; -use bevy_render::{ - mesh::Mesh3d, - primitives::{CascadesFrusta, CubemapFrusta, Frustum}, - sync_world::SyncToRenderWorld, - view::{InheritedVisibility, ViewVisibility, Visibility}, -}; -use bevy_transform::components::{GlobalTransform, Transform}; - -/// A component bundle for PBR entities with a [`Mesh3d`] and a [`MeshMaterial3d`]. -#[deprecated( - since = "0.15.0", - note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub type PbrBundle = MaterialMeshBundle; - -/// A component bundle for entities with a [`Mesh3d`] and a [`MeshMaterial3d`]. -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub struct MaterialMeshBundle { - pub mesh: Mesh3d, - pub material: MeshMaterial3d, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// User indication of whether an entity is visible - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, -} - -impl Default for MaterialMeshBundle { - fn default() -> Self { - Self { - mesh: Default::default(), - material: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - visibility: Default::default(), - inherited_visibility: Default::default(), - view_visibility: Default::default(), - } - } -} - -/// 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)] -pub struct VisibleMeshEntities { - #[reflect(ignore)] - pub entities: Vec, -} - -#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] -#[reflect(Component, Debug, Default)] -pub struct RenderVisibleMeshEntities { - #[reflect(ignore)] - pub entities: Vec<(Entity, MainEntity)>, -} - -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component, Debug, Default)] -pub struct CubemapVisibleEntities { - #[reflect(ignore)] - 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)] -pub struct RenderCubemapVisibleEntities { - #[reflect(ignore)] - pub(crate) data: [RenderVisibleMeshEntities; 6], -} - -impl RenderCubemapVisibleEntities { - pub fn get(&self, i: usize) -> &RenderVisibleMeshEntities { - &self.data[i] - } - - pub fn get_mut(&mut self, i: usize) -> &mut RenderVisibleMeshEntities { - &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)] -pub struct CascadesVisibleEntities { - /// Map of view entity to the visible entities for each cascade frustum. - #[reflect(ignore)] - pub entities: EntityHashMap>, -} - -#[derive(Component, Clone, Debug, Default, Reflect)] -#[reflect(Component)] -pub struct RenderCascadesVisibleEntities { - /// Map of view entity to the visible entities for each cascade frustum. - #[reflect(ignore)] - pub entities: EntityHashMap>, -} - -/// A component bundle for [`PointLight`] entities. -#[derive(Debug, Bundle, Default, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `PointLight` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct PointLightBundle { - pub point_light: PointLight, - pub cubemap_visible_entities: CubemapVisibleEntities, - pub cubemap_frusta: CubemapFrusta, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// Enables or disables the light - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} - -/// A component bundle for spot light entities -#[derive(Debug, Bundle, Default, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `SpotLight` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct SpotLightBundle { - pub spot_light: SpotLight, - pub visible_entities: VisibleMeshEntities, - pub frustum: Frustum, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// Enables or disables the light - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} - -/// A component bundle for [`DirectionalLight`] entities. -#[derive(Debug, Bundle, Default, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `DirectionalLight` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct DirectionalLightBundle { - pub directional_light: DirectionalLight, - pub frusta: CascadesFrusta, - pub cascades: Cascades, - pub cascade_shadow_config: CascadeShadowConfig, - pub visible_entities: CascadesVisibleEntities, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// Enables or disables the light - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 69de548b57ae6..297672a78f995 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -17,7 +17,8 @@ use bevy_render::{ view::{RenderLayers, ViewVisibility}, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::{prelude::default, tracing::warn}; +use bevy_utils::prelude::default; +use tracing::warn; use crate::{ prelude::EnvironmentMapLight, ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight, @@ -26,8 +27,6 @@ use crate::{ MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, }; -use super::ClusterableObjectOrderData; - const NDC_MIN: Vec2 = Vec2::NEG_ONE; const NDC_MAX: Vec2 = Vec2::ONE; @@ -136,7 +135,6 @@ impl ClusterableObjectType { } // NOTE: Run this before update_point_light_frusta! -#[allow(clippy::too_many_arguments)] pub(crate) fn assign_objects_to_clusters( mut commands: Commands, mut global_clusterable_objects: ResMut, @@ -254,16 +252,10 @@ pub(crate) fn assign_objects_to_clusters( if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS && !supports_storage_buffers { - clusterable_objects.sort_by(|clusterable_object_1, clusterable_object_2| { - crate::clusterable_object_order( - ClusterableObjectOrderData { - entity: &clusterable_object_1.entity, - object_type: &clusterable_object_1.object_type, - }, - ClusterableObjectOrderData { - entity: &clusterable_object_2.entity, - object_type: &clusterable_object_2.object_type, - }, + clusterable_objects.sort_by_cached_key(|clusterable_object| { + ( + clusterable_object.object_type.ordering(), + clusterable_object.entity, ) }); @@ -849,7 +841,6 @@ pub(crate) fn assign_objects_to_clusters( } } -#[allow(clippy::too_many_arguments)] fn compute_aabb_for_cluster( z_near: f32, z_far: f32, diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 41932a2aaadc1..eb7834871ba8b 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -2,7 +2,6 @@ use core::num::NonZero; -use self::assign::ClusterableObjectType; use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ component::Component, @@ -24,7 +23,8 @@ use bevy_render::{ sync_world::RenderEntity, Extract, }; -use bevy_utils::{tracing::warn, HashSet}; +use bevy_utils::HashSet; +use tracing::warn; pub(crate) use crate::cluster::assign::assign_objects_to_clusters; use crate::MeshPipeline; @@ -516,34 +516,6 @@ impl Default for GpuClusterableObjectsUniform { } } -pub(crate) struct ClusterableObjectOrderData<'a> { - pub(crate) entity: &'a Entity, - pub(crate) object_type: &'a ClusterableObjectType, -} - -#[allow(clippy::too_many_arguments)] -// Sort clusterable objects by: -// -// * object type, so that we can iterate point lights, spot lights, etc. in -// contiguous blocks in the fragment shader, -// -// * then those with shadows enabled first, so that the index can be used to -// render at most `point_light_shadow_maps_count` point light shadows and -// `spot_light_shadow_maps_count` spot light shadow maps, -// -// * then by entity as a stable key to ensure that a consistent set of -// clusterable objects are chosen if the clusterable object count limit is -// exceeded. -pub(crate) fn clusterable_object_order( - a: ClusterableObjectOrderData, - b: ClusterableObjectOrderData, -) -> core::cmp::Ordering { - a.object_type - .ordering() - .cmp(&b.object_type.ordering()) - .then_with(|| a.entity.cmp(b.entity)) // stable -} - /// Extracts clusters from the main world from the render world. pub fn extract_clusters( mut commands: Commands, @@ -853,7 +825,7 @@ impl ViewClusterBuffers { // the number of light probes is irrelevant. fn pack_offset_and_counts(offset: usize, point_count: u32, spot_count: u32) -> u32 { ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2)) - | (point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE + | ((point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE) | (spot_count & CLUSTER_COUNT_MASK) } diff --git a/crates/bevy_pbr/src/components.rs b/crates/bevy_pbr/src/components.rs new file mode 100644 index 0000000000000..189862cc55e9a --- /dev/null +++ b/crates/bevy_pbr/src/components.rs @@ -0,0 +1,89 @@ +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)] +pub struct VisibleMeshEntities { + #[reflect(ignore)] + pub entities: Vec, +} + +#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Default)] +pub struct RenderVisibleMeshEntities { + #[reflect(ignore)] + pub entities: Vec<(Entity, MainEntity)>, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Debug, Default)] +pub struct CubemapVisibleEntities { + #[reflect(ignore)] + 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)] +pub struct RenderCubemapVisibleEntities { + #[reflect(ignore)] + pub(crate) data: [RenderVisibleMeshEntities; 6], +} + +impl RenderCubemapVisibleEntities { + pub fn get(&self, i: usize) -> &RenderVisibleMeshEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut RenderVisibleMeshEntities { + &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)] +pub struct CascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore)] + pub entities: EntityHashMap>, +} + +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct RenderCascadesVisibleEntities { + /// Map of view entity to the visible entities for each cascade frustum. + #[reflect(ignore)] + pub entities: EntityHashMap>, +} diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs new file mode 100644 index 0000000000000..e580ae922a49b --- /dev/null +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -0,0 +1,128 @@ +use crate::{ + ExtendedMaterial, Material, MaterialExtension, MaterialExtensionKey, MaterialExtensionPipeline, + MaterialPlugin, StandardMaterial, +}; +use bevy_app::{App, Plugin}; +use bevy_asset::{load_internal_asset, Asset, Assets, Handle}; +use bevy_ecs::component::{require, Component}; +use bevy_math::{prelude::Rectangle, Quat, Vec2, Vec3}; +use bevy_reflect::{Reflect, TypePath}; +use bevy_render::{ + alpha::AlphaMode, + mesh::{Mesh, Mesh3d, MeshBuilder, MeshVertexBufferLayoutRef, Meshable}, + render_resource::{ + AsBindGroup, CompareFunction, RenderPipelineDescriptor, Shader, + SpecializedMeshPipelineError, + }, +}; + +const FORWARD_DECAL_MESH_HANDLE: Handle = Handle::weak_from_u128(19376620402995522466); +const FORWARD_DECAL_SHADER_HANDLE: Handle = Handle::weak_from_u128(29376620402995522466); + +/// 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 + ); + + app.register_type::(); + + app.world_mut().resource_mut::>().insert( + FORWARD_DECAL_MESH_HANDLE.id(), + Rectangle::from_size(Vec2::ONE) + .mesh() + .build() + .rotated_by(Quat::from_rotation_arc(Vec3::Z, Vec3::Y)) + .with_generated_tangents() + .unwrap(), + ); + + app.add_plugins(MaterialPlugin::> { + prepass_enabled: false, + shadows_enabled: false, + ..Default::default() + }); + } +} + +/// A decal that renders via a 1x1 transparent quad mesh, smoothly alpha-blending with the underlying +/// geometry towards the edges. +/// +/// Because forward decals are meshes, you can use arbitrary materials to control their appearance. +/// +/// # Usage Notes +/// +/// * Spawn this component on an entity with a [`crate::MeshMaterial3d`] component holding a [`ForwardDecalMaterial`]. +/// * Any camera rendering a forward decal must have the [`bevy_core_pipeline::DepthPrepass`] component. +/// * Looking at forward decals at a steep angle can cause distortion. This can be mitigated by padding your decal's +/// texture with extra transparent pixels on the edges. +#[derive(Component, Reflect)] +#[require(Mesh3d(|| Mesh3d(FORWARD_DECAL_MESH_HANDLE)))] +pub struct ForwardDecal; + +/// Type alias for an extended material with a [`ForwardDecalMaterialExt`] extension. +/// +/// Make sure to register the [`MaterialPlugin`] for this material in your app setup. +/// +/// [`StandardMaterial`] comes with out of the box support for forward decals. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type ForwardDecalMaterial = ExtendedMaterial; + +/// Material extension for a [`ForwardDecal`]. +/// +/// In addition to wrapping your material type with this extension, your shader must use +/// the `bevy_pbr::decal::forward::get_forward_decal_info` function. +/// +/// The `FORWARD_DECAL` shader define will be made available to your shader so that you can gate +/// the forward decal code behind an ifdef. +#[derive(Asset, AsBindGroup, TypePath, Clone, Debug)] +pub struct ForwardDecalMaterialExt { + /// Controls how far away a surface must be before the decal will stop blending with it, and instead render as opaque. + /// + /// Decreasing this value will cause the decal to blend only to surfaces closer to it. + /// + /// Units are in meters. + #[uniform(200)] + pub depth_fade_factor: f32, +} + +impl MaterialExtension for ForwardDecalMaterialExt { + fn alpha_mode() -> Option { + Some(AlphaMode::Blend) + } + + fn specialize( + _pipeline: &MaterialExtensionPipeline, + descriptor: &mut RenderPipelineDescriptor, + _layout: &MeshVertexBufferLayoutRef, + _key: MaterialExtensionKey, + ) -> Result<(), SpecializedMeshPipelineError> { + descriptor.depth_stencil.as_mut().unwrap().depth_compare = CompareFunction::Always; + + descriptor.vertex.shader_defs.push("FORWARD_DECAL".into()); + + if let Some(fragment) = &mut descriptor.fragment { + fragment.shader_defs.push("FORWARD_DECAL".into()); + } + + if let Some(label) = &mut descriptor.label { + *label = format!("forward_decal_{}", label).into(); + } + + Ok(()) + } +} + +impl Default for ForwardDecalMaterialExt { + fn default() -> Self { + Self { + depth_fade_factor: 8.0, + } + } +} diff --git a/crates/bevy_pbr/src/decal/forward_decal.wgsl b/crates/bevy_pbr/src/decal/forward_decal.wgsl new file mode 100644 index 0000000000000..dbc6bbc1c46a3 --- /dev/null +++ b/crates/bevy_pbr/src/decal/forward_decal.wgsl @@ -0,0 +1,52 @@ +#define_import_path bevy_pbr::decal::forward + +#import bevy_pbr::{ + forward_io::VertexOutput, + mesh_functions::get_world_from_local, + mesh_view_bindings::view, + pbr_functions::calculate_tbn_mikktspace, + prepass_utils::prepass_depth, + view_transformations::depth_ndc_to_view_z, +} +#import bevy_render::maths::project_onto + +@group(2) @binding(200) +var depth_fade_factor: f32; + +struct ForwardDecalInformation { + world_position: vec4, + uv: vec2, + alpha: f32, +} + +fn get_forward_decal_info(in: VertexOutput) -> ForwardDecalInformation { + let world_from_local = get_world_from_local(in.instance_index); + let scale = (world_from_local * vec4(1.0, 1.0, 1.0, 0.0)).xyz; + let scaled_tangent = vec4(in.world_tangent.xyz / scale, in.world_tangent.w); + + let V = normalize(view.world_position - in.world_position.xyz); + + // Transform V from fragment to camera in world space to tangent space. + let TBN = calculate_tbn_mikktspace(in.world_normal, scaled_tangent); + let T = TBN[0]; + let B = TBN[1]; + let N = TBN[2]; + let Vt = vec3(dot(V, T), dot(V, B), dot(V, N)); + + let frag_depth = depth_ndc_to_view_z(in.position.z); + let depth_pass_depth = depth_ndc_to_view_z(prepass_depth(in.position, 0u)); + let diff_depth = frag_depth - depth_pass_depth; + let diff_depth_abs = abs(diff_depth); + + // Apply UV parallax + let contact_on_decal = project_onto(V * diff_depth, in.world_normal); + let normal_depth = length(contact_on_decal); + let view_steepness = abs(Vt.z); + let delta_uv = normal_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; + let uv = in.uv + delta_uv; + + let world_position = vec4(in.world_position.xyz + V * diff_depth_abs, in.world_position.w); + let alpha = saturate(1.0 - normal_depth * depth_fade_factor); + + return ForwardDecalInformation(world_position, uv, alpha); +} diff --git a/crates/bevy_pbr/src/decal/mod.rs b/crates/bevy_pbr/src/decal/mod.rs new file mode 100644 index 0000000000000..e78f23c52a475 --- /dev/null +++ b/crates/bevy_pbr/src/decal/mod.rs @@ -0,0 +1,10 @@ +//! Decal rendering. +//! +//! Decals are a material that render on top of the surface that they're placed above. +//! They can be used to render signs, paint, snow, impact craters, and other effects on top of surfaces. + +// TODO: Once other decal types are added, write a paragraph comparing the different types in the module docs. + +mod forward; + +pub use forward::*; diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 1b2d48e4c69f9..17ea201561f60 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -2,6 +2,7 @@ use bevy_asset::{Asset, Handle}; use bevy_ecs::system::SystemParamItem; use bevy_reflect::{impl_type_path, Reflect}; use bevy_render::{ + alpha::AlphaMode, mesh::MeshVertexBufferLayoutRef, render_resource::{ AsBindGroup, AsBindGroupError, BindGroupLayout, RenderPipelineDescriptor, Shader, @@ -37,11 +38,15 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the base material mesh fragment shader /// will be used. - #[allow(unused_variables)] fn fragment_shader() -> ShaderRef { ShaderRef::Default } + // Returns this material’s AlphaMode. If None is returned, the base material alpha mode will be used. + fn alpha_mode() -> Option { + None + } + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the base material prepass vertex shader /// will be used. fn prepass_vertex_shader() -> ShaderRef { @@ -50,7 +55,6 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material prepass fragment shader /// will be used. - #[allow(unused_variables)] fn prepass_fragment_shader() -> ShaderRef { ShaderRef::Default } @@ -63,14 +67,12 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the base material deferred fragment shader /// will be used. - #[allow(unused_variables)] fn deferred_fragment_shader() -> ShaderRef { ShaderRef::Default } /// Returns this material's [`crate::meshlet::MeshletMesh`] fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh fragment shader will be used. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -78,7 +80,6 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's [`crate::meshlet::MeshletMesh`] prepass fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh prepass fragment shader will be used. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -86,7 +87,6 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Returns this material's [`crate::meshlet::MeshletMesh`] deferred fragment shader. If [`ShaderRef::Default`] is returned, /// the default meshlet mesh deferred fragment shader will be used. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -95,7 +95,10 @@ pub trait MaterialExtension: Asset + AsBindGroup + Clone + Sized { /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. /// Specialization for the base material is applied before this function is called. - #[allow(unused_variables)] + #[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( pipeline: &MaterialExtensionPipeline, @@ -233,8 +236,11 @@ impl Material for ExtendedMaterial { } } - fn alpha_mode(&self) -> crate::AlphaMode { - B::alpha_mode(&self.base) + fn alpha_mode(&self) -> AlphaMode { + match E::alpha_mode() { + Some(specified) => specified, + None => B::alpha_mode(&self.base), + } } fn opaque_render_method(&self) -> crate::OpaqueRendererMethod { diff --git a/crates/bevy_pbr/src/fog.rs b/crates/bevy_pbr/src/fog.rs index 198d218334dc2..831ec6928c424 100644 --- a/crates/bevy_pbr/src/fog.rs +++ b/crates/bevy_pbr/src/fog.rs @@ -70,9 +70,6 @@ pub struct DistanceFog { pub falloff: FogFalloff, } -#[deprecated(since = "0.15.0", note = "Renamed to `DistanceFog`")] -pub type FogSettings = DistanceFog; - /// Allows switching between different fog falloff modes, and configuring their parameters. /// /// ## Convenience Methods diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index f219518dfcce2..d5ea8726a3e3c 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -1,6 +1,6 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![deny(unsafe_code)] +#![forbid(unsafe_code)] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" @@ -24,8 +24,9 @@ pub mod experimental { } } -mod bundle; mod cluster; +mod components; +pub mod decal; pub mod deferred; mod extended_material; mod fog; @@ -33,7 +34,7 @@ mod light; mod light_probe; mod lightmap; mod material; -mod material_bind_groups; +pub mod material_bind_groups; mod mesh_material; mod parallax; mod pbr_material; @@ -46,10 +47,9 @@ mod volumetric_fog; use crate::material_bind_groups::FallbackBindlessResources; use bevy_color::{Color, LinearRgba}; -use core::marker::PhantomData; -pub use bundle::*; pub use cluster::*; +pub use components::*; pub use extended_material::*; pub use fog::*; pub use light::*; @@ -63,29 +63,17 @@ pub use prepass::*; pub use render::*; pub use ssao::*; pub use ssr::*; -#[allow(deprecated)] -pub use volumetric_fog::{ - FogVolume, FogVolumeBundle, VolumetricFog, VolumetricFogPlugin, VolumetricFogSettings, - VolumetricLight, -}; +pub use volumetric_fog::{FogVolume, VolumetricFog, VolumetricFogPlugin, VolumetricLight}; /// The PBR prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ - bundle::{ - DirectionalLightBundle, MaterialMeshBundle, PbrBundle, PointLightBundle, - SpotLightBundle, - }, fog::{DistanceFog, FogFalloff}, light::{light_consts, AmbientLight, DirectionalLight, PointLight, SpotLight}, - light_probe::{ - environment_map::{EnvironmentMapLight, ReflectionProbeBundle}, - LightProbe, - }, + light_probe::{environment_map::EnvironmentMapLight, LightProbe}, material::{Material, MaterialPlugin}, mesh_material::MeshMaterial3d, parallax::ParallaxMappingMethod, @@ -110,6 +98,8 @@ pub mod graph { GpuPreprocess, /// Label for the screen space reflections pass. ScreenSpaceReflections, + /// Label for the indirect parameters building pass. + BuildIndirectParameters, } } @@ -121,10 +111,7 @@ use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_render::{ alpha::AlphaMode, - camera::{ - CameraProjection, CameraUpdateSystem, OrthographicProjection, PerspectiveProjection, - Projection, - }, + camera::{CameraUpdateSystem, Projection}, extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, render_asset::prepare_assets, @@ -163,8 +150,8 @@ pub const RGB9E5_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(26590 const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = Handle::weak_from_u128(2325134235233421); -const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 23; -const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 24; +pub const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 23; +pub const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 24; /// Sets up the entire PBR infrastructure of bevy. pub struct PbrPlugin { @@ -341,9 +328,7 @@ impl Plugin for PbrPlugin { ExtractComponentPlugin::::default(), LightmapPlugin, LightProbePlugin, - PbrProjectionPlugin::::default(), - PbrProjectionPlugin::::default(), - PbrProjectionPlugin::::default(), + PbrProjectionPlugin, GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, @@ -351,9 +336,11 @@ impl Plugin for PbrPlugin { ScreenSpaceReflectionsPlugin, )) .add_plugins(( + decal::ForwardDecalPlugin, SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), + ExtractComponentPlugin::::default(), )) .configure_sets( PostUpdate, @@ -480,20 +467,16 @@ impl Plugin for PbrPlugin { } } -/// [`CameraProjection`] specific PBR functionality. -pub struct PbrProjectionPlugin(PhantomData); -impl Plugin for PbrProjectionPlugin { +/// 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:: + build_directional_light_cascades .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) .after(clear_directional_light_cascades), ); } } -impl Default for PbrProjectionPlugin { - fn default() -> Self { - Self(Default::default()) - } -} diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs index 068e445f3b496..f09bab51f69ed 100644 --- a/crates/bevy_pbr/src/light/ambient_light.rs +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -4,6 +4,8 @@ use super::*; /// /// This resource is inserted by the [`PbrPlugin`] 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. +/// /// # Examples /// /// Make ambient light slightly brighter: @@ -15,8 +17,9 @@ use super::*; /// ambient_light.brightness = 100.0; /// } /// ``` -#[derive(Resource, Clone, Debug, ExtractResource, Reflect)] -#[reflect(Resource, Debug, Default)] +#[derive(Resource, Component, Clone, Debug, ExtractResource, ExtractComponent, Reflect)] +#[reflect(Resource, Component, Debug, Default)] +#[require(Camera)] pub struct AmbientLight { pub color: Color, diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 87543e1377b72..5525029ef4250 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::{Camera, CameraProjection}, + camera::{Camera, CameraProjection, Projection}, extract_component::ExtractComponent, extract_resource::ExtractResource, mesh::Mesh3d, @@ -78,7 +78,7 @@ pub mod light_consts { pub const OFFICE: f32 = 320.; /// The amount of light (lux) during sunrise or sunset on a clear day. pub const CLEAR_SUNRISE: f32 = 400.; - /// The amount of light (lux) on a overcast day; typical TV studio lighting + /// The amount of light (lux) on an overcast day; typical TV studio lighting pub const OVERCAST_DAY: f32 = 1000.; /// The amount of light (lux) from ambient daylight (not direct sunlight). pub const AMBIENT_DAYLIGHT: f32 = 10_000.; @@ -305,9 +305,9 @@ pub fn clear_directional_light_cascades(mut lights: Query<(&DirectionalLight, &m } } -pub fn build_directional_light_cascades( +pub fn build_directional_light_cascades( directional_light_shadow_map: Res, - views: Query<(Entity, &GlobalTransform, &P, &Camera)>, + views: Query<(Entity, &GlobalTransform, &Projection, &Camera)>, mut lights: Query<( &GlobalTransform, &DirectionalLight, @@ -522,24 +522,6 @@ pub enum SimulationLightSystems { CheckLightVisibility, } -// Sort lights by -// - those with volumetric (and shadows) enabled first, so that the volumetric -// lighting pass can quickly find the volumetric lights; -// - then those with shadows enabled second, so that the index can be used to -// render at most `directional_light_shadow_maps_count` directional light -// shadows; -// - then by entity as a stable key to ensure that a consistent set of lights -// are chosen if the light count limit is exceeded. -pub(crate) fn directional_light_order( - (entity_1, volumetric_1, shadows_enabled_1): (&Entity, &bool, &bool), - (entity_2, volumetric_2, shadows_enabled_2): (&Entity, &bool, &bool), -) -> core::cmp::Ordering { - volumetric_2 - .cmp(volumetric_1) // volumetric before shadows - .then_with(|| shadows_enabled_2.cmp(shadows_enabled_1)) // shadow casters before non-casters - .then_with(|| entity_1.cmp(entity_2)) // stable -} - pub fn update_directional_light_frusta( mut views: Query< ( @@ -842,7 +824,6 @@ pub fn check_dir_light_mesh_visibility( }); } -#[allow(clippy::too_many_arguments)] pub fn check_point_light_mesh_visibility( visible_point_lights: Query<&VisibleClusterableObjects>, mut point_lights: Query<( diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index 4bbb7c76afb25..06985ef1b727e 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -15,7 +15,7 @@ //! environment maps are added to every point of the scene, including //! interior enclosed areas. //! -//! 2. If attached to a [`LightProbe`], environment maps represent the immediate +//! 2. If attached to a [`crate::LightProbe`], environment maps represent the immediate //! surroundings of a specific location in the scene. These types of //! environment maps are known as *reflection probes*. //! @@ -44,33 +44,29 @@ //! //! [several pre-filtered environment maps]: https://github.com/KhronosGroup/glTF-Sample-Environments -#![expect(deprecated)] - use bevy_asset::{AssetId, Handle}; use bevy_ecs::{ - bundle::Bundle, component::Component, query::QueryItem, reflect::ReflectComponent, - system::lifetimeless::Read, + component::Component, query::QueryItem, reflect::ReflectComponent, system::lifetimeless::Read, }; use bevy_image::Image; use bevy_math::Quat; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ extract_instances::ExtractInstance, - prelude::SpatialBundle, render_asset::RenderAssets, render_resource::{ binding_types::{self, uniform_buffer}, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, ShaderStages, TextureSampleType, TextureView, }, - renderer::RenderDevice, + renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, }; use core::{num::NonZero, ops::Deref}; use crate::{ - add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, LightProbe, + add_cubemap_texture_view, binding_arrays_are_usable, EnvironmentMapUniform, MAX_VIEW_LIGHT_PROBES, }; @@ -142,26 +138,6 @@ pub struct EnvironmentMapIds { pub(crate) specular: AssetId, } -/// A bundle that contains everything needed to make an entity a reflection -/// probe. -/// -/// A reflection probe is a type of environment map that specifies the light -/// surrounding a region in space. For more information, see -/// [`crate::environment_map`]. -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `LightProbe` and `EnvironmentMapLight` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub struct ReflectionProbeBundle { - /// Contains a transform that specifies the position of this reflection probe in space. - pub spatial: SpatialBundle, - /// Marks this environment map as a light probe. - pub light_probe: LightProbe, - /// The cubemaps that make up this environment map. - pub environment_map: EnvironmentMapLight, -} - /// All the bind group entries necessary for PBR shaders to access the /// environment maps exposed to a view. pub(crate) enum RenderViewEnvironmentMapBindGroupEntries<'a> { @@ -232,10 +208,11 @@ impl ExtractInstance for EnvironmentMapIds { /// specular binding arrays respectively, in addition to the sampler. pub(crate) fn get_bind_group_layout_entries( render_device: &RenderDevice, + render_adapter: &RenderAdapter, ) -> [BindGroupLayoutEntryBuilder; 4] { let mut texture_cube_binding = binding_types::texture_cube(TextureSampleType::Float { filterable: true }); - if binding_arrays_are_usable(render_device) { + if binding_arrays_are_usable(render_device, render_adapter) { texture_cube_binding = texture_cube_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } @@ -256,8 +233,9 @@ impl<'a> RenderViewEnvironmentMapBindGroupEntries<'a> { images: &'a RenderAssets, fallback_image: &'a FallbackImage, render_device: &RenderDevice, + render_adapter: &RenderAdapter, ) -> RenderViewEnvironmentMapBindGroupEntries<'a> { - if binding_arrays_are_usable(render_device) { + if binding_arrays_are_usable(render_device, render_adapter) { let mut diffuse_texture_views = vec![]; let mut specular_texture_views = vec![]; let mut sampler = None; diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 141e70e191b84..b1e974711d882 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -140,7 +140,7 @@ use bevy_render::{ binding_types, BindGroupLayoutEntryBuilder, Sampler, SamplerBindingType, Shader, TextureSampleType, TextureView, }, - renderer::RenderDevice, + renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, GpuImage}, }; use bevy_utils::default; @@ -242,8 +242,9 @@ impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> { images: &'a RenderAssets, fallback_image: &'a FallbackImage, render_device: &RenderDevice, + render_adapter: &RenderAdapter, ) -> RenderViewIrradianceVolumeBindGroupEntries<'a> { - if binding_arrays_are_usable(render_device) { + if binding_arrays_are_usable(render_device, render_adapter) { RenderViewIrradianceVolumeBindGroupEntries::get_multiple( render_view_irradiance_volumes, images, @@ -328,10 +329,11 @@ impl<'a> RenderViewIrradianceVolumeBindGroupEntries<'a> { /// respectively. pub(crate) fn get_bind_group_layout_entries( render_device: &RenderDevice, + render_adapter: &RenderAdapter, ) -> [BindGroupLayoutEntryBuilder; 2] { let mut texture_3d_binding = binding_types::texture_3d(TextureSampleType::Float { filterable: true }); - if binding_arrays_are_usable(render_device) { + if binding_arrays_are_usable(render_device, render_adapter) { texture_3d_binding = texture_3d_binding.count(NonZero::::new(MAX_VIEW_LIGHT_PROBES as _).unwrap()); } diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index b259a3e7927e2..75bf47b5ce61a 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -20,7 +20,7 @@ use bevy_render::{ primitives::{Aabb, Frustum}, render_asset::RenderAssets, render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView}, - renderer::{RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, settings::WgpuFeatures, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, @@ -28,7 +28,8 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; -use bevy_utils::{tracing::error, HashMap}; +use bevy_utils::HashMap; +use tracing::error; use core::{hash::Hash, ops::Deref}; @@ -184,7 +185,6 @@ pub struct ViewLightProbesUniformOffset(u32); /// This information is parameterized by the [`LightProbeComponent`] type. This /// will either be [`EnvironmentMapLight`] for reflection probes or /// [`IrradianceVolume`] for irradiance volumes. -#[allow(dead_code)] struct LightProbeInfo where C: LightProbeComponent, @@ -778,15 +778,20 @@ pub(crate) fn add_cubemap_texture_view<'a>( /// enough texture bindings available in the fragment shader. /// /// 3. If binding arrays aren't supported on the hardware, then we obviously -/// can't use them. +/// can't use them. Adreno <= 610 claims to support bindless, but seems to be +/// too buggy to be usable. /// /// 4. If binding arrays are supported on the hardware, but they can only be /// accessed by uniform indices, that's not good enough, and we bail out. /// /// If binding arrays aren't usable, we disable reflection probes and limit the /// number of irradiance volumes in the scene to 1. -pub(crate) fn binding_arrays_are_usable(render_device: &RenderDevice) -> bool { +pub(crate) fn binding_arrays_are_usable( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, +) -> bool { !cfg!(feature = "shader_format_glsl") + && bevy_render::get_adreno_model(render_adapter).is_none_or(|model| model > 610) && render_device.limits().max_storage_textures_per_shader_stage >= (STANDARD_MATERIAL_FRAGMENT_SHADER_MIN_TEXTURE_BINDINGS + MAX_VIEW_LIGHT_PROBES) as u32 diff --git a/crates/bevy_pbr/src/lightmap/lightmap.wgsl b/crates/bevy_pbr/src/lightmap/lightmap.wgsl index da2eaeb2f9dee..da10ece9b1c01 100644 --- a/crates/bevy_pbr/src/lightmap/lightmap.wgsl +++ b/crates/bevy_pbr/src/lightmap/lightmap.wgsl @@ -3,8 +3,8 @@ #import bevy_pbr::mesh_bindings::mesh #ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY -@group(1) @binding(4) var lightmaps_textures: binding_array>; -@group(1) @binding(5) var lightmaps_samplers: binding_array; +@group(1) @binding(4) var lightmaps_textures: binding_array, 4>; +@group(1) @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; @@ -13,33 +13,87 @@ // Samples the lightmap, if any, and returns indirect illumination from it. fn lightmap(uv: vec2, exposure: f32, instance_index: u32) -> vec3 { let packed_uv_rect = mesh[instance_index].lightmap_uv_rect; - let uv_rect = vec4(vec4( - packed_uv_rect.x & 0xffffu, - packed_uv_rect.x >> 16u, - packed_uv_rect.y & 0xffffu, - packed_uv_rect.y >> 16u)) / 65535.0; - + let uv_rect = vec4( + unpack2x16unorm(packed_uv_rect.x), + unpack2x16unorm(packed_uv_rect.y), + ); let lightmap_uv = mix(uv_rect.xy, uv_rect.zw, uv); + let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; + + // Bicubic 4-tap + // https://developer.nvidia.com/gpugems/gpugems2/part-iii-high-quality-rendering/chapter-20-fast-third-order-texture-filtering + // https://advances.realtimerendering.com/s2021/jpatry_advances2021/index.html#/111/0/2 +#ifdef LIGHTMAP_BICUBIC_SAMPLING + let texture_size = vec2(lightmap_size(lightmap_slot)); + let texel_size = 1.0 / texture_size; + let puv = lightmap_uv * texture_size + 0.5; + let iuv = floor(puv); + let fuv = fract(puv); + let g0x = g0(fuv.x); + let g1x = g1(fuv.x); + let h0x = h0_approx(fuv.x); + let h1x = h1_approx(fuv.x); + let h0y = h0_approx(fuv.y); + let h1y = h1_approx(fuv.y); + let p0 = (vec2(iuv.x + h0x, iuv.y + h0y) - 0.5) * texel_size; + let p1 = (vec2(iuv.x + h1x, iuv.y + h0y) - 0.5) * texel_size; + let p2 = (vec2(iuv.x + h0x, iuv.y + h1y) - 0.5) * texel_size; + let p3 = (vec2(iuv.x + h1x, iuv.y + h1y) - 0.5) * texel_size; + let color = g0(fuv.y) * (g0x * sample(p0, lightmap_slot) + g1x * sample(p1, lightmap_slot)) + g1(fuv.y) * (g0x * sample(p2, lightmap_slot) + g1x * sample(p3, lightmap_slot)); +#else + let color = sample(lightmap_uv, lightmap_slot); +#endif + + return color * exposure; +} + +fn lightmap_size(lightmap_slot: u32) -> vec2 { +#ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureDimensions(lightmaps_textures[lightmap_slot]); +#else + return textureDimensions(lightmaps_texture); +#endif +} +fn sample(uv: vec2, lightmap_slot: u32) -> vec3 { // Mipmapping lightmaps is usually a bad idea due to leaking across UV // islands, so there's no harm in using mip level 0 and it lets us avoid // control flow uniformity problems. - // - // TODO(pcwalton): Consider bicubic filtering. #ifdef MULTIPLE_LIGHTMAPS_IN_ARRAY - let lightmap_slot = mesh[instance_index].material_and_lightmap_bind_group_slot >> 16u; - return textureSampleLevel( - lightmaps_textures[lightmap_slot], - lightmaps_samplers[lightmap_slot], - lightmap_uv, - 0.0 - ).rgb * exposure; -#else // MULTIPLE_LIGHTMAPS_IN_ARRAY - return textureSampleLevel( - lightmaps_texture, - lightmaps_sampler, - lightmap_uv, - 0.0 - ).rgb * exposure; -#endif // MULTIPLE_LIGHTMAPS_IN_ARRAY + return textureSampleLevel(lightmaps_textures[lightmap_slot], lightmaps_samplers[lightmap_slot], uv, 0.0).rgb; +#else + return textureSampleLevel(lightmaps_texture, lightmaps_sampler, uv, 0.0).rgb; +#endif +} + +fn w0(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-a + 3.0) - 3.0) + 1.0); +} + +fn w1(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * (3.0 * a - 6.0) + 4.0); +} + +fn w2(a: f32) -> f32 { + return (1.0 / 6.0) * (a * (a * (-3.0 * a + 3.0) + 3.0) + 1.0); +} + +fn w3(a: f32) -> f32 { + return (1.0 / 6.0) * (a * a * a); +} + +fn g0(a: f32) -> f32 { + return w0(a) + w1(a); +} + +fn g1(a: f32) -> f32 { + return w2(a) + w3(a); +} + +fn h0_approx(a: f32) -> f32 { + return -0.2 - a * (0.24 * a - 0.44); +} + +fn h1_approx(a: f32) -> f32 { + return 1.0 + a * (0.24 * a - 0.04); } diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 9206b407779a0..3a7b8aa3ad80e 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -50,15 +50,17 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ render_asset::RenderAssets, render_resource::{Sampler, Shader, TextureView, WgpuSampler, WgpuTextureView}, + renderer::RenderAdapter, sync_world::MainEntity, texture::{FallbackImage, GpuImage}, view::ViewVisibility, Extract, ExtractSchedule, RenderApp, }; use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap}; -use bevy_utils::{default, tracing::error, HashSet}; +use bevy_utils::{default, HashSet}; use fixedbitset::FixedBitSet; use nonmax::{NonMaxU16, NonMaxU32}; +use tracing::error; use crate::{binding_arrays_are_usable, ExtractMeshesSet}; @@ -71,7 +73,7 @@ pub const LIGHTMAP_SHADER_HANDLE: Handle = /// /// If bindless textures aren't in use, then only a single lightmap can be bound /// at a time. -pub const LIGHTMAPS_PER_SLAB: usize = 16; +pub const LIGHTMAPS_PER_SLAB: usize = 4; /// A plugin that provides an implementation of lightmaps. pub struct LightmapPlugin; @@ -98,6 +100,13 @@ pub struct Lightmap { /// This field allows lightmaps for a variety of meshes to be packed into a /// single atlas. pub uv_rect: Rect, + + /// Whether bicubic sampling should be used for sampling this lightmap. + /// + /// Bicubic sampling is higher quality, but slower, and may lead to light leaks. + /// + /// If true, the lightmap texture's sampler must be set to [`bevy_image::ImageSampler::linear`]. + pub bicubic_sampling: bool, } /// Lightmap data stored in the render world. @@ -124,6 +133,9 @@ pub(crate) struct RenderLightmap { /// /// If bindless lightmaps aren't in use, this will be 0. pub(crate) slot_index: LightmapSlotIndex, + + // Whether or not bicubic sampling should be used for this lightmap. + pub(crate) bicubic_sampling: bool, } /// Stores data for all lightmaps in the render world. @@ -235,6 +247,7 @@ fn extract_lightmaps( lightmap.uv_rect, slab_index, slot_index, + lightmap.bicubic_sampling, ), ); @@ -294,12 +307,14 @@ impl RenderLightmap { uv_rect: Rect, slab_index: LightmapSlabIndex, slot_index: LightmapSlotIndex, + bicubic_sampling: bool, ) -> Self { Self { image, uv_rect, slab_index, slot_index, + bicubic_sampling, } } } @@ -325,6 +340,7 @@ impl Default for Lightmap { Self { image: Default::default(), uv_rect: Rect::new(0.0, 0.0, 1.0, 1.0), + bicubic_sampling: false, } } } @@ -332,7 +348,9 @@ impl Default for Lightmap { impl FromWorld for RenderLightmaps { fn from_world(world: &mut World) -> Self { let render_device = world.resource::(); - let bindless_supported = binding_arrays_are_usable(render_device); + let render_adapter = world.resource::(); + + let bindless_supported = binding_arrays_are_usable(render_device, render_adapter); RenderLightmaps { render_lightmaps: default(), diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index f894774af140d..0ffe305bd7cb1 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -30,6 +30,7 @@ use bevy_ecs::{ use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::{ + batching::gpu_preprocessing::GpuPreprocessingSupport, camera::TemporalJitter, extract_resource::ExtractResource, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, @@ -42,8 +43,9 @@ use bevy_render::{ }; use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap}; use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; -use bevy_utils::{hashbrown::hash_map::Entry, tracing::error}; +use bevy_utils::hashbrown::hash_map::Entry; use core::{hash::Hash, marker::PhantomData}; +use tracing::error; /// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level @@ -122,7 +124,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// Returns this material's fragment shader. If [`ShaderRef::Default`] is returned, the default mesh fragment shader /// will be used. - #[allow(unused_variables)] fn fragment_shader() -> ShaderRef { ShaderRef::Default } @@ -172,7 +173,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// /// This is used for the various [prepasses](bevy_core_pipeline::prepass) as well as for generating the depth maps /// required for shadow mapping. - #[allow(unused_variables)] fn prepass_fragment_shader() -> ShaderRef { ShaderRef::Default } @@ -185,7 +185,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// Returns this material's deferred fragment shader. If [`ShaderRef::Default`] is returned, the default deferred fragment shader /// will be used. - #[allow(unused_variables)] fn deferred_fragment_shader() -> ShaderRef { ShaderRef::Default } @@ -196,7 +195,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -208,7 +206,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_prepass_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -220,7 +217,6 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// This is part of an experimental feature, and is unnecessary to implement unless you are using `MeshletMesh`'s. /// /// See [`crate::meshlet::MeshletMesh`] for limitations. - #[allow(unused_variables)] #[cfg(feature = "meshlet")] fn meshlet_mesh_deferred_fragment_shader() -> ShaderRef { ShaderRef::Default @@ -228,7 +224,10 @@ pub trait Material: Asset + AsBindGroup + Clone + Sized { /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayoutRef`] as input. - #[allow(unused_variables)] + #[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( pipeline: &MaterialPipeline, @@ -582,7 +581,7 @@ pub const fn screen_space_specular_transmission_pipeline_key( } } -fn extract_mesh_materials( +pub fn extract_mesh_materials( mut material_instances: ResMut>, mut material_ids: ResMut, mut material_bind_group_allocator: ResMut>, @@ -608,7 +607,6 @@ fn extract_mesh_materials( /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. -#[allow(clippy::too_many_arguments)] pub fn queue_material_meshes( ( opaque_draw_functions, @@ -630,16 +628,16 @@ pub fn queue_material_meshes( render_material_instances: Res>, render_lightmaps: Res, render_visibility_ranges: Res, - (mesh_allocator, material_bind_group_allocator): ( + (mesh_allocator, material_bind_group_allocator, gpu_preprocessing_support): ( Res, Res>, + Res, ), mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, mut transmissive_render_phases: ResMut>, mut transparent_render_phases: ResMut>, views: Query<( - Entity, &ExtractedView, &RenderVisibleEntities, &Msaa, @@ -666,7 +664,6 @@ pub fn queue_material_meshes( M::Data: PartialEq + Eq + Hash + Clone, { for ( - view_entity, view, visible_entities, msaa, @@ -688,10 +685,10 @@ pub fn queue_material_meshes( Some(transmissive_phase), Some(transparent_phase), ) = ( - opaque_render_phases.get_mut(&view_entity), - alpha_mask_render_phases.get_mut(&view_entity), - transmissive_render_phases.get_mut(&view_entity), - transparent_render_phases.get_mut(&view_entity), + opaque_render_phases.get_mut(&view.retained_view_entity), + alpha_mask_render_phases.get_mut(&view.retained_view_entity), + transmissive_render_phases.get_mut(&view.retained_view_entity), + transparent_render_phases.get_mut(&view.retained_view_entity), ) else { continue; @@ -741,6 +738,7 @@ pub fn queue_material_meshes( view_key |= match projection { Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, }; } @@ -804,12 +802,14 @@ pub fn queue_material_meshes( | MeshPipelineKey::from_bits_retain(mesh.key_bits.bits()) | mesh_pipeline_key_bits; - let lightmap_slab_index = render_lightmaps - .render_lightmaps - .get(visible_entity) - .map(|lightmap| lightmap.slab_index); - if lightmap_slab_index.is_some() { + let mut lightmap_slab = None; + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + lightmap_slab = Some(*lightmap.slab_index); mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } } if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { @@ -851,6 +851,9 @@ pub fn queue_material_meshes( } }; + // Fetch the slabs that this mesh resides in. + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + match mesh_key .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) { @@ -865,26 +868,28 @@ pub fn queue_material_meshes( distance, batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), }); } else if material.properties.render_method == OpaqueRendererMethod::Forward { - let (vertex_slab, index_slab) = - mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = Opaque3dBatchSetKey { + pipeline: pipeline_id, + draw_function: draw_opaque_pbr, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + lightmap_slab, + }; let bin_key = Opaque3dBinKey { - batch_set_key: Opaque3dBatchSetKey { - draw_function: draw_opaque_pbr, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - vertex_slab: vertex_slab.unwrap_or_default(), - index_slab, - lightmap_slab: lightmap_slab_index - .map(|lightmap_slab_index| *lightmap_slab_index), - }, asset_id: mesh_instance.mesh_asset_id.into(), }; opaque_phase.add( + batch_set_key, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } } @@ -900,20 +905,27 @@ pub fn queue_material_meshes( distance, batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), }); } else if material.properties.render_method == OpaqueRendererMethod::Forward { + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: draw_alpha_mask_pbr, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; let bin_key = OpaqueNoLightmap3dBinKey { - batch_set_key: OpaqueNoLightmap3dBatchSetKey { - draw_function: draw_alpha_mask_pbr, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - }, asset_id: mesh_instance.mesh_asset_id.into(), }; alpha_mask_phase.add( + batch_set_key, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } } @@ -927,6 +939,7 @@ pub fn queue_material_meshes( distance, batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: index_slab.is_some(), }); } } diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index 718b4fef535e0..a7c06dfcf21ad 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -19,11 +19,11 @@ use bevy_render::{ UnpreparedBindGroup, WgpuSampler, WgpuTextureView, }, renderer::RenderDevice, - settings::WgpuFeatures, texture::FallbackImage, }; -use bevy_utils::{default, tracing::error, HashMap}; +use bevy_utils::{default, HashMap}; use core::{any, iter, marker::PhantomData, num::NonZero}; +use tracing::error; /// An object that creates and stores bind groups for a single material type. /// @@ -202,7 +202,7 @@ where M: Material, { /// Creates or recreates any bind groups that were modified this frame. - pub(crate) fn prepare_bind_groups( + pub fn prepare_bind_groups( &mut self, render_device: &RenderDevice, fallback_image: &FallbackImage, @@ -221,12 +221,12 @@ where /// Returns the bind group with the given index, if it exists. #[inline] - pub(crate) fn get(&self, index: MaterialBindGroupIndex) -> Option<&MaterialBindGroup> { + pub fn get(&self, index: MaterialBindGroupIndex) -> Option<&MaterialBindGroup> { self.bind_groups.get(index.0 as usize) } /// Allocates a new binding slot and returns its ID. - pub(crate) fn allocate(&mut self) -> MaterialBindingId { + pub fn allocate(&mut self) -> MaterialBindingId { let group_index = self.free_bind_groups.pop().unwrap_or_else(|| { let group_index = self.bind_groups.len() as u32; self.bind_groups @@ -249,7 +249,7 @@ where /// Assigns an unprepared bind group to the group and slot specified in the /// [`MaterialBindingId`]. - pub(crate) fn init( + pub fn init( &mut self, render_device: &RenderDevice, material_binding_id: MaterialBindingId, @@ -268,7 +268,7 @@ where /// This is only a meaningful operation for non-bindless bind groups. It's /// rarely used, but see the `texture_binding_array` example for an example /// demonstrating how this feature might see use in practice. - pub(crate) fn init_custom( + pub fn init_custom( &mut self, material_binding_id: MaterialBindingId, bind_group: BindGroup, @@ -279,7 +279,7 @@ where } /// Marks the slot corresponding to the given [`MaterialBindingId`] as free. - pub(crate) fn free(&mut self, material_binding_id: MaterialBindingId) { + pub fn free(&mut self, material_binding_id: MaterialBindingId) { let bind_group = &mut self.bind_groups[material_binding_id.group.0 as usize]; let was_full = bind_group.is_full(); @@ -795,10 +795,7 @@ pub fn material_uses_bindless_resources(render_device: &RenderDevice) -> bool where M: Material, { - M::bindless_slot_count().is_some() - && render_device - .features() - .contains(WgpuFeatures::BUFFER_BINDING_ARRAY | WgpuFeatures::TEXTURE_BINDING_ARRAY) + M::bindless_slot_count().is_some() && M::bindless_supported(render_device) } impl FromWorld for FallbackBindlessResources { diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index 66a84ed8329de..c158650d1bd4c 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -31,7 +31,6 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 1; /// * Do not use normal maps baked from higher-poly geometry. Use the high-poly geometry directly and skip the normal map. /// * If additional detail is needed, a smaller tiling normal map not baked from a mesh is ok. /// * Material shaders must not use builtin functions that automatically calculate derivatives . -/// * Use `pbr_functions::sample_texture` to sample textures instead. /// * Performing manual arithmetic on texture coordinates (UVs) is forbidden. Use the chain-rule version of arithmetic functions instead (TODO: not yet implemented). /// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes. /// * Materials must use the [`crate::Material::meshlet_mesh_fragment_shader`] method (and similar variants for prepass/deferred shaders) diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 14b46e55ba46e..8d4f1c7533ba2 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -9,14 +9,14 @@ use bevy_render::{ }; use bevy_utils::HashMap; use bitvec::{order::Lsb0, vec::BitVec, view::BitView}; -use core::iter; +use core::{iter, ops::Range}; use half::f16; use itertools::Itertools; use meshopt::{ build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi, simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream, }; -use metis::Graph; +use metis::{option::Opt, Graph}; use smallvec::SmallVec; use thiserror::Error; @@ -67,12 +67,29 @@ impl MeshletMesh { // Validate mesh format let indices = validate_input_mesh(mesh)?; - // Split the mesh into an initial list of meshlets (LOD 0) + // Get meshlet vertices let vertex_buffer = mesh.create_packed_vertex_buffer_data(); let vertex_stride = mesh.get_vertex_size() as usize; let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap(); let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]); - let mut meshlets = compute_meshlets(&indices, &vertices); + + // Generate a position-only vertex buffer for determining triangle/meshlet connectivity + let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi( + vertices.vertex_count, + &[VertexStream::new_with_stride::( + vertex_buffer.as_ptr(), + vertex_stride, + )], + Some(&indices), + ); + + // Split the mesh into an initial list of meshlets (LOD 0) + let mut meshlets = compute_meshlets( + &indices, + &vertices, + &position_only_vertex_remap, + position_only_vertex_count, + ); let mut bounding_spheres = meshlets .iter() .map(|meshlet| compute_meshlet_bounds(meshlet, &vertices)) @@ -92,25 +109,14 @@ impl MeshletMesh { .take(meshlets.len()) .collect::>(); - // Generate a position-only vertex buffer for determining what meshlets are connected for use in grouping - let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi( - vertices.vertex_count, - &[VertexStream::new_with_stride::( - vertex_buffer.as_ptr(), - vertex_stride, - )], - Some(&indices), - ); - let mut vertex_locks = vec![false; vertices.vertex_count]; // Build further LODs - let mut simplification_queue = Vec::from_iter(0..meshlets.len()); - let mut retry_queue = Vec::new(); + let mut simplification_queue = 0..meshlets.len(); while simplification_queue.len() > 1 { // For each meshlet build a list of connected meshlets (meshlets that share a vertex) let connected_meshlets_per_meshlet = find_connected_meshlets( - &simplification_queue, + simplification_queue.clone(), &meshlets, &position_only_vertex_remap, position_only_vertex_count, @@ -118,7 +124,10 @@ 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(&connected_meshlets_per_meshlet, &simplification_queue); + let groups = group_meshlets( + &connected_meshlets_per_meshlet, + simplification_queue.clone(), + ); // Lock borders between groups to prevent cracks when simplifying lock_group_borders( @@ -131,9 +140,8 @@ impl MeshletMesh { let next_lod_start = meshlets.len(); for group_meshlets in groups.into_iter() { - // If the group only has a single meshlet, we can't simplify it well, so retry later + // If the group only has a single meshlet we can't simplify it if group_meshlets.len() == 1 { - retry_queue.push(group_meshlets[0]); continue; } @@ -146,8 +154,7 @@ impl MeshletMesh { vertex_stride, &vertex_locks, ) else { - // Couldn't simplify the group enough, retry its meshlets later - retry_queue.extend_from_slice(&group_meshlets); + // Couldn't simplify the group enough continue; }; @@ -163,6 +170,8 @@ impl MeshletMesh { let new_meshlets_count = split_simplified_group_into_new_meshlets( &simplified_group_indices, &vertices, + &position_only_vertex_remap, + position_only_vertex_count, &mut meshlets, ); @@ -187,12 +196,8 @@ impl MeshletMesh { ); } - // Set simplification queue to the list of newly created (and retrying) meshlets - simplification_queue.clear(); - simplification_queue.extend(next_lod_start..meshlets.len()); - if !simplification_queue.is_empty() { - simplification_queue.append(&mut retry_queue); - } + // Set simplification queue to the list of newly created meshlets + simplification_queue = next_lod_start..meshlets.len(); } // Copy vertex attributes per meshlet and compress @@ -247,27 +252,122 @@ fn validate_input_mesh(mesh: &Mesh) -> Result, MeshToMeshletMeshC } } -fn compute_meshlets(indices: &[u32], vertices: &VertexDataAdapter) -> Meshlets { - build_meshlets(indices, vertices, 255, 128, 0.0) // Meshoptimizer won't currently let us do 256 vertices +fn compute_meshlets( + indices: &[u32], + vertices: &VertexDataAdapter, + position_only_vertex_remap: &[u32], + position_only_vertex_count: usize, +) -> Meshlets { + // 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() { + let vertex_id = position_only_vertex_remap[*index as usize]; + let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize]; + vertex_to_triangles.push(i / 3); + } + + // For each triangle pair, count how many vertices they share + let mut triangle_pair_to_shared_vertex_count = >::default(); + for vertex_triangle_ids in vertices_to_triangles { + for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() { + let count = triangle_pair_to_shared_vertex_count + .entry(( + triangle_id1.min(triangle_id2), + triangle_id1.max(triangle_id2), + )) + .or_insert(0); + *count += 1; + } + } + + // For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count + let triangle_count = indices.len() / 3; + let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count]; + for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count + { + // We record both id1->id2 and id2->id1 as adjacency is symmetrical + connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count)); + connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count)); + } + + // The order of triangles depends on hash traversal order; to produce deterministic results, sort them + for list in connected_triangles_per_triangle.iter_mut() { + list.sort_unstable(); + } + + let mut xadj = Vec::with_capacity(triangle_count + 1); + let mut adjncy = Vec::new(); + let mut adjwgt = Vec::new(); + for connected_triangles in connected_triangles_per_triangle { + xadj.push(adjncy.len() as i32); + for (connected_triangle_id, shared_vertex_count) in connected_triangles { + adjncy.push(connected_triangle_id as i32); + adjwgt.push(shared_vertex_count); + // TODO: Additional weight based on triangle center spatial proximity? + } + } + xadj.push(adjncy.len() as i32); + + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions + + let mut meshlet_per_triangle = vec![0; triangle_count]; + let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet + Graph::new(1, partition_count as i32, &xadj, &adjncy) + .unwrap() + .set_options(&options) + .set_adjwgt(&adjwgt) + .part_recursive(&mut meshlet_per_triangle) + .unwrap(); + + let mut indices_per_meshlet = vec![Vec::new(); partition_count]; + for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() { + let meshlet_indices = &mut indices_per_meshlet[meshlet as usize]; + let base_index = triangle_id * 3; + meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]); + } + + // Use meshopt to build meshlets from the sets of triangles + let mut meshlets = Meshlets { + meshlets: Vec::new(), + vertices: Vec::new(), + triangles: Vec::new(), + }; + 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 + })); + } + meshlets } fn find_connected_meshlets( - simplification_queue: &[usize], + simplification_queue: Range, 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_queue_id, meshlet_id) in simplification_queue.iter().enumerate() { - let meshlet = meshlets.get(*meshlet_id); + for meshlet_id in simplification_queue.clone() { + let meshlet = meshlets.get(meshlet_id); 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_queue_id) { - vertex_to_meshlets.push(meshlet_queue_id); + if vertex_to_meshlets.last() != Some(&meshlet_id) { + vertex_to_meshlets.push(meshlet_id); } } } @@ -275,14 +375,9 @@ fn find_connected_meshlets( // For each meshlet pair, count how many vertices they share let mut meshlet_pair_to_shared_vertex_count = >::default(); for vertex_meshlet_ids in vertices_to_meshlets { - for (meshlet_queue_id1, meshlet_queue_id2) in - vertex_meshlet_ids.into_iter().tuple_combinations() - { + for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() { let count = meshlet_pair_to_shared_vertex_count - .entry(( - meshlet_queue_id1.min(meshlet_queue_id2), - meshlet_queue_id1.max(meshlet_queue_id2), - )) + .entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2))) .or_insert(0); *count += 1; } @@ -290,12 +385,12 @@ fn find_connected_meshlets( // For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()]; - for ((meshlet_queue_id1, meshlet_queue_id2), shared_count) in - meshlet_pair_to_shared_vertex_count - { + 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_queue_id1].push((meshlet_queue_id2, shared_count)); - connected_meshlets_per_meshlet[meshlet_queue_id2].push((meshlet_queue_id1, shared_count)); + 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)); } // The order of meshlets depends on hash traversal order; to produce deterministic results, sort them @@ -309,35 +404,39 @@ fn find_connected_meshlets( // METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf fn group_meshlets( connected_meshlets_per_meshlet: &[Vec<(usize, usize)>], - simplification_queue: &[usize], + simplification_queue: Range, ) -> 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_queue_id, shared_vertex_count) in connected_meshlets { - adjncy.push(*connected_meshlet_queue_id as i32); + for (connected_meshlet_id, shared_vertex_count) in connected_meshlets { + adjncy.push((connected_meshlet_id - simplification_queue.start) as i32); adjwgt.push(*shared_vertex_count as i32); // TODO: Additional weight based on meshlet spatial proximity } } xadj.push(adjncy.len() as i32); + let mut options = [-1; metis::NOPTIONS]; + options[metis::option::Seed::INDEX] = 17; + options[metis::option::UFactor::INDEX] = 200; + let mut group_per_meshlet = vec![0; simplification_queue.len()]; let partition_count = simplification_queue .len() .div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic Graph::new(1, partition_count as i32, &xadj, &adjncy) .unwrap() - .set_option(metis::option::Seed(17)) + .set_options(&options) .set_adjwgt(&adjwgt) - .part_kway(&mut group_per_meshlet) + .part_recursive(&mut group_per_meshlet) .unwrap(); let mut groups = vec![SmallVec::new(); partition_count]; - for (meshlet_queue_id, meshlet_group) in group_per_meshlet.into_iter().enumerate() { - groups[meshlet_group as usize].push(simplification_queue[meshlet_queue_id]); + for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() { + groups[meshlet_group as usize].push(i + simplification_queue.start); } groups } @@ -471,9 +570,16 @@ fn compute_lod_group_data( 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); + let simplified_meshlets = compute_meshlets( + simplified_group_indices, + vertices, + position_only_vertex_remap, + position_only_vertex_count, + ); let new_meshlets_count = simplified_meshlets.len(); let vertex_offset = meshlets.vertices.len() as u32; @@ -495,7 +601,6 @@ fn split_simplified_group_into_new_meshlets( new_meshlets_count } -#[allow(clippy::too_many_arguments)] fn build_and_compress_per_meshlet_vertex_data( meshlet: &meshopt_Meshlet, meshlet_vertex_ids: &[u32], @@ -619,7 +724,7 @@ fn pack2x16snorm(v: Vec2) -> u32 { pub enum MeshToMeshletMeshConversionError { #[error("Mesh primitive topology is not TriangleList")] WrongMeshPrimitiveTopology, - #[error("Mesh attributes are not {{POSITION, NORMAL, UV_0}}")] + #[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}")] WrongMeshVertexAttributes, #[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 c190d7ea367b0..7a78abe482104 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -81,7 +81,6 @@ impl InstanceManager { } } - #[allow(clippy::too_many_arguments)] pub fn add_instance( &mut self, instance: MainEntity, diff --git a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs index 3d41688add68e..2a44e5934c4cf 100644 --- a/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs +++ b/crates/bevy_pbr/src/meshlet/material_pipeline_prepare.rs @@ -29,7 +29,6 @@ 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`]. -#[allow(clippy::too_many_arguments)] pub fn prepare_material_meshlet_meshes_main_opaque_pass( resource_manager: ResMut, mut instance_manager: ResMut, @@ -114,6 +113,7 @@ pub fn prepare_material_meshlet_meshes_main_opaque_pass( view_key |= match projection { Projection::Perspective(_) => MeshPipelineKey::VIEW_PROJECTION_PERSPECTIVE, Projection::Orthographic(_) => MeshPipelineKey::VIEW_PROJECTION_ORTHOGRAPHIC, + Projection::Custom(_) => MeshPipelineKey::VIEW_PROJECTION_NONSTANDARD, }; } @@ -246,7 +246,6 @@ pub struct MeshletViewMaterialsDeferredGBufferPrepass( /// Prepare [`Material`] pipelines for [`super::MeshletMesh`] entities for use in [`super::MeshletPrepassNode`], /// and [`super::MeshletDeferredGBufferPrepassNode`] and register the material with [`InstanceManager`]. -#[allow(clippy::too_many_arguments)] pub fn prepare_material_meshlet_meshes_prepass( resource_manager: ResMut, mut instance_manager: ResMut, diff --git a/crates/bevy_pbr/src/meshlet/mod.rs b/crates/bevy_pbr/src/meshlet/mod.rs index 0ad880877d1cb..5db0644f97972 100644 --- a/crates/bevy_pbr/src/meshlet/mod.rs +++ b/crates/bevy_pbr/src/meshlet/mod.rs @@ -1,4 +1,3 @@ -#![expect(deprecated)] //! Render high-poly 3d meshes using an efficient GPU-driven method. See [`MeshletPlugin`] and [`MeshletMesh`] for details. mod asset; @@ -40,7 +39,6 @@ pub use self::asset::{ pub use self::from_mesh::{ MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR, }; - use self::{ graph::NodeMeshlet, instance_manager::extract_meshlet_mesh_entities, @@ -58,7 +56,8 @@ use self::{ }, visibility_buffer_raster_node::MeshletVisibilityBufferRasterPassNode, }; -use crate::{graph::NodePbr, Material, MeshMaterial3d, PreviousGlobalTransform}; +use crate::graph::NodePbr; +use crate::PreviousGlobalTransform; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, AssetApp, AssetId, Handle}; use bevy_core_pipeline::{ @@ -67,7 +66,6 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - bundle::Bundle, component::{require, Component}, entity::Entity, query::Has, @@ -81,15 +79,12 @@ use bevy_render::{ render_resource::Shader, renderer::RenderDevice, settings::WgpuFeatures, - view::{ - self, prepare_view_targets, InheritedVisibility, Msaa, ViewVisibility, Visibility, - VisibilityClass, - }, + view::{self, prepare_view_targets, Msaa, Visibility, VisibilityClass}, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::tracing::error; +use bevy_transform::components::Transform; use derive_more::From; +use tracing::error; const MESHLET_BINDINGS_SHADER_HANDLE: Handle = Handle::weak_from_u128(1325134235233421); const MESHLET_MESH_MATERIAL_SHADER_HANDLE: Handle = @@ -314,39 +309,6 @@ impl From<&MeshletMesh3d> for AssetId { } } -/// A component bundle for entities with a [`MeshletMesh`] and a [`Material`]. -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `MeshletMesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub struct MaterialMeshletMeshBundle { - pub meshlet_mesh: MeshletMesh3d, - pub material: MeshMaterial3d, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// User indication of whether an entity is visible - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, -} - -impl Default for MaterialMeshletMeshBundle { - fn default() -> Self { - Self { - meshlet_mesh: Default::default(), - material: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - visibility: Default::default(), - inherited_visibility: Default::default(), - view_visibility: Default::default(), - } - } -} - fn configure_meshlet_views( mut views_3d: Query<( Entity, diff --git a/crates/bevy_pbr/src/meshlet/resource_manager.rs b/crates/bevy_pbr/src/meshlet/resource_manager.rs index 79473b2c36fe1..c918990869cec 100644 --- a/crates/bevy_pbr/src/meshlet/resource_manager.rs +++ b/crates/bevy_pbr/src/meshlet/resource_manager.rs @@ -574,7 +574,6 @@ pub fn prepare_meshlet_per_frame_resources( } } -#[allow(clippy::too_many_arguments)] pub fn prepare_meshlet_view_bind_groups( meshlet_mesh_manager: Res, resource_manager: Res, 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 aa549ae679924..9890deb4dbfc1 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_raster_node.rs @@ -358,7 +358,6 @@ fn fill_cluster_buffers_pass( ); } -#[allow(clippy::too_many_arguments)] fn cull_pass( label: &'static str, render_context: &mut RenderContext, @@ -405,7 +404,6 @@ fn cull_pass( } } -#[allow(clippy::too_many_arguments)] fn raster_pass( first_pass: bool, render_context: &mut RenderContext, diff --git a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl index 33c8df6a0e2c6..f28645013d1ec 100644 --- a/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl +++ b/crates/bevy_pbr/src/meshlet/visibility_buffer_resolve.wgsl @@ -94,6 +94,7 @@ struct VertexOutput { world_tangent: vec4, mesh_flags: u32, cluster_id: u32, + material_bind_group_slot: u32, #ifdef PREPASS_FRAGMENT #ifdef MOTION_VECTOR_PREPASS motion_vector: vec2, @@ -173,6 +174,7 @@ fn resolve_vertex_output(frag_coord: vec4) -> VertexOutput { world_tangent, instance_uniform.flags, instance_id ^ meshlet_id, + instance_uniform.material_and_lightmap_bind_group_slot & 0xffffu, #ifdef PREPASS_FRAGMENT #ifdef MOTION_VECTOR_PREPASS motion_vector, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index 2e93b68094e17..150f58acf9664 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -23,8 +23,8 @@ pub enum UvChannel { Uv1, } -/// A material with "standard" properties used in PBR lighting -/// Standard property values with pictures here +/// A material with "standard" properties used in PBR lighting. +/// Standard property values with pictures here: /// . /// /// May be created directly from a [`Color`] or an [`Image`]. @@ -38,7 +38,7 @@ pub struct StandardMaterial { /// /// Doubles as diffuse albedo for non-metallic, specular for metallic and a mix for everything /// in between. If used together with a `base_color_texture`, this is factored into the final - /// base color as `base_color * base_color_texture_value` + /// base color as `base_color * base_color_texture_value`. /// /// Defaults to [`Color::WHITE`]. pub base_color: Color, @@ -183,7 +183,7 @@ pub struct StandardMaterial { #[doc(alias = "specular_intensity")] pub reflectance: f32, - /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”) + /// The amount of light transmitted _diffusely_ through the material (i.e. “translucency”). /// /// Implemented as a second, flipped [Lambertian diffuse](https://en.wikipedia.org/wiki/Lambertian_reflectance) lobe, /// which provides an inexpensive but plausible approximation of translucency for thin dielectric objects (e.g. paper, @@ -221,7 +221,7 @@ pub struct StandardMaterial { #[cfg(feature = "pbr_transmission_textures")] pub diffuse_transmission_texture: Option>, - /// The amount of light transmitted _specularly_ through the material (i.e. via refraction) + /// The amount of light transmitted _specularly_ through the material (i.e. via refraction). /// /// - When set to `0.0` (the default) no light is transmitted. /// - When set to `1.0` all light is transmitted through the material. @@ -522,7 +522,7 @@ pub struct StandardMaterial { /// [`StandardMaterial::anisotropy_rotation`] to vary across the mesh. /// /// The [`KHR_materials_anisotropy` specification] defines the format that - /// this texture must take. To summarize: The direction vector is encoded in + /// this texture must take. To summarize: the direction vector is encoded in /// the red and green channels, while the strength is encoded in the blue /// channels. For the direction vector, the red and green channels map the /// color range [0, 1] to the vector range [-1, 1]. The direction vector @@ -1238,7 +1238,9 @@ impl From<&StandardMaterial> for StandardMaterialKey { } key.insert(StandardMaterialKey::from_bits_retain( - (material.depth_bias as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, + // Casting to i32 first to ensure the full i32 range is preserved. + // (wgpu expects the depth_bias as an i32 when this is extracted in a later step) + (material.depth_bias as i32 as u64) << STANDARD_MATERIAL_KEY_DEPTH_BIAS_SHIFT, )); key } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index ac5cbeca4216b..373119f25ffde 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -2,8 +2,10 @@ mod prepass_bindings; use crate::material_bind_groups::MaterialBindGroupAllocator; use bevy_render::{ - mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, + batching::gpu_preprocessing::GpuPreprocessingSupport, + mesh::{allocator::MeshAllocator, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_resource::binding_types::uniform_buffer, + renderer::RenderAdapter, sync_world::RenderEntity, view::{RenderVisibilityRanges, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT}, }; @@ -32,7 +34,7 @@ use bevy_render::{ Extract, }; use bevy_transform::prelude::GlobalTransform; -use bevy_utils::tracing::error; +use tracing::error; #[cfg(feature = "meshlet")] use crate::meshlet::{ @@ -259,12 +261,18 @@ pub struct PrepassPipeline { pub skins_use_uniform_buffers: bool, pub depth_clip_control_supported: bool, + + /// Whether binding arrays (a.k.a. bindless textures) are usable on the + /// current render device. + pub binding_arrays_are_usable: bool, + _marker: PhantomData, } 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 @@ -352,6 +360,7 @@ impl FromWorld for PrepassPipeline { material_pipeline: world.resource::>().clone(), 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), _marker: PhantomData, } } @@ -479,6 +488,12 @@ where if key.mesh_key.contains(MeshPipelineKey::LIGHTMAPPED) { shader_defs.push("LIGHTMAP".into()); } + if key + .mesh_key + .contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) + { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } if layout.0.contains(Mesh::ATTRIBUTE_COLOR) { shader_defs.push("VERTEX_COLORS".into()); @@ -505,6 +520,10 @@ where shader_defs.push("BINDLESS".into()); } + if self.binding_arrays_are_usable { + shader_defs.push("MULTIPLE_LIGHTMAPS_IN_ARRAY".into()); + } + if key .mesh_key .contains(MeshPipelineKey::VISIBILITY_RANGE_DITHER) @@ -742,7 +761,6 @@ pub fn prepare_prepass_view_bind_group( } } -#[allow(clippy::too_many_arguments)] pub fn queue_prepass_material_meshes( ( opaque_draw_functions, @@ -758,29 +776,32 @@ pub fn queue_prepass_material_meshes( prepass_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, - render_meshes: Res>, - render_mesh_instances: Res, + (render_meshes, render_mesh_instances): ( + Res>, + Res, + ), render_materials: Res>>, render_material_instances: Res>, render_lightmaps: Res, render_visibility_ranges: Res, - material_bind_group_allocator: Res>, + (mesh_allocator, material_bind_group_allocator): ( + Res, + Res>, + ), + gpu_preprocessing_support: Res, mut opaque_prepass_render_phases: ResMut>, mut alpha_mask_prepass_render_phases: ResMut>, mut opaque_deferred_render_phases: ResMut>, mut alpha_mask_deferred_render_phases: ResMut>, - views: Query< - ( - Entity, - &RenderVisibleEntities, - &Msaa, - Option<&DepthPrepass>, - Option<&NormalPrepass>, - Option<&MotionVectorPrepass>, - Option<&DeferredPrepass>, - ), - With, - >, + views: Query<( + &ExtractedView, + &RenderVisibleEntities, + &Msaa, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + Option<&MotionVectorPrepass>, + Option<&DeferredPrepass>, + )>, ) where M::Data: PartialEq + Eq + Hash + Clone, { @@ -801,7 +822,7 @@ pub fn queue_prepass_material_meshes( .get_id::>() .unwrap(); for ( - view, + extracted_view, visible_entities, msaa, depth_prepass, @@ -816,10 +837,10 @@ pub fn queue_prepass_material_meshes( mut opaque_deferred_phase, mut alpha_mask_deferred_phase, ) = ( - opaque_prepass_render_phases.get_mut(&view), - alpha_mask_prepass_render_phases.get_mut(&view), - opaque_deferred_render_phases.get_mut(&view), - alpha_mask_deferred_render_phases.get_mut(&view), + opaque_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_prepass_render_phases.get_mut(&extracted_view.retained_view_entity), + opaque_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), + alpha_mask_deferred_render_phases.get_mut(&extracted_view.retained_view_entity), ); // Skip if there's no place to put the mesh. @@ -893,16 +914,17 @@ pub fn queue_prepass_material_meshes( mesh_key |= MeshPipelineKey::DEFERRED_PREPASS; } - // Even though we don't use the lightmap in the prepass, the - // `SetMeshBindGroup` render command will bind the data for it. So - // we need to include the appropriate flag in the mesh pipeline key - // to ensure that the necessary bind group layout entries are - // present. - if render_lightmaps - .render_lightmaps - .contains_key(visible_entity) - { + if let Some(lightmap) = render_lightmaps.render_lightmaps.get(visible_entity) { + // Even though we don't use the lightmap in the forward prepass, the + // `SetMeshBindGroup` render command will bind the data for it. So + // we need to include the appropriate flag in the mesh pipeline key + // to ensure that the necessary bind group layout entries are + // present. mesh_key |= MeshPipelineKey::LIGHTMAPPED; + + if lightmap.bicubic_sampling && deferred { + mesh_key |= MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING; + } } if render_visibility_ranges.entity_has_crossfading_visibility_ranges(*visible_entity) { @@ -944,67 +966,97 @@ pub fn queue_prepass_material_meshes( } }; + let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + match mesh_key .intersection(MeshPipelineKey::BLEND_RESERVED_BITS | MeshPipelineKey::MAY_DISCARD) { MeshPipelineKey::BLEND_OPAQUE | MeshPipelineKey::BLEND_ALPHA_TO_COVERAGE => { if deferred { opaque_deferred_phase.as_mut().unwrap().add( + OpaqueNoLightmap3dBatchSetKey { + draw_function: opaque_draw_deferred, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, OpaqueNoLightmap3dBinKey { - batch_set_key: OpaqueNoLightmap3dBatchSetKey { - draw_function: opaque_draw_deferred, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - }, asset_id: mesh_instance.mesh_asset_id.into(), }, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } else if let Some(opaque_phase) = opaque_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); opaque_phase.add( + OpaqueNoLightmap3dBatchSetKey { + draw_function: opaque_draw_prepass, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }, OpaqueNoLightmap3dBinKey { - batch_set_key: OpaqueNoLightmap3dBatchSetKey { - draw_function: opaque_draw_prepass, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - }, asset_id: mesh_instance.mesh_asset_id.into(), }, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } } // Alpha mask MeshPipelineKey::MAY_DISCARD => { if deferred { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: alpha_mask_draw_deferred, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; let bin_key = OpaqueNoLightmap3dBinKey { - batch_set_key: OpaqueNoLightmap3dBatchSetKey { - draw_function: alpha_mask_draw_deferred, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - }, asset_id: mesh_instance.mesh_asset_id.into(), }; alpha_mask_deferred_phase.as_mut().unwrap().add( + batch_set_key, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } else if let Some(alpha_mask_phase) = alpha_mask_phase.as_mut() { + let (vertex_slab, index_slab) = + mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = OpaqueNoLightmap3dBatchSetKey { + draw_function: alpha_mask_draw_prepass, + pipeline: pipeline_id, + material_bind_group_index: Some(material.binding.group.0), + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; let bin_key = OpaqueNoLightmap3dBinKey { - batch_set_key: OpaqueNoLightmap3dBatchSetKey { - draw_function: alpha_mask_draw_prepass, - pipeline: pipeline_id, - material_bind_group_index: Some(material.binding.group.0), - }, asset_id: mesh_instance.mesh_asset_id.into(), }; alpha_mask_phase.add( + batch_set_key, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } } diff --git a/crates/bevy_pbr/src/render/build_indirect_params.wgsl b/crates/bevy_pbr/src/render/build_indirect_params.wgsl new file mode 100644 index 0000000000000..90741e9064971 --- /dev/null +++ b/crates/bevy_pbr/src/render/build_indirect_params.wgsl @@ -0,0 +1,106 @@ +// Builds GPU indirect draw parameters from metadata. +// +// This only runs when indirect drawing is enabled. It takes the output of +// `mesh_preprocess.wgsl` and creates indirect parameters for the GPU. +// +// This shader runs separately for indexed and non-indexed meshes. Unlike +// `mesh_preprocess.wgsl`, which runs one instance per mesh *instance*, one +// instance of this shader corresponds to a single *batch* which could contain +// arbitrarily many instances of a single mesh. + +#import bevy_pbr::mesh_preprocess_types::{ + IndirectBatchSet, + IndirectParametersIndexed, + IndirectParametersNonIndexed, + IndirectParametersMetadata, + MeshInput +} + +// The data for each mesh that the CPU supplied to the GPU. +@group(0) @binding(0) var current_input: array; + +// Data that we use to generate the indirect parameters. +// +// The `mesh_preprocess.wgsl` shader emits these. +@group(0) @binding(1) var indirect_parameters_metadata: array; + +// Information about each batch set. +// +// A *batch set* is a set of meshes that might be multi-drawn together. +@group(0) @binding(2) var indirect_batch_sets: array; + +#ifdef INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for indexed meshes. +@group(0) @binding(3) var indirect_parameters: + array; +#else // INDEXED +// The buffer of indirect draw parameters that we generate, and that the GPU +// reads to issue the draws. +// +// This buffer is for non-indexed meshes. +@group(0) @binding(3) var indirect_parameters: + array; +#endif // INDEXED + +@compute +@workgroup_size(64) +fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { + // Figure out our instance index (i.e. batch index). If this thread doesn't + // correspond to any index, bail. + let instance_index = global_invocation_id.x; + if (instance_index >= arrayLength(&indirect_parameters_metadata)) { + return; + } + + // Unpack the metadata for this batch. + let mesh_index = indirect_parameters_metadata[instance_index].mesh_index; + let base_output_index = indirect_parameters_metadata[instance_index].base_output_index; + let batch_set_index = indirect_parameters_metadata[instance_index].batch_set_index; + let instance_count = atomicLoad(&indirect_parameters_metadata[instance_index].instance_count); + + // If we aren't using `multi_draw_indirect_count`, we have a 1:1 fixed + // assignment of batches to slots in the indirect parameters buffer, so we + // can just use the instance index as the index of our indirect parameters. + var indirect_parameters_index = instance_index; + + // If the current hardware and driver support `multi_draw_indirect_count`, + // dynamically reserve an index for the indirect parameters we're to + // generate. +#ifdef MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + if (instance_count == 0u) { + return; + } + + // If this batch belongs to a batch set, then allocate space for the + // indirect commands in that batch set. + if (batch_set_index != 0xffffffffu) { + let indirect_parameters_base = + indirect_batch_sets[batch_set_index].indirect_parameters_base; + let indirect_parameters_offset = + atomicAdd(&indirect_batch_sets[batch_set_index].indirect_parameters_count, 1u); + + indirect_parameters_index = indirect_parameters_base + indirect_parameters_offset; + } +#endif // MULTI_DRAW_INDIRECT_COUNT_SUPPORTED + + // Build up the indirect parameters. The structures for indexed and + // non-indexed meshes are slightly different. + + indirect_parameters[indirect_parameters_index].instance_count = instance_count; + indirect_parameters[indirect_parameters_index].first_instance = base_output_index; + indirect_parameters[indirect_parameters_index].base_vertex = + current_input[mesh_index].first_vertex_index; + +#ifdef INDEXED + indirect_parameters[indirect_parameters_index].index_count = + current_input[mesh_index].index_count; + indirect_parameters[indirect_parameters_index].first_index = + current_input[mesh_index].first_index_index; +#else // INDEXED + indirect_parameters[indirect_parameters_index].vertex_count = + current_input[mesh_index].index_count; +#endif // INDEXED +} \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 434ed64135a2e..8ac5a7c96e29c 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -6,10 +6,12 @@ //! [`MeshInputUniform`]s instead and use the GPU to calculate the remaining //! derived fields in [`MeshUniform`]. -use core::num::NonZero; +use core::num::{NonZero, NonZeroU64}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, @@ -20,33 +22,43 @@ use bevy_ecs::{ }; use bevy_render::{ batching::gpu_preprocessing::{ - BatchedInstanceBuffers, GpuPreprocessingSupport, IndirectParameters, - IndirectParametersBuffer, PreprocessWorkItem, + BatchedInstanceBuffers, GpuPreprocessingSupport, IndirectBatchSet, + IndirectParametersBuffers, IndirectParametersIndexed, IndirectParametersMetadata, + IndirectParametersNonIndexed, PreprocessWorkItem, PreprocessWorkItemBuffers, }, - graph::CameraDriverLabel, - render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext}, + render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext}, render_resource::{ binding_types::{storage_buffer, storage_buffer_read_only, uniform_buffer}, - BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, BufferBinding, + BindGroup, BindGroupEntries, BindGroupLayout, BindingResource, Buffer, BufferBinding, CachedComputePipelineId, ComputePassDescriptor, ComputePipelineDescriptor, DynamicBindGroupLayoutEntries, PipelineCache, Shader, ShaderStages, ShaderType, SpecializedComputePipeline, SpecializedComputePipelines, }, renderer::{RenderContext, RenderDevice, RenderQueue}, + settings::WgpuFeatures, view::{NoIndirectDrawing, ViewUniform, ViewUniformOffset, ViewUniforms}, Render, RenderApp, RenderSet, }; -use bevy_utils::tracing::warn; +use bevy_utils::TypeIdMap; use bitflags::bitflags; use smallvec::{smallvec, SmallVec}; +use tracing::warn; use crate::{ graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform, }; +use super::ViewLightEntities; + /// The handle to the `mesh_preprocess.wgsl` compute shader. pub const MESH_PREPROCESS_SHADER_HANDLE: Handle = Handle::weak_from_u128(16991728318640779533); +/// The handle to the `mesh_preprocess_types.wgsl` compute shader. +pub const MESH_PREPROCESS_TYPES_SHADER_HANDLE: Handle = + Handle::weak_from_u128(2720440370122465935); +/// The handle to the `build_indirect_params.wgsl` compute shader. +pub const BUILD_INDIRECT_PARAMS_SHADER_HANDLE: Handle = + Handle::weak_from_u128(3711077208359699672); /// The GPU workgroup size. const WORKGROUP_SIZE: usize = 64; @@ -63,28 +75,58 @@ pub struct GpuMeshPreprocessPlugin { pub use_gpu_instance_buffer_builder: bool, } -/// The render node for the mesh uniform building pass. +/// The render node for the mesh preprocessing pass. +/// +/// This pass runs a compute shader to cull invisible meshes (if that wasn't +/// done by the CPU), transforms them, and, if indirect drawing is on, populates +/// indirect draw parameter metadata for the subsequent +/// [`BuildIndirectParametersNode`]. pub struct GpuPreprocessNode { view_query: QueryState< ( Entity, - Read, + Read, Read, Has, ), Without, >, + main_view_query: QueryState>, +} + +/// The render node for the indirect parameter building pass. +/// +/// This node runs a compute shader on the output of the [`GpuPreprocessNode`] +/// in order to transform the [`IndirectParametersMetadata`] into +/// properly-formatted [`IndirectParametersIndexed`] and +/// [`IndirectParametersNonIndexed`]. +pub struct BuildIndirectParametersNode { + view_query: QueryState< + Read, + (Without, Without), + >, } -/// The compute shader pipelines for the mesh uniform building pass. +/// The compute shader pipelines for the GPU mesh preprocessing and indirect +/// parameter building passes. #[derive(Resource)] pub struct PreprocessPipelines { /// The pipeline used for CPU culling. This pipeline doesn't populate - /// indirect parameters. - pub direct: PreprocessPipeline, + /// indirect parameter metadata. + pub direct_preprocess: PreprocessPipeline, /// The pipeline used for GPU culling. This pipeline populates indirect + /// parameter metadata. + pub gpu_culling_preprocess: PreprocessPipeline, + /// The pipeline used for indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into indexed indirect /// parameters. - pub gpu_culling: PreprocessPipeline, + pub build_indexed_indirect_params: BuildIndirectParametersPipeline, + /// The pipeline used for non-indexed indirect parameter building. + /// + /// This pipeline converts indirect parameter metadata into non-indexed + /// indirect parameters. + pub build_non_indexed_indirect_params: BuildIndirectParametersPipeline, } /// The pipeline for the GPU mesh preprocessing shader. @@ -97,6 +139,16 @@ pub struct PreprocessPipeline { pub pipeline_id: Option, } +/// The pipeline for the indirect parameter building shader. +pub struct BuildIndirectParametersPipeline { + /// The bind group layout for the compute shader. + pub bind_group_layout: BindGroupLayout, + /// The pipeline ID for the compute shader. + /// + /// This gets filled in `prepare_preprocess_pipelines`. + pub pipeline_id: Option, +} + bitflags! { /// Specifies variants of the mesh preprocessing shader. #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -106,17 +158,77 @@ bitflags! { /// This `#define`'s `GPU_CULLING` in the shader. const GPU_CULLING = 1; } + + /// Specifies variants of the indirect parameter building shader. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct BuildIndirectParametersPipelineKey: u8 { + /// Whether the indirect parameter building shader is processing indexed + /// meshes (those that have index buffers). + /// + /// This defines `INDEXED` in the shader. + const INDEXED = 1; + /// Whether the GPU and driver supports `multi_draw_indirect_count`. + /// + /// This defines `MULTI_DRAW_INDIRECT_COUNT_SUPPORTED` in the shader. + const MULTI_DRAW_INDIRECT_COUNT_SUPPORTED = 2; + } } -/// The compute shader bind group for the mesh uniform building pass. +/// The compute shader bind group for the mesh preprocessing pass for each +/// render phase. /// -/// This goes on the view. -#[derive(Component, Clone)] -pub struct PreprocessBindGroup(BindGroup); +/// This goes on the view. It maps the [`core::any::TypeId`] of a render phase +/// (e.g. [`bevy_core_pipeline::core_3d::Opaque3d`]) to the +/// [`PhasePreprocessBindGroups`] for that phase. +#[derive(Component, Clone, Deref, DerefMut)] +pub struct PreprocessBindGroups(pub TypeIdMap); + +/// The compute shader bind group for the mesh preprocessing step for a single +/// render phase on a single view. +#[derive(Clone)] +pub enum PhasePreprocessBindGroups { + /// The bind group used for the single invocation of the compute shader when + /// indirect drawing is *not* being used. + /// + /// Because direct drawing doesn't require splitting the meshes into indexed + /// and non-indexed meshes, there's only one bind group in this case. + Direct(BindGroup), + + /// The bind groups used for the compute shader when indirect drawing is + /// being used. + /// + /// Because indirect drawing requires splitting the meshes into indexed and + /// non-indexed meshes, there are two bind groups here. + Indirect { + /// The bind group used for indexed meshes. + /// + /// This will be `None` if there are no indexed meshes. + indexed: Option, + /// The bind group used for non-indexed meshes. + /// + /// This will be `None` if there are no non-indexed meshes. + non_indexed: Option, + }, +} + +/// The bind groups for the indirect parameters building compute shader. +/// +/// This is shared among all views and phases. +#[derive(Resource)] +pub struct BuildIndirectParametersBindGroups { + /// The bind group used for indexed meshes. + /// + /// This will be `None` if there are no indexed meshes. + indexed: Option, + /// The bind group used for non-indexed meshes. + /// + /// This will be `None` if there are no non-indexed meshes. + non_indexed: Option, +} /// Stops the `GpuPreprocessNode` attempting to generate the buffer for this view /// useful to avoid duplicating effort if the bind group is shared between views -#[derive(Component)] +#[derive(Component, Default)] pub struct SkipGpuPreprocess; impl Plugin for GpuMeshPreprocessPlugin { @@ -127,6 +239,18 @@ impl Plugin for GpuMeshPreprocessPlugin { "mesh_preprocess.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + MESH_PREPROCESS_TYPES_SHADER_HANDLE, + "mesh_preprocess_types.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + BUILD_INDIRECT_PARAMS_SHADER_HANDLE, + "build_indirect_params.wgsl", + Shader::from_wgsl + ); } fn finish(&self, app: &mut App) { @@ -141,15 +265,10 @@ impl Plugin for GpuMeshPreprocessPlugin { return; } - // Stitch the node in. - let gpu_preprocess_node = GpuPreprocessNode::from_world(render_app.world_mut()); - let mut render_graph = render_app.world_mut().resource_mut::(); - render_graph.add_node(NodePbr::GpuPreprocess, gpu_preprocess_node); - render_graph.add_node_edge(NodePbr::GpuPreprocess, CameraDriverLabel); - render_app .init_resource::() .init_resource::>() + .init_resource::>() .add_systems( Render, ( @@ -161,6 +280,19 @@ impl Plugin for GpuMeshPreprocessPlugin { .in_set(RenderSet::PrepareBindGroups), write_mesh_culling_data_buffer.in_set(RenderSet::PrepareResourcesFlush), ) + ) + .add_render_graph_node::(Core3d, NodePbr::GpuPreprocess) + .add_render_graph_node::( + Core3d, + NodePbr::BuildIndirectParameters + ) + .add_render_graph_edges( + Core3d, + (NodePbr::GpuPreprocess, NodePbr::BuildIndirectParameters, Node3d::Prepass) + ) + .add_render_graph_edges( + Core3d, + (NodePbr::GpuPreprocess, NodePbr::BuildIndirectParameters, NodePbr::ShadowPass) ); } } @@ -169,6 +301,7 @@ impl FromWorld for GpuPreprocessNode { fn from_world(world: &mut World) -> Self { Self { view_query: QueryState::new(world), + main_view_query: QueryState::new(world), } } } @@ -176,11 +309,12 @@ impl FromWorld for GpuPreprocessNode { impl Node for GpuPreprocessNode { fn update(&mut self, world: &mut World) { self.view_query.update_archetypes(world); + self.main_view_query.update_archetypes(world); } fn run<'w>( &self, - _: &mut RenderGraphContext, + graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, world: &'w World, ) -> Result<(), NodeRunError> { @@ -201,12 +335,25 @@ impl Node for GpuPreprocessNode { timestamp_writes: None, }); - // Run the compute passes. - for (view, bind_group, view_uniform_offset, no_indirect_drawing) in - self.view_query.iter_manual(world) + let mut all_views: SmallVec<[_; 8]> = SmallVec::new(); + all_views.push(graph.view_entity()); + if let Ok(shadow_cascade_views) = + self.main_view_query.get_manual(world, graph.view_entity()) { - // Grab the index buffer for this view. - let Some(index_buffer) = index_buffers.get(&view) else { + all_views.extend(shadow_cascade_views.lights.iter().copied()); + } + + // Run the compute passes. + + for view_entity in all_views { + let Ok((view, bind_groups, view_uniform_offset, no_indirect_drawing)) = + self.view_query.get_manual(world, view_entity) + else { + continue; + }; + + // Grab the work item buffers for this view. + let Some(view_work_item_buffers) = index_buffers.get(&view) else { warn!("The preprocessing index buffer wasn't present"); continue; }; @@ -214,34 +361,204 @@ impl Node for GpuPreprocessNode { // Select the right pipeline, depending on whether GPU culling is in // use. let maybe_pipeline_id = if !no_indirect_drawing { - preprocess_pipelines.gpu_culling.pipeline_id + preprocess_pipelines.gpu_culling_preprocess.pipeline_id } else { - preprocess_pipelines.direct.pipeline_id + preprocess_pipelines.direct_preprocess.pipeline_id }; // Fetch the pipeline. let Some(preprocess_pipeline_id) = maybe_pipeline_id else { warn!("The build mesh uniforms pipeline wasn't ready"); - return Ok(()); + continue; }; let Some(preprocess_pipeline) = pipeline_cache.get_compute_pipeline(preprocess_pipeline_id) else { // This will happen while the pipeline is being compiled and is fine. - return Ok(()); + continue; }; compute_pass.set_pipeline(preprocess_pipeline); - let mut dynamic_offsets: SmallVec<[u32; 1]> = smallvec![]; - if !no_indirect_drawing { - dynamic_offsets.push(view_uniform_offset.offset); + // Loop over each render phase. + for (phase_type_id, phase_work_item_buffers) in view_work_item_buffers { + // Fetch the bind group for the render phase. + let Some(phase_bind_groups) = bind_groups.get(phase_type_id) else { + continue; + }; + + // If we're drawing indirectly, make sure the mesh preprocessing + // shader has access to the view info it needs to do culling. + let mut dynamic_offsets: SmallVec<[u32; 1]> = smallvec![]; + if !no_indirect_drawing { + dynamic_offsets.push(view_uniform_offset.offset); + } + + // Are we drawing directly or indirectly? + match *phase_bind_groups { + PhasePreprocessBindGroups::Direct(ref bind_group) => { + // Invoke the mesh preprocessing shader to transform + // meshes only, but not cull. + let PreprocessWorkItemBuffers::Direct(phase_work_item_buffer) = + phase_work_item_buffers + else { + continue; + }; + compute_pass.set_bind_group(0, bind_group, &dynamic_offsets); + let workgroup_count = phase_work_item_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + PhasePreprocessBindGroups::Indirect { + indexed: ref maybe_indexed_bind_group, + non_indexed: ref maybe_non_indexed_bind_group, + } => { + // Invoke the mesh preprocessing shader to transform and + // cull the meshes. + let PreprocessWorkItemBuffers::Indirect { + indexed: indexed_buffer, + non_indexed: non_indexed_buffer, + .. + } = phase_work_item_buffers + else { + continue; + }; + + // Transform and cull indexed meshes if there are any. + if let Some(indexed_bind_group) = maybe_indexed_bind_group { + compute_pass.set_bind_group(0, indexed_bind_group, &dynamic_offsets); + let workgroup_count = indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Transform and cull non-indexed meshes if there are any. + if let Some(non_indexed_bind_group) = maybe_non_indexed_bind_group { + compute_pass.set_bind_group( + 0, + non_indexed_bind_group, + &dynamic_offsets, + ); + let workgroup_count = non_indexed_buffer.len().div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + } + } } - compute_pass.set_bind_group(0, &bind_group.0, &dynamic_offsets); + } + + Ok(()) + } +} + +impl FromWorld for BuildIndirectParametersNode { + fn from_world(world: &mut World) -> Self { + Self { + view_query: QueryState::new(world), + } + } +} + +impl Node for BuildIndirectParametersNode { + fn update(&mut self, world: &mut World) { + self.view_query.update_archetypes(world); + } - let workgroup_count = index_buffer.buffer.len().div_ceil(WORKGROUP_SIZE); - compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + fn run<'w>( + &self, + _: &mut RenderGraphContext, + render_context: &mut RenderContext<'w>, + world: &'w World, + ) -> Result<(), NodeRunError> { + // Fetch the bind group. + let Some(build_indirect_params_bind_groups) = + world.get_resource::() + else { + return Ok(()); + }; + + // Fetch the pipelines and the buffers we need. + let pipeline_cache = world.resource::(); + let preprocess_pipelines = world.resource::(); + let indirect_parameters_buffers = world.resource::(); + + // Create the compute pass. + let mut compute_pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("build indirect parameters"), + timestamp_writes: None, + }); + + // Fetch the pipelines. + + let (maybe_indexed_pipeline_id, maybe_non_indexed_pipeline_id) = ( + preprocess_pipelines + .build_indexed_indirect_params + .pipeline_id, + preprocess_pipelines + .build_non_indexed_indirect_params + .pipeline_id, + ); + + let ( + Some(build_indexed_indirect_params_pipeline_id), + Some(build_non_indexed_indirect_params_pipeline_id), + ) = (maybe_indexed_pipeline_id, maybe_non_indexed_pipeline_id) + else { + warn!("The build indirect parameters pipelines weren't ready"); + return Ok(()); + }; + + let ( + Some(build_indexed_indirect_params_pipeline), + Some(build_non_indexed_indirect_params_pipeline), + ) = ( + pipeline_cache.get_compute_pipeline(build_indexed_indirect_params_pipeline_id), + pipeline_cache.get_compute_pipeline(build_non_indexed_indirect_params_pipeline_id), + ) + else { + // This will happen while the pipeline is being compiled and is fine. + return Ok(()); + }; + + // Transform the [`IndirectParametersMetadata`] that the GPU mesh + // preprocessing phase wrote to [`IndirectParametersIndexed`] for + // indexed meshes, if we have any. + if let Some(ref build_indirect_indexed_params_bind_group) = + build_indirect_params_bind_groups.indexed + { + compute_pass.set_pipeline(build_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_indexed_params_bind_group, &[]); + let workgroup_count = indirect_parameters_buffers + .indexed_batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } + } + + // Transform the [`IndirectParametersMetadata`] that the GPU mesh + // preprocessing phase wrote to [`IndirectParametersNonIndexed`] for + // non-indexed meshes, if we have any. + if let Some(ref build_indirect_non_indexed_params_bind_group) = + build_indirect_params_bind_groups.non_indexed + { + compute_pass.set_pipeline(build_non_indexed_indirect_params_pipeline); + compute_pass.set_bind_group(0, build_indirect_non_indexed_params_bind_group, &[]); + let workgroup_count = indirect_parameters_buffers + .non_indexed_batch_count() + .div_ceil(WORKGROUP_SIZE); + if workgroup_count > 0 { + compute_pass.dispatch_workgroups(workgroup_count as u32, 1, 1); + } } Ok(()) @@ -249,8 +566,15 @@ impl Node for GpuPreprocessNode { } impl PreprocessPipelines { + /// Returns true if the preprocessing and indirect parameters pipelines have + /// been loaded or false otherwise. pub(crate) fn pipelines_are_loaded(&self, pipeline_cache: &PipelineCache) -> bool { - self.direct.is_loaded(pipeline_cache) && self.gpu_culling.is_loaded(pipeline_cache) + self.direct_preprocess.is_loaded(pipeline_cache) + && self.gpu_culling_preprocess.is_loaded(pipeline_cache) + && self.build_indexed_indirect_params.is_loaded(pipeline_cache) + && self + .build_non_indexed_indirect_params + .is_loaded(pipeline_cache) } } @@ -261,6 +585,15 @@ impl PreprocessPipeline { } } +impl BuildIndirectParametersPipeline { + /// Returns true if this pipeline has been loaded into the pipeline cache or + /// false otherwise. + fn is_loaded(&self, pipeline_cache: &PipelineCache) -> bool { + self.pipeline_id + .is_some_and(|pipeline_id| pipeline_cache.get_compute_pipeline(pipeline_id).is_some()) + } +} + impl SpecializedComputePipeline for PreprocessPipeline { type Key = PreprocessPipelineKey; @@ -302,14 +635,24 @@ impl FromWorld for PreprocessPipelines { let direct_bind_group_layout_entries = preprocess_direct_bind_group_layout_entries(); let gpu_culling_bind_group_layout_entries = preprocess_direct_bind_group_layout_entries() .extend_sequential(( - // `indirect_parameters` - storage_buffer::(/* has_dynamic_offset= */ false), + // `indirect_parameters_metadata` + storage_buffer::(/* has_dynamic_offset= */ false), // `mesh_culling_data` storage_buffer_read_only::(/* has_dynamic_offset= */ false), // `view` uniform_buffer::(/* has_dynamic_offset= */ true), )); + // Indexed and non-indexed bind group parameters share all the bind + // group layout entries except the final one. + let build_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + let build_non_indexed_indirect_params_bind_group_layout_entries = + build_indirect_params_bind_group_layout_entries() + .extend_sequential((storage_buffer::(false),)); + + // Create the bind group layouts. let direct_bind_group_layout = render_device.create_bind_group_layout( "build mesh uniforms direct bind group layout", &direct_bind_group_layout_entries, @@ -318,16 +661,34 @@ impl FromWorld for PreprocessPipelines { "build mesh uniforms GPU culling bind group layout", &gpu_culling_bind_group_layout_entries, ); + let build_indexed_indirect_params_bind_group_layout = render_device + .create_bind_group_layout( + "build indexed indirect parameters bind group layout", + &build_indexed_indirect_params_bind_group_layout_entries, + ); + let build_non_indexed_indirect_params_bind_group_layout = render_device + .create_bind_group_layout( + "build non-indexed indirect parameters bind group layout", + &build_non_indexed_indirect_params_bind_group_layout_entries, + ); PreprocessPipelines { - direct: PreprocessPipeline { + direct_preprocess: PreprocessPipeline { bind_group_layout: direct_bind_group_layout, pipeline_id: None, }, - gpu_culling: PreprocessPipeline { + gpu_culling_preprocess: PreprocessPipeline { bind_group_layout: gpu_culling_bind_group_layout, pipeline_id: None, }, + build_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_indexed_indirect_params_bind_group_layout, + pipeline_id: None, + }, + build_non_indexed_indirect_params: BuildIndirectParametersPipeline { + bind_group_layout: build_non_indexed_indirect_params_bind_group_layout, + pipeline_id: None, + }, } } } @@ -348,22 +709,66 @@ fn preprocess_direct_bind_group_layout_entries() -> DynamicBindGroupLayoutEntrie ) } -/// A system that specializes the `mesh_preprocess.wgsl` pipelines if necessary. +// Returns the first 3 bind group layout entries shared between all invocations +// of the indirect parameters building shader. +fn build_indirect_params_bind_group_layout_entries() -> DynamicBindGroupLayoutEntries { + DynamicBindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only::(false), + storage_buffer_read_only::(false), + storage_buffer::(false), + ), + ) +} + +/// A system that specializes the `mesh_preprocess.wgsl` and +/// `build_indirect_params.wgsl` pipelines if necessary. pub fn prepare_preprocess_pipelines( pipeline_cache: Res, - mut pipelines: ResMut>, + render_device: Res, + mut specialized_preprocess_pipelines: ResMut>, + mut specialized_build_indirect_parameters_pipelines: ResMut< + SpecializedComputePipelines, + >, mut preprocess_pipelines: ResMut, ) { - preprocess_pipelines.direct.prepare( + preprocess_pipelines.direct_preprocess.prepare( &pipeline_cache, - &mut pipelines, + &mut specialized_preprocess_pipelines, PreprocessPipelineKey::empty(), ); - preprocess_pipelines.gpu_culling.prepare( + preprocess_pipelines.gpu_culling_preprocess.prepare( &pipeline_cache, - &mut pipelines, + &mut specialized_preprocess_pipelines, PreprocessPipelineKey::GPU_CULLING, ); + + let mut build_indirect_parameters_pipeline_key = BuildIndirectParametersPipelineKey::empty(); + + // If the GPU and driver support `multi_draw_indirect_count`, tell the + // shader that. + if render_device + .wgpu_device() + .features() + .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT) + { + build_indirect_parameters_pipeline_key + .insert(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED); + } + + preprocess_pipelines.build_indexed_indirect_params.prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key | BuildIndirectParametersPipelineKey::INDEXED, + ); + preprocess_pipelines + .build_non_indexed_indirect_params + .prepare( + &pipeline_cache, + &mut specialized_build_indirect_parameters_pipelines, + build_indirect_parameters_pipeline_key, + ); } impl PreprocessPipeline { @@ -382,96 +787,344 @@ impl PreprocessPipeline { } } +impl SpecializedComputePipeline for BuildIndirectParametersPipeline { + type Key = BuildIndirectParametersPipelineKey; + + fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { + let mut shader_defs = vec![]; + if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + shader_defs.push("INDEXED".into()); + } + if key.contains(BuildIndirectParametersPipelineKey::MULTI_DRAW_INDIRECT_COUNT_SUPPORTED) { + shader_defs.push("MULTI_DRAW_INDIRECT_COUNT_SUPPORTED".into()); + } + + ComputePipelineDescriptor { + label: if key.contains(BuildIndirectParametersPipelineKey::INDEXED) { + Some("build indexed indirect parameters".into()) + } else { + Some("build non-indexed indirect parameters".into()) + }, + layout: vec![self.bind_group_layout.clone()], + push_constant_ranges: vec![], + shader: BUILD_INDIRECT_PARAMS_SHADER_HANDLE, + shader_defs, + entry_point: "main".into(), + zero_initialize_workgroup_memory: false, + } + } +} + +impl BuildIndirectParametersPipeline { + fn prepare( + &mut self, + pipeline_cache: &PipelineCache, + pipelines: &mut SpecializedComputePipelines, + key: BuildIndirectParametersPipelineKey, + ) { + if self.pipeline_id.is_some() { + return; + } + + let build_indirect_parameters_pipeline_id = pipelines.specialize(pipeline_cache, self, key); + self.pipeline_id = Some(build_indirect_parameters_pipeline_id); + } +} + /// A system that attaches the mesh uniform buffers to the bind groups for the /// variants of the mesh preprocessing compute shader. pub fn prepare_preprocess_bind_groups( mut commands: Commands, render_device: Res, batched_instance_buffers: Res>, - indirect_parameters_buffer: Res, + indirect_parameters_buffers: Res, mesh_culling_data_buffer: Res, view_uniforms: Res, pipelines: Res, ) { // Grab the `BatchedInstanceBuffers`. + let batched_instance_buffers = batched_instance_buffers.into_inner(); + + let Some(current_input_buffer) = batched_instance_buffers + .current_input_buffer + .buffer() + .buffer() + else { + return; + }; + + // Keep track of whether any of the phases will be drawn indirectly. If + // they are, then we'll need bind groups for the indirect parameters + // building shader too. + let mut any_indirect = false; + + for (view, phase_work_item_buffers) in &batched_instance_buffers.work_item_buffers { + let mut bind_groups = TypeIdMap::default(); + + for (&phase_id, work_item_buffers) in phase_work_item_buffers { + if let Some(bind_group) = prepare_preprocess_bind_group_for_phase( + &render_device, + &pipelines, + &view_uniforms, + &indirect_parameters_buffers, + &mesh_culling_data_buffer, + batched_instance_buffers, + work_item_buffers, + &mut any_indirect, + ) { + bind_groups.insert(phase_id, bind_group); + } + } + + commands + .entity(*view) + .insert(PreprocessBindGroups(bind_groups)); + } + + // If any of the phases will be drawn indirectly, create the bind groups for + // the indirect parameters building shader. + if any_indirect { + create_build_indirect_parameters_bind_groups( + &mut commands, + &render_device, + &pipelines, + current_input_buffer, + &indirect_parameters_buffers, + ); + } +} + +// Creates the bind group for the GPU preprocessing shader for a single phase +// for a single view. +#[expect( + clippy::too_many_arguments, + reason = "it's a system that needs a bunch of parameters" +)] +fn prepare_preprocess_bind_group_for_phase( + render_device: &RenderDevice, + pipelines: &PreprocessPipelines, + view_uniforms: &ViewUniforms, + indirect_parameters_buffers: &IndirectParametersBuffers, + mesh_culling_data_buffer: &MeshCullingDataBuffer, + batched_instance_buffers: &BatchedInstanceBuffers, + work_item_buffers: &PreprocessWorkItemBuffers, + any_indirect: &mut bool, +) -> Option { + // Get the current input buffers. + let BatchedInstanceBuffers { data_buffer: ref data_buffer_vec, - work_item_buffers: ref index_buffers, current_input_buffer: ref current_input_buffer_vec, previous_input_buffer: ref previous_input_buffer_vec, - } = batched_instance_buffers.into_inner(); + .. + } = batched_instance_buffers; - let (Some(current_input_buffer), Some(previous_input_buffer), Some(data_buffer)) = ( - current_input_buffer_vec.buffer().buffer(), - previous_input_buffer_vec.buffer().buffer(), - data_buffer_vec.buffer(), - ) else { - return; - }; + let current_input_buffer = current_input_buffer_vec.buffer().buffer()?; + let previous_input_buffer = previous_input_buffer_vec.buffer().buffer()?; + let data_buffer = data_buffer_vec.buffer()?; - for (view, index_buffer_vec) in index_buffers { - let Some(index_buffer) = index_buffer_vec.buffer.buffer() else { - continue; - }; + // Build the appropriate bind group, depending on whether we're drawing + // directly or indirectly. - // Don't use `as_entire_binding()` here; the shader reads the array - // length and the underlying buffer may be longer than the actual size - // of the vector. - let index_buffer_size = NonZero::::try_from( - index_buffer_vec.buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), - ) - .ok(); - - let bind_group = if !index_buffer_vec.no_indirect_drawing { - let ( - Some(indirect_parameters_buffer), - Some(mesh_culling_data_buffer), - Some(view_uniforms_binding), - ) = ( - indirect_parameters_buffer.buffer(), - mesh_culling_data_buffer.buffer(), - view_uniforms.uniforms.binding(), + match *work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref work_item_buffer_vec) => { + let work_item_buffer = work_item_buffer_vec.buffer()?; + + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let work_item_buffer_size = NonZero::::try_from( + work_item_buffer_vec.len() as u64 * u64::from(PreprocessWorkItem::min_size()), ) - else { - continue; - }; + .ok(); - PreprocessBindGroup(render_device.create_bind_group( - "preprocess_gpu_culling_bind_group", - &pipelines.gpu_culling.bind_group_layout, - &BindGroupEntries::sequential(( - current_input_buffer.as_entire_binding(), - previous_input_buffer.as_entire_binding(), - BindingResource::Buffer(BufferBinding { - buffer: index_buffer, - offset: 0, - size: index_buffer_size, - }), - data_buffer.as_entire_binding(), - indirect_parameters_buffer.as_entire_binding(), - mesh_culling_data_buffer.as_entire_binding(), - view_uniforms_binding, - )), + Some(PhasePreprocessBindGroups::Direct( + render_device.create_bind_group( + "preprocess_direct_bind_group", + &pipelines.direct_preprocess.bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + previous_input_buffer.as_entire_binding(), + BindingResource::Buffer(BufferBinding { + buffer: work_item_buffer, + offset: 0, + size: work_item_buffer_size, + }), + data_buffer.as_entire_binding(), + )), + ), )) - } else { - PreprocessBindGroup(render_device.create_bind_group( - "preprocess_direct_bind_group", - &pipelines.direct.bind_group_layout, + } + + PreprocessWorkItemBuffers::Indirect { + indexed: ref indexed_buffer, + non_indexed: ref non_indexed_buffer, + } => { + // For indirect drawing, we need two separate bind groups, one for indexed meshes and one for non-indexed meshes. + + let mesh_culling_data_buffer = mesh_culling_data_buffer.buffer()?; + let view_uniforms_binding = view_uniforms.uniforms.binding()?; + + let indexed_bind_group = match ( + indexed_buffer.buffer(), + indirect_parameters_buffers.indexed_metadata_buffer(), + ) { + ( + Some(indexed_work_item_buffer), + Some(indexed_indirect_parameters_metadata_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let indexed_work_item_buffer_size = NonZero::::try_from( + indexed_buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + Some(render_device.create_bind_group( + "preprocess_indexed_indirect_gpu_culling_bind_group", + &pipelines.gpu_culling_preprocess.bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + previous_input_buffer.as_entire_binding(), + BindingResource::Buffer(BufferBinding { + buffer: indexed_work_item_buffer, + offset: 0, + size: indexed_work_item_buffer_size, + }), + data_buffer.as_entire_binding(), + indexed_indirect_parameters_metadata_buffer.as_entire_binding(), + mesh_culling_data_buffer.as_entire_binding(), + view_uniforms_binding.clone(), + )), + )) + } + _ => None, + }; + + let non_indexed_bind_group = match ( + non_indexed_buffer.buffer(), + indirect_parameters_buffers.non_indexed_metadata_buffer(), + ) { + ( + Some(non_indexed_work_item_buffer), + Some(non_indexed_indirect_parameters_metadata_buffer), + ) => { + // Don't use `as_entire_binding()` here; the shader reads the array + // length and the underlying buffer may be longer than the actual size + // of the vector. + let non_indexed_work_item_buffer_size = NonZero::::try_from( + non_indexed_buffer.len() as u64 * u64::from(PreprocessWorkItem::min_size()), + ) + .ok(); + Some(render_device.create_bind_group( + "preprocess_non_indexed_indirect_gpu_culling_bind_group", + &pipelines.gpu_culling_preprocess.bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + previous_input_buffer.as_entire_binding(), + BindingResource::Buffer(BufferBinding { + buffer: non_indexed_work_item_buffer, + offset: 0, + size: non_indexed_work_item_buffer_size, + }), + data_buffer.as_entire_binding(), + non_indexed_indirect_parameters_metadata_buffer.as_entire_binding(), + mesh_culling_data_buffer.as_entire_binding(), + view_uniforms_binding, + )), + )) + } + _ => None, + }; + + // Note that we found phases that will be drawn indirectly so that + // we remember to build the bind groups for the indirect parameter + // building shader. + *any_indirect = true; + + Some(PhasePreprocessBindGroups::Indirect { + indexed: indexed_bind_group, + non_indexed: non_indexed_bind_group, + }) + } + } +} + +/// A system that creates bind groups from the indirect parameters metadata and +/// data buffers for the indirect parameter building shader. +fn create_build_indirect_parameters_bind_groups( + commands: &mut Commands, + render_device: &RenderDevice, + pipelines: &PreprocessPipelines, + current_input_buffer: &Buffer, + indirect_parameters_buffer: &IndirectParametersBuffers, +) { + commands.insert_resource(BuildIndirectParametersBindGroups { + indexed: match ( + indirect_parameters_buffer.indexed_metadata_buffer(), + indirect_parameters_buffer.indexed_data_buffer(), + indirect_parameters_buffer.indexed_batch_sets_buffer(), + ) { + ( + Some(indexed_indirect_parameters_metadata_buffer), + Some(indexed_indirect_parameters_data_buffer), + Some(indexed_batch_sets_buffer), + ) => Some(render_device.create_bind_group( + "build_indexed_indirect_parameters_bind_group", + &pipelines.build_indexed_indirect_params.bind_group_layout, &BindGroupEntries::sequential(( current_input_buffer.as_entire_binding(), - previous_input_buffer.as_entire_binding(), - BindingResource::Buffer(BufferBinding { - buffer: index_buffer, + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: indexed_indirect_parameters_metadata_buffer, offset: 0, - size: index_buffer_size, - }), - data_buffer.as_entire_binding(), + size: NonZeroU64::new( + indirect_parameters_buffer.indexed_batch_count() as u64 + * size_of::() as u64, + ), + }, + indexed_batch_sets_buffer.as_entire_binding(), + indexed_indirect_parameters_data_buffer.as_entire_binding(), )), - )) - }; - - commands.entity(*view).insert(bind_group); - } + )), + _ => None, + }, + non_indexed: match ( + indirect_parameters_buffer.non_indexed_metadata_buffer(), + indirect_parameters_buffer.non_indexed_data_buffer(), + indirect_parameters_buffer.non_indexed_batch_sets_buffer(), + ) { + ( + Some(non_indexed_indirect_parameters_metadata_buffer), + Some(non_indexed_indirect_parameters_data_buffer), + Some(non_indexed_batch_sets_buffer), + ) => Some( + render_device.create_bind_group( + "build_non_indexed_indirect_parameters_bind_group", + &pipelines + .build_non_indexed_indirect_params + .bind_group_layout, + &BindGroupEntries::sequential(( + current_input_buffer.as_entire_binding(), + // Don't use `as_entire_binding` here; the shader reads + // the length and `RawBufferVec` overallocates. + BufferBinding { + buffer: non_indexed_indirect_parameters_metadata_buffer, + offset: 0, + size: NonZeroU64::new( + indirect_parameters_buffer.non_indexed_batch_count() as u64 + * size_of::() as u64, + ), + }, + non_indexed_batch_sets_buffer.as_entire_binding(), + non_indexed_indirect_parameters_data_buffer.as_entire_binding(), + )), + ), + ), + _ => None, + }, + }); } /// Writes the information needed to do GPU mesh culling to the GPU. diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 50e126da73a12..4a1187400e5fe 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -6,7 +6,7 @@ use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::{EntityHash, EntityHashMap, EntityHashSet}, + entity::{EntityHashMap, EntityHashSet}, prelude::*, system::lifetimeless::Read, }; @@ -15,7 +15,7 @@ use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, camera::SortedCameras, mesh::allocator::MeshAllocator, - view::NoIndirectDrawing, + view::{NoIndirectDrawing, RetainedViewEntity}, }; use bevy_render::{ diagnostic::RecordDiagnostics, @@ -35,14 +35,11 @@ use bevy_render::{ sync_world::{MainEntity, RenderEntity}, }; use bevy_transform::{components::GlobalTransform, prelude::Transform}; -#[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; -use bevy_utils::{ - default, - tracing::{error, warn}, - HashMap, -}; +use bevy_utils::{default, HashMap, HashSet}; use core::{hash::Hash, ops::Range}; +#[cfg(feature = "trace")] +use tracing::info_span; +use tracing::{error, warn}; #[derive(Component)] pub struct ExtractedPointLight { @@ -209,7 +206,6 @@ impl FromWorld for ShadowSamplers { } } -#[allow(clippy::too_many_arguments)] pub fn extract_lights( mut commands: Commands, point_light_shadow_map: Extract>, @@ -217,6 +213,7 @@ pub fn extract_lights( global_point_lights: Extract>, point_lights: Extract< Query<( + Entity, RenderEntity, &PointLight, &CubemapVisibleEntities, @@ -228,6 +225,7 @@ pub fn extract_lights( >, spot_lights: Extract< Query<( + Entity, RenderEntity, &SpotLight, &VisibleMeshEntities, @@ -240,6 +238,7 @@ pub fn extract_lights( directional_lights: Extract< Query< ( + Entity, RenderEntity, &DirectionalLight, &CascadesVisibleEntities, @@ -278,6 +277,7 @@ pub fn extract_lights( let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); for entity in global_point_lights.iter().copied() { let Ok(( + main_entity, render_entity, point_light, cubemap_visible_entities, @@ -331,6 +331,7 @@ pub fn extract_lights( extracted_point_light, render_cubemap_visible_entities, (*frusta).clone(), + MainEntity::from(main_entity), ), )); } @@ -340,6 +341,7 @@ pub fn extract_lights( let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); for entity in global_point_lights.iter().copied() { if let Ok(( + main_entity, render_entity, spot_light, visible_entities, @@ -391,6 +393,7 @@ pub fn extract_lights( }, render_visible_entities, *frustum, + MainEntity::from(main_entity), ), )); } @@ -399,6 +402,7 @@ pub fn extract_lights( commands.insert_or_spawn_batch(spot_lights_values); for ( + main_entity, entity, directional_light, visible_entities, @@ -478,6 +482,7 @@ pub fn extract_lights( RenderCascadesVisibleEntities { entities: cascade_visible_entities, }, + MainEntity::from(main_entity), )); } } @@ -518,7 +523,7 @@ pub(crate) fn extracted_light_removed( mut commands: Commands, ) { if let Some(mut v) = commands.get_entity(trigger.target()) { - v.remove::(); + v.try_remove::(); } } @@ -609,8 +614,18 @@ pub struct ViewShadowBindings { pub directional_light_depth_texture_view: TextureView, } +/// A component that holds the shadow cascade views for all shadow cascades +/// associated with a camera. +/// +/// Note: Despite the name, this component actually holds the shadow cascade +/// views, not the lights themselves. #[derive(Component)] pub struct ViewLightEntities { + /// The shadow cascade views for all shadow cascades associated with a + /// camera. + /// + /// Note: Despite the name, this component actually holds the shadow cascade + /// views, not the lights themselves. pub lights: Vec, } @@ -687,7 +702,6 @@ pub(crate) fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { Mat4::perspective_infinite_reverse_rh(angle * 2.0, 1.0, near_z) } -#[allow(clippy::too_many_arguments)] pub fn prepare_lights( mut commands: Commands, mut texture_cache: ResMut, @@ -697,10 +711,12 @@ pub fn prepare_lights( views: Query< ( Entity, + MainEntity, &ExtractedView, &ExtractedClusterConfig, Option<&RenderLayers>, Has, + Option<&AmbientLight>, ), With, >, @@ -712,13 +728,14 @@ pub fn prepare_lights( mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted, mut live_shadow_mapping_lights, - ): (Local, Local, Local), + ): (Local, Local, Local>), point_lights: Query<( Entity, + &MainEntity, &ExtractedPointLight, AnyOf<(&CubemapFrusta, &Frustum)>, )>, - directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, + directional_lights: Query<(Entity, &MainEntity, &ExtractedDirectionalLight)>, mut light_view_entities: Query<&mut LightViewEntities>, sorted_cameras: Res, gpu_preprocessing_support: Res, @@ -774,7 +791,7 @@ pub fn prepare_lights( if !*max_cascades_per_light_warning_emitted && directional_lights .iter() - .any(|(_, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT) + .any(|(_, _, light)| light.cascade_shadow_config.bounds.len() > MAX_CASCADES_PER_LIGHT) { warn!( "The number of cascades configured for a directional light exceeds the supported limit of {}.", @@ -785,50 +802,50 @@ pub fn prepare_lights( let point_light_count = point_lights .iter() - .filter(|light| light.1.spot_light_angles.is_none()) + .filter(|light| light.2.spot_light_angles.is_none()) .count(); let point_light_volumetric_enabled_count = point_lights .iter() - .filter(|(_, light, _)| light.volumetric && light.spot_light_angles.is_none()) + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_none()) .count() .min(max_texture_cubes); let point_light_shadow_maps_count = point_lights .iter() - .filter(|light| light.1.shadows_enabled && light.1.spot_light_angles.is_none()) + .filter(|light| light.2.shadows_enabled && light.2.spot_light_angles.is_none()) .count() .min(max_texture_cubes); let directional_volumetric_enabled_count = directional_lights .iter() .take(MAX_DIRECTIONAL_LIGHTS) - .filter(|(_, light)| light.volumetric) + .filter(|(_, _, light)| light.volumetric) .count() .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); let directional_shadow_enabled_count = directional_lights .iter() .take(MAX_DIRECTIONAL_LIGHTS) - .filter(|(_, light)| light.shadows_enabled) + .filter(|(_, _, light)| light.shadows_enabled) .count() .min(max_texture_array_layers / MAX_CASCADES_PER_LIGHT); let spot_light_count = point_lights .iter() - .filter(|(_, light, _)| light.spot_light_angles.is_some()) + .filter(|(_, _, light, _)| light.spot_light_angles.is_some()) .count() .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); let spot_light_volumetric_enabled_count = point_lights .iter() - .filter(|(_, light, _)| light.volumetric && light.spot_light_angles.is_some()) + .filter(|(_, _, light, _)| light.volumetric && light.spot_light_angles.is_some()) .count() .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); let spot_light_shadow_maps_count = point_lights .iter() - .filter(|(_, light, _)| light.shadows_enabled && light.spot_light_angles.is_some()) + .filter(|(_, _, light, _)| light.shadows_enabled && light.spot_light_angles.is_some()) .count() .min(max_texture_array_layers - directional_shadow_enabled_count * MAX_CASCADES_PER_LIGHT); @@ -837,16 +854,10 @@ pub fn prepare_lights( // - then those with shadows enabled first, so that the index can be used to render at most `point_light_shadow_maps_count` // point light shadows and `spot_light_shadow_maps_count` spot light shadow maps, // - 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(|(entity_1, light_1, _), (entity_2, light_2, _)| { - clusterable_object_order( - ClusterableObjectOrderData { - entity: entity_1, - object_type: &ClusterableObjectType::from_point_or_spot_light(light_1), - }, - ClusterableObjectOrderData { - entity: entity_2, - object_type: &ClusterableObjectType::from_point_or_spot_light(light_2), - }, + point_lights.sort_by_cached_key(|(entity, _, light, _)| { + ( + ClusterableObjectType::from_point_or_spot_light(light).ordering(), + *entity, ) }); @@ -858,11 +869,10 @@ pub fn prepare_lights( // shadows // - then by entity as a stable key to ensure that a consistent set of // lights are chosen if the light count limit is exceeded. - directional_lights.sort_by(|(entity_1, light_1), (entity_2, light_2)| { - directional_light_order( - (entity_1, &light_1.volumetric, &light_1.shadows_enabled), - (entity_2, &light_2.volumetric, &light_2.shadows_enabled), - ) + // - because entities are unique, we can use `sort_unstable_by_key` + // and still end up with a stable order. + directional_lights.sort_unstable_by_key(|(entity, _, light)| { + (light.volumetric, light.shadows_enabled, *entity) }); if global_light_meta.entity_to_index.capacity() < point_lights.len() { @@ -872,7 +882,7 @@ pub fn prepare_lights( } let mut gpu_point_lights = Vec::new(); - for (index, &(entity, light, _)) in point_lights.iter().enumerate() { + for (index, &(entity, _, light, _)) in point_lights.iter().enumerate() { let mut flags = PointLightFlags::NONE; // Lights are sorted, shadow enabled lights are first @@ -961,7 +971,7 @@ pub fn prepare_lights( let mut gpu_directional_lights = [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS]; let mut num_directional_cascades_enabled = 0usize; - for (index, (_light_entity, light)) in directional_lights + for (index, (_light_entity, _, light)) in directional_lights .iter() .enumerate() .take(MAX_DIRECTIONAL_LIGHTS) @@ -1114,10 +1124,18 @@ pub fn prepare_lights( array_layer_count: None, }); - let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash); + let mut live_views = EntityHashSet::with_capacity(views_count); // set up light data for each view - for (entity, extracted_view, clusters, maybe_layers, no_indirect_drawing) in sorted_cameras + for ( + entity, + camera_main_entity, + extracted_view, + clusters, + maybe_layers, + no_indirect_drawing, + maybe_ambient_override, + ) in sorted_cameras .0 .iter() .filter_map(|sorted_camera| views.get(sorted_camera.entity).ok()) @@ -1140,6 +1158,7 @@ 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_lights = GpuLights { directional_lights: gpu_directional_lights, ambient_color: Vec4::from_slice(&LinearRgba::from(ambient_light.color).to_f32_array()) @@ -1163,7 +1182,7 @@ pub fn prepare_lights( }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query - for &(light_entity, light, (point_light_frusta, _)) in point_lights + for &(light_entity, light_main_entity, light, (point_light_frusta, _)) in point_lights .iter() // Lights are sorted, shadow enabled lights are first .take(point_light_count.min(max_texture_cubes)) @@ -1231,6 +1250,12 @@ pub fn prepare_lights( }) .clone(); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + face_index as u32, + ); + commands.entity(view_light_entity).insert(( ShadowView { depth_attachment, @@ -1241,6 +1266,7 @@ pub fn prepare_lights( ), }, ExtractedView { + retained_view_entity, viewport: UVec4::new( 0, 0, @@ -1268,18 +1294,20 @@ pub fn prepare_lights( if first { // Subsequent views with the same light entity will reuse the same shadow map - shadow_render_phases.insert_or_clear(view_light_entity, gpu_preprocessing_mode); - live_shadow_mapping_lights.insert(view_light_entity); + shadow_render_phases + .insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); } } } // spot lights - for (light_index, &(light_entity, light, (_, spot_light_frustum))) in point_lights - .iter() - .skip(point_light_count) - .take(spot_light_count) - .enumerate() + for (light_index, &(light_entity, light_main_entity, light, (_, spot_light_frustum))) in + point_lights + .iter() + .skip(point_light_count) + .take(spot_light_count) + .enumerate() { let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { continue; @@ -1330,12 +1358,16 @@ pub fn prepare_lights( let view_light_entity = light_view_entities[0]; + let retained_view_entity = + RetainedViewEntity::new(*light_main_entity, Some(camera_main_entity.into()), 0); + commands.entity(view_light_entity).insert(( ShadowView { depth_attachment, pass_name: format!("shadow pass spot light {light_index}"), }, ExtractedView { + retained_view_entity, viewport: UVec4::new( 0, 0, @@ -1360,15 +1392,15 @@ pub fn prepare_lights( if first { // Subsequent views with the same light entity will reuse the same shadow map - shadow_render_phases.insert_or_clear(view_light_entity, gpu_preprocessing_mode); - live_shadow_mapping_lights.insert(view_light_entity); + shadow_render_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); } } // directional lights let mut directional_depth_texture_array_index = 0u32; let view_layers = maybe_layers.unwrap_or_default(); - for (light_index, &(light_entity, light)) in directional_lights + for (light_index, &(light_entity, light_main_entity, light)) in directional_lights .iter() .enumerate() .take(MAX_DIRECTIONAL_LIGHTS) @@ -1460,6 +1492,12 @@ pub fn prepare_lights( frustum.half_spaces[4] = HalfSpace::new(frustum.half_spaces[4].normal().extend(f32::INFINITY)); + let retained_view_entity = RetainedViewEntity::new( + *light_main_entity, + Some(camera_main_entity.into()), + cascade_index as u32, + ); + commands.entity(view_light_entity).insert(( ShadowView { depth_attachment, @@ -1468,6 +1506,7 @@ pub fn prepare_lights( ), }, ExtractedView { + retained_view_entity, viewport: UVec4::new( 0, 0, @@ -1496,8 +1535,8 @@ pub fn prepare_lights( // Subsequent views with the same light entity will **NOT** reuse the same shadow map // (Because the cascades are unique to each view) // TODO: Implement GPU culling for shadow passes. - shadow_render_phases.insert_or_clear(view_light_entity, gpu_preprocessing_mode); - live_shadow_mapping_lights.insert(view_light_entity); + shadow_render_phases.insert_or_clear(retained_view_entity, gpu_preprocessing_mode); + live_shadow_mapping_lights.insert(retained_view_entity); } } @@ -1543,15 +1582,12 @@ fn despawn_entities(commands: &mut Commands, entities: Vec) { /// For each shadow cascade, iterates over all the meshes "visible" from it and /// adds them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as /// appropriate. -#[allow(clippy::too_many_arguments)] pub fn queue_shadows( shadow_draw_functions: Res>, prepass_pipeline: Res>, - (render_meshes, render_mesh_instances): ( + (render_meshes, render_mesh_instances, render_materials, render_material_instances): ( Res>, Res, - ), - (render_materials, render_material_instances): ( Res>>, Res>, ), @@ -1560,9 +1596,10 @@ pub fn queue_shadows( mut pipelines: ResMut>>, pipeline_cache: Res, render_lightmaps: Res, + gpu_preprocessing_support: Res, mesh_allocator: Res, - view_lights: Query<(Entity, &ViewLightEntities)>, - view_light_entities: Query<&LightEntity>, + view_lights: Query<(Entity, &ViewLightEntities), With>, + view_light_entities: Query<(&LightEntity, &ExtractedView)>, point_light_entities: Query<&RenderCubemapVisibleEntities, With>, directional_light_entities: Query< &RenderCascadesVisibleEntities, @@ -1575,10 +1612,14 @@ pub fn queue_shadows( for (entity, view_lights) in &view_lights { let draw_shadow_mesh = shadow_draw_functions.read().id::>(); for view_light_entity in view_lights.lights.iter().copied() { - let Ok(light_entity) = view_light_entities.get(view_light_entity) else { + let Ok((light_entity, extracted_view_light)) = + view_light_entities.get(view_light_entity) + else { continue; }; - let Some(shadow_phase) = shadow_render_phases.get_mut(&view_light_entity) else { + let Some(shadow_phase) = + shadow_render_phases.get_mut(&extracted_view_light.retained_view_entity) + else { continue; }; @@ -1681,18 +1722,23 @@ pub fn queue_shadows( let (vertex_slab, index_slab) = mesh_allocator.mesh_slabs(&mesh_instance.mesh_asset_id); + let batch_set_key = ShadowBatchSetKey { + pipeline: pipeline_id, + draw_function: draw_shadow_mesh, + vertex_slab: vertex_slab.unwrap_or_default(), + index_slab, + }; + shadow_phase.add( + batch_set_key, ShadowBinKey { - batch_set_key: ShadowBatchSetKey { - pipeline: pipeline_id, - draw_function: draw_shadow_mesh, - vertex_slab: vertex_slab.unwrap_or_default(), - index_slab, - }, asset_id: mesh_instance.mesh_asset_id.into(), }, (entity, main_entity), - BinnedRenderPhaseType::mesh(mesh_instance.should_batch()), + BinnedRenderPhaseType::mesh( + mesh_instance.should_batch(), + &gpu_preprocessing_support, + ), ); } } @@ -1700,7 +1746,13 @@ pub fn queue_shadows( } pub struct Shadow { - pub key: ShadowBinKey, + /// Determines which objects can be placed into a *batch set*. + /// + /// Objects in a single batch set can potentially be multi-drawn together, + /// if it's enabled and the current platform supports it. + pub batch_set_key: ShadowBatchSetKey, + /// Information that separates items into bins. + pub bin_key: ShadowBinKey, pub representative_entity: (Entity, MainEntity), pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, @@ -1731,27 +1783,19 @@ pub struct ShadowBatchSetKey { pub index_slab: Option, } +impl PhaseItemBatchSetKey for ShadowBatchSetKey { + fn indexed(&self) -> bool { + self.index_slab.is_some() + } +} + /// Data used to bin each object in the shadow map phase. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ShadowBinKey { - /// The key of the *batch set*. - /// - /// As batches belong to a batch set, meshes in a batch must obviously be - /// able to be placed in a single batch set. - pub batch_set_key: ShadowBatchSetKey, - /// The object. pub asset_id: UntypedAssetId, } -impl PhaseItemBinKey for ShadowBinKey { - type BatchSetKey = ShadowBatchSetKey; - - fn get_batch_set_key(&self) -> Option { - Some(self.batch_set_key.clone()) - } -} - impl PhaseItem for Shadow { #[inline] fn entity(&self) -> Entity { @@ -1764,7 +1808,7 @@ impl PhaseItem for Shadow { #[inline] fn draw_function(&self) -> DrawFunctionId { - self.key.batch_set_key.draw_function + self.batch_set_key.draw_function } #[inline] @@ -1789,17 +1833,20 @@ impl PhaseItem for Shadow { } impl BinnedPhaseItem for Shadow { + type BatchSetKey = ShadowBatchSetKey; type BinKey = ShadowBinKey; #[inline] fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self { Shadow { - key, + batch_set_key, + bin_key, representative_entity, batch_range, extra_index, @@ -1810,13 +1857,13 @@ impl BinnedPhaseItem for Shadow { impl CachedRenderPipelinePhaseItem for Shadow { #[inline] fn cached_pipeline(&self) -> CachedRenderPipelineId { - self.key.batch_set_key.pipeline + self.batch_set_key.pipeline } } pub struct ShadowPassNode { main_view_query: QueryState>, - view_light_query: QueryState>, + view_light_query: QueryState<(Read, Read)>, } impl ShadowPassNode { @@ -1853,14 +1900,17 @@ impl Node for ShadowPassNode { if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) { for view_light_entity in view_lights.lights.iter().copied() { - let Some(shadow_phase) = shadow_render_phases.get(&view_light_entity) else { + let Ok((view_light, extracted_light_view)) = + self.view_light_query.get_manual(world, view_light_entity) + else { continue; }; - let view_light = self - .view_light_query - .get_manual(world, view_light_entity) - .unwrap(); + let Some(shadow_phase) = + shadow_render_phases.get(&extracted_light_view.retained_view_entity) + else { + continue; + }; let depth_stencil_attachment = Some(view_light.depth_attachment.get_attachment(StoreOp::Store)); diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 30e2a13c56ef4..dd92ef7828eec 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -20,7 +20,8 @@ use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; use bevy_render::{ batching::{ gpu_preprocessing::{ - self, GpuPreprocessingSupport, IndirectParameters, IndirectParametersBuffer, + self, GpuPreprocessingSupport, IndirectBatchSet, IndirectParametersBuffers, + IndirectParametersIndexed, IndirectParametersMetadata, IndirectParametersNonIndexed, InstanceInputUniformBuffer, }, no_gpu_preprocessing, GetBatchData, GetFullBatchData, NoAutomaticBatching, @@ -34,23 +35,19 @@ use bevy_render::{ RenderCommandResult, SortedRenderPhasePlugin, TrackedRenderPass, }, render_resource::*, - renderer::{RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderDevice, RenderQueue}, texture::DefaultImageSampler, view::{ - prepare_view_targets, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, - ViewTarget, ViewUniformOffset, ViewVisibility, VisibilityRange, + NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, + ViewVisibility, VisibilityRange, }, Extract, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::{ - default, - hashbrown::hash_map::Entry, - tracing::{error, warn}, - HashMap, Parallel, -}; +use bevy_utils::{default, hashbrown::hash_map::Entry, HashMap, Parallel}; use material_bind_groups::MaterialBindingId; use render::skin::{self, SkinIndex}; +use tracing::{error, warn}; use crate::{ render::{ @@ -137,6 +134,10 @@ impl Plugin for MeshRenderPlugin { load_internal_asset!(app, SKINNING_HANDLE, "skinning.wgsl", Shader::from_wgsl); load_internal_asset!(app, MORPH_HANDLE, "morph.wgsl", Shader::from_wgsl); + if app.get_sub_app(RenderApp).is_none() { + return; + } + app.add_systems( PostUpdate, (no_automatic_skin_batching, no_automatic_morph_batching), @@ -217,8 +218,7 @@ impl Plugin for MeshRenderPlugin { gpu_preprocessing::write_batched_instance_buffers:: .in_set(RenderSet::PrepareResourcesFlush), gpu_preprocessing::delete_old_work_item_buffers:: - .in_set(RenderSet::ManageViews) - .after(prepare_view_targets), + .in_set(RenderSet::PrepareResources), collect_meshes_for_gpu_building .in_set(RenderSet::PrepareAssets) .after(allocator::allocate_and_free_meshes) @@ -353,6 +353,17 @@ pub struct MeshInputUniform { /// [`MeshAllocator`]). This value stores the offset of the first vertex in /// this mesh in that buffer. pub first_vertex_index: u32, + /// The index of this mesh's first index in the index buffer, if any. + /// + /// Multiple meshes can be packed into a single index buffer (see + /// [`MeshAllocator`]). This value stores the offset of the first index in + /// this mesh in that buffer. + /// + /// If this mesh isn't indexed, this value is ignored. + pub first_index_index: u32, + /// For an indexed mesh, the number of indices that make it up; for a + /// non-indexed mesh, the number of vertices in it. + pub index_count: u32, /// The current skin index, or `u32::MAX` if there's no skin. pub current_skin_index: u32, /// The previous skin index, or `u32::MAX` if there's no previous skin. @@ -362,6 +373,10 @@ pub struct MeshInputUniform { /// Low 16 bits: index of the material inside the bind group data. /// High 16 bits: index of the lightmap in the binding array. pub material_and_lightmap_bind_group_slot: u32, + /// Padding. + pub pad_a: u32, + /// Padding. + pub pad_b: u32, } /// Information about each mesh instance needed to cull it on GPU. @@ -897,7 +912,6 @@ impl RenderMeshInstanceGpuQueue { impl RenderMeshInstanceGpuBuilder { /// Flushes this mesh instance to the [`RenderMeshInstanceGpu`] and /// [`MeshInputUniform`] tables, replacing the existing entry if applicable. - #[allow(clippy::too_many_arguments)] fn update( mut self, entity: MainEntity, @@ -909,11 +923,23 @@ impl RenderMeshInstanceGpuBuilder { render_lightmaps: &RenderLightmaps, skin_indices: &SkinIndices, ) -> u32 { - let first_vertex_index = match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) - { - Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, - None => 0, - }; + let (first_vertex_index, vertex_count) = + match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) { + Some(mesh_vertex_slice) => ( + mesh_vertex_slice.range.start, + mesh_vertex_slice.range.end - mesh_vertex_slice.range.start, + ), + None => (0, 0), + }; + let (mesh_is_indexed, first_index_index, index_count) = + match mesh_allocator.mesh_index_slice(&self.shared.mesh_asset_id) { + Some(mesh_index_slice) => ( + true, + mesh_index_slice.range.start, + mesh_index_slice.range.end - mesh_index_slice.range.start, + ), + None => (false, 0, 0), + }; let current_skin_index = match skin_indices.current.get(&entity) { Some(skin_indices) => skin_indices.index(), @@ -940,11 +966,19 @@ impl RenderMeshInstanceGpuBuilder { flags: self.mesh_flags.bits(), previous_input_index: u32::MAX, first_vertex_index, + first_index_index, + index_count: if mesh_is_indexed { + index_count + } else { + vertex_count + }, current_skin_index, previous_skin_index, material_and_lightmap_bind_group_slot: u32::from( self.shared.material_bindings_index.slot, ) | ((lightmap_slot as u32) << 16), + pad_a: 0, + pad_b: 0, }; // Did the last frame contain this entity as well? @@ -958,7 +992,8 @@ impl RenderMeshInstanceGpuBuilder { // Save the old mesh input uniform. The mesh preprocessing // shader will need it to compute motion vectors. - let previous_mesh_input_uniform = current_input_buffer.get(current_uniform_index); + let previous_mesh_input_uniform = + current_input_buffer.get_unchecked(current_uniform_index); let previous_input_index = previous_input_buffer.add(previous_mesh_input_uniform); mesh_input_uniform.previous_input_index = previous_input_index; @@ -1170,7 +1205,6 @@ pub fn extract_meshes_for_cpu_building( /// /// This is the variant of the system that runs when we're using GPU /// [`MeshUniform`] building. -#[allow(clippy::too_many_arguments)] pub fn extract_meshes_for_gpu_building( mut render_mesh_instances: ResMut, render_visibility_ranges: Res, @@ -1351,7 +1385,6 @@ fn set_mesh_motion_vector_flags( /// Creates the [`RenderMeshInstanceGpu`]s and [`MeshInputUniform`]s when GPU /// mesh uniforms are built. -#[allow(clippy::too_many_arguments)] pub fn collect_meshes_for_gpu_building( render_mesh_instances: ResMut, batched_instance_buffers: ResMut< @@ -1484,11 +1517,12 @@ impl FromWorld for MeshPipeline { fn from_world(world: &mut World) -> Self { let mut system_state: SystemState<( Res, + Res, Res, Res, Res, )> = SystemState::new(world); - let (render_device, default_sampler, render_queue, view_layouts) = + let (render_device, render_adapter, default_sampler, render_queue, view_layouts) = system_state.get_mut(world); let clustered_forward_buffer_binding_type = render_device @@ -1532,9 +1566,9 @@ impl FromWorld for MeshPipeline { view_layouts: view_layouts.clone(), clustered_forward_buffer_binding_type, dummy_white_gpu_image, - mesh_layouts: MeshLayouts::new(&render_device), + mesh_layouts: MeshLayouts::new(&render_device, &render_adapter), per_object_buffer_batch_size: GpuArrayBuffer::::batch_size(&render_device), - binding_arrays_are_usable: binding_arrays_are_usable(&render_device), + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), skins_use_uniform_buffers: skin::skins_use_uniform_buffers(&render_device), } } @@ -1627,7 +1661,7 @@ impl GetFullBatchData for MeshPipeline { fn get_index_and_compare_data( (mesh_instances, lightmaps, _, _, _): &SystemParamItem, - (_entity, main_entity): (Entity, MainEntity), + main_entity: MainEntity, ) -> Option<(NonMaxU32, Option)> { // This should only be called during GPU building. let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { @@ -1653,7 +1687,7 @@ impl GetFullBatchData for MeshPipeline { fn get_binned_batch_data( (mesh_instances, lightmaps, _, mesh_allocator, skin_indices): &SystemParamItem, - (_entity, main_entity): (Entity, MainEntity), + main_entity: MainEntity, ) -> Option { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { error!( @@ -1684,7 +1718,7 @@ impl GetFullBatchData for MeshPipeline { fn get_binned_index( (mesh_instances, _, _, _, _): &SystemParamItem, - (_entity, main_entity): (Entity, MainEntity), + main_entity: MainEntity, ) -> Option { // This should only be called during GPU building. let RenderMeshInstances::GpuBuilding(ref mesh_instances) = **mesh_instances else { @@ -1700,76 +1734,31 @@ impl GetFullBatchData for MeshPipeline { .map(|entity| entity.current_uniform_index) } - fn get_batch_indirect_parameters_index( - (mesh_instances, _, meshes, mesh_allocator, _): &SystemParamItem, - indirect_parameters_buffer: &mut IndirectParametersBuffer, - entity: (Entity, MainEntity), - instance_index: u32, - ) -> Option { - get_batch_indirect_parameters_index( - mesh_instances, - meshes, - mesh_allocator, - indirect_parameters_buffer, - entity, - instance_index, - ) - } -} - -/// Pushes a set of [`IndirectParameters`] onto the [`IndirectParametersBuffer`] -/// for the given mesh instance, and returns the index of those indirect -/// parameters. -fn get_batch_indirect_parameters_index( - mesh_instances: &RenderMeshInstances, - meshes: &RenderAssets, - mesh_allocator: &MeshAllocator, - indirect_parameters_buffer: &mut IndirectParametersBuffer, - (_entity, main_entity): (Entity, MainEntity), - instance_index: u32, -) -> Option { - // This should only be called during GPU building. - let RenderMeshInstances::GpuBuilding(ref mesh_instances) = *mesh_instances else { - error!( - "`get_batch_indirect_parameters_index` should never be called in CPU mesh uniform \ - building mode" - ); - return None; - }; - - let mesh_instance = mesh_instances.get(&main_entity)?; - let mesh = meshes.get(mesh_instance.mesh_asset_id)?; - let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)?; - - // Note that `IndirectParameters` covers both of these structures, even - // though they actually have distinct layouts. See the comment above that - // type for more information. - let indirect_parameters = match mesh.buffer_info { - RenderMeshBufferInfo::Indexed { - count: index_count, .. - } => { - let index_buffer_slice = - mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)?; - IndirectParameters { - vertex_or_index_count: index_count, - instance_count: 0, - first_vertex_or_first_index: index_buffer_slice.range.start, - base_vertex_or_first_instance: vertex_buffer_slice.range.start, - first_instance: instance_index, - } - } - RenderMeshBufferInfo::NonIndexed => IndirectParameters { - vertex_or_index_count: mesh.vertex_count, + fn write_batch_indirect_parameters_metadata( + mesh_index: u32, + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + indirect_parameters_buffer: &mut IndirectParametersBuffers, + indirect_parameters_offset: u32, + ) { + let indirect_parameters = IndirectParametersMetadata { + mesh_index, + base_output_index, + batch_set_index: match batch_set_index { + Some(batch_set_index) => u32::from(batch_set_index), + None => !0, + }, instance_count: 0, - first_vertex_or_first_index: vertex_buffer_slice.range.start, - base_vertex_or_first_instance: instance_index, - first_instance: instance_index, - }, - }; + }; - (indirect_parameters_buffer.push(indirect_parameters) as u32) - .try_into() - .ok() + if indexed { + indirect_parameters_buffer.set_indexed(indirect_parameters_offset, indirect_parameters); + } else { + indirect_parameters_buffer + .set_non_indexed(indirect_parameters_offset, indirect_parameters); + } + } } bitflags::bitflags! { @@ -1802,12 +1791,13 @@ bitflags::bitflags! { const TEMPORAL_JITTER = 1 << 11; const READS_VIEW_TRANSMISSION_TEXTURE = 1 << 12; const LIGHTMAPPED = 1 << 13; - const IRRADIANCE_VOLUME = 1 << 14; - const VISIBILITY_RANGE_DITHER = 1 << 15; - const SCREEN_SPACE_REFLECTIONS = 1 << 16; - const HAS_PREVIOUS_SKIN = 1 << 17; - const HAS_PREVIOUS_MORPH = 1 << 18; - const OIT_ENABLED = 1 << 19; + const LIGHTMAP_BICUBIC_SAMPLING = 1 << 14; + const IRRADIANCE_VOLUME = 1 << 15; + const VISIBILITY_RANGE_DITHER = 1 << 16; + const SCREEN_SPACE_REFLECTIONS = 1 << 17; + const HAS_PREVIOUS_SKIN = 1 << 18; + const HAS_PREVIOUS_MORPH = 1 << 19; + const OIT_ENABLED = 1 << 20; const LAST_FLAG = Self::OIT_ENABLED.bits(); // Bitfields @@ -2231,6 +2221,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if key.contains(MeshPipelineKey::LIGHTMAPPED) { shader_defs.push("LIGHTMAP".into()); } + if key.contains(MeshPipelineKey::LIGHTMAP_BICUBIC_SAMPLING) { + shader_defs.push("LIGHTMAP_BICUBIC_SAMPLING".into()); + } if key.contains(MeshPipelineKey::TEMPORAL_JITTER) { shader_defs.push("TEMPORAL_JITTER".into()); @@ -2400,7 +2393,6 @@ impl MeshBindGroupPair { } } -#[allow(clippy::too_many_arguments)] pub fn prepare_mesh_bind_group( meshes: Res>, mut groups: ResMut, @@ -2676,12 +2668,12 @@ impl RenderCommand

for DrawMesh { type Param = ( SRes>, SRes, - SRes, + SRes, SRes, SRes, Option>, ); - type ViewQuery = Has; + type ViewQuery = Has; type ItemQuery = (); #[inline] fn render<'w>( @@ -2724,26 +2716,6 @@ impl RenderCommand

for DrawMesh { return RenderCommandResult::Skip; }; - // Calculate the indirect offset, and look up the buffer. - let indirect_parameters = match item.extra_index() { - PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => None, - PhaseItemExtraIndex::IndirectParametersIndex(indices) => { - match indirect_parameters_buffer.buffer() { - None => { - warn!( - "Not rendering mesh because indirect parameters buffer wasn't present" - ); - return RenderCommandResult::Skip; - } - Some(buffer) => Some(( - indices.start as u64 * size_of::() as u64, - indices.end - indices.start, - buffer, - )), - } - } - }; - pass.set_vertex_buffer(0, vertex_buffer_slice.buffer.slice(..)); let batch_range = item.batch_range(); @@ -2763,8 +2735,8 @@ impl RenderCommand

for DrawMesh { pass.set_index_buffer(index_buffer_slice.buffer.slice(..), 0, *index_format); - match indirect_parameters { - None => { + match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { pass.draw_indexed( index_buffer_slice.range.start ..(index_buffer_slice.range.start + *count), @@ -2772,33 +2744,112 @@ impl RenderCommand

for DrawMesh { batch_range.clone(), ); } - Some(( - indirect_parameters_offset, - indirect_parameters_count, - indirect_parameters_buffer, - )) => { - pass.multi_draw_indexed_indirect( - indirect_parameters_buffer, - indirect_parameters_offset, - indirect_parameters_count, - ); + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as + // the buffer we're going to use for + // `multi_draw_indexed_indirect_count` (if available). + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + indirect_parameters_buffer.indexed_data_buffer(), + indirect_parameters_buffer.indexed_batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because indexed indirect parameters buffer \ + wasn't present", + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters + // within the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in + // the batch sets buffer. Otherwise, supply the size of + // the batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = u32::from(batch_set_index) + * (size_of::() as u32); + pass.multi_draw_indexed_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indexed_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } } } } - RenderMeshBufferInfo::NonIndexed => match indirect_parameters { - None => { + + RenderMeshBufferInfo::NonIndexed => match item.extra_index() { + PhaseItemExtraIndex::None | PhaseItemExtraIndex::DynamicOffset(_) => { pass.draw(vertex_buffer_slice.range, batch_range.clone()); } - Some(( - indirect_parameters_offset, - indirect_parameters_count, - indirect_parameters_buffer, - )) => { - pass.multi_draw_indirect( - indirect_parameters_buffer, - indirect_parameters_offset, - indirect_parameters_count, - ); + PhaseItemExtraIndex::IndirectParametersIndex { + range: indirect_parameters_range, + batch_set_index, + } => { + // Look up the indirect parameters buffer, as well as the + // buffer we're going to use for + // `multi_draw_indirect_count` (if available). + let (Some(indirect_parameters_buffer), Some(batch_sets_buffer)) = ( + indirect_parameters_buffer.non_indexed_data_buffer(), + indirect_parameters_buffer.non_indexed_batch_sets_buffer(), + ) else { + warn!( + "Not rendering mesh because non-indexed indirect parameters buffer \ + wasn't present" + ); + return RenderCommandResult::Skip; + }; + + // Calculate the location of the indirect parameters within + // the buffer. + let indirect_parameters_offset = indirect_parameters_range.start as u64 + * size_of::() as u64; + let indirect_parameters_count = + indirect_parameters_range.end - indirect_parameters_range.start; + + // If we're using `multi_draw_indirect_count`, take the + // number of batches from the appropriate position in the + // batch sets buffer. Otherwise, supply the size of the + // batch set. + match batch_set_index { + Some(batch_set_index) => { + let count_offset = + u32::from(batch_set_index) * (size_of::() as u32); + pass.multi_draw_indirect_count( + indirect_parameters_buffer, + indirect_parameters_offset, + batch_sets_buffer, + count_offset as u64, + indirect_parameters_count, + ); + } + None => { + pass.multi_draw_indirect( + indirect_parameters_buffer, + indirect_parameters_offset, + indirect_parameters_count, + ); + } + } } }, } diff --git a/crates/bevy_pbr/src/render/mesh_bindings.rs b/crates/bevy_pbr/src/render/mesh_bindings.rs index 3e3210a026325..51b28389dcd0c 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_bindings.rs @@ -1,7 +1,11 @@ //! Bind group layout related definitions for the mesh pipeline. use bevy_math::Mat4; -use bevy_render::{mesh::morph::MAX_MORPH_WEIGHTS, render_resource::*, renderer::RenderDevice}; +use bevy_render::{ + mesh::morph::MAX_MORPH_WEIGHTS, + render_resource::*, + renderer::{RenderAdapter, RenderDevice}, +}; use crate::{binding_arrays_are_usable, render::skin::MAX_JOINTS, LightmapSlab}; @@ -194,10 +198,10 @@ impl MeshLayouts { /// Prepare the layouts used by the default bevy [`Mesh`]. /// /// [`Mesh`]: bevy_render::prelude::Mesh - pub fn new(render_device: &RenderDevice) -> Self { + pub fn new(render_device: &RenderDevice, render_adapter: &RenderAdapter) -> Self { MeshLayouts { model_only: Self::model_only_layout(render_device), - lightmapped: Self::lightmapped_layout(render_device), + lightmapped: Self::lightmapped_layout(render_device, render_adapter), skinned: Self::skinned_layout(render_device), skinned_motion: Self::skinned_motion_layout(render_device), morphed: Self::morphed_layout(render_device), @@ -329,8 +333,11 @@ impl MeshLayouts { ) } - fn lightmapped_layout(render_device: &RenderDevice) -> BindGroupLayout { - if binding_arrays_are_usable(render_device) { + fn lightmapped_layout( + render_device: &RenderDevice, + render_adapter: &RenderAdapter, + ) -> BindGroupLayout { + if binding_arrays_are_usable(render_device, render_adapter) { render_device.create_bind_group_layout( "lightmapped_mesh_layout", &BindGroupLayoutEntries::with_indices( @@ -488,7 +495,6 @@ impl MeshLayouts { } /// Creates the bind group for meshes with skins and morph targets. - #[allow(clippy::too_many_arguments)] pub fn morphed_skinned( &self, render_device: &RenderDevice, @@ -516,7 +522,6 @@ impl MeshLayouts { /// [`MeshLayouts::morphed_motion`] above for more information about the /// `current_skin`, `prev_skin`, `current_weights`, and `prev_weights` /// buffers. - #[allow(clippy::too_many_arguments)] pub fn morphed_skinned_motion( &self, render_device: &RenderDevice, diff --git a/crates/bevy_pbr/src/render/mesh_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_bindings.wgsl index 2366dab155623..62b967c56f1b9 100644 --- a/crates/bevy_pbr/src/render/mesh_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_bindings.wgsl @@ -2,8 +2,10 @@ #import bevy_pbr::mesh_types::Mesh +#ifndef MESHLET_MESH_MATERIAL_PASS #ifdef PER_OBJECT_BUFFER_BATCH_SIZE @group(1) @binding(0) var mesh: array; #else @group(1) @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_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index b58004cadf1e9..23857bc6aa12d 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -12,6 +12,7 @@ } #import bevy_render::maths::{affine3_to_square, mat2x4_f32_to_mat3x3_unpack} +#ifndef MESHLET_MESH_MATERIAL_PASS fn get_world_from_local(instance_index: u32) -> mat4x4 { return affine3_to_square(mesh[instance_index].world_from_local); @@ -21,6 +22,8 @@ fn get_previous_world_from_local(instance_index: u32) -> mat4x4 { return affine3_to_square(mesh[instance_index].previous_world_from_local); } +#endif // MESHLET_MESH_MATERIAL_PASS + fn mesh_position_local_to_world(world_from_local: mat4x4, vertex_position: vec4) -> vec4 { return world_from_local * vertex_position; } @@ -33,6 +36,8 @@ fn mesh_position_local_to_clip(world_from_local: mat4x4, vertex_position: v return position_world_to_clip(world_position.xyz); } +#ifndef MESHLET_MESH_MATERIAL_PASS + fn mesh_normal_local_to_world(vertex_normal: vec3, instance_index: u32) -> vec3 { // NOTE: The mikktspace method of normal mapping requires that the world normal is // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents @@ -53,6 +58,8 @@ fn mesh_normal_local_to_world(vertex_normal: vec3, instance_index: u32) -> } } +#endif // MESHLET_MESH_MATERIAL_PASS + // Calculates the sign of the determinant of the 3x3 model matrix based on a // mesh flag fn sign_determinant_model_3x3m(mesh_flags: u32) -> f32 { @@ -62,6 +69,8 @@ fn sign_determinant_model_3x3m(mesh_flags: u32) -> f32 { return f32(bool(mesh_flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0; } +#ifndef MESHLET_MESH_MATERIAL_PASS + fn mesh_tangent_local_to_world(world_from_local: mat4x4, vertex_tangent: vec4, instance_index: u32) -> vec4 { // NOTE: The mikktspace method of normal mapping requires that the world tangent is // re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents @@ -88,6 +97,8 @@ fn mesh_tangent_local_to_world(world_from_local: mat4x4, vertex_tangent: ve } } +#endif // MESHLET_MESH_MATERIAL_PASS + // Returns an appropriate dither level for the current mesh instance. // // This looks up the LOD range in the `visibility_ranges` table and compares the diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 6e19f6b8004b0..df73454a3e880 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -8,28 +8,10 @@ // so that TAA works. #import bevy_pbr::mesh_types::{Mesh, MESH_FLAGS_NO_FRUSTUM_CULLING_BIT} +#import bevy_pbr::mesh_preprocess_types::{MeshInput, IndirectParametersMetadata} #import bevy_render::maths #import bevy_render::view::View -// Per-frame data that the CPU supplies to the GPU. -struct MeshInput { - // The model transform. - world_from_local: mat3x4, - // The lightmap UV rect, packed into 64 bits. - lightmap_uv_rect: vec2, - // Various flags. - flags: u32, - // The index of this mesh's `MeshInput` in the `previous_input` array, if - // applicable. If not present, this is `u32::MAX`. - previous_input_index: u32, - first_vertex_index: u32, - current_skin_index: u32, - previous_skin_index: u32, - // Low 16 bits: index of the material inside the bind group data. - // High 16 bits: index of the lightmap in the binding array. - material_and_lightmap_bind_group_slot: u32, -} - // Information about each mesh instance needed to cull it on GPU. // // At the moment, this just consists of its axis-aligned bounding box (AABB). @@ -47,26 +29,11 @@ struct PreprocessWorkItem { // The index of the `MeshInput` in the `current_input` buffer that we read // from. input_index: u32, - // In direct mode, the index of the `Mesh` in `output` that we write to. In - // indirect mode, the index of the `IndirectParameters` in - // `indirect_parameters` that we write to. + // The index of the `Mesh` in `output` that we write to. output_index: u32, -} - -// The `wgpu` indirect parameters structure. This is a union of two structures. -// For more information, see the corresponding comment in -// `gpu_preprocessing.rs`. -struct IndirectParameters { - // `vertex_count` or `index_count`. - data0: u32, - // `instance_count` in both structures. - instance_count: atomic, - // `first_vertex` in both structures. - first_vertex: u32, - // `first_instance` or `base_vertex`. - data1: u32, - // A read-only copy of `instance_index`. - instance_index: u32, + // The index of the `IndirectParameters` in `indirect_parameters` that we + // write to. + indirect_parameters_index: u32, } // The current frame's `MeshInput`. @@ -82,7 +49,8 @@ struct IndirectParameters { #ifdef INDIRECT // The array of indirect parameters for drawcalls. -@group(0) @binding(4) var indirect_parameters: array; +@group(0) @binding(4) var indirect_parameters_metadata: + array; #endif #ifdef FRUSTUM_CULLING @@ -138,9 +106,12 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { return; } - // Unpack. + // Unpack the work item. let input_index = work_items[instance_index].input_index; let output_index = work_items[instance_index].output_index; + let indirect_parameters_index = work_items[instance_index].indirect_parameters_index; + + // Unpack the input matrix. let world_from_local_affine_transpose = current_input[input_index].world_from_local; let world_from_local = maths::affine3_to_square(world_from_local_affine_transpose); @@ -178,14 +149,18 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { } // Figure out the output index. In indirect mode, this involves bumping the - // instance index in the indirect parameters structure. Otherwise, this - // index was directly supplied to us. + // instance index in the indirect parameters metadata, which + // `build_indirect_params.wgsl` will use to generate the actual indirect + // parameters. Otherwise, this index was directly supplied to us. #ifdef INDIRECT - let mesh_output_index = indirect_parameters[output_index].instance_index + - atomicAdd(&indirect_parameters[output_index].instance_count, 1u); -#else + let batch_output_index = + atomicAdd(&indirect_parameters_metadata[indirect_parameters_index].instance_count, 1u); + let mesh_output_index = + indirect_parameters_metadata[indirect_parameters_index].base_output_index + + batch_output_index; +#else // INDIRECT let mesh_output_index = output_index; -#endif +#endif // INDIRECT // Write the output. output[mesh_output_index].world_from_local = world_from_local_affine_transpose; diff --git a/crates/bevy_pbr/src/render/mesh_preprocess_types.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess_types.wgsl new file mode 100644 index 0000000000000..974a9d303aa6d --- /dev/null +++ b/crates/bevy_pbr/src/render/mesh_preprocess_types.wgsl @@ -0,0 +1,98 @@ +// Types needed for GPU mesh uniform building. + +#define_import_path bevy_pbr::mesh_preprocess_types + +// Per-frame data that the CPU supplies to the GPU. +struct MeshInput { + // The model transform. + world_from_local: mat3x4, + // The lightmap UV rect, packed into 64 bits. + lightmap_uv_rect: vec2, + // A set of bitflags corresponding to `MeshFlags` on the Rust side. See the + // `MESH_FLAGS_` flags in `mesh_types.wgsl` for a list of these. + flags: u32, + // The index of this mesh's `MeshInput` in the `previous_input` array, if + // applicable. If not present, this is `u32::MAX`. + previous_input_index: u32, + // The index of the first vertex in the vertex slab. + first_vertex_index: u32, + // The index of the first vertex index in the index slab. + // + // If this mesh isn't indexed, this value is ignored. + first_index_index: u32, + // For indexed meshes, the number of indices that this mesh has; for + // non-indexed meshes, the number of vertices that this mesh consists of. + index_count: u32, + current_skin_index: u32, + previous_skin_index: u32, + // Low 16 bits: index of the material inside the bind group data. + // High 16 bits: index of the lightmap in the binding array. + material_and_lightmap_bind_group_slot: u32, +} + +// The `wgpu` indirect parameters structure for indexed meshes. +// +// The `build_indirect_params.wgsl` shader generates these. +struct IndirectParametersIndexed { + // The number of indices that this mesh has. + index_count: u32, + // The number of instances we are to draw. + instance_count: u32, + // The offset of the first index for this mesh in the index buffer slab. + first_index: u32, + // The offset of the first vertex for this mesh in the vertex buffer slab. + base_vertex: u32, + // The index of the first mesh instance in the `Mesh` buffer. + first_instance: u32, +} + +// The `wgpu` indirect parameters structure for non-indexed meshes. +// +// The `build_indirect_params.wgsl` shader generates these. +struct IndirectParametersNonIndexed { + // The number of vertices that this mesh has. + vertex_count: u32, + // The number of instances we are to draw. + instance_count: u32, + // The offset of the first vertex for this mesh in the vertex buffer slab. + base_vertex: u32, + // The index of the first mesh instance in the `Mesh` buffer. + first_instance: u32, +} + +// Information needed to generate the `IndirectParametersIndexed` and +// `IndirectParametersNonIndexed` draw commands. +struct IndirectParametersMetadata { + // The index of the mesh in the `MeshInput` buffer. + mesh_index: u32, + // The index of the first instance corresponding to this batch in the `Mesh` + // buffer. + base_output_index: u32, + // The index of the batch set in the `IndirectBatchSet` buffer. + batch_set_index: u32, + // The number of instances that are to be drawn. + // + // The `mesh_preprocess.wgsl` shader determines this, and the + // `build_indirect_params.wgsl` shader copies this value into the indirect + // draw command. + instance_count: atomic, +} + +// Information about each batch set. +// +// A *batch set* is a set of meshes that might be multi-drawn together. +// +// The CPU creates this structure, and the `build_indirect_params.wgsl` shader +// modifies it. If `multi_draw_indirect_count` is in use, the GPU reads this +// value when multi-drawing a batch set in order to determine how many commands +// make up the batch set. +struct IndirectBatchSet { + // The number of commands that make up this batch set. + // + // The CPU initializes this value to zero. The `build_indirect_params.wgsl` + // shader increments this value as it processes batches. + indirect_parameters_count: atomic, + // The offset of the first batch corresponding to this batch set within the + // `IndirectParametersIndexed` or `IndirectParametersNonIndexed` arrays. + indirect_parameters_base: u32, +} diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 8067680eff923..385c942c46835 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -21,7 +21,7 @@ use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, render_asset::RenderAssets, render_resource::{binding_types::*, *}, - renderer::RenderDevice, + renderer::{RenderAdapter, RenderDevice}, texture::{FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, view::{ Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, @@ -29,16 +29,8 @@ use bevy_render::{ }, }; use core::{array, num::NonZero}; - -#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] -use bevy_render::render_resource::binding_types::texture_cube; -use bevy_render::renderer::RenderAdapter; -#[cfg(debug_assertions)] -use bevy_utils::warn_once; use environment_map::EnvironmentMapLight; -#[cfg(debug_assertions)] -use crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES; use crate::{ environment_map::{self, RenderViewEnvironmentMapBindGroupEntries}, irradiance_volume::{ @@ -52,6 +44,12 @@ use crate::{ ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; +#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] +use bevy_render::render_resource::binding_types::texture_cube; + +#[cfg(debug_assertions)] +use {crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES, bevy_utils::once, tracing::warn}; + #[derive(Clone)] pub struct MeshPipelineViewLayout { pub bind_group_layout: BindGroupLayout, @@ -312,7 +310,8 @@ fn layout_entries( ); // EnvironmentMapLight - let environment_map_entries = environment_map::get_bind_group_layout_entries(render_device); + 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]), @@ -323,7 +322,7 @@ fn layout_entries( // Irradiance volumes if IRRADIANCE_VOLUMES_ARE_USABLE { let irradiance_volume_entries = - irradiance_volume::get_bind_group_layout_entries(render_device); + 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]), @@ -444,7 +443,7 @@ impl MeshPipelineViewLayouts { #[cfg(debug_assertions)] if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { // Issue our own warning here because Naga's error message is a bit cryptic in this situation - warn_once!("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."); + 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 @@ -489,10 +488,10 @@ pub struct MeshViewBindGroup { pub value: BindGroup, } -#[allow(clippy::too_many_arguments)] pub fn prepare_mesh_view_bind_groups( mut commands: Commands, render_device: Res, + render_adapter: Res, mesh_pipeline: Res, shadow_samplers: Res, (light_meta, global_light_meta): (Res, Res), @@ -607,6 +606,7 @@ pub fn prepare_mesh_view_bind_groups( &images, &fallback_image, &render_device, + &render_adapter, ); match environment_map_bind_group_entries { @@ -642,6 +642,7 @@ pub fn prepare_mesh_view_bind_groups( &images, &fallback_image, &render_device, + &render_adapter, )) } else { None diff --git a/crates/bevy_pbr/src/render/parallax_mapping.wgsl b/crates/bevy_pbr/src/render/parallax_mapping.wgsl index 706c96850e172..780b5c290a416 100644 --- a/crates/bevy_pbr/src/render/parallax_mapping.wgsl +++ b/crates/bevy_pbr/src/render/parallax_mapping.wgsl @@ -5,8 +5,7 @@ mesh_bindings::mesh } -fn sample_depth_map(uv: vec2, instance_index: u32) -> f32 { - let slot = mesh[instance_index].material_and_lightmap_bind_group_slot & 0xffffu; +fn sample_depth_map(uv: vec2, material_bind_group_slot: u32) -> f32 { // We use `textureSampleLevel` over `textureSample` because the wgpu DX12 // backend (Fxc) panics when using "gradient instructions" inside a loop. // It results in the whole loop being unrolled by the shader compiler, @@ -19,8 +18,8 @@ fn sample_depth_map(uv: vec2, instance_index: u32) -> f32 { // See https://stackoverflow.com/questions/56581141/direct3d11-gradient-instruction-used-in-a-loop-with-varying-iteration-forcing return textureSampleLevel( #ifdef BINDLESS - depth_map_texture[slot], - depth_map_sampler[slot], + depth_map_texture[material_bind_group_slot], + depth_map_sampler[material_bind_group_slot], #else // BINDLESS depth_map_texture, depth_map_sampler, @@ -40,7 +39,7 @@ fn parallaxed_uv( original_uv: vec2, // The vector from the camera to the fragment at the surface in tangent space Vt: vec3, - instance_index: u32, + material_bind_group_slot: u32, ) -> vec2 { if max_layer_count < 1.0 { return original_uv; @@ -68,7 +67,7 @@ fn parallaxed_uv( var delta_uv = depth_scale * layer_depth * Vt.xy * vec2(1.0, -1.0) / view_steepness; var current_layer_depth = 0.0; - var texture_depth = sample_depth_map(uv, instance_index); + var texture_depth = sample_depth_map(uv, material_bind_group_slot); // texture_depth > current_layer_depth means the depth map depth is deeper // than the depth the ray would be at this UV offset so the ray has not @@ -76,7 +75,7 @@ fn parallaxed_uv( for (var i: i32 = 0; texture_depth > current_layer_depth && i <= i32(layer_count); i++) { current_layer_depth += layer_depth; uv += delta_uv; - texture_depth = sample_depth_map(uv, instance_index); + texture_depth = sample_depth_map(uv, material_bind_group_slot); } #ifdef RELIEF_MAPPING @@ -94,7 +93,7 @@ fn parallaxed_uv( current_layer_depth -= delta_depth; for (var i: u32 = 0u; i < max_steps; i++) { - texture_depth = sample_depth_map(uv, instance_index); + texture_depth = sample_depth_map(uv, material_bind_group_slot); // Halve the deltas for the next step delta_uv *= 0.5; @@ -118,7 +117,8 @@ fn parallaxed_uv( // may skip small details and result in writhing material artifacts. let previous_uv = uv - delta_uv; let next_depth = texture_depth - current_layer_depth; - let previous_depth = sample_depth_map(previous_uv, instance_index) - current_layer_depth + layer_depth; + let previous_depth = sample_depth_map(previous_uv, material_bind_group_slot) - + current_layer_depth + layer_depth; let weight = next_depth / (next_depth - previous_depth); diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 652fa5ac4e41e..12083f1b3a102 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -26,26 +26,38 @@ #import bevy_core_pipeline::oit::oit_draw #endif // OIT_ENABLED +#ifdef FORWARD_DECAL +#import bevy_pbr::decal::forward::get_forward_decal_info +#endif + @fragment fn fragment( #ifdef MESHLET_MESH_MATERIAL_PASS @builtin(position) frag_coord: vec4, #else - in: VertexOutput, + vertex_output: VertexOutput, @builtin(front_facing) is_front: bool, #endif ) -> FragmentOutput { #ifdef MESHLET_MESH_MATERIAL_PASS - let in = resolve_vertex_output(frag_coord); + let vertex_output = resolve_vertex_output(frag_coord); let is_front = true; #endif + var in = vertex_output; + // If we're in the crossfade section of a visibility range, conditionally // discard the fragment according to the visibility pattern. #ifdef VISIBILITY_RANGE_DITHER pbr_functions::visibility_range_dither(in.position, in.visibility_range_dither); #endif +#ifdef FORWARD_DECAL + let forward_decal_info = get_forward_decal_info(in); + in.world_position = forward_decal_info.world_position; + in.uv = forward_decal_info.uv; +#endif + // generate a PbrInput struct from the StandardMaterial bindings var pbr_input = pbr_input_from_standard_material(in, is_front); @@ -79,5 +91,9 @@ fn fragment( } #endif // OIT_ENABLED - return out; +#ifdef FORWARD_DECAL + out.color.a = min(forward_decal_info.alpha, out.color.a); +#endif + + return out; } diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index cd7500d1ac054..a8a02b3f71a4e 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -71,11 +71,16 @@ fn pbr_input_from_standard_material( is_front: bool, ) -> pbr_types::PbrInput { #ifdef BINDLESS +#ifdef MESHLET_MESH_MATERIAL_PASS + let slot = in.material_bind_group_slot; +#else // MESHLET_MESH_MATERIAL_PASS let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; +#endif // MESHLET_MESH_MATERIAL_PASS let flags = pbr_bindings::material[slot].flags; let base_color = pbr_bindings::material[slot].base_color; let deferred_lighting_pass_id = pbr_bindings::material[slot].deferred_lighting_pass_id; #else // BINDLESS + let slot = mesh[in.instance_index].material_and_lightmap_bind_group_slot & 0xffffu; let flags = pbr_bindings::material.flags; let base_color = pbr_bindings::material.base_color; let deferred_lighting_pass_id = pbr_bindings::material.deferred_lighting_pass_id; @@ -146,7 +151,7 @@ fn pbr_input_from_standard_material( // parallax mapping algorithm easier to understand and reason // about. -Vt, - in.instance_index, + slot, ); #endif @@ -167,7 +172,7 @@ fn pbr_input_from_standard_material( // parallax mapping algorithm easier to understand and reason // about. -Vt, - in.instance_index, + slot, ); #else uv_b = uv; diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index b6187bc4b2b4d..161b59be3165e 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -33,9 +33,8 @@ #endif -// Biasing info needed to sample from a texture when calling `sample_texture`. -// How this is done depends on whether we're rendering meshlets or regular -// meshes. +// Biasing info needed to sample from a texture. How this is done depends on +// whether we're rendering meshlets or regular meshes. struct SampleBias { #ifdef MESHLET_MESH_MATERIAL_PASS ddx_uv: vec2, @@ -443,7 +442,7 @@ fn apply_pbr_lighting( } let transmitted_light_contrib = - lighting::point_light(light_id, &transmissive_lighting_input); + lighting::point_light(light_id, &transmissive_lighting_input, enable_diffuse); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -501,7 +500,7 @@ fn apply_pbr_lighting( } let transmitted_light_contrib = - lighting::spot_light(light_id, &transmissive_lighting_input); + lighting::spot_light(light_id, &transmissive_lighting_input, enable_diffuse); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } @@ -557,7 +556,7 @@ fn apply_pbr_lighting( } let transmitted_light_contrib = - lighting::directional_light(i, &transmissive_lighting_input); + lighting::directional_light(i, &transmissive_lighting_input, enable_diffuse); transmitted_light += transmitted_light_contrib * transmitted_shadow; #endif } diff --git a/crates/bevy_pbr/src/render/pbr_transmission.wgsl b/crates/bevy_pbr/src/render/pbr_transmission.wgsl index 83a71096ebdfe..720a42bca9631 100644 --- a/crates/bevy_pbr/src/render/pbr_transmission.wgsl +++ b/crates/bevy_pbr/src/render/pbr_transmission.wgsl @@ -15,7 +15,7 @@ #endif fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, view_z: f32, N: vec3, V: vec3, F0: vec3, ior: f32, thickness: f32, perceptual_roughness: f32, specular_transmissive_color: vec3, transmitted_environment_light_specular: vec3) -> vec3 { - // Calculate the ratio between refaction indexes. Assume air/vacuum for the space outside the mesh + // Calculate the ratio between refraction indexes. Assume air/vacuum for the space outside the mesh let eta = 1.0 / ior; // Calculate incidence vector (opposite to view vector) and its dot product with the mesh normal @@ -26,7 +26,7 @@ fn specular_transmissive_light(world_position: vec4, frag_coord: vec3, let k = 1.0 - eta * eta * (1.0 - NdotI * NdotI); let T = eta * I - (eta * NdotI + sqrt(k)) * N; - // Calculate the exit position of the refracted ray, by propagating refacted direction through thickness + // Calculate the exit position of the refracted ray, by propagating refracted direction through thickness let exit_position = world_position.xyz + T * thickness; // Transform exit_position into clip space diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index fd47511da5d16..96c88702312e9 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - use crate::NodePbr; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; @@ -9,7 +7,7 @@ use bevy_core_pipeline::{ prepass::{DepthPrepass, NormalPrepass, ViewPrepassTextures}, }; use bevy_ecs::{ - prelude::{require, Bundle, Component, Entity}, + prelude::{require, Component, Entity}, query::{Has, QueryItem, With}, reflect::ReflectComponent, schedule::IntoSystemConfigs, @@ -36,11 +34,9 @@ use bevy_render::{ view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_utils::{ - prelude::default, - tracing::{error, warn}, -}; +use bevy_utils::prelude::default; use core::mem; +use tracing::{error, warn}; const PREPROCESS_DEPTH_SHADER_HANDLE: Handle = Handle::weak_from_u128(102258915420479); const SSAO_SHADER_HANDLE: Handle = Handle::weak_from_u128(253938746510568); @@ -132,18 +128,6 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { } } -/// Bundle to apply screen space ambient occlusion. -#[derive(Bundle, Default, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `ScreenSpaceAmbientOcclusion` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct ScreenSpaceAmbientOcclusionBundle { - pub settings: ScreenSpaceAmbientOcclusion, - pub depth_prepass: DepthPrepass, - pub normal_prepass: NormalPrepass, -} - /// Component to apply screen space ambient occlusion to a 3d camera. /// /// Screen space ambient occlusion (SSAO) approximates small-scale, @@ -185,9 +169,6 @@ impl Default for ScreenSpaceAmbientOcclusion { } } -#[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceAmbientOcclusion`")] -pub type ScreenSpaceAmbientOcclusionSettings = ScreenSpaceAmbientOcclusion; - #[derive(Reflect, PartialEq, Eq, Hash, Clone, Copy, Default, Debug)] pub enum ScreenSpaceAmbientOcclusionQualityLevel { Low, @@ -771,17 +752,9 @@ fn prepare_ssao_bind_groups( } } -#[allow(clippy::needless_range_loop)] fn generate_hilbert_index_lut() -> [[u16; 64]; 64] { - let mut t = [[0; 64]; 64]; - - for x in 0..64 { - for y in 0..64 { - t[x][y] = hilbert_index(x as u16, y as u16); - } - } - - t + use core::array::from_fn; + from_fn(|x| from_fn(|y| hilbert_index(x as u16, y as u16))) } // https://www.shadertoy.com/view/3tB3z3 diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 69a32acd75f5b..aa4e1037d5196 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -1,7 +1,5 @@ //! Screen space reflections implemented via raymarching. -#![expect(deprecated)] - use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_core_pipeline::{ @@ -14,7 +12,6 @@ use bevy_core_pipeline::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - bundle::Bundle, component::{require, Component}, entity::Entity, query::{Has, QueryItem, With}, @@ -25,6 +22,7 @@ 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}, @@ -36,11 +34,12 @@ use bevy_render::{ ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureFormat, TextureSampleType, }, - renderer::{RenderContext, RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, view::{ExtractedView, Msaa, ViewTarget, ViewUniformOffset}, Render, RenderApp, RenderSet, }; -use bevy_utils::{info_once, prelude::default}; +use bevy_utils::{once, prelude::default}; +use tracing::info; use crate::{ binding_arrays_are_usable, graph::NodePbr, prelude::EnvironmentMapLight, @@ -57,22 +56,6 @@ const RAYMARCH_SHADER_HANDLE: Handle = Handle::weak_from_u128(8517409683 /// Screen-space reflections are currently only supported with deferred rendering. pub struct ScreenSpaceReflectionsPlugin; -/// A convenient bundle to add screen space reflections to a camera, along with -/// the depth and deferred prepasses required to enable them. -#[derive(Bundle, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `ScreenSpaceReflections` components instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct ScreenSpaceReflectionsBundle { - /// The component that enables SSR. - pub settings: ScreenSpaceReflections, - /// The depth prepass, needed for SSR. - pub depth_prepass: DepthPrepass, - /// The deferred prepass, needed for SSR. - pub deferred_prepass: DeferredPrepass, -} - /// Add this component to a camera to enable *screen-space reflections* (SSR). /// /// Screen-space reflections currently require deferred rendering in order to @@ -141,9 +124,6 @@ pub struct ScreenSpaceReflections { pub use_secant: bool, } -#[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceReflections`")] -pub type ScreenSpaceReflectionsSettings = ScreenSpaceReflections; - /// A version of [`ScreenSpaceReflections`] for upload to the GPU. /// /// For more information on these fields, see the corresponding documentation in @@ -233,8 +213,19 @@ impl Plugin for ScreenSpaceReflectionsPlugin { render_app .init_resource::() - .init_resource::>() - .add_render_graph_edges( + .init_resource::>(); + + // only reference the default deferred lighting pass + // if it has been added + let has_default_deferred_lighting_pass = render_app + .world_mut() + .resource_mut::() + .sub_graph(Core3d) + .get_node_state(NodePbr::DeferredLightingPass) + .is_ok(); + + if has_default_deferred_lighting_pass { + render_app.add_render_graph_edges( Core3d, ( NodePbr::DeferredLightingPass, @@ -242,6 +233,12 @@ impl Plugin for ScreenSpaceReflectionsPlugin { Node3d::MainOpaquePass, ), ); + } else { + render_app.add_render_graph_edges( + Core3d, + (NodePbr::ScreenSpaceReflections, Node3d::MainOpaquePass), + ); + } } } @@ -354,6 +351,7 @@ impl FromWorld for ScreenSpaceReflectionsPipeline { fn from_world(world: &mut World) -> Self { let mesh_view_layouts = world.resource::().clone(); let render_device = world.resource::(); + let render_adapter = world.resource::(); // Create the bind group layout. let bind_group_layout = render_device.create_bind_group_layout( @@ -404,7 +402,7 @@ impl FromWorld for ScreenSpaceReflectionsPipeline { depth_linear_sampler, depth_nearest_sampler, bind_group_layout, - binding_arrays_are_usable: binding_arrays_are_usable(render_device), + binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), } } } @@ -505,10 +503,10 @@ impl ExtractComponent for ScreenSpaceReflections { fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { - info_once!( + once!(info!( "Disabling screen-space reflections on this platform because depth textures \ aren't supported correctly" - ); + )); return None; } diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 0d998b2a06daa..4b90d63afccb7 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -29,8 +29,6 @@ //! //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction -#![expect(deprecated)] - use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Assets, Handle}; use bevy_color::Color; @@ -39,7 +37,6 @@ use bevy_core_pipeline::core_3d::{ prepare_core_3d_depth_textures, }; use bevy_ecs::{ - bundle::Bundle, component::{require, Component}, reflect::ReflectComponent, schedule::IntoSystemConfigs as _, @@ -55,10 +52,10 @@ use bevy_render::{ render_graph::{RenderGraphApp, ViewNodeRunner}, render_resource::{Shader, SpecializedRenderPipelines}, sync_component::SyncComponentPlugin, - view::{InheritedVisibility, ViewVisibility, Visibility}, + view::Visibility, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_transform::components::{GlobalTransform, Transform}; +use bevy_transform::components::Transform; use render::{ VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer, CUBE_MESH, PLANE_MESH, VOLUMETRIC_FOG_HANDLE, @@ -120,32 +117,6 @@ pub struct VolumetricFog { pub step_count: u32, } -#[deprecated(since = "0.15.0", note = "Renamed to `VolumetricFog`")] -pub type VolumetricFogSettings = VolumetricFog; - -/// A convenient [`Bundle`] that contains all components necessary to generate a -/// fog volume. -#[derive(Bundle, Clone, Debug, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `FogVolume` component instead. Inserting it will now also insert the other components required by it automatically." -)] -pub struct FogVolumeBundle { - /// The actual fog volume. - pub fog_volume: FogVolume, - /// Visibility. - pub visibility: Visibility, - /// Inherited visibility. - pub inherited_visibility: InheritedVisibility, - /// View visibility. - pub view_visibility: ViewVisibility, - /// The local transform. Set this to change the position, and scale of the - /// fog's axis-aligned bounding box (AABB). - pub transform: Transform, - /// The global transform. - pub global_transform: GlobalTransform, -} - #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Default, Debug)] #[require(Transform, Visibility)] diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 76039bbbe448b..a9eedad1e8e70 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -607,7 +607,6 @@ impl SpecializedRenderPipeline for VolumetricFogPipeline { } /// Specializes volumetric fog pipelines for all views with that effect enabled. -#[allow(clippy::too_many_arguments)] pub fn prepare_volumetric_fog_pipelines( mut commands: Commands, pipeline_cache: Res, diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 413933135c85a..aaec62986f33b 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -127,7 +127,6 @@ fn global_color_changed( } /// Updates the wireframe material when the color in [`WireframeColor`] changes -#[allow(clippy::type_complexity)] fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< @@ -204,7 +203,7 @@ fn apply_global_wireframe_material( } } -/// Gets an handle to a wireframe material with a fallback on the default material +/// Gets a handle to a wireframe material with a fallback on the default material fn get_wireframe_material( maybe_color: Option<&WireframeColor>, wireframe_materials: &mut Assets, diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index 3deba7d21bcba..011f55c995080 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_picking" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides screen picking functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -12,23 +12,26 @@ license = "MIT OR Apache-2.0" bevy_mesh_picking_backend = ["dep:bevy_mesh", "dep:crossbeam-channel"] [dependencies] -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_input = { path = "../bevy_input", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.15.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } +# 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_hierarchy = { path = "../bevy_hierarchy", 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" } +# other crossbeam-channel = { version = "0.5", optional = true } uuid = { version = "1.1", features = ["v4"] } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 2e950fa804db8..c7715723bf923 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -20,7 +20,7 @@ //! - The [`PointerHits`] events produced by a backend do **not** need to be sorted or filtered, all //! that is needed is an unordered list of entities and their [`HitData`]. //! -//! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may +//! - Backends do not need to consider the [`Pickable`](crate::Pickable) component, though they may //! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy //! may want to exit early if it intersects an entity that blocks lower entities from being //! picked. @@ -42,7 +42,7 @@ pub mod prelude { pub use super::{ray::RayMap, HitData, PointerHits}; pub use crate::{ pointer::{PointerId, PointerLocation}, - PickSet, PickingBehavior, + PickSet, Pickable, }; } diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index bb2458b7af0a7..ae3d879e9a47d 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -3,7 +3,7 @@ //! //! # Usage //! -//! To receive events from this module, you must use an [`Observer`] +//! To receive events from this module, you must use an [`Observer`] or [`EventReader`] with [`Pointer`] events. //! The simplest example, registering a callback when an entity is hovered over by a pointer, looks like this: //! //! ```rust @@ -23,9 +23,9 @@ //! //! The order in which interaction events are received is extremely important, and you can read more //! about it on the docs for the dispatcher system: [`pointer_events`]. This system runs in -//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickSet::Focus`](crate::PickSet::Focus). All pointer-event +//! [`PreUpdate`](bevy_app::PreUpdate) in [`PickSet::Hover`](crate::PickSet::Hover). All pointer-event //! observers resolve during the sync point between [`pointer_events`] and -//! [`update_interactions`](crate::focus::update_interactions). +//! [`update_interactions`](crate::hover::update_interactions). //! //! # Events Types //! @@ -35,21 +35,22 @@ //! + 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 -//! general metadata about the pointer and it's location. +//! general metadata about the pointer event. -use core::fmt::Debug; +use core::{fmt::Debug, time::Duration}; use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_hierarchy::Parent; use bevy_math::Vec2; use bevy_reflect::prelude::*; use bevy_render::camera::NormalizedRenderTarget; -use bevy_utils::{tracing::debug, Duration, HashMap, Instant}; +use bevy_utils::{HashMap, Instant}; use bevy_window::Window; +use tracing::debug; use crate::{ backend::{prelude::PointerLocation, HitData}, - focus::{HoverMap, PreviousHoverMap}, + hover::{HoverMap, PreviousHoverMap}, pointer::{ Location, PointerAction, PointerButton, PointerId, PointerInput, PointerMap, PressDirection, }, @@ -385,7 +386,7 @@ pub struct PickingEventWriters<'w> { /// receive [`Out`] and then entity B will receive [`Over`]. No entity will ever /// receive both an [`Over`] and and a [`Out`] event during the same frame. /// -/// When we account for event bubbling, this is no longer true. When focus shifts +/// When we account for event bubbling, this is no longer true. When the hovering focus shifts /// between children, parent entities may receive redundant [`Out`] → [`Over`] pairs. /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. @@ -393,7 +394,7 @@ pub struct PickingEventWriters<'w> { /// Both [`Click`] and [`Released`] 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 sequentally after an [`Out`] event (but always on the same frame +/// be received sequentially after an [`Out`] event (but always on the same frame /// as the [`Out`] event). /// /// Note: Though it is common for the [`PointerInput`] stream may contain @@ -401,7 +402,6 @@ pub struct PickingEventWriters<'w> { /// determined only by the pointer's *final position*. Since the hover state /// ultimately determines which entities receive events, this may mean that an /// entity can receive events from before or after it was actually hovered. -#[allow(clippy::too_many_arguments)] pub fn pointer_events( // Input mut input_events: EventReader, @@ -661,6 +661,9 @@ pub fn pointer_events( } // Moved PointerAction::Moved { delta } => { + if delta == Vec2::ZERO { + continue; // If delta is zero, the following events will not be triggered. + } // Triggers during movement even if not over an entity for button in PointerButton::iter() { let state = pointer_state.get_mut(pointer_id, button); @@ -692,6 +695,10 @@ pub fn pointer_events( // Emit Drag events to the entities we are dragging for (drag_target, drag) in state.dragging.iter_mut() { + let delta = location.position - drag.latest_pos; + if delta == Vec2::ZERO { + continue; // No need to emit a Drag event if there is no movement + } let drag_event = Pointer::new( pointer_id, location.clone(), @@ -699,7 +706,7 @@ pub fn pointer_events( Drag { button, distance: location.position - drag.start_pos, - delta: location.position - drag.latest_pos, + delta, }, ); commands.trigger_targets(drag_event.clone(), *drag_target); diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/hover.rs similarity index 93% rename from crates/bevy_picking/src/focus.rs rename to crates/bevy_picking/src/hover.rs index e730e41ce7d45..2b3a68fe8772a 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/hover.rs @@ -10,7 +10,7 @@ use std::collections::HashSet; use crate::{ backend::{self, HitData}, pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPress}, - PickingBehavior, + Pickable, }; use bevy_derive::{Deref, DerefMut}; @@ -43,8 +43,8 @@ type OverMap = HashMap; /// between it and the pointer block interactions. /// /// For example, if a pointer is hitting a UI button and a 3d mesh, but the button is in front of -/// the mesh, the UI button will be hovered, but the mesh will not. Unless, the [`PickingBehavior`] -/// component is present with [`should_block_lower`](PickingBehavior::should_block_lower) set to `false`. +/// the mesh, the UI button will be hovered, but the mesh will not. Unless, the [`Pickable`] +/// component is present with [`should_block_lower`](Pickable::should_block_lower) set to `false`. /// /// # Advanced Users /// @@ -62,9 +62,9 @@ pub struct PreviousHoverMap(pub HashMap>); /// Coalesces all data from inputs and backends to generate a map of the currently hovered entities. /// This is the final focusing step to determine which entity the pointer is hovering over. -pub fn update_focus( +pub fn generate_hovermap( // Inputs - picking_behavior: Query<&PickingBehavior>, + pickable: Query<&Pickable>, pointers: Query<&PointerId>, mut under_pointer: EventReader, mut pointer_input: EventReader, @@ -81,7 +81,7 @@ pub fn update_focus( &pointers, ); build_over_map(&mut under_pointer, &mut over_map, &mut pointer_input); - build_hover_map(&pointers, picking_behavior, &over_map, &mut hover_map); + build_hover_map(&pointers, pickable, &over_map, &mut hover_map); } /// Clear non-empty local maps, reusing allocated memory. @@ -148,12 +148,12 @@ fn build_over_map( } } -/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`PickingBehavior`]. Note -/// that unlike the pointer map, this uses [`PickingBehavior`] to determine if lower entities receive hover +/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`Pickable`]. Note +/// that unlike the pointer map, this uses [`Pickable`] to determine if lower entities receive hover /// focus. Often, only a single entity per pointer will be hovered. fn build_hover_map( pointers: &Query<&PointerId>, - picking_behavior: Query<&PickingBehavior>, + pickable: Query<&Pickable>, over_map: &Local, // Output hover_map: &mut HoverMap, @@ -163,11 +163,11 @@ fn build_hover_map( if let Some(layer_map) = over_map.get(pointer_id) { // Note we reverse here to start from the highest layer first. for (entity, pick_data) in layer_map.values().rev().flatten() { - if let Ok(picking_behavior) = picking_behavior.get(*entity) { - if picking_behavior.is_hoverable { + if let Ok(pickable) = pickable.get(*entity) { + if pickable.is_hoverable { pointer_entity_set.insert(*entity, pick_data.clone()); } - if picking_behavior.should_block_lower { + if pickable.should_block_lower { break; } } else { @@ -231,7 +231,7 @@ pub fn update_interactions( if let Some(pointers_hovered_entities) = hover_map.get(pointer) { // Insert a sorted list of hit entities into the pointer's interaction component. let mut sorted_entities: Vec<_> = pointers_hovered_entities.clone().drain().collect(); - sorted_entities.sort_by_key(|(_entity, hit)| FloatOrd(hit.depth)); + sorted_entities.sort_by_key(|(_, hit)| FloatOrd(hit.depth)); pointer_interaction.sorted_entities = sorted_entities; for hovered_entity in pointers_hovered_entities.iter().map(|(entity, _)| entity) { diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs index 321ed6b5e6da2..94e195e4b3c78 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -22,8 +22,9 @@ use bevy_input::{ use bevy_math::Vec2; use bevy_reflect::prelude::*; use bevy_render::camera::RenderTarget; -use bevy_utils::{tracing::debug, HashMap, HashSet}; +use bevy_utils::{HashMap, HashSet}; use bevy_window::{PrimaryWindow, WindowEvent, WindowRef}; +use tracing::debug; use crate::pointer::{ Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation, diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index ed77e98926c25..9e825a38262e2 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -1,12 +1,14 @@ -//! This crate provides 'picking' capabilities for the Bevy game engine. That means, in simple terms, figuring out -//! how to connect up a user's clicks or taps to the entities they are trying to interact with. +//! This crate provides 'picking' capabilities for the Bevy game engine, allowing pointers to +//! interact with entities using hover, click, and drag events. //! //! ## Overview //! //! In the simplest case, this plugin allows you to click on things in the scene. However, it also //! allows you to express more complex interactions, like detecting when a touch input drags a UI -//! element and drops it on a 3d mesh rendered to a different camera. The crate also provides a set of -//! interaction callbacks, allowing you to receive input directly on entities like here: +//! element and drops it on a 3d mesh rendered to a different camera. +//! +//! Pointer events bubble up the entity hieararchy and can be used with observers, allowing you to +//! succinctly express rich interaction behaviors by attaching pointer callbacks to entities: //! //! ```rust //! # use bevy_ecs::prelude::*; @@ -16,7 +18,8 @@ //! # let mut world = World::new(); //! world.spawn(MyComponent) //! .observe(|mut trigger: Trigger>| { -//! // Get the underlying event type +//! println!("I was just clicked!"); +//! // Get the underlying pointer event data //! let click_event: &Pointer = trigger.event(); //! // Stop the event from bubbling up the entity hierarchy //! trigger.propagate(false); @@ -24,16 +27,19 @@ //! ``` //! //! At its core, this crate provides a robust abstraction for computing picking state regardless of -//! pointing devices, or what you are hit testing against. It is designed to work with any input, including -//! mouse, touch, pens, or virtual pointers controlled by gamepads. +//! pointing devices, or what you are hit testing against. It is designed to work with any input, +//! including mouse, touch, pens, or virtual pointers controlled by gamepads. //! //! ## Expressive Events //! -//! The events in this module (see [`events`]) cannot be listened to with normal `EventReader`s. -//! Instead, they are dispatched to *observers* attached to specific entities. 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. +//! Although the events in this module (see [`events`]) can be listened to with normal +//! `EventReader`s, using observers is often more expressive, with less boilerplate. This is because +//! observers allow you to attach event handling logic to specific entities, as well as make use of +//! event bubbling. +//! +//! 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. //! //! This allows you to run callbacks when any children of an entity are interacted with, and leads //! to succinct, expressive code: @@ -54,7 +60,7 @@ //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) //! .observe(|trigger: Trigger>, mut commands: Commands| { -//! println!("Entity {:?} goes BOOM!", trigger.target()); +//! println!("Entity {} goes BOOM!", trigger.target()); //! commands.entity(trigger.target()).despawn(); //! }) //! .observe(|trigger: Trigger>, mut events: EventWriter| { @@ -74,8 +80,9 @@ //! #### Input Agnostic //! //! Picking provides a generic Pointer abstraction, which is useful for reacting to many different -//! types of input devices. Pointers can be controlled with anything, whether it's the included mouse -//! or touch inputs, or a custom gamepad input system you write yourself to control a virtual pointer. +//! types of input devices. Pointers can be controlled with anything, whether it's the included +//! mouse or touch inputs, or a custom gamepad input system you write yourself to control a virtual +//! pointer. //! //! ## Robustness //! @@ -90,8 +97,8 @@ //! #### Next Steps //! //! To learn more, take a look at the examples in the -//! [examples](https://github.com/bevyengine/bevy/tree/main/examples/picking). You -//! can read the next section to understand how the plugin works. +//! [examples](https://github.com/bevyengine/bevy/tree/main/examples/picking). You can read the next +//! section to understand how the plugin works. //! //! # The Picking Pipeline //! @@ -101,11 +108,11 @@ //! #### Pointers ([`pointer`](mod@pointer)) //! //! The first stage of the pipeline is to gather inputs and update pointers. This stage is -//! ultimately responsible for generating [`PointerInput`](pointer::PointerInput) events. The provided -//! crate does this automatically for mouse, touch, and pen inputs. If you wanted to implement your own -//! pointer, controlled by some other input, you can do that here. The ordering of events within the -//! [`PointerInput`](pointer::PointerInput) stream is meaningful for events with the same -//! [`PointerId`](pointer::PointerId), but not between different pointers. +//! ultimately responsible for generating [`PointerInput`](pointer::PointerInput) events. The +//! provided crate does this automatically for mouse, touch, and pen inputs. If you wanted to +//! implement your own pointer, controlled by some other input, you can do that here. The ordering +//! of events within the [`PointerInput`](pointer::PointerInput) stream is meaningful for events +//! with the same [`PointerId`](pointer::PointerId), but not between different pointers. //! //! Because pointer positions and presses are driven by these events, you can use them to mock //! inputs for testing. @@ -115,28 +122,28 @@ //! //! #### Backend ([`backend`]) //! -//! A picking backend only has one job: reading [`PointerLocation`](pointer::PointerLocation) components, -//! and producing [`PointerHits`](backend::PointerHits). You can find all documentation and types needed to -//! implement a backend at [`backend`]. +//! A picking backend only has one job: reading [`PointerLocation`](pointer::PointerLocation) +//! components, and producing [`PointerHits`](backend::PointerHits). You can find all documentation +//! and types needed to implement a backend at [`backend`]. //! //! You will eventually need to choose which picking backend(s) you want to use. This crate does not -//! supply any backends, and expects you to select some from the other bevy crates or the third-party -//! ecosystem. You can find all the provided backends in the [`backend`] module. +//! supply any backends, and expects you to select some from the other bevy crates or the +//! third-party ecosystem. //! //! It's important to understand that you can mix and match backends! For example, you might have a //! backend for your UI, and one for the 3d scene, with each being specialized for their purpose. -//! This crate provides some backends out of the box, but you can even write your own. It's been -//! made as easy as possible intentionally; the `bevy_mod_raycast` backend is 50 lines of code. +//! Bevy provides some backends out of the box, but you can even write your own. It's been made as +//! easy as possible intentionally; the `bevy_mod_raycast` backend is 50 lines of code. //! -//! #### Focus ([`focus`]) +//! #### Hover ([`hover`]) //! //! The next step is to use the data from the backends, combine and sort the results, and determine -//! what each cursor is hovering over, producing a [`HoverMap`](`crate::focus::HoverMap`). Note that +//! what each cursor is hovering over, producing a [`HoverMap`](`crate::hover::HoverMap`). Note that //! just because a pointer is over an entity, it is not necessarily *hovering* that entity. Although -//! multiple backends may be reporting that a pointer is hitting an entity, the focus system needs +//! multiple backends may be reporting that a pointer is hitting an entity, the hover system needs //! to determine which entities are actually being hovered by this pointer based on the pick depth, -//! order of the backend, and the optional [`PickingBehavior`] component of the entity. In other words, -//! if one entity is in front of another, usually only the topmost one will be hovered. +//! order of the backend, and the optional [`Pickable`] component of the entity. In other +//! words, if one entity is in front of another, usually only the topmost one will be hovered. //! //! #### Events ([`events`]) //! @@ -144,9 +151,8 @@ //! a pointer hovers or clicks an entity. These simple events are then used to generate more complex //! events for dragging and dropping. //! -//! Because it is completely agnostic to the earlier stages of the pipeline, you can easily -//! extend the plugin with arbitrary backends and input methods, yet still use all the high level -//! features. +//! Because it is completely agnostic to the earlier stages of the pipeline, you can easily extend +//! the plugin with arbitrary backends and input methods, yet still use all the high level features. #![deny(missing_docs)] @@ -154,7 +160,7 @@ extern crate alloc; pub mod backend; pub mod events; -pub mod focus; +pub mod hover; pub mod input; #[cfg(feature = "bevy_mesh_picking_backend")] pub mod mesh_picking; @@ -178,16 +184,19 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, - InteractionPlugin, PickingBehavior, PickingPlugin, + InteractionPlugin, Pickable, PickingPlugin, }; } -/// An optional component that overrides default picking behavior for an entity, allowing you to -/// make an entity non-hoverable, or allow items below it to be hovered. See the documentation on -/// the fields for more details. +/// An optional component that marks an entity as usable by a backend, and overrides default +/// picking behavior for an entity. +/// +/// This allows you to make an entity non-hoverable, or allow items below it to be hovered. +/// +/// See the documentation on the fields for more details. #[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Debug, PartialEq)] -pub struct PickingBehavior { +pub struct Pickable { /// Should this entity block entities below it from being picked? /// /// This is useful if you want picking to continue hitting entities below this one. Normally, @@ -201,13 +210,13 @@ pub struct PickingBehavior { /// /// For example, if a pointer is over a UI element, as well as a 3d mesh, backends will report /// hits for both of these entities. Additionally, the hits will be sorted by the camera order, - /// so if the UI is drawing on top of the 3d mesh, the UI will be "above" the mesh. When focus + /// so if the UI is drawing on top of the 3d mesh, the UI will be "above" the mesh. When hovering /// is computed, the UI element will be checked first to see if it this field is set to block - /// lower entities. If it does (default), the focus system will stop there, and only the UI + /// lower entities. If it does (default), the hovering system will stop there, and only the UI /// element will be marked as hovered. However, if this field is set to `false`, both the UI /// element *and* the mesh will be marked as hovered. /// - /// Entities without the [`PickingBehavior`] component will block by default. + /// Entities without the [`Pickable`] component will block by default. pub should_block_lower: bool, /// If this is set to `false` and `should_block_lower` is set to true, this entity will block @@ -222,11 +231,11 @@ pub struct PickingBehavior { /// components mark it as hovered. This can be combined with the other field /// [`Self::should_block_lower`], which is orthogonal to this one. /// - /// Entities without the [`PickingBehavior`] component are hoverable by default. + /// Entities without the [`Pickable`] component are hoverable by default. pub is_hoverable: bool, } -impl PickingBehavior { +impl Pickable { /// This entity will not block entities beneath it, nor will it emit events. /// /// If a backend reports this entity as being hit, the picking plugin will completely ignore it. @@ -236,7 +245,7 @@ impl PickingBehavior { }; } -impl Default for PickingBehavior { +impl Default for Pickable { fn default() -> Self { Self { should_block_lower: true, @@ -257,12 +266,12 @@ pub enum PickSet { ProcessInput, /// Reads inputs and produces [`backend::PointerHits`]s. In the [`PreUpdate`] schedule. Backend, - /// Reads [`backend::PointerHits`]s, and updates focus, selection, and highlighting states. In + /// Reads [`backend::PointerHits`]s, and updates the hovermap, selection, and highlighting states. In /// the [`PreUpdate`] schedule. - Focus, - /// Runs after all the focus systems are done, before event listeners are triggered. In the + Hover, + /// Runs after all the [`PickSet::Hover`] systems are done, before event listeners are triggered. In the /// [`PreUpdate`] schedule. - PostFocus, + PostHover, /// Runs after all other picking sets. In the [`PreUpdate`] schedule. Last, } @@ -298,7 +307,7 @@ pub struct PickingPlugin { /// Enables and disables input collection. pub is_input_enabled: bool, /// Enables and disables updating interaction states of entities. - pub is_focus_enabled: bool, + pub is_hover_enabled: bool, /// Enables or disables picking for window entities. pub is_window_picking_enabled: bool, } @@ -309,10 +318,10 @@ impl PickingPlugin { state.is_input_enabled && state.is_enabled } - /// Whether or not systems updating entities' [`PickingInteraction`](focus::PickingInteraction) + /// Whether or not systems updating entities' [`PickingInteraction`](hover::PickingInteraction) /// component should be running. - pub fn focus_should_run(state: Res) -> bool { - state.is_focus_enabled && state.is_enabled + pub fn hover_should_run(state: Res) -> bool { + state.is_hover_enabled && state.is_enabled } /// Whether or not window entities should receive pick events. @@ -326,7 +335,7 @@ impl Default for PickingPlugin { Self { is_enabled: true, is_input_enabled: true, - is_focus_enabled: true, + is_hover_enabled: true, is_window_picking_enabled: true, } } @@ -370,14 +379,15 @@ impl Plugin for PickingPlugin { ( PickSet::ProcessInput.run_if(Self::input_should_run), PickSet::Backend, - PickSet::Focus.run_if(Self::focus_should_run), - PickSet::PostFocus, + PickSet::Hover.run_if(Self::hover_should_run), + PickSet::PostHover, PickSet::Last, ) .chain(), ) .register_type::() - .register_type::() + .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -393,10 +403,10 @@ pub struct InteractionPlugin; impl Plugin for InteractionPlugin { fn build(&self, app: &mut App) { use events::*; - use focus::{update_focus, update_interactions}; + use hover::{generate_hovermap, update_interactions}; - app.init_resource::() - .init_resource::() + app.init_resource::() + .init_resource::() .init_resource::() .add_event::>() .add_event::>() @@ -414,9 +424,9 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_systems( PreUpdate, - (update_focus, update_interactions, pointer_events) + (generate_hovermap, update_interactions, pointer_events) .chain() - .in_set(PickSet::Focus), + .in_set(PickSet::Hover), ); } } diff --git a/crates/bevy_picking/src/mesh_picking/mod.rs b/crates/bevy_picking/src/mesh_picking/mod.rs index a848097a6854f..57a49a2b0ba60 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 [`PickingBehavior::IGNORE`]. +//! by adding [`Pickable::IGNORE`]. //! //! To make mesh picking entirely opt-in, set [`MeshPickingSettings::require_markers`] //! to `true` and add a [`RayCastPickable`] component to the desired camera and target entities. @@ -68,12 +68,11 @@ impl Plugin for MeshPickingPlugin { } /// Casts rays into the scene using [`MeshPickingSettings`] and sends [`PointerHits`] events. -#[allow(clippy::too_many_arguments)] pub fn update_hits( backend_settings: Res, ray_map: Res, picking_cameras: Query<(&Camera, Option<&RayCastPickable>, Option<&RenderLayers>)>, - pickables: Query<&PickingBehavior>, + pickables: Query<&Pickable>, marked_targets: Query<&RayCastPickable>, layers: Query<&RenderLayers>, mut ray_cast: MeshRayCast, @@ -99,10 +98,7 @@ pub fn update_hits( let entity_layers = layers.get(entity).cloned().unwrap_or_default(); let render_layers_match = cam_layers.intersects(&entity_layers); - let is_pickable = pickables - .get(entity) - .map(|p| p.is_hoverable) - .unwrap_or(true); + let is_pickable = pickables.get(entity).ok().is_none_or(|p| p.is_hoverable); marker_requirement && render_layers_match && is_pickable }, 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 2ba76f79606f6..ef6f187416b46 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -18,7 +18,7 @@ use bevy_ecs::{prelude::*, system::lifetimeless::Read, system::SystemParam}; use bevy_math::FloatOrd; use bevy_render::{prelude::*, primitives::Aabb}; use bevy_transform::components::GlobalTransform; -use bevy_utils::tracing::*; +use tracing::*; /// How a ray cast should handle [`Visibility`]. #[derive(Clone, Copy, Reflect)] diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index d8a65d9588a02..42291f7a7daa6 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -235,8 +235,7 @@ impl Location { camera .logical_viewport_rect() - .map(|rect| rect.contains(self.position)) - .unwrap_or(false) + .is_some_and(|rect| rect.contains(self.position)) } } diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index 0aea96ebd023b..d2c3db4fbc73c 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_ptr" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Utilities for working with untyped pointers in a more safe way" homepage = "https://bevyengine.org" diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index d767a25d4c63d..adb72409f96fb 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -289,7 +289,9 @@ impl<'a, A: IsAligned> Ptr<'a, A> { /// Transforms this [`Ptr`] into an [`PtrMut`] /// /// # Safety - /// Another [`PtrMut`] for the same [`Ptr`] must not be created until the first is dropped. + /// * The data pointed to by this `Ptr` must be valid for writes. + /// * There must be no active references (mutable or otherwise) to the data underlying this `Ptr`. + /// * Another [`PtrMut`] for the same [`Ptr`] must not be created until the first is dropped. #[inline] pub unsafe fn assert_unique(self) -> PtrMut<'a, A> { PtrMut(self.0, PhantomData) diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 193a0b7c86b62..751a0cc825ff5 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Dynamically interact with rust types" homepage = "https://bevyengine.org" @@ -39,11 +39,11 @@ functions = ["bevy_reflect_derive/functions"] [dependencies] # bevy -bevy_reflect_derive = { path = "derive", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false, features = [ +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.15.0-dev" } +bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" } # used by bevy-utils, but it also needs reflect impls foldhash = { version = "0.1.3", default-features = false } @@ -53,7 +53,7 @@ erased-serde = { version = "0.4", default-features = false, features = [ "alloc", ] } disqualified = { version = "1.0", default-features = false } -downcast-rs = { version = "1.2", 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"] } serde = { version = "1", default-features = false, features = ["alloc"] } diff --git a/crates/bevy_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index 468e89e8fbff5..8c4717c287786 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_reflect_derive" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Derive implementations for bevy_reflect" homepage = "https://bevyengine.org" @@ -19,7 +19,7 @@ documentation = [] functions = [] [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["full"] } diff --git a/crates/bevy_reflect/derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs index b134c571c0501..bdb94db06bc7e 100644 --- a/crates/bevy_reflect/derive/src/container_attributes.rs +++ b/crates/bevy_reflect/derive/src/container_attributes.rs @@ -89,10 +89,7 @@ pub(crate) struct FromReflectAttrs { impl FromReflectAttrs { /// Returns true if `FromReflect` should be automatically derived as part of the `Reflect` derive. pub fn should_auto_derive(&self) -> bool { - self.auto_derive - .as_ref() - .map(LitBool::value) - .unwrap_or(true) + self.auto_derive.as_ref().is_none_or(LitBool::value) } } @@ -112,10 +109,7 @@ pub(crate) struct TypePathAttrs { impl TypePathAttrs { /// Returns true if `TypePath` should be automatically derived as part of the `Reflect` derive. pub fn should_auto_derive(&self) -> bool { - self.auto_derive - .as_ref() - .map(LitBool::value) - .unwrap_or(true) + self.auto_derive.as_ref().is_none_or(LitBool::value) } } 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 c47c328f9ae13..f7d1e0b8893e4 100644 --- a/crates/bevy_reflect/derive/src/impls/func/into_return.rs +++ b/crates/bevy_reflect/derive/src/impls/func/into_return.rs @@ -14,7 +14,7 @@ pub(crate) fn impl_into_return( quote! { impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause { fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { - #bevy_reflect::func::Return::Owned(Box::new(self)) + #bevy_reflect::func::Return::Owned(#bevy_reflect::__macro_exports::alloc_utils::Box::new(self)) } } diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 0753c0e345c1a..3deff6f315c6e 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -175,11 +175,6 @@ impl DynamicArray { } } - #[deprecated(since = "0.15.0", note = "use from_iter")] - pub fn from_vec(values: Vec) -> Self { - Self::from_iter(values) - } - /// Sets the [type] to be represented by this `DynamicArray`. /// /// # Panics @@ -513,6 +508,8 @@ pub fn array_debug(dyn_array: &dyn Array, f: &mut Formatter<'_>) -> core::fmt::R #[cfg(test)] mod tests { use crate::Reflect; + use alloc::boxed::Box; + #[test] fn next_index_increment() { const SIZE: usize = if cfg!(debug_assertions) { diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index 0e751fa57a258..cc952d93f15c8 100644 --- a/crates/bevy_reflect/src/attributes.rs +++ b/crates/bevy_reflect/src/attributes.rs @@ -152,7 +152,6 @@ macro_rules! impl_custom_attribute_methods { $self.custom_attributes().get::() } - #[allow(rustdoc::redundant_explicit_links)] /// Gets a custom attribute by its [`TypeId`](core::any::TypeId). /// /// This is the dynamic equivalent of [`get_attribute`](Self::get_attribute). @@ -181,6 +180,7 @@ mod tests { use super::*; use crate as bevy_reflect; use crate::{type_info::Typed, TypeInfo, VariantInfo}; + use alloc::{format, string::String}; use core::ops::RangeInclusive; #[derive(Reflect, PartialEq, Debug)] diff --git a/crates/bevy_reflect/src/enums/mod.rs b/crates/bevy_reflect/src/enums/mod.rs index 95a94e68e97e1..89b5f17b1c8b8 100644 --- a/crates/bevy_reflect/src/enums/mod.rs +++ b/crates/bevy_reflect/src/enums/mod.rs @@ -12,6 +12,7 @@ pub use variants::*; mod tests { use crate as bevy_reflect; use crate::*; + use alloc::boxed::Box; #[derive(Reflect, Debug, PartialEq)] enum MyEnum { diff --git a/crates/bevy_reflect/src/from_reflect.rs b/crates/bevy_reflect/src/from_reflect.rs index 71a0dd571591c..dc113d869f3c7 100644 --- a/crates/bevy_reflect/src/from_reflect.rs +++ b/crates/bevy_reflect/src/from_reflect.rs @@ -112,7 +112,6 @@ impl ReflectFromReflect { /// /// This will convert the object to a concrete type if it wasn't already, and return /// the value as `Box`. - #[allow(clippy::wrong_self_convention)] pub fn from_reflect(&self, reflect_value: &dyn PartialReflect) -> Option> { (self.from_reflect)(reflect_value) } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index 60698a3d7e0a7..35e32c606758e 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -2,12 +2,9 @@ use crate::{ func::args::{ArgError, FromArg, Ownership}, PartialReflect, Reflect, TypePath, }; -use alloc::boxed::Box; +use alloc::{boxed::Box, string::ToString}; use core::ops::Deref; -#[cfg(not(feature = "std"))] -use alloc::{format, vec}; - /// Represents an argument that can be passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index 65c4caa6e8449..bd32bd5e5aadd 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -4,9 +4,6 @@ use thiserror::Error; use crate::func::args::Ownership; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// An error that occurs when converting an [argument]. /// /// [argument]: crate::func::args::Arg diff --git a/crates/bevy_reflect/src/func/args/from_arg.rs b/crates/bevy_reflect/src/func/args/from_arg.rs index ddb1e014eb8b7..88d04aefe7525 100644 --- a/crates/bevy_reflect/src/func/args/from_arg.rs +++ b/crates/bevy_reflect/src/func/args/from_arg.rs @@ -1,8 +1,5 @@ use crate::func::args::{Arg, ArgError}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A trait for types that can be created from an [`Arg`]. /// /// This trait exists so that types can be automatically converted into an [`Arg`] diff --git a/crates/bevy_reflect/src/func/args/info.rs b/crates/bevy_reflect/src/func/args/info.rs index 3919771f349d4..b1a81f3059a2e 100644 --- a/crates/bevy_reflect/src/func/args/info.rs +++ b/crates/bevy_reflect/src/func/args/info.rs @@ -6,9 +6,6 @@ use crate::{ Type, TypePath, }; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// Type information for an [`Arg`] used in a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`Arg`]: crate::func::args::Arg diff --git a/crates/bevy_reflect/src/func/args/list.rs b/crates/bevy_reflect/src/func/args/list.rs index 145414424f4b1..de8bcdcb92ede 100644 --- a/crates/bevy_reflect/src/func/args/list.rs +++ b/crates/bevy_reflect/src/func/args/list.rs @@ -10,9 +10,6 @@ use alloc::{ collections::vec_deque::{Iter, VecDeque}, }; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A list of arguments that can be passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// # Example @@ -308,6 +305,7 @@ impl<'a> ArgList<'a> { #[cfg(test)] mod tests { use super::*; + use alloc::string::String; #[test] fn should_push_arguments_in_order() { diff --git a/crates/bevy_reflect/src/func/args/ownership.rs b/crates/bevy_reflect/src/func/args/ownership.rs index 448efed9f6b1f..b9395c742f404 100644 --- a/crates/bevy_reflect/src/func/args/ownership.rs +++ b/crates/bevy_reflect/src/func/args/ownership.rs @@ -1,8 +1,5 @@ use core::fmt::{Display, Formatter}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A trait for getting the ownership of a type. /// /// This trait exists so that [`TypedFunction`] can automatically generate diff --git a/crates/bevy_reflect/src/func/dynamic_function.rs b/crates/bevy_reflect/src/func/dynamic_function.rs index d513cb68084b5..408cdda640015 100644 --- a/crates/bevy_reflect/src/func/dynamic_function.rs +++ b/crates/bevy_reflect/src/func/dynamic_function.rs @@ -15,9 +15,6 @@ use alloc::{borrow::Cow, boxed::Box, sync::Arc}; use bevy_reflect_derive::impl_type_path; use core::fmt::{Debug, Formatter}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// An [`Arc`] containing a callback to a reflected function. /// /// The `Arc` is used to both ensure that it is `Send + Sync` @@ -473,6 +470,7 @@ mod tests { use crate::func::signature::ArgumentSignature; use crate::func::{FunctionError, IntoReturn, SignatureInfo}; use crate::Type; + use alloc::{format, string::String, vec, vec::Vec}; use bevy_utils::HashSet; use core::ops::Add; diff --git a/crates/bevy_reflect/src/func/dynamic_function_internal.rs b/crates/bevy_reflect/src/func/dynamic_function_internal.rs index 427de1263d14a..4f06c5c2d7082 100644 --- a/crates/bevy_reflect/src/func/dynamic_function_internal.rs +++ b/crates/bevy_reflect/src/func/dynamic_function_internal.rs @@ -1,7 +1,7 @@ use crate::func::args::ArgCount; use crate::func::signature::{ArgListSignature, ArgumentSignature}; use crate::func::{ArgList, FunctionError, FunctionInfo, FunctionOverloadError}; -use alloc::borrow::Cow; +use alloc::{borrow::Cow, vec, vec::Vec}; use bevy_utils::HashMap; use core::fmt::{Debug, Formatter}; diff --git a/crates/bevy_reflect/src/func/dynamic_function_mut.rs b/crates/bevy_reflect/src/func/dynamic_function_mut.rs index bcbf6a96337ab..c9615f2d91ee0 100644 --- a/crates/bevy_reflect/src/func/dynamic_function_mut.rs +++ b/crates/bevy_reflect/src/func/dynamic_function_mut.rs @@ -1,9 +1,6 @@ use alloc::{borrow::Cow, boxed::Box, sync::Arc}; use core::fmt::{Debug, Formatter}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - use crate::func::{ args::{ArgCount, ArgList}, dynamic_function_internal::DynamicFunctionInternal, @@ -381,6 +378,7 @@ impl<'env> IntoFunctionMut<'env, ()> for DynamicFunctionMut<'env> { mod tests { use super::*; use crate::func::{FunctionError, IntoReturn, SignatureInfo}; + use alloc::vec; use core::ops::Add; #[test] diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index ad6796be19f1e..7f0fae4662aef 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -7,9 +7,6 @@ use alloc::borrow::Cow; use bevy_utils::HashSet; use thiserror::Error; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// An error that occurs when calling a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction diff --git a/crates/bevy_reflect/src/func/function.rs b/crates/bevy_reflect/src/func/function.rs index 0d8e94ca95aff..2c4c8f7527d0f 100644 --- a/crates/bevy_reflect/src/func/function.rs +++ b/crates/bevy_reflect/src/func/function.rs @@ -8,9 +8,6 @@ use crate::{ use alloc::borrow::Cow; use core::fmt::Debug; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A trait used to power [function-like] operations via [reflection]. /// /// This trait allows types to be called like regular functions @@ -74,6 +71,7 @@ pub trait Function: PartialReflect + Debug { mod tests { use super::*; use crate::func::IntoFunction; + use alloc::boxed::Box; #[test] fn should_call_dyn_function() { diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 797b97e7e1bac..53737fd891dbd 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -1,9 +1,6 @@ -use alloc::{borrow::Cow, vec}; +use alloc::{borrow::Cow, boxed::Box, vec, vec::Vec}; use core::fmt::{Debug, Formatter}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - use crate::{ func::args::{ArgCount, ArgCountOutOfBoundsError, ArgInfo, GetOwnership, Ownership}, func::signature::ArgumentSignature, @@ -615,7 +612,6 @@ macro_rules! impl_typed_function { FunctionInfo::new( create_info::() .with_args({ - #[allow(unused_mut)] let mut _index = 0; vec![ $(ArgInfo::new::<$Arg>({ @@ -641,7 +637,6 @@ macro_rules! impl_typed_function { FunctionInfo::new( create_info::() .with_args({ - #[allow(unused_mut)] let mut _index = 1; vec![ ArgInfo::new::<&Receiver>(0), @@ -668,7 +663,6 @@ macro_rules! impl_typed_function { FunctionInfo::new( create_info::() .with_args({ - #[allow(unused_mut)] let mut _index = 1; vec![ ArgInfo::new::<&mut Receiver>(0), @@ -695,7 +689,6 @@ macro_rules! impl_typed_function { FunctionInfo::new( create_info::() .with_args({ - #[allow(unused_mut)] let mut _index = 1; vec![ ArgInfo::new::<&mut Receiver>(0), diff --git a/crates/bevy_reflect/src/func/into_function.rs b/crates/bevy_reflect/src/func/into_function.rs index e913045f8cc2a..d3276a6bd7828 100644 --- a/crates/bevy_reflect/src/func/into_function.rs +++ b/crates/bevy_reflect/src/func/into_function.rs @@ -1,8 +1,5 @@ use crate::func::{DynamicFunction, ReflectFn, TypedFunction}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A trait for types that can be converted into a [`DynamicFunction`]. /// /// This trait is automatically implemented for any type that implements diff --git a/crates/bevy_reflect/src/func/into_function_mut.rs b/crates/bevy_reflect/src/func/into_function_mut.rs index 8f7f1b0a6dd1a..38eae0aee4f84 100644 --- a/crates/bevy_reflect/src/func/into_function_mut.rs +++ b/crates/bevy_reflect/src/func/into_function_mut.rs @@ -1,8 +1,5 @@ use crate::func::{DynamicFunctionMut, ReflectFnMut, TypedFunction}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// A trait for types that can be converted into a [`DynamicFunctionMut`]. /// /// This trait is automatically implemented for any type that implements diff --git a/crates/bevy_reflect/src/func/macros.rs b/crates/bevy_reflect/src/func/macros.rs index 410aaba456acd..3fb93a2230610 100644 --- a/crates/bevy_reflect/src/func/macros.rs +++ b/crates/bevy_reflect/src/func/macros.rs @@ -1,6 +1,3 @@ -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// Helper macro to implement the necessary traits for function reflection. /// /// This macro calls the following macros: diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index f990e135f017d..f7d581ab465b8 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -96,7 +96,7 @@ //! //! # Generic Functions //! -//! In Rust, generic functions are [monomophized] by the compiler, +//! In Rust, generic functions are [monomorphized] by the compiler, //! which means that a separate copy of the function is generated for each concrete set of type parameters. //! //! When converting a generic function to a [`DynamicFunction`] or [`DynamicFunctionMut`], @@ -153,7 +153,7 @@ //! [`Reflect`]: crate::Reflect //! [lack of variadic generics]: https://poignardazur.github.io/2024/05/25/report-on-rustnl-variadics/ //! [coherence issues]: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#coherence-leak-check -//! [monomophized]: https://en.wikipedia.org/wiki/Monomorphization +//! [monomorphized]: https://en.wikipedia.org/wiki/Monomorphization //! [overloading]: #overloading-functions //! [function overloading]: https://en.wikipedia.org/wiki/Function_overloading //! [variadic functions]: https://en.wikipedia.org/wiki/Variadic_function diff --git a/crates/bevy_reflect/src/func/reflect_fn.rs b/crates/bevy_reflect/src/func/reflect_fn.rs index 38a18141fcf43..98e1b010f3dca 100644 --- a/crates/bevy_reflect/src/func/reflect_fn.rs +++ b/crates/bevy_reflect/src/func/reflect_fn.rs @@ -1,8 +1,5 @@ use variadics_please::all_tuples; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - use crate::{ func::{ args::{ArgCount, FromArg}, @@ -91,7 +88,14 @@ macro_rules! impl_reflect_fn { // This clause essentially asserts that `Arg::This` is the same type as `Arg` Function: for<'a> Fn($($Arg::This<'a>),*) -> ReturnType + 'env, { - #[allow(unused_mut)] + #[expect( + clippy::allow_attributes, + reason = "This lint is part of a macro, which may not always trigger the `unused_mut` lint." + )] + #[allow( + unused_mut, + reason = "Some invocations of this macro may trigger the `unused_mut` lint, where others won't." + )] fn reflect_call<'a>(&self, mut args: ArgList<'a>) -> FunctionResult<'a> { const COUNT: usize = count_tokens!($($Arg)*); diff --git a/crates/bevy_reflect/src/func/reflect_fn_mut.rs b/crates/bevy_reflect/src/func/reflect_fn_mut.rs index 760e657037c5b..15353e46b8741 100644 --- a/crates/bevy_reflect/src/func/reflect_fn_mut.rs +++ b/crates/bevy_reflect/src/func/reflect_fn_mut.rs @@ -1,8 +1,5 @@ use variadics_please::all_tuples; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - use crate::{ func::{ args::{ArgCount, FromArg}, @@ -98,7 +95,14 @@ macro_rules! impl_reflect_fn_mut { // This clause essentially asserts that `Arg::This` is the same type as `Arg` Function: for<'a> FnMut($($Arg::This<'a>),*) -> ReturnType + 'env, { - #[allow(unused_mut)] + #[expect( + clippy::allow_attributes, + reason = "This lint is part of a macro, which may not always trigger the `unused_mut` lint." + )] + #[allow( + unused_mut, + reason = "Some invocations of this macro may trigger the `unused_mut` lint, where others won't." + )] fn reflect_call_mut<'a>(&mut self, mut args: ArgList<'a>) -> FunctionResult<'a> { const COUNT: usize = count_tokens!($($Arg)*); diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs index 82d1f1542a2b5..4bb38603fbea5 100644 --- a/crates/bevy_reflect/src/func/registry.rs +++ b/crates/bevy_reflect/src/func/registry.rs @@ -2,9 +2,6 @@ use alloc::{borrow::Cow, sync::Arc}; use core::fmt::Debug; use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - use bevy_utils::HashMap; use crate::func::{ @@ -173,7 +170,7 @@ impl FunctionRegistry { /// .register_with_name(core::any::type_name_of_val(&mul), mul)? /// // Registering an existing function with a custom name /// .register_with_name("my_crate::mul", mul)?; - /// + /// /// // Be careful not to register anonymous functions with their type name. /// // This code works but registers the function with a non-unique name like `foo::bar::{{closure}}` /// registry.register_with_name(core::any::type_name_of_val(&div), div)?; @@ -359,6 +356,7 @@ impl FunctionRegistryArc { mod tests { use super::*; use crate::func::{ArgList, IntoFunction}; + use alloc::format; #[test] fn should_register_function() { diff --git a/crates/bevy_reflect/src/func/return_type.rs b/crates/bevy_reflect/src/func/return_type.rs index 3d1153912cc75..bab3c04b25962 100644 --- a/crates/bevy_reflect/src/func/return_type.rs +++ b/crates/bevy_reflect/src/func/return_type.rs @@ -1,9 +1,6 @@ use crate::PartialReflect; use alloc::boxed::Box; -#[cfg(not(feature = "std"))] -use alloc::{format, vec}; - /// The return type of a [`DynamicFunction`] or [`DynamicFunctionMut`]. /// /// [`DynamicFunction`]: crate::func::DynamicFunction diff --git a/crates/bevy_reflect/src/func/signature.rs b/crates/bevy_reflect/src/func/signature.rs index 965ff401e00b8..278f18f1f5776 100644 --- a/crates/bevy_reflect/src/func/signature.rs +++ b/crates/bevy_reflect/src/func/signature.rs @@ -14,6 +14,7 @@ use crate::func::args::ArgInfo; use crate::func::{ArgList, SignatureInfo}; use crate::Type; +use alloc::boxed::Box; use bevy_utils::hashbrown::Equivalent; use core::borrow::Borrow; use core::fmt::{Debug, Formatter}; @@ -203,6 +204,7 @@ impl From<&ArgList<'_>> for ArgumentSignature { mod tests { use super::*; use crate::func::TypedFunction; + use alloc::{format, string::String, vec}; #[test] fn should_generate_signature_from_function_info() { diff --git a/crates/bevy_reflect/src/generics.rs b/crates/bevy_reflect/src/generics.rs index fa91fc35c5126..f64cefe2d9ed2 100644 --- a/crates/bevy_reflect/src/generics.rs +++ b/crates/bevy_reflect/src/generics.rs @@ -241,6 +241,7 @@ mod tests { use super::*; use crate as bevy_reflect; use crate::{Reflect, Typed}; + use alloc::string::String; use core::fmt::Debug; #[test] diff --git a/crates/bevy_reflect/src/impls/glam.rs b/crates/bevy_reflect/src/impls/glam.rs index ba1fa00549329..a9e451bf63d98 100644 --- a/crates/bevy_reflect/src/impls/glam.rs +++ b/crates/bevy_reflect/src/impls/glam.rs @@ -4,16 +4,16 @@ use assert_type_match::assert_type_match; use bevy_reflect_derive::{impl_reflect, impl_reflect_opaque}; use glam::*; -#[cfg(not(feature = "std"))] -use alloc::format; - /// Reflects the given foreign type as an enum and asserts that the variants/fields match up. macro_rules! reflect_enum { ($(#[$meta:meta])* enum $ident:ident { $($ty:tt)* } ) => { impl_reflect!($(#[$meta])* enum $ident { $($ty)* }); #[assert_type_match($ident, test_only)] - #[allow(clippy::upper_case_acronyms)] + #[expect( + clippy::upper_case_acronyms, + reason = "The variants used are not acronyms." + )] enum $ident { $($ty)* } }; } @@ -381,6 +381,7 @@ impl_reflect_opaque!(::glam::BVec4A(Debug, Default, Deserialize, Serialize)); #[cfg(test)] mod tests { + use alloc::{format, string::String}; use ron::{ ser::{to_string_pretty, PrettyConfig}, Deserializer, diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index 793ca2001ccc1..5d92b03181fb9 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -1,11 +1,8 @@ -use alloc::boxed::Box; +use alloc::{boxed::Box, vec::Vec}; use bevy_reflect_derive::impl_type_path; use core::any::Any; use smallvec::{Array as SmallArray, SmallVec}; -#[cfg(not(feature = "std"))] -use alloc::{format, vec}; - use crate::{ self as bevy_reflect, utility::GenericTypeInfoCell, ApplyError, FromReflect, FromType, Generics, GetTypeRegistration, List, ListInfo, ListIter, MaybeTyped, PartialReflect, Reflect, diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 6679fa63151fe..a765dbd4bf7b5 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1,5 +1,7 @@ -// Temporary workaround for impl_reflect!(Option/Result false-positive -#![allow(unused_qualifications)] +#![expect( + unused_qualifications, + reason = "Temporary workaround for impl_reflect!(Option/Result false-positive" +)] use crate::{ self as bevy_reflect, impl_type_path, map_apply, map_partial_eq, map_try_apply, @@ -116,7 +118,7 @@ impl_reflect_opaque!(::core::ops::RangeTo()); impl_reflect_opaque!(::core::ops::RangeToInclusive()); impl_reflect_opaque!(::core::ops::RangeFull()); impl_reflect_opaque!(::core::ops::Bound()); -impl_reflect_opaque!(::bevy_utils::Duration( +impl_reflect_opaque!(::core::time::Duration( Debug, Hash, PartialEq, @@ -236,7 +238,6 @@ macro_rules! impl_reflect_for_atomic { #[cfg(feature = "functions")] crate::func::macros::impl_function_traits!($ty); - #[allow(unused_mut)] impl GetTypeRegistration for $ty where $ty: Any + Send + Sync, @@ -366,10 +367,12 @@ 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 @@ -920,7 +923,7 @@ macro_rules! impl_reflect_for_hashset { from_reflect = V::from_reflect(value); from_reflect.as_ref() }) - .map_or(false, |value| self.remove(value)) + .is_some_and(|value| self.remove(value)) } fn contains(&self, value: &dyn PartialReflect) -> bool { @@ -931,7 +934,7 @@ macro_rules! impl_reflect_for_hashset { from_reflect = V::from_reflect(value); from_reflect.as_ref() }) - .map_or(false, |value| self.contains(value)) + .is_some_and(|value| self.contains(value)) } } @@ -2428,9 +2431,12 @@ mod tests { self as bevy_reflect, Enum, FromReflect, PartialReflect, Reflect, ReflectSerialize, TypeInfo, TypeRegistry, Typed, VariantInfo, VariantType, }; - use alloc::collections::BTreeMap; - use bevy_utils::{Duration, HashMap, Instant}; - use core::f32::consts::{PI, TAU}; + use alloc::{collections::BTreeMap, string::String, vec}; + use bevy_utils::{HashMap, Instant}; + use core::{ + f32::consts::{PI, TAU}, + time::Duration, + }; use static_assertions::assert_impl_all; use std::path::Path; diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index d5d16715c09c4..3eef10d0e55eb 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -274,6 +274,7 @@ impl ReflectOwned { #[cfg(test)] mod tests { + use alloc::vec; use std::collections::HashSet; use super::*; diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index c6e5ba0b4515d..5604adeacd6ce 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -557,7 +557,10 @@ //! [`ArgList`]: crate::func::ArgList //! [derive `Reflect`]: derive@crate::Reflect -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; extern crate alloc; @@ -683,7 +686,10 @@ pub mod __macro_exports { note = "consider annotating `{Self}` with `#[derive(Reflect)]`" )] pub trait RegisterForReflection { - #[allow(unused_variables)] + #[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 __register(registry: &mut TypeRegistry) {} } @@ -709,10 +715,20 @@ pub mod __macro_exports { } #[cfg(test)] -#[allow(clippy::disallowed_types, clippy::approx_constant)] +#[expect( + clippy::approx_constant, + reason = "We don't need the exact value of Pi here." +)] mod tests { use ::serde::{de::DeserializeSeed, Deserialize, Serialize}; - use alloc::borrow::Cow; + use alloc::{ + borrow::Cow, + boxed::Box, + format, + string::{String, ToString}, + vec, + vec::Vec, + }; use bevy_utils::HashMap; use core::{ any::TypeId, @@ -866,7 +882,6 @@ mod tests { } #[test] - #[allow(clippy::disallowed_types)] fn reflect_unit_struct() { #[derive(Reflect)] struct Foo(u32, u64); @@ -2138,7 +2153,7 @@ mod tests { enum_struct: SomeEnum, custom: CustomDebug, #[reflect(ignore)] - #[allow(dead_code)] + #[expect(dead_code, reason = "This value is intended to not be reflected.")] ignored: isize, } @@ -2556,6 +2571,8 @@ bevy_reflect::tests::Test { #[test] fn should_reflect_remote_type() { mod external_crate { + use alloc::string::String; + #[derive(Debug, Default)] pub struct TheirType { pub value: String, @@ -2631,6 +2648,8 @@ bevy_reflect::tests::Test { #[test] fn should_reflect_remote_value_type() { mod external_crate { + use alloc::string::String; + #[derive(Clone, Debug, Default)] pub struct TheirType { pub value: String, @@ -2714,6 +2733,8 @@ bevy_reflect::tests::Test { // error[E0433]: failed to resolve: use of undeclared crate or module `external_crate` // ``` pub mod external_crate { + use alloc::string::String; + pub struct TheirType { pub value: String, } @@ -2735,6 +2756,8 @@ bevy_reflect::tests::Test { #[test] fn should_reflect_remote_enum() { mod external_crate { + use alloc::string::String; + #[derive(Debug, PartialEq, Eq)] pub enum TheirType { Unit, @@ -2899,6 +2922,8 @@ bevy_reflect::tests::Test { #[test] fn should_take_remote_type() { mod external_crate { + use alloc::string::String; + #[derive(Debug, Default, PartialEq, Eq)] pub struct TheirType { pub value: String, @@ -2931,6 +2956,8 @@ bevy_reflect::tests::Test { #[test] fn should_try_take_remote_type() { mod external_crate { + use alloc::string::String; + #[derive(Debug, Default, PartialEq, Eq)] pub struct TheirType { pub value: String, diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 58fca368b88da..2aff62241f252 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -535,6 +535,7 @@ pub fn list_debug(dyn_list: &dyn List, f: &mut Formatter<'_>) -> core::fmt::Resu mod tests { use super::DynamicList; use crate::Reflect; + use alloc::{boxed::Box, vec}; use core::assert_eq; #[test] diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index e5205e90afa38..74743a0df1cbe 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -631,6 +631,10 @@ pub fn map_try_apply(a: &mut M, b: &dyn PartialReflect) -> Result<(), Ap #[cfg(test)] mod tests { use super::{DynamicMap, Map}; + use alloc::{ + borrow::ToOwned, + string::{String, ToString}, + }; #[test] fn test_into_iter() { diff --git a/crates/bevy_reflect/src/path/mod.rs b/crates/bevy_reflect/src/path/mod.rs index 3fe0504cf7408..320c414cdeed2 100644 --- a/crates/bevy_reflect/src/path/mod.rs +++ b/crates/bevy_reflect/src/path/mod.rs @@ -127,10 +127,12 @@ impl<'a> ReflectPath<'a> for &'a str { /// Note that a leading dot (`.`) or hash (`#`) token is implied for the first item in a path, /// and may therefore be omitted. /// +/// Additionally, an empty path may be used to get the struct itself. +/// /// ### Example /// ``` /// # use bevy_reflect::{GetPath, Reflect}; -/// #[derive(Reflect)] +/// #[derive(Reflect, PartialEq, Debug)] /// struct MyStruct { /// value: u32 /// } @@ -140,6 +142,8 @@ impl<'a> ReflectPath<'a> for &'a str { /// assert_eq!(my_struct.path::(".value").unwrap(), &123); /// // Access via field index /// assert_eq!(my_struct.path::("#0").unwrap(), &123); +/// // Access self +/// assert_eq!(*my_struct.path::("").unwrap(), my_struct); /// ``` /// /// ## Tuples and Tuple Structs @@ -502,13 +506,17 @@ impl core::ops::IndexMut for ParsedPath { } #[cfg(test)] -#[allow(clippy::float_cmp, clippy::approx_constant)] +#[expect( + clippy::approx_constant, + reason = "We don't need the exact value of Pi here." +)] mod tests { use super::*; use crate as bevy_reflect; use crate::*; + use alloc::vec; - #[derive(Reflect)] + #[derive(Reflect, PartialEq, Debug)] struct A { w: usize, x: B, @@ -521,21 +529,21 @@ mod tests { tuple: (bool, f32), } - #[derive(Reflect)] + #[derive(Reflect, PartialEq, Debug)] struct B { foo: usize, łørđ: C, } - #[derive(Reflect)] + #[derive(Reflect, PartialEq, Debug)] struct C { mосква: f32, } - #[derive(Reflect)] + #[derive(Reflect, PartialEq, Debug)] struct D(E); - #[derive(Reflect)] + #[derive(Reflect, PartialEq, Debug)] struct E(f32, usize); #[derive(Reflect, PartialEq, Debug)] @@ -735,6 +743,7 @@ mod tests { fn reflect_path() { let mut a = a_sample(); + assert_eq!(*a.path::("").unwrap(), a); assert_eq!(*a.path::("w").unwrap(), 1); assert_eq!(*a.path::("x.foo").unwrap(), 10); assert_eq!(*a.path::("x.łørđ.mосква").unwrap(), 3.14); diff --git a/crates/bevy_reflect/src/path/parse.rs b/crates/bevy_reflect/src/path/parse.rs index bc48fe9c01be0..2ab2939a30ae4 100644 --- a/crates/bevy_reflect/src/path/parse.rs +++ b/crates/bevy_reflect/src/path/parse.rs @@ -64,7 +64,10 @@ impl<'a> PathParser<'a> { // the last byte before an ASCII utf-8 character (ie: it is a char // boundary). // - The slice always starts after a symbol ie: an ASCII character's boundary. - #[allow(unsafe_code)] + #[expect( + unsafe_code, + reason = "We have fulfilled the Safety requirements for `from_utf8_unchecked`." + )] let ident = unsafe { from_utf8_unchecked(ident) }; self.remaining = remaining; diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index bf7844dbde91b..0898ff3630fe9 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -17,7 +17,7 @@ use crate::utility::NonGenericTypeInfoCell; #[derive(Error, Debug)] 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 a enum. + /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to an enum. MismatchedKinds { from_kind: ReflectKind, to_kind: ReflectKind, @@ -352,8 +352,7 @@ impl dyn PartialReflect { #[inline] pub fn represents(&self) -> bool { self.get_represented_type_info() - .map(|t| t.type_path() == T::type_path()) - .unwrap_or(false) + .is_some_and(|t| t.type_path() == T::type_path()) } /// Downcasts the value to type `T`, consuming the trait object. diff --git a/crates/bevy_reflect/src/serde/de/error_utils.rs b/crates/bevy_reflect/src/serde/de/error_utils.rs index f028976805791..d570c47f0c369 100644 --- a/crates/bevy_reflect/src/serde/de/error_utils.rs +++ b/crates/bevy_reflect/src/serde/de/error_utils.rs @@ -1,6 +1,9 @@ use core::fmt::Display; use serde::de::Error; +#[cfg(feature = "debug_stack")] +use std::thread_local; + #[cfg(feature = "debug_stack")] thread_local! { /// The thread-local [`TypeInfoStack`] used for debugging. diff --git a/crates/bevy_reflect/src/serde/de/mod.rs b/crates/bevy_reflect/src/serde/de/mod.rs index e55897166e926..e8b2df862ddb6 100644 --- a/crates/bevy_reflect/src/serde/de/mod.rs +++ b/crates/bevy_reflect/src/serde/de/mod.rs @@ -24,11 +24,16 @@ mod tuples; #[cfg(test)] mod tests { + use alloc::{ + boxed::Box, + string::{String, ToString}, + vec, + vec::Vec, + }; use bincode::Options; use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive}; - use serde::{de::IgnoredAny, Deserializer}; - use serde::{de::DeserializeSeed, Deserialize}; + use serde::{de::IgnoredAny, Deserializer}; use bevy_utils::{HashMap, HashSet}; diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 3fcaa6aafc7b3..dcc38c3cc5987 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -189,7 +189,7 @@ mod tests { use crate::serde::{DeserializeWithRegistry, ReflectDeserializeWithRegistry}; use crate::serde::{ReflectSerializeWithRegistry, SerializeWithRegistry}; use crate::{ReflectFromReflect, TypePath}; - use alloc::sync::Arc; + use alloc::{format, string::String, sync::Arc, vec, vec::Vec}; use bevy_reflect_derive::reflect_trait; use core::any::TypeId; use core::fmt::{Debug, Formatter}; @@ -199,7 +199,7 @@ mod tests { #[reflect_trait] trait Enemy: Reflect + Debug { - #[allow(dead_code, reason = "this method is purely for testing purposes")] + #[expect(dead_code, reason = "this method is purely for testing purposes")] fn hp(&self) -> u8; } diff --git a/crates/bevy_reflect/src/serde/ser/error_utils.rs b/crates/bevy_reflect/src/serde/ser/error_utils.rs index 8e6570c6691a2..d252e7f591d69 100644 --- a/crates/bevy_reflect/src/serde/ser/error_utils.rs +++ b/crates/bevy_reflect/src/serde/ser/error_utils.rs @@ -1,6 +1,9 @@ use core::fmt::Display; use serde::ser::Error; +#[cfg(feature = "debug_stack")] +use std::thread_local; + #[cfg(feature = "debug_stack")] thread_local! { /// The thread-local [`TypeInfoStack`] used for debugging. diff --git a/crates/bevy_reflect/src/serde/ser/mod.rs b/crates/bevy_reflect/src/serde/ser/mod.rs index 53afacde37430..77f2b9d0fe5d6 100644 --- a/crates/bevy_reflect/src/serde/ser/mod.rs +++ b/crates/bevy_reflect/src/serde/ser/mod.rs @@ -25,6 +25,12 @@ mod tests { serde::{ReflectSerializer, ReflectSerializerProcessor}, PartialReflect, Reflect, ReflectSerialize, Struct, TypeRegistry, }; + use alloc::{ + boxed::Box, + string::{String, ToString}, + vec, + vec::Vec, + }; use bevy_utils::{HashMap, HashSet}; use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive}; use ron::{extensions::Extensions, ser::PrettyConfig}; @@ -647,6 +653,7 @@ mod tests { mod functions { use super::*; use crate::func::{DynamicFunction, IntoFunction}; + use alloc::string::ToString; #[test] fn should_not_serialize_function() { 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 25922a5bd9456..9c5bfb06f1ca8 100644 --- a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -75,7 +75,7 @@ impl FromType for ReflectSerializeWithReg serialize: |value: &dyn Reflect, registry| { let value = value.downcast_ref::().unwrap_or_else(|| { panic!( - "Expected value to be of type {:?} but received {:?}", + "Expected value to be of type {} but received {}", core::any::type_name::(), value.reflect_type_path() ) diff --git a/crates/bevy_reflect/src/serde/ser/structs.rs b/crates/bevy_reflect/src/serde/ser/structs.rs index 4eb3e76700d57..828eb3e6cb829 100644 --- a/crates/bevy_reflect/src/serde/ser/structs.rs +++ b/crates/bevy_reflect/src/serde/ser/structs.rs @@ -48,10 +48,7 @@ impl Serialize for StructSerializer<'_, P> { )?; for (index, value) in self.struct_value.iter_fields().enumerate() { - if serialization_data - .map(|data| data.is_field_skipped(index)) - .unwrap_or(false) - { + if serialization_data.is_some_and(|data| data.is_field_skipped(index)) { continue; } let key = struct_info.field_at(index).unwrap().name(); diff --git a/crates/bevy_reflect/src/serde/ser/tuple_structs.rs b/crates/bevy_reflect/src/serde/ser/tuple_structs.rs index 5bf2ec64ae7e0..00554c0a86694 100644 --- a/crates/bevy_reflect/src/serde/ser/tuple_structs.rs +++ b/crates/bevy_reflect/src/serde/ser/tuple_structs.rs @@ -57,10 +57,7 @@ impl Serialize for TupleStructSerializer<'_, P> { )?; for (index, value) in self.tuple_struct.iter_fields().enumerate() { - if serialization_data - .map(|data| data.is_field_skipped(index)) - .unwrap_or(false) - { + if serialization_data.is_some_and(|data| data.is_field_skipped(index)) { continue; } state.serialize_field(&TypedReflectSerializer::new_internal( diff --git a/crates/bevy_reflect/src/set.rs b/crates/bevy_reflect/src/set.rs index 0d46d9f9df771..663b99715c0ee 100644 --- a/crates/bevy_reflect/src/set.rs +++ b/crates/bevy_reflect/src/set.rs @@ -498,6 +498,7 @@ pub fn set_try_apply(a: &mut S, b: &dyn PartialReflect) -> Result<(), Ap #[cfg(test)] mod tests { use super::DynamicSet; + use alloc::string::{String, ToString}; #[test] fn test_into_iter() { diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 4ccac40508a43..2add261aa2b68 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -547,6 +547,8 @@ pub(crate) use impl_type_methods; /// For example, [`i32`] cannot be broken down any further, so it is represented by an [`OpaqueInfo`]. /// And while [`String`] itself is a struct, its fields are private, so we don't really treat /// it _as_ a struct. It therefore makes more sense to represent it as an [`OpaqueInfo`]. +/// +/// [`String`]: alloc::string::String #[derive(Debug, Clone)] pub struct OpaqueInfo { ty: Type, @@ -585,6 +587,7 @@ impl OpaqueInfo { #[cfg(test)] mod tests { use super::*; + use alloc::vec::Vec; #[test] fn should_return_error_on_invalid_cast() { diff --git a/crates/bevy_reflect/src/type_info_stack.rs b/crates/bevy_reflect/src/type_info_stack.rs index 8f1161485f1aa..cdc19244de295 100644 --- a/crates/bevy_reflect/src/type_info_stack.rs +++ b/crates/bevy_reflect/src/type_info_stack.rs @@ -1,12 +1,10 @@ use crate::TypeInfo; +use alloc::vec::Vec; use core::{ fmt::{Debug, Formatter}, slice::Iter, }; -#[cfg(not(feature = "std"))] -use alloc::{boxed::Box, format, vec}; - /// Helper struct for managing a stack of [`TypeInfo`] instances. /// /// This is useful for tracking the type hierarchy when serializing and deserializing types. diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 609c66d1856ac..6c97825bccca3 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -79,8 +79,7 @@ pub trait GetTypeRegistration: 'static { /// /// This method is called by [`TypeRegistry::register`] to register any other required types. /// Often, this is done for fields of structs and enum variants to ensure all types are properly registered. - #[allow(unused_variables)] - fn register_type_dependencies(registry: &mut TypeRegistry) {} + fn register_type_dependencies(_registry: &mut TypeRegistry) {} } impl Default for TypeRegistry { @@ -785,7 +784,10 @@ pub struct ReflectFromPtr { from_ptr_mut: unsafe fn(PtrMut) -> &mut dyn Reflect, } -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "We must interact with pointers here, which are inherently unsafe." +)] impl ReflectFromPtr { /// Returns the [`TypeId`] that the [`ReflectFromPtr`] was constructed for. pub fn type_id(&self) -> TypeId { @@ -837,7 +839,10 @@ impl ReflectFromPtr { } } -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "We must interact with pointers here, which are inherently unsafe." +)] impl FromType for ReflectFromPtr { fn from_type() -> Self { ReflectFromPtr { @@ -857,7 +862,10 @@ impl FromType for ReflectFromPtr { } #[cfg(test)] -#[allow(unsafe_code)] +#[expect( + unsafe_code, + reason = "We must interact with pointers here, which are inherently unsafe." +)] mod test { use super::*; use crate as bevy_reflect; diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index f106baf622135..996a661c70a49 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -24,6 +24,7 @@ pub trait TypedProperty: sealed::Sealed { /// Used to store a [`String`] in a [`GenericTypePathCell`] as part of a [`TypePath`] implementation. /// /// [`TypePath`]: crate::TypePath +/// [`String`]: alloc::string::String pub struct TypePathComponent; mod sealed { diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 4a12f7742c997..92a261ea7993a 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_remote" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "The Bevy Remote Protocol" homepage = "https://bevyengine.org" @@ -14,15 +14,15 @@ http = ["dep:async-io", "dep:smol-hyper"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", features = [ +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 = [ "serialize", ] } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" } +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" } # other anyhow = "1" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 840cc102eabea..a36c2d92e8c1b 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,19 +8,21 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, query::QueryBuilder, - reflect::{AppTypeRegistry, ReflectComponent}, + reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; use bevy_hierarchy::BuildChildren as _; use bevy_reflect::{ + prelude::ReflectDefault, serde::{ReflectSerializer, TypedReflectDeserializer}, - PartialReflect, TypeRegistration, TypeRegistry, + GetPath as _, NamedField, OpaqueInfo, PartialReflect, ReflectDeserialize, ReflectSerialize, + TypeInfo, TypeRegistration, TypeRegistry, VariantInfo, }; use bevy_utils::HashMap; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; -use serde_json::{Map, Value}; +use serde_json::{json, Map, Value}; use crate::{error_codes, BrpError, BrpResult}; @@ -48,12 +50,18 @@ pub const BRP_REPARENT_METHOD: &str = "bevy/reparent"; /// The method path for a `bevy/list` request. pub const BRP_LIST_METHOD: &str = "bevy/list"; +/// The method path for a `bevy/reparent` request. +pub const BRP_MUTATE_COMPONENT_METHOD: &str = "bevy/mutate_component"; + /// The method path for a `bevy/get+watch` request. pub const BRP_GET_AND_WATCH_METHOD: &str = "bevy/get+watch"; /// The method path for a `bevy/list+watch` request. pub const BRP_LIST_AND_WATCH_METHOD: &str = "bevy/list+watch"; +/// The method path for a `bevy/registry/schema` request. +pub const BRP_REGISTRY_SCHEMA_METHOD: &str = "bevy/registry/schema"; + /// `bevy/get`: Retrieves one or more components from the entity with the given /// ID. /// @@ -192,6 +200,28 @@ pub struct BrpListParams { pub entity: Entity, } +/// `bevy/mutate`: +/// +/// The server responds with a null. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct BrpMutateParams { + /// The entity of the component to mutate. + pub entity: Entity, + + /// The [full path] of component to mutate. + /// + /// [full path]: bevy_reflect::TypePath::type_path + pub component: String, + + /// The [path] of the field within the component. + /// + /// [path]: bevy_reflect::GetPath + pub path: String, + + /// The value to insert at `path`. + pub value: Value, +} + /// Describes the data that is to be fetched in a query. #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] pub struct BrpQuery { @@ -236,6 +266,38 @@ pub struct BrpQueryFilter { pub with: Vec, } +/// Constraints that can be placed on a query to include or exclude +/// certain definitions. +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] +pub struct BrpJsonSchemaQueryFilter { + /// The crate name of the type name of each component that must not be + /// present on the entity for it to be included in the results. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub without_crates: Vec, + + /// The crate name of the type name of each component that must be present + /// on the entity for it to be included in the results. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub with_crates: Vec, + + /// Constrain resource by type + #[serde(default)] + pub type_limit: JsonSchemaTypeLimit, +} + +/// Additional [`BrpJsonSchemaQueryFilter`] constraints that can be placed on a query to include or exclude +/// certain definitions. +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq)] +pub struct JsonSchemaTypeLimit { + /// Schema cannot have specified reflect types + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub without: Vec, + + /// Schema needs to have specified reflect types + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub with: Vec, +} + /// A response from the world to the client that specifies a single entity. /// /// This is sent in response to `bevy/spawn`. @@ -657,6 +719,70 @@ pub fn process_remote_insert_request( Ok(Value::Null) } +/// Handles a `bevy/mutate_component` request coming from a client. +/// +/// This method allows you to mutate a single field inside an Entity's +/// component. +pub fn process_remote_mutate_component_request( + In(params): In>, + world: &mut World, +) -> BrpResult { + let BrpMutateParams { + entity, + component, + path, + value, + } = parse_some(params)?; + let app_type_registry = world.resource::().clone(); + let type_registry = app_type_registry.read(); + + // Get the fully-qualified type names of the component to be mutated. + let component_type: &TypeRegistration = type_registry + .get_with_type_path(&component) + .ok_or_else(|| { + BrpError::component_error(anyhow!("Unknown component type: `{}`", component)) + })?; + + // Get the reflected representation of the component. + let mut reflected = component_type + .data::() + .ok_or_else(|| { + BrpError::component_error(anyhow!("Component `{}` isn't registered.", component)) + })? + .reflect_mut(world.entity_mut(entity)) + .ok_or_else(|| { + BrpError::component_error(anyhow!("Cannot reflect component `{}`", component)) + })?; + + // Get the type of the field in the component that is to be + // mutated. + let value_type: &TypeRegistration = type_registry + .get_with_type_path( + reflected + .reflect_path(path.as_str()) + .map_err(BrpError::component_error)? + .reflect_type_path(), + ) + .ok_or_else(|| { + BrpError::component_error(anyhow!("Unknown component field type: `{}`", component)) + })?; + + // Get the reflected representation of the value to be inserted + // into the component. + let value: Box = TypedReflectDeserializer::new(value_type, &type_registry) + .deserialize(&value) + .map_err(BrpError::component_error)?; + + // Apply the mutation. + reflected + .reflect_path_mut(path.as_str()) + .map_err(BrpError::component_error)? + .try_apply(value.as_ref()) + .map_err(BrpError::component_error)?; + + Ok(Value::Null) +} + /// Handles a `bevy/remove` request (remove components) coming from a client. pub fn process_remote_remove_request( In(params): In>, @@ -801,6 +927,389 @@ pub fn process_remote_list_watching_request( } } +/// Handles a `bevy/registry/schema` request (list all registry types in form of schema) coming from a client. +pub fn export_registry_types(In(params): In>, world: &World) -> BrpResult { + let filter: BrpJsonSchemaQueryFilter = match params { + None => Default::default(), + Some(params) => parse(params)?, + }; + + let types = world.resource::(); + let types = types.read(); + let schemas = types + .iter() + .map(export_type) + .filter(|(_, schema)| { + if let Some(crate_name) = &schema.crate_name { + if !filter.with_crates.is_empty() + && !filter.with_crates.iter().any(|c| crate_name.eq(c)) + { + return false; + } + if !filter.without_crates.is_empty() + && filter.without_crates.iter().any(|c| crate_name.eq(c)) + { + return false; + } + } + if !filter.type_limit.with.is_empty() + && !filter + .type_limit + .with + .iter() + .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) + { + return false; + } + if !filter.type_limit.without.is_empty() + && filter + .type_limit + .without + .iter() + .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) + { + return false; + } + + true + }) + .collect::>(); + + serde_json::to_value(schemas).map_err(BrpError::internal) +} + +/// Exports schema info for a given type +fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { + 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_registrered_reflect_types(reg), + short_path: short_path.to_owned(), + type_path: type_path.to_owned(), + crate_name: binding.crate_name().map(str::to_owned), + module_path: binding.module_path().map(str::to_owned), + ..Default::default() + }; + match t { + TypeInfo::Struct(info) => { + typed_schema.properties = info + .iter() + .map(|field| (field.name().to_owned(), field.ty().ref_type())) + .collect::>(); + typed_schema.required = info + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(|f| f.name().to_owned()) + .collect::>(); + typed_schema.additional_properties = Some(false); + typed_schema.schema_type = SchemaType::Object; + typed_schema.kind = SchemaKind::Struct; + } + TypeInfo::Enum(info) => { + typed_schema.kind = SchemaKind::Enum; + + let simple = info + .iter() + .all(|variant| matches!(variant, VariantInfo::Unit(_))); + if simple { + typed_schema.schema_type = SchemaType::String; + typed_schema.one_of = info + .iter() + .map(|variant| match variant { + VariantInfo::Unit(v) => v.name().into(), + _ => unreachable!(), + }) + .collect::>(); + } else { + typed_schema.schema_type = SchemaType::Object; + typed_schema.one_of = info + .iter() + .map(|variant| match variant { + VariantInfo::Struct(v) => json!({ + "type": "object", + "kind": "Struct", + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + "properties": v + .iter() + .map(|field| (field.name().to_owned(), field.ref_type())) + .collect::>(), + "additionalProperties": false, + "required": v + .iter() + .filter(|field| !field.type_path().starts_with("core::option::Option")) + .map(NamedField::name) + .collect::>(), + }), + VariantInfo::Tuple(v) => json!({ + "type": "array", + "kind": "Tuple", + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + "prefixItems": v + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(), + "items": false, + }), + VariantInfo::Unit(v) => json!({ + "typePath": format!("{}::{}", type_path, v.name()), + "shortPath": v.name(), + }), + }) + .collect::>(); + } + } + TypeInfo::TupleStruct(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::TupleStruct; + typed_schema.prefix_items = info + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(); + typed_schema.items = Some(false.into()); + } + TypeInfo::List(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::List; + typed_schema.items = info.item_ty().ref_type().into(); + } + TypeInfo::Array(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::Array; + typed_schema.items = info.item_ty().ref_type().into(); + } + TypeInfo::Map(info) => { + typed_schema.schema_type = SchemaType::Object; + typed_schema.kind = SchemaKind::Map; + typed_schema.key_type = info.key_ty().ref_type().into(); + typed_schema.value_type = info.value_ty().ref_type().into(); + } + TypeInfo::Tuple(info) => { + typed_schema.schema_type = SchemaType::Array; + typed_schema.kind = SchemaKind::Tuple; + typed_schema.prefix_items = info + .iter() + .map(SchemaJsonReference::ref_type) + .collect::>(); + typed_schema.items = Some(false.into()); + } + TypeInfo::Set(info) => { + typed_schema.schema_type = SchemaType::Set; + typed_schema.kind = SchemaKind::Set; + typed_schema.items = info.value_ty().ref_type().into(); + } + TypeInfo::Opaque(info) => { + typed_schema.schema_type = info.map_json_type(); + typed_schema.kind = SchemaKind::Value; + } + }; + + (t.type_path().to_owned(), typed_schema) +} + +fn get_registrered_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()); + } + } + result +} + +/// JSON Schema type for Bevy Registry Types +/// It tries to follow this standard: +/// +/// To take the full advantage from info provided by Bevy registry it provides extra fields +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "camelCase")] +pub struct JsonSchemaBevyType { + /// Bevy specific field, short path of the type. + pub short_path: String, + /// Bevy specific field, full path of the type. + pub type_path: String, + /// Bevy specific field, path of the module that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub module_path: Option, + /// Bevy specific field, name of the crate that type is part of. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub crate_name: Option, + /// Bevy specific field, names of the types that type reflects. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub reflect_types: Vec, + /// Bevy specific field, [`TypeInfo`] type mapping. + pub kind: SchemaKind, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of key of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub key_type: Option, + /// Bevy specific field, provided when [`SchemaKind`] `kind` field is equal to [`SchemaKind::Map`]. + /// + /// It contains type info of value of the Map. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub value_type: Option, + /// The type keyword is fundamental to JSON Schema. It specifies the data type for a schema. + #[serde(rename = "type")] + pub schema_type: SchemaType, + /// The behavior of this keyword depends on the presence and annotation results of "properties" + /// and "patternProperties" within the same schema object. + /// Validation with "additionalProperties" applies only to the child + /// values of instance names that do not appear in the annotation results of either "properties" or "patternProperties". + #[serde(skip_serializing_if = "Option::is_none", default)] + pub additional_properties: Option, + /// Validation succeeds if, for each name that appears in both the instance and as a name + /// within this keyword's value, the child instance for that name successfully validates + /// against the corresponding schema. + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + pub properties: HashMap, + /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub required: Vec, + /// An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub one_of: Vec, + /// Validation succeeds if each element of the instance validates against the schema at the same position, if any. This keyword does not constrain the length of the array. If the array is longer than this keyword's value, this keyword validates only the prefix of matching length. + /// + /// This keyword produces an annotation value which is the largest index to which this keyword + /// applied a subschema. The value MAY be a boolean true if a subschema was applied to every + /// index of the instance, such as is produced by the "items" keyword. + /// This annotation affects the behavior of "items" and "unevaluatedItems". + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub prefix_items: Vec, + /// This keyword applies its subschema to all instance elements at indexes greater + /// than the length of the "prefixItems" array in the same schema object, + /// as reported by the annotation result of that "prefixItems" keyword. + /// If no such annotation result exists, "items" applies its subschema to all + /// instance array elements. + /// + /// If the "items" subschema is applied to any positions within the instance array, + /// it produces an annotation result of boolean true, indicating that all remaining + /// array elements have been evaluated against this keyword's subschema. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub items: Option, +} + +/// Kind of json schema, maps [`TypeInfo`] type +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub enum SchemaKind { + /// Struct + #[default] + Struct, + /// Enum type + Enum, + /// A key-value map + Map, + /// Array + Array, + /// List + List, + /// Fixed size collection of items + Tuple, + /// Fixed size collection of items with named fields + TupleStruct, + /// Set of unique values + Set, + /// Single value, eg. primitive types + Value, +} + +/// Type of json schema +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum SchemaType { + /// Represents a string value. + String, + /// Represents a floating-point number. + Float, + + /// Represents an unsigned integer. + Uint, + + /// Represents a signed integer. + Int, + + /// Represents an object with key-value pairs. + Object, + + /// Represents an array of values. + Array, + + /// Represents a boolean value (true or false). + Boolean, + + /// Represents a set of unique values. + Set, + + /// Represents a null value. + #[default] + Null, +} + +/// Helper trait for generating json schema reference +trait SchemaJsonReference { + /// Reference to another type in schema. + /// The value `$ref` is a URI-reference that is resolved against the schema. + fn ref_type(self) -> Value; +} + +/// Helper trait for mapping bevy type path into json schema type +trait SchemaJsonType { + /// Bevy Reflect type path + fn get_type_path(&self) -> &'static str; + + /// JSON Schema type keyword from Bevy reflect type path into + fn map_json_type(&self) -> SchemaType { + match self.get_type_path() { + "bool" => SchemaType::Boolean, + "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => SchemaType::Uint, + "i8" | "i16" | "i32" | "i64" | "i128" | "isize" => SchemaType::Int, + "f32" | "f64" => SchemaType::Float, + "char" | "str" | "alloc::string::String" => SchemaType::String, + _ => SchemaType::Object, + } + } +} + +impl SchemaJsonType for OpaqueInfo { + fn get_type_path(&self) -> &'static str { + self.type_path() + } +} + +impl SchemaJsonReference for &bevy_reflect::Type { + fn ref_type(self) -> Value { + let path = self.path(); + json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) + } +} + +impl SchemaJsonReference for &bevy_reflect::UnnamedField { + fn ref_type(self) -> Value { + let path = self.type_path(); + json!({"type": json!({ "$ref": format!("#/$defs/{path}") })}) + } +} + +impl SchemaJsonReference for &NamedField { + fn ref_type(self) -> Value { + let type_path = self.type_path(); + json!({"type": json!({ "$ref": format!("#/$defs/{type_path}") }), "typePath": self.name()}) + } +} + /// Immutably retrieves an entity from the [`World`], returning an error if the /// entity isn't present. fn get_entity(world: &World, entity: Entity) -> Result, BrpError> { @@ -1003,6 +1512,8 @@ mod tests { ); } use super::*; + use bevy_ecs::{component::Component, system::Resource}; + use bevy_reflect::Reflect; #[test] fn serialization_tests() { @@ -1013,8 +1524,205 @@ mod tests { }); test_serialize_deserialize(BrpListWatchingResponse::default()); test_serialize_deserialize(BrpQuery::default()); + test_serialize_deserialize(BrpJsonSchemaQueryFilter::default()); + test_serialize_deserialize(BrpJsonSchemaQueryFilter { + type_limit: JsonSchemaTypeLimit { + with: vec!["Resource".to_owned()], + ..Default::default() + }, + ..Default::default() + }); test_serialize_deserialize(BrpListParams { entity: Entity::from_raw(0), }); } + + #[test] + fn reflect_export_struct() { + #[derive(Reflect, Resource, Default, Deserialize, Serialize)] + #[reflect(Resource, Default, Serialize, Deserialize)] + struct Foo { + a: f32, + b: Option, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); + + assert!( + !schema.reflect_types.contains(&"Component".to_owned()), + "Should not be a component" + ); + assert!( + schema.reflect_types.contains(&"Resource".to_owned()), + "Should be a resource" + ); + let _ = schema.properties.get("a").expect("Missing `a` field"); + let _ = schema.properties.get("b").expect("Missing `b` field"); + assert!( + schema.required.contains(&"a".to_owned()), + "Field a should be required" + ); + assert!( + !schema.required.contains(&"b".to_owned()), + "Field b should not be required" + ); + } + + #[test] + fn reflect_export_enum() { + #[derive(Reflect, Component, Default, Deserialize, Serialize)] + #[reflect(Component, Default, Serialize, Deserialize)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + assert!( + schema.reflect_types.contains(&"Component".to_owned()), + "Should be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + 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_struct_without_reflect_types() { + #[derive(Reflect, Component, Default, Deserialize, Serialize)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + assert!( + !schema.reflect_types.contains(&"Component".to_owned()), + "Should not be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + 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)] + #[reflect(Component, Default, Serialize, Deserialize)] + struct TupleStructType(usize, i32); + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + println!("{}", &serde_json::to_string_pretty(&schema).unwrap()); + assert!( + schema.reflect_types.contains(&"Component".to_owned()), + "Should be a component" + ); + assert!( + !schema.reflect_types.contains(&"Resource".to_owned()), + "Should not be a resource" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.prefix_items.len() == 2, "Should have 2 prefix items"); + } + + #[test] + fn reflect_export_serialization_check() { + #[derive(Reflect, Resource, Default, Deserialize, Serialize)] + #[reflect(Resource, Default)] + struct Foo { + a: f32, + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + } + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration); + let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); + let value = json!({ + "shortPath": "Foo", + "typePath": "bevy_remote::builtin_methods::tests::Foo", + "modulePath": "bevy_remote::builtin_methods::tests", + "crateName": "bevy_remote", + "reflectTypes": [ + "Resource", + "Default", + ], + "kind": "Struct", + "type": "object", + "additionalProperties": false, + "properties": { + "a": { + "type": { + "$ref": "#/$defs/f32" + } + }, + }, + "required": [ + "a" + ] + }); + assert_eq!(schema_as_value, value); + } } diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 3d6781444148d..bbb8e18cc37ba 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -133,7 +133,8 @@ //! //! `params`: //! - `data`: -//! - `components` (optional): An array of [fully-qualified type names] of components to fetch. +//! - `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. //! - `has` (optional): An array of fully-qualified type names of components whose presence will be //! reported as boolean values. @@ -142,8 +143,8 @@ //! on entities in order for them to be included in results. //! - `without` (optional): An array of fully-qualified type names of components that must *not* be //! present on entities in order for them to be included in results. -//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the -//! components is not present or can not be reflected. Defaults to false. +//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the +//! components is not present or can not be reflected. Defaults to false. //! //! `result`: An array, each of which is an object containing: //! - `entity`: The ID of a query-matching entity. @@ -152,6 +153,8 @@ //! - `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. //! +//! +//! //! ### bevy/spawn //! //! Create a new entity with the provided components and return the resulting entity ID. @@ -191,6 +194,19 @@ //! //! `result`: null. //! +//! ### `bevy/mutate_component` +//! +//! Mutate a field in a component. +//! +//! `params`: +//! - `entity`: The ID of the entity to with the component to mutate. +//! - `component`: The component's [fully-qualified type name]. +//! - `path`: The path of the field within the component. See +//! [`GetPath`](bevy_reflect::GetPath#syntax) for more information on formatting this string. +//! - `value`: The value to insert at `path`. +//! +//! `result`: null. +//! //! ### bevy/reparent //! //! Assign a new parent to one or more entities. @@ -406,6 +422,14 @@ impl Default for RemotePlugin { builtin_methods::BRP_LIST_METHOD, builtin_methods::process_remote_list_request, ) + .with_method( + builtin_methods::BRP_REGISTRY_SCHEMA_METHOD, + builtin_methods::export_registry_types, + ) + .with_method( + builtin_methods::BRP_MUTATE_COMPONENT_METHOD, + builtin_methods::process_remote_mutate_component_request, + ) .with_watching_method( builtin_methods::BRP_GET_AND_WATCH_METHOD, builtin_methods::process_remote_get_watching_request, @@ -563,6 +587,26 @@ pub struct RemoteWatchingRequests(Vec<(BrpMessage, RemoteWatchingMethodSystemId) /// } /// } /// ``` +/// Or, to list all the fully-qualified type paths in **your** project, pass Null to the +/// `params`. +/// ```json +/// { +/// "jsonrpc": "2.0", +/// "method": "bevy/list", +/// "id": 0, +/// "params": null +///} +///``` +/// +/// In Rust: +/// ```ignore +/// let req = BrpRequest { +/// jsonrpc: "2.0".to_string(), +/// method: BRP_LIST_METHOD.to_string(), // All the methods have consts +/// id: Some(ureq::json!(0)), +/// params: None, +/// }; +/// ``` #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BrpRequest { /// This field is mandatory and must be set to `"2.0"` for the request to be accepted. diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 6e1407f326cbf..577ff685bb4c1 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides rendering functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -34,29 +34,29 @@ detailed_trace = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev", features = [ +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 = [ "serialize", "wgpu-types", ] } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_encase_derive = { path = "../bevy_encase_derive", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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_hierarchy = { path = "../bevy_hierarchy", 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", features = [ "bevy", ] } -bevy_render_macros = { path = "macros", version = "0.15.0-dev" } -bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.15.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" } # rendering image = { version = "0.25.2", default-features = false } @@ -77,7 +77,7 @@ wgpu = { version = "23.0.1", default-features = false, features = [ naga = { version = "23", features = ["wgsl-in"] } serde = { version = "1", features = ["derive"] } bytemuck = { version = "1.5", features = ["derive", "must_cast"] } -downcast-rs = "1.2.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"] } futures-lite = "2.0.1" @@ -92,6 +92,7 @@ nonmax = "0.5" smallvec = { version = "1.11", features = ["const_new"] } offset-allocator = "0.2" variadics_please = "1.1" +tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index fab68977bc98b..237cc516c5372 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_render_macros" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Derive implementations for bevy_render" homepage = "https://bevyengine.org" @@ -12,7 +12,7 @@ keywords = ["bevy"] proc-macro = true [dependencies] -bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.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 96176e071ca6c..d5ce58d46c107 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -132,6 +132,11 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { } }; + // Count the number of sampler fields needed. We might have to disable + // bindless if bindless arrays take the GPU over the maximum number of + // samplers. + let mut sampler_binding_count = 0; + // Read field-level attributes for field in fields { // Search ahead for texture attributes so we can use them with any @@ -341,6 +346,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }); + sampler_binding_count += 1; + binding_layouts.push(quote! { #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, @@ -417,6 +424,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { ) }); + sampler_binding_count += 1; + binding_layouts.push(quote!{ #render_path::render_resource::BindGroupLayoutEntry { binding: #binding_index, @@ -440,11 +449,7 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { Some(_) => { quote! { let (#uniform_binding_type, #uniform_buffer_usages) = - if render_device.features().contains( - #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | - #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY - ) && render_device.limits().max_storage_buffers_per_shader_stage > 0 && - !force_no_bindless { + if Self::bindless_supported(render_device) && !force_no_bindless { ( #render_path::render_resource::BufferBindingType::Storage { read_only: true }, #render_path::render_resource::BufferUsages::STORAGE, @@ -563,6 +568,17 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { (prepared_data.clone(), prepared_data) }; + // Calculate the number of samplers that we need, so that we don't go over + // the limit on certain platforms. See + // https://github.com/bevyengine/bevy/issues/16988. + let samplers_needed = match attr_bindless_count { + Some(Lit::Int(ref bindless_count)) => match bindless_count.base10_parse::() { + Ok(bindless_count) => sampler_binding_count * bindless_count, + Err(_) => 0, + }, + _ => 0, + }; + // Calculate the actual number of bindless slots, taking hardware // limitations into account. let (bindless_slot_count, actual_bindless_slot_count_declaration) = match attr_bindless_count { @@ -571,13 +587,19 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { fn bindless_slot_count() -> Option { Some(#bindless_count) } + + fn bindless_supported(render_device: &#render_path::renderer::RenderDevice) -> bool { + render_device.features().contains( + #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | + #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY + ) && + render_device.limits().max_storage_buffers_per_shader_stage > 0 && + render_device.limits().max_samplers_per_shader_stage >= #samplers_needed + } }, quote! { - let #actual_bindless_slot_count = if render_device.features().contains( - #render_path::settings::WgpuFeatures::BUFFER_BINDING_ARRAY | - #render_path::settings::WgpuFeatures::TEXTURE_BINDING_ARRAY - ) && render_device.limits().max_storage_buffers_per_shader_stage > 0 && - !force_no_bindless { + let #actual_bindless_slot_count = if Self::bindless_supported(render_device) && + !force_no_bindless { ::core::num::NonZeroU32::new(#bindless_count) } else { None diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 07379443a4081..2e893616f9294 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -1,7 +1,8 @@ //! Batching functionality when GPU preprocessing is in use. +use core::any::TypeId; + use bevy_app::{App, Plugin}; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, query::{Has, With}, @@ -10,21 +11,22 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_encase_derive::ShaderType; -use bevy_utils::{default, tracing::error}; +use bevy_utils::{default, TypeIdMap}; use bytemuck::{Pod, Zeroable}; use nonmax::NonMaxU32; +use tracing::error; use wgpu::{BindingResource, BufferUsages, DownlevelFlags, Features}; use crate::{ render_phase::{ - BinnedPhaseItem, BinnedRenderPhaseBatch, BinnedRenderPhaseBatchSets, - CachedRenderPipelinePhaseItem, PhaseItemBinKey as _, PhaseItemExtraIndex, SortedPhaseItem, - SortedRenderPhase, UnbatchableBinnedEntityIndices, ViewBinnedRenderPhases, - ViewSortedRenderPhases, + BinnedPhaseItem, BinnedRenderPhaseBatch, BinnedRenderPhaseBatchSet, + BinnedRenderPhaseBatchSets, CachedRenderPipelinePhaseItem, PhaseItemBatchSetKey as _, + PhaseItemExtraIndex, SortedPhaseItem, SortedRenderPhase, UnbatchableBinnedEntityIndices, + ViewBinnedRenderPhases, ViewSortedRenderPhases, }, - render_resource::{BufferVec, GpuArrayBufferable, RawBufferVec, UninitBufferVec}, + render_resource::{Buffer, BufferVec, GpuArrayBufferable, RawBufferVec, UninitBufferVec}, renderer::{RenderAdapter, RenderDevice, RenderQueue}, - view::{ExtractedView, NoIndirectDrawing, ViewTarget}, + view::{ExtractedView, NoIndirectDrawing}, Render, RenderApp, RenderSet, }; @@ -39,10 +41,14 @@ impl Plugin for BatchingPlugin { }; render_app - .insert_resource(IndirectParametersBuffer::new()) + .insert_resource(IndirectParametersBuffers::new()) + .add_systems( + Render, + write_indirect_parameters_buffers.in_set(RenderSet::PrepareResourcesFlush), + ) .add_systems( Render, - write_indirect_parameters_buffer.in_set(RenderSet::PrepareResourcesFlush), + clear_indirect_parameters_buffers.in_set(RenderSet::ManageViews), ); } @@ -137,7 +143,7 @@ where /// corresponds to each instance. /// /// This is keyed off each view. Each view has a separate buffer. - pub work_item_buffers: EntityHashMap, + pub work_item_buffers: EntityHashMap>, /// The uniform data inputs for the current frame. /// @@ -217,11 +223,31 @@ where } /// Returns the piece of buffered data at the given index. - pub fn get(&self, uniform_index: u32) -> BDI { + /// + /// Returns [`None`] if the index is out of bounds or the data is removed. + pub fn get(&self, uniform_index: u32) -> Option { + if (uniform_index as usize) >= self.buffer.len() + || self.free_uniform_indices.contains(&uniform_index) + { + None + } else { + Some(self.get_unchecked(uniform_index)) + } + } + + /// Returns the piece of buffered data at the given index. + /// Can return data that has previously been removed. + /// + /// # Panics + /// if `uniform_index` is not in bounds of [`Self::buffer`]. + pub fn get_unchecked(&self, uniform_index: u32) -> BDI { self.buffer.values()[uniform_index as usize] } /// Stores a piece of buffered data at the given index. + /// + /// # Panics + /// if `uniform_index` is not in bounds of [`Self::buffer`]. pub fn set(&mut self, uniform_index: u32, element: BDI) { self.buffer.values_mut()[uniform_index as usize] = element; } @@ -245,105 +271,449 @@ where } /// The buffer of GPU preprocessing work items for a single view. -pub struct PreprocessWorkItemBuffer { - /// The buffer of work items. - pub buffer: BufferVec, - /// True if we're drawing directly instead of indirectly. - pub no_indirect_drawing: bool, +pub enum PreprocessWorkItemBuffers { + /// The work items we use if we aren't using indirect drawing. + /// + /// Because we don't have to separate indexed from non-indexed meshes in + /// direct mode, we only have a single buffer here. + Direct(BufferVec), + + /// The buffer of work items we use if we are using indirect drawing. + /// + /// We need to separate out indexed meshes from non-indexed meshes in this + /// case because the indirect parameters for these two types of meshes have + /// different sizes. + Indirect { + /// The buffer of work items corresponding to indexed meshes. + indexed: BufferVec, + /// The buffer of work items corresponding to non-indexed meshes. + non_indexed: BufferVec, + }, +} + +impl PreprocessWorkItemBuffers { + /// Creates a new set of buffers. + /// + /// `no_indirect_drawing` specifies whether we're drawing directly or + /// indirectly. + pub fn new(no_indirect_drawing: bool) -> Self { + if no_indirect_drawing { + PreprocessWorkItemBuffers::Direct(BufferVec::new(BufferUsages::STORAGE)) + } else { + PreprocessWorkItemBuffers::Indirect { + indexed: BufferVec::new(BufferUsages::STORAGE), + non_indexed: BufferVec::new(BufferUsages::STORAGE), + } + } + } + + /// Adds a new work item to the appropriate buffer. + /// + /// `indexed` specifies whether the work item corresponds to an indexed + /// mesh. + pub fn push(&mut self, indexed: bool, preprocess_work_item: PreprocessWorkItem) { + match *self { + PreprocessWorkItemBuffers::Direct(ref mut buffer) => { + buffer.push(preprocess_work_item); + } + PreprocessWorkItemBuffers::Indirect { + indexed: ref mut indexed_buffer, + non_indexed: ref mut non_indexed_buffer, + } => { + if indexed { + indexed_buffer.push(preprocess_work_item); + } else { + non_indexed_buffer.push(preprocess_work_item); + } + } + } + } } /// One invocation of the preprocessing shader: i.e. one mesh instance in a /// view. -#[derive(Clone, Copy, Pod, Zeroable, ShaderType)] +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] #[repr(C)] pub struct PreprocessWorkItem { /// The index of the batch input data in the input buffer that the shader /// reads from. pub input_index: u32, - /// In direct mode, this is the index of the `MeshUniform` in the output - /// buffer that we write to. In indirect mode, this is the index of the - /// [`IndirectParameters`]. + /// The index of the `MeshUniform` in the output buffer that we write to. + /// In direct mode, this is the index of the uniform. In indirect mode, this + /// is the first index uniform in the batch set. pub output_index: u32, + /// The index of the [`IndirectParametersMetadata`] in the + /// `IndirectParametersBuffers::indexed_metadata` or + /// `IndirectParametersBuffers::non_indexed_metadata`. + pub indirect_parameters_index: u32, } -/// The `wgpu` indirect parameters structure. +/// The `wgpu` indirect parameters structure that specifies a GPU draw command. /// -/// This is actually a union of the two following structures: +/// This is the variant for indexed meshes. We generate the instances of this +/// structure in the `build_indirect_params.wgsl` compute shader. +#[derive(Clone, Copy, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersIndexed { + /// The number of indices that this mesh has. + pub index_count: u32, + /// The number of instances we are to draw. + pub instance_count: u32, + /// The offset of the first index for this mesh in the index buffer slab. + pub first_index: u32, + /// The offset of the first vertex for this mesh in the vertex buffer slab. + pub base_vertex: u32, + /// The index of the first mesh instance in the `MeshUniform` buffer. + pub first_instance: u32, +} + +/// The `wgpu` indirect parameters structure that specifies a GPU draw command. /// -/// ``` -/// #[repr(C)] -/// struct ArrayIndirectParameters { -/// vertex_count: u32, -/// instance_count: u32, -/// first_vertex: u32, -/// first_instance: u32, -/// } +/// This is the variant for non-indexed meshes. We generate the instances of +/// this structure in the `build_indirect_params.wgsl` compute shader. +#[derive(Clone, Copy, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersNonIndexed { + /// The number of vertices that this mesh has. + pub vertex_count: u32, + /// The number of instances we are to draw. + pub instance_count: u32, + /// The offset of the first vertex for this mesh in the vertex buffer slab. + pub base_vertex: u32, + /// The index of the first mesh instance in the `Mesh` buffer. + pub first_instance: u32, +} + +/// A structure, shared between CPU and GPU, that records how many instances of +/// each mesh are actually to be drawn. /// -/// #[repr(C)] -/// struct ElementIndirectParameters { -/// index_count: u32, -/// instance_count: u32, -/// first_vertex: u32, -/// base_vertex: u32, -/// first_instance: u32, -/// } -/// ``` +/// The CPU writes to this structure in order to initialize the fields other +/// than [`Self::instance_count`]. The GPU mesh preprocessing shader increments +/// the [`Self::instance_count`] as it determines that meshes are visible. The +/// indirect parameter building shader reads this metadata in order to construct +/// the indirect draw parameters. /// -/// We actually generally treat these two variants identically in code. To do -/// that, we make the following two observations: +/// Each batch will have one instance of this structure. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] +#[repr(C)] +pub struct IndirectParametersMetadata { + /// The index of the mesh in the array of `MeshInputUniform`s. + pub mesh_index: u32, + + /// The index of the first instance of this mesh in the array of + /// `MeshUniform`s. + /// + /// Note that this is the *first* output index in this batch. Since each + /// instance of this structure refers to arbitrarily many instances, the + /// `MeshUniform`s corresponding to this batch span the indices + /// `base_output_index..(base_output_index + instance_count)`. + pub base_output_index: u32, + + /// The index of the batch set that this batch belongs to in the + /// [`IndirectBatchSet`] buffer. + /// + /// A *batch set* is a set of meshes that may be multi-drawn together. + /// Multiple batches (and therefore multiple instances of + /// [`IndirectParametersMetadata`] structures) can be part of the same batch + /// set. + pub batch_set_index: u32, + + /// The number of instances that have been judged potentially visible. + /// + /// The CPU sets this value to 0, and the GPU mesh preprocessing shader + /// increments it as it culls mesh instances. + pub instance_count: u32, +} + +/// A structure, shared between CPU and GPU, that holds the number of on-GPU +/// indirect draw commands for each *batch set*. /// -/// 1. `instance_count` is in the same place in both structures. So we can -/// access it regardless of the structure we're looking at. +/// A *batch set* is a set of meshes that may be multi-drawn together. /// -/// 2. The second structure is one word larger than the first. Thus we need to -/// pad out the first structure by one word in order to place both structures in -/// an array. If we pad out `ArrayIndirectParameters` by copying the -/// `first_instance` field into the padding, then the resulting union structure -/// will always have a read-only copy of `first_instance` in the final word. We -/// take advantage of this in the shader to reduce branching. -#[derive(Clone, Copy, Pod, Zeroable, ShaderType)] +/// If the current hardware and driver support `multi_draw_indirect_count`, the +/// indirect parameters building shader increments +/// [`Self::indirect_parameters_count`] as it generates indirect parameters. The +/// `multi_draw_indirect_count` command reads +/// [`Self::indirect_parameters_count`] in order to determine how many commands +/// belong to each batch set. +#[derive(Clone, Copy, Default, Pod, Zeroable, ShaderType)] #[repr(C)] -pub struct IndirectParameters { - /// For `ArrayIndirectParameters`, `vertex_count`; for - /// `ElementIndirectParameters`, `index_count`. - pub vertex_or_index_count: u32, +pub struct IndirectBatchSet { + /// The number of indirect parameter commands (i.e. batches) in this batch + /// set. + /// + /// The CPU sets this value to 0 before uploading this structure to GPU. The + /// indirect parameters building shader increments this value as it creates + /// indirect parameters. Then the `multi_draw_indirect_count` command reads + /// this value in order to determine how many indirect draw commands to + /// process. + pub indirect_parameters_count: u32, + + /// The offset within the `IndirectParametersBuffers::indexed_data` or + /// `IndirectParametersBuffers::non_indexed_data` of the first indirect draw + /// command for this batch set. + /// + /// The CPU fills out this value. + pub indirect_parameters_base: u32, +} - /// The number of instances we're going to draw. +/// The buffers containing all the information that indirect draw commands +/// (`multi_draw_indirect`, `multi_draw_indirect_count`) use to draw the scene. +/// +/// In addition to the indirect draw buffers themselves, this structure contains +/// the buffers that store [`IndirectParametersMetadata`], which are the +/// structures that culling writes to so that the indirect parameter building +/// pass can determine how many meshes are actually to be drawn. +/// +/// These buffers will remain empty if indirect drawing isn't in use. +#[derive(Resource)] +pub struct IndirectParametersBuffers { + /// The GPU buffer that stores the indirect draw parameters for non-indexed + /// meshes. /// - /// This field is in the same place in both structures. - pub instance_count: u32, + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + non_indexed_data: UninitBufferVec, - /// For `ArrayIndirectParameters`, `first_vertex`; for - /// `ElementIndirectParameters`, `first_index`. - pub first_vertex_or_first_index: u32, + /// The GPU buffer that holds the data used to construct indirect draw + /// parameters for non-indexed meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + non_indexed_metadata: RawBufferVec, - /// For `ArrayIndirectParameters`, `first_instance`; for - /// `ElementIndirectParameters`, `base_vertex`. - pub base_vertex_or_first_instance: u32, + /// The GPU buffer that holds the number of indirect draw commands for each + /// phase of each view, for non-indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + non_indexed_batch_sets: RawBufferVec, - /// For `ArrayIndirectParameters`, this is padding; for - /// `ElementIndirectParameters`, this is `first_instance`. + /// The GPU buffer that stores the indirect draw parameters for indexed + /// meshes. /// - /// Conventionally, we copy `first_instance` into this field when padding - /// out `ArrayIndirectParameters`. That way, shader code can read this value - /// at the same place, regardless of the specific structure this represents. - pub first_instance: u32, + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + indexed_data: UninitBufferVec, + + /// The GPU buffer that holds the data used to construct indirect draw + /// parameters for indexed meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + indexed_metadata: RawBufferVec, + + /// The GPU buffer that holds the number of indirect draw commands for each + /// phase of each view, for indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + indexed_batch_sets: RawBufferVec, } -/// The buffer containing the list of [`IndirectParameters`], for draw commands. -#[derive(Resource, Deref, DerefMut)] -pub struct IndirectParametersBuffer(pub BufferVec); +impl IndirectParametersBuffers { + /// Creates the indirect parameters buffers. + pub fn new() -> IndirectParametersBuffers { + IndirectParametersBuffers { + non_indexed_data: UninitBufferVec::new(BufferUsages::STORAGE | BufferUsages::INDIRECT), + non_indexed_metadata: RawBufferVec::new(BufferUsages::STORAGE), + non_indexed_batch_sets: RawBufferVec::new( + BufferUsages::STORAGE | BufferUsages::INDIRECT, + ), + indexed_data: UninitBufferVec::new(BufferUsages::STORAGE | BufferUsages::INDIRECT), + indexed_metadata: RawBufferVec::new(BufferUsages::STORAGE), + indexed_batch_sets: RawBufferVec::new(BufferUsages::STORAGE | BufferUsages::INDIRECT), + } + } + + /// Returns the GPU buffer that stores the indirect draw parameters for + /// indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + #[inline] + pub fn indexed_data_buffer(&self) -> Option<&Buffer> { + self.indexed_data.buffer() + } + + /// Returns the GPU buffer that holds the data used to construct indirect + /// draw parameters for indexed meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + #[inline] + pub fn indexed_metadata_buffer(&self) -> Option<&Buffer> { + self.indexed_metadata.buffer() + } + + /// Returns the GPU buffer that holds the number of indirect draw commands + /// for each phase of each view, for indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + #[inline] + pub fn indexed_batch_sets_buffer(&self) -> Option<&Buffer> { + self.indexed_batch_sets.buffer() + } + + /// Returns the GPU buffer that stores the indirect draw parameters for + /// non-indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, while the + /// `multi_draw_indirect` or `multi_draw_indirect_count` commands read from + /// it to perform the draws. + #[inline] + pub fn non_indexed_data_buffer(&self) -> Option<&Buffer> { + self.non_indexed_data.buffer() + } + + /// Returns the GPU buffer that holds the data used to construct indirect + /// draw parameters for non-indexed meshes. + /// + /// The GPU mesh preprocessing shader writes to this buffer, and the + /// indirect parameters building shader reads this buffer to construct the + /// indirect draw parameters. + #[inline] + pub fn non_indexed_metadata_buffer(&self) -> Option<&Buffer> { + self.non_indexed_metadata.buffer() + } + + /// Returns the GPU buffer that holds the number of indirect draw commands + /// for each phase of each view, for non-indexed meshes. + /// + /// The indirect parameters building shader writes to this buffer, and the + /// `multi_draw_indirect_count` command reads from it in order to know how + /// many indirect draw commands to process. + #[inline] + pub fn non_indexed_batch_sets_buffer(&self) -> Option<&Buffer> { + self.non_indexed_batch_sets.buffer() + } + + /// Reserves space for `count` new batches corresponding to indexed meshes. + /// + /// This allocates in both the [`Self::indexed_metadata`] and + /// [`Self::indexed_data`] buffers. + fn allocate_indexed(&mut self, count: u32) -> u32 { + let length = self.indexed_data.len(); + self.indexed_metadata.reserve_internal(count as usize); + for _ in 0..count { + self.indexed_data.add(); + self.indexed_metadata + .push(IndirectParametersMetadata::default()); + } + length as u32 + } + + /// Reserves space for `count` new batches corresponding to non-indexed + /// meshes. + /// + /// This allocates in both the [`Self::non_indexed_metadata`] and + /// [`Self::non_indexed_data`] buffers. + fn allocate_non_indexed(&mut self, count: u32) -> u32 { + let length = self.non_indexed_data.len(); + self.non_indexed_metadata.reserve_internal(count as usize); + for _ in 0..count { + self.non_indexed_data.add(); + self.non_indexed_metadata + .push(IndirectParametersMetadata::default()); + } + length as u32 + } + + /// Reserves space for `count` new batches. + /// + /// The `indexed` parameter specifies whether the meshes that these batches + /// correspond to are indexed or not. + pub fn allocate(&mut self, indexed: bool, count: u32) -> u32 { + if indexed { + self.allocate_indexed(count) + } else { + self.allocate_non_indexed(count) + } + } + + /// Initializes the batch corresponding to an indexed mesh at the given + /// index with the given [`IndirectParametersMetadata`]. + pub fn set_indexed(&mut self, index: u32, value: IndirectParametersMetadata) { + self.indexed_metadata.set(index, value); + } + + /// Initializes the batch corresponding to a non-indexed mesh at the given + /// index with the given [`IndirectParametersMetadata`]. + pub fn set_non_indexed(&mut self, index: u32, value: IndirectParametersMetadata) { + self.non_indexed_metadata.set(index, value); + } + + /// Returns the number of batches currently allocated. + /// + /// The `indexed` parameter specifies whether the meshes that these batches + /// correspond to are indexed or not. + fn batch_count(&self, indexed: bool) -> usize { + if indexed { + self.indexed_batch_count() + } else { + self.non_indexed_batch_count() + } + } + + /// Returns the number of batches corresponding to indexed meshes that are + /// currently allocated. + #[inline] + pub fn indexed_batch_count(&self) -> usize { + self.indexed_data.len() + } -impl IndirectParametersBuffer { - /// Creates the indirect parameters buffer. - pub fn new() -> IndirectParametersBuffer { - IndirectParametersBuffer(BufferVec::new( - BufferUsages::STORAGE | BufferUsages::INDIRECT, - )) + /// Returns the number of batches corresponding to non-indexed meshes that + /// are currently allocated. + #[inline] + pub fn non_indexed_batch_count(&self) -> usize { + self.non_indexed_data.len() + } + + /// Returns the number of batch sets currently allocated. + /// + /// The `indexed` parameter specifies whether the meshes that these batch + /// sets correspond to are indexed or not. + pub fn batch_set_count(&self, indexed: bool) -> usize { + if indexed { + self.indexed_batch_sets.len() + } else { + self.non_indexed_batch_sets.len() + } + } + + /// Adds a new batch set to `Self::indexed_batch_sets` or + /// `Self::non_indexed_batch_sets` as appropriate. + /// + /// `indexed` specifies whether the meshes that these batch sets correspond + /// to are indexed or not. `indirect_parameters_base` specifies the offset + /// within `Self::indexed_data` or `Self::non_indexed_data` of the first + /// batch in this batch set. + pub fn add_batch_set(&mut self, indexed: bool, indirect_parameters_base: u32) { + if indexed { + self.indexed_batch_sets.push(IndirectBatchSet { + indirect_parameters_base, + indirect_parameters_count: 0, + }); + } else { + self.non_indexed_batch_sets.push(IndirectBatchSet { + indirect_parameters_base, + indirect_parameters_count: 0, + }); + } } } -impl Default for IndirectParametersBuffer { +impl Default for IndirectParametersBuffers { fn default() -> Self { Self::new() } @@ -354,31 +724,16 @@ impl FromWorld for GpuPreprocessingSupport { let adapter = world.resource::(); let device = world.resource::(); - // filter some Qualcomm devices on Android as they crash when using GPU preprocessing. + // Filter some Qualcomm devices on Android as they crash when using GPU + // preprocessing. + // We filter out Adreno 730 and earlier GPUs (except 720, as it's newer + // than 730). fn is_non_supported_android_device(adapter: &RenderAdapter) -> bool { - if cfg!(target_os = "android") { - let adapter_name = adapter.get_info().name; - - // Filter out Adreno 730 and earlier GPUs (except 720, as it's newer than 730) - // while also taking suffixes into account like Adreno 642L. - let non_supported_adreno_model = |model: &str| -> bool { - let model = model - .chars() - .map_while(|c| c.to_digit(10)) - .fold(0, |acc, digit| acc * 10 + digit); - - model != 720 && model <= 730 - }; - - adapter_name - .strip_prefix("Adreno (TM) ") - .is_some_and(non_supported_adreno_model) - } else { - false - } + crate::get_adreno_model(adapter).is_some_and(|model| model != 720 && model <= 730) } - let max_supported_mode = if device.limits().max_compute_workgroup_size_x == 0 || is_non_supported_android_device(adapter) + let max_supported_mode = if device.limits().max_compute_workgroup_size_x == 0 || + is_non_supported_android_device(adapter) { GpuPreprocessingMode::None } else if !device @@ -423,8 +778,20 @@ where /// Clears out the buffers in preparation for a new frame. pub fn clear(&mut self) { self.data_buffer.clear(); - for work_item_buffer in self.work_item_buffers.values_mut() { - work_item_buffer.buffer.clear(); + + for view_work_item_buffers in self.work_item_buffers.values_mut() { + for phase_work_item_buffers in view_work_item_buffers.values_mut() { + match *phase_work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref mut buffer_vec) => buffer_vec.clear(), + PreprocessWorkItemBuffers::Indirect { + ref mut indexed, + ref mut non_indexed, + } => { + indexed.clear(); + non_indexed.clear(); + } + } + } } } } @@ -452,8 +819,11 @@ where /// The index of the first instance in this batch in the instance buffer. instance_start_index: u32, + /// True if the mesh in question has an index buffer; false otherwise. + indexed: bool, + /// The index of the indirect parameters for this batch in the - /// [`IndirectParametersBuffer`]. + /// [`IndirectParametersBuffers`]. /// /// If CPU culling is being used, then this will be `None`. indirect_parameters_index: Option, @@ -474,8 +844,12 @@ where /// /// `instance_end_index` is the index of the last instance in this batch /// plus one. - fn flush(self, instance_end_index: u32, phase: &mut SortedRenderPhase) - where + fn flush( + self, + instance_end_index: u32, + phase: &mut SortedRenderPhase, + indirect_parameters_buffers: &mut IndirectParametersBuffers, + ) where I: CachedRenderPipelinePhaseItem + SortedPhaseItem, { let (batch_range, batch_extra_index) = @@ -483,6 +857,11 @@ where *batch_range = self.instance_start_index..instance_end_index; *batch_extra_index = PhaseItemExtraIndex::maybe_indirect_parameters_index(self.indirect_parameters_index); + + if let Some(indirect_parameters_index) = self.indirect_parameters_index { + indirect_parameters_buffers + .add_batch_set(self.indexed, indirect_parameters_index.into()); + } } } @@ -505,22 +884,22 @@ pub fn clear_batched_gpu_instance_buffers( } /// A system that removes GPU preprocessing work item buffers that correspond to -/// deleted [`ViewTarget`]s. +/// deleted [`ExtractedView`]s. /// /// This is a separate system from [`clear_batched_gpu_instance_buffers`] -/// because [`ViewTarget`]s aren't created until after the extraction phase is -/// completed. +/// because [`ExtractedView`]s aren't created until after the extraction phase +/// is completed. pub fn delete_old_work_item_buffers( mut gpu_batched_instance_buffers: ResMut< BatchedInstanceBuffers, >, - view_targets: Query>, + extracted_views: Query>, ) where GFBD: GetFullBatchData, { gpu_batched_instance_buffers .work_item_buffers - .retain(|entity, _| view_targets.contains(*entity)); + .retain(|entity, _| extracted_views.contains(*entity)); } /// Batch the items in a sorted render phase, when GPU instance buffer building @@ -528,9 +907,9 @@ pub fn delete_old_work_item_buffers( /// trying to combine the draws into a batch. pub fn batch_and_prepare_sorted_render_phase( gpu_array_buffer: ResMut>, - mut indirect_parameters_buffer: ResMut, + mut indirect_parameters_buffers: ResMut, mut sorted_render_phases: ResMut>, - mut views: Query<(Entity, Has), With>, + mut views: Query<(Entity, &ExtractedView, Has)>, system_param_item: StaticSystemParam, ) where I: CachedRenderPipelinePhaseItem + SortedPhaseItem, @@ -543,27 +922,29 @@ pub fn batch_and_prepare_sorted_render_phase( .. } = gpu_array_buffer.into_inner(); - for (view, no_indirect_drawing) in &mut views { - let Some(phase) = sorted_render_phases.get_mut(&view) else { + for (view, extracted_view, no_indirect_drawing) in &mut views { + let Some(phase) = sorted_render_phases.get_mut(&extracted_view.retained_view_entity) else { continue; }; // Create the work item buffer if necessary. - let work_item_buffer = - work_item_buffers - .entry(view) - .or_insert_with(|| PreprocessWorkItemBuffer { - buffer: BufferVec::new(BufferUsages::STORAGE), - no_indirect_drawing, - }); + let work_item_buffer = work_item_buffers + .entry(view) + .or_insert_with(TypeIdMap::default) + .entry(TypeId::of::()) + .or_insert_with(|| PreprocessWorkItemBuffers::new(no_indirect_drawing)); // Walk through the list of phase items, building up batches as we go. let mut batch: Option> = None; + + let mut first_output_index = data_buffer.len() as u32; + for current_index in 0..phase.items.len() { // Get the index of the input data, and comparison metadata, for // this entity. let item = &phase.items[current_index]; - let entity = (item.entity(), item.main_entity()); + let entity = item.main_entity(); + let item_is_indexed = item.indexed(); let current_batch_input_index = GFBD::get_index_and_compare_data(&system_param_item, entity); @@ -574,7 +955,11 @@ pub fn batch_and_prepare_sorted_render_phase( let Some((current_input_index, current_meta)) = current_batch_input_index else { // Break a batch if we need to. if let Some(batch) = batch.take() { - batch.flush(data_buffer.len() as u32, phase); + batch.flush( + data_buffer.len() as u32, + phase, + &mut indirect_parameters_buffers, + ); } continue; @@ -593,52 +978,74 @@ pub fn batch_and_prepare_sorted_render_phase( }); // Make space in the data buffer for this instance. - let item = &phase.items[current_index]; - let entity = (item.entity(), item.main_entity()); let output_index = data_buffer.add() as u32; // If we can't batch, break the existing batch and make a new one. if !can_batch { // Break a batch if we need to. if let Some(batch) = batch.take() { - batch.flush(output_index, phase); + batch.flush(output_index, phase, &mut indirect_parameters_buffers); } + let indirect_parameters_index = if no_indirect_drawing { + None + } else if item_is_indexed { + Some(indirect_parameters_buffers.allocate_indexed(1)) + } else { + Some(indirect_parameters_buffers.allocate_non_indexed(1)) + }; + // Start a new batch. - let indirect_parameters_index = if !no_indirect_drawing { - GFBD::get_batch_indirect_parameters_index( - &system_param_item, - &mut indirect_parameters_buffer, - entity, + if let Some(indirect_parameters_index) = indirect_parameters_index { + GFBD::write_batch_indirect_parameters_metadata( + current_input_index.into(), + item_is_indexed, output_index, - ) - } else { - None + None, + &mut indirect_parameters_buffers, + indirect_parameters_index, + ); }; + batch = Some(SortedRenderBatch { phase_item_start_index: current_index as u32, instance_start_index: output_index, - indirect_parameters_index, + indexed: item_is_indexed, + indirect_parameters_index: indirect_parameters_index.and_then(NonMaxU32::new), meta: current_meta, }); + + first_output_index = output_index; } // Add a new preprocessing work item so that the preprocessing // shader will copy the per-instance data over. if let Some(batch) = batch.as_ref() { - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: current_input_index.into(), - output_index: match batch.indirect_parameters_index { - Some(indirect_parameters_index) => indirect_parameters_index.into(), - None => output_index, + work_item_buffer.push( + item_is_indexed, + PreprocessWorkItem { + input_index: current_input_index.into(), + output_index: if no_indirect_drawing { + output_index + } else { + first_output_index + }, + indirect_parameters_index: match batch.indirect_parameters_index { + Some(indirect_parameters_index) => indirect_parameters_index.into(), + None => 0, + }, }, - }); + ); } } // Flush the final batch if necessary. if let Some(batch) = batch.take() { - batch.flush(data_buffer.len() as u32, phase); + batch.flush( + data_buffer.len() as u32, + phase, + &mut indirect_parameters_buffers, + ); } } } @@ -646,9 +1053,9 @@ pub fn batch_and_prepare_sorted_render_phase( /// Creates batches for a render phase that uses bins. pub fn batch_and_prepare_binned_render_phase( gpu_array_buffer: ResMut>, - mut indirect_parameters_buffer: ResMut, + mut indirect_parameters_buffers: ResMut, mut binned_render_phases: ResMut>, - mut views: Query<(Entity, Has), With>, + mut views: Query<(Entity, &ExtractedView, Has)>, param: StaticSystemParam, ) where BPI: BinnedPhaseItem, @@ -662,35 +1069,129 @@ pub fn batch_and_prepare_binned_render_phase( .. } = gpu_array_buffer.into_inner(); - for (view, no_indirect_drawing) in &mut views { - let Some(phase) = binned_render_phases.get_mut(&view) else { + for (view, extracted_view, no_indirect_drawing) in &mut views { + let Some(phase) = binned_render_phases.get_mut(&extracted_view.retained_view_entity) else { continue; }; // Create the work item buffer if necessary; otherwise, just mark it as // used this frame. - let work_item_buffer = - work_item_buffers - .entry(view) - .or_insert_with(|| PreprocessWorkItemBuffer { - buffer: BufferVec::new(BufferUsages::STORAGE), - no_indirect_drawing, - }); + let work_item_buffer = work_item_buffers + .entry(view) + .or_insert_with(TypeIdMap::default) + .entry(TypeId::of::()) + .or_insert_with(|| PreprocessWorkItemBuffers::new(no_indirect_drawing)); + + // Prepare multidrawables. + + for batch_set_key in &phase.multidrawable_mesh_keys { + let mut batch_set = None; + let indirect_parameters_base = + indirect_parameters_buffers.batch_count(batch_set_key.indexed()) as u32; + for (bin_key, bin) in &phase.multidrawable_mesh_values[batch_set_key] { + let first_output_index = data_buffer.len() as u32; + let mut batch: Option = None; + + for &(entity, main_entity) in &bin.entities { + let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity) + else { + continue; + }; + let output_index = data_buffer.add() as u32; + + match batch { + Some(ref mut batch) => { + // Append to the current batch. + batch.instance_range.end = output_index + 1; + work_item_buffer.push( + batch_set_key.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index: first_output_index, + indirect_parameters_index: match batch.extra_index { + PhaseItemExtraIndex::IndirectParametersIndex { + ref range, + .. + } => range.start, + PhaseItemExtraIndex::DynamicOffset(_) + | PhaseItemExtraIndex::None => 0, + }, + }, + ); + } - // Prepare batchables. + None => { + // Start a new batch, in indirect mode. + let indirect_parameters_index = + indirect_parameters_buffers.allocate(batch_set_key.indexed(), 1); + let batch_set_index = NonMaxU32::new( + indirect_parameters_buffers.batch_set_count(batch_set_key.indexed()) + as u32, + ); + GFBD::write_batch_indirect_parameters_metadata( + input_index.into(), + batch_set_key.indexed(), + output_index, + batch_set_index, + &mut indirect_parameters_buffers, + indirect_parameters_index, + ); + work_item_buffer.push( + batch_set_key.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index: first_output_index, + indirect_parameters_index, + }, + ); + batch = Some(BinnedRenderPhaseBatch { + representative_entity: (entity, main_entity), + instance_range: output_index..output_index + 1, + extra_index: PhaseItemExtraIndex::maybe_indirect_parameters_index( + NonMaxU32::new(indirect_parameters_index), + ), + }); + } + } + } - // If multi-draw is in use, as we step through the list of batchables, - // we gather adjacent batches that have the same *batch set* key into - // batch sets. This variable stores the last batch set key that we've - // seen. If our current batch set key is identical to this one, we can - // merge the current batch into the last batch set. - let mut maybe_last_multidraw_key = None; + if let Some(batch) = batch { + match batch_set { + None => { + batch_set = Some(BinnedRenderPhaseBatchSet { + batches: vec![batch], + bin_key: bin_key.clone(), + index: indirect_parameters_buffers + .batch_set_count(batch_set_key.indexed()) + as u32, + }); + } + Some(ref mut batch_set) => { + batch_set.batches.push(batch); + } + } + } + } + + if let BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut batch_sets) = + phase.batch_sets + { + if let Some(batch_set) = batch_set { + batch_sets.push(batch_set); + indirect_parameters_buffers + .add_batch_set(batch_set_key.indexed(), indirect_parameters_base); + } + } + } + + // Prepare batchables. for key in &phase.batchable_mesh_keys { + let first_output_index = data_buffer.len() as u32; + let mut batch: Option = None; - for &(entity, main_entity) in &phase.batchable_mesh_values[key] { - let Some(input_index) = - GFBD::get_binned_index(&system_param_item, (entity, main_entity)) + for &(entity, main_entity) in &phase.batchable_mesh_values[key].entities { + let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity) else { continue; }; @@ -698,47 +1199,78 @@ pub fn batch_and_prepare_binned_render_phase( match batch { Some(ref mut batch) => { - // Append to the current batch. batch.instance_range.end = output_index + 1; - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: input_index.into(), - output_index: match batch.extra_index { - PhaseItemExtraIndex::IndirectParametersIndex(ref range) => { - range.start - } - PhaseItemExtraIndex::DynamicOffset(_) - | PhaseItemExtraIndex::None => output_index, + + // Append to the current batch. + // + // If we're in indirect mode, then we write the first + // output index of this batch, so that we have a + // tightly-packed buffer if GPU culling discards some of + // the instances. Otherwise, we can just write the + // output index directly. + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index: if no_indirect_drawing { + output_index + } else { + first_output_index + }, + indirect_parameters_index: match batch.extra_index { + PhaseItemExtraIndex::IndirectParametersIndex { + range: ref indirect_parameters_range, + .. + } => indirect_parameters_range.start, + PhaseItemExtraIndex::DynamicOffset(_) + | PhaseItemExtraIndex::None => 0, + }, }, - }); + ); } None if !no_indirect_drawing => { // Start a new batch, in indirect mode. - let indirect_parameters_index = GFBD::get_batch_indirect_parameters_index( - &system_param_item, - &mut indirect_parameters_buffer, - (entity, main_entity), + let indirect_parameters_index = + indirect_parameters_buffers.allocate(key.0.indexed(), 1); + let batch_set_index = NonMaxU32::new( + indirect_parameters_buffers.batch_set_count(key.0.indexed()) as u32, + ); + GFBD::write_batch_indirect_parameters_metadata( + input_index.into(), + key.0.indexed(), output_index, + batch_set_index, + &mut indirect_parameters_buffers, + indirect_parameters_index, + ); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index: first_output_index, + indirect_parameters_index, + }, ); - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: input_index.into(), - output_index: indirect_parameters_index.unwrap_or_default().into(), - }); batch = Some(BinnedRenderPhaseBatch { representative_entity: (entity, main_entity), instance_range: output_index..output_index + 1, extra_index: PhaseItemExtraIndex::maybe_indirect_parameters_index( - indirect_parameters_index, + NonMaxU32::new(indirect_parameters_index), ), }); } None => { // Start a new batch, in direct mode. - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: input_index.into(), - output_index, - }); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index, + indirect_parameters_index: 0, + }, + ); batch = Some(BinnedRenderPhaseBatch { representative_entity: (entity, main_entity), instance_range: output_index..output_index + 1, @@ -756,21 +1288,17 @@ pub fn batch_and_prepare_binned_render_phase( BinnedRenderPhaseBatchSets::Direct(ref mut vec) => { vec.push(batch); } - BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut batch_sets) => { - // We're in multi-draw mode. Check to see whether our - // batch set key is the same as the last one. If so, - // merge this batch into the preceding batch set. - match (&maybe_last_multidraw_key, key.get_batch_set_key()) { - (Some(ref last_multidraw_key), Some(this_multidraw_key)) - if *last_multidraw_key == this_multidraw_key => - { - batch_sets.last_mut().unwrap().push(batch); - } - (_, maybe_this_multidraw_key) => { - maybe_last_multidraw_key = maybe_this_multidraw_key; - batch_sets.push(vec![batch]); - } - } + BinnedRenderPhaseBatchSets::MultidrawIndirect(ref mut vec) => { + // The Bevy renderer will never mark a mesh as batchable + // but not multidrawable if multidraw is in use. + // However, custom render pipelines might do so, such as + // the `specialized_mesh_pipeline` example. + vec.push(BinnedRenderPhaseBatchSet { + batches: vec![batch], + bin_key: key.1.clone(), + index: indirect_parameters_buffers.batch_set_count(key.0.indexed()) + as u32, + }); } } } @@ -779,42 +1307,69 @@ pub fn batch_and_prepare_binned_render_phase( // Prepare unbatchables. for key in &phase.unbatchable_mesh_keys { let unbatchables = phase.unbatchable_mesh_values.get_mut(key).unwrap(); - for &(entity, main_entity) in &unbatchables.entities { - let Some(input_index) = - GFBD::get_binned_index(&system_param_item, (entity, main_entity)) + + // Allocate the indirect parameters if necessary. + let mut indirect_parameters_offset = if no_indirect_drawing { + None + } else if key.0.indexed() { + Some( + indirect_parameters_buffers + .allocate_indexed(unbatchables.entities.len() as u32), + ) + } else { + Some( + indirect_parameters_buffers + .allocate_non_indexed(unbatchables.entities.len() as u32), + ) + }; + + for &(_, main_entity) in &unbatchables.entities { + let Some(input_index) = GFBD::get_binned_index(&system_param_item, main_entity) else { continue; }; let output_index = data_buffer.add() as u32; - if !no_indirect_drawing { + if let Some(ref mut indirect_parameters_index) = indirect_parameters_offset { // We're in indirect mode, so add an indirect parameters // index. - let indirect_parameters_index = GFBD::get_batch_indirect_parameters_index( - &system_param_item, - &mut indirect_parameters_buffer, - (entity, main_entity), + GFBD::write_batch_indirect_parameters_metadata( + input_index.into(), + key.0.indexed(), output_index, - ) - .unwrap_or_default(); - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: input_index.into(), - output_index: indirect_parameters_index.into(), - }); + None, + &mut indirect_parameters_buffers, + *indirect_parameters_index, + ); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index, + indirect_parameters_index: *indirect_parameters_index, + }, + ); unbatchables .buffer_indices .add(UnbatchableBinnedEntityIndices { - instance_index: indirect_parameters_index.into(), - extra_index: PhaseItemExtraIndex::IndirectParametersIndex( - u32::from(indirect_parameters_index) - ..(u32::from(indirect_parameters_index) + 1), - ), + instance_index: *indirect_parameters_index, + extra_index: PhaseItemExtraIndex::IndirectParametersIndex { + range: *indirect_parameters_index..(*indirect_parameters_index + 1), + batch_set_index: None, + }, }); + indirect_parameters_buffers + .add_batch_set(key.0.indexed(), *indirect_parameters_index); + *indirect_parameters_index += 1; } else { - work_item_buffer.buffer.push(PreprocessWorkItem { - input_index: input_index.into(), - output_index, - }); + work_item_buffer.push( + key.0.indexed(), + PreprocessWorkItem { + input_index: input_index.into(), + output_index, + indirect_parameters_index: 0, + }, + ); unbatchables .buffer_indices .add(UnbatchableBinnedEntityIndices { @@ -837,7 +1392,7 @@ pub fn write_batched_instance_buffers( { let BatchedInstanceBuffers { ref mut data_buffer, - work_item_buffers: ref mut index_buffers, + ref mut work_item_buffers, ref mut current_input_buffer, ref mut previous_input_buffer, } = gpu_array_buffer.into_inner(); @@ -850,18 +1405,76 @@ pub fn write_batched_instance_buffers( .buffer .write_buffer(&render_device, &render_queue); - for index_buffer in index_buffers.values_mut() { - index_buffer - .buffer - .write_buffer(&render_device, &render_queue); + for view_work_item_buffers in work_item_buffers.values_mut() { + for phase_work_item_buffers in view_work_item_buffers.values_mut() { + match *phase_work_item_buffers { + PreprocessWorkItemBuffers::Direct(ref mut buffer_vec) => { + buffer_vec.write_buffer(&render_device, &render_queue); + } + PreprocessWorkItemBuffers::Indirect { + ref mut indexed, + ref mut non_indexed, + } => { + indexed.write_buffer(&render_device, &render_queue); + non_indexed.write_buffer(&render_device, &render_queue); + } + } + } } } -pub fn write_indirect_parameters_buffer( +pub fn clear_indirect_parameters_buffers( + mut indirect_parameters_buffers: ResMut, +) { + indirect_parameters_buffers.indexed_data.clear(); + indirect_parameters_buffers.indexed_metadata.clear(); + indirect_parameters_buffers.indexed_batch_sets.clear(); + indirect_parameters_buffers.non_indexed_data.clear(); + indirect_parameters_buffers.non_indexed_metadata.clear(); + indirect_parameters_buffers.non_indexed_batch_sets.clear(); +} + +pub fn write_indirect_parameters_buffers( render_device: Res, render_queue: Res, - mut indirect_parameters_buffer: ResMut, + mut indirect_parameters_buffers: ResMut, ) { - indirect_parameters_buffer.write_buffer(&render_device, &render_queue); - indirect_parameters_buffer.clear(); + indirect_parameters_buffers + .indexed_data + .write_buffer(&render_device); + indirect_parameters_buffers + .non_indexed_data + .write_buffer(&render_device); + + indirect_parameters_buffers + .indexed_metadata + .write_buffer(&render_device, &render_queue); + indirect_parameters_buffers + .non_indexed_metadata + .write_buffer(&render_device, &render_queue); + + indirect_parameters_buffers + .indexed_batch_sets + .write_buffer(&render_device, &render_queue); + indirect_parameters_buffers + .non_indexed_batch_sets + .write_buffer(&render_device, &render_queue); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn instance_buffer_correct_behavior() { + let mut instance_buffer = InstanceInputUniformBuffer::new(); + + let index = instance_buffer.add(2); + instance_buffer.remove(index); + assert_eq!(instance_buffer.get_unchecked(index), 2); + assert_eq!(instance_buffer.get(index), None); + + instance_buffer.add(5); + assert_eq!(instance_buffer.buffer().len(), 1); + } } diff --git a/crates/bevy_render/src/batching/mod.rs b/crates/bevy_render/src/batching/mod.rs index 31adef26f62e8..214fdda13644e 100644 --- a/crates/bevy_render/src/batching/mod.rs +++ b/crates/bevy_render/src/batching/mod.rs @@ -6,7 +6,7 @@ use bevy_ecs::{ use bytemuck::Pod; use nonmax::NonMaxU32; -use self::gpu_preprocessing::IndirectParametersBuffer; +use self::gpu_preprocessing::IndirectParametersBuffers; use crate::{render_phase::PhaseItemExtraIndex, sync_world::MainEntity}; use crate::{ render_phase::{ @@ -20,7 +20,7 @@ pub mod gpu_preprocessing; pub mod no_gpu_preprocessing; /// Add this component to mesh entities to disable automatic batching -#[derive(Component)] +#[derive(Component, Default)] pub struct NoAutomaticBatching; /// Data necessary to be equal for two draw commands to be mergeable @@ -58,7 +58,9 @@ impl BatchMeta { PhaseItemExtraIndex::DynamicOffset(dynamic_offset) => { NonMaxU32::new(dynamic_offset) } - PhaseItemExtraIndex::None | PhaseItemExtraIndex::IndirectParametersIndex(_) => None, + PhaseItemExtraIndex::None | PhaseItemExtraIndex::IndirectParametersIndex { .. } => { + None + } }, user_data, } @@ -114,7 +116,7 @@ pub trait GetFullBatchData: GetBatchData { /// [`GetFullBatchData::get_index_and_compare_data`] instead. fn get_binned_batch_data( param: &SystemParamItem, - query_item: (Entity, MainEntity), + query_item: MainEntity, ) -> Option; /// Returns the index of the [`GetFullBatchData::BufferInputData`] that the @@ -126,7 +128,7 @@ pub trait GetFullBatchData: GetBatchData { /// function will never be called. fn get_index_and_compare_data( param: &SystemParamItem, - query_item: (Entity, MainEntity), + query_item: MainEntity, ) -> Option<(NonMaxU32, Option)>; /// Returns the index of the [`GetFullBatchData::BufferInputData`] that the @@ -138,21 +140,40 @@ pub trait GetFullBatchData: GetBatchData { /// function will never be called. fn get_binned_index( param: &SystemParamItem, - query_item: (Entity, MainEntity), + query_item: MainEntity, ) -> Option; - /// Pushes [`gpu_preprocessing::IndirectParameters`] necessary to draw this - /// batch onto the given [`IndirectParametersBuffer`], and returns its - /// index. + /// Writes the [`gpu_preprocessing::IndirectParametersMetadata`] necessary + /// to draw this batch into the given metadata buffer at the given index. /// /// This is only used if GPU culling is enabled (which requires GPU /// preprocessing). - fn get_batch_indirect_parameters_index( - param: &SystemParamItem, - indirect_parameters_buffer: &mut IndirectParametersBuffer, - entity: (Entity, MainEntity), - instance_index: u32, - ) -> Option; + /// + /// * `mesh_index` describes the index of the first mesh instance in this + /// batch in the `MeshInputUniform` buffer. + /// + /// * `indexed` is true if the mesh is indexed or false if it's non-indexed. + /// + /// * `base_output_index` is the index of the first mesh instance in this + /// batch in the `MeshUniform` output buffer. + /// + /// * `batch_set_index` is the index of the batch set in the + /// [`gpu_preprocessing::IndirectBatchSet`] buffer, if this batch belongs to + /// a batch set. + /// + /// * `indirect_parameters_buffers` is the buffer in which to write the + /// metadata. + /// + /// * `indirect_parameters_offset` is the index in that buffer at which to + /// write the metadata. + fn write_batch_indirect_parameters_metadata( + mesh_index: u32, + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + indirect_parameters_buffers: &mut IndirectParametersBuffers, + indirect_parameters_offset: u32, + ); } /// Sorts a render phase that uses bins. @@ -161,6 +182,7 @@ where BPI: BinnedPhaseItem, { for phase in phases.values_mut() { + phase.multidrawable_mesh_keys.sort_unstable(); phase.batchable_mesh_keys.sort_unstable(); phase.unbatchable_mesh_keys.sort_unstable(); } diff --git a/crates/bevy_render/src/batching/no_gpu_preprocessing.rs b/crates/bevy_render/src/batching/no_gpu_preprocessing.rs index 41ddea778cfd6..d1e6c5b248ddc 100644 --- a/crates/bevy_render/src/batching/no_gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/no_gpu_preprocessing.rs @@ -2,8 +2,8 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::system::{Res, ResMut, Resource, StaticSystemParam}; -use bevy_utils::tracing::error; use smallvec::{smallvec, SmallVec}; +use tracing::error; use wgpu::BindingResource; use crate::{ @@ -108,9 +108,9 @@ pub fn batch_and_prepare_binned_render_phase( for key in &phase.batchable_mesh_keys { let mut batch_set: SmallVec<[BinnedRenderPhaseBatch; 1]> = smallvec![]; - for &(entity, main_entity) in &phase.batchable_mesh_values[key] { + for &(entity, main_entity) in &phase.batchable_mesh_values[key].entities { let Some(buffer_data) = - GFBD::get_binned_batch_data(&system_param_item, (entity, main_entity)) + GFBD::get_binned_batch_data(&system_param_item, main_entity) else { continue; }; @@ -145,7 +145,7 @@ pub fn batch_and_prepare_binned_render_phase( batch_sets.push(batch_set); } BinnedRenderPhaseBatchSets::Direct(_) - | BinnedRenderPhaseBatchSets::MultidrawIndirect(_) => { + | BinnedRenderPhaseBatchSets::MultidrawIndirect { .. } => { error!( "Dynamic uniform batch sets should be used when GPU preprocessing is off" ); @@ -156,8 +156,9 @@ pub fn batch_and_prepare_binned_render_phase( // Prepare unbatchables. for key in &phase.unbatchable_mesh_keys { let unbatchables = phase.unbatchable_mesh_values.get_mut(key).unwrap(); - for &entity in &unbatchables.entities { - let Some(buffer_data) = GFBD::get_binned_batch_data(&system_param_item, entity) + for &(_, main_entity) in &unbatchables.entities { + let Some(buffer_data) = + GFBD::get_binned_batch_data(&system_param_item, main_entity) else { continue; }; diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index d552000d0b5fd..1bc4b3737ad81 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1,3 +1,7 @@ +#![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}, @@ -10,7 +14,7 @@ use crate::{ texture::GpuImage, view::{ ColorGrading, ExtractedView, ExtractedWindows, Msaa, NoIndirectDrawing, RenderLayers, - RenderVisibleEntities, ViewUniformOffset, Visibility, VisibleEntities, + RenderVisibleEntities, RetainedViewEntity, ViewUniformOffset, Visibility, VisibleEntities, }, Extract, }; @@ -18,8 +22,8 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::{Component, ComponentId, Mutable}, - entity::Entity, + component::{Component, ComponentId}, + entity::{Entity, EntityBorrow}, event::EventReader, prelude::{require, With}, query::Has, @@ -32,13 +36,14 @@ use bevy_math::{ops, vec2, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, UVec use bevy_reflect::prelude::*; use bevy_render_macros::ExtractComponent; use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::{tracing::warn, HashMap, HashSet}; +use bevy_utils::{HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, WindowScaleFactorChanged, }; use core::ops::Range; use derive_more::derive::From; +use tracing::warn; use wgpu::{BlendState, TextureFormat, TextureUsages}; /// Render viewport configuration for the [`Camera`] component. @@ -883,13 +888,7 @@ impl NormalizedRenderTarget { /// 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. It also queries any [`CameraProjection`] component associated with the same -/// entity as the [`Camera`] one, to automatically update the camera projection matrix. -/// -/// The system function is generic over the camera projection type, and only instances of -/// [`OrthographicProjection`] and [`PerspectiveProjection`] are automatically added to -/// the app, as well as the runtime-selected [`Projection`]. -/// The system runs during [`PostUpdate`](bevy_app::PostUpdate). +/// [`Projection`] if needed. /// /// ## World Resources /// @@ -898,8 +897,7 @@ impl NormalizedRenderTarget { /// /// [`OrthographicProjection`]: crate::camera::OrthographicProjection /// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection -#[allow(clippy::too_many_arguments)] -pub fn camera_system>( +pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, mut window_scale_factor_changed_events: EventReader, @@ -908,7 +906,7 @@ pub fn camera_system>( windows: Query<(Entity, &Window)>, images: Res>, manual_texture_views: Res, - mut cameras: Query<(&mut Camera, &mut T)>, + mut cameras: Query<(&mut Camera, &mut Projection)>, ) { let primary_window = primary_window.iter().next(); @@ -1047,6 +1045,7 @@ pub fn extract_cameras( mut commands: Commands, query: Extract< Query<( + Entity, RenderEntity, &Camera, &CameraRenderGraph, @@ -1067,6 +1066,7 @@ pub fn extract_cameras( ) { let primary_window = primary_window.iter().next(); for ( + main_entity, render_entity, camera, camera_render_graph, @@ -1153,6 +1153,7 @@ pub fn extract_cameras( 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, @@ -1220,10 +1221,7 @@ pub fn sort_cameras( // sort by order and ensure within an order, RenderTargets of the same type are packed together sorted_cameras .0 - .sort_by(|c1, c2| match c1.order.cmp(&c2.order) { - core::cmp::Ordering::Equal => c1.target.cmp(&c2.target), - ord => ord, - }); + .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(); diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index 274b12bca84fa..b5698a0f821c4 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -4,7 +4,7 @@ use crate::{ renderer::RenderContext, view::ExtractedWindows, }; -use bevy_ecs::{prelude::QueryState, world::World}; +use bevy_ecs::{entity::EntityBorrow, prelude::QueryState, world::World}; use bevy_utils::HashSet; use wgpu::{LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp}; @@ -71,7 +71,7 @@ impl Node for CameraDriverNode { }; #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("no_camera_clear_pass").entered(); + let _span = tracing::info_span!("no_camera_clear_pass").entered(); let pass_descriptor = RenderPassDescriptor { label: Some("no_camera_clear_pass"), color_attachments: &[Some(RenderPassColorAttachment { diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 83c882cc3ed8e..9b3c6907f60fc 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -1,4 +1,3 @@ -#[allow(clippy::module_inception)] mod camera; mod camera_driver_node; mod clear_color; @@ -33,9 +32,7 @@ impl Plugin for CameraPlugin { .init_resource::() .init_resource::() .add_plugins(( - CameraProjectionPlugin::::default(), - CameraProjectionPlugin::::default(), - CameraProjectionPlugin::::default(), + CameraProjectionPlugin, ExtractResourcePlugin::::default(), ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index fd3880c1db6bf..3b04334f5b207 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,12 +1,11 @@ -use core::marker::PhantomData; +use core::fmt::Debug; use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; -use bevy_ecs::{component::Mutable, prelude::*}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; -use bevy_reflect::{ - std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, -}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_transform::{components::GlobalTransform, TransformSystem}; use derive_more::derive::From; use serde::{Deserialize, Serialize}; @@ -14,49 +13,31 @@ use serde::{Deserialize, Serialize}; /// Adds [`Camera`](crate::camera::Camera) driver systems for a given projection type. /// /// If you are using `bevy_pbr`, then you need to add `PbrProjectionPlugin` along with this. -pub struct CameraProjectionPlugin( - PhantomData, -); -impl + GetTypeRegistration> Plugin - for CameraProjectionPlugin -{ +#[derive(Default)] +pub struct CameraProjectionPlugin; + +impl Plugin for CameraProjectionPlugin { fn build(&self, app: &mut App) { - app.register_type::() + app.register_type::() + .register_type::() + .register_type::() + .register_type::() .add_systems( PostStartup, - crate::camera::camera_system:: - .in_set(CameraUpdateSystem) - // We assume that each camera will only have one projection, - // so we can ignore ambiguities with all other monomorphizations. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(CameraUpdateSystem), + crate::camera::camera_system.in_set(CameraUpdateSystem), ) .add_systems( PostUpdate, ( - crate::camera::camera_system:: - .in_set(CameraUpdateSystem) - // We assume that each camera will only have one projection, - // so we can ignore ambiguities with all other monomorphizations. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(CameraUpdateSystem), - crate::view::update_frusta:: + crate::camera::camera_system.in_set(CameraUpdateSystem), + crate::view::update_frusta .in_set(VisibilitySystems::UpdateFrusta) - .after(crate::camera::camera_system::) - .after(TransformSystem::TransformPropagate) - // We assume that no camera will have more than one projection component, - // 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(VisibilitySystems::UpdateFrusta), + .after(crate::camera::camera_system) + .after(TransformSystem::TransformPropagate), ), ); } } -impl Default for CameraProjectionPlugin { - fn default() -> Self { - Self(Default::default()) - } -} /// Label for [`camera_system`], shared across all `T`. /// @@ -64,21 +45,40 @@ impl Default for CameraPr #[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] pub struct CameraUpdateSystem; -/// Trait to control the projection matrix of a camera. +/// Describes a type that can generate a projection matrix, allowing it to be added to a +/// [`Camera`]'s [`Projection`] component. /// -/// Components implementing this trait are automatically polled for changes, and used -/// to recompute the camera projection matrix of the [`Camera`] component attached to -/// the same entity as the component implementing this trait. +/// Once implemented, the projection can be added to a camera using [`Projection::custom`]. /// -/// Use the plugins [`CameraProjectionPlugin`] and `bevy::pbr::PbrProjectionPlugin` to setup the -/// systems for your [`CameraProjection`] implementation. +/// The projection will be automatically updated as the render area is resized. This is useful when, +/// for example, a projection type has a field like `fov` that should change when the window width +/// is changed but not when the height changes. +/// +/// This trait is implemented by bevy's built-in projections [`PerspectiveProjection`] and +/// [`OrthographicProjection`]. /// /// [`Camera`]: crate::camera::Camera pub trait CameraProjection { + /// Generate the projection matrix. fn get_clip_from_view(&self) -> Mat4; + + /// Generate the projection matrix for a [`SubCameraView`](super::SubCameraView). fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4; + + /// When the area this camera renders to changes dimensions, this method will be automatically + /// called. Use this to update any projection properties that depend on the aspect ratio or + /// dimensions of the render area. fn update(&mut self, width: f32, height: f32); + + /// The far plane distance of the projection. fn far(&self) -> f32; + + /// The eight corners of the camera frustum, as defined by this projection. + /// + /// The corners should be provided in the following order: first the bottom right, top right, + /// top left, bottom left for the near plane, then similar for the far plane. + // TODO: This seems somewhat redundant with `compute_frustum`, and similarly should be possible + // to compute with a default impl. fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8]; /// Compute camera frustum for camera with given projection and transform. @@ -97,12 +97,152 @@ pub trait CameraProjection { } } -/// A configurable [`CameraProjection`] that can select its projection type at runtime. +mod sealed { + use super::CameraProjection; + + /// A wrapper trait to make it possible to implement Clone for boxed [`super::CameraProjection`] + /// trait objects, without breaking object safety rules by making it `Sized`. Additional bounds + /// are included for downcasting, and fulfilling the trait bounds on `Projection`. + pub trait DynCameraProjection: + CameraProjection + core::fmt::Debug + Send + Sync + downcast_rs::Downcast + { + fn clone_box(&self) -> Box; + } + + downcast_rs::impl_downcast!(DynCameraProjection); + + impl DynCameraProjection for T + where + T: 'static + CameraProjection + core::fmt::Debug + Send + Sync + Clone, + { + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } + } +} + +/// Holds a dynamic [`CameraProjection`] trait object. Use [`Projection::custom()`] to construct a +/// custom projection. +/// +/// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`]. +#[derive(Component, Debug, Reflect, Deref, DerefMut)] +#[reflect(Default)] +pub struct CustomProjection { + #[reflect(ignore)] + #[deref] + dyn_projection: Box, +} + +impl Default for CustomProjection { + fn default() -> Self { + Self { + dyn_projection: Box::new(PerspectiveProjection::default()), + } + } +} + +impl Clone for CustomProjection { + fn clone(&self) -> Self { + Self { + dyn_projection: self.dyn_projection.clone_box(), + } + } +} + +impl CustomProjection { + /// Returns a reference to the [`CameraProjection`] `P`. + /// + /// Returns `None` if this dynamic object is not a projection of type `P`. + /// + /// ``` + /// # use bevy_render::prelude::{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 }; + /// + /// // At this point the projection type is erased. + /// // We can use `get()` if we know what kind of projection we have. + /// let perspective = custom.get::().unwrap(); + /// + /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov); + /// ``` + pub fn get

(&self) -> Option<&P> + where + P: CameraProjection + Debug + Send + Sync + Clone + 'static, + { + self.dyn_projection.downcast_ref() + } + + /// Returns a mutable reference to the [`CameraProjection`] `P`. + /// + /// Returns `None` if this dynamic object is not a projection of type `P`. + /// + /// ``` + /// # use bevy_render::prelude::{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 }; + /// + /// // At this point the projection type is erased. + /// // We can use `get_mut()` if we know what kind of projection we have. + /// let perspective = custom.get_mut::().unwrap(); + /// + /// assert_eq!(perspective.fov, PerspectiveProjection::default().fov); + /// perspective.fov = 1.0; + /// ``` + pub fn get_mut

(&mut self) -> Option<&mut P> + where + P: CameraProjection + Debug + Send + Sync + Clone + 'static, + { + self.dyn_projection.downcast_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 +/// majority of use cases. Custom projections can be added using the [`CameraProjection`] trait and +/// the [`Projection::custom`] constructor. +/// +/// ## What's a projection? +/// +/// A camera projection essentially describes how 3d points from the point of view of a camera are +/// projected onto a 2d screen. This is where properties like a camera's field of view are defined. +/// More specifically, a projection is a 4x4 matrix that transforms points from view space (the +/// point of view of the camera) into clip space. Clip space is almost, but not quite, equivalent to +/// the rectangle that is rendered to your screen, with a depth axis. Any points that land outside +/// the bounds of this cuboid are "clipped" and not rendered. +/// +/// You can also think of the projection as the thing that describes the shape of a camera's +/// frustum: the volume in 3d space that is visible to a camera. +/// +/// [`Camera`]: crate::camera::Camera #[derive(Component, Debug, Clone, Reflect, From)] #[reflect(Component, Default, Debug)] pub enum Projection { Perspective(PerspectiveProjection), Orthographic(OrthographicProjection), + Custom(CustomProjection), +} + +impl Projection { + /// Construct a new custom camera projection from a type that implements [`CameraProjection`]. + pub fn custom

(projection: P) -> Self + where + // Implementation note: pushing these trait bounds all the way out to this function makes + // errors nice for users. If a trait is missing, they will get a helpful error telling them + // 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 + // trait the user should be implementing - they only need to worry about implementing + // `CameraProjection`. + P: CameraProjection + Debug + Send + Sync + Clone + 'static, + { + Projection::Custom(CustomProjection { + dyn_projection: Box::new(projection), + }) + } } impl CameraProjection for Projection { @@ -110,6 +250,7 @@ impl CameraProjection for Projection { 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(), } } @@ -117,6 +258,7 @@ impl CameraProjection for Projection { 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), } } @@ -124,6 +266,7 @@ impl CameraProjection for Projection { match self { Projection::Perspective(projection) => projection.update(width, height), Projection::Orthographic(projection) => projection.update(width, height), + Projection::Custom(projection) => projection.update(width, height), } } @@ -131,6 +274,7 @@ impl CameraProjection for Projection { match self { Projection::Perspective(projection) => projection.far(), Projection::Orthographic(projection) => projection.far(), + Projection::Custom(projection) => projection.far(), } } @@ -138,6 +282,7 @@ impl CameraProjection for Projection { 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), } } } @@ -149,8 +294,8 @@ impl Default for Projection { } /// A 3D camera projection in which distant objects appear smaller than close objects. -#[derive(Component, Debug, Clone, Reflect)] -#[reflect(Component, Default, Debug)] +#[derive(Debug, Clone, Reflect)] +#[reflect(Default, Debug)] pub struct PerspectiveProjection { /// The vertical field of view (FOV) in radians. /// @@ -341,8 +486,8 @@ pub enum ScalingMode { /// ..OrthographicProjection::default_2d() /// }); /// ``` -#[derive(Component, Debug, Clone, Reflect)] -#[reflect(Component, Debug, FromWorld)] +#[derive(Debug, Clone, Reflect)] +#[reflect(Debug, FromWorld)] pub struct OrthographicProjection { /// The distance of the near clipping plane in world units. /// diff --git a/crates/bevy_render/src/diagnostic/internal.rs b/crates/bevy_render/src/diagnostic/internal.rs index 872323f80fc2d..ba15a95dde55a 100644 --- a/crates/bevy_render/src/diagnostic/internal.rs +++ b/crates/bevy_render/src/diagnostic/internal.rs @@ -7,7 +7,7 @@ use std::thread::{self, ThreadId}; use bevy_diagnostic::{Diagnostic, DiagnosticMeasurement, DiagnosticPath, DiagnosticsStore}; use bevy_ecs::system::{Res, ResMut, Resource}; -use bevy_utils::{tracing, Instant}; +use bevy_utils::Instant; use std::sync::Mutex; use wgpu::{ Buffer, BufferDescriptor, BufferUsages, CommandEncoder, ComputePass, Features, MapMode, diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index 6e91e2a736e79..09b6052c10ebe 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -43,7 +43,6 @@ use super::{RenderDevice, RenderQueue}; /// # Supported platforms /// Timestamp queries and pipeline statistics are currently supported only on Vulkan and DX12. /// On other platforms (Metal, WebGPU, WebGL2) only CPU time will be recorded. -#[allow(clippy::doc_markdown)] #[derive(Default)] pub struct RenderDiagnosticsPlugin; diff --git a/crates/bevy_render/src/extract_resource.rs b/crates/bevy_render/src/extract_resource.rs index 2d36e694be7e5..cec8647ffc048 100644 --- a/crates/bevy_render/src/extract_resource.rs +++ b/crates/bevy_render/src/extract_resource.rs @@ -3,6 +3,7 @@ use core::marker::PhantomData; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; pub use bevy_render_macros::ExtractResource; +use bevy_utils::once; use crate::{Extract, ExtractSchedule, RenderApp}; @@ -34,10 +35,10 @@ impl Plugin for ExtractResourcePlugin { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems(ExtractSchedule, extract_resource::); } else { - bevy_utils::error_once!( + once!(tracing::error!( "Render app did not exist when trying to add `extract_resource` for <{}>.", core::any::type_name::() - ); + )); } } } @@ -56,12 +57,13 @@ pub fn extract_resource( } else { #[cfg(debug_assertions)] if !main_resource.is_added() { - bevy_utils::warn_once!( + once!(tracing::warn!( "Removing resource {} from render world not expected, adding using `Commands`. This may decrease performance", core::any::type_name::() - ); + )); } + commands.insert_resource(R::extract_resource(main_resource)); } } diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index d67db45e0986b..75742de909622 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -23,11 +23,12 @@ use bevy_ecs::{ use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_reflect::Reflect; use bevy_render_macros::ExtractComponent; -use bevy_utils::{tracing::warn, HashMap}; +use bevy_utils::HashMap; use encase::internal::ReadFrom; use encase::private::Reader; use encase::ShaderType; -use wgpu::{CommandEncoder, COPY_BYTES_PER_ROW_ALIGNMENT}; +use tracing::warn; +use wgpu::CommandEncoder; /// A plugin that enables reading back gpu buffers and textures to the cpu. pub struct GpuReadbackPlugin { @@ -339,7 +340,7 @@ fn map_buffers(mut readbacks: ResMut) { drop(data); buffer.unmap(); if let Err(e) = tx.try_send((entity, buffer, result)) { - warn!("Failed to send readback result: {:?}", e); + warn!("Failed to send readback result: {}", e); } }); readbacks.mapped.push(readback); @@ -348,14 +349,17 @@ fn map_buffers(mut readbacks: ResMut) { // Utils -pub(crate) fn align_byte_size(value: u32) -> u32 { - value + (COPY_BYTES_PER_ROW_ALIGNMENT - (value % COPY_BYTES_PER_ROW_ALIGNMENT)) +/// Round up a given value to be a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]. +pub(crate) const fn align_byte_size(value: u32) -> u32 { + RenderDevice::align_copy_bytes_per_row(value as usize) as u32 } -pub(crate) fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 { +/// Get the size of a image when the size of each row has been rounded up to [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]. +pub(crate) const fn get_aligned_size(width: u32, height: u32, pixel_size: u32) -> u32 { height * align_byte_size(width * pixel_size) } +/// Get a [`ImageDataLayout`] aligned such that the image can be copied into a buffer. pub(crate) fn layout_data(width: u32, height: u32, format: TextureFormat) -> ImageDataLayout { ImageDataLayout { bytes_per_row: if height > 1 { diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 0acdd5ad50748..32bc6d0305278 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -1,5 +1,5 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] -#![expect(unsafe_code)] +#![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")] #![cfg_attr( any(docsrs, docsrs_dep), expect( @@ -40,7 +40,6 @@ pub mod render_phase; pub mod render_resource; pub mod renderer; pub mod settings; -mod spatial_bundle; pub mod storage; pub mod sync_component; pub mod sync_world; @@ -50,7 +49,6 @@ pub mod view; /// The render prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -64,9 +62,8 @@ pub mod prelude { Mesh3d, }, render_resource::Shader, - spatial_bundle::SpatialBundle, texture::ImagePlugin, - view::{InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilityBundle}, + view::{InheritedVisibility, Msaa, ViewVisibility, Visibility}, ExtractSchedule, }; } @@ -80,7 +77,7 @@ use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; -use renderer::{RenderDevice, RenderQueue}; +use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use sync_world::{ despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, SyncWorldPlugin, @@ -101,9 +98,9 @@ use alloc::sync::Arc; use bevy_app::{App, AppLabel, Plugin, SubApp}; use bevy_asset::{load_internal_asset, AssetApp, AssetServer, Handle}; use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; -use bevy_utils::tracing::debug; use core::ops::{Deref, DerefMut}; use std::sync::Mutex; +use tracing::debug; /// Contains the default Bevy rendering backend based on wgpu. /// @@ -477,10 +474,8 @@ unsafe fn initialize_render_app(app: &mut App) { // This set applies the commands from the extract schedule while the render schedule // is running in parallel with the main app. apply_extract_commands.in_set(RenderSet::ExtractCommands), - ( - PipelineCache::process_pipeline_queue_system.before(render_system), - render_system, - ) + (PipelineCache::process_pipeline_queue_system, render_system) + .chain() .in_set(RenderSet::Render), despawn_temporary_render_entities.in_set(RenderSet::PostCleanup), ), @@ -489,7 +484,7 @@ unsafe fn initialize_render_app(app: &mut App) { render_app.set_extract(|main_world, render_world| { { #[cfg(feature = "trace")] - let _stage_span = bevy_utils::tracing::info_span!("entity_sync").entered(); + let _stage_span = tracing::info_span!("entity_sync").entered(); entity_sync_system(main_world, render_world); } @@ -514,3 +509,23 @@ fn apply_extract_commands(render_world: &mut World) { .apply_deferred(render_world); }); } + +/// If the [`RenderAdapter`] is a Qualcomm Adreno, returns its model number. +/// +/// This lets us work around hardware bugs. +pub fn get_adreno_model(adapter: &RenderAdapter) -> Option { + if !cfg!(target_os = "android") { + return None; + } + + let adapter_name = adapter.get_info().name; + let adreno_model = adapter_name.strip_prefix("Adreno (TM) ")?; + + // Take suffixes into account (like Adreno 642L). + Some( + adreno_model + .chars() + .map_while(|c| c.to_digit(10)) + .fold(0, |acc, digit| acc * 10 + digit), + ) +} diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index a9cb80c0fcabb..0098c8237c544 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -93,3 +93,9 @@ fn sphere_intersects_plane_half_space( fn powsafe(color: vec3, power: f32) -> vec3 { return pow(abs(color), vec3(power)) * sign(color); } + +// https://en.wikipedia.org/wiki/Vector_projection#Vector_projection_2 +fn project_onto(lhs: vec3, rhs: vec3) -> vec3 { + let other_len_sq_rcp = 1.0 / dot(rhs, rhs); + return rhs * dot(lhs, rhs) * other_len_sq_rcp; +} diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index 6e7afaa4d7cde..f144b371639ee 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -15,8 +15,9 @@ use bevy_ecs::{ system::{Res, ResMut, Resource}, world::{FromWorld, World}, }; -use bevy_utils::{default, tracing::error, HashMap, HashSet}; +use bevy_utils::{default, HashMap, HashSet}; use offset_allocator::{Allocation, Allocator}; +use tracing::error; use wgpu::{ BufferDescriptor, BufferSize, BufferUsages, CommandEncoderDescriptor, DownlevelFlags, COPY_BUFFER_ALIGNMENT, @@ -155,7 +156,6 @@ pub struct MeshBufferSlice<'a> { pub struct SlabId(pub NonMaxU32); /// Data for a single slab. -#[allow(clippy::large_enum_variant)] enum Slab { /// A slab that can contain multiple objects. General(GeneralSlab), @@ -526,7 +526,6 @@ impl MeshAllocator { } /// A generic function that copies either vertex or index data into a slab. - #[allow(clippy::too_many_arguments)] fn copy_element_data( &mut self, mesh_id: &AssetId, @@ -785,7 +784,7 @@ impl MeshAllocator { slab_to_grow: SlabToReallocate, ) { let Some(Slab::General(slab)) = self.slabs.get_mut(&slab_id) else { - error!("Couldn't find slab {:?} to grow", slab_id); + error!("Couldn't find slab {} to grow", slab_id); return; }; @@ -862,7 +861,7 @@ impl MeshAllocator { } impl GeneralSlab { - /// Creates a new growable slab big enough to hold an single element of + /// Creates a new growable slab big enough to hold a single element of /// `data_slot_count` size with the given `layout`. fn new( new_slab_id: SlabId, diff --git a/crates/bevy_render/src/mesh/components.rs b/crates/bevy_render/src/mesh/components.rs index 10229be41210d..2b887c65d32c5 100644 --- a/crates/bevy_render/src/mesh/components.rs +++ b/crates/bevy_render/src/mesh/components.rs @@ -2,11 +2,15 @@ use crate::{ mesh::Mesh, view::{self, Visibility, VisibilityClass}, }; -use bevy_asset::{AssetId, Handle}; +use bevy_asset::{AssetEvent, AssetId, Handle}; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{component::Component, prelude::require, reflect::ReflectComponent}; +use bevy_ecs::{ + change_detection::DetectChangesMut, component::Component, event::EventReader, prelude::require, + reflect::ReflectComponent, system::Query, +}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_transform::components::Transform; +use bevy_utils::{FixedHasher, HashSet}; use derive_more::derive::From; /// A component for 2D meshes. Requires a [`MeshMaterial2d`] to be rendered, commonly using a [`ColorMaterial`]. @@ -101,3 +105,32 @@ impl From<&Mesh3d> for AssetId { mesh.id() } } + +/// A system that marks a [`Mesh3d`] as changed if the associated [`Mesh`] asset +/// has changed. +/// +/// This is needed because the systems that extract meshes, such as +/// `extract_meshes_for_gpu_building`, write some metadata about the mesh (like +/// the location within each slab) into the GPU structures that they build that +/// needs to be kept up to date if the contents of the mesh change. +pub fn mark_3d_meshes_as_changed_if_their_assets_changed( + mut meshes_3d: Query<&mut Mesh3d>, + mut mesh_asset_events: EventReader>, +) { + let mut changed_meshes: HashSet, FixedHasher> = HashSet::default(); + for mesh_asset_event in mesh_asset_events.read() { + if let AssetEvent::Modified { id } = mesh_asset_event { + changed_meshes.insert(*id); + } + } + + if changed_meshes.is_empty() { + return; + } + + for mut mesh_3d in &mut meshes_3d { + if changed_meshes.contains(&mesh_3d.0.id()) { + mesh_3d.set_changed(); + } + } +} diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 7a7829e0f4ef1..703333675da74 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -9,6 +9,7 @@ use crate::{ render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_resource::TextureView, texture::GpuImage, + view::VisibilitySystems, RenderApp, }; use allocator::MeshAllocatorPlugin; @@ -17,6 +18,7 @@ use bevy_asset::{AssetApp, AssetId, RenderAssetUsages}; use bevy_ecs::{ entity::Entity, query::{Changed, With}, + schedule::IntoSystemConfigs, system::Query, }; use bevy_ecs::{ @@ -42,7 +44,12 @@ impl Plugin for MeshPlugin { .register_type::>() // 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready .add_plugins(RenderAssetPlugin::::default()) - .add_plugins(MeshAllocatorPlugin); + .add_plugins(MeshAllocatorPlugin) + .add_systems( + PostUpdate, + components::mark_3d_meshes_as_changed_if_their_assets_changed + .ambiguous_with(VisibilitySystems::CalculateBounds), + ); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -130,6 +137,12 @@ impl RenderMesh { pub fn primitive_topology(&self) -> PrimitiveTopology { self.key_bits.primitive_topology() } + + /// Returns true if this mesh uses an index buffer or false otherwise. + #[inline] + pub fn indexed(&self) -> bool { + matches!(self.buffer_info, RenderMeshBufferInfo::Indexed { .. }) + } } /// The index/vertex buffer info of a [`RenderMesh`]. diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 41279e7d25db1..39caaedac7048 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -148,7 +148,7 @@ impl Plugin for PipelinedRenderingPlugin { std::thread::spawn(move || { #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("render thread").entered(); + let _span = tracing::info_span!("render thread").entered(); let compute_task_pool = ComputeTaskPool::get(); loop { @@ -164,8 +164,7 @@ impl Plugin for PipelinedRenderingPlugin { { #[cfg(feature = "trace")] - let _sub_app_span = - bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered(); + let _sub_app_span = tracing::info_span!("sub app", name = ?RenderApp).entered(); render_app.update(); } @@ -174,7 +173,7 @@ impl Plugin for PipelinedRenderingPlugin { } } - bevy_utils::tracing::debug!("exiting pipelined rendering thread"); + tracing::debug!("exiting pipelined rendering thread"); }); } } diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 2757dceb9fa81..bd115e4e57d32 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -11,12 +11,10 @@ use bevy_ecs::{ world::{FromWorld, Mut}, }; use bevy_render_macros::ExtractResource; -use bevy_utils::{ - tracing::{debug, error}, - HashMap, HashSet, -}; +use bevy_utils::{HashMap, HashSet}; use core::marker::PhantomData; use thiserror::Error; +use tracing::{debug, error}; #[derive(Debug, Error)] pub enum PrepareAssetError { @@ -55,7 +53,10 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { /// Size of the data the asset will upload to the gpu. Specifying a return value /// will allow the asset to be throttled via [`RenderAssetBytesPerFrame`]. #[inline] - #[allow(unused_variables)] + #[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(source_asset: &Self::SourceAsset) -> Option { None } @@ -237,7 +238,10 @@ pub(crate) fn extract_render_asset( let mut removed = >::default(); for event in events.read() { - #[allow(clippy::match_same_arms)] + #[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 } | AssetEvent::Modified { id } => { changed_assets.insert(*id); diff --git a/crates/bevy_render/src/render_graph/app.rs b/crates/bevy_render/src/render_graph/app.rs index 80ffcdb2a1f8d..338ae75d7a284 100644 --- a/crates/bevy_render/src/render_graph/app.rs +++ b/crates/bevy_render/src/render_graph/app.rs @@ -1,6 +1,6 @@ use bevy_app::{App, SubApp}; use bevy_ecs::world::FromWorld; -use bevy_utils::tracing::warn; +use tracing::warn; use super::{IntoRenderNodeArray, Node, RenderGraph, RenderLabel, RenderSubGraph}; diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 91775fef7ccef..cc4875a7791a8 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -238,7 +238,7 @@ pub struct NodeState { impl Debug for NodeState { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - writeln!(f, "{:?} ({:?})", self.label, self.type_name) + writeln!(f, "{:?} ({})", self.label, self.type_name) } } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 6f45e1b39783f..22f4ce5fc8fb6 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -22,7 +22,10 @@ pub trait Draw: Send + Sync + 'static { /// Prepares the draw function to be used. This is called once and only once before the phase /// begins. There may be zero or more [`draw`](Draw::draw) calls following a call to this function. /// Implementing this is optional. - #[allow(unused_variables)] + #[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 prepare(&mut self, world: &'_ World) {} /// Draws a [`PhaseItem`] by issuing zero or more `draw` calls via the [`TrackedRenderPass`]. @@ -232,7 +235,14 @@ macro_rules! render_command_tuple_impl { type ViewQuery = ($($name::ViewQuery,)*); type ItemQuery = ($($name::ItemQuery,)*); - #[allow(non_snake_case)] + #[expect( + clippy::allow_attributes, + reason = "We are in a macro; as such, `non_snake_case` may not always lint." + )] + #[allow( + non_snake_case, + reason = "Parameter and variable names are provided by the macro invocation, not by us." + )] fn render<'w>( _item: &P, ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index 4ba3e410870a5..a7b8acdc00393 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -8,10 +8,13 @@ use crate::{ renderer::RenderDevice, }; use bevy_color::LinearRgba; -use bevy_utils::{default, detailed_trace}; +use bevy_utils::default; use core::ops::Range; use wgpu::{IndexFormat, QuerySet, RenderPass}; +#[cfg(feature = "detailed_trace")] +use tracing::trace; + /// Tracks the state of a [`TrackedRenderPass`]. /// /// This is used to skip redundant operations on the [`TrackedRenderPass`] (e.g. setting an already @@ -164,7 +167,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// Subsequent draw calls will exhibit the behavior defined by the `pipeline`. pub fn set_render_pipeline(&mut self, pipeline: &'a RenderPipeline) { - detailed_trace!("set pipeline: {:?}", pipeline); + #[cfg(feature = "detailed_trace")] + trace!("set pipeline: {:?}", pipeline); if self.state.is_pipeline_set(pipeline.id()) { return; } @@ -189,7 +193,8 @@ impl<'a> TrackedRenderPass<'a> { .state .is_bind_group_set(index, bind_group.id(), dynamic_uniform_indices) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set bind_group {} (already set): {:?} ({:?})", index, bind_group, @@ -197,7 +202,8 @@ impl<'a> TrackedRenderPass<'a> { ); return; } - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set bind_group {}: {:?} ({:?})", index, bind_group, @@ -222,7 +228,8 @@ impl<'a> TrackedRenderPass<'a> { /// [`draw_indexed`]: TrackedRenderPass::draw_indexed pub fn set_vertex_buffer(&mut self, slot_index: usize, buffer_slice: BufferSlice<'a>) { if self.state.is_vertex_buffer_set(slot_index, &buffer_slice) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set vertex buffer {} (already set): {:?} (offset = {}, size = {})", slot_index, buffer_slice.id(), @@ -231,7 +238,8 @@ impl<'a> TrackedRenderPass<'a> { ); return; } - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set vertex buffer {}: {:?} (offset = {}, size = {})", slot_index, buffer_slice.id(), @@ -258,14 +266,16 @@ impl<'a> TrackedRenderPass<'a> { .state .is_index_buffer_set(buffer_slice.id(), offset, index_format) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set index buffer (already set): {:?} ({})", buffer_slice.id(), offset ); return; } - detailed_trace!("set index buffer: {:?} ({})", buffer_slice.id(), offset); + #[cfg(feature = "detailed_trace")] + trace!("set index buffer: {:?} ({})", buffer_slice.id(), offset); self.pass.set_index_buffer(*buffer_slice, index_format); self.state .set_index_buffer(buffer_slice.id(), offset, index_format); @@ -275,7 +285,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// The active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`]. pub fn draw(&mut self, vertices: Range, instances: Range) { - detailed_trace!("draw: {:?} {:?}", vertices, instances); + #[cfg(feature = "detailed_trace")] + trace!("draw: {:?} {:?}", vertices, instances); self.pass.draw(vertices, instances); } @@ -284,7 +295,8 @@ impl<'a> TrackedRenderPass<'a> { /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the /// active vertex buffer(s) can be set with [`TrackedRenderPass::set_vertex_buffer`]. pub fn draw_indexed(&mut self, indices: Range, base_vertex: i32, instances: Range) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "draw indexed: {:?} {} {:?}", indices, base_vertex, @@ -311,7 +323,8 @@ impl<'a> TrackedRenderPass<'a> { /// } /// ``` pub fn draw_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) { - detailed_trace!("draw indirect: {:?} {}", indirect_buffer, indirect_offset); + #[cfg(feature = "detailed_trace")] + trace!("draw indirect: {:?} {}", indirect_buffer, indirect_offset); self.pass.draw_indirect(indirect_buffer, indirect_offset); } @@ -335,7 +348,8 @@ impl<'a> TrackedRenderPass<'a> { /// } /// ``` pub fn draw_indexed_indirect(&mut self, indirect_buffer: &'a Buffer, indirect_offset: u64) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "draw indexed indirect: {:?} {}", indirect_buffer, indirect_offset @@ -367,7 +381,8 @@ impl<'a> TrackedRenderPass<'a> { indirect_offset: u64, count: u32, ) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "multi draw indirect: {:?} {}, {}x", indirect_buffer, indirect_offset, @@ -407,7 +422,8 @@ impl<'a> TrackedRenderPass<'a> { count_offset: u64, max_count: u32, ) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "multi draw indirect count: {:?} {}, ({:?} {})x, max {}x", indirect_buffer, indirect_offset, @@ -449,7 +465,8 @@ impl<'a> TrackedRenderPass<'a> { indirect_offset: u64, count: u32, ) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "multi draw indexed indirect: {:?} {}, {}x", indirect_buffer, indirect_offset, @@ -491,7 +508,8 @@ impl<'a> TrackedRenderPass<'a> { count_offset: u64, max_count: u32, ) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "multi draw indexed indirect count: {:?} {}, ({:?} {})x, max {}x", indirect_buffer, indirect_offset, @@ -512,7 +530,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// Subsequent stencil tests will test against this value. pub fn set_stencil_reference(&mut self, reference: u32) { - detailed_trace!("set stencil reference: {}", reference); + #[cfg(feature = "detailed_trace")] + trace!("set stencil reference: {}", reference); self.pass.set_stencil_reference(reference); } @@ -520,7 +539,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// Subsequent draw calls will discard any fragments that fall outside this region. pub fn set_scissor_rect(&mut self, x: u32, y: u32, width: u32, height: u32) { - detailed_trace!("set_scissor_rect: {} {} {} {}", x, y, width, height); + #[cfg(feature = "detailed_trace")] + trace!("set_scissor_rect: {} {} {} {}", x, y, width, height); self.pass.set_scissor_rect(x, y, width, height); } @@ -528,7 +548,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// `Features::PUSH_CONSTANTS` must be enabled on the device in order to call these functions. pub fn set_push_constants(&mut self, stages: ShaderStages, offset: u32, data: &[u8]) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set push constants: {:?} offset: {} data.len: {}", stages, offset, @@ -549,7 +570,8 @@ impl<'a> TrackedRenderPass<'a> { min_depth: f32, max_depth: f32, ) { - detailed_trace!( + #[cfg(feature = "detailed_trace")] + trace!( "set viewport: {} {} {} {} {} {}", x, y, @@ -580,7 +602,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// This is a GPU debugging feature. This has no effect on the rendering itself. pub fn insert_debug_marker(&mut self, label: &str) { - detailed_trace!("insert debug marker: {}", label); + #[cfg(feature = "detailed_trace")] + trace!("insert debug marker: {}", label); self.pass.insert_debug_marker(label); } @@ -605,7 +628,8 @@ impl<'a> TrackedRenderPass<'a> { /// [`push_debug_group`]: TrackedRenderPass::push_debug_group /// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group pub fn push_debug_group(&mut self, label: &str) { - detailed_trace!("push_debug_group marker: {}", label); + #[cfg(feature = "detailed_trace")] + trace!("push_debug_group marker: {}", label); self.pass.push_debug_group(label); } @@ -622,7 +646,8 @@ impl<'a> TrackedRenderPass<'a> { /// [`push_debug_group`]: TrackedRenderPass::push_debug_group /// [`pop_debug_group`]: TrackedRenderPass::pop_debug_group pub fn pop_debug_group(&mut self) { - detailed_trace!("pop_debug_group"); + #[cfg(feature = "detailed_trace")] + trace!("pop_debug_group"); self.pass.pop_debug_group(); } @@ -630,7 +655,8 @@ impl<'a> TrackedRenderPass<'a> { /// /// Subsequent blending tests will test against this value. pub fn set_blend_constant(&mut self, color: LinearRgba) { - detailed_trace!("set blend constant: {:?}", color); + #[cfg(feature = "detailed_trace")] + trace!("set blend constant: {:?}", color); self.pass.set_blend_constant(wgpu::Color::from(color)); } } diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 5899bc9c01ea2..2b8d0c9e8a362 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -36,9 +36,12 @@ pub use draw_state::*; use encase::{internal::WriteInto, ShaderSize}; use nonmax::NonMaxU32; pub use rangefinder::*; +use wgpu::Features; -use crate::batching::gpu_preprocessing::GpuPreprocessingMode; +use crate::batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}; +use crate::renderer::RenderDevice; use crate::sync_world::MainEntity; +use crate::view::RetainedViewEntity; use crate::{ batching::{ self, @@ -50,7 +53,6 @@ use crate::{ Render, RenderApp, RenderSet, }; use bevy_ecs::{ - entity::EntityHashMap, prelude::*, system::{lifetimeless::SRes, SystemParamItem}, }; @@ -63,7 +65,7 @@ use smallvec::SmallVec; /// They're cleared out every frame, but storing them in a resource like this /// allows us to reuse allocations. #[derive(Resource, Deref, DerefMut)] -pub struct ViewBinnedRenderPhases(pub EntityHashMap>) +pub struct ViewBinnedRenderPhases(pub HashMap>) where BPI: BinnedPhaseItem; @@ -85,28 +87,55 @@ pub struct BinnedRenderPhase where BPI: BinnedPhaseItem, { - /// A list of `BinKey`s for batchable items. + /// A list of `BatchSetKey`s for batchable, multidrawable items. + /// + /// These are accumulated in `queue_material_meshes` and then sorted in + /// `batching::sort_binned_render_phase`. + pub multidrawable_mesh_keys: Vec, + + /// The multidrawable bins themselves. + /// + /// Each batch set key maps to a *batch set*, which in this case is a set of + /// meshes that can be drawn together in one multidraw call. Each batch set + /// is subdivided into *bins*, each of which represents a particular mesh. + /// Each bin contains the entity IDs of instances of that mesh. + /// + /// So, for example, if there are two cubes and a sphere present in the + /// scene, we would generally have one batch set containing two bins, + /// assuming that the cubes and sphere meshes are allocated together and use + /// the same pipeline. The first bin, corresponding to the cubes, will have + /// two entities in it. The second bin, corresponding to the sphere, will + /// have one entity in it. + pub multidrawable_mesh_values: HashMap>, + + /// A list of `BinKey`s for batchable items that aren't multidrawable. /// /// These are accumulated in `queue_material_meshes` and then sorted in /// `batch_and_prepare_binned_render_phase`. - pub batchable_mesh_keys: Vec, + /// + /// Usually, batchable items aren't multidrawable due to platform or + /// hardware limitations. However, it's also possible to have batchable + /// items alongside multidrawable items with custom mesh pipelines. See + /// `specialized_mesh_pipeline` for an example. + pub batchable_mesh_keys: Vec<(BPI::BatchSetKey, BPI::BinKey)>, - /// The batchable bins themselves. + /// The bins corresponding to batchable items that aren't multidrawable. /// - /// Each bin corresponds to a single batch set. For unbatchable entities, - /// prefer `unbatchable_values` instead. - pub batchable_mesh_values: HashMap>, + /// For multidrawable entities, use `multidrawable_mesh_values`; for + /// unbatchable entities, use `unbatchable_values`. + pub batchable_mesh_values: HashMap<(BPI::BatchSetKey, BPI::BinKey), RenderBin>, /// A list of `BinKey`s for unbatchable items. /// /// These are accumulated in `queue_material_meshes` and then sorted in /// `batch_and_prepare_binned_render_phase`. - pub unbatchable_mesh_keys: Vec, + pub unbatchable_mesh_keys: Vec<(BPI::BatchSetKey, BPI::BinKey)>, /// The unbatchable bins. /// /// Each entity here is rendered in a separate drawcall. - pub unbatchable_mesh_values: HashMap, + pub unbatchable_mesh_values: + HashMap<(BPI::BatchSetKey, BPI::BinKey), UnbatchableBinnedEntities>, /// Items in the bin that aren't meshes at all. /// @@ -115,7 +144,7 @@ where /// entity are simply called in order at rendering time. /// /// See the `custom_phase_item` example for an example of how to use this. - pub non_mesh_items: Vec<(BPI::BinKey, (Entity, MainEntity))>, + pub non_mesh_items: Vec<(BPI::BatchSetKey, BPI::BinKey, (Entity, MainEntity))>, /// Information on each batch set. /// @@ -125,15 +154,23 @@ where /// platforms that support storage buffers, a batch set always consists of /// at most one batch. /// - /// The unbatchable entities immediately follow the batches in the storage - /// buffers. - pub(crate) batch_sets: BinnedRenderPhaseBatchSets, + /// Multidrawable entities come first, then batchable entities, then + /// unbatchable entities. + pub(crate) batch_sets: BinnedRenderPhaseBatchSets, +} + +/// All entities that share a mesh and a material and can be batched as part of +/// a [`BinnedRenderPhase`]. +#[derive(Default)] +pub struct RenderBin { + /// A list of the entities in each bin. + pub entities: Vec<(Entity, MainEntity)>, } /// How we store and render the batch sets. /// /// Each one of these corresponds to a [`GpuPreprocessingMode`]. -pub enum BinnedRenderPhaseBatchSets { +pub enum BinnedRenderPhaseBatchSets { /// Batches are grouped into batch sets based on dynamic uniforms. /// /// This corresponds to [`GpuPreprocessingMode::None`]. @@ -148,10 +185,16 @@ pub enum BinnedRenderPhaseBatchSets { /// be multi-drawn together. /// /// This corresponds to [`GpuPreprocessingMode::Culling`]. - MultidrawIndirect(Vec>), + MultidrawIndirect(Vec>), +} + +pub struct BinnedRenderPhaseBatchSet { + pub(crate) batches: Vec, + pub(crate) bin_key: BK, + pub(crate) index: u32, } -impl BinnedRenderPhaseBatchSets { +impl BinnedRenderPhaseBatchSets { fn clear(&mut self) { match *self { BinnedRenderPhaseBatchSets::DynamicUniforms(ref mut vec) => vec.clear(), @@ -237,8 +280,12 @@ pub(crate) struct UnbatchableBinnedEntityIndices { /// placed in. #[derive(Clone, Copy, PartialEq, Debug)] pub enum BinnedRenderPhaseType { - /// The item is a mesh that's eligible for indirect rendering and can be - /// batched with other meshes of the same type. + /// The item is a mesh that's eligible for multi-draw indirect rendering and + /// can be batched with other meshes of the same type. + MultidrawableMesh, + + /// The item is a mesh that's eligible for single-draw indirect rendering + /// and can be batched with other meshes of the same type. BatchableMesh, /// The item is a mesh that's eligible for indirect rendering, but can't be @@ -282,8 +329,12 @@ impl ViewBinnedRenderPhases where BPI: BinnedPhaseItem, { - pub fn insert_or_clear(&mut self, entity: Entity, gpu_preprocessing: GpuPreprocessingMode) { - match self.entry(entity) { + pub fn insert_or_clear( + &mut self, + retained_view_entity: RetainedViewEntity, + gpu_preprocessing: GpuPreprocessingMode, + ) { + match self.entry(retained_view_entity) { Entry::Occupied(mut entry) => entry.get_mut().clear(), Entry::Vacant(entry) => { entry.insert(BinnedRenderPhase::::new(gpu_preprocessing)); @@ -303,28 +354,65 @@ where /// type. pub fn add( &mut self, - key: BPI::BinKey, - entity: (Entity, MainEntity), + batch_set_key: BPI::BatchSetKey, + bin_key: BPI::BinKey, + (entity, main_entity): (Entity, MainEntity), phase_type: BinnedRenderPhaseType, ) { match phase_type { + BinnedRenderPhaseType::MultidrawableMesh => { + match self.multidrawable_mesh_values.entry(batch_set_key.clone()) { + Entry::Occupied(mut entry) => { + entry + .get_mut() + .entry(bin_key) + .or_default() + .entities + .push((entity, main_entity)); + } + Entry::Vacant(entry) => { + self.multidrawable_mesh_keys.push(batch_set_key); + let mut new_batch_set = HashMap::default(); + new_batch_set.insert( + bin_key, + RenderBin { + entities: vec![(entity, main_entity)], + }, + ); + entry.insert(new_batch_set); + } + } + } + BinnedRenderPhaseType::BatchableMesh => { - match self.batchable_mesh_values.entry(key.clone()) { - Entry::Occupied(mut entry) => entry.get_mut().push(entity), + match self + .batchable_mesh_values + .entry((batch_set_key.clone(), bin_key.clone()).clone()) + { + Entry::Occupied(mut entry) => { + entry.get_mut().entities.push((entity, main_entity)); + } Entry::Vacant(entry) => { - self.batchable_mesh_keys.push(key); - entry.insert(vec![entity]); + self.batchable_mesh_keys.push((batch_set_key, bin_key)); + entry.insert(RenderBin { + entities: vec![(entity, main_entity)], + }); } } } BinnedRenderPhaseType::UnbatchableMesh => { - match self.unbatchable_mesh_values.entry(key.clone()) { - Entry::Occupied(mut entry) => entry.get_mut().entities.push(entity), + match self + .unbatchable_mesh_values + .entry((batch_set_key.clone(), bin_key.clone())) + { + Entry::Occupied(mut entry) => { + entry.get_mut().entities.push((entity, main_entity)); + } Entry::Vacant(entry) => { - self.unbatchable_mesh_keys.push(key); + self.unbatchable_mesh_keys.push((batch_set_key, bin_key)); entry.insert(UnbatchableBinnedEntities { - entities: vec![entity], + entities: vec![(entity, main_entity)], buffer_indices: default(), }); } @@ -333,7 +421,8 @@ where BinnedRenderPhaseType::NonMesh => { // We don't process these items further. - self.non_mesh_items.push((key, entity)); + self.non_mesh_items + .push((batch_set_key, bin_key, (entity, main_entity))); } } } @@ -370,14 +459,22 @@ where let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); + let render_device = world.resource::(); + let multi_draw_indirect_count_supported = render_device + .features() + .contains(Features::MULTI_DRAW_INDIRECT_COUNT); + match self.batch_sets { BinnedRenderPhaseBatchSets::DynamicUniforms(ref batch_sets) => { debug_assert_eq!(self.batchable_mesh_keys.len(), batch_sets.len()); - for (key, batch_set) in self.batchable_mesh_keys.iter().zip(batch_sets.iter()) { + for ((batch_set_key, bin_key), batch_set) in + self.batchable_mesh_keys.iter().zip(batch_sets.iter()) + { for batch in batch_set { let binned_phase_item = BPI::new( - key.clone(), + batch_set_key.clone(), + bin_key.clone(), batch.representative_entity, batch.instance_range.clone(), batch.extra_index.clone(), @@ -396,9 +493,12 @@ where } BinnedRenderPhaseBatchSets::Direct(ref batch_set) => { - for (batch, key) in batch_set.iter().zip(self.batchable_mesh_keys.iter()) { + for (batch, (batch_set_key, bin_key)) in + batch_set.iter().zip(self.batchable_mesh_keys.iter()) + { let binned_phase_item = BPI::new( - key.clone(), + batch_set_key.clone(), + bin_key.clone(), batch.representative_entity, batch.instance_range.clone(), batch.extra_index.clone(), @@ -416,17 +516,29 @@ where } BinnedRenderPhaseBatchSets::MultidrawIndirect(ref batch_sets) => { - let mut batchable_mesh_key_index = 0; - for batch_set in batch_sets.iter() { - let Some(batch) = batch_set.first() else { + for (batch_set_key, batch_set) in self + .multidrawable_mesh_keys + .iter() + .chain( + self.batchable_mesh_keys + .iter() + .map(|(batch_set_key, _)| batch_set_key), + ) + .zip(batch_sets.iter()) + { + let Some(batch) = batch_set.batches.first() else { continue; }; - let key = &self.batchable_mesh_keys[batchable_mesh_key_index]; - batchable_mesh_key_index += batch_set.len(); + let batch_set_index = if multi_draw_indirect_count_supported { + NonMaxU32::new(batch_set.index) + } else { + None + }; let binned_phase_item = BPI::new( - key.clone(), + batch_set_key.clone(), + batch_set.bin_key.clone(), batch.representative_entity, batch.instance_range.clone(), match batch.extra_index { @@ -434,10 +546,12 @@ where PhaseItemExtraIndex::DynamicOffset(ref dynamic_offset) => { PhaseItemExtraIndex::DynamicOffset(*dynamic_offset) } - PhaseItemExtraIndex::IndirectParametersIndex(ref range) => { - PhaseItemExtraIndex::IndirectParametersIndex( - range.start..(range.start + batch_set.len() as u32), - ) + PhaseItemExtraIndex::IndirectParametersIndex { ref range, .. } => { + PhaseItemExtraIndex::IndirectParametersIndex { + range: range.start + ..(range.start + batch_set.batches.len() as u32), + batch_set_index, + } } }, ); @@ -467,8 +581,9 @@ where let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); - for key in &self.unbatchable_mesh_keys { - let unbatchable_entities = &self.unbatchable_mesh_values[key]; + for (batch_set_key, bin_key) in &self.unbatchable_mesh_keys { + let unbatchable_entities = + &self.unbatchable_mesh_values[&(batch_set_key.clone(), bin_key.clone())]; for (entity_index, &entity) in unbatchable_entities.entities.iter().enumerate() { let unbatchable_dynamic_offset = match &unbatchable_entities.buffer_indices { UnbatchableBinnedEntityIndexSet::NoEntities => { @@ -486,10 +601,11 @@ where let first_indirect_parameters_index_for_entity = u32::from(*first_indirect_parameters_index) + entity_index as u32; - PhaseItemExtraIndex::IndirectParametersIndex( - first_indirect_parameters_index_for_entity + PhaseItemExtraIndex::IndirectParametersIndex { + range: first_indirect_parameters_index_for_entity ..(first_indirect_parameters_index_for_entity + 1), - ) + batch_set_index: None, + } } }, }, @@ -499,7 +615,8 @@ where }; let binned_phase_item = BPI::new( - key.clone(), + batch_set_key.clone(), + bin_key.clone(), entity, unbatchable_dynamic_offset.instance_index ..(unbatchable_dynamic_offset.instance_index + 1), @@ -530,10 +647,16 @@ where let draw_functions = world.resource::>(); let mut draw_functions = draw_functions.write(); - for &(ref key, entity) in &self.non_mesh_items { + for &(ref batch_set_key, ref bin_key, entity) in &self.non_mesh_items { // Come up with a fake batch range and extra index. The draw // function is expected to manage any sort of batching logic itself. - let binned_phase_item = BPI::new(key.clone(), entity, 0..1, PhaseItemExtraIndex::None); + let binned_phase_item = BPI::new( + batch_set_key.clone(), + bin_key.clone(), + entity, + 0..1, + PhaseItemExtraIndex::None, + ); let Some(draw_function) = draw_functions.get_mut(binned_phase_item.draw_function()) else { @@ -547,12 +670,15 @@ where } pub fn is_empty(&self) -> bool { - self.batchable_mesh_keys.is_empty() + self.multidrawable_mesh_keys.is_empty() + && self.batchable_mesh_keys.is_empty() && self.unbatchable_mesh_keys.is_empty() && self.non_mesh_items.is_empty() } pub fn clear(&mut self) { + self.multidrawable_mesh_keys.clear(); + self.multidrawable_mesh_values.clear(); self.batchable_mesh_keys.clear(); self.batchable_mesh_values.clear(); self.unbatchable_mesh_keys.clear(); @@ -568,6 +694,8 @@ where { fn new(gpu_preprocessing: GpuPreprocessingMode) -> Self { Self { + multidrawable_mesh_keys: vec![], + multidrawable_mesh_values: HashMap::default(), batchable_mesh_keys: vec![], batchable_mesh_values: HashMap::default(), unbatchable_mesh_keys: vec![], @@ -614,10 +742,11 @@ impl UnbatchableBinnedEntityIndexSet { u32::from(*first_indirect_parameters_index) + entity_index; Some(UnbatchableBinnedEntityIndices { instance_index: instance_range.start + entity_index, - extra_index: PhaseItemExtraIndex::IndirectParametersIndex( - first_indirect_parameters_index_for_this_batch + extra_index: PhaseItemExtraIndex::IndirectParametersIndex { + range: first_indirect_parameters_index_for_this_batch ..(first_indirect_parameters_index_for_this_batch + 1), - ), + batch_set_index: None, + }, }) } UnbatchableBinnedEntityIndexSet::Dense(ref indices) => { @@ -685,7 +814,7 @@ where /// They're cleared out every frame, but storing them in a resource like this /// allows us to reuse allocations. #[derive(Resource, Deref, DerefMut)] -pub struct ViewSortedRenderPhases(pub EntityHashMap>) +pub struct ViewSortedRenderPhases(pub HashMap>) where SPI: SortedPhaseItem; @@ -702,8 +831,8 @@ impl ViewSortedRenderPhases where SPI: SortedPhaseItem, { - pub fn insert_or_clear(&mut self, entity: Entity) { - match self.entry(entity) { + pub fn insert_or_clear(&mut self, retained_view_entity: RetainedViewEntity) { + match self.entry(retained_view_entity) { Entry::Occupied(mut entry) => entry.get_mut().clear(), Entry::Vacant(entry) => { entry.insert(default()); @@ -779,12 +908,17 @@ impl UnbatchableBinnedEntityIndexSet { first_indirect_parameters_index: None, } } - PhaseItemExtraIndex::IndirectParametersIndex(ref range) => { + PhaseItemExtraIndex::IndirectParametersIndex { + range: ref indirect_parameters_index, + .. + } => { // This is the first entity we've seen, and we have compute // shaders. Initialize the fast path. *self = UnbatchableBinnedEntityIndexSet::Sparse { instance_range: indices.instance_index..indices.instance_index + 1, - first_indirect_parameters_index: NonMaxU32::new(range.start), + first_indirect_parameters_index: NonMaxU32::new( + indirect_parameters_index.start, + ), } } } @@ -798,7 +932,10 @@ impl UnbatchableBinnedEntityIndexSet { && indices.extra_index == PhaseItemExtraIndex::None) || first_indirect_parameters_index.is_some_and( |first_indirect_parameters_index| match indices.extra_index { - PhaseItemExtraIndex::IndirectParametersIndex(ref this_range) => { + PhaseItemExtraIndex::IndirectParametersIndex { + range: ref this_range, + .. + } => { u32::from(first_indirect_parameters_index) + instance_range.end - instance_range.start == this_range.start @@ -1018,7 +1155,22 @@ pub enum PhaseItemExtraIndex { /// An index into the buffer that specifies the indirect parameters for this /// [`PhaseItem`]'s drawcall. This is used when indirect mode is on (as used /// for GPU culling). - IndirectParametersIndex(Range), + IndirectParametersIndex { + /// The range of indirect parameters within the indirect parameters array. + /// + /// If we're using `multi_draw_indirect_count`, this specifies the + /// maximum range of indirect parameters within that array. If batches + /// are ultimately culled out on the GPU, the actual number of draw + /// commands might be lower than the length of this range. + range: Range, + /// If `multi_draw_indirect_count` is in use, and this phase item is + /// part of a batch set, specifies the index of the batch set that this + /// phase item is a part of. + /// + /// If `multi_draw_indirect_count` isn't in use, or this phase item + /// isn't part of a batch set, this is `None`. + batch_set_index: Option, + }, } impl PhaseItemExtraIndex { @@ -1028,9 +1180,11 @@ impl PhaseItemExtraIndex { indirect_parameters_index: Option, ) -> PhaseItemExtraIndex { match indirect_parameters_index { - Some(indirect_parameters_index) => PhaseItemExtraIndex::IndirectParametersIndex( - u32::from(indirect_parameters_index)..(u32::from(indirect_parameters_index) + 1), - ), + Some(indirect_parameters_index) => PhaseItemExtraIndex::IndirectParametersIndex { + range: u32::from(indirect_parameters_index) + ..(u32::from(indirect_parameters_index) + 1), + batch_set_index: None, + }, None => PhaseItemExtraIndex::None, } } @@ -1059,7 +1213,13 @@ pub trait BinnedPhaseItem: PhaseItem { /// lowest variable bind group id such as the material bind group id, and /// its dynamic offsets if any, next bind group and offsets, etc. This /// reduces the need for rebinding between bins and improves performance. - type BinKey: PhaseItemBinKey; + type BinKey: Clone + Send + Sync + PartialEq + Eq + Ord + Hash; + + /// The key used to combine batches into batch sets. + /// + /// A *batch set* is a set of meshes that can potentially be multi-drawn + /// together. + type BatchSetKey: PhaseItemBatchSetKey; /// Creates a new binned phase item from the key and per-entity data. /// @@ -1067,31 +1227,25 @@ pub trait BinnedPhaseItem: PhaseItem { /// before rendering. The resulting phase item isn't stored in any data /// structures, resulting in significant memory savings. fn new( - key: Self::BinKey, + batch_set_key: Self::BatchSetKey, + bin_key: Self::BinKey, representative_entity: (Entity, MainEntity), batch_range: Range, extra_index: PhaseItemExtraIndex, ) -> Self; } -/// A trait that allows fetching the *batch set key* from a bin key. -/// -/// A *batch set* is a set of mesh batches that will be rendered with multi-draw -/// if multi-draw is in use. The *batch set key* is the data that has to be -/// identical between meshes in order to place them in the same batch set. A -/// batch set can therefore span multiple bins. +/// A key used to combine batches into batch sets. /// -/// The batch set key should be at the beginning of the bin key structure so -/// that batches in the same batch set will be adjacent to one another in the -/// sorted list of bins. -pub trait PhaseItemBinKey: Clone + Send + Sync + PartialEq + Eq + Ord + Hash { - type BatchSetKey: Clone + PartialEq; - - /// Returns the batch set key, if applicable. +/// A *batch set* is a set of meshes that can potentially be multi-drawn +/// together. +pub trait PhaseItemBatchSetKey: Clone + Send + Sync + PartialEq + Eq + Ord + Hash { + /// Returns true if this batch set key describes indexed meshes or false if + /// it describes non-indexed meshes. /// - /// If this returns `None`, no batches in this phase item can be grouped - /// together into batch sets. - fn get_batch_set_key(&self) -> Option; + /// Bevy uses this in order to determine which kind of indirect draw + /// parameters to use, if indirect drawing is enabled. + fn indexed(&self) -> bool; } /// Represents phase items that must be sorted. The `SortKey` specifies the @@ -1112,7 +1266,7 @@ pub trait SortedPhaseItem: PhaseItem { /// Sorts a slice of phase items into render order. Generally if the same type /// is batched this should use a stable sort like [`slice::sort_by_key`]. /// In almost all other cases, this should not be altered from the default, - /// which uses a unstable sort, as this provides the best balance of CPU and GPU + /// which uses an unstable sort, as this provides the best balance of CPU and GPU /// performance. /// /// Implementers can optionally not sort the list at all. This is generally advisable if and @@ -1125,6 +1279,17 @@ pub trait SortedPhaseItem: PhaseItem { fn sort(items: &mut [Self]) { items.sort_unstable_by_key(Self::sort_key); } + + /// Whether this phase item targets indexed meshes (those with both vertex + /// and index buffers as opposed to just vertex buffers). + /// + /// Bevy needs this information in order to properly group phase items + /// together for multi-draw indirect, because the GPU layout of indirect + /// commands differs between indexed and non-indexed meshes. + /// + /// If you're implementing a custom phase item that doesn't describe a mesh, + /// you can safely return false here. + fn indexed(&self) -> bool; } /// A [`PhaseItem`] item, that automatically sets the appropriate render pipeline, @@ -1176,13 +1341,14 @@ where } impl BinnedRenderPhaseType { - /// Creates the appropriate [`BinnedRenderPhaseType`] for a mesh, given its - /// batchability. - pub fn mesh(batchable: bool) -> BinnedRenderPhaseType { - if batchable { - BinnedRenderPhaseType::BatchableMesh - } else { - BinnedRenderPhaseType::UnbatchableMesh + pub fn mesh( + batchable: bool, + gpu_preprocessing_support: &GpuPreprocessingSupport, + ) -> BinnedRenderPhaseType { + match (batchable, gpu_preprocessing_support.max_supported_mode) { + (true, GpuPreprocessingMode::Culling) => BinnedRenderPhaseType::MultidrawableMesh, + (true, _) => BinnedRenderPhaseType::BatchableMesh, + (false, _) => BinnedRenderPhaseType::UnbatchableMesh, } } } diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 8d0ed47f8395f..2fe2401c56411 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -342,6 +342,15 @@ pub trait AsBindGroup { None } + /// True if the hardware *actually* supports bindless textures for this + /// type, taking the device and driver capabilities into account. + /// + /// If this type doesn't use bindless textures, then the return value from + /// this function is meaningless. + fn bindless_supported(_: &RenderDevice) -> bool { + true + } + /// label fn label() -> Option<&'static str> { None @@ -405,7 +414,7 @@ pub trait AsBindGroup { ) } - /// Returns a vec of bind group layout entries + /// Returns a vec of bind group layout entries. /// /// Set `force_no_bindless` to true to require that bindless textures *not* /// be used. `ExtendedMaterial` uses this in order to ensure that the base 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 3a811a5dbe41b..be03306c1afc7 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 @@ -222,7 +222,7 @@ impl IntoBindGroupLayoutEntryBuilder for BindingType { impl IntoBindGroupLayoutEntryBuilder for BindGroupLayoutEntry { fn into_bind_group_layout_entry_builder(self) -> BindGroupLayoutEntryBuilder { if self.binding != u32::MAX { - bevy_utils::tracing::warn!("The BindGroupLayoutEntries api ignores the binding index when converting a raw wgpu::BindGroupLayoutEntry. You can ignore this warning by setting it to u32::MAX."); + tracing::warn!("The BindGroupLayoutEntries api ignores the binding index when converting a raw wgpu::BindGroupLayoutEntry. You can ignore this warning by setting it to u32::MAX."); } BindGroupLayoutEntryBuilder { ty: self.ty, diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index 3a04e5c45be1d..8191671c15bb8 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -103,6 +103,20 @@ impl RawBufferVec { self.values.append(&mut other.values); } + /// Sets the value at the given index. + /// + /// The index must be less than [`RawBufferVec::len`]. + pub fn set(&mut self, index: u32, value: T) { + self.values[index as usize] = value; + } + + /// Preallocates space for `count` elements in the internal CPU-side buffer. + /// + /// Unlike [`RawBufferVec::reserve`], this doesn't have any effect on the GPU buffer. + pub fn reserve_internal(&mut self, count: usize) { + self.values.reserve(count); + } + /// Changes the debugging label of the buffer. /// /// The next time the buffer is updated (via [`reserve`](Self::reserve)), Bevy will inform diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 0e041968de1cc..29cca0aba01f6 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -11,16 +11,12 @@ use bevy_ecs::{ system::{Res, ResMut, Resource}, }; use bevy_tasks::Task; -use bevy_utils::{ - default, - hashbrown::hash_map::EntryRef, - tracing::{debug, error}, - HashMap, HashSet, -}; +use bevy_utils::{default, hashbrown::hash_map::EntryRef, HashMap, HashSet}; use core::{future::Future, hash::Hash, mem, ops::Deref}; use naga::valid::Capabilities; use std::sync::{Mutex, PoisonError}; use thiserror::Error; +use tracing::{debug, error}; #[cfg(feature = "shader_format_spirv")] use wgpu::util::make_spirv; use wgpu::{ @@ -30,7 +26,7 @@ use wgpu::{ /// A descriptor for a [`Pipeline`]. /// -/// Used to store an heterogenous collection of render and compute pipeline descriptors together. +/// Used to store a heterogenous collection of render and compute pipeline descriptors together. #[derive(Debug)] pub enum PipelineDescriptor { RenderPipelineDescriptor(Box), @@ -39,7 +35,7 @@ pub enum PipelineDescriptor { /// A pipeline defining the data layout and shader logic for a specific GPU task. /// -/// Used to store an heterogenous collection of render and compute pipelines together. +/// Used to store a heterogenous collection of render and compute pipelines together. #[derive(Debug)] pub enum Pipeline { RenderPipeline(RenderPipeline), @@ -214,7 +210,6 @@ impl ShaderCache { Ok(()) } - #[allow(clippy::result_large_err)] fn get( &mut self, render_device: &RenderDevice, @@ -264,7 +259,7 @@ impl ShaderCache { )); debug!( - "processing shader {:?}, with shader defs {:?}", + "processing shader {}, with shader defs {:?}", id, shader_defs ); let shader_source = match &shader.source { @@ -329,7 +324,7 @@ impl ShaderCache { // So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms, // and on wasm the error will be handled by wgpu and crash the application. if let Some(Some(wgpu::Error::Validation { description, .. })) = - bevy_utils::futures::now_or_never(error) + bevy_tasks::futures::now_or_never(error) { return Err(PipelineCacheError::CreateShaderModule(description)); } @@ -874,7 +869,7 @@ impl PipelineCache { } CachedPipelineState::Creating(ref mut task) => { - match bevy_utils::futures::check_ready(task) { + match bevy_tasks::futures::check_ready(task) { Some(Ok(pipeline)) => { cached_pipeline.state = CachedPipelineState::Ok(pipeline); return; @@ -921,7 +916,10 @@ impl PipelineCache { mut events: Extract>>, ) { for event in events.read() { - #[allow(clippy::match_same_arms)] + #[expect( + clippy::match_same_arms, + reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." + )] match event { // PERF: Instead of blocking waiting for the shader cache lock, try again next frame if the lock is currently held AssetEvent::Added { id } | AssetEvent::Modified { id } => { diff --git a/crates/bevy_render/src/render_resource/pipeline_specializer.rs b/crates/bevy_render/src/render_resource/pipeline_specializer.rs index 3ee7a78ed7793..7801381acb59a 100644 --- a/crates/bevy_render/src/render_resource/pipeline_specializer.rs +++ b/crates/bevy_render/src/render_resource/pipeline_specializer.rs @@ -9,11 +9,11 @@ use bevy_ecs::system::Resource; use bevy_utils::{ default, hashbrown::hash_map::{RawEntryMut, VacantEntry}, - tracing::error, Entry, FixedHasher, HashMap, }; use core::{fmt::Debug, hash::Hash}; use thiserror::Error; +use tracing::error; pub trait SpecializedRenderPipeline { type Key: Clone + Hash + PartialEq + Eq; diff --git a/crates/bevy_render/src/render_resource/resource_macros.rs b/crates/bevy_render/src/render_resource/resource_macros.rs index 86a0bf285f31a..6cdf3b69794f5 100644 --- a/crates/bevy_render/src/render_resource/resource_macros.rs +++ b/crates/bevy_render/src/render_resource/resource_macros.rs @@ -4,9 +4,11 @@ macro_rules! define_atomic_id { #[derive(Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] pub struct $atomic_id_type(core::num::NonZero); - // We use new instead of default to indicate that each ID created will be unique. - #[allow(clippy::new_without_default)] impl $atomic_id_type { + #[expect( + clippy::new_without_default, + reason = "Implementing the `Default` trait on atomic IDs would imply that two `::default()` equal each other. By only implementing `new()`, we indicate that each atomic ID created will be unique." + )] pub fn new() -> Self { use core::sync::atomic::{AtomicU32, Ordering}; diff --git a/crates/bevy_render/src/render_resource/storage_buffer.rs b/crates/bevy_render/src/render_resource/storage_buffer.rs index c559712a76389..d7ce706cb0348 100644 --- a/crates/bevy_render/src/render_resource/storage_buffer.rs +++ b/crates/bevy_render/src/render_resource/storage_buffer.rs @@ -156,6 +156,8 @@ impl<'a, T: ShaderType + WriteInto> IntoBinding<'a> for &'a StorageBuffer { /// Stores data to be transferred to the GPU and made accessible to shaders as a dynamic storage buffer. /// +/// This is just a [`StorageBuffer`], but also allows you to set dynamic offsets. +/// /// Dynamic storage buffers can be made available to shaders in some combination of read/write mode, and can store large amounts /// of data. Note however that WebGL2 does not support storage buffers, so consider alternative options in this case. Dynamic /// storage buffers support multiple separate bindings at dynamic byte offsets and so have a diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index 15b3240638e7c..6433a39da1c1d 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -1,7 +1,7 @@ use bevy_ecs::{prelude::Entity, world::World}; -#[cfg(feature = "trace")] -use bevy_utils::tracing::info_span; use bevy_utils::HashMap; +#[cfg(feature = "trace")] +use tracing::info_span; use alloc::{borrow::Cow, collections::VecDeque}; use smallvec::{smallvec, SmallVec}; @@ -68,6 +68,7 @@ impl RenderGraphRunner { render_device: RenderDevice, mut diagnostics_recorder: Option, queue: &wgpu::Queue, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] adapter: &wgpu::Adapter, world: &World, finalizer: impl FnOnce(&mut wgpu::CommandEncoder), @@ -76,8 +77,12 @@ impl RenderGraphRunner { recorder.begin_frame(); } - let mut render_context = - RenderContext::new(render_device, adapter.get_info(), diagnostics_recorder); + let mut render_context = RenderContext::new( + render_device, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] + adapter.get_info(), + diagnostics_recorder, + ); Self::run_graph(graph, None, &mut render_context, world, &[], None)?; finalizer(render_context.command_encoder()); diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 85213476bf4c4..1cd17e8a02a86 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -2,10 +2,11 @@ mod graph_runner; mod render_device; use bevy_derive::{Deref, DerefMut}; +#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] use bevy_tasks::ComputeTaskPool; -use bevy_utils::tracing::{error, info, info_span, warn}; pub use graph_runner::*; pub use render_device::*; +use tracing::{error, info, info_span, warn}; use crate::{ diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, @@ -35,6 +36,7 @@ pub fn render_system(world: &mut World, state: &mut SystemState(); let render_device = world.resource::(); let render_queue = world.resource::(); + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] let render_adapter = world.resource::(); let res = RenderGraphRunner::run( @@ -42,6 +44,7 @@ pub fn render_system(world: &mut World, state: &mut SystemState { render_device: RenderDevice, command_encoder: Option, command_buffer_queue: Vec>, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] force_serial: bool, diagnostics_recorder: Option>, } @@ -387,6 +391,7 @@ impl<'w> RenderContext<'w> { /// Creates a new [`RenderContext`] from a [`RenderDevice`]. pub fn new( render_device: RenderDevice, + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] adapter_info: AdapterInfo, diagnostics_recorder: Option, ) -> Self { @@ -394,7 +399,10 @@ impl<'w> RenderContext<'w> { #[cfg(target_os = "windows")] let force_serial = adapter_info.driver.contains("AMD") && adapter_info.backend == wgpu::Backend::Vulkan; - #[cfg(not(target_os = "windows"))] + #[cfg(not(any( + target_os = "windows", + all(target_arch = "wasm32", target_feature = "atomics") + )))] let force_serial = { drop(adapter_info); false @@ -404,6 +412,7 @@ impl<'w> RenderContext<'w> { render_device, command_encoder: None, command_buffer_queue: Vec::new(), + #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] force_serial, diagnostics_recorder: diagnostics_recorder.map(Arc::new), } diff --git a/crates/bevy_render/src/renderer/render_device.rs b/crates/bevy_render/src/renderer/render_device.rs index 407d1b361b0e5..827d0d2ca328f 100644 --- a/crates/bevy_render/src/renderer/render_device.rs +++ b/crates/bevy_render/src/renderer/render_device.rs @@ -231,10 +231,16 @@ impl RenderDevice { buffer.map_async(map_mode, callback); } - pub fn align_copy_bytes_per_row(row_bytes: usize) -> usize { + // Rounds up `row_bytes` to be a multiple of [`wgpu::COPY_BYTES_PER_ROW_ALIGNMENT`]. + pub const fn align_copy_bytes_per_row(row_bytes: usize) -> usize { let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; - let padded_bytes_per_row_padding = (align - row_bytes % align) % align; - row_bytes + padded_bytes_per_row_padding + + // If row_bytes is aligned calculate a value just under the next aligned value. + // Otherwise calculate a value greater than the next aligned value. + let over_aligned = row_bytes + align - 1; + + // Round the number *down* to the nearest aligned value. + (over_aligned / align) * align } pub fn get_supported_read_only_binding_type( @@ -248,3 +254,19 @@ impl RenderDevice { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn align_copy_bytes_per_row() { + // Test for https://github.com/bevyengine/bevy/issues/16992 + let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; + + assert_eq!(RenderDevice::align_copy_bytes_per_row(0), 0); + assert_eq!(RenderDevice::align_copy_bytes_per_row(1), align); + assert_eq!(RenderDevice::align_copy_bytes_per_row(align + 1), align * 2); + assert_eq!(RenderDevice::align_copy_bytes_per_row(align), align); + } +} diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index fe8933656a28c..1ceaf81357523 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -86,7 +86,11 @@ impl Default for WgpuSettings { { wgpu::Limits::downlevel_webgl2_defaults() } else { - #[allow(unused_mut)] + #[expect(clippy::allow_attributes, reason = "`unused_mut` is not always linted")] + #[allow( + unused_mut, + reason = "This variable needs to be mutable if the `ci_limits` feature is enabled" + )] let mut limits = wgpu::Limits::default(); #[cfg(feature = "ci_limits")] { diff --git a/crates/bevy_render/src/spatial_bundle.rs b/crates/bevy_render/src/spatial_bundle.rs deleted file mode 100644 index d50bd31dfd3fd..0000000000000 --- a/crates/bevy_render/src/spatial_bundle.rs +++ /dev/null @@ -1,72 +0,0 @@ -#![expect(deprecated)] -use bevy_ecs::prelude::Bundle; -use bevy_transform::prelude::{GlobalTransform, Transform}; - -use crate::view::{InheritedVisibility, ViewVisibility, Visibility}; - -/// A [`Bundle`] that allows the correct positional rendering of an entity. -/// -/// It consists of transform components, -/// controlling position, rotation and scale of the entity, -/// but also visibility components, -/// which determine whether the entity is visible or not. -/// -/// Parent-child hierarchies of entities must contain -/// all the [`Component`]s in this `Bundle` -/// to be rendered correctly. -/// -/// [`Component`]: bevy_ecs::component::Component -#[derive(Bundle, Clone, Debug, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `Transform` and `Visibility` components instead. - Inserting `Transform` will now also insert a `GlobalTransform` automatically. - Inserting 'Visibility' will now also insert `InheritedVisibility` and `ViewVisibility` automatically." -)] -pub struct SpatialBundle { - /// The visibility of the entity. - pub visibility: Visibility, - /// The inherited visibility of the entity. - pub inherited_visibility: InheritedVisibility, - /// The view visibility of the entity. - pub view_visibility: ViewVisibility, - /// The transform of the entity. - pub transform: Transform, - /// The global transform of the entity. - pub global_transform: GlobalTransform, -} - -impl SpatialBundle { - /// Creates a new [`SpatialBundle`] from a [`Transform`]. - /// - /// This initializes [`GlobalTransform`] as identity, and visibility as visible - #[inline] - pub const fn from_transform(transform: Transform) -> Self { - SpatialBundle { - transform, - ..Self::INHERITED_IDENTITY - } - } - - /// A [`SpatialBundle`] with inherited visibility and identity transform. - pub const INHERITED_IDENTITY: Self = SpatialBundle { - visibility: Visibility::Inherited, - inherited_visibility: InheritedVisibility::HIDDEN, - view_visibility: ViewVisibility::HIDDEN, - transform: Transform::IDENTITY, - global_transform: GlobalTransform::IDENTITY, - }; - - /// An invisible [`SpatialBundle`] with identity transform. - pub const HIDDEN_IDENTITY: Self = SpatialBundle { - visibility: Visibility::Hidden, - ..Self::INHERITED_IDENTITY - }; -} - -impl From for SpatialBundle { - #[inline] - fn from(transform: Transform) -> Self { - Self::from_transform(transform) - } -} diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index b3948c5c513ca..15fb582fb7682 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -3,7 +3,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; use bevy_ecs::{ component::Component, - entity::Entity, + entity::{Entity, EntityBorrow, TrustedEntityBorrow}, observer::Trigger, query::With, reflect::ReflectComponent, @@ -140,6 +140,15 @@ impl From for RenderEntity { } } +impl EntityBorrow for RenderEntity { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits. +unsafe impl TrustedEntityBorrow 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. @@ -158,6 +167,15 @@ impl From for MainEntity { } } +impl EntityBorrow for MainEntity { + fn entity(&self) -> Entity { + self.id() + } +} + +// SAFETY: RenderEntity is a newtype around Entity that derives its comparison traits. +unsafe impl TrustedEntityBorrow for MainEntity {} + /// A [`HashMap`](hashbrown::HashMap) pre-configured to use [`EntityHash`] hashing with a [`MainEntity`]. pub type MainEntityHashMap = hashbrown::HashMap; diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 06e2db79f28df..b5ad7a541e297 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -17,6 +17,7 @@ use crate::{ render_phase::ViewRangefinder3d, render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, + sync_world::MainEntity, texture::{ CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment, TextureCache, @@ -184,8 +185,69 @@ impl Msaa { } } +/// An identifier for a view that is stable across frames. +/// +/// We can't use [`Entity`] for this because render world entities aren't +/// stable, and we can't use just [`MainEntity`] because some main world views +/// extract to multiple render world views. For example, a directional light +/// extracts to one render world view per cascade, and a point light extracts to +/// one render world view per cubemap face. So we pair the main entity with an +/// *auxiliary entity* and a *subview index*, which *together* uniquely identify +/// a view in the render world in a way that's stable from frame to frame. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RetainedViewEntity { + /// The main entity that this view corresponds to. + pub main_entity: MainEntity, + + /// Another entity associated with the view entity. + /// + /// This is currently used for shadow cascades. If there are multiple + /// cameras, each camera needs to have its own set of shadow cascades. Thus + /// the light and subview index aren't themselves enough to uniquely + /// identify a shadow cascade: we need the camera that the cascade is + /// associated with as well. This entity stores that camera. + /// + /// If not present, this will be `MainEntity(Entity::PLACEHOLDER)`. + pub auxiliary_entity: MainEntity, + + /// The index of the view corresponding to the entity. + /// + /// For example, for point lights that cast shadows, this is the index of + /// the cubemap face (0 through 5 inclusive). For directional lights, this + /// is the index of the cascade. + pub subview_index: u32, +} + +impl RetainedViewEntity { + /// Creates a new [`RetainedViewEntity`] from the given main world entity, + /// auxiliary main world entity, and subview index. + /// + /// See [`RetainedViewEntity::subview_index`] for an explanation of what + /// `auxiliary_entity` and `subview_index` are. + pub fn new( + main_entity: MainEntity, + auxiliary_entity: Option, + subview_index: u32, + ) -> Self { + Self { + main_entity, + auxiliary_entity: auxiliary_entity.unwrap_or(Entity::PLACEHOLDER.into()), + subview_index, + } + } +} + +/// Describes a camera in the render world. +/// +/// Each entity in the main world can potentially extract to multiple subviews, +/// each of which has a [`RetainedViewEntity::subview_index`]. For instance, 3D +/// cameras extract to both a 3D camera subview with index 0 and a special UI +/// subview with index 1. Likewise, point lights with shadows extract to 6 +/// subviews, one for each side of the shadow cubemap. #[derive(Component)] 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: /// /// Perspective (infinite reverse z) @@ -631,10 +693,10 @@ impl From for ColorGradingUniform { /// /// The vast majority of applications will not need to use this component, as it /// generally reduces rendering performance. -#[derive(Component)] +#[derive(Component, Default)] pub struct NoIndirectDrawing; -#[derive(Component)] +#[derive(Component, Default)] pub struct NoCpuCulling; impl ViewTarget { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 3e991a6cbdde0..b3b7c15bb2687 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - mod range; mod render_layers; @@ -22,7 +20,7 @@ use bevy_utils::{Parallel, TypeIdMap}; use smallvec::SmallVec; use super::NoCpuCulling; -use crate::sync_world::MainEntity; +use crate::{camera::Projection, sync_world::MainEntity}; use crate::{ camera::{Camera, CameraProjection}, mesh::{Mesh, Mesh3d, MeshAabb}, @@ -200,28 +198,6 @@ impl ViewVisibility { } } -/// A [`Bundle`] of the [`Visibility`], [`InheritedVisibility`], and [`ViewVisibility`] -/// [`Component`]s, which describe the visibility of an entity. -/// -/// * To show or hide an entity, you should set its [`Visibility`]. -/// * To get the inherited visibility of an entity, you should get its [`InheritedVisibility`]. -/// * For visibility hierarchies to work correctly, you must have both all of [`Visibility`], [`InheritedVisibility`], and [`ViewVisibility`]. -/// * ~~You may use the [`VisibilityBundle`] to guarantee this.~~ [`VisibilityBundle`] is now deprecated. -/// [`InheritedVisibility`] and [`ViewVisibility`] are automatically inserted whenever [`Visibility`] is inserted. -#[derive(Bundle, Debug, Clone, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `Visibility` component instead. Inserting it will now also insert `InheritedVisibility` and `ViewVisibility` automatically." -)] -pub struct VisibilityBundle { - /// The visibility of the entity. - pub visibility: Visibility, - // The inherited visibility of the entity. - pub inherited_visibility: InheritedVisibility, - // The computed visibility of the entity. - pub view_visibility: ViewVisibility, -} - /// Use this component to opt-out of built-in frustum culling for entities, see /// [`Frustum`]. /// @@ -398,10 +374,10 @@ pub fn calculate_bounds( /// Updates [`Frustum`]. /// /// This system is used in [`CameraProjectionPlugin`](crate::camera::CameraProjectionPlugin). -pub fn update_frusta( +pub fn update_frusta( mut views: Query< - (&GlobalTransform, &T, &mut Frustum), - Or<(Changed, Changed)>, + (&GlobalTransform, &Projection, &mut Frustum), + Or<(Changed, Changed)>, >, ) { for (transform, projection, mut frustum) in &mut views { @@ -412,7 +388,10 @@ pub fn update_frusta( fn visibility_propagate_system( changed: Query< (Entity, &Visibility, Option<&Parent>, Option<&Children>), - (With, Changed), + ( + With, + Or<(Changed, Changed)>, + ), >, mut visibility_query: Query<(&Visibility, &mut InheritedVisibility)>, children_query: Query<&Children, (With, With)>, @@ -424,7 +403,7 @@ fn visibility_propagate_system( // fall back to true if no parent is found or parent lacks components Visibility::Inherited => parent .and_then(|p| visibility_query.get(p.get()).ok()) - .map_or(true, |(_, x)| x.get()), + .is_none_or(|(_, x)| x.get()), }; let (_, mut inherited_visibility) = visibility_query .get_mut(entity) @@ -757,6 +736,58 @@ mod test { ); } + #[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).set_parent(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}; diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 14c0fe45befc7..e6c257e4a6510 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -5,11 +5,7 @@ use crate::{ }; use bevy_app::{App, Plugin}; use bevy_ecs::{entity::EntityHashMap, prelude::*}; -use bevy_utils::{ - default, - tracing::{debug, warn}, - HashSet, -}; +use bevy_utils::{default, HashSet}; use bevy_window::{ CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; @@ -17,6 +13,7 @@ use core::{ num::NonZero, ops::{Deref, DerefMut}, }; +use tracing::{debug, warn}; use wgpu::{ SurfaceConfiguration, SurfaceTargetUnsafe, TextureFormat, TextureUsages, TextureViewDescriptor, }; @@ -216,7 +213,6 @@ impl WindowSurfaces { /// another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and /// [`Backends::GL`](crate::settings::Backends::GL) if your GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or /// later. -#[allow(clippy::too_many_arguments)] pub fn prepare_windows( mut windows: ResMut, mut window_surfaces: ResMut, @@ -269,7 +265,7 @@ pub fn prepare_windows( } #[cfg(target_os = "linux")] Err(wgpu::SurfaceError::Timeout) if may_erroneously_timeout() => { - bevy_utils::tracing::trace!( + tracing::trace!( "Couldn't get swap chain texture. This is probably a quirk \ of your Linux GPU driver, so it can be safely ignored." ); @@ -322,8 +318,8 @@ pub fn create_surfaces( .entry(window.entity) .or_insert_with(|| { let surface_target = SurfaceTargetUnsafe::RawHandle { - raw_display_handle: window.handle.display_handle, - raw_window_handle: window.handle.window_handle, + raw_display_handle: window.handle.get_display_handle(), + raw_window_handle: window.handle.get_window_handle(), }; // SAFETY: The window handles in ExtractedWindows will always be valid objects to create surfaces on let surface = unsafe { diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index aab9b08c680d7..01460efcad59b 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -26,11 +26,7 @@ use bevy_hierarchy::DespawnRecursiveExt; use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_reflect::Reflect; use bevy_tasks::AsyncComputeTaskPool; -use bevy_utils::{ - default, - tracing::{error, info, warn}, - HashSet, -}; +use bevy_utils::{default, HashSet}; use bevy_window::{PrimaryWindow, WindowRef}; use core::ops::Deref; use std::{ @@ -40,6 +36,7 @@ use std::{ Mutex, }, }; +use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; #[derive(Event, Deref, DerefMut, Reflect, Debug)] @@ -74,12 +71,12 @@ pub struct ScreenshotCaptured(pub Image); pub struct Screenshot(pub RenderTarget); /// A marker component that indicates that a screenshot is currently being captured. -#[derive(Component)] +#[derive(Component, Default)] pub struct Capturing; /// A marker component that indicates that a screenshot has been captured, the image is ready, and /// the screenshot entity can be despawned. -#[derive(Component)] +#[derive(Component, Default)] pub struct Captured; impl Screenshot { @@ -239,7 +236,7 @@ fn extract_screenshots( }; if seen_targets.contains(&render_target) { warn!( - "Duplicate render target for screenshot, skipping entity {:?}: {:?}", + "Duplicate render target for screenshot, skipping entity {}: {:?}", entity, render_target ); // If we don't despawn the entity here, it will be captured again in the next frame @@ -254,7 +251,6 @@ fn extract_screenshots( system_state.apply(&mut main_world); } -#[allow(clippy::too_many_arguments)] fn prepare_screenshots( targets: Res, mut prepared: ResMut, @@ -273,7 +269,7 @@ fn prepare_screenshots( NormalizedRenderTarget::Window(window) => { let window = window.entity(); let Some(surface_data) = window_surfaces.surfaces.get(&window) else { - warn!("Unknown window for screenshot, skipping: {:?}", window); + warn!("Unknown window for screenshot, skipping: {}", window); continue; }; let format = surface_data.configuration.format.add_srgb_suffix(); @@ -579,7 +575,6 @@ pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEnc } } -#[allow(clippy::too_many_arguments)] fn render_screenshot( encoder: &mut CommandEncoder, prepared: &RenderScreenshotsPrepared, @@ -628,7 +623,7 @@ fn render_screenshot( pub(crate) fn collect_screenshots(world: &mut World) { #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("collect_screenshots").entered(); + let _span = tracing::info_span!("collect_screenshots").entered(); let sender = world.resource::().deref().clone(); let prepared = world.resource::(); @@ -690,7 +685,7 @@ pub(crate) fn collect_screenshots(world: &mut World) { RenderAssetUsages::RENDER_WORLD, ), )) { - error!("Failed to send screenshot: {:?}", e); + error!("Failed to send screenshot: {}", e); } }; diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 91eecb563b69b..69a288fe5c77b 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_scene" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides scene functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -14,17 +14,17 @@ serialize = ["dep:serde", "uuid/serde", "bevy_ecs/serialize"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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", features = [ "bevy", ] } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev", optional = true } +bevy_hierarchy = { path = "../bevy_hierarchy", 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 } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_scene/src/bundle.rs b/crates/bevy_scene/src/bundle.rs deleted file mode 100644 index 0024b2f77729b..0000000000000 --- a/crates/bevy_scene/src/bundle.rs +++ /dev/null @@ -1,188 +0,0 @@ -#![expect(deprecated)] - -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - bundle::Bundle, - change_detection::ResMut, - entity::Entity, - prelude::{Changed, Component, Without}, - system::{Commands, Query}, -}; -#[cfg(feature = "bevy_render")] -use bevy_render::prelude::{InheritedVisibility, ViewVisibility, Visibility}; -use bevy_transform::components::{GlobalTransform, Transform}; - -use crate::{DynamicSceneRoot, InstanceId, SceneRoot, SceneSpawner}; - -/// [`InstanceId`] of a spawned scene. It can be used with the [`SceneSpawner`] to -/// interact with the spawned scene. -#[derive(Component, Deref, DerefMut)] -pub struct SceneInstance(pub(crate) InstanceId); - -/// A component bundle for a [`Scene`](crate::Scene) root. -/// -/// The scene from `scene` will be spawned as a child of the entity with this component. -/// Once it's spawned, the entity will have a [`SceneInstance`] component. -#[derive(Default, Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `SceneRoot` component instead. Inserting `SceneRoot` will also insert the other components required by scenes automatically." -)] -pub struct SceneBundle { - /// Handle to the scene to spawn. - pub scene: SceneRoot, - /// Transform of the scene root entity. - pub transform: Transform, - /// Global transform of the scene root entity. - pub global_transform: GlobalTransform, - - /// User-driven visibility of the scene root entity. - #[cfg(feature = "bevy_render")] - pub visibility: Visibility, - /// Inherited visibility of the scene root entity. - #[cfg(feature = "bevy_render")] - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed visibility of the scene root entity for rendering. - #[cfg(feature = "bevy_render")] - pub view_visibility: ViewVisibility, -} - -/// A component bundle for a [`DynamicScene`](crate::DynamicScene) root. -/// -/// The dynamic scene from `scene` will be spawn as a child of the entity with this component. -/// Once it's spawned, the entity will have a [`SceneInstance`] component. -#[derive(Default, Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `DynamicSceneRoot` component instead. Inserting `DynamicSceneRoot` will also insert the other components required by scenes automatically." -)] -pub struct DynamicSceneBundle { - /// Handle to the scene to spawn. - pub scene: DynamicSceneRoot, - /// Transform of the scene root entity. - pub transform: Transform, - /// Global transform of the scene root entity. - pub global_transform: GlobalTransform, - - /// User-driven visibility of the scene root entity. - #[cfg(feature = "bevy_render")] - pub visibility: Visibility, - /// Inherited visibility of the scene root entity. - #[cfg(feature = "bevy_render")] - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed visibility of the scene root entity for rendering. - #[cfg(feature = "bevy_render")] - pub view_visibility: ViewVisibility, -} - -/// System that will spawn scenes from the [`SceneRoot`] and [`DynamicSceneRoot`] components. -pub fn scene_spawner( - mut commands: Commands, - mut scene_to_spawn: Query< - (Entity, &SceneRoot, Option<&mut SceneInstance>), - (Changed, Without), - >, - mut dynamic_scene_to_spawn: Query< - (Entity, &DynamicSceneRoot, Option<&mut SceneInstance>), - (Changed, Without), - >, - mut scene_spawner: ResMut, -) { - for (entity, scene, instance) in &mut scene_to_spawn { - let new_instance = scene_spawner.spawn_as_child(scene.0.clone(), entity); - if let Some(mut old_instance) = instance { - scene_spawner.despawn_instance(**old_instance); - *old_instance = SceneInstance(new_instance); - } else { - commands.entity(entity).insert(SceneInstance(new_instance)); - } - } - for (entity, dynamic_scene, instance) in &mut dynamic_scene_to_spawn { - let new_instance = scene_spawner.spawn_dynamic_as_child(dynamic_scene.0.clone(), entity); - if let Some(mut old_instance) = instance { - scene_spawner.despawn_instance(**old_instance); - *old_instance = SceneInstance(new_instance); - } else { - commands.entity(entity).insert(SceneInstance(new_instance)); - } - } -} - -#[cfg(test)] -mod tests { - use crate::{DynamicScene, DynamicSceneRoot, ScenePlugin, SceneSpawner}; - use bevy_app::{App, ScheduleRunnerPlugin}; - use bevy_asset::{AssetPlugin, Assets}; - use bevy_ecs::{ - component::Component, - entity::Entity, - prelude::{AppTypeRegistry, ReflectComponent, World}, - }; - use bevy_hierarchy::{Children, HierarchyPlugin}; - use bevy_reflect::Reflect; - - #[derive(Component, Reflect, Default)] - #[reflect(Component)] - struct ComponentA { - pub x: f32, - pub y: f32, - } - - #[test] - fn spawn_and_delete() { - let mut app = App::new(); - - app.add_plugins(ScheduleRunnerPlugin::default()) - .add_plugins(HierarchyPlugin) - .add_plugins(AssetPlugin::default()) - .add_plugins(ScenePlugin) - .register_type::(); - app.update(); - - let mut scene_world = World::new(); - - // create a new DynamicScene manually - let type_registry = app.world().resource::().clone(); - scene_world.insert_resource(type_registry); - scene_world.spawn(ComponentA { x: 3.0, y: 4.0 }); - let scene = DynamicScene::from_world(&scene_world); - let scene_handle = app - .world_mut() - .resource_mut::>() - .add(scene); - - // spawn the scene as a child of `entity` using `DynamicSceneRoot` - let entity = app - .world_mut() - .spawn(DynamicSceneRoot(scene_handle.clone())) - .id(); - - // run the app's schedule once, so that the scene gets spawned - app.update(); - - // make sure that the scene was added as a child of the root entity - let (scene_entity, scene_component_a) = app - .world_mut() - .query::<(Entity, &ComponentA)>() - .single(app.world()); - assert_eq!(scene_component_a.x, 3.0); - assert_eq!(scene_component_a.y, 4.0); - assert_eq!( - app.world().entity(entity).get::().unwrap().len(), - 1 - ); - - // let's try to delete the scene - let mut scene_spawner = app.world_mut().resource_mut::(); - scene_spawner.despawn(&scene_handle); - - // run the scene spawner system to despawn the scene - app.update(); - - // the scene entity does not exist anymore - assert!(app.world().get_entity(scene_entity).is_err()); - - // the root entity does not have any children anymore - assert!(app.world().entity(entity).get::().is_none()); - } -} diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index babc45f7f119f..f29c925f7e4d4 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -204,9 +204,9 @@ mod tests { }, reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource}, system::Resource, - world::{Command, World}, + world::World, }; - use bevy_hierarchy::{AddChild, Parent}; + use bevy_hierarchy::{BuildChildren, Parent}; use bevy_reflect::Reflect; use crate::dynamic_scene::DynamicScene; @@ -271,11 +271,9 @@ mod tests { .register::(); let original_parent_entity = world.spawn_empty().id(); let original_child_entity = world.spawn_empty().id(); - AddChild { - parent: original_parent_entity, - child: original_child_entity, - } - .apply(&mut world); + world + .entity_mut(original_parent_entity) + .add_child(original_child_entity); // We then write this relationship to a new scene, and then write that scene back to the // world to create another parent and child relationship @@ -292,11 +290,9 @@ mod tests { // We then add the parent from the scene as a child of the original child // Hierarchy should look like: // Original Parent <- Original Child <- Scene Parent <- Scene Child - AddChild { - parent: original_child_entity, - child: from_scene_parent_entity, - } - .apply(&mut world); + world + .entity_mut(original_child_entity) + .add_child(from_scene_parent_entity); // We then reload the scene to make sure that from_scene_parent_entity's parent component // isn't updated with the entity map, since this component isn't defined in the scene. diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 8a21b2040d78e..76673219c5e7d 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -13,7 +13,6 @@ extern crate alloc; -mod bundle; mod components; mod dynamic_scene; mod dynamic_scene_builder; @@ -29,7 +28,6 @@ pub mod serde; pub use bevy_asset::ron; use bevy_ecs::schedule::IntoSystemConfigs; -pub use bundle::*; pub use components::*; pub use dynamic_scene::*; pub use dynamic_scene_builder::*; @@ -41,12 +39,11 @@ pub use scene_spawner::*; /// The scene prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ - DynamicScene, DynamicSceneBuilder, DynamicSceneBundle, DynamicSceneRoot, Scene, - SceneBundle, SceneFilter, SceneRoot, SceneSpawner, + DynamicScene, DynamicSceneBuilder, DynamicSceneRoot, Scene, SceneFilter, SceneRoot, + SceneSpawner, }; } diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 32d829b8c726a..0f47710446141 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -5,14 +5,21 @@ use bevy_ecs::{ event::{Event, EventCursor, Events}, reflect::AppTypeRegistry, system::Resource, - world::{Command, Mut, World}, + world::{Mut, World}, }; -use bevy_hierarchy::{AddChild, BuildChildren, DespawnRecursiveExt, Parent}; +use bevy_hierarchy::{BuildChildren, DespawnRecursiveExt, Parent}; use bevy_reflect::Reflect; use bevy_utils::{HashMap, HashSet}; use thiserror::Error; use uuid::Uuid; +use crate::{DynamicSceneRoot, SceneRoot}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + change_detection::ResMut, + prelude::{Changed, Component, Without}, + system::{Commands, Query}, +}; /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// /// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. @@ -375,18 +382,14 @@ impl SceneSpawner { // the scene parent if !world .get_entity(entity) + .ok() // This will filter only the scene root entity, as all other from the // scene have a parent - .map(|entity| entity.contains::()) - // Default is true so that it won't run on an entity that wouldn't exist anymore + // Entities that wouldn't exist anymore are also skipped // this case shouldn't happen anyway - .unwrap_or(true) + .is_none_or(|entity| entity.contains::()) { - AddChild { - parent, - child: entity, - } - .apply(world); + world.entity_mut(parent).add_child(entity); } } @@ -400,7 +403,7 @@ impl SceneSpawner { } } - /// Check that an scene instance spawned previously is ready to use + /// Check that a scene instance spawned previously is ready to use pub fn instance_is_ready(&self, instance_id: InstanceId) -> bool { self.spawned_instances.contains_key(&instance_id) } @@ -472,6 +475,44 @@ pub fn scene_spawner_system(world: &mut World) { }); } +/// [`InstanceId`] of a spawned scene. It can be used with the [`SceneSpawner`] to +/// interact with the spawned scene. +#[derive(Component, Deref, DerefMut)] +pub struct SceneInstance(pub(crate) InstanceId); + +/// System that will spawn scenes from the [`SceneRoot`] and [`DynamicSceneRoot`] components. +pub fn scene_spawner( + mut commands: Commands, + mut scene_to_spawn: Query< + (Entity, &SceneRoot, Option<&mut SceneInstance>), + (Changed, Without), + >, + mut dynamic_scene_to_spawn: Query< + (Entity, &DynamicSceneRoot, Option<&mut SceneInstance>), + (Changed, Without), + >, + mut scene_spawner: ResMut, +) { + for (entity, scene, instance) in &mut scene_to_spawn { + let new_instance = scene_spawner.spawn_as_child(scene.0.clone(), entity); + if let Some(mut old_instance) = instance { + scene_spawner.despawn_instance(**old_instance); + *old_instance = SceneInstance(new_instance); + } else { + commands.entity(entity).insert(SceneInstance(new_instance)); + } + } + for (entity, dynamic_scene, instance) in &mut dynamic_scene_to_spawn { + let new_instance = scene_spawner.spawn_dynamic_as_child(dynamic_scene.0.clone(), entity); + if let Some(mut old_instance) = instance { + scene_spawner.despawn_instance(**old_instance); + *old_instance = SceneInstance(new_instance); + } else { + commands.entity(entity).insert(SceneInstance(new_instance)); + } + } +} + #[cfg(test)] mod tests { use bevy_app::App; @@ -488,6 +529,79 @@ mod tests { use crate::{DynamicSceneBuilder, DynamicSceneRoot, ScenePlugin}; use super::*; + use crate::{DynamicScene, SceneSpawner}; + use bevy_app::ScheduleRunnerPlugin; + use bevy_asset::Assets; + use bevy_ecs::{ + entity::Entity, + prelude::{AppTypeRegistry, World}, + }; + use bevy_hierarchy::{Children, HierarchyPlugin}; + + #[derive(Component, Reflect, Default)] + #[reflect(Component)] + struct ComponentA { + pub x: f32, + pub y: f32, + } + + #[test] + fn spawn_and_delete() { + let mut app = App::new(); + + app.add_plugins(ScheduleRunnerPlugin::default()) + .add_plugins(HierarchyPlugin) + .add_plugins(AssetPlugin::default()) + .add_plugins(ScenePlugin) + .register_type::(); + app.update(); + + let mut scene_world = World::new(); + + // create a new DynamicScene manually + let type_registry = app.world().resource::().clone(); + scene_world.insert_resource(type_registry); + scene_world.spawn(ComponentA { x: 3.0, y: 4.0 }); + let scene = DynamicScene::from_world(&scene_world); + let scene_handle = app + .world_mut() + .resource_mut::>() + .add(scene); + + // spawn the scene as a child of `entity` using `DynamicSceneRoot` + let entity = app + .world_mut() + .spawn(DynamicSceneRoot(scene_handle.clone())) + .id(); + + // run the app's schedule once, so that the scene gets spawned + app.update(); + + // make sure that the scene was added as a child of the root entity + let (scene_entity, scene_component_a) = app + .world_mut() + .query::<(Entity, &ComponentA)>() + .single(app.world()); + assert_eq!(scene_component_a.x, 3.0); + assert_eq!(scene_component_a.y, 4.0); + assert_eq!( + app.world().entity(entity).get::().unwrap().len(), + 1 + ); + + // let's try to delete the scene + let mut scene_spawner = app.world_mut().resource_mut::(); + scene_spawner.despawn(&scene_handle); + + // run the scene spawner system to despawn the scene + app.update(); + + // the scene entity does not exist anymore + assert!(app.world().get_entity(scene_entity).is_err()); + + // the root entity does not have any children anymore + assert!(app.world().entity(entity).get::().is_none()); + } #[derive(Reflect, Component, Debug, PartialEq, Eq, Clone, Copy, Default)] #[reflect(Component)] @@ -542,7 +656,7 @@ mod tests { #[derive(Component, Reflect, Default)] #[reflect(Component)] - struct ComponentA; + struct ComponentF; #[derive(Resource, Default)] struct TriggerCount(u32); @@ -552,9 +666,9 @@ mod tests { app.add_plugins((AssetPlugin::default(), ScenePlugin)); app.init_resource::(); - app.register_type::(); - app.world_mut().spawn(ComponentA); - app.world_mut().spawn(ComponentA); + app.register_type::(); + app.world_mut().spawn(ComponentF); + app.world_mut().spawn(ComponentF); app } @@ -708,7 +822,7 @@ mod tests { fn despawn_scene() { let mut app = App::new(); app.add_plugins((AssetPlugin::default(), ScenePlugin)); - app.register_type::(); + app.register_type::(); let asset_server = app.world().resource::(); @@ -729,7 +843,7 @@ mod tests { // Spawn scene. for _ in 0..count { app.world_mut() - .spawn((ComponentA, DynamicSceneRoot(scene.clone()))); + .spawn((ComponentF, DynamicSceneRoot(scene.clone()))); } app.update(); @@ -738,7 +852,7 @@ mod tests { // Despawn scene. app.world_mut() .run_system_once( - |mut commands: Commands, query: Query>| { + |mut commands: Commands, query: Query>| { for entity in query.iter() { commands.entity(entity).despawn_recursive(); } diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 0c74b8e7ee894..a59fa67692ca1 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -180,7 +180,7 @@ impl<'a> Serialize for SceneMapSerializer<'a> { ) }) .collect::>(); - entries.sort_by_key(|(type_path, _partial_reflect)| *type_path); + entries.sort_by_key(|(type_path, _)| *type_path); entries }; @@ -935,7 +935,7 @@ mod tests { .entities .iter() .find(|dynamic_entity| dynamic_entity.entity == expected.entity) - .unwrap_or_else(|| panic!("missing entity (expected: `{:?}`)", expected.entity)); + .unwrap_or_else(|| panic!("missing entity (expected: `{}`)", expected.entity)); assert_eq!(expected.entity, received.entity, "entities did not match"); diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 8800f6cb6e115..fc4316317013d 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_sprite" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides sprite functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -10,40 +10,36 @@ keywords = ["bevy"] [features] bevy_sprite_picking_backend = ["bevy_picking", "bevy_window"] -serialize = ["dep:serde"] webgl = [] webgpu = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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", features = [ "bevy", ] } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true } -bevy_derive = { path = "../bevy_derive", version = "0.15.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" } # other bytemuck = { version = "1", features = ["derive", "must_cast"] } fixedbitset = "0.5" -guillotiere = "0.6.0" -thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } -rectangle-pack = "0.4" bitflags = "2.3" radsort = "0.1" nonmax = "0.5" -serde = { version = "1", features = ["derive"], optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs deleted file mode 100644 index fdc2f8ed515d1..0000000000000 --- a/crates/bevy_sprite/src/bundle.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![expect(deprecated)] -use crate::Sprite; -use bevy_ecs::bundle::Bundle; -use bevy_render::{ - sync_world::SyncToRenderWorld, - view::{InheritedVisibility, ViewVisibility, Visibility}, -}; -use bevy_transform::components::{GlobalTransform, Transform}; - -/// A [`Bundle`] of components for drawing a single sprite from an image. -#[derive(Bundle, Clone, Debug, Default)] -#[deprecated( - since = "0.15.0", - note = "Use the `Sprite` component instead. Inserting it will now also insert `Transform` and `Visibility` automatically." -)] -pub struct SpriteBundle { - /// Specifies the rendering properties of the sprite, such as color tint and flip. - pub sprite: Sprite, - /// The local transform of the sprite, relative to its parent. - pub transform: Transform, - /// The absolute transform of the sprite. This should generally not be written to directly. - pub global_transform: GlobalTransform, - /// User indication of whether an entity is visible - pub visibility: Visibility, - /// Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering - pub view_visibility: ViewVisibility, - /// Marker component that indicates that its entity needs to be synchronized to the render world - pub sync: SyncToRenderWorld, -} diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 39204eb4cc9e0..2ab05ea14679d 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -10,56 +10,43 @@ extern crate alloc; -mod bundle; -mod dynamic_texture_atlas_builder; mod mesh2d; #[cfg(feature = "bevy_sprite_picking_backend")] mod picking_backend; mod render; mod sprite; -mod texture_atlas; -mod texture_atlas_builder; mod texture_slice; /// The sprite prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. -#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ - bundle::SpriteBundle, sprite::{Sprite, SpriteImageMode}, - texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder, + ColorMaterial, MeshMaterial2d, }; } -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -pub use bundle::*; -pub use dynamic_texture_atlas_builder::*; pub use mesh2d::*; #[cfg(feature = "bevy_sprite_picking_backend")] pub use picking_backend::*; pub use render::*; pub use sprite::*; -pub use texture_atlas::*; -pub use texture_atlas_builder::*; pub use texture_slice::*; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle}; +use bevy_asset::{load_internal_asset, Assets, Handle}; use bevy_core_pipeline::core_2d::Transparent2d; -use bevy_ecs::{prelude::*, query::QueryItem}; -use bevy_image::Image; +use bevy_ecs::prelude::*; +use bevy_image::{prelude::*, TextureAtlasPlugin}; use bevy_render::{ - extract_component::{ExtractComponent, ExtractComponentPlugin}, mesh::{Mesh, Mesh2d, MeshAabb}, primitives::Aabb, render_phase::AddRenderCommand, render_resource::{Shader, SpecializedRenderPipelines}, - view::{self, NoFrustumCulling, VisibilityClass, VisibilitySystems}, + view::{NoFrustumCulling, VisibilitySystems}, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -70,6 +57,14 @@ pub struct SpritePlugin { pub add_picking: bool, } +#[expect( + clippy::allow_attributes, + reason = "clippy::derivable_impls is not always linted" +)] +#[allow( + clippy::derivable_impls, + reason = "Known false positive with clippy: " +)] impl Default for SpritePlugin { fn default() -> Self { Self { @@ -90,16 +85,6 @@ pub enum SpriteSystem { ComputeSlices, } -/// A component that marks entities that aren't themselves sprites but become -/// sprites during rendering. -/// -/// Right now, this is used for `Text`. -#[derive(Component, Reflect, Clone, Copy, Debug, Default)] -#[reflect(Component, Default, Debug)] -#[require(VisibilityClass)] -#[component(on_add = view::add_visibility_class::)] -pub struct SpriteSource; - impl Plugin for SpritePlugin { fn build(&self, app: &mut App) { load_internal_asset!( @@ -114,20 +99,17 @@ impl Plugin for SpritePlugin { "render/sprite_view_bindings.wgsl", Shader::from_wgsl ); - app.init_asset::() - .register_asset_reflect::() - .register_type::() + + if !app.is_plugin_added::() { + app.add_plugins(TextureAtlasPlugin); + } + + app.register_type::() .register_type::() .register_type::() .register_type::() - .register_type::() .register_type::() - .register_type::() - .add_plugins(( - Mesh2dRenderPlugin, - ColorMaterialPlugin, - ExtractComponentPlugin::::default(), - )) + .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) .add_systems( PostUpdate, ( @@ -229,18 +211,6 @@ pub fn calculate_bounds_2d( } } -impl ExtractComponent for SpriteSource { - type QueryData = (); - - type QueryFilter = With; - - type Out = SpriteSource; - - fn extract_component(_: QueryItem<'_, Self::QueryData>) -> Option { - Some(SpriteSource) - } -} - #[cfg(test)] mod test { diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 8c3267c40ba4d..e844fc3a997a8 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,6 +1,4 @@ -#![expect(deprecated)] - -use crate::{AlphaMode2d, Material2d, Material2dPlugin, MaterialMesh2dBundle}; +use crate::{AlphaMode2d, Material2d, Material2dPlugin}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; @@ -157,10 +155,3 @@ impl Material2d for ColorMaterial { self.alpha_mode } } - -/// A component bundle for entities with a [`Mesh2d`](crate::Mesh2d) and a [`ColorMaterial`]. -#[deprecated( - since = "0.15.0", - note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub type ColorMesh2dBundle = MaterialMesh2dBundle; diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index fcc830301c554..a4dfc376682c4 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,5 +1,3 @@ -#![expect(deprecated)] - use crate::{ DrawMesh2d, Mesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, SetMesh2dBindGroup, SetMesh2dViewBindGroup, @@ -7,7 +5,9 @@ use crate::{ use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ - core_2d::{AlphaMask2d, AlphaMask2dBinKey, Opaque2d, Opaque2dBinKey, Transparent2d}, + core_2d::{ + AlphaMask2d, AlphaMask2dBinKey, BatchSetKey2d, Opaque2d, Opaque2dBinKey, Transparent2d, + }, tonemapping::{DebandDither, Tonemapping}, }; use bevy_derive::{Deref, DerefMut}; @@ -17,7 +17,6 @@ use bevy_ecs::{ }; use bevy_math::FloatOrd; use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_render::view::RenderVisibleEntities; use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{ @@ -34,14 +33,13 @@ use bevy_render::{ SpecializedMeshPipelineError, SpecializedMeshPipelines, }, renderer::RenderDevice, - view::{ExtractedView, InheritedVisibility, Msaa, ViewVisibility, Visibility}, + view::{ExtractedView, Msaa, RenderVisibleEntities, ViewVisibility}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_render::{render_resource::BindingResources, sync_world::MainEntityHashMap}; -use bevy_transform::components::{GlobalTransform, Transform}; -use bevy_utils::tracing::error; use core::{hash::Hash, marker::PhantomData}; use derive_more::derive::From; +use tracing::error; /// Materials are used alongside [`Material2dPlugin`], [`Mesh2d`], and [`MeshMaterial2d`] /// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level @@ -139,7 +137,10 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { } /// Customizes the default [`RenderPipelineDescriptor`]. - #[allow(unused_variables)] + #[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, @@ -278,7 +279,7 @@ impl Default for RenderMaterial2dInstances { } } -fn extract_mesh_materials_2d( +pub fn extract_mesh_materials_2d( mut material_instances: ResMut>, query: Extract), With>>, ) { @@ -468,7 +469,6 @@ pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelin } } -#[allow(clippy::too_many_arguments)] pub fn queue_material2d_meshes( opaque_draw_functions: Res>, alpha_mask_draw_functions: Res>, @@ -476,15 +476,16 @@ pub fn queue_material2d_meshes( material2d_pipeline: Res>, mut pipelines: ResMut>>, pipeline_cache: Res, - render_meshes: Res>, - render_materials: Res>>, + (render_meshes, render_materials): ( + Res>, + Res>>, + ), mut render_mesh_instances: ResMut, render_material_instances: Res>, mut transparent_render_phases: ResMut>, mut opaque_render_phases: ResMut>, mut alpha_mask_render_phases: ResMut>, views: Query<( - Entity, &ExtractedView, &RenderVisibleEntities, &Msaa, @@ -498,14 +499,16 @@ pub fn queue_material2d_meshes( return; } - for (view_entity, view, visible_entities, msaa, tonemapping, dither) in &views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + for (view, visible_entities, msaa, tonemapping, dither) in &views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; - let Some(opaque_phase) = opaque_render_phases.get_mut(&view_entity) else { + let Some(opaque_phase) = opaque_render_phases.get_mut(&view.retained_view_entity) else { continue; }; - let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view_entity) else { + let Some(alpha_mask_phase) = alpha_mask_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -563,6 +566,17 @@ pub fn queue_material2d_meshes( mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.world_from_local.translation.z; + // We don't support multidraw yet for 2D meshes, so we use this + // custom logic to generate the `BinnedRenderPhaseType` instead of + // `BinnedRenderPhaseType::mesh`, which can return + // `BinnedRenderPhaseType::MultidrawableMesh` if the hardware + // supports multidraw. + let binned_render_phase_type = if mesh_instance.automatic_batching { + BinnedRenderPhaseType::BatchableMesh + } else { + BinnedRenderPhaseType::UnbatchableMesh + }; + match material_2d.properties.alpha_mode { AlphaMode2d::Opaque => { let bin_key = Opaque2dBinKey { @@ -572,9 +586,12 @@ pub fn queue_material2d_meshes( material_bind_group_id: material_2d.get_bind_group_id().0, }; opaque_phase.add( + BatchSetKey2d { + indexed: mesh.indexed(), + }, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), + binned_render_phase_type, ); } AlphaMode2d::Mask(_) => { @@ -585,9 +602,12 @@ pub fn queue_material2d_meshes( material_bind_group_id: material_2d.get_bind_group_id().0, }; alpha_mask_phase.add( + BatchSetKey2d { + indexed: mesh.indexed(), + }, bin_key, (*render_entity, *visible_entity), - BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching), + binned_render_phase_type, ); } AlphaMode2d::Blend => { @@ -603,6 +623,7 @@ pub fn queue_material2d_meshes( // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::None, + indexed: mesh.indexed(), }); } } @@ -674,36 +695,3 @@ impl RenderAsset for PreparedMaterial2d { } } } - -/// A component bundle for entities with a [`Mesh2d`] and a [`MeshMaterial2d`]. -#[derive(Bundle, Clone)] -#[deprecated( - since = "0.15.0", - note = "Use the `Mesh2d` and `MeshMaterial2d` components instead. Inserting them will now also insert the other components required by them automatically." -)] -pub struct MaterialMesh2dBundle { - pub mesh: Mesh2d, - pub material: MeshMaterial2d, - pub transform: Transform, - pub global_transform: GlobalTransform, - /// User indication of whether an entity is visible - pub visibility: Visibility, - // Inherited visibility of an entity. - pub inherited_visibility: InheritedVisibility, - // Indication of whether an entity is visible in any view. - pub view_visibility: ViewVisibility, -} - -impl Default for MaterialMesh2dBundle { - fn default() -> Self { - Self { - mesh: Default::default(), - material: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - visibility: Default::default(), - inherited_visibility: Default::default(), - view_visibility: Default::default(), - } - } -} diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index bc6a5e9556e94..52309fd492aab 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -18,7 +18,7 @@ use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_math::{Affine3, Vec4}; use bevy_render::{ batching::{ - gpu_preprocessing::IndirectParameters, + gpu_preprocessing::IndirectParametersMetadata, no_gpu_preprocessing::{ self, batch_and_prepare_binned_render_phase, batch_and_prepare_sorted_render_phase, write_batched_instance_buffer, BatchedInstanceBuffer, @@ -44,8 +44,8 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::tracing::error; use nonmax::NonMaxU32; +use tracing::error; #[derive(Default)] pub struct Mesh2dRenderPlugin; @@ -203,7 +203,7 @@ pub struct RenderMesh2dInstance { #[derive(Default, Resource, Deref, DerefMut)] pub struct RenderMesh2dInstances(MainEntityHashMap); -#[derive(Component)] +#[derive(Component, Default)] pub struct Mesh2dMarker; pub fn extract_mesh2d( @@ -375,7 +375,7 @@ impl GetFullBatchData for Mesh2dPipeline { fn get_binned_batch_data( (mesh_instances, _, _): &SystemParamItem, - (_entity, main_entity): (Entity, MainEntity), + main_entity: MainEntity, ) -> Option { let mesh_instance = mesh_instances.get(&main_entity)?; Some((&mesh_instance.transforms).into()) @@ -383,7 +383,7 @@ impl GetFullBatchData for Mesh2dPipeline { fn get_index_and_compare_data( _: &SystemParamItem, - _query_item: (Entity, MainEntity), + _query_item: MainEntity, ) -> Option<(NonMaxU32, Option)> { error!( "`get_index_and_compare_data` is only intended for GPU mesh uniform building, \ @@ -394,7 +394,7 @@ impl GetFullBatchData for Mesh2dPipeline { fn get_binned_index( _: &SystemParamItem, - _query_item: (Entity, MainEntity), + _query_item: MainEntity, ) -> Option { error!( "`get_binned_index` is only intended for GPU mesh uniform building, \ @@ -403,45 +403,33 @@ impl GetFullBatchData for Mesh2dPipeline { None } - fn get_batch_indirect_parameters_index( - (mesh_instances, meshes, mesh_allocator): &SystemParamItem, - indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::IndirectParametersBuffer, - (_entity, main_entity): (Entity, MainEntity), - instance_index: u32, - ) -> Option { - let mesh_instance = mesh_instances.get(&main_entity)?; - let mesh = meshes.get(mesh_instance.mesh_asset_id)?; - let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)?; - + fn write_batch_indirect_parameters_metadata( + input_index: u32, + indexed: bool, + base_output_index: u32, + batch_set_index: Option, + indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::IndirectParametersBuffers, + indirect_parameters_offset: u32, + ) { // Note that `IndirectParameters` covers both of these structures, even // though they actually have distinct layouts. See the comment above that // type for more information. - let indirect_parameters = match mesh.buffer_info { - RenderMeshBufferInfo::Indexed { - count: index_count, .. - } => { - let index_buffer_slice = - mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)?; - IndirectParameters { - vertex_or_index_count: index_count, - instance_count: 0, - first_vertex_or_first_index: index_buffer_slice.range.start, - base_vertex_or_first_instance: vertex_buffer_slice.range.start, - first_instance: instance_index, - } - } - RenderMeshBufferInfo::NonIndexed => IndirectParameters { - vertex_or_index_count: mesh.vertex_count, - instance_count: 0, - first_vertex_or_first_index: vertex_buffer_slice.range.start, - base_vertex_or_first_instance: instance_index, - first_instance: instance_index, + let indirect_parameters = IndirectParametersMetadata { + mesh_index: input_index, + base_output_index, + batch_set_index: match batch_set_index { + None => !0, + Some(batch_set_index) => u32::from(batch_set_index), }, + instance_count: 0, }; - (indirect_parameters_buffer.push(indirect_parameters) as u32) - .try_into() - .ok() + if indexed { + indirect_parameters_buffer.set_indexed(indirect_parameters_offset, indirect_parameters); + } else { + indirect_parameters_buffer + .set_non_indexed(indirect_parameters_offset, indirect_parameters); + } } } @@ -706,7 +694,6 @@ pub struct Mesh2dViewBindGroup { pub value: BindGroup, } -#[allow(clippy::too_many_arguments)] pub fn prepare_mesh2d_view_bind_groups( mut commands: Commands, render_device: Res, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 6f2659fbaaf91..8366d13d946ca 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -124,7 +124,6 @@ fn global_color_changed( } /// Updates the wireframe material when the color in [`Wireframe2dColor`] changes -#[allow(clippy::type_complexity)] fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index bd57aaf202036..5517629d322fb 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -2,23 +2,27 @@ //! sprites with arbitrary transforms. Picking is done based on sprite bounds, not visible pixels. //! This means a partially transparent sprite is pickable even in its transparent areas. -use core::cmp::Reverse; - -use crate::{Sprite, TextureAtlasLayout}; +use crate::Sprite; use bevy_app::prelude::*; use bevy_asset::prelude::*; use bevy_color::Alpha; use bevy_ecs::prelude::*; -use bevy_image::Image; -use bevy_math::{prelude::*, FloatExt, FloatOrd}; +use bevy_image::prelude::*; +use bevy_math::{prelude::*, FloatExt}; use bevy_picking::backend::prelude::*; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; +/// A component that marks cameras that should be used in the [`SpritePickingPlugin`]. +#[derive(Debug, Clone, Default, Component, Reflect)] +#[reflect(Debug, Default, Component)] +pub struct SpritePickingCamera; + /// How should the [`SpritePickingPlugin`] handle picking and how should it handle transparent pixels #[derive(Debug, Clone, Copy, Reflect)] +#[reflect(Debug)] pub enum SpritePickingMode { /// Even if a sprite is picked on a transparent pixel, it should still count within the backend. /// Only consider the rect of a given sprite. @@ -32,15 +36,22 @@ pub enum SpritePickingMode { #[derive(Resource, Reflect)] #[reflect(Resource, Default)] pub struct SpritePickingSettings { + /// When set to `true` sprite picking will only consider cameras marked with + /// [`SpritePickingCamera`] and entities marked with [`Pickable`]. `false` by default. + /// + /// This setting is provided to give you fine-grained control over which cameras and entities + /// should be used by the sprite picking backend at runtime. + pub require_markers: bool, /// Should the backend count transparent pixels as part of the sprite for picking purposes or should it use the bounding box of the sprite alone. /// - /// Defaults to an incusive alpha threshold of 0.1 + /// Defaults to an inclusive alpha threshold of 0.1 pub picking_mode: SpritePickingMode, } impl Default for SpritePickingSettings { fn default() -> Self { Self { + require_markers: false, picking_mode: SpritePickingMode::AlphaThreshold(0.1), } } @@ -52,14 +63,24 @@ pub struct SpritePickingPlugin; impl Plugin for SpritePickingPlugin { fn build(&self, app: &mut App) { app.init_resource::() + .register_type::<( + SpritePickingCamera, + SpritePickingMode, + SpritePickingSettings, + )>() .add_systems(PreUpdate, sprite_picking.in_set(PickSet::Backend)); } } -#[allow(clippy::too_many_arguments)] fn sprite_picking( pointers: Query<(&PointerId, &PointerLocation)>, - cameras: Query<(Entity, &Camera, &GlobalTransform, &OrthographicProjection)>, + cameras: Query<( + Entity, + &Camera, + &GlobalTransform, + &Projection, + Has, + )>, primary_window: Query>, images: Res>, texture_atlas_layout: Res>, @@ -68,22 +89,27 @@ fn sprite_picking( Entity, &Sprite, &GlobalTransform, - Option<&PickingBehavior>, + Option<&Pickable>, &ViewVisibility, )>, mut output: EventWriter, ) { let mut sorted_sprites: Vec<_> = sprite_query .iter() - .filter_map(|(entity, sprite, transform, picking_behavior, vis)| { - if !transform.affine().is_nan() && vis.get() { - Some((entity, sprite, transform, picking_behavior)) + .filter_map(|(entity, sprite, transform, pickable, vis)| { + let marker_requirement = !settings.require_markers || pickable.is_some(); + if !transform.affine().is_nan() && vis.get() && marker_requirement { + Some((entity, sprite, transform, pickable)) } else { None } }) .collect(); - sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.2.translation().z))); + + // radsort is a stable radix sort that performed better than `slice::sort_by_key` + radsort::sort_by_key(&mut sorted_sprites, |(_, _, transform, _)| { + -transform.translation().z + }); let primary_window = primary_window.get_single().ok(); @@ -91,16 +117,19 @@ fn sprite_picking( pointer_location.location().map(|loc| (pointer, loc)) }) { let mut blocked = false; - let Some((cam_entity, camera, cam_transform, cam_ortho)) = cameras - .iter() - .filter(|(_, camera, _, _)| camera.is_active) - .find(|(_, camera, _, _)| { - camera - .target - .normalize(primary_window) - .map(|x| x == location.target) - .unwrap_or(false) - }) + let Some((cam_entity, camera, cam_transform, Projection::Orthographic(cam_ortho), _)) = + cameras + .iter() + .filter(|(_, camera, _, _, cam_can_pick)| { + let marker_requirement = !settings.require_markers || *cam_can_pick; + camera.is_active && marker_requirement + }) + .find(|(_, camera, _, _, _)| { + camera + .target + .normalize(primary_window) + .is_some_and(|x| x == location.target) + }) else { continue; }; @@ -120,7 +149,7 @@ fn sprite_picking( let picks: Vec<(Entity, HitData)> = sorted_sprites .iter() .copied() - .filter_map(|(entity, sprite, sprite_transform, picking_behavior)| { + .filter_map(|(entity, sprite, sprite_transform, pickable)| { if blocked { return None; } @@ -186,9 +215,7 @@ fn sprite_picking( }; blocked = cursor_in_valid_pixels_of_sprite - && picking_behavior - .map(|p| p.should_block_lower) - .unwrap_or(true); + && pickable.is_none_or(|p| p.should_block_lower); cursor_in_valid_pixels_of_sprite.then(|| { let hit_pos_world = diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 51e5f41b97fd1..585229052e04f 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,8 +1,6 @@ use core::ops::Range; -use crate::{ - texture_atlas::TextureAtlasLayout, ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE, -}; +use crate::{ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE}; use bevy_asset::{AssetEvent, AssetId, Assets}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ @@ -17,7 +15,7 @@ use bevy_ecs::{ query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, }; -use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo}; use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; use bevy_render::sync_world::MainEntity; use bevy_render::view::RenderVisibleEntities; @@ -494,7 +492,6 @@ pub struct ImageBindGroups { values: HashMap, BindGroup>, } -#[allow(clippy::too_many_arguments)] pub fn queue_sprites( mut view_entities: Local, draw_functions: Res>, @@ -504,7 +501,6 @@ pub fn queue_sprites( extracted_sprites: Res, mut transparent_render_phases: ResMut>, mut views: Query<( - Entity, &RenderVisibleEntities, &ExtractedView, &Msaa, @@ -514,8 +510,9 @@ pub fn queue_sprites( ) { let draw_sprite_function = draw_functions.read().id::(); - for (view_entity, visible_entities, view, msaa, tonemapping, dither) in &mut views { - let Some(transparent_phase) = transparent_render_phases.get_mut(&view_entity) else { + for (visible_entities, view, msaa, tonemapping, dither) in &mut views { + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + else { continue; }; @@ -577,12 +574,12 @@ pub fn queue_sprites( // batch_range and dynamic_offset will be calculated in prepare_sprites batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, + indexed: true, }); } } } -#[allow(clippy::too_many_arguments)] pub fn prepare_sprite_view_bind_groups( mut commands: Commands, render_device: Res, @@ -616,7 +613,6 @@ pub fn prepare_sprite_view_bind_groups( } } -#[allow(clippy::too_many_arguments)] pub fn prepare_sprite_image_bind_groups( mut commands: Commands, mut previous_len: Local, @@ -670,8 +666,7 @@ pub fn prepare_sprite_image_bind_groups( continue; }; - let batch_image_changed = batch_image_handle != extracted_sprite.image_handle_id; - if batch_image_changed { + if batch_image_handle != extracted_sprite.image_handle_id { let Some(gpu_image) = gpu_images.get(extracted_sprite.image_handle_id) else { continue; }; @@ -691,6 +686,15 @@ pub fn prepare_sprite_image_bind_groups( )), ) }); + + batch_item_index = item_index; + batches.push(( + item.entity(), + SpriteBatch { + image_handle_id: batch_image_handle, + range: index..index, + }, + )); } // By default, the size of the quad is the size of the texture @@ -742,18 +746,6 @@ pub fn prepare_sprite_image_bind_groups( &uv_offset_scale, )); - if batch_image_changed { - batch_item_index = item_index; - - batches.push(( - item.entity(), - SpriteBatch { - image_handle_id: batch_image_handle, - range: index..index, - }, - )); - } - transparent_phase.items[batch_item_index] .batch_range_mut() .end += 1; diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index c6550f51d7e62..82101a8b38024 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -4,7 +4,7 @@ use bevy_ecs::{ component::{require, Component}, reflect::ReflectComponent, }; -use bevy_image::Image; +use bevy_image::{Image, TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ @@ -13,7 +13,7 @@ use bevy_render::{ }; use bevy_transform::components::Transform; -use crate::{TextureAtlas, TextureAtlasLayout, TextureSlicer}; +use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera #[derive(Component, Debug, Default, Clone, Reflect)] @@ -230,10 +230,11 @@ mod tests { use bevy_asset::{Assets, RenderAssetUsages}; use bevy_color::Color; use bevy_image::Image; + use bevy_image::{TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, URect, UVec2, Vec2}; use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat}; - use crate::{Anchor, TextureAtlas, TextureAtlasLayout}; + use crate::Anchor; use super::Sprite; diff --git a/crates/bevy_sprite/src/texture_slice/mod.rs b/crates/bevy_sprite/src/texture_slice/mod.rs index 2dea51adc6d41..7ea01d5839dc2 100644 --- a/crates/bevy_sprite/src/texture_slice/mod.rs +++ b/crates/bevy_sprite/src/texture_slice/mod.rs @@ -22,7 +22,7 @@ pub struct TextureSlice { } impl TextureSlice { - /// Transforms the given slice in an collection of tiled subdivisions. + /// Transforms the given slice in a collection of tiled subdivisions. /// /// # Arguments /// @@ -86,7 +86,7 @@ impl TextureSlice { remaining_columns -= size_y; } if slices.len() > 1_000 { - bevy_utils::tracing::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len()); + tracing::warn!("One of your tiled textures has generated {} slices. You might want to use higher stretch values to avoid a great performance cost", slices.len()); } slices } diff --git a/crates/bevy_sprite/src/texture_slice/slicer.rs b/crates/bevy_sprite/src/texture_slice/slicer.rs index 310be429796a5..7250533550dff 100644 --- a/crates/bevy_sprite/src/texture_slice/slicer.rs +++ b/crates/bevy_sprite/src/texture_slice/slicer.rs @@ -217,7 +217,7 @@ impl TextureSlicer { if self.border.left + self.border.right >= rect.size().x || self.border.top + self.border.bottom >= rect.size().y { - bevy_utils::tracing::error!( + tracing::error!( "TextureSlicer::border has out of bounds values. No slicing will be applied" ); return vec![TextureSlice { diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index 30ddcfd6eddc2..bc32d18f6dd6f 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_state" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Finite state machines for Bevy" homepage = "https://bevyengine.org" @@ -8,23 +8,67 @@ repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [features] -default = ["bevy_reflect", "bevy_app", "bevy_hierarchy"] -bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] -bevy_app = ["dep:bevy_app"] +default = ["std", "bevy_reflect", "bevy_app", "bevy_hierarchy"] + +# Functionality + +## Adds runtime reflection support using `bevy_reflect`. +bevy_reflect = [ + "dep:bevy_reflect", + "bevy_ecs/bevy_reflect", + "bevy_hierarchy?/reflect", + "bevy_app?/bevy_reflect", +] + +## Adds integration with the `bevy_app` plugin API. +bevy_app = ["dep:bevy_app", "bevy_hierarchy?/bevy_app"] + +## Adds integration with the `bevy_hierarchy` `Parent` and `Children` API. bevy_hierarchy = ["dep:bevy_hierarchy"] +# Platform Compatibility + +## 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 = [ + "bevy_ecs/std", + "bevy_utils/std", + "bevy_reflect?/std", + "bevy_app?/std", + "bevy_hierarchy?/std", +] + +## `critical-section` provides the building blocks for synchronization primitives +## on all platforms, including `no_std`. +critical-section = [ + "bevy_ecs/critical-section", + "bevy_utils/critical-section", + "bevy_app?/critical-section", +] + +## `portable-atomic` provides additional platform support for atomic types and +## operations, even on targets without native support. +portable-atomic = [ + "bevy_ecs/portable-atomic", + "bevy_utils/portable-atomic", + "bevy_app?/portable-atomic", +] + [dependencies] -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_state_macros = { path = "macros", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", optional = true } -bevy_app = { path = "../bevy_app", version = "0.15.0-dev", optional = true } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev", optional = true } +# 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_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev", default-features = false, optional = true } variadics_please = "1.1" +# other +log = { version = "0.4", default-features = false } + [lints] workspace = true diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml index 50a4d468b1508..2b734d2d1c637 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.15.0-dev" +version = "0.16.0-dev" description = "Macros for bevy_state" edition = "2021" 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.15.0-dev" } +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } syn = { version = "2.0", features = ["full"] } quote = "1.0" diff --git a/crates/bevy_state/src/app.rs b/crates/bevy_state/src/app.rs index c1aaf0933337a..b00c60de53738 100644 --- a/crates/bevy_state/src/app.rs +++ b/crates/bevy_state/src/app.rs @@ -1,6 +1,7 @@ use bevy_app::{App, MainScheduleOrder, Plugin, PreStartup, PreUpdate, SubApp}; use bevy_ecs::{event::Events, schedule::IntoSystemConfigs, world::FromWorld}; -use bevy_utils::{tracing::warn, warn_once}; +use bevy_utils::once; +use log::warn; use crate::{ state::{ @@ -87,7 +88,9 @@ pub trait AppExtStates { /// Separate function to only warn once for all state installation methods. fn warn_if_no_states_plugin_installed(app: &SubApp) { if !app.is_plugin_added::() { - warn_once!("States were added to the app, but `StatesPlugin` is not installed."); + once!(warn!( + "States were added to the app, but `StatesPlugin` is not installed." + )); } } diff --git a/crates/bevy_state/src/commands.rs b/crates/bevy_state/src/commands.rs index 036e35d6033ee..d9da362b628f4 100644 --- a/crates/bevy_state/src/commands.rs +++ b/crates/bevy_state/src/commands.rs @@ -1,5 +1,5 @@ use bevy_ecs::{system::Commands, world::World}; -use bevy_utils::tracing::debug; +use log::debug; use crate::state::{FreelyMutableState, NextState}; diff --git a/crates/bevy_state/src/condition.rs b/crates/bevy_state/src/condition.rs index dac281b7a3201..f0a619708776d 100644 --- a/crates/bevy_state/src/condition.rs +++ b/crates/bevy_state/src/condition.rs @@ -9,11 +9,14 @@ use bevy_ecs::{change_detection::DetectChanges, system::Res}; /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_state::prelude::*; +/// # use bevy_app::{App, Update}; +/// # use bevy_state::app::StatesPlugin; /// # #[derive(Resource, Default)] /// # struct Counter(u8); -/// # let mut app = Schedule::default(); -/// # let mut world = World::new(); -/// # world.init_resource::(); +/// # let mut app = App::new(); +/// # app +/// # .init_resource::() +/// # .add_plugins(StatesPlugin); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] /// enum GameState { /// #[default] @@ -21,7 +24,7 @@ use bevy_ecs::{change_detection::DetectChanges, system::Res}; /// Paused, /// } /// -/// app.add_systems( +/// app.add_systems(Update, /// // `state_exists` will only return true if the /// // given state exists /// my_system.run_if(state_exists::), @@ -31,15 +34,15 @@ use bevy_ecs::{change_detection::DetectChanges, system::Res}; /// counter.0 += 1; /// } /// -/// // `GameState` does not yet exist `my_system` won't run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 0); +/// // `GameState` does not yet exist so `my_system` won't run +/// app.update(); +/// assert_eq!(app.world().resource::().0, 0); /// -/// world.init_resource::>(); +/// app.init_state::(); /// /// // `GameState` now exists so `my_system` will run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 1); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 1); /// ``` pub fn state_exists(current_state: Option>>) -> bool { current_state.is_some() @@ -55,11 +58,14 @@ pub fn state_exists(current_state: Option>>) -> bool { /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_state::prelude::*; +/// # use bevy_app::{App, Update}; +/// # use bevy_state::app::StatesPlugin; /// # #[derive(Resource, Default)] /// # struct Counter(u8); -/// # let mut app = Schedule::default(); -/// # let mut world = World::new(); -/// # world.init_resource::(); +/// # let mut app = App::new(); +/// # app +/// # .init_resource::() +/// # .add_plugins(StatesPlugin); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] /// enum GameState { /// #[default] @@ -67,14 +73,14 @@ pub fn state_exists(current_state: Option>>) -> bool { /// Paused, /// } /// -/// world.init_resource::>(); -/// -/// app.add_systems(( -/// // `in_state` will only return true if the -/// // given state equals the given value -/// play_system.run_if(in_state(GameState::Playing)), -/// pause_system.run_if(in_state(GameState::Paused)), -/// )); +/// app +/// .init_state::() +/// .add_systems(Update, ( +/// // `in_state` will only return true if the +/// // given state equals the given value +/// play_system.run_if(in_state(GameState::Playing)), +/// pause_system.run_if(in_state(GameState::Paused)), +/// )); /// /// fn play_system(mut counter: ResMut) { /// counter.0 += 1; @@ -85,14 +91,14 @@ pub fn state_exists(current_state: Option>>) -> bool { /// } /// /// // We default to `GameState::Playing` so `play_system` runs -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 1); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 1); /// -/// *world.resource_mut::>() = State::new(GameState::Paused); +/// app.insert_state(GameState::Paused); /// /// // Now that we are in `GameState::Pause`, `pause_system` will run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 0); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 0); /// ``` pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { move |current_state: Option>>| match current_state { @@ -114,11 +120,14 @@ pub fn in_state(state: S) -> impl FnMut(Option>>) -> boo /// ``` /// # use bevy_ecs::prelude::*; /// # use bevy_state::prelude::*; +/// # use bevy_state::app::StatesPlugin; +/// # use bevy_app::{App, Update}; /// # #[derive(Resource, Default)] /// # struct Counter(u8); -/// # let mut app = Schedule::default(); -/// # let mut world = World::new(); -/// # world.init_resource::(); +/// # let mut app = App::new(); +/// # app +/// # .init_resource::() +/// # .add_plugins(StatesPlugin); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] /// enum GameState { /// #[default] @@ -126,32 +135,32 @@ pub fn in_state(state: S) -> impl FnMut(Option>>) -> boo /// Paused, /// } /// -/// world.init_resource::>(); -/// -/// app.add_systems( -/// // `state_changed` will only return true if the -/// // given states value has just been updated or -/// // the state has just been added -/// my_system.run_if(state_changed::), -/// ); +/// app +/// .init_state::() +/// .add_systems(Update, +/// // `state_changed` will only return true if the +/// // given states value has just been updated or +/// // the state has just been added +/// my_system.run_if(state_changed::), +/// ); /// /// fn my_system(mut counter: ResMut) { /// counter.0 += 1; /// } /// /// // `GameState` has just been added so `my_system` will run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 1); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 1); /// /// // `GameState` has not been updated so `my_system` will not run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 1); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 1); /// -/// *world.resource_mut::>() = State::new(GameState::Paused); +/// app.insert_state(GameState::Paused); /// /// // Now that `GameState` has been updated `my_system` will run -/// app.run(&mut world); -/// assert_eq!(world.resource::().0, 2); +/// app.update(); +/// assert_eq!(app.world().resource::().0, 2); /// ``` pub fn state_changed(current_state: Option>>) -> bool { let Some(current_state) = current_state else { diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index 796516f5a6b9e..cdcb37f4ca24d 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -1,3 +1,5 @@ +#![no_std] + //! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. //! //! This module provides 3 distinct types of state, all of which implement the [`States`](state::States) trait: @@ -36,6 +38,11 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] +#[cfg(feature = "std")] +extern crate std; + +extern crate alloc; + #[cfg(feature = "bevy_app")] /// Provides [`App`](bevy_app::App) and [`SubApp`](bevy_app::SubApp) with state installation methods pub mod app; diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index d02d3a32ed452..c71827cc5f06b 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -17,6 +17,7 @@ pub use transitions::*; #[cfg(test)] mod tests { + use alloc::vec::Vec; use bevy_ecs::{event::EventRegistry, prelude::*}; use bevy_state_macros::{States, SubStates}; diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index fbeafe545310b..1906370ad3f60 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -1,3 +1,4 @@ +use alloc::vec::Vec; use core::marker::PhantomData; use bevy_app::{App, SubApp}; diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 00ec38e4005c2..e0a61ecb1debd 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_tasks" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "A task executor for Bevy Engine" homepage = "https://bevyengine.org" diff --git a/crates/bevy_tasks/src/executor.rs b/crates/bevy_tasks/src/executor.rs index 04667c1b16d59..3c18ccd897fa1 100644 --- a/crates/bevy_tasks/src/executor.rs +++ b/crates/bevy_tasks/src/executor.rs @@ -8,14 +8,13 @@ //! [`async-executor`]: https://crates.io/crates/async-executor //! [`edge-executor`]: https://crates.io/crates/edge-executor -pub use async_task::Task; use core::{ fmt, panic::{RefUnwindSafe, UnwindSafe}, }; use derive_more::{Deref, DerefMut}; -#[cfg(feature = "multi_threaded")] +#[cfg(all(feature = "multi_threaded", not(target_arch = "wasm32")))] pub use async_task::FallibleTask; #[cfg(feature = "async_executor")] @@ -51,6 +50,7 @@ pub struct LocalExecutor<'a>(LocalExecutorInner<'a>); impl Executor<'_> { /// Construct a new [`Executor`] + #[expect(clippy::allow_attributes, reason = "This lint may not always trigger.")] #[allow(dead_code, reason = "not all feature flags require this function")] pub const fn new() -> Self { Self(ExecutorInner::new()) @@ -59,6 +59,7 @@ impl Executor<'_> { impl LocalExecutor<'_> { /// Construct a new [`LocalExecutor`] + #[expect(clippy::allow_attributes, reason = "This lint may not always trigger.")] #[allow(dead_code, reason = "not all feature flags require this function")] pub const fn new() -> Self { Self(LocalExecutorInner::new()) diff --git a/crates/bevy_utils/src/futures.rs b/crates/bevy_tasks/src/futures.rs similarity index 96% rename from crates/bevy_utils/src/futures.rs rename to crates/bevy_tasks/src/futures.rs index 6a4f9ff9cc9e4..a28138e0ecaa2 100644 --- a/crates/bevy_utils/src/futures.rs +++ b/crates/bevy_tasks/src/futures.rs @@ -1,3 +1,5 @@ +#![expect(unsafe_code, reason = "Futures require unsafe code.")] + //! Utilities for working with [`Future`]s. use core::{ future::Future, diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 3f3db301bbb00..220f3dcae2631 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -4,10 +4,46 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] -#![cfg_attr(not(feature = "std"), no_std)] +#![no_std] + +#[cfg(feature = "std")] +extern crate std; extern crate alloc; +#[cfg(not(any(feature = "async_executor", feature = "edge_executor")))] +compile_error!("Either of the `async_executor` or the `edge_executor` features must be enabled."); + +#[cfg(not(target_arch = "wasm32"))] +mod conditional_send { + /// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. Wasm), + /// futures aren't Send. + pub trait ConditionalSend: Send {} + impl ConditionalSend for T {} +} + +#[cfg(target_arch = "wasm32")] +#[expect(missing_docs, reason = "Not all docs are written yet (#3492).")] +mod conditional_send { + pub trait ConditionalSend {} + impl ConditionalSend for T {} +} + +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: core::future::Future + ConditionalSend {} +impl ConditionalSendFuture for T {} + +use alloc::boxed::Box; + +/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. +pub type BoxedFuture<'a, T> = core::pin::Pin + 'a>>; + +pub mod futures; + +#[cfg(any(feature = "async_executor", feature = "edge_executor"))] mod executor; mod slice; diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 51adc739c1f8c..598c8f9f11689 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -3,18 +3,59 @@ use core::{cell::RefCell, future::Future, marker::PhantomData, mem}; use crate::Task; +#[cfg(feature = "std")] +use std::thread_local; + #[cfg(feature = "portable-atomic")] use portable_atomic_util::Arc; #[cfg(not(feature = "portable-atomic"))] use alloc::sync::Arc; -#[cfg(feature = "std")] +#[cfg(all( + feature = "std", + any(feature = "async_executor", feature = "edge_executor") +))] use crate::executor::LocalExecutor; -#[cfg(not(feature = "std"))] +#[cfg(all( + not(feature = "std"), + any(feature = "async_executor", feature = "edge_executor") +))] use crate::executor::Executor as LocalExecutor; +#[cfg(not(any(feature = "async_executor", feature = "edge_executor")))] +mod dummy_executor { + use async_task::Task; + use core::{future::Future, marker::PhantomData}; + + /// Dummy implementation of a `LocalExecutor` to allow for a cleaner compiler error + /// due to missing feature flags. + #[doc(hidden)] + #[derive(Debug)] + pub struct LocalExecutor<'a>(PhantomData); + + impl<'a> LocalExecutor<'a> { + /// Dummy implementation + pub const fn new() -> Self { + Self(PhantomData) + } + + /// Dummy implementation + pub fn try_tick(&self) -> bool { + unimplemented!() + } + + /// Dummy implementation + pub fn spawn(&self, _: impl Future + 'a) -> Task { + unimplemented!() + } + } +} + +#[cfg(not(any(feature = "async_executor", feature = "edge_executor")))] +use dummy_executor::LocalExecutor; + #[cfg(feature = "std")] thread_local! { static LOCAL_EXECUTOR: LocalExecutor<'static> = const { LocalExecutor::new() }; @@ -198,7 +239,7 @@ impl TaskPool { where T: 'static + MaybeSend + MaybeSync, { - #[cfg(all(target_arch = "wasm32", feature = "std"))] + #[cfg(target_arch = "wasm32")] return Task::wrap_future(future); #[cfg(all(not(target_arch = "wasm32"), feature = "std"))] @@ -210,7 +251,7 @@ impl TaskPool { Task::new(task) }); - #[cfg(not(feature = "std"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))] return { let task = LOCAL_EXECUTOR.spawn(future); // Loop until all tasks are done diff --git a/crates/bevy_tasks/src/slice.rs b/crates/bevy_tasks/src/slice.rs index 5f964a4561778..a705314a34502 100644 --- a/crates/bevy_tasks/src/slice.rs +++ b/crates/bevy_tasks/src/slice.rs @@ -215,6 +215,7 @@ impl ParallelSliceMut for S where S: AsMut<[T]> {} #[cfg(test)] mod tests { use crate::*; + use alloc::vec; #[test] fn test_par_chunks_map() { diff --git a/crates/bevy_tasks/src/task.rs b/crates/bevy_tasks/src/task.rs index cf5095408b0f3..d4afb775f2e01 100644 --- a/crates/bevy_tasks/src/task.rs +++ b/crates/bevy_tasks/src/task.rs @@ -14,11 +14,11 @@ use core::{ /// Tasks that panic get immediately canceled. Awaiting a canceled task also causes a panic. #[derive(Debug)] #[must_use = "Tasks are canceled when dropped, use `.detach()` to run them in the background."] -pub struct Task(crate::executor::Task); +pub struct Task(async_task::Task); impl Task { /// Creates a new task from a given `async_executor::Task` - pub fn new(task: crate::executor::Task) -> Self { + pub fn new(task: async_task::Task) -> Self { Self(task) } diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 215981215f2f7..c16aeca355a67 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -1,12 +1,16 @@ +use alloc::{boxed::Box, format, string::String, vec::Vec}; use core::{future::Future, marker::PhantomData, mem, panic::AssertUnwindSafe}; -use std::thread::{self, JoinHandle}; +use std::{ + thread::{self, JoinHandle}, + thread_local, +}; use crate::executor::FallibleTask; use concurrent_queue::ConcurrentQueue; use futures_lite::FutureExt; #[cfg(feature = "portable-atomic")] -use {alloc::boxed::Box, portable_atomic_util::Arc}; +use portable_atomic_util::Arc; #[cfg(not(feature = "portable-atomic"))] use alloc::sync::Arc; @@ -330,7 +334,7 @@ impl TaskPool { T: Send + 'static, { Self::THREAD_EXECUTOR.with(|scope_executor| { - // If a `external_executor` is passed use that. Otherwise get the executor stored + // If an `external_executor` is passed, use that. Otherwise, get the executor stored // in the `THREAD_EXECUTOR` thread local. if let Some(external_executor) = external_executor { self.scope_with_executor_inner( @@ -694,7 +698,6 @@ where } #[cfg(test)] -#[allow(clippy::disallowed_types)] mod tests { use super::*; use core::sync::atomic::{AtomicBool, AtomicI32, Ordering}; diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs index 0f8a9c3be9038..48fb3e2861c05 100644 --- a/crates/bevy_tasks/src/thread_executor.rs +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -1,7 +1,8 @@ use core::marker::PhantomData; use std::thread::{self, ThreadId}; -use crate::executor::{Executor, Task}; +use crate::executor::Executor; +use async_task::Task; use futures_lite::Future; /// An executor that can only be ticked on the thread it was instantiated on. But diff --git a/crates/bevy_tasks/src/wasm_task.rs b/crates/bevy_tasks/src/wasm_task.rs index cdf805b2b840c..0cc569c47913d 100644 --- a/crates/bevy_tasks/src/wasm_task.rs +++ b/crates/bevy_tasks/src/wasm_task.rs @@ -1,3 +1,4 @@ +use alloc::boxed::Box; use core::{ any::Any, future::{Future, IntoFuture}, @@ -59,7 +60,12 @@ impl Future for Task { // NOTE: Propagating the panic here sorta has parity with the async_executor behavior. // For those tasks, polling them after a panic returns a `None` which gets `unwrap`ed, so // using `resume_unwind` here is essentially keeping the same behavior while adding more information. + #[cfg(feature = "std")] Poll::Ready(Ok(Err(panic))) => std::panic::resume_unwind(panic), + #[cfg(not(feature = "std"))] + Poll::Ready(Ok(Err(_panic))) => { + unreachable!("catching a panic is only possible with std") + } Poll::Ready(Err(_)) => panic!("Polled a task after it was cancelled"), Poll::Pending => Poll::Pending, } @@ -74,6 +80,14 @@ struct CatchUnwind(#[pin] F); impl Future for CatchUnwind { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - std::panic::catch_unwind(AssertUnwindSafe(|| self.project().0.poll(cx)))?.map(Ok) + let f = AssertUnwindSafe(|| self.project().0.poll(cx)); + + #[cfg(feature = "std")] + let result = std::panic::catch_unwind(f)?; + + #[cfg(not(feature = "std"))] + let result = f(); + + result.map(Ok) } } diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 80f89a6e5d1e4..2dcee021df8e1 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_text" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides text functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -13,22 +13,22 @@ default_font = [] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } -bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } -bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } -bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } -bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +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_hierarchy = { path = "../bevy_hierarchy", 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", features = [ "bevy", ] } -bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.15.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" } # other cosmic-text = { version = "0.12", features = ["shape-run-cache"] } @@ -37,6 +37,7 @@ serde = { version = "1", features = ["derive"] } smallvec = "1.13" unicode-bidi = "0.3.13" sys-locale = "0.3.0" +tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] approx = "0.5.1" diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 4ce4ea62072db..f053bb6ab13fa 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,11 +1,10 @@ use bevy_asset::{Assets, Handle}; -use bevy_image::{Image, ImageSampler}; +use bevy_image::{prelude::*, ImageSampler}; use bevy_math::{IVec2, UVec2}; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }; -use bevy_sprite::{DynamicTextureAtlasBuilder, TextureAtlasLayout}; use bevy_utils::HashMap; use crate::{FontSmoothing, GlyphAtlasLocation, TextError}; @@ -21,7 +20,7 @@ use crate::{FontSmoothing, GlyphAtlasLocation, TextError}; /// providing a trade-off between visual quality and performance. /// /// A [`CacheKey`](cosmic_text::CacheKey) encodes all of the information of a subpixel-offset glyph and is used to -/// find that glyphs raster in a [`TextureAtlas`](bevy_sprite::TextureAtlas) through its corresponding [`GlyphAtlasLocation`]. +/// find that glyphs raster in a [`TextureAtlas`] through its corresponding [`GlyphAtlasLocation`]. pub struct FontAtlas { /// Used to update the [`TextureAtlasLayout`]. pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 60374daf49bfc..4028f7eab8754 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -3,14 +3,13 @@ use bevy_ecs::{ event::EventReader, system::{ResMut, Resource}, }; -use bevy_image::Image; +use bevy_image::prelude::*; use bevy_math::{IVec2, UVec2}; use bevy_reflect::TypePath; use bevy_render::{ render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, }; -use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo}; @@ -92,9 +91,7 @@ impl FontAtlasSet { pub fn has_glyph(&self, cache_key: cosmic_text::CacheKey, font_size: &FontAtlasKey) -> bool { self.font_atlases .get(font_size) - .map_or(false, |font_atlas| { - font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key)) - }) + .is_some_and(|font_atlas| font_atlas.iter().any(|atlas| atlas.has_glyph(cache_key))) } /// Adds the given subpixel-offset glyph to the [`FontAtlas`]es in this set @@ -181,22 +178,15 @@ impl FontAtlasSet { self.font_atlases .get(&FontAtlasKey(cache_key.font_size_bits, font_smoothing)) .and_then(|font_atlases| { - font_atlases - .iter() - .find_map(|atlas| { - atlas.get_glyph_index(cache_key).map(|location| { - ( - location, - atlas.texture_atlas.clone_weak(), - atlas.texture.clone_weak(), - ) + font_atlases.iter().find_map(|atlas| { + atlas + .get_glyph_index(cache_key) + .map(|location| GlyphAtlasInfo { + location, + texture_atlas: atlas.texture_atlas.clone_weak(), + texture: atlas.texture.clone_weak(), }) - }) - .map(|(location, texture_atlas, texture)| GlyphAtlasInfo { - texture_atlas, - location, - texture, - }) + }) }) } diff --git a/crates/bevy_text/src/glyph.rs b/crates/bevy_text/src/glyph.rs index b5295c655e76a..6de501266c23d 100644 --- a/crates/bevy_text/src/glyph.rs +++ b/crates/bevy_text/src/glyph.rs @@ -1,10 +1,9 @@ //! This module exports types related to rendering glyphs. use bevy_asset::Handle; -use bevy_image::Image; +use bevy_image::prelude::*; use bevy_math::{IVec2, Vec2}; use bevy_reflect::Reflect; -use bevy_sprite::TextureAtlasLayout; /// A glyph of a font, typically representing a single character, positioned in screen space. /// diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index ad9feb7fdcf0f..7f71f486e4eb6 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -29,8 +29,6 @@ //! 3. [`PositionedGlyph`]s are stored in a [`TextLayoutInfo`], //! which contains all the information that downstream systems need for rendering. -#![allow(clippy::type_complexity)] - extern crate alloc; mod bounds; @@ -61,9 +59,6 @@ pub use text_access::*; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { - #[doc(hidden)] - #[allow(deprecated)] - pub use crate::Text2dBundle; #[doc(hidden)] pub use crate::{ Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, @@ -113,6 +108,7 @@ impl Plugin for TextPlugin { app.init_asset::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index f8e7274ef31f2..6eb9ed44636b6 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -9,10 +9,9 @@ use bevy_ecs::{ reflect::ReflectComponent, system::{ResMut, Resource}, }; -use bevy_image::Image; +use bevy_image::prelude::*; use bevy_math::{UVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_sprite::TextureAtlasLayout; use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; @@ -81,7 +80,6 @@ impl TextPipeline { /// Utilizes [`cosmic_text::Buffer`] to shape and layout text /// /// Negative or 0.0 font sizes will not be laid out. - #[allow(clippy::too_many_arguments)] pub fn update_buffer<'a>( &mut self, fonts: &Assets, @@ -98,6 +96,7 @@ impl TextPipeline { // Collect span information into a vec. This is necessary because font loading requires mut access // to FontSystem, which the cosmic-text Buffer also needs. let mut font_size: f32 = 0.; + let mut line_height: f32 = 0.0; let mut spans: Vec<(usize, &str, &TextFont, FontFaceInfo, Color)> = core::mem::take(&mut self.spans_buffer) .into_iter() @@ -130,6 +129,7 @@ impl TextPipeline { // Get max font size for use in cosmic Metrics. font_size = font_size.max(text_font.font_size); + line_height = line_height.max(text_font.line_height.eval(text_font.font_size)); // Load Bevy fonts into cosmic-text's font system. let face_info = load_font_to_fontdb( @@ -146,7 +146,6 @@ impl TextPipeline { spans.push((span_index, span, text_font, face_info, color)); } - let line_height = font_size * 1.2; let mut metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32); // Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling // through' to call `Buffer::set_rich_text` with zero spans so any cached text will be cleared without @@ -171,8 +170,7 @@ impl TextPipeline { // Update the buffer. let buffer = &mut computed.buffer; - buffer.set_metrics(font_system, metrics); - buffer.set_size(font_system, bounds.width, bounds.height); + buffer.set_metrics_and_size(font_system, metrics, bounds.width, bounds.height); buffer.set_wrap( font_system, @@ -193,6 +191,14 @@ impl TextPipeline { } buffer.shape_until_scroll(font_system, false); + // 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 { + let dimensions = buffer_dimensions(buffer); + // `set_size` causes a re-layout to occur. + buffer.set_size(font_system, Some(dimensions.x), bounds.height); + } + // Recover the spans buffer. spans.clear(); self.spans_buffer = spans @@ -207,7 +213,6 @@ impl TextPipeline { /// /// Produces a [`TextLayoutInfo`], containing [`PositionedGlyph`]s /// which contain information for rendering the text. - #[allow(clippy::too_many_arguments)] pub fn queue_text<'a>( &mut self, layout_info: &mut TextLayoutInfo, @@ -341,7 +346,6 @@ impl TextPipeline { /// /// Produces a [`TextMeasureInfo`] which can be used by a layout system /// to measure the text area on demand. - #[allow(clippy::too_many_arguments)] pub fn create_text_measure<'a>( &mut self, entity: Entity, @@ -486,7 +490,13 @@ fn get_attrs<'a>( .stretch(face_info.stretch) .style(face_info.style) .weight(face_info.weight) - .metrics(Metrics::relative(text_font.font_size, 1.2).scale(scale_factor as f32)) + .metrics( + Metrics { + font_size: text_font.font_size, + line_height: text_font.line_height.eval(text_font.font_size), + } + .scale(scale_factor as f32), + ) .color(cosmic_text::Color(color.to_linear().as_u32())); attrs } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 0effb361aa302..06f8adf86c38f 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -10,10 +10,11 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_hierarchy::{Children, Parent}; use bevy_reflect::prelude::*; -use bevy_utils::warn_once; +use bevy_utils::once; use cosmic_text::{Buffer, Metrics}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; +use tracing::warn; /// Wrapper for [`cosmic_text::Buffer`] #[derive(Deref, DerefMut, Debug, Clone)] @@ -232,7 +233,8 @@ impl From for TextSpan { /// This only affects the internal positioning of the lines of text within a text entity and /// does not affect the text entity's position. /// -/// _Has no affect on a single line text entity._ +/// _Has no affect on a single line text entity_, unless used together with a +/// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] pub enum JustifyText { @@ -284,6 +286,11 @@ pub struct TextFont { /// A new font atlas is generated for every combination of font handle and scaled font size /// which can have a strong performance impact. pub font_size: f32, + /// The vertical height of a line of text, from the top of one line to the top of the + /// next. + /// + /// Defaults to `LineHeight::RelativeToFont(1.2)` + pub line_height: LineHeight, /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, } @@ -316,6 +323,12 @@ impl TextFont { self.font_smoothing = font_smoothing; self } + + /// Returns this [`TextFont`] with the specified [`LineHeight`]. + pub const fn with_line_height(mut self, line_height: LineHeight) -> Self { + self.line_height = line_height; + self + } } impl Default for TextFont { @@ -323,11 +336,39 @@ impl Default for TextFont { Self { font: Default::default(), font_size: 20.0, + line_height: LineHeight::default(), font_smoothing: Default::default(), } } } +/// 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)] +pub enum LineHeight { + /// Set line height to a specific number of pixels + Px(f32), + /// Set line height to a multiple of the font size + RelativeToFont(f32), +} + +impl LineHeight { + pub(crate) fn eval(self, font_size: f32) -> f32 { + match self { + LineHeight::Px(px) => px, + LineHeight::RelativeToFont(scale) => scale * font_size, + } + } +} + +impl Default for LineHeight { + fn default() -> Self { + LineHeight::RelativeToFont(1.2) + } +} + /// The color of the text for this section. #[derive(Component, Copy, Clone, Debug, Deref, DerefMut, Reflect)] #[reflect(Component, Default, Debug)] @@ -442,8 +483,8 @@ pub fn detect_text_needs_rerender( // - Root children changed (can include additions and removals). for root in changed_roots.iter() { let Ok((_, Some(mut computed), _)) = computed.get_mut(root) else { - warn_once!("found entity {:?} with a root text component ({}) but no ComputedTextBlock; this warning only \ - prints once", root, core::any::type_name::()); + once!(warn!("found entity {} with a root text component ({}) but no ComputedTextBlock; this warning only \ + prints once", root, core::any::type_name::())); continue; }; computed.needs_rerender = true; @@ -455,18 +496,18 @@ pub fn detect_text_needs_rerender( // - Span children changed (can include additions and removals). for (entity, maybe_span_parent, has_text_block) in changed_spans.iter() { if has_text_block { - warn_once!("found entity {:?} with a TextSpan that has a TextLayout, which should only be on root \ + once!(warn!("found entity {} with a TextSpan that has a TextLayout, which should only be on root \ text entities (that have {}); this warning only prints once", - entity, core::any::type_name::()); + entity, core::any::type_name::())); } let Some(span_parent) = maybe_span_parent else { - warn_once!( - "found entity {:?} with a TextSpan that has no parent; it should have an ancestor \ + once!(warn!( + "found entity {} with a TextSpan that has no parent; it should have an ancestor \ with a root text component ({}); this warning only prints once", entity, core::any::type_name::() - ); + )); continue; }; let mut parent: Entity = **span_parent; @@ -476,9 +517,9 @@ pub fn detect_text_needs_rerender( // is outweighed by the expense of tracking visited spans. loop { let Ok((maybe_parent, maybe_computed, has_span)) = computed.get_mut(parent) else { - warn_once!("found entity {:?} with a TextSpan that is part of a broken hierarchy with a Parent \ - component that points at non-existent entity {:?}; this warning only prints once", - entity, parent); + once!(warn!("found entity {} with a TextSpan that is part of a broken hierarchy with a Parent \ + component that points at non-existent entity {}; this warning only prints once", + entity, parent)); break; }; if let Some(mut computed) = maybe_computed { @@ -486,18 +527,18 @@ pub fn detect_text_needs_rerender( break; } if !has_span { - warn_once!("found entity {:?} with a TextSpan that has an ancestor ({}) that does not have a text \ + once!(warn!("found entity {} with a TextSpan that has an ancestor ({}) that does not have a text \ span component or a ComputedTextBlock component; this warning only prints once", - entity, parent); + entity, parent)); break; } let Some(next_parent) = maybe_parent else { - warn_once!( - "found entity {:?} with a TextSpan that has no ancestor with the root text \ + once!(warn!( + "found entity {} with a TextSpan that has no ancestor with the root text \ component ({}); this warning only prints once", entity, core::any::type_name::() - ); + )); break; }; parent = **next_parent; diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index f0e1a9fa441ac..062532e377f88 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -7,6 +7,7 @@ use crate::{ use bevy_asset::Assets; use bevy_color::LinearRgba; use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::entity::EntityHashSet; use bevy_ecs::{ change_detection::{DetectChanges, Ref}, component::{require, Component}, @@ -15,34 +16,21 @@ use bevy_ecs::{ query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; -use bevy_image::Image; +use bevy_image::prelude::*; use bevy_math::Vec2; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_render::sync_world::TemporaryRenderEntity; -use bevy_render::view::Visibility; +use bevy_render::view::{self, Visibility, VisibilityClass}; use bevy_render::{ primitives::Aabb, view::{NoFrustumCulling, ViewVisibility}, Extract, }; -use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, SpriteSource, TextureAtlasLayout}; +use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, Sprite}; use bevy_transform::components::Transform; use bevy_transform::prelude::GlobalTransform; -use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window}; -/// [`Text2dBundle`] was removed in favor of required components. -/// The core component is now [`Text2d`] which can contain a single text segment. -/// Indexed access to segments can be done with the new [`Text2dReader`] and [`Text2dWriter`] system params. -/// Additional segments can be added through children with [`TextSpan`](crate::text::TextSpan). -/// Text configuration can be done with [`TextLayout`], [`TextFont`] and [`TextColor`], -/// while sprite-related configuration uses [`TextBounds`] and [`Anchor`] components. -#[deprecated( - since = "0.15.0", - note = "Text2dBundle has been migrated to required components. Follow the documentation for more information." -)] -pub struct Text2dBundle {} - /// The top-level 2D text component. /// /// Adding `Text2d` to an entity will pull in required components for setting up 2d text. @@ -94,10 +82,11 @@ pub struct Text2dBundle {} TextColor, TextBounds, Anchor, - SpriteSource, Visibility, + VisibilityClass, Transform )] +#[component(on_add = view::add_visibility_class::)] pub struct Text2d(pub String); impl Text2d { @@ -232,11 +221,10 @@ pub fn extract_text2d_sprite( /// /// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. -#[allow(clippy::too_many_arguments)] pub fn update_text2d_layout( - mut last_scale_factor: Local, + mut last_scale_factor: Local>, // Text items which should be reprocessed again, generally when the font hasn't loaded yet. - mut queue: Local>, + mut queue: Local, mut textures: ResMut>, fonts: Res>, windows: Query<&Window, With>, @@ -257,19 +245,21 @@ pub fn update_text2d_layout( // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows .get_single() + .ok() .map(|window| window.resolution.scale_factor()) - .unwrap_or(1.0); + .or(*last_scale_factor) + .unwrap_or(1.); let inverse_scale_factor = scale_factor.recip(); - let factor_changed = *last_scale_factor != scale_factor; - *last_scale_factor = scale_factor; + let factor_changed = *last_scale_factor != Some(scale_factor); + *last_scale_factor = Some(scale_factor); for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query { if factor_changed || computed.needs_rerender() || bounds.is_changed() - || queue.remove(&entity) + || (!queue.is_empty() && queue.remove(&entity)) { let text_bounds = TextBounds { width: if block.linebreak == LineBreak::NoWrap { diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index e12b04423c463..1738cdcdff795 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_time" -version = "0.15.0-dev" +version = "0.16.0-dev" edition = "2021" description = "Provides time functionality for Bevy Engine" homepage = "https://bevyengine.org" @@ -14,18 +14,19 @@ serialize = ["serde"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } -bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", features = [ +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", features = [ "bevy_reflect", ] } -bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", ], optional = true } -bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } # other crossbeam-channel = "0.5.0" serde = { version = "1", features = ["derive"], optional = true } +tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] workspace = true diff --git a/crates/bevy_time/src/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index e00acf8cffb8b..bb9e666319a08 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -1,6 +1,6 @@ use crate::{Real, Time, Timer, TimerMode, Virtual}; use bevy_ecs::system::Res; -use bevy_utils::Duration; +use core::time::Duration; /// Run condition that is active on a regular time interval, using [`Time`] to advance /// the timer. The timer ticks at the rate of [`Time::relative_speed`]. @@ -8,7 +8,7 @@ use bevy_utils::Duration; /// ```no_run /// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, Update}; /// # use bevy_ecs::schedule::IntoSystemConfigs; -/// # use bevy_utils::Duration; +/// # use core::time::Duration; /// # use bevy_time::common_conditions::on_timer; /// fn main() { /// App::new() @@ -48,7 +48,7 @@ pub fn on_timer(duration: Duration) -> impl FnMut(Res