diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 432363bdcc764..cd44ff1615d12 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,9 +6,10 @@ labels: C-Bug, S-Needs-Triage assignees: '' --- -## Bevy version +## Bevy version and features -The release number or commit hash of the version you're using. +- The release number or commit hash of the version you're using. +- If you're not using default features, the combination of bevy's cargo features you are using. ## \[Optional\] Relevant system information diff --git a/.github/workflows/action-on-PR-labeled.yml b/.github/workflows/action-on-PR-labeled.yml index 9e5835c1f79ea..536977d752d1b 100644 --- a/.github/workflows/action-on-PR-labeled.yml +++ b/.github/workflows/action-on-PR-labeled.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: github.event.label.name == 'M-Needs-Migration-Guide' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 2 @@ -46,7 +46,7 @@ jobs: runs-on: ubuntu-latest if: github.event.label.name == 'M-Needs-Release-Note' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 2 @@ -70,5 +70,5 @@ jobs: repo: context.repo.repo, body: `It looks like your PR has been selected for a highlight in the next release blog post, but **you didn't provide a release note**. - Please review the [instructions for writing release notes](https://github.com/bevyengine/bevy/tree/main/release-content/release_notes.md), then expand or revise the content in the [release notes directory](https://github.com/bevyengine/bevy/tree/main/release-content/release_notes) to showcase your changes.` + Please review the [instructions for writing release notes](https://github.com/bevyengine/bevy/tree/main/release-content/release_notes.md), then expand or revise the content in the [release notes directory](https://github.com/bevyengine/bevy/tree/main/release-content/release-notes) to showcase your changes.` }) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ad9a1ce76777..80775fc5c7fca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -92,7 +92,7 @@ jobs: runs-on: macos-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -128,7 +128,7 @@ jobs: timeout-minutes: 30 needs: ci steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -157,7 +157,7 @@ jobs: timeout-minutes: 30 needs: ci steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -185,7 +185,7 @@ jobs: timeout-minutes: 30 needs: ci steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -213,7 +213,7 @@ jobs: timeout-minutes: 30 needs: ci steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -241,7 +241,7 @@ jobs: timeout-minutes: 30 needs: build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -265,12 +265,11 @@ jobs: run: cargo check --target wasm32-unknown-unknown build-wasm-atomics: - if: ${{ false }} # Disabled temporarily due to https://github.com/rust-lang/rust/issues/145101 runs-on: ubuntu-latest timeout-minutes: 30 needs: build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -301,7 +300,7 @@ jobs: needs: check-missing-features-in-docs if: always() steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: # Full git history is needed to get a proper list of changed files within `super-linter` fetch-depth: 0 @@ -317,7 +316,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 # Update in sync with BINSTALL_VERSION - uses: cargo-bins/cargo-binstall@v1.14.1 - name: Install taplo @@ -338,9 +337,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check for typos - uses: crate-ci/typos@v1.34.0 + uses: crate-ci/typos@v1.35.5 - name: Typos info if: failure() run: | @@ -354,7 +353,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/cache/restore@v4 with: # key won't match, will rely on restore-keys @@ -392,7 +391,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: check for missing metadata id: missing-metadata @@ -427,7 +426,7 @@ jobs: timeout-minutes: 30 needs: check-missing-examples-in-docs steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: check for missing features id: missing-features @@ -462,7 +461,7 @@ jobs: timeout-minutes: 30 needs: build steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: get MSRV id: msrv @@ -506,7 +505,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Check for internal Bevy imports shell: bash run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2983d98119bff..f82dedc81d4f0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,7 +39,7 @@ jobs: build-mode: none steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml index 84c6852f86eb1..53bceffaf99a8 100644 --- a/.github/workflows/dependencies.yml +++ b/.github/workflows/dependencies.yml @@ -26,7 +26,7 @@ jobs: check-advisories: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Install cargo-deny run: cargo install cargo-deny @@ -36,7 +36,7 @@ jobs: check-bans: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Install cargo-deny run: cargo install cargo-deny @@ -46,7 +46,7 @@ jobs: check-licenses: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Install cargo-deny run: cargo install cargo-deny @@ -56,7 +56,7 @@ jobs: check-sources: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Install cargo-deny run: cargo install cargo-deny diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e732cbe66df5d..a5cced1c9ac96 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: diff --git a/.github/workflows/example-run-report.yml b/.github/workflows/example-run-report.yml index fbde12411fc57..32696670704f7 100644 --- a/.github/workflows/example-run-report.yml +++ b/.github/workflows/example-run-report.yml @@ -86,7 +86,7 @@ jobs: needs: [make-macos-screenshots-available, compare-macos-screenshots] if: ${{ always() && needs.compare-macos-screenshots.result == 'failure' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: "Check if PR already has label" id: check-label env: diff --git a/.github/workflows/example-run.yml b/.github/workflows/example-run.yml index b25468bba46df..2f812fae9cbba 100644 --- a/.github/workflows/example-run.yml +++ b/.github/workflows/example-run.yml @@ -20,10 +20,10 @@ env: jobs: run-examples-macos-metal: - runs-on: macos-latest + runs-on: macos-14 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Disable audio # Disable audio through a patch. on github m1 runners, audio timeouts after 15 minutes @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install Linux dependencies uses: ./.github/actions/install-linux-deps # At some point this may be merged into `install-linux-deps`, but for now it is its own step. @@ -164,7 +164,7 @@ jobs: runs-on: windows-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: actions/cache/restore@v4 with: diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml index 485861ebdfa0f..a2082117c6ccc 100644 --- a/.github/workflows/post-release.yml +++ b/.github/workflows/post-release.yml @@ -15,7 +15,7 @@ jobs: contents: write pull-requests: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Install cargo-release run: cargo install cargo-release diff --git a/.github/workflows/send-screenshots-to-pixeleagle.yml b/.github/workflows/send-screenshots-to-pixeleagle.yml index 3a8ac2c1393cf..bff3505237472 100644 --- a/.github/workflows/send-screenshots-to-pixeleagle.yml +++ b/.github/workflows/send-screenshots-to-pixeleagle.yml @@ -51,7 +51,7 @@ jobs: branch: ${{ inputs.branch }} run: | # Create a new run with its associated metadata - metadata='{"os":"${{ inputs.os }}", "commit": "${{ inputs.commit }}", "branch": "$branch"}' + metadata='{"os":"${{ inputs.os }}", "commit": "${{ inputs.commit }}", "branch": "'$branch'"}' run=`curl https://pixel-eagle.com/$project/runs --json "$metadata" --oauth2-bearer ${{ secrets.PIXELEAGLE_TOKEN }} | jq '.id'` SAVEIFS=$IFS diff --git a/.github/workflows/update-caches.yml b/.github/workflows/update-caches.yml index 3935030eccc0f..5caf458546a4a 100644 --- a/.github/workflows/update-caches.yml +++ b/.github/workflows/update-caches.yml @@ -29,7 +29,7 @@ jobs: NIGHTLY_TOOLCHAIN: ${{ steps.env.outputs.NIGHTLY_TOOLCHAIN }} MSRV: ${{ steps.msrv.outputs.MSRV }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: get MSRV id: msrv @@ -81,7 +81,7 @@ jobs: - os: ubuntu-latest toolchain: stable target: aarch64-linux-android - - os: macos-latest + - os: macos-14 toolchain: stable target: aarch64-apple-ios-sim @@ -94,7 +94,7 @@ jobs: shell: bash - name: Checkout Bevy main branch - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: "bevyengine/bevy" ref: "main" diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index 2e4575e384a8f..333bf9824d642 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -26,10 +26,10 @@ env: jobs: build-and-install-on-iOS: if: ${{ github.event_name == 'merge_group' }} - runs-on: macos-latest + runs-on: macos-14 timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable @@ -59,12 +59,12 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: "17" distribution: "temurin" @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: @@ -161,7 +161,7 @@ jobs: crate: [bevy_ecs, bevy_reflect, bevy] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: Install Linux dependencies uses: ./.github/actions/install-linux-deps @@ -189,7 +189,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.NIGHTLY_TOOLCHAIN }} @@ -219,7 +219,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: actions/cache/restore@v4 with: diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index 3a1cf2a76db30..71b10e04e14f0 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -42,7 +42,7 @@ jobs: if: github.repository == 'bevyengine/bevy' timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@beta - name: Install Linux dependencies uses: ./.github/actions/install-linux-deps @@ -58,7 +58,7 @@ jobs: if: github.repository == 'bevyengine/bevy' timeout-minutes: 30 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@beta with: components: rustfmt, clippy @@ -78,7 +78,7 @@ jobs: timeout-minutes: 30 needs: test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@beta - name: Install Linux dependencies uses: ./.github/actions/install-linux-deps diff --git a/.gitignore b/.gitignore index 0d39edea49083..cdde3c7eec3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ Cargo.lock assets/**/*.meta crates/bevy_asset/imported_assets imported_assets +.web-asset-cache # Bevy Examples example_showcase_config.ron diff --git a/Cargo.toml b/Cargo.toml index 085e3732c1a6e..257d8d628b307 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,8 +73,6 @@ allow_attributes = "warn" allow_attributes_without_reason = "warn" [workspace.lints.rust] -# Strictly temporary until encase fixes dead code generation from ShaderType macros -dead_code = "allow" missing_docs = "warn" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" @@ -120,8 +118,6 @@ allow_attributes = "warn" allow_attributes_without_reason = "warn" [lints.rust] -# Strictly temporary until encase fixes dead code generation from ShaderType macros -dead_code = "allow" missing_docs = "warn" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" @@ -139,8 +135,9 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_post_process", "bevy_core_widgets", - "bevy_anti_aliasing", + "bevy_anti_alias", "bevy_gilrs", "bevy_gizmos", "bevy_gltf", @@ -151,8 +148,14 @@ default = [ "bevy_picking", "bevy_render", "bevy_scene", + "bevy_image", + "bevy_mesh", + "bevy_camera", + "bevy_light", + "bevy_shader", "bevy_sprite", "bevy_sprite_picking_backend", + "bevy_sprite_render", "bevy_state", "bevy_text", "bevy_ui", @@ -163,6 +166,7 @@ default = [ "custom_cursor", "default_font", "hdr", + "ktx2", "multi_threaded", "png", "reflect_auto_register", @@ -181,22 +185,13 @@ default = [ default_no_std = ["libm", "critical-section", "bevy_color", "bevy_state"] # Provides an implementation for picking meshes -bevy_mesh_picking_backend = [ - "bevy_picking", - "bevy_internal/bevy_mesh_picking_backend", -] +bevy_mesh_picking_backend = ["bevy_internal/bevy_mesh_picking_backend"] # Provides an implementation for picking sprites -bevy_sprite_picking_backend = [ - "bevy_picking", - "bevy_internal/bevy_sprite_picking_backend", -] +bevy_sprite_picking_backend = ["bevy_internal/bevy_sprite_picking_backend"] # Provides an implementation for picking UI -bevy_ui_picking_backend = [ - "bevy_picking", - "bevy_internal/bevy_ui_picking_backend", -] +bevy_ui_picking_backend = ["bevy_internal/bevy_ui_picking_backend"] # Provides a debug overlay for bevy UI bevy_ui_debug = ["bevy_internal/bevy_ui_debug"] @@ -208,7 +203,7 @@ dynamic_linking = ["dep:bevy_dylib", "bevy_internal/dynamic_linking"] sysinfo_plugin = ["bevy_internal/sysinfo_plugin"] # Provides animation functionality -bevy_animation = ["bevy_internal/bevy_animation", "bevy_color"] +bevy_animation = ["bevy_internal/bevy_animation"] # Provides asset functionality bevy_asset = ["bevy_internal/bevy_asset"] @@ -220,88 +215,73 @@ bevy_audio = ["bevy_internal/bevy_audio"] bevy_color = ["bevy_internal/bevy_color"] # Provides cameras and other basic render pipeline features -bevy_core_pipeline = [ - "bevy_internal/bevy_core_pipeline", - "bevy_asset", - "bevy_render", -] +bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"] + +# Provides post process effects such as depth of field, bloom, chromatic aberration. +bevy_post_process = ["bevy_internal/bevy_post_process"] # Provides various anti aliasing solutions -bevy_anti_aliasing = [ - "bevy_internal/bevy_anti_aliasing", - "bevy_asset", - "bevy_render", -] +bevy_anti_alias = ["bevy_internal/bevy_anti_alias"] # Adds gamepad support bevy_gilrs = ["bevy_internal/bevy_gilrs"] # [glTF](https://www.khronos.org/gltf/) support -bevy_gltf = ["bevy_internal/bevy_gltf", "bevy_asset", "bevy_scene", "bevy_pbr"] +bevy_gltf = ["bevy_internal/bevy_gltf"] # Adds PBR rendering -bevy_pbr = [ - "bevy_internal/bevy_pbr", - "bevy_asset", - "bevy_render", - "bevy_core_pipeline", - "bevy_anti_aliasing", -] +bevy_pbr = ["bevy_internal/bevy_pbr"] # Provides picking functionality bevy_picking = ["bevy_internal/bevy_picking"] # Provides rendering functionality -bevy_render = ["bevy_internal/bevy_render", "bevy_color"] +bevy_render = ["bevy_internal/bevy_render"] # Provides scene functionality -bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +bevy_scene = ["bevy_internal/bevy_scene"] # Provides raytraced lighting (experimental) -bevy_solari = [ - "bevy_internal/bevy_solari", - "bevy_asset", - "bevy_core_pipeline", - "bevy_pbr", - "bevy_render", -] +bevy_solari = ["bevy_internal/bevy_solari"] # Provides sprite functionality -bevy_sprite = [ - "bevy_internal/bevy_sprite", - "bevy_render", - "bevy_core_pipeline", - "bevy_color", - "bevy_anti_aliasing", -] +bevy_sprite = ["bevy_internal/bevy_sprite"] + +# Provides sprite rendering functionality +bevy_sprite_render = ["bevy_internal/bevy_sprite_render"] # Provides text functionality -bevy_text = ["bevy_internal/bevy_text", "bevy_asset", "bevy_sprite"] +bevy_text = ["bevy_internal/bevy_text"] # A custom ECS-driven UI framework -bevy_ui = [ - "bevy_internal/bevy_ui", - "bevy_core_pipeline", - "bevy_text", - "bevy_sprite", - "bevy_color", - "bevy_anti_aliasing", -] +bevy_ui = ["bevy_internal/bevy_ui"] # Provides rendering functionality for bevy_ui -bevy_ui_render = ["bevy_internal/bevy_ui_render", "bevy_render", "bevy_ui"] +bevy_ui_render = ["bevy_internal/bevy_ui_render"] # Windowing layer bevy_window = ["bevy_internal/bevy_window"] # winit window and input backend -bevy_winit = ["bevy_internal/bevy_winit", "bevy_window"] +bevy_winit = ["bevy_internal/bevy_winit"] # Load and access image data. Usually added by an image format bevy_image = ["bevy_internal/bevy_image"] +# Provides a mesh format and some primitive meshing routines. +bevy_mesh = ["bevy_internal/bevy_mesh"] + +# Provides camera and visibility types, as well as culling primitives. +bevy_camera = ["bevy_internal/bevy_camera"] + +# Provides light types such as point lights, directional lights, spotlights. +bevy_light = ["bevy_internal/bevy_light"] + +# Provides shaders usable through asset handles. +bevy_shader = ["bevy_internal/bevy_shader"] + # Adds support for rendering gizmos -bevy_gizmos = ["bevy_internal/bevy_gizmos", "bevy_color"] +bevy_gizmos = ["bevy_internal/bevy_gizmos"] # Provides a collection of developer tools bevy_dev_tools = ["bevy_internal/bevy_dev_tools"] @@ -327,6 +307,9 @@ spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] # Statically linked DXC shader compiler for DirectX 12 statically-linked-dxc = ["bevy_internal/statically-linked-dxc"] +# Forces the wgpu instance to be initialized using the raw Vulkan HAL, enabling additional configuration +raw_vulkan_init = ["bevy_internal/raw_vulkan_init"] + # Tracing support, saving a file in Chrome Tracing format trace_chrome = ["trace", "bevy_internal/trace_chrome"] @@ -334,11 +317,7 @@ trace_chrome = ["trace", "bevy_internal/trace_chrome"] trace_tracy = ["trace", "bevy_internal/trace_tracy"] # Tracing support, with memory profiling, exposing a port for Tracy -trace_tracy_memory = [ - "trace", - "bevy_internal/trace_tracy", - "bevy_internal/trace_tracy_memory", -] +trace_tracy_memory = ["bevy_internal/trace_tracy_memory"] # Tracing support trace = ["bevy_internal/trace", "dep:tracing"] @@ -467,11 +446,20 @@ android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"] detailed_trace = ["bevy_internal/detailed_trace"] # Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method for your `Camera2d` or `Camera3d`. -tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "bevy_image/zstd"] +tonemapping_luts = ["bevy_internal/tonemapping_luts"] # Include SMAA Look Up Tables KTX2 Files smaa_luts = ["bevy_internal/smaa_luts"] +# Include spatio-temporal blue noise KTX2 file used by generated environment maps, Solari and atmosphere +bluenoise_texture = ["bevy_internal/bluenoise_texture"] + +# NVIDIA Deep Learning Super Sampling +dlss = ["bevy_internal/dlss"] + +# Forcibly disable DLSS so that cargo build --all-features works without the DLSS SDK being installed. Not meant for users. +force_disable_dlss = ["bevy_internal/force_disable_dlss"] + # Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.) accesskit_unix = ["bevy_internal/accesskit_unix"] @@ -531,6 +519,15 @@ file_watcher = ["bevy_internal/file_watcher"] # Enables watching in memory asset providers for Bevy Asset hot-reloading embedded_watcher = ["bevy_internal/embedded_watcher"] +# Enables downloading assets from HTTP sources. Warning: there are security implications. Read the docs on WebAssetPlugin. +http = ["bevy_internal/http"] + +# Enables downloading assets from HTTPS sources. Warning: there are security implications. Read the docs on WebAssetPlugin. +https = ["bevy_internal/https"] + +# Enable caching downloaded assets on the filesystem. NOTE: this cache currently never invalidates entries! +web_asset_cache = ["bevy_internal/web_asset_cache"] + # Enable stepping-based debugging of Bevy systems bevy_debug_stepping = [ "bevy_internal/bevy_debug_stepping", @@ -585,21 +582,9 @@ web = ["bevy_internal/web"] # Enable hotpatching of Bevy systems hotpatching = ["bevy_internal/hotpatching"] -# Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18. -gltf_convert_coordinates_default = [ - "bevy_internal/gltf_convert_coordinates_default", -] - # Enable collecting debug information about systems and components to help with diagnostics debug = ["bevy_internal/debug"] -# Include spatio-temporal blue noise KTX2 file used by generated environment maps, Solari and atmosphere -bluenoise_texture = [ - "bevy_internal/bluenoise_texture", - "ktx2", - "bevy_image/zstd", -] - [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.17.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } @@ -616,8 +601,8 @@ flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" bytemuck = "1" -bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. +bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false } bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false } bevy_state = { path = "crates/bevy_state", version = "0.17.0-dev", default-features = false } bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false } @@ -626,7 +611,7 @@ bevy_image = { path = "crates/bevy_image", version = "0.17.0-dev", default-featu bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.17.0-dev", default-features = false } # Needed to poll Task examples futures-lite = "2.0.1" -async-std = "1.13" +futures-timer = { version = "3", features = ["wasm-bindgen", "gloo-timers"] } crossbeam-channel = "0.5.0" argh = "0.1.12" thiserror = "2.0" @@ -874,6 +859,17 @@ description = "Generates text in 2D" category = "2D Rendering" wasm = true +[[example]] +name = "multi_window_text" +path = "examples/window/multi_window_text.rs" +doc-scrape-examples = true + +[package.metadata.example.multi_window_text] +name = "Multi-Window Text" +description = "Renders text to multiple windows with different scale factors using both Text and Text2d" +category = "2D Rendering" +wasm = true + [[example]] name = "text_input_2d" path = "examples/2d/text_input_2d.rs" @@ -1038,7 +1034,7 @@ doc-scrape-examples = true [package.metadata.example.anti_aliasing] name = "Anti-aliasing" -description = "Compares different anti-aliasing methods" +description = "Compares different anti-aliasing techniques supported by Bevy" category = "3D Rendering" # TAA not supported by WebGL wasm = false @@ -1359,7 +1355,7 @@ wasm = true name = "solari" path = "examples/3d/solari.rs" doc-scrape-examples = true -required-features = ["bevy_solari"] +required-features = ["bevy_solari", "https"] [package.metadata.example.solari] name = "Solari" @@ -1484,7 +1480,7 @@ wasm = false name = "meshlet" path = "examples/3d/meshlet.rs" doc-scrape-examples = true -required-features = ["meshlet"] +required-features = ["meshlet", "https"] [package.metadata.example.meshlet] name = "Meshlet" @@ -1492,19 +1488,6 @@ description = "Meshlet rendering for dense high-poly scenes (experimental)" category = "3D Rendering" # Requires compute shaders and WGPU extensions, not supported by WebGL nor WebGPU. wasm = false -setup = [ - [ - "mkdir", - "-p", - "assets/external/models", - ], - [ - "curl", - "-o", - "assets/external/models/bunny.meshlet_mesh", - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/7a7c14138021f63904b584d5f7b73b695c7f4bbf/bunny.meshlet_mesh", - ], -] [[example]] name = "mesh_ray_cast" @@ -1939,12 +1922,24 @@ path = "examples/asset/extra_source.rs" doc-scrape-examples = true [package.metadata.example.extra_asset_source] -name = "Extra asset source" +name = "Extra Asset Source" description = "Load an asset from a non-standard asset source" category = "Assets" # Uses non-standard asset path wasm = false +[[example]] +name = "web_asset" +path = "examples/asset/web_asset.rs" +doc-scrape-examples = true +required-features = ["https"] + +[package.metadata.example.web_asset] +name = "Web Asset" +description = "Load an asset from the web" +category = "Assets" +wasm = true + [[example]] name = "hot_asset_reloading" path = "examples/asset/hot_asset_reloading.rs" @@ -3402,6 +3397,17 @@ description = "Illustrates creating and updating a button" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_drag_and_drop" +path = "examples/ui/ui_drag_and_drop.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_drag_and_drop] +name = "UI Drag and Drop" +description = "Demonstrates dragging and dropping UI nodes" +category = "UI (User Interface)" +wasm = true + [[example]] name = "display_and_visibility" path = "examples/ui/display_and_visibility.rs" @@ -3713,6 +3719,17 @@ description = "An example demonstrating how to translate, rotate and scale UI el category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_target_camera" +path = "examples/ui/ui_target_camera.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_target_camera] +name = "UI Target Camera" +description = "Demonstrates how to use `UiTargetCamera` and camera ordering." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -4675,3 +4692,14 @@ description = "Gallery of Feathers Widgets" category = "UI (User Interface)" wasm = true hidden = true + +[[example]] +name = "render_depth_to_texture" +path = "examples/shader_advanced/render_depth_to_texture.rs" +doc-scrape-examples = true + +[package.metadata.example.render_depth_to_texture] +name = "Render Depth to Texture" +description = "Demonstrates how to use depth-only cameras" +category = "Shaders" +wasm = true diff --git a/README.md b/README.md index 9e9d49a5a37ec..73cc0e7a8d51c 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ To draw a window with standard functionality enabled, use: use bevy::prelude::*; fn main() { - App::new() - .add_plugins(DefaultPlugins) - .run(); + App::new() + .add_plugins(DefaultPlugins) + .run(); } ``` diff --git a/assets/shaders/game_of_life.wgsl b/assets/shaders/game_of_life.wgsl index 0eb5e32e6ec56..7ad264fc33f7a 100644 --- a/assets/shaders/game_of_life.wgsl +++ b/assets/shaders/game_of_life.wgsl @@ -4,9 +4,15 @@ // Two textures are needed for the game of life as each pixel of step N depends on the state of its // neighbors at step N-1. -@group(0) @binding(0) var input: texture_storage_2d; +@group(0) @binding(0) var input: texture_storage_2d; -@group(0) @binding(1) var output: texture_storage_2d; +@group(0) @binding(1) var output: texture_storage_2d; + +@group(0) @binding(2) var config: GameOfLifeUniforms; + +struct GameOfLifeUniforms { + alive_color: vec4, +} fn hash(value: u32) -> u32 { var state = value; @@ -29,14 +35,15 @@ fn init(@builtin(global_invocation_id) invocation_id: vec3, @builtin(num_wo let randomNumber = randomFloat((invocation_id.y << 16u) | invocation_id.x); let alive = randomNumber > 0.9; - let color = vec4(f32(alive)); + // Use alpha channel to keep track of cell's state + let color = vec4(config.alive_color.rgb, f32(alive)); textureStore(output, location, color); } fn is_alive(location: vec2, offset_x: i32, offset_y: i32) -> i32 { let value: vec4 = textureLoad(input, location + vec2(offset_x, offset_y)); - return i32(value.x); + return i32(value.a); } fn count_alive(location: vec2) -> i32 { @@ -65,7 +72,7 @@ fn update(@builtin(global_invocation_id) invocation_id: vec3) { } else { alive = false; } - let color = vec4(f32(alive)); + let color = vec4(config.alive_color.rgb, f32(alive)); textureStore(output, location, color); } diff --git a/assets/shaders/show_depth_texture_material.wgsl b/assets/shaders/show_depth_texture_material.wgsl new file mode 100644 index 0000000000000..693f9226fe6eb --- /dev/null +++ b/assets/shaders/show_depth_texture_material.wgsl @@ -0,0 +1,11 @@ +#import bevy_pbr::forward_io::VertexOutput + +@group(#{MATERIAL_BIND_GROUP}) @binding(0) var depth_texture: texture_depth_2d; +@group(#{MATERIAL_BIND_GROUP}) @binding(1) var depth_sampler: sampler_comparison; + +@fragment +fn fragment(input: VertexOutput) -> @location(0) vec4 { + // Just draw the depth. + let st = vec2(input.uv * vec2(textureDimensions(depth_texture).xy)); + return vec4(vec3(textureLoad(depth_texture, st, 0)), 1.0); +} \ No newline at end of file diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 2622ecdddd16c..3b862cc1a631f 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -113,6 +113,6 @@ fn add_listeners_to_hierarchy( } } -fn empty_listener(trigger: On>) { - black_box(trigger); +fn empty_listener(event: On>) { + black_box(event); } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 29ade4d2d1032..a4915e6afa807 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -46,8 +46,8 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(trigger: On) { - black_box(trigger); +fn empty_listener_base(event: On) { + black_box(event); } fn send_base_event(world: &mut World, entities: impl TriggerTargets) { diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index e9fd0caf9f1e0..2f49a7a9145e7 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -2,7 +2,7 @@ use core::hint::black_box; use std::time::Duration; use benches::bench; -use bevy_math::{Dir3, Mat4, Ray3d, Vec3}; +use bevy_math::{Affine3A, Dir3, Ray3d, Vec3}; use bevy_picking::mesh_picking::ray_cast::{self, Backfaces}; use criterion::{criterion_group, AxisScale, BenchmarkId, Criterion, PlotConfiguration}; @@ -103,8 +103,8 @@ impl Benchmarks { ) } - fn mesh_to_world(&self) -> Mat4 { - Mat4::IDENTITY + fn mesh_to_world(&self) -> Affine3A { + Affine3A::IDENTITY } fn backface_culling(&self) -> Backfaces { diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 593ca04d2e025..1493f604b6ca7 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -353,7 +353,7 @@ impl WideCubicKeyframeCurve { let values: Vec = values.into_iter().collect(); let divisor = times.len() * 3; - if values.len() % divisor != 0 { + if !values.len().is_multiple_of(divisor) { return Err(WideKeyframeCurveError::LengthMismatch { values_given: values.len(), divisor, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index a848ee47f8d2f..386eb614be3a3 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1245,7 +1245,7 @@ impl Plugin for AnimationPlugin { // `PostUpdate`. For now, we just disable ambiguity testing // for this system. animate_targets - .before(bevy_mesh::InheritWeights) + .before(bevy_mesh::InheritWeightSystems) .ambiguous_with_all(), trigger_untargeted_animation_events, expire_completed_transitions, diff --git a/crates/bevy_anti_aliasing/Cargo.toml b/crates/bevy_anti_alias/Cargo.toml similarity index 83% rename from crates/bevy_anti_aliasing/Cargo.toml rename to crates/bevy_anti_alias/Cargo.toml index 663d168b88535..57150ecdf363d 100644 --- a/crates/bevy_anti_aliasing/Cargo.toml +++ b/crates/bevy_anti_alias/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bevy_anti_aliasing" +name = "bevy_anti_alias" version = "0.17.0-dev" edition = "2024" description = "Provides various anti aliasing implementations for Bevy Engine" @@ -12,7 +12,9 @@ keywords = ["bevy"] trace = [] webgl = [] webgpu = [] -smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] +smaa_luts = ["bevy_image/ktx2", "bevy_image/zstd"] +dlss = ["dep:dlss_wgpu", "dep:uuid", "bevy_render/raw_vulkan_init"] +force_disable_dlss = ["dlss_wgpu?/mock"] [dependencies] # bevy @@ -32,6 +34,8 @@ bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" } # other tracing = { version = "0.1", default-features = false, features = ["std"] } +dlss_wgpu = { version = "1", optional = true } +uuid = { version = "1", optional = true } [lints] workspace = true diff --git a/crates/bevy_anti_aliasing/LICENSE-APACHE b/crates/bevy_anti_alias/LICENSE-APACHE similarity index 100% rename from crates/bevy_anti_aliasing/LICENSE-APACHE rename to crates/bevy_anti_alias/LICENSE-APACHE diff --git a/crates/bevy_anti_aliasing/LICENSE-MIT b/crates/bevy_anti_alias/LICENSE-MIT similarity index 100% rename from crates/bevy_anti_aliasing/LICENSE-MIT rename to crates/bevy_anti_alias/LICENSE-MIT diff --git a/crates/bevy_anti_aliasing/README.md b/crates/bevy_anti_alias/README.md similarity index 96% rename from crates/bevy_anti_aliasing/README.md rename to crates/bevy_anti_alias/README.md index ba0123c31bc44..4b0a030dd2199 100644 --- a/crates/bevy_anti_aliasing/README.md +++ b/crates/bevy_anti_alias/README.md @@ -1,4 +1,4 @@ -# Bevy Anti Aliasing +# Bevy Anti Alias [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) [![Crates.io](https://img.shields.io/crates/v/bevy_core_pipeline.svg)](https://crates.io/crates/bevy_core_pipeline) diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_alias/src/contrast_adaptive_sharpening/mod.rs similarity index 99% rename from crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs rename to crates/bevy_anti_alias/src/contrast_adaptive_sharpening/mod.rs index fd10e3b7b91a0..43790dc393520 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_alias/src/contrast_adaptive_sharpening/mod.rs @@ -98,6 +98,7 @@ impl ExtractComponent for ContrastAdaptiveSharpening { } /// Adds Support for Contrast Adaptive Sharpening (CAS). +#[derive(Default)] pub struct CasPlugin; impl Plugin for CasPlugin { diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/node.rs b/crates/bevy_anti_alias/src/contrast_adaptive_sharpening/node.rs similarity index 100% rename from crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/node.rs rename to crates/bevy_anti_alias/src/contrast_adaptive_sharpening/node.rs diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl b/crates/bevy_anti_alias/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl similarity index 100% rename from crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl rename to crates/bevy_anti_alias/src/contrast_adaptive_sharpening/robust_contrast_adaptive_sharpening.wgsl diff --git a/crates/bevy_anti_alias/src/dlss/extract.rs b/crates/bevy_anti_alias/src/dlss/extract.rs new file mode 100644 index 0000000000000..c8af2dbd25720 --- /dev/null +++ b/crates/bevy_anti_alias/src/dlss/extract.rs @@ -0,0 +1,28 @@ +use super::{prepare::DlssRenderContext, Dlss, DlssFeature}; +use bevy_camera::{Camera, MainPassResolutionOverride, Projection}; +use bevy_ecs::{ + query::{Has, With}, + system::{Commands, Query, ResMut}, +}; +use bevy_render::{sync_world::RenderEntity, view::Hdr, MainWorld}; + +pub fn extract_dlss( + mut commands: Commands, + mut main_world: ResMut, + cleanup_query: Query>>, +) { + let mut cameras_3d = main_world + .query_filtered::<(RenderEntity, &Camera, &Projection, Option<&mut Dlss>), With>(); + + for (entity, camera, camera_projection, mut dlss) in cameras_3d.iter_mut(&mut main_world) { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if dlss.is_some() && camera.is_active && camera_projection.is_perspective() { + entity_commands.insert(dlss.as_deref().unwrap().clone()); + dlss.as_mut().unwrap().reset = false; + } else if cleanup_query.get(entity) == Ok(true) { + entity_commands.remove::<(Dlss, DlssRenderContext, MainPassResolutionOverride)>(); + } + } +} diff --git a/crates/bevy_anti_alias/src/dlss/mod.rs b/crates/bevy_anti_alias/src/dlss/mod.rs new file mode 100644 index 0000000000000..50586f5d03d02 --- /dev/null +++ b/crates/bevy_anti_alias/src/dlss/mod.rs @@ -0,0 +1,404 @@ +//! NVIDIA Deep Learning Super Sampling (DLSS). +//! +//! DLSS uses machine learning models to upscale and anti-alias images. +//! +//! Requires a NVIDIA RTX GPU, and the Windows/Linux Vulkan rendering backend. Does not work on other platforms. +//! +//! See https://github.com/bevyengine/dlss_wgpu for licensing requirements and setup instructions. +//! +//! # Usage +//! 1. Enable Bevy's `dlss` feature +//! 2. During app setup, insert the `DlssProjectId` resource before `DefaultPlugins` +//! 3. Check for the presence of `Option>` at runtime to see if DLSS is supported on the current machine +//! 4. Add the `Dlss` component to your camera entity, optionally setting a specific `DlssPerfQualityMode` (defaults to `Auto`) +//! 5. Optionally add sharpening via `ContrastAdaptiveSharpening` +//! 6. Custom rendering code, including third party crates, should account for the optional `MainPassResolutionOverride` to work with DLSS (see the `custom_render_phase` example) + +mod extract; +mod node; +mod prepare; + +pub use dlss_wgpu::DlssPerfQualityMode; + +use bevy_app::{App, Plugin}; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DepthPrepass, MotionVectorPrepass}, +}; +use bevy_ecs::prelude::*; +use bevy_math::{UVec2, Vec2}; +use bevy_reflect::{reflect_remote, Reflect}; +use bevy_render::{ + camera::{MipBias, TemporalJitter}, + render_graph::{RenderGraphExt, ViewNodeRunner}, + renderer::{ + raw_vulkan_init::{AdditionalVulkanFeatures, RawVulkanInitSettings}, + RenderDevice, RenderQueue, + }, + texture::CachedTexture, + view::{prepare_view_targets, Hdr}, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use dlss_wgpu::{ + ray_reconstruction::{ + DlssRayReconstruction, DlssRayReconstructionDepthMode, DlssRayReconstructionRoughnessMode, + }, + super_resolution::DlssSuperResolution, + FeatureSupport, +}; +use std::{ + marker::PhantomData, + ops::Deref, + sync::{Arc, Mutex}, +}; +use tracing::info; +use uuid::Uuid; + +/// Initializes DLSS support in the renderer. This must be registered before [`RenderPlugin`](bevy_render::RenderPlugin) because +/// it configures render init code. +#[derive(Default)] +pub struct DlssInitPlugin; + +impl Plugin for DlssInitPlugin { + #[allow(unsafe_code)] + fn build(&self, app: &mut App) { + let dlss_project_id = app.world().get_resource::() + .expect("The `dlss` feature is enabled, but DlssProjectId was not added to the App before DlssInitPlugin.").0; + let mut raw_vulkan_settings = app + .world_mut() + .get_resource_or_init::(); + + // SAFETY: this does not remove any instance features and only enables features that are supported + unsafe { + raw_vulkan_settings.add_create_instance_callback( + move |mut args, additional_vulkan_features| { + let mut feature_support = FeatureSupport::default(); + match dlss_wgpu::register_instance_extensions( + dlss_project_id, + &mut args, + &mut feature_support, + ) { + Ok(_) => { + if feature_support.super_resolution_supported { + additional_vulkan_features.insert::(); + } + if feature_support.ray_reconstruction_supported { + additional_vulkan_features + .insert::(); + } + } + Err(_) => {} + } + }, + ); + } + + // SAFETY: this does not remove any device features and only enables features that are supported + unsafe { + raw_vulkan_settings.add_create_device_callback( + move |mut args, adapter, additional_vulkan_features| { + let mut feature_support = FeatureSupport::default(); + match dlss_wgpu::register_device_extensions( + dlss_project_id, + &mut args, + adapter, + &mut feature_support, + ) { + Ok(_) => { + if feature_support.super_resolution_supported { + additional_vulkan_features.insert::(); + } else { + additional_vulkan_features.remove::(); + } + if feature_support.ray_reconstruction_supported { + additional_vulkan_features + .insert::(); + } else { + additional_vulkan_features + .remove::(); + } + } + Err(_) => {} + } + }, + ) + }; + } +} + +/// Enables DLSS support. This requires [`DlssInitPlugin`] to function, which must be manually registered in the correct order +/// prior to registering this plugin. +#[derive(Default)] +pub struct DlssPlugin; + +impl Plugin for DlssPlugin { + fn build(&self, app: &mut App) { + app.register_type::>() + .register_type::>(); + } + + fn finish(&self, app: &mut App) { + let (super_resolution_supported, ray_reconstruction_supported) = { + let features = app + .sub_app_mut(RenderApp) + .world() + .resource::(); + ( + features.has::(), + features.has::(), + ) + }; + if !super_resolution_supported { + return; + } + + let wgpu_device = { + let render_world = app.sub_app(RenderApp).world(); + let render_device = render_world.resource::().wgpu_device(); + render_device.clone() + }; + let project_id = app.world().get_resource::() + .expect("The `dlss` feature is enabled, but DlssProjectId was not added to the App before DlssPlugin."); + let dlss_sdk = dlss_wgpu::DlssSdk::new(project_id.0, wgpu_device); + if dlss_sdk.is_err() { + info!("DLSS is not supported on this system"); + return; + } + + app.insert_resource(DlssSuperResolutionSupported); + if ray_reconstruction_supported { + app.insert_resource(DlssRayReconstructionSupported); + } + + app.sub_app_mut(RenderApp) + .insert_resource(DlssSdk(dlss_sdk.unwrap())) + .add_systems( + ExtractSchedule, + ( + extract::extract_dlss::, + extract::extract_dlss::, + ), + ) + .add_systems( + Render, + ( + prepare::prepare_dlss::, + prepare::prepare_dlss::, + ) + .in_set(RenderSystems::ManageViews) + .before(prepare_view_targets), + ) + .add_render_graph_node::>>( + Core3d, + Node3d::DlssSuperResolution, + ) + .add_render_graph_node::>>( + Core3d, + Node3d::DlssRayReconstruction, + ) + .add_render_graph_edges( + Core3d, + ( + Node3d::EndMainPass, + Node3d::MotionBlur, // Running before DLSS reduces edge artifacts and noise + Node3d::DlssSuperResolution, + Node3d::DlssRayReconstruction, + Node3d::Bloom, + Node3d::Tonemapping, + ), + ); + } +} + +/// Camera component to enable DLSS. +#[derive(Component, Reflect, Clone)] +#[reflect(Component)] +#[require(TemporalJitter, MipBias, DepthPrepass, MotionVectorPrepass, Hdr)] +pub struct Dlss { + /// How much upscaling should be applied. + #[reflect(remote = DlssPerfQualityModeRemoteReflect)] + pub perf_quality_mode: DlssPerfQualityMode, + /// Set to true to delete the saved temporal history (past frames). + /// + /// Useful for preventing ghosting when the history is no longer + /// representative of the current frame, such as in sudden camera cuts. + /// + /// After setting this to true, it will automatically be toggled + /// back to false at the end of the frame. + pub reset: bool, + #[reflect(ignore)] + pub _phantom_data: PhantomData, +} + +impl Default for Dlss { + fn default() -> Self { + Self { + perf_quality_mode: Default::default(), + reset: Default::default(), + _phantom_data: Default::default(), + } + } +} + +pub trait DlssFeature: Reflect + Clone + Default { + type Context: Send; + + fn upscaled_resolution(context: &Self::Context) -> UVec2; + + fn render_resolution(context: &Self::Context) -> UVec2; + + fn suggested_jitter( + context: &Self::Context, + frame_number: u32, + render_resolution: UVec2, + ) -> Vec2; + + fn suggested_mip_bias(context: &Self::Context, render_resolution: UVec2) -> f32; + + fn new_context( + upscaled_resolution: UVec2, + perf_quality_mode: DlssPerfQualityMode, + feature_flags: dlss_wgpu::DlssFeatureFlags, + sdk: Arc>, + device: &RenderDevice, + queue: &RenderQueue, + ) -> Result; +} + +/// DLSS Super Resolution. +/// +/// Only available when the [`DlssSuperResolutionSupported`] resource exists. +#[derive(Reflect, Clone, Default)] +pub struct DlssSuperResolutionFeature; + +impl DlssFeature for DlssSuperResolutionFeature { + type Context = DlssSuperResolution; + + fn upscaled_resolution(context: &Self::Context) -> UVec2 { + context.upscaled_resolution() + } + + fn render_resolution(context: &Self::Context) -> UVec2 { + context.render_resolution() + } + + fn suggested_jitter( + context: &Self::Context, + frame_number: u32, + render_resolution: UVec2, + ) -> Vec2 { + context.suggested_jitter(frame_number, render_resolution) + } + + fn suggested_mip_bias(context: &Self::Context, render_resolution: UVec2) -> f32 { + context.suggested_mip_bias(render_resolution) + } + + fn new_context( + upscaled_resolution: UVec2, + perf_quality_mode: DlssPerfQualityMode, + feature_flags: dlss_wgpu::DlssFeatureFlags, + sdk: Arc>, + device: &RenderDevice, + queue: &RenderQueue, + ) -> Result { + DlssSuperResolution::new( + upscaled_resolution, + perf_quality_mode, + feature_flags, + sdk, + device.wgpu_device(), + queue.deref(), + ) + } +} + +/// DLSS Ray Reconstruction. +/// +/// Only available when the [`DlssRayReconstructionSupported`] resource exists. +#[derive(Reflect, Clone, Default)] +pub struct DlssRayReconstructionFeature; + +impl DlssFeature for DlssRayReconstructionFeature { + type Context = DlssRayReconstruction; + + fn upscaled_resolution(context: &Self::Context) -> UVec2 { + context.upscaled_resolution() + } + + fn render_resolution(context: &Self::Context) -> UVec2 { + context.render_resolution() + } + + fn suggested_jitter( + context: &Self::Context, + frame_number: u32, + render_resolution: UVec2, + ) -> Vec2 { + context.suggested_jitter(frame_number, render_resolution) + } + + fn suggested_mip_bias(context: &Self::Context, render_resolution: UVec2) -> f32 { + context.suggested_mip_bias(render_resolution) + } + + fn new_context( + upscaled_resolution: UVec2, + perf_quality_mode: DlssPerfQualityMode, + feature_flags: dlss_wgpu::DlssFeatureFlags, + sdk: Arc>, + device: &RenderDevice, + queue: &RenderQueue, + ) -> Result { + DlssRayReconstruction::new( + upscaled_resolution, + perf_quality_mode, + feature_flags, + DlssRayReconstructionRoughnessMode::Packed, + DlssRayReconstructionDepthMode::Hardware, + sdk, + device.wgpu_device(), + queue.deref(), + ) + } +} + +/// Additional textures needed as inputs for [`DlssRayReconstructionFeature`]. +#[derive(Component)] +pub struct ViewDlssRayReconstructionTextures { + pub diffuse_albedo: CachedTexture, + pub specular_albedo: CachedTexture, + pub normal_roughness: CachedTexture, + pub specular_motion_vectors: CachedTexture, +} + +#[reflect_remote(DlssPerfQualityMode)] +#[derive(Default)] +enum DlssPerfQualityModeRemoteReflect { + #[default] + Auto, + Dlaa, + Quality, + Balanced, + Performance, + UltraPerformance, +} + +#[derive(Resource)] +struct DlssSdk(Arc>); + +/// Application-specific ID for DLSS. +/// +/// See the DLSS programming guide for more info. +#[derive(Resource, Clone)] +pub struct DlssProjectId(pub Uuid); + +/// When DLSS Super Resolution is supported by the current system, this resource will exist in the main world. +/// Otherwise this resource will be absent. +#[derive(Resource, Clone, Copy)] +pub struct DlssSuperResolutionSupported; + +/// When DLSS Ray Reconstruction is supported by the current system, this resource will exist in the main world. +/// Otherwise this resource will be absent. +#[derive(Resource, Clone, Copy)] +pub struct DlssRayReconstructionSupported; diff --git a/crates/bevy_anti_alias/src/dlss/node.rs b/crates/bevy_anti_alias/src/dlss/node.rs new file mode 100644 index 0000000000000..aae59fa95b821 --- /dev/null +++ b/crates/bevy_anti_alias/src/dlss/node.rs @@ -0,0 +1,165 @@ +use super::{ + prepare::DlssRenderContext, Dlss, DlssFeature, DlssRayReconstructionFeature, + DlssSuperResolutionFeature, ViewDlssRayReconstructionTextures, +}; +use bevy_camera::MainPassResolutionOverride; +use bevy_core_pipeline::prepass::ViewPrepassTextures; +use bevy_ecs::{query::QueryItem, world::World}; +use bevy_render::{ + camera::TemporalJitter, + diagnostic::RecordDiagnostics, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + renderer::{RenderAdapter, RenderContext}, + view::ViewTarget, +}; +use dlss_wgpu::{ + ray_reconstruction::{ + DlssRayReconstructionRenderParameters, DlssRayReconstructionSpecularGuide, + }, + super_resolution::{DlssSuperResolutionExposure, DlssSuperResolutionRenderParameters}, +}; +use std::marker::PhantomData; + +#[derive(Default)] +pub struct DlssNode(PhantomData); + +impl ViewNode for DlssNode { + type ViewQuery = ( + &'static Dlss, + &'static DlssRenderContext, + &'static MainPassResolutionOverride, + &'static TemporalJitter, + &'static ViewTarget, + &'static ViewPrepassTextures, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + dlss, + dlss_context, + resolution_override, + temporal_jitter, + view_target, + prepass_textures, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let adapter = world.resource::(); + let (Some(prepass_depth_texture), Some(prepass_motion_vectors_texture)) = + (&prepass_textures.depth, &prepass_textures.motion_vectors) + else { + return Ok(()); + }; + + let view_target = view_target.post_process_write(); + + let render_resolution = resolution_override.0; + let render_parameters = DlssSuperResolutionRenderParameters { + color: &view_target.source, + depth: &prepass_depth_texture.texture.default_view, + motion_vectors: &prepass_motion_vectors_texture.texture.default_view, + exposure: DlssSuperResolutionExposure::Automatic, // TODO + bias: None, // TODO + dlss_output: &view_target.destination, + reset: dlss.reset, + jitter_offset: -temporal_jitter.offset, + partial_texture_size: Some(render_resolution), + motion_vector_scale: Some(-render_resolution.as_vec2()), + }; + + let diagnostics = render_context.diagnostic_recorder(); + let command_encoder = render_context.command_encoder(); + let mut dlss_context = dlss_context.context.lock().unwrap(); + + command_encoder.push_debug_group("dlss_super_resolution"); + let time_span = diagnostics.time_span(command_encoder, "dlss_super_resolution"); + + dlss_context + .render(render_parameters, command_encoder, &adapter) + .expect("Failed to render DLSS Super Resolution"); + + time_span.end(command_encoder); + command_encoder.pop_debug_group(); + + Ok(()) + } +} + +impl ViewNode for DlssNode { + type ViewQuery = ( + &'static Dlss, + &'static DlssRenderContext, + &'static MainPassResolutionOverride, + &'static TemporalJitter, + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static ViewDlssRayReconstructionTextures, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + ( + dlss, + dlss_context, + resolution_override, + temporal_jitter, + view_target, + prepass_textures, + ray_reconstruction_textures, + ): QueryItem, + world: &World, + ) -> Result<(), NodeRunError> { + let adapter = world.resource::(); + let (Some(prepass_depth_texture), Some(prepass_motion_vectors_texture)) = + (&prepass_textures.depth, &prepass_textures.motion_vectors) + else { + return Ok(()); + }; + + let view_target = view_target.post_process_write(); + + let render_resolution = resolution_override.0; + let render_parameters = DlssRayReconstructionRenderParameters { + diffuse_albedo: &ray_reconstruction_textures.diffuse_albedo.default_view, + specular_albedo: &ray_reconstruction_textures.specular_albedo.default_view, + normals: &ray_reconstruction_textures.normal_roughness.default_view, + roughness: None, + color: &view_target.source, + depth: &prepass_depth_texture.texture.default_view, + motion_vectors: &prepass_motion_vectors_texture.texture.default_view, + specular_guide: DlssRayReconstructionSpecularGuide::SpecularMotionVectors( + &ray_reconstruction_textures + .specular_motion_vectors + .default_view, + ), + screen_space_subsurface_scattering_guide: None, // TODO + bias: None, // TODO + dlss_output: &view_target.destination, + reset: dlss.reset, + jitter_offset: -temporal_jitter.offset, + partial_texture_size: Some(render_resolution), + motion_vector_scale: Some(-render_resolution.as_vec2()), + }; + + let diagnostics = render_context.diagnostic_recorder(); + let command_encoder = render_context.command_encoder(); + let mut dlss_context = dlss_context.context.lock().unwrap(); + + command_encoder.push_debug_group("dlss_ray_reconstruction"); + let time_span = diagnostics.time_span(command_encoder, "dlss_ray_reconstruction"); + + dlss_context + .render(render_parameters, command_encoder, &adapter) + .expect("Failed to render DLSS Ray Reconstruction"); + + time_span.end(command_encoder); + command_encoder.pop_debug_group(); + + Ok(()) + } +} diff --git a/crates/bevy_anti_alias/src/dlss/prepare.rs b/crates/bevy_anti_alias/src/dlss/prepare.rs new file mode 100644 index 0000000000000..a8e88f57d012e --- /dev/null +++ b/crates/bevy_anti_alias/src/dlss/prepare.rs @@ -0,0 +1,116 @@ +use super::{Dlss, DlssFeature, DlssSdk}; +use bevy_camera::{Camera3d, CameraMainTextureUsages, MainPassResolutionOverride}; +use bevy_core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass}; +use bevy_diagnostic::FrameCount; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res}, +}; +use bevy_math::Vec4Swizzles; +use bevy_render::{ + camera::{MipBias, TemporalJitter}, + render_resource::TextureUsages, + renderer::{RenderDevice, RenderQueue}, + view::ExtractedView, +}; +use dlss_wgpu::{DlssFeatureFlags, DlssPerfQualityMode}; +use std::sync::{Arc, Mutex}; + +#[derive(Component)] +pub struct DlssRenderContext { + pub context: Mutex, + pub perf_quality_mode: DlssPerfQualityMode, + pub feature_flags: DlssFeatureFlags, +} + +pub fn prepare_dlss( + mut query: Query< + ( + Entity, + &ExtractedView, + &Dlss, + &mut Camera3d, + &mut CameraMainTextureUsages, + &mut TemporalJitter, + &mut MipBias, + Option<&mut DlssRenderContext>, + ), + ( + With, + With, + With, + With, + ), + >, + dlss_sdk: Res, + render_device: Res, + render_queue: Res, + frame_count: Res, + mut commands: Commands, +) { + for ( + entity, + view, + dlss, + mut camera_3d, + mut camera_main_texture_usages, + mut temporal_jitter, + mut mip_bias, + mut dlss_context, + ) in &mut query + { + camera_main_texture_usages.0 |= TextureUsages::STORAGE_BINDING; + + let mut depth_texture_usages = TextureUsages::from(camera_3d.depth_texture_usages); + depth_texture_usages |= TextureUsages::TEXTURE_BINDING; + camera_3d.depth_texture_usages = depth_texture_usages.into(); + + let upscaled_resolution = view.viewport.zw(); + + let dlss_feature_flags = DlssFeatureFlags::LowResolutionMotionVectors + | DlssFeatureFlags::InvertedDepth + | DlssFeatureFlags::HighDynamicRange + | DlssFeatureFlags::AutoExposure; // TODO + + match dlss_context.as_deref_mut() { + Some(dlss_context) + if upscaled_resolution + == F::upscaled_resolution(&dlss_context.context.lock().unwrap()) + && dlss.perf_quality_mode == dlss_context.perf_quality_mode + && dlss_feature_flags == dlss_context.feature_flags => + { + let dlss_context = dlss_context.context.lock().unwrap(); + let render_resolution = F::render_resolution(&dlss_context); + temporal_jitter.offset = + F::suggested_jitter(&dlss_context, frame_count.0, render_resolution); + } + _ => { + let dlss_context = F::new_context( + upscaled_resolution, + dlss.perf_quality_mode, + dlss_feature_flags, + Arc::clone(&dlss_sdk.0), + &render_device, + &render_queue, + ) + .expect("Failed to create DlssRenderContext"); + + let render_resolution = F::render_resolution(&dlss_context); + temporal_jitter.offset = + F::suggested_jitter(&dlss_context, frame_count.0, render_resolution); + mip_bias.0 = F::suggested_mip_bias(&dlss_context, render_resolution); + + commands.entity(entity).insert(( + DlssRenderContext:: { + context: Mutex::new(dlss_context), + perf_quality_mode: dlss.perf_quality_mode, + feature_flags: dlss_feature_flags, + }, + MainPassResolutionOverride(render_resolution), + )); + } + } + } +} diff --git a/crates/bevy_anti_aliasing/src/fxaa/fxaa.wgsl b/crates/bevy_anti_alias/src/fxaa/fxaa.wgsl similarity index 100% rename from crates/bevy_anti_aliasing/src/fxaa/fxaa.wgsl rename to crates/bevy_anti_alias/src/fxaa/fxaa.wgsl diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_alias/src/fxaa/mod.rs similarity index 99% rename from crates/bevy_anti_aliasing/src/fxaa/mod.rs rename to crates/bevy_anti_alias/src/fxaa/mod.rs index 69b26ffa8abc7..075c7a82f610e 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_alias/src/fxaa/mod.rs @@ -82,6 +82,7 @@ impl Default for Fxaa { } /// Adds support for Fast Approximate Anti-Aliasing (FXAA) +#[derive(Default)] pub struct FxaaPlugin; impl Plugin for FxaaPlugin { fn build(&self, app: &mut App) { diff --git a/crates/bevy_anti_aliasing/src/fxaa/node.rs b/crates/bevy_anti_alias/src/fxaa/node.rs similarity index 100% rename from crates/bevy_anti_aliasing/src/fxaa/node.rs rename to crates/bevy_anti_alias/src/fxaa/node.rs diff --git a/crates/bevy_anti_aliasing/src/lib.rs b/crates/bevy_anti_alias/src/lib.rs similarity index 53% rename from crates/bevy_anti_aliasing/src/lib.rs rename to crates/bevy_anti_alias/src/lib.rs index 12b7982cb57a1..326df3c28d5b4 100644 --- a/crates/bevy_anti_aliasing/src/lib.rs +++ b/crates/bevy_anti_alias/src/lib.rs @@ -1,5 +1,4 @@ #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] -#![forbid(unsafe_code)] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( html_logo_url = "https://bevy.org/assets/icon.png", @@ -13,14 +12,25 @@ use smaa::SmaaPlugin; use taa::TemporalAntiAliasPlugin; pub mod contrast_adaptive_sharpening; +#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] +pub mod dlss; pub mod fxaa; pub mod smaa; pub mod taa; +/// Adds fxaa, smaa, taa, contrast aware sharpening, and optional dlss support. #[derive(Default)] -pub struct AntiAliasingPlugin; -impl Plugin for AntiAliasingPlugin { +pub struct AntiAliasPlugin; + +impl Plugin for AntiAliasPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_plugins((FxaaPlugin, SmaaPlugin, TemporalAntiAliasPlugin, CasPlugin)); + app.add_plugins(( + FxaaPlugin, + SmaaPlugin, + TemporalAntiAliasPlugin, + CasPlugin, + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + dlss::DlssPlugin, + )); } } diff --git a/crates/bevy_anti_aliasing/src/smaa/SMAAAreaLUT.ktx2 b/crates/bevy_anti_alias/src/smaa/SMAAAreaLUT.ktx2 similarity index 100% rename from crates/bevy_anti_aliasing/src/smaa/SMAAAreaLUT.ktx2 rename to crates/bevy_anti_alias/src/smaa/SMAAAreaLUT.ktx2 diff --git a/crates/bevy_anti_aliasing/src/smaa/SMAASearchLUT.ktx2 b/crates/bevy_anti_alias/src/smaa/SMAASearchLUT.ktx2 similarity index 100% rename from crates/bevy_anti_aliasing/src/smaa/SMAASearchLUT.ktx2 rename to crates/bevy_anti_alias/src/smaa/SMAASearchLUT.ktx2 diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_alias/src/smaa/mod.rs similarity index 99% rename from crates/bevy_anti_aliasing/src/smaa/mod.rs rename to crates/bevy_anti_alias/src/smaa/mod.rs index 3a65f6ce6c593..9d7d7cd6593c5 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_alias/src/smaa/mod.rs @@ -80,6 +80,7 @@ use bevy_shader::{Shader, ShaderDefVal}; use bevy_utils::prelude::default; /// Adds support for subpixel morphological antialiasing, or SMAA. +#[derive(Default)] pub struct SmaaPlugin; /// A component for enabling Subpixel Morphological Anti-Aliasing (SMAA) diff --git a/crates/bevy_anti_aliasing/src/smaa/smaa.wgsl b/crates/bevy_anti_alias/src/smaa/smaa.wgsl similarity index 99% rename from crates/bevy_anti_aliasing/src/smaa/smaa.wgsl rename to crates/bevy_anti_alias/src/smaa/smaa.wgsl index 24dc6baa25902..7facd0f963716 100644 --- a/crates/bevy_anti_aliasing/src/smaa/smaa.wgsl +++ b/crates/bevy_anti_alias/src/smaa/smaa.wgsl @@ -894,7 +894,7 @@ fn area(dist: vec2, e1: f32, e2: f32, offset: f32) -> vec2 { tex_coord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; // Do it! - return textureSample(area_texture, edges_sampler, tex_coord).rg; + return textureSampleLevel(area_texture, edges_sampler, tex_coord, 0.0).rg; } //----------------------------------------------------------------------------- diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_alias/src/taa/mod.rs similarity index 98% rename from crates/bevy_anti_aliasing/src/taa/mod.rs rename to crates/bevy_anti_alias/src/taa/mod.rs index fa477daf42472..5a65726f39d81 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_alias/src/taa/mod.rs @@ -45,6 +45,7 @@ use tracing::warn; /// Plugin for temporal anti-aliasing. /// /// See [`TemporalAntiAliasing`] for more details. +#[derive(Default)] pub struct TemporalAntiAliasPlugin; impl Plugin for TemporalAntiAliasPlugin { @@ -351,13 +352,12 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut(); for (entity, camera, camera_projection, taa_settings) in cameras_3d.iter_mut(&mut main_world) { - let has_perspective_projection = matches!(camera_projection, Projection::Perspective(_)); let mut entity_commands = commands .get_entity(entity) .expect("Camera entity wasn't synced."); if let Some(mut taa_settings) = taa_settings && camera.is_active - && has_perspective_projection + && camera_projection.is_perspective() { entity_commands.insert(taa_settings.clone()); taa_settings.reset = false; @@ -439,7 +439,7 @@ fn prepare_taa_history_textures( texture_descriptor.label = Some("taa_history_2_texture"); let history_2_texture = texture_cache.get(&render_device, texture_descriptor); - let textures = if frame_count.0 % 2 == 0 { + let textures = if frame_count.0.is_multiple_of(2) { TemporalAntiAliasHistoryTextures { write: history_1_texture, read: history_2_texture, diff --git a/crates/bevy_anti_aliasing/src/taa/taa.wgsl b/crates/bevy_anti_alias/src/taa/taa.wgsl similarity index 100% rename from crates/bevy_anti_aliasing/src/taa/taa.wgsl rename to crates/bevy_anti_alias/src/taa/taa.wgsl diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index c6c16cb5bed86..7ef2a4a82b773 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -255,28 +255,40 @@ impl App { /// Runs [`Plugin::finish`] for each plugin. This is usually called by the event loop once all /// plugins are ready, but can be useful for situations where you want to use [`App::update`]. pub fn finish(&mut self) { + #[cfg(feature = "trace")] + let _finish_span = info_span!("plugin finish").entered(); // plugins installed to main should see all sub-apps - let plugins = core::mem::take(&mut self.main_mut().plugin_registry); - for plugin in &plugins { - plugin.finish(self); + // do hokey pokey with a boxed zst plugin (doesn't allocate) + let mut hokeypokey: Box = Box::new(HokeyPokey); + for i in 0..self.main().plugin_registry.len() { + core::mem::swap(&mut self.main_mut().plugin_registry[i], &mut hokeypokey); + #[cfg(feature = "trace")] + let _plugin_finish_span = + info_span!("plugin finish", plugin = hokeypokey.name()).entered(); + hokeypokey.finish(self); + core::mem::swap(&mut self.main_mut().plugin_registry[i], &mut hokeypokey); } - let main = self.main_mut(); - main.plugin_registry = plugins; - main.plugins_state = PluginsState::Finished; + self.main_mut().plugins_state = PluginsState::Finished; self.sub_apps.iter_mut().skip(1).for_each(SubApp::finish); } /// Runs [`Plugin::cleanup`] for each plugin. This is usually called by the event loop after /// [`App::finish`], but can be useful for situations where you want to use [`App::update`]. pub fn cleanup(&mut self) { + #[cfg(feature = "trace")] + let _cleanup_span = info_span!("plugin cleanup").entered(); // plugins installed to main should see all sub-apps - let plugins = core::mem::take(&mut self.main_mut().plugin_registry); - for plugin in &plugins { - plugin.cleanup(self); + // do hokey pokey with a boxed zst plugin (doesn't allocate) + let mut hokeypokey: Box = Box::new(HokeyPokey); + for i in 0..self.main().plugin_registry.len() { + core::mem::swap(&mut self.main_mut().plugin_registry[i], &mut hokeypokey); + #[cfg(feature = "trace")] + let _plugin_cleanup_span = + info_span!("plugin cleanup", plugin = hokeypokey.name()).entered(); + hokeypokey.cleanup(self); + core::mem::swap(&mut self.main_mut().plugin_registry[i], &mut hokeypokey); } - let main = self.main_mut(); - main.plugin_registry = plugins; - main.plugins_state = PluginsState::Cleaned; + self.main_mut().plugins_state = PluginsState::Cleaned; self.sub_apps.iter_mut().skip(1).for_each(SubApp::cleanup); } @@ -479,6 +491,9 @@ impl App { self.main_mut().plugin_build_depth += 1; + #[cfg(feature = "trace")] + let _plugin_build_span = info_span!("plugin build", plugin = plugin.name()).entered(); + let f = AssertUnwindSafe(|| plugin.build(self)); #[cfg(feature = "std")] @@ -1330,8 +1345,8 @@ impl App { /// # struct Friend; /// # /// - /// app.add_observer(|trigger: On, friends: Query>, mut commands: Commands| { - /// if trigger.event().friends_allowed { + /// app.add_observer(|event: On, friends: Query>, mut commands: Commands| { + /// if event.friends_allowed { /// for friend in friends.iter() { /// commands.trigger_targets(Invite, friend); /// } @@ -1390,6 +1405,12 @@ impl App { } } +// Used for doing hokey pokey in finish and cleanup +pub(crate) struct HokeyPokey; +impl Plugin for HokeyPokey { + fn build(&self, _: &mut App) {} +} + type RunnerFn = Box AppExit>; fn run_once(mut app: App) -> AppExit { @@ -1526,6 +1547,38 @@ mod tests { } } + struct PluginF; + + impl Plugin for PluginF { + fn build(&self, _app: &mut App) {} + + fn finish(&self, app: &mut App) { + // Ensure other plugins are available during finish + assert_eq!( + app.is_plugin_added::(), + !app.get_added_plugins::().is_empty(), + ); + } + + fn cleanup(&self, app: &mut App) { + // Ensure other plugins are available during finish + assert_eq!( + app.is_plugin_added::(), + !app.get_added_plugins::().is_empty(), + ); + } + } + + struct PluginG; + + impl Plugin for PluginG { + fn build(&self, _app: &mut App) {} + + fn finish(&self, app: &mut App) { + app.add_plugins(PluginB); + } + } + #[test] fn can_add_two_plugins() { App::new().add_plugins((PluginA, PluginB)); @@ -1595,6 +1648,39 @@ mod tests { app.finish(); } + #[test] + fn test_get_added_plugins_works_during_finish_and_cleanup() { + let mut app = App::new(); + app.add_plugins(PluginA); + app.add_plugins(PluginF); + app.finish(); + } + + #[test] + fn test_adding_plugin_works_during_finish() { + let mut app = App::new(); + app.add_plugins(PluginA); + app.add_plugins(PluginG); + app.finish(); + assert_eq!( + app.main().plugin_registry[0].name(), + "bevy_app::main_schedule::MainSchedulePlugin" + ); + assert_eq!( + app.main().plugin_registry[1].name(), + "bevy_app::app::tests::PluginA" + ); + assert_eq!( + app.main().plugin_registry[2].name(), + "bevy_app::app::tests::PluginG" + ); + // PluginG adds PluginB during finish + assert_eq!( + app.main().plugin_registry[3].name(), + "bevy_app::app::tests::PluginB" + ); + } + #[test] fn test_derive_app_label() { use super::AppLabel; @@ -1611,9 +1697,17 @@ mod tests { b: u32, } + #[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(AppLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyTupleLabel(); + #[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(AppLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyStructLabel {} diff --git a/crates/bevy_app/src/plugin_group.rs b/crates/bevy_app/src/plugin_group.rs index ee1c8cb44b3cc..a2904ff0ba5c3 100644 --- a/crates/bevy_app/src/plugin_group.rs +++ b/crates/bevy_app/src/plugin_group.rs @@ -143,7 +143,7 @@ macro_rules! plugin_group { $($(#[doc = concat!( " - [`", stringify!($plugin_group_name), "`](" $(, stringify!($plugin_group_path), "::")*, stringify!($plugin_group_name), ")" $(, " - with feature `", $plugin_group_feature, "`")? - )]),+)? + )])+)? $( /// $(#[doc = $post_doc])+ @@ -559,18 +559,21 @@ mod tests { use core::{any::TypeId, fmt::Debug}; use super::PluginGroupBuilder; - use crate::{App, NoopPluginGroup, Plugin}; + use crate::{App, NoopPluginGroup, Plugin, PluginGroup}; + #[derive(Default)] struct PluginA; impl Plugin for PluginA { fn build(&self, _: &mut App) {} } + #[derive(Default)] struct PluginB; impl Plugin for PluginB { fn build(&self, _: &mut App) {} } + #[derive(Default)] struct PluginC; impl Plugin for PluginC { fn build(&self, _: &mut App) {} @@ -873,4 +876,30 @@ mod tests { ] ); } + + plugin_group! { + #[derive(Default)] + struct PluginGroupA { + :PluginA + } + } + plugin_group! { + #[derive(Default)] + struct PluginGroupB { + :PluginB + } + } + plugin_group! { + struct PluginGroupC { + :PluginC + #[plugin_group] + :PluginGroupA, + #[plugin_group] + :PluginGroupB, + } + } + #[test] + fn construct_nested_plugin_groups() { + PluginGroupC {}.build(); + } } diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 56a496f2b59bd..c83cc3bfaf38d 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -401,25 +401,35 @@ impl SubApp { /// Runs [`Plugin::finish`] for each plugin. pub fn finish(&mut self) { - let plugins = core::mem::take(&mut self.plugin_registry); - self.run_as_app(|app| { - for plugin in &plugins { - plugin.finish(app); - } - }); - self.plugin_registry = plugins; + // do hokey pokey with a boxed zst plugin (doesn't allocate) + let mut hokeypokey: Box = Box::new(crate::HokeyPokey); + for i in 0..self.plugin_registry.len() { + core::mem::swap(&mut self.plugin_registry[i], &mut hokeypokey); + #[cfg(feature = "trace")] + let _plugin_finish_span = + info_span!("plugin finish", plugin = hokeypokey.name()).entered(); + self.run_as_app(|app| { + hokeypokey.finish(app); + }); + core::mem::swap(&mut self.plugin_registry[i], &mut hokeypokey); + } self.plugins_state = PluginsState::Finished; } /// Runs [`Plugin::cleanup`] for each plugin. pub fn cleanup(&mut self) { - let plugins = core::mem::take(&mut self.plugin_registry); - self.run_as_app(|app| { - for plugin in &plugins { - plugin.cleanup(app); - } - }); - self.plugin_registry = plugins; + // do hokey pokey with a boxed zst plugin (doesn't allocate) + let mut hokeypokey: Box = Box::new(crate::HokeyPokey); + for i in 0..self.plugin_registry.len() { + core::mem::swap(&mut self.plugin_registry[i], &mut hokeypokey); + #[cfg(feature = "trace")] + let _plugin_cleanup_span = + info_span!("plugin cleanup", plugin = hokeypokey.name()).entered(); + self.run_as_app(|app| { + hokeypokey.cleanup(app); + }); + core::mem::swap(&mut self.plugin_registry[i], &mut hokeypokey); + } self.plugins_state = PluginsState::Cleaned; } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 2476f967280b5..b4797dd4c8a7d 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -14,6 +14,9 @@ keywords = ["bevy"] file_watcher = ["notify-debouncer-full", "watch", "multi_threaded"] embedded_watcher = ["file_watcher"] multi_threaded = ["bevy_tasks/multi_threaded"] +http = ["blocking", "ureq"] +https = ["blocking", "ureq", "ureq/rustls"] +web_asset_cache = [] asset_processor = [] watch = [] trace = [] @@ -87,6 +90,12 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-featu [target.'cfg(not(target_arch = "wasm32"))'.dependencies] notify-debouncer-full = { version = "0.5.0", default-features = false, optional = true } +# updating ureq: while ureq is semver stable, it depends on rustls which is not, meaning unlikely but possible breaking changes on minor releases. https://github.com/bevyengine/bevy/pull/16366#issuecomment-2572890794 +ureq = { version = "3", optional = true, default-features = false } +blocking = { version = "1.6", optional = true } + +[dev-dependencies] +async-channel = "2" [lints] workspace = true diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 0ab9267fff679..0ab590cd797fc 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -52,7 +52,7 @@ impl FileAssetReader { /// Returns the base path of the assets directory, which is normally the executable's parent /// directory. /// - /// To change this, set [`AssetPlugin.file_path`]. + /// To change this, set [`AssetPlugin::file_path`][crate::AssetPlugin::file_path]. pub fn get_base_path() -> PathBuf { get_base_path() } diff --git a/crates/bevy_asset/src/io/file/sync_file_asset.rs b/crates/bevy_asset/src/io/file/sync_file_asset.rs index 9281d323c86a7..7ad454860f20f 100644 --- a/crates/bevy_asset/src/io/file/sync_file_asset.rs +++ b/crates/bevy_asset/src/io/file/sync_file_asset.rs @@ -140,10 +140,10 @@ impl AssetReader for FileAssetReader { f.ok().and_then(|dir_entry| { let path = dir_entry.path(); // filter out meta files as they are not considered assets - if let Some(ext) = path.extension().and_then(|e| e.to_str()) { - if ext.eq_ignore_ascii_case("meta") { - return None; - } + if let Some(ext) = path.extension().and_then(|e| e.to_str()) + && ext.eq_ignore_ascii_case("meta") + { + return None; } // filter out hidden files. they are not listed by default but are directly targetable if path diff --git a/crates/bevy_asset/src/io/gated.rs b/crates/bevy_asset/src/io/gated.rs index fa4f0f0d3fd30..7d2759974fe5c 100644 --- a/crates/bevy_asset/src/io/gated.rs +++ b/crates/bevy_asset/src/io/gated.rs @@ -1,7 +1,7 @@ use crate::io::{AssetReader, AssetReaderError, PathStream, Reader}; use alloc::{boxed::Box, sync::Arc}; +use async_channel::{Receiver, Sender}; use bevy_platform::collections::HashMap; -use crossbeam_channel::{Receiver, Sender}; use parking_lot::RwLock; use std::path::Path; @@ -35,8 +35,8 @@ impl GateOpener { let mut gates = self.gates.write(); let gates = gates .entry_ref(path.as_ref()) - .or_insert_with(crossbeam_channel::unbounded); - gates.0.send(()).unwrap(); + .or_insert_with(async_channel::unbounded); + gates.0.send_blocking(()).unwrap(); } } @@ -61,10 +61,10 @@ impl AssetReader for GatedReader { let mut gates = self.gates.write(); let gates = gates .entry_ref(path.as_ref()) - .or_insert_with(crossbeam_channel::unbounded); + .or_insert_with(async_channel::unbounded); gates.1.clone() }; - receiver.recv().unwrap(); + receiver.recv().await.unwrap(); let result = self.reader.read(path).await?; Ok(result) } diff --git a/crates/bevy_asset/src/io/mod.rs b/crates/bevy_asset/src/io/mod.rs index aa7256bbd927b..778b96553f64b 100644 --- a/crates/bevy_asset/src/io/mod.rs +++ b/crates/bevy_asset/src/io/mod.rs @@ -10,11 +10,15 @@ pub mod android; pub mod embedded; #[cfg(not(target_arch = "wasm32"))] pub mod file; -pub mod gated; pub mod memory; pub mod processor_gated; #[cfg(target_arch = "wasm32")] pub mod wasm; +#[cfg(any(feature = "http", feature = "https"))] +pub mod web; + +#[cfg(test)] +pub mod gated; mod source; @@ -46,7 +50,8 @@ pub enum AssetReaderError { Io(Arc), /// The HTTP request completed but returned an unhandled [HTTP response status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status). - /// If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`]. + /// - If the request returns a 404 error, expect [`AssetReaderError::NotFound`]. + /// - If the request fails before getting a status code (e.g. request timeout, interrupted connection, etc), expect [`AssetReaderError::Io`]. #[error("Encountered HTTP status {0:?} when loading asset")] HttpError(u16), } @@ -762,11 +767,16 @@ impl Reader for SliceReader<'_> { } } -/// Appends `.meta` to the given path. +/// Appends `.meta` to the given path: +/// - `foo` becomes `foo.meta` +/// - `foo.bar` becomes `foo.bar.meta` pub(crate) fn get_meta_path(path: &Path) -> PathBuf { let mut meta_path = path.to_path_buf(); let mut extension = path.extension().unwrap_or_default().to_os_string(); - extension.push(".meta"); + if !extension.is_empty() { + extension.push("."); + } + extension.push("meta"); meta_path.set_extension(extension); meta_path } @@ -783,3 +793,24 @@ impl Stream for EmptyPathStream { Poll::Ready(None) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_meta_path_no_extension() { + assert_eq!( + get_meta_path(Path::new("foo")).to_str().unwrap(), + "foo.meta" + ); + } + + #[test] + fn get_meta_path_with_extension() { + assert_eq!( + get_meta_path(Path::new("foo.bar")).to_str().unwrap(), + "foo.bar.meta" + ); + } +} diff --git a/crates/bevy_asset/src/io/source.rs b/crates/bevy_asset/src/io/source.rs index 4852a2a71fff2..500a1657f353c 100644 --- a/crates/bevy_asset/src/io/source.rs +++ b/crates/bevy_asset/src/io/source.rs @@ -72,26 +72,12 @@ impl<'a> AssetSourceId<'a> { } } -impl AssetSourceId<'static> { - /// Indicates this [`AssetSourceId`] should have a static lifetime. - #[inline] - pub fn as_static(self) -> Self { - match self { - Self::Default => Self::Default, - Self::Name(value) => Self::Name(value.as_static()), - } - } - - /// Constructs an [`AssetSourceId`] with a static lifetime. - #[inline] - pub fn from_static(value: impl Into) -> Self { - value.into().as_static() - } -} - -impl<'a> From<&'a str> for AssetSourceId<'a> { - fn from(value: &'a str) -> Self { - AssetSourceId::Name(CowArc::Borrowed(value)) +// This is only implemented for static lifetimes to ensure `Path::clone` does not allocate +// by ensuring that this is stored as a `CowArc::Static`. +// Please read https://github.com/bevyengine/bevy/issues/19844 before changing this! +impl From<&'static str> for AssetSourceId<'static> { + fn from(value: &'static str) -> Self { + AssetSourceId::Name(value.into()) } } @@ -101,10 +87,10 @@ impl<'a, 'b> From<&'a AssetSourceId<'b>> for AssetSourceId<'b> { } } -impl<'a> From> for AssetSourceId<'a> { - fn from(value: Option<&'a str>) -> Self { +impl From> for AssetSourceId<'static> { + fn from(value: Option<&'static str>) -> Self { match value { - Some(value) => AssetSourceId::Name(CowArc::Borrowed(value)), + Some(value) => AssetSourceId::Name(value.into()), None => AssetSourceId::Default, } } @@ -329,7 +315,7 @@ pub struct AssetSourceBuilders { impl AssetSourceBuilders { /// Inserts a new builder with the given `id` pub fn insert(&mut self, id: impl Into>, source: AssetSourceBuilder) { - match AssetSourceId::from_static(id) { + match id.into() { AssetSourceId::Default => { self.default = Some(source); } diff --git a/crates/bevy_asset/src/io/wasm.rs b/crates/bevy_asset/src/io/wasm.rs index 4ed7162d2bafc..0dd0ca8e0899d 100644 --- a/crates/bevy_asset/src/io/wasm.rs +++ b/crates/bevy_asset/src/io/wasm.rs @@ -52,7 +52,8 @@ fn js_value_to_err(context: &str) -> impl FnOnce(JsValue) -> std::io::Error + '_ } impl HttpWasmAssetReader { - async fn fetch_bytes(&self, path: PathBuf) -> Result { + // Also used by [`WebAssetReader`](crate::web::WebAssetReader) + pub(crate) async fn fetch_bytes(&self, path: PathBuf) -> Result { // The JS global scope includes a self-reference via a specializing name, which can be used to determine the type of global context available. let global: Global = js_sys::global().unchecked_into(); let promise = if !global.window().is_undefined() { diff --git a/crates/bevy_asset/src/io/web.rs b/crates/bevy_asset/src/io/web.rs new file mode 100644 index 0000000000000..b0b297126bce6 --- /dev/null +++ b/crates/bevy_asset/src/io/web.rs @@ -0,0 +1,314 @@ +use crate::io::{AssetReader, AssetReaderError, Reader}; +use crate::io::{AssetSource, PathStream}; +use crate::{AssetApp, AssetPlugin}; +use alloc::{borrow::ToOwned, boxed::Box}; +use bevy_app::{App, Plugin}; +use bevy_tasks::ConditionalSendFuture; +use blocking::unblock; +use std::path::{Path, PathBuf}; +use tracing::warn; + +/// Adds the `http` and `https` asset sources to the app. +/// +/// NOTE: Make sure to add this plugin *before* `AssetPlugin` to properly register http asset sources. +/// +/// WARNING: be careful about where your URLs are coming from! URLs can potentially be exploited by an +/// attacker to trigger vulnerabilities in our asset loaders, or DOS by downloading enormous files. We +/// are not aware of any such vulnerabilities at the moment, just be careful! +/// +/// Any asset path that begins with `http` (when the `http` feature is enabled) or `https` (when the +/// `https` feature is enabled) will be loaded from the web via `fetch` (wasm) or `ureq` (native). +/// +/// Example usage: +/// +/// ```rust +/// # use bevy_app::{App, Startup}; +/// # use bevy_ecs::prelude::{Commands, Res}; +/// # use bevy_asset::web::{WebAssetPlugin, AssetServer}; +/// # struct DefaultPlugins; +/// # impl DefaultPlugins { fn set(plugin: WebAssetPlugin) -> WebAssetPlugin { plugin } } +/// # use bevy_asset::web::AssetServer; +/// # #[derive(Asset, TypePath, Default)] +/// # struct Image; +/// # #[derive(Component)] +/// # struct Sprite; +/// # impl Sprite { fn from_image(_: Handle) -> Self { Sprite } } +/// # fn main() { +/// App::new() +/// .add_plugins(DefaultPlugins.set(WebAssetPlugin { +/// silence_startup_warning: true, +/// })) +/// # .add_systems(Startup, setup).run(); +/// # } +/// // ... +/// # fn setup(mut commands: Commands, asset_server: Res) { +/// commands.spawn(Sprite::from_image(asset_server.load("https://example.com/favicon.png"))); +/// # } +/// ``` +/// +/// By default, `ureq`'s HTTP compression is disabled. To enable gzip and brotli decompression, add +/// the following dependency and features to your Cargo.toml. This will improve bandwidth +/// utilization when its supported by the server. +/// +/// ```toml +/// [target.'cfg(not(target_family = "wasm"))'.dev-dependencies] +/// ureq = { version = "3", default-features = false, features = ["gzip", "brotli"] } +/// ``` +#[derive(Default)] +pub struct WebAssetPlugin { + pub silence_startup_warning: bool, +} + +impl Plugin for WebAssetPlugin { + fn build(&self, app: &mut App) { + if !self.silence_startup_warning { + warn!("WebAssetPlugin is potentially insecure! Make sure to verify asset URLs are safe to load before loading them. \ + If you promise you know what you're doing, you can silence this warning by setting silence_startup_warning: true \ + in the WebAssetPlugin construction."); + } + if app.is_plugin_added::() { + warn!("WebAssetPlugin must be added before AssetPlugin for it to work!"); + } + #[cfg(feature = "http")] + app.register_asset_source( + "http", + AssetSource::build() + .with_reader(move || Box::new(WebAssetReader::Http)) + .with_processed_reader(move || Box::new(WebAssetReader::Http)), + ); + + #[cfg(feature = "https")] + app.register_asset_source( + "https", + AssetSource::build() + .with_reader(move || Box::new(WebAssetReader::Https)) + .with_processed_reader(move || Box::new(WebAssetReader::Https)), + ); + } +} + +/// Asset reader that treats paths as urls to load assets from. +pub enum WebAssetReader { + /// Unencrypted connections. + Http, + /// Use TLS for setting up connections. + Https, +} + +impl WebAssetReader { + fn make_uri(&self, path: &Path) -> PathBuf { + let prefix = match self { + Self::Http => "http://", + Self::Https => "https://", + }; + PathBuf::from(prefix).join(path) + } + + /// See [`crate::io::get_meta_path`] + fn make_meta_uri(&self, path: &Path) -> PathBuf { + let meta_path = crate::io::get_meta_path(path); + self.make_uri(&meta_path) + } +} + +#[cfg(target_arch = "wasm32")] +async fn get<'a>(path: PathBuf) -> Result, AssetReaderError> { + use crate::io::wasm::HttpWasmAssetReader; + + HttpWasmAssetReader::new("") + .fetch_bytes(path) + .await + .map(|r| Box::new(r) as Box) +} + +#[cfg(not(target_arch = "wasm32"))] +async fn get(path: PathBuf) -> Result, AssetReaderError> { + use crate::io::VecReader; + use alloc::{boxed::Box, vec::Vec}; + use bevy_platform::sync::LazyLock; + use std::io::{self, BufReader, Read}; + + let str_path = path.to_str().ok_or_else(|| { + AssetReaderError::Io( + io::Error::other(std::format!("non-utf8 path: {}", path.display())).into(), + ) + })?; + + #[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))] + if let Some(data) = web_asset_cache::try_load_from_cache(str_path).await? { + return Ok(Box::new(VecReader::new(data))); + } + use ureq::Agent; + + static AGENT: LazyLock = LazyLock::new(|| Agent::config_builder().build().new_agent()); + + let uri = str_path.to_owned(); + // Use [`unblock`] to run the http request on a separately spawned thread as to not block bevy's + // async executor. + let response = unblock(|| AGENT.get(uri).call()).await; + + match response { + Ok(mut response) => { + let mut reader = BufReader::new(response.body_mut().with_config().reader()); + + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer)?; + + #[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))] + web_asset_cache::save_to_cache(str_path, &buffer).await?; + + Ok(Box::new(VecReader::new(buffer))) + } + // ureq considers all >=400 status codes as errors + Err(ureq::Error::StatusCode(code)) => { + if code == 404 { + Err(AssetReaderError::NotFound(path)) + } else { + Err(AssetReaderError::HttpError(code)) + } + } + Err(err) => Err(AssetReaderError::Io( + io::Error::other(std::format!( + "unexpected error while loading asset {}: {}", + path.display(), + err + )) + .into(), + )), + } +} + +impl AssetReader for WebAssetReader { + fn read<'a>( + &'a self, + path: &'a Path, + ) -> impl ConditionalSendFuture, AssetReaderError>> { + get(self.make_uri(path)) + } + + async fn read_meta<'a>(&'a self, path: &'a Path) -> Result, AssetReaderError> { + let uri = self.make_meta_uri(path); + get(uri).await + } + + async fn is_directory<'a>(&'a self, _path: &'a Path) -> Result { + Ok(false) + } + + async fn read_directory<'a>( + &'a self, + path: &'a Path, + ) -> Result, AssetReaderError> { + Err(AssetReaderError::NotFound(self.make_uri(path))) + } +} + +/// A naive implementation of a cache for assets downloaded from the web that never invalidates. +/// `ureq` currently does not support caching, so this is a simple workaround. +/// It should eventually be replaced by `http-cache` or similar, see [tracking issue](https://github.com/06chaynes/http-cache/issues/91) +#[cfg(all(not(target_arch = "wasm32"), feature = "web_asset_cache"))] +mod web_asset_cache { + use alloc::string::String; + use alloc::vec::Vec; + use core::hash::{Hash, Hasher}; + use futures_lite::AsyncWriteExt; + use std::collections::hash_map::DefaultHasher; + use std::io; + use std::path::PathBuf; + + use crate::io::Reader; + + const CACHE_DIR: &str = ".web-asset-cache"; + + fn url_to_hash(url: &str) -> String { + let mut hasher = DefaultHasher::new(); + url.hash(&mut hasher); + std::format!("{:x}", hasher.finish()) + } + + pub async fn try_load_from_cache(url: &str) -> Result>, io::Error> { + let filename = url_to_hash(url); + let cache_path = PathBuf::from(CACHE_DIR).join(&filename); + + if cache_path.exists() { + let mut file = async_fs::File::open(&cache_path).await?; + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer).await?; + Ok(Some(buffer)) + } else { + Ok(None) + } + } + + pub async fn save_to_cache(url: &str, data: &[u8]) -> Result<(), io::Error> { + let filename = url_to_hash(url); + let cache_path = PathBuf::from(CACHE_DIR).join(&filename); + + async_fs::create_dir_all(CACHE_DIR).await.ok(); + + let mut cache_file = async_fs::File::create(&cache_path).await?; + cache_file.write_all(data).await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn make_http_uri() { + assert_eq!( + WebAssetReader::Http + .make_uri(Path::new("example.com/favicon.png")) + .to_str() + .unwrap(), + "http://example.com/favicon.png" + ); + } + + #[test] + fn make_https_uri() { + assert_eq!( + WebAssetReader::Https + .make_uri(Path::new("example.com/favicon.png")) + .to_str() + .unwrap(), + "https://example.com/favicon.png" + ); + } + + #[test] + fn make_http_meta_uri() { + assert_eq!( + WebAssetReader::Http + .make_meta_uri(Path::new("example.com/favicon.png")) + .to_str() + .unwrap(), + "http://example.com/favicon.png.meta" + ); + } + + #[test] + fn make_https_meta_uri() { + assert_eq!( + WebAssetReader::Https + .make_meta_uri(Path::new("example.com/favicon.png")) + .to_str() + .unwrap(), + "https://example.com/favicon.png.meta" + ); + } + + #[test] + fn make_https_without_extension_meta_uri() { + assert_eq!( + WebAssetReader::Https + .make_meta_uri(Path::new("example.com/favicon")) + .to_str() + .unwrap(), + "https://example.com/favicon.meta" + ); + } +} diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index ed3204cc30b06..8d0946d68b372 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -267,6 +267,8 @@ pub struct AssetPlugin { /// app will include scripts or modding support, as it could allow arbitrary file /// access for malicious code. /// +/// The default value is [`Forbid`](UnapprovedPathMode::Forbid). +/// /// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved) #[derive(Clone, Default)] pub enum UnapprovedPathMode { @@ -485,6 +487,22 @@ impl VisitAssetDependencies for Option { } } +impl VisitAssetDependencies for [Handle; N] { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + for dependency in self { + visit(dependency.id().untyped()); + } + } +} + +impl VisitAssetDependencies for [UntypedHandle; N] { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + for dependency in self { + visit(dependency.id()); + } + } +} + impl VisitAssetDependencies for Vec> { fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { for dependency in self { @@ -501,6 +519,22 @@ impl VisitAssetDependencies for Vec { } } +impl VisitAssetDependencies for HashSet> { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + for dependency in self { + visit(dependency.id().untyped()); + } + } +} + +impl VisitAssetDependencies for HashSet { + fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + for dependency in self { + visit(dependency.id()); + } + } +} + /// Adds asset-related builder methods to [`App`]. pub trait AssetApp { /// Registers the given `loader` in the [`App`]'s [`AssetServer`]. @@ -560,7 +594,7 @@ impl AssetApp for App { id: impl Into>, source: AssetSourceBuilder, ) -> &mut Self { - let id = AssetSourceId::from_static(id); + let id = id.into(); if self.world().get_resource::().is_some() { error!("{} must be registered before `AssetPlugin` (typically added as part of `DefaultPlugins`)", id); } @@ -677,6 +711,7 @@ mod tests { loader::{AssetLoader, LoadContext}, Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, UnapprovedPathMode, + UntypedHandle, }; use alloc::{ boxed::Box, @@ -692,7 +727,7 @@ mod tests { prelude::*, schedule::{LogLevel, ScheduleBuildSettings}, }; - use bevy_platform::collections::HashMap; + use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::TypePath; use core::time::Duration; use serde::{Deserialize, Serialize}; @@ -893,10 +928,6 @@ mod tests { #[test] fn load_dependencies() { - // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi_threaded"))] - panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); - let dir = Dir::default(); let a_path = "a.cool.ron"; @@ -1201,10 +1232,6 @@ mod tests { #[test] fn failure_load_states() { - // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi_threaded"))] - panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); - let dir = Dir::default(); let a_path = "a.cool.ron"; @@ -1334,10 +1361,6 @@ mod tests { #[test] fn dependency_load_states() { - // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi_threaded"))] - panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); - let a_path = "a.cool.ron"; let a_ron = r#" ( @@ -1473,10 +1496,6 @@ mod tests { #[test] fn manual_asset_management() { - // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi_threaded"))] - panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); - let dir = Dir::default(); let dep_path = "dep.cool.ron"; @@ -1574,10 +1593,6 @@ mod tests { #[test] fn load_folder() { - // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi_threaded"))] - panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); - let dir = Dir::default(); let a_path = "text/a.cool.ron"; @@ -1897,19 +1912,39 @@ mod tests { vec_handles: Vec>, #[dependency] embedded: TestAsset, + #[dependency] + set_handles: HashSet>, + #[dependency] + untyped_set_handles: HashSet, }, StructStyle(#[dependency] TestAsset), Empty, } + #[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(Asset, TypePath)] pub struct StructTestAsset { #[dependency] handle: Handle, #[dependency] embedded: TestAsset, + #[dependency] + array_handles: [Handle; 5], + #[dependency] + untyped_array_handles: [UntypedHandle; 5], + #[dependency] + set_handles: HashSet>, + #[dependency] + untyped_set_handles: HashSet, } + #[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(Asset, TypePath)] pub struct TupleTestAsset(#[dependency] Handle); @@ -2000,94 +2035,6 @@ mod tests { app.world_mut().run_schedule(Update); } - #[test] - #[ignore = "blocked on https://github.com/bevyengine/bevy/issues/11111"] - fn same_asset_different_settings() { - // Test loading the same asset twice with different settings. This should - // produce two distinct assets. - - // First, implement an asset that's a single u8, whose value is copied from - // the loader settings. - - #[derive(Asset, TypePath)] - struct U8Asset(u8); - - #[derive(Serialize, Deserialize, Default)] - struct U8LoaderSettings(u8); - - struct U8Loader; - - impl AssetLoader for U8Loader { - type Asset = U8Asset; - type Settings = U8LoaderSettings; - type Error = crate::loader::LoadDirectError; - - async fn load( - &self, - _: &mut dyn Reader, - settings: &Self::Settings, - _: &mut LoadContext<'_>, - ) -> Result { - Ok(U8Asset(settings.0)) - } - - fn extensions(&self) -> &[&str] { - &["u8"] - } - } - - // Create a test asset. - - let dir = Dir::default(); - dir.insert_asset(Path::new("test.u8"), &[]); - - let asset_source = AssetSource::build() - .with_reader(move || Box::new(MemoryAssetReader { root: dir.clone() })); - - // Set up the app. - - let mut app = App::new(); - - app.register_asset_source(AssetSourceId::Default, asset_source) - .add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) - .init_asset::() - .register_asset_loader(U8Loader); - - let asset_server = app.world().resource::(); - - // Load the test asset twice but with different settings. - - fn load(asset_server: &AssetServer, path: &str, value: u8) -> Handle { - asset_server.load_with_settings::( - path, - move |s: &mut U8LoaderSettings| s.0 = value, - ) - } - - let handle_1 = load(asset_server, "test.u8", 1); - let handle_2 = load(asset_server, "test.u8", 2); - - // Handles should be different. - - assert_ne!(handle_1, handle_2); - - run_app_until(&mut app, |world| { - let (Some(asset_1), Some(asset_2)) = ( - world.resource::>().get(&handle_1), - world.resource::>().get(&handle_2), - ) else { - return None; - }; - - // Values should match the settings. - - assert_eq!(asset_1.0, 1); - assert_eq!(asset_2.0, 2); - - Some(()) - }); - } - #[test] fn insert_dropped_handle_returns_error() { let mut app = App::new(); diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index d4e8b9a0370af..de9dfa5ca583a 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -535,43 +535,17 @@ impl<'a> AssetPath<'a> { } } -impl AssetPath<'static> { - /// Indicates this [`AssetPath`] should have a static lifetime. +// This is only implemented for static lifetimes to ensure `Path::clone` does not allocate +// by ensuring that this is stored as a `CowArc::Static`. +// Please read https://github.com/bevyengine/bevy/issues/19844 before changing this! +impl From<&'static str> for AssetPath<'static> { #[inline] - pub fn as_static(self) -> Self { - let Self { - source, - path, - label, - } = self; - - let source = source.as_static(); - let path = path.as_static(); - let label = label.map(CowArc::as_static); - - Self { - source, - path, - label, - } - } - - /// Constructs an [`AssetPath`] with a static lifetime. - #[inline] - pub fn from_static(value: impl Into) -> Self { - value.into().as_static() - } -} - -impl<'a> From<&'a str> for AssetPath<'a> { - #[inline] - fn from(asset_path: &'a str) -> Self { + fn from(asset_path: &'static str) -> Self { let (source, path, label) = Self::parse_internal(asset_path).unwrap(); - AssetPath { source: source.into(), - path: CowArc::Borrowed(path), - label: label.map(CowArc::Borrowed), + path: CowArc::Static(path), + label: label.map(CowArc::Static), } } } @@ -590,12 +564,12 @@ impl From for AssetPath<'static> { } } -impl<'a> From<&'a Path> for AssetPath<'a> { +impl From<&'static Path> for AssetPath<'static> { #[inline] - fn from(path: &'a Path) -> Self { + fn from(path: &'static Path) -> Self { Self { source: AssetSourceId::Default, - path: CowArc::Borrowed(path), + path: CowArc::Static(path), label: None, } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 641952a67150e..d71ed9a4616b7 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -325,7 +325,7 @@ impl AssetServer { self.load_with_meta_transform(path, None, (), false) } - /// Same as [`load`](AssetServer::load), but you can load assets from unaproved paths + /// Same as [`load`](AssetServer::load), but you can load assets from unapproved paths /// if [`AssetPlugin::unapproved_path_mode`](super::AssetPlugin::unapproved_path_mode) /// is [`Deny`](UnapprovedPathMode::Deny). /// @@ -358,7 +358,7 @@ impl AssetServer { self.load_with_meta_transform(path, None, guard, false) } - /// Same as [`load`](AssetServer::load_acquire), but you can load assets from unaproved paths + /// Same as [`load`](AssetServer::load_acquire), but you can load assets from unapproved paths /// if [`AssetPlugin::unapproved_path_mode`](super::AssetPlugin::unapproved_path_mode) /// is [`Deny`](UnapprovedPathMode::Deny). /// @@ -388,7 +388,7 @@ impl AssetServer { ) } - /// Same as [`load`](AssetServer::load_with_settings), but you can load assets from unaproved paths + /// Same as [`load`](AssetServer::load_with_settings), but you can load assets from unapproved paths /// if [`AssetPlugin::unapproved_path_mode`](super::AssetPlugin::unapproved_path_mode) /// is [`Deny`](UnapprovedPathMode::Deny). /// @@ -430,7 +430,7 @@ impl AssetServer { ) } - /// Same as [`load`](AssetServer::load_acquire_with_settings), but you can load assets from unaproved paths + /// Same as [`load`](AssetServer::load_acquire_with_settings), but you can load assets from unapproved paths /// if [`AssetPlugin::unapproved_path_mode`](super::AssetPlugin::unapproved_path_mode) /// is [`Deny`](UnapprovedPathMode::Deny). /// @@ -1906,7 +1906,7 @@ pub enum AssetLoadError { actual_asset_name: &'static str, loader_name: &'static str, }, - #[error("Could not find an asset loader matching: Loader Name: {loader_name:?}; Asset Type: {loader_name:?}; Extension: {extension:?}; Path: {asset_path:?};")] + #[error("Could not find an asset loader matching: Loader Name: {loader_name:?}; Asset Type: {asset_type_id:?}; Extension: {extension:?}; Path: {asset_path:?};")] MissingAssetLoader { loader_name: Option, asset_type_id: Option, diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index dff20913e65eb..35f165fc469a5 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -8,7 +8,7 @@ use bevy_asset::Handle; use bevy_derive::Deref; use bevy_ecs::{component::Component, entity::Entity, reflect::ReflectComponent}; use bevy_image::Image; -use bevy_math::{ops, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, Vec2, Vec3}; +use bevy_math::{ops, Dir3, FloatOrd, Mat4, Ray3d, Rect, URect, UVec2, Vec2, Vec3, Vec3A}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_window::{NormalizedWindowRef, WindowRef}; @@ -509,10 +509,10 @@ impl Camera { .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space if ndc_space_coords.z < 0.0 { - return Err(ViewportConversionError::PastNearPlane); + return Err(ViewportConversionError::PastFarPlane); } if ndc_space_coords.z > 1.0 { - return Err(ViewportConversionError::PastFarPlane); + return Err(ViewportConversionError::PastNearPlane); } // Flip the Y co-ordinate origin from the bottom to the top. @@ -547,10 +547,10 @@ impl Camera { .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space if ndc_space_coords.z < 0.0 { - return Err(ViewportConversionError::PastNearPlane); + return Err(ViewportConversionError::PastFarPlane); } if ndc_space_coords.z > 1.0 { - return Err(ViewportConversionError::PastFarPlane); + return Err(ViewportConversionError::PastNearPlane); } // Stretching ndc depth to value via near plane and negating result to be in positive room again. @@ -610,23 +610,29 @@ impl Camera { let target_rect = self .logical_viewport_rect() .ok_or(ViewportConversionError::NoViewportSize)?; - let mut rect_relative = (viewport_position - target_rect.min) / target_rect.size(); - // Flip the Y co-ordinate origin from the top to the bottom. - rect_relative.y = 1.0 - rect_relative.y; + let rect_relative = (viewport_position - target_rect.min) / target_rect.size(); + let mut ndc_xy = rect_relative * 2. - Vec2::ONE; + // Flip the Y co-ordinate from the top to the bottom to enter NDC. + ndc_xy.y = -ndc_xy.y; - let ndc = rect_relative * 2. - Vec2::ONE; - let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); - let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); + let ndc_point_near = ndc_xy.extend(1.0).into(); // Using EPSILON because an ndc with Z = 0 returns NaNs. - let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); - - // The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN. - Dir3::new(world_far_plane - world_near_plane) + let ndc_point_far = ndc_xy.extend(f32::EPSILON).into(); + let view_from_clip = self.computed.clip_from_view.inverse(); + let world_from_view = camera_transform.affine(); + // We multiply the point by `view_from_clip` and then `world_from_view` in sequence to avoid the precision loss + // (and performance penalty) incurred by pre-composing an affine transform with a projective transform. + // Additionally, we avoid adding and subtracting translation to the direction component to maintain precision. + let view_point_near = view_from_clip.project_point3a(ndc_point_near); + let view_point_far = view_from_clip.project_point3a(ndc_point_far); + let view_dir = view_point_far - view_point_near; + let origin = world_from_view.transform_point3a(view_point_near).into(); + let direction = world_from_view.transform_vector3a(view_dir).into(); + + // The fallible direction constructor ensures that direction isn't NaN. + Dir3::new(direction) .map_err(|_| ViewportConversionError::InvalidData) - .map(|direction| Ray3d { - origin: world_near_plane, - direction, - }) + .map(|direction| Ray3d { origin, direction }) } /// Returns a 2D world position computed from a position on this [`Camera`]'s viewport. @@ -686,10 +692,10 @@ impl Camera { Ok(world_near_plane.truncate()) } - /// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates. + /// Given a point in world space, use the camera's viewport to compute the Normalized Device Coordinates of the point. /// - /// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes, - /// and between 0.0 and 1.0 on the Z axis. + /// When the point is within the viewport the values returned will be between -1.0 (bottom left) and 1.0 (top right) + /// on the X and Y axes, and between 0.0 (far) and 1.0 (near) on the Z axis. /// To get the coordinates in the render target's viewport dimensions, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// @@ -699,17 +705,16 @@ impl Camera { /// # Panics /// /// Will panic if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled. - pub fn world_to_ndc( + pub fn world_to_ndc + From>( &self, camera_transform: &GlobalTransform, - world_position: Vec3, - ) -> Option { - // Build a transformation matrix to convert from world space to NDC using camera data - let clip_from_world: Mat4 = - self.computed.clip_from_view * camera_transform.to_matrix().inverse(); - let ndc_space_coords: Vec3 = clip_from_world.project_point3(world_position); + world_point: V, + ) -> Option { + let view_from_world = camera_transform.affine().inverse(); + let view_point = view_from_world.transform_point3a(world_point.into()); + let ndc_point = self.computed.clip_from_view.project_point3a(view_point); - (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) + (!ndc_point.is_nan()).then_some(ndc_point.into()) } /// Given a position in Normalized Device Coordinates, @@ -726,13 +731,21 @@ impl Camera { /// # Panics /// /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. - pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { - // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); - - let world_space_coords = ndc_to_world.project_point3(ndc); + pub fn ndc_to_world + From>( + &self, + camera_transform: &GlobalTransform, + ndc_point: V, + ) -> Option { + // We multiply the point by `view_from_clip` and then `world_from_view` in sequence to avoid the precision loss + // (and performance penalty) incurred by pre-composing an affine transform with a projective transform. + let view_point = self + .computed + .clip_from_view + .inverse() + .project_point3a(ndc_point.into()); + let world_point = camera_transform.affine().transform_point3a(view_point); - (!world_space_coords.is_nan()).then_some(world_space_coords) + (!world_point.is_nan()).then_some(world_point.into()) } /// Converts the depth in Normalized Device Coordinates @@ -795,6 +808,14 @@ pub enum RenderTarget { /// Texture View to which the camera's view is rendered. /// Useful when the texture view needs to be created outside of Bevy, for example OpenXR. TextureView(ManualTextureViewHandle), + /// The camera won't render to any color target. + /// + /// This is useful when you want a camera that *only* renders prepasses, for + /// example a depth prepass. See the `render_depth_to_texture` example. + None { + /// The physical size of the viewport. + size: UVec2, + }, } impl RenderTarget { @@ -818,6 +839,10 @@ impl RenderTarget { .map(NormalizedRenderTarget::Window), RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())), RenderTarget::TextureView(id) => Some(NormalizedRenderTarget::TextureView(*id)), + RenderTarget::None { size } => Some(NormalizedRenderTarget::None { + width: size.x, + height: size.y, + }), } } } @@ -835,6 +860,16 @@ pub enum NormalizedRenderTarget { /// Texture View to which the camera's view is rendered. /// Useful when the texture view needs to be created outside of Bevy, for example OpenXR. TextureView(ManualTextureViewHandle), + /// The camera won't render to any color target. + /// + /// This is useful when you want a camera that *only* renders prepasses, for + /// example a depth prepass. See the `render_depth_to_texture` example. + None { + /// The physical width of the viewport. + width: u32, + /// The physical height of the viewport. + height: u32, + }, } /// A unique id that corresponds to a specific `ManualTextureView` in the `ManualTextureViews` collection. @@ -896,3 +931,88 @@ impl CameraMainTextureUsages { self } } + +#[cfg(test)] +mod test { + use bevy_math::{Vec2, Vec3}; + use bevy_transform::components::GlobalTransform; + + use crate::{ + Camera, OrthographicProjection, PerspectiveProjection, Projection, RenderTargetInfo, + Viewport, + }; + + fn make_camera(mut projection: Projection, physical_size: Vec2) -> Camera { + let viewport = Viewport { + physical_size: physical_size.as_uvec2(), + ..Default::default() + }; + let mut camera = Camera { + viewport: Some(viewport.clone()), + ..Default::default() + }; + camera.computed.target_info = Some(RenderTargetInfo { + physical_size: viewport.physical_size, + scale_factor: 1.0, + }); + projection.update( + viewport.physical_size.x as f32, + viewport.physical_size.y as f32, + ); + camera.computed.clip_from_view = projection.get_clip_from_view(); + camera + } + + #[test] + fn viewport_to_world_orthographic_3d_returns_forward() { + let transform = GlobalTransform::default(); + let size = Vec2::new(1600.0, 900.0); + let camera = make_camera( + Projection::Orthographic(OrthographicProjection::default_3d()), + size, + ); + let ray = camera.viewport_to_world(&transform, Vec2::ZERO).unwrap(); + assert_eq!(ray.direction, transform.forward()); + assert!(ray + .origin + .abs_diff_eq(Vec3::new(-size.x * 0.5, size.y * 0.5, 0.0), 1e-4)); + let ray = camera.viewport_to_world(&transform, size).unwrap(); + assert_eq!(ray.direction, transform.forward()); + assert!(ray + .origin + .abs_diff_eq(Vec3::new(size.x * 0.5, -size.y * 0.5, 0.0), 1e-4)); + } + + #[test] + fn viewport_to_world_orthographic_2d_returns_forward() { + let transform = GlobalTransform::default(); + let size = Vec2::new(1600.0, 900.0); + let camera = make_camera( + Projection::Orthographic(OrthographicProjection::default_2d()), + size, + ); + let ray = camera.viewport_to_world(&transform, Vec2::ZERO).unwrap(); + assert_eq!(ray.direction, transform.forward()); + assert!(ray + .origin + .abs_diff_eq(Vec3::new(-size.x * 0.5, size.y * 0.5, 1000.0), 1e-4)); + let ray = camera.viewport_to_world(&transform, size).unwrap(); + assert_eq!(ray.direction, transform.forward()); + assert!(ray + .origin + .abs_diff_eq(Vec3::new(size.x * 0.5, -size.y * 0.5, 1000.0), 1e-4)); + } + + #[test] + fn viewport_to_world_perspective_center_returns_forward() { + let transform = GlobalTransform::default(); + let size = Vec2::new(1600.0, 900.0); + let camera = make_camera( + Projection::Perspective(PerspectiveProjection::default()), + size, + ); + let ray = camera.viewport_to_world(&transform, size * 0.5).unwrap(); + assert_eq!(ray.direction, transform.forward()); + assert_eq!(ray.origin, transform.forward() * 0.1); + } +} diff --git a/crates/bevy_camera/src/lib.rs b/crates/bevy_camera/src/lib.rs index e698e635dab7c..1d5ab008599d8 100644 --- a/crates/bevy_camera/src/lib.rs +++ b/crates/bevy_camera/src/lib.rs @@ -6,6 +6,7 @@ pub mod primitives; mod projection; pub mod visibility; +use bevy_ecs::schedule::SystemSet; pub use camera::*; pub use clear_color::*; pub use components::*; @@ -37,3 +38,11 @@ pub mod prelude { PerspectiveProjection, Projection, }; } + +/// Label for `camera_system`, shared across all `T`. +#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] +pub struct CameraUpdateSystems; + +/// Deprecated alias for [`CameraUpdateSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `CameraUpdateSystems`.")] +pub type CameraUpdateSystem = CameraUpdateSystems; diff --git a/crates/bevy_camera/src/primitives.rs b/crates/bevy_camera/src/primitives.rs index eb6246d25990c..52149613385b5 100644 --- a/crates/bevy_camera/src/primitives.rs +++ b/crates/bevy_camera/src/primitives.rs @@ -294,6 +294,7 @@ impl Frustum { row3 - row }); } + half_spaces[5] = HalfSpace::new(Vec4::new(0.0, 0.0, 0.0, f32::MAX)); Self { half_spaces } } diff --git a/crates/bevy_camera/src/projection.rs b/crates/bevy_camera/src/projection.rs index 1d2bfdab27561..7a1cb5f80ea01 100644 --- a/crates/bevy_camera/src/projection.rs +++ b/crates/bevy_camera/src/projection.rs @@ -27,14 +27,6 @@ impl Plugin for CameraProjectionPlugin { } } -/// Label for `camera_system`, shared across all `T`. -#[derive(SystemSet, Clone, Eq, PartialEq, Hash, Debug)] -pub struct CameraUpdateSystems; - -/// Deprecated alias for [`CameraUpdateSystems`]. -#[deprecated(since = "0.17.0", note = "Renamed to `CameraUpdateSystems`.")] -pub type CameraUpdateSystem = CameraUpdateSystems; - /// Describes a type that can generate a projection matrix, allowing it to be added to a /// [`Camera`]'s [`Projection`] component. /// @@ -76,7 +68,7 @@ pub trait CameraProjection { /// This code is called by [`update_frusta`](crate::visibility::update_frusta) system /// for each camera to update its frustum. fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum { - let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse(); + let clip_from_world = self.get_clip_from_view() * camera_transform.affine().inverse(); Frustum::from_clip_from_world_custom_far( &clip_from_world, &camera_transform.translation(), @@ -245,6 +237,16 @@ impl Projection { dyn_projection: Box::new(projection), }) } + + /// Check if the projection is perspective. + /// For [`CustomProjection`], this checks if the projection matrix's w-axis's w is 0.0. + pub fn is_perspective(&self) -> bool { + match self { + Projection::Perspective(_) => true, + Projection::Orthographic(_) => false, + Projection::Custom(projection) => projection.get_clip_from_view().w_axis.w == 0.0, + } + } } impl Deref for Projection { diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs index f059beab829ef..8ac0ea4fe565d 100644 --- a/crates/bevy_camera/src/visibility/mod.rs +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -23,7 +23,7 @@ use crate::{ primitives::{Aabb, Frustum, MeshAabb, Sphere}, Projection, }; -use bevy_mesh::{Mesh, Mesh2d, Mesh3d}; +use bevy_mesh::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh, Mesh2d, Mesh3d}; #[derive(Component, Default)] pub struct NoCpuCulling; @@ -346,7 +346,12 @@ impl Plugin for VisibilityPlugin { .register_required_components::() .configure_sets( PostUpdate, - (CalculateBounds, UpdateFrusta, VisibilityPropagate) + ( + CalculateBounds + .ambiguous_with(mark_3d_meshes_as_changed_if_their_assets_changed), + UpdateFrusta, + VisibilityPropagate, + ) .before(CheckVisibility) .after(TransformSystems::Propagate), ) diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 010b3df249678..3797c7500d82e 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -275,7 +275,7 @@ impl From for Laba { } else { (Laba::CIE_KAPPA * yr + 16.0) / 116.0 }; - let fz = if yr > Laba::CIE_EPSILON { + let fz = if zr > Laba::CIE_EPSILON { ops::cbrt(zr) } else { (Laba::CIE_KAPPA * zr + 16.0) / 116.0 diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 05a1b7ce37986..e9b288498b0a8 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -2,10 +2,6 @@ name = "bevy_core_pipeline" version = "0.17.0-dev" edition = "2024" -authors = [ - "Bevy Contributors ", - "Carter Anderson ", -] description = "Provides a core render pipeline for Bevy Engine." homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" @@ -16,7 +12,7 @@ keywords = ["bevy"] trace = [] webgl = [] webgpu = [] -tonemapping_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] +tonemapping_luts = ["bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 5351c08726f9f..ffc25046a57e5 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -29,8 +29,10 @@ pub mod graph { EndMainPass, Wireframe, LateDownsampleDepth, - Taa, MotionBlur, + Taa, + DlssSuperResolution, + DlssRayReconstruction, Bloom, AutoExposure, DepthOfField, @@ -119,7 +121,6 @@ use crate::{ AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT, }, - dof::DepthOfFieldNode, prepass::{ node::{EarlyPrepassNode, LatePrepassNode}, AlphaMask3dPrepass, DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, @@ -212,7 +213,6 @@ impl Plugin for Core3dPlugin { Node3d::MainTransparentPass, ) .add_render_graph_node::(Core3d, Node3d::EndMainPass) - .add_render_graph_node::>(Core3d, Node3d::DepthOfField) .add_render_graph_node::>(Core3d, Node3d::Tonemapping) .add_render_graph_node::(Core3d, Node3d::EndMainPassPostProcessing) .add_render_graph_node::>(Core3d, Node3d::Upscaling) diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 80cb8cdb0dbad..6ca7915a7c2b8 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -6,18 +6,12 @@ html_favicon_url = "https://bevy.org/assets/icon.png" )] -pub mod auto_exposure; pub mod blit; -pub mod bloom; pub mod core_2d; pub mod core_3d; pub mod deferred; -pub mod dof; pub mod experimental; -pub mod motion_blur; -pub mod msaa_writeback; pub mod oit; -pub mod post_process; pub mod prepass; pub mod tonemapping; pub mod upscaling; @@ -29,11 +23,10 @@ mod fullscreen_vertex_shader; mod skybox; use crate::{ - blit::BlitPlugin, bloom::BloomPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, - deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, dof::DepthOfFieldPlugin, - experimental::mip_generation::MipGenerationPlugin, motion_blur::MotionBlurPlugin, - msaa_writeback::MsaaWritebackPlugin, post_process::PostProcessingPlugin, - tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, + blit::BlitPlugin, core_2d::Core2dPlugin, core_3d::Core3dPlugin, + deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, + experimental::mip_generation::MipGenerationPlugin, tonemapping::TonemappingPlugin, + upscaling::UpscalingPlugin, }; use bevy_app::{App, Plugin}; use bevy_asset::embedded_asset; @@ -50,13 +43,8 @@ impl Plugin for CorePipelinePlugin { app.add_plugins((Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin)) .add_plugins(( BlitPlugin, - MsaaWritebackPlugin, TonemappingPlugin, UpscalingPlugin, - BloomPlugin, - MotionBlurPlugin, - DepthOfFieldPlugin, - PostProcessingPlugin, OrderIndependentTransparencyPlugin, MipGenerationPlugin, )); diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index 56fc5d8b6060b..aa35210a0eb1f 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -122,9 +122,7 @@ impl ExtractComponent for Skybox { skybox.clone(), SkyboxUniforms { brightness: skybox.brightness * exposure, - transform: Transform::from_rotation(skybox.rotation) - .to_matrix() - .inverse(), + transform: Transform::from_rotation(skybox.rotation.inverse()).to_matrix(), #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: 0, #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index 70b6fb1cb355f..059a2751c863d 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -30,44 +30,44 @@ pub struct CoreButton { } fn button_on_key_event( - mut trigger: On>, + mut event: On>, q_state: Query<(&CoreButton, Has)>, mut commands: Commands, ) { - if let Ok((bstate, disabled)) = q_state.get(trigger.target()) + if let Ok((bstate, disabled)) = q_state.get(event.entity()) && !disabled { - let event = &trigger.event().input; - if !event.repeat - && event.state == ButtonState::Pressed - && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + let input_event = &event.input; + if !input_event.repeat + && input_event.state == ButtonState::Pressed + && (input_event.key_code == KeyCode::Enter || input_event.key_code == KeyCode::Space) { - trigger.propagate(false); - commands.notify_with(&bstate.on_activate, Activate(trigger.target())); + event.propagate(false); + commands.notify_with(&bstate.on_activate, Activate(event.entity())); } } } fn button_on_pointer_click( - mut trigger: On>, + mut event: On>, mut q_state: Query<(&CoreButton, Has, Has)>, mut commands: Commands, ) { - if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(event.entity()) { + event.propagate(false); if pressed && !disabled { - commands.notify_with(&bstate.on_activate, Activate(trigger.target())); + commands.notify_with(&bstate.on_activate, Activate(event.entity())); } } } fn button_on_pointer_down( - mut trigger: On>, + mut event: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { + event.propagate(false); if !disabled && !pressed { commands.entity(button).insert(Pressed); } @@ -75,12 +75,12 @@ fn button_on_pointer_down( } fn button_on_pointer_up( - mut trigger: On>, + mut event: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { + event.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } @@ -88,12 +88,12 @@ fn button_on_pointer_up( } fn button_on_pointer_drag_end( - mut trigger: On>, + mut event: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { + event.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } @@ -101,12 +101,12 @@ fn button_on_pointer_drag_end( } fn button_on_pointer_cancel( - mut trigger: On>, + mut event: On>, mut q_state: Query<(Entity, Has, Has), With>, mut commands: Commands, ) { - if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((button, disabled, pressed)) = q_state.get_mut(event.entity()) { + event.propagate(false); if !disabled && pressed { commands.entity(button).remove::(); } diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs index 6ac92e6cce3c5..ed84efd44d259 100644 --- a/crates/bevy_core_widgets/src/core_checkbox.rs +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -42,14 +42,14 @@ fn checkbox_on_key_input( q_checkbox: Query<(&CoreCheckbox, Has), Without>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.entity()) { let event = &ev.event().input; if event.state == ButtonState::Pressed && !event.repeat && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { ev.propagate(false); - set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); } } } @@ -61,11 +61,11 @@ fn checkbox_on_pointer_click( focus_visible: Option>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { // Clicking on a button makes it the focused input, // and hides the focus ring if it was visible. if let Some(mut focus) = focus { - focus.0 = Some(ev.target()); + focus.0 = Some(ev.entity()); } if let Some(mut focus_visible) = focus_visible { focus_visible.0 = false; @@ -73,7 +73,7 @@ fn checkbox_on_pointer_click( ev.propagate(false); if !disabled { - set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); } } } @@ -127,7 +127,7 @@ fn checkbox_on_set_checked( q_checkbox: Query<(&CoreCheckbox, Has, Has)>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { ev.propagate(false); if disabled { return; @@ -135,7 +135,7 @@ fn checkbox_on_set_checked( let will_be_checked = ev.event().0; if will_be_checked != is_checked { - set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked); + set_checkbox_state(&mut commands, ev.entity(), checkbox, will_be_checked); } } } @@ -145,13 +145,13 @@ fn checkbox_on_toggle_checked( q_checkbox: Query<(&CoreCheckbox, Has, Has)>, mut commands: Commands, ) { - if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.entity()) { ev.propagate(false); if disabled { return; } - set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + set_checkbox_state(&mut commands, ev.entity(), checkbox, !is_checked); } } diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs index 4cea61e717517..e5ba867a3410f 100644 --- a/crates/bevy_core_widgets/src/core_radio.rs +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -61,7 +61,7 @@ fn radio_group_on_key_input( q_children: Query<&Children>, mut commands: Commands, ) { - if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.entity()) { let event = &ev.event().input; if event.state == ButtonState::Pressed && !event.repeat @@ -80,7 +80,7 @@ fn radio_group_on_key_input( // Find all radio descendants that are not disabled let radio_buttons = q_children - .iter_descendants(ev.target()) + .iter_descendants(ev.entity()) .filter_map(|child_id| match q_radio.get(child_id) { Ok((checked, false)) => Some((child_id, checked)), Ok((_, true)) | Err(_) => None, @@ -149,14 +149,14 @@ fn radio_group_on_button_click( q_children: Query<&Children>, mut commands: Commands, ) { - if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.entity()) { // Starting with the original target, search upward for a radio button. - let radio_id = if q_radio.contains(ev.original_target()) { - ev.original_target() + let radio_id = if q_radio.contains(ev.original_entity()) { + ev.original_entity() } else { // Search ancestors for the first radio button let mut found_radio = None; - for ancestor in q_parents.iter_ancestors(ev.original_target()) { + for ancestor in q_parents.iter_ancestors(ev.original_entity()) { if q_group.contains(ancestor) { // We reached a radio group before finding a radio button, bail out return; @@ -180,7 +180,7 @@ fn radio_group_on_button_click( // Gather all the enabled radio group descendants for exclusion. let radio_buttons = q_children - .iter_descendants(ev.target()) + .iter_descendants(ev.entity()) .filter_map(|child_id| match q_radio.get(child_id) { Ok((checked, false)) => Some((child_id, checked)), Ok((_, true)) | Err(_) => None, diff --git a/crates/bevy_core_widgets/src/core_scrollbar.rs b/crates/bevy_core_widgets/src/core_scrollbar.rs index 5f5a7087483d1..00569243ec593 100644 --- a/crates/bevy_core_widgets/src/core_scrollbar.rs +++ b/crates/bevy_core_widgets/src/core_scrollbar.rs @@ -12,7 +12,7 @@ use bevy_math::Vec2; use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_ui::{ - ComputedNode, ComputedNodeTarget, Node, ScrollPosition, UiGlobalTransform, UiScale, Val, + ComputedNode, ComputedUiRenderTargetInfo, Node, ScrollPosition, UiGlobalTransform, UiScale, Val, }; /// Used to select the orientation of a scrollbar, slider, or other oriented control. @@ -104,16 +104,16 @@ fn scrollbar_on_pointer_down( mut q_scrollbar: Query<( &CoreScrollbar, &ComputedNode, - &ComputedNodeTarget, + &ComputedUiRenderTargetInfo, &UiGlobalTransform, )>, mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, ui_scale: Res, ) { - if q_thumb.contains(ev.target()) { + if q_thumb.contains(ev.entity()) { // If they click on the thumb, do nothing. This will be handled by the drag event. ev.propagate(false); - } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.target()) { + } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.entity()) { // If they click on the scrollbar track, page up or down. ev.propagate(false); @@ -162,7 +162,7 @@ fn scrollbar_on_drag_start( q_scrollbar: Query<&CoreScrollbar>, q_scroll_area: Query<&ScrollPosition>, ) { - if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.target()) { + if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.entity()) { ev.propagate(false); if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) && let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) @@ -183,7 +183,7 @@ fn scrollbar_on_drag( mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without>, ui_scale: Res, ) { - if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.target()) + if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.entity()) && let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) { ev.propagate(false); @@ -219,7 +219,7 @@ fn scrollbar_on_drag_end( mut ev: On>, mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { - if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + if let Ok(mut drag) = q_thumb.get_mut(ev.entity()) { ev.propagate(false); if drag.dragging { drag.dragging = false; @@ -231,7 +231,7 @@ fn scrollbar_on_drag_cancel( mut ev: On>, mut q_thumb: Query<&mut CoreScrollbarDragState, With>, ) { - if let Ok(mut drag) = q_thumb.get_mut(ev.target()) { + if let Ok(mut drag) = q_thumb.get_mut(ev.entity()) { ev.propagate(false); if drag.dragging { drag.dragging = false; diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 64570c2ed1963..e680b849eda63 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -23,7 +23,9 @@ use bevy_log::warn_once; use bevy_math::ops; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; +use bevy_ui::{ + ComputedNode, ComputedUiRenderTargetInfo, InteractionDisabled, UiGlobalTransform, UiScale, +}; use crate::{Callback, Notify, ValueChange}; @@ -226,7 +228,7 @@ pub struct CoreSliderDragState { } pub(crate) fn slider_on_pointer_down( - mut trigger: On>, + mut event: On>, q_slider: Query<( &CoreSlider, &SliderValue, @@ -234,7 +236,7 @@ pub(crate) fn slider_on_pointer_down( &SliderStep, Option<&SliderPrecision>, &ComputedNode, - &ComputedNodeTarget, + &ComputedUiRenderTargetInfo, &UiGlobalTransform, Has, )>, @@ -243,9 +245,9 @@ pub(crate) fn slider_on_pointer_down( mut commands: Commands, ui_scale: Res, ) { - if q_thumb.contains(trigger.target()) { + if q_thumb.contains(event.entity()) { // Thumb click, stop propagation to prevent track click. - trigger.propagate(false); + event.propagate(false); } else if let Ok(( slider, value, @@ -256,10 +258,10 @@ pub(crate) fn slider_on_pointer_down( node_target, transform, disabled, - )) = q_slider.get(trigger.target()) + )) = q_slider.get(event.entity()) { // Track click - trigger.propagate(false); + event.propagate(false); if disabled { return; @@ -267,13 +269,13 @@ pub(crate) fn slider_on_pointer_down( // Find thumb size by searching descendants for the first entity with CoreSliderThumb let thumb_size = q_children - .iter_descendants(trigger.target()) + .iter_descendants(event.entity()) .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) .unwrap_or(0.0); // Detect track click. let local_pos = transform.try_inverse().unwrap().transform_point2( - trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + event.pointer_location.position * node_target.scale_factor() / ui_scale.0, ); let track_width = node.size().x - thumb_size; // Avoid division by zero @@ -302,13 +304,13 @@ pub(crate) fn slider_on_pointer_down( if matches!(slider.on_change, Callback::Ignore) { commands - .entity(trigger.target()) + .entity(event.entity()) .insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: trigger.target(), + source: event.entity(), value: new_value, }, ); @@ -317,7 +319,7 @@ pub(crate) fn slider_on_pointer_down( } pub(crate) fn slider_on_drag_start( - mut trigger: On>, + mut event: On>, mut q_slider: Query< ( &SliderValue, @@ -327,8 +329,8 @@ pub(crate) fn slider_on_drag_start( With, >, ) { - if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(event.entity()) { + event.propagate(false); if !disabled { drag.dragging = true; drag.offset = value.0; @@ -337,7 +339,7 @@ pub(crate) fn slider_on_drag_start( } pub(crate) fn slider_on_drag( - mut trigger: On>, + mut event: On>, mut q_slider: Query<( &ComputedNode, &CoreSlider, @@ -353,16 +355,16 @@ pub(crate) fn slider_on_drag( ui_scale: Res, ) { if let Ok((node, slider, range, precision, transform, drag, disabled)) = - q_slider.get_mut(trigger.target()) + q_slider.get_mut(event.entity()) { - trigger.propagate(false); + event.propagate(false); if drag.dragging && !disabled { - let mut distance = trigger.event().distance / ui_scale.0; + let mut distance = event.distance / ui_scale.0; distance.y *= -1.; let distance = transform.transform_vector2(distance); // Find thumb size by searching descendants for the first entity with CoreSliderThumb let thumb_size = q_children - .iter_descendants(trigger.target()) + .iter_descendants(event.entity()) .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) .unwrap_or(0.0); let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); @@ -380,13 +382,13 @@ pub(crate) fn slider_on_drag( if matches!(slider.on_change, Callback::Ignore) { commands - .entity(trigger.target()) + .entity(event.entity()) .insert(SliderValue(rounded_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: trigger.target(), + source: event.entity(), value: rounded_value, }, ); @@ -396,11 +398,11 @@ pub(crate) fn slider_on_drag( } pub(crate) fn slider_on_drag_end( - mut trigger: On>, + mut event: On>, mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>, ) { - if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) { - trigger.propagate(false); + if let Ok((_slider, mut drag)) = q_slider.get_mut(event.entity()) { + event.propagate(false); if drag.dragging { drag.dragging = false; } @@ -408,7 +410,7 @@ pub(crate) fn slider_on_drag_end( } fn slider_on_key_input( - mut trigger: On>, + mut event: On>, q_slider: Query<( &CoreSlider, &SliderValue, @@ -418,10 +420,10 @@ fn slider_on_key_input( )>, mut commands: Commands, ) { - if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target()) { - let event = &trigger.event().input; - if !disabled && event.state == ButtonState::Pressed { - let new_value = match event.key_code { + if let Ok((slider, value, range, step, disabled)) = q_slider.get(event.entity()) { + let input_event = &event.input; + if !disabled && input_event.state == ButtonState::Pressed { + let new_value = match input_event.key_code { KeyCode::ArrowLeft => range.clamp(value.0 - step.0), KeyCode::ArrowRight => range.clamp(value.0 + step.0), KeyCode::Home => range.start(), @@ -430,16 +432,16 @@ fn slider_on_key_input( return; } }; - trigger.propagate(false); + event.propagate(false); if matches!(slider.on_change, Callback::Ignore) { commands - .entity(trigger.target()) + .entity(event.entity()) .insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: trigger.target(), + source: event.entity(), value: new_value, }, ); @@ -448,23 +450,23 @@ fn slider_on_key_input( } } -pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target()); +pub(crate) fn slider_on_insert(event: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(event.entity()); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_orientation(Orientation::Horizontal); } } -pub(crate) fn slider_on_insert_value(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target()); +pub(crate) fn slider_on_insert_value(event: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(event.entity()); let value = entity.get::().unwrap().0; if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_numeric_value(value.into()); } } -pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target()); +pub(crate) fn slider_on_insert_range(event: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(event.entity()); let range = *entity.get::().unwrap(); if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_min_numeric_value(range.start().into()); @@ -472,8 +474,8 @@ pub(crate) fn slider_on_insert_range(trigger: On, mut world } } -pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) { - let mut entity = world.entity_mut(trigger.target()); +pub(crate) fn slider_on_insert_step(event: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(event.entity()); let step = entity.get::().unwrap().0; if let Some(mut accessibility) = entity.get_mut::() { accessibility.set_numeric_value_step(step.into()); @@ -516,13 +518,13 @@ pub enum SetSliderValue { } fn slider_on_set_value( - mut trigger: On, + mut event: On, q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, mut commands: Commands, ) { - if let Ok((slider, value, range, step)) = q_slider.get(trigger.target()) { - trigger.propagate(false); - let new_value = match trigger.event() { + if let Ok((slider, value, range, step)) = q_slider.get(event.entity()) { + event.propagate(false); + let new_value = match event.event() { SetSliderValue::Absolute(new_value) => range.clamp(*new_value), SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta), SetSliderValue::RelativeStep(delta) => { @@ -531,13 +533,13 @@ fn slider_on_set_value( }; if matches!(slider.on_change, Callback::Ignore) { commands - .entity(trigger.target()) + .entity(event.entity()) .insert(SliderValue(new_value)); } else { commands.notify_with( &slider.on_change, ValueChange { - source: trigger.target(), + source: event.entity(), value: new_value, }, ); diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 5abd1fe7a7fe7..de04e5ec9946b 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -69,14 +69,14 @@ log = { version = "0.4", default-features = false } # macOS [target.'cfg(all(target_os="macos"))'.dependencies] # Some features of sysinfo are not supported by apple. This will disable those features on apple devices -sysinfo = { version = "0.36.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.37.0", optional = true, default-features = false, features = [ "apple-app-store", "system", ] } # Only include when on linux/windows/android/freebsd [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android", target_os = "freebsd"))'.dependencies] -sysinfo = { version = "0.36.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.37.0", optional = true, default-features = false, features = [ "system", ] } diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index 302ab44bfe23c..9ca4e9300044a 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -313,8 +313,8 @@ struct Speak { let mut world = World::new(); -world.add_observer(|trigger: On| { - println!("{}", trigger.message); +world.add_observer(|event: On| { + println!("{}", event.message); }); world.flush(); @@ -339,9 +339,9 @@ struct Explode; let mut world = World::new(); let entity = world.spawn_empty().id(); -world.add_observer(|trigger: On, mut commands: Commands| { - println!("Entity {} goes BOOM!", trigger.target()); - commands.entity(trigger.target()).despawn(); +world.add_observer(|event: On, mut commands: Commands| { + println!("Entity {} goes BOOM!", event.entity()); + commands.entity(event.entity()).despawn(); }); world.flush(); diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index f682554ce9a51..bbb22b59b8f2d 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -137,16 +137,16 @@ pub(crate) struct ArchetypeAfterBundleInsert { pub archetype_id: ArchetypeId, /// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle), /// indicate if the component is newly added to the target archetype or if it already existed. - pub bundle_status: Vec, + bundle_status: Box<[ComponentStatus]>, /// The set of additional required components that must be initialized immediately when adding this Bundle. /// /// The initial values are determined based on the provided constructor, falling back to the `Default` trait if none is given. - pub required_components: Vec, + pub required_components: Box<[RequiredComponentConstructor]>, /// The components added by this bundle. This includes any Required Components that are inserted when adding this bundle. - pub added: Vec, + added: Box<[ComponentId]>, /// The components that were explicitly contributed by this bundle, but already existed in the archetype. This _does not_ include any /// Required Components. - pub existing: Vec, + existing: Box<[ComponentId]>, } impl ArchetypeAfterBundleInsert { @@ -242,19 +242,19 @@ impl Edges { &mut self, bundle_id: BundleId, archetype_id: ArchetypeId, - bundle_status: Vec, - required_components: Vec, - added: Vec, - existing: Vec, + bundle_status: impl Into>, + required_components: impl Into>, + added: impl Into>, + existing: impl Into>, ) { self.insert_bundle.insert( bundle_id, ArchetypeAfterBundleInsert { archetype_id, - bundle_status, - required_components, - added, - existing, + bundle_status: bundle_status.into(), + required_components: required_components.into(), + added: added.into(), + existing: existing.into(), }, ); } diff --git a/crates/bevy_ecs/src/bundle/impls.rs b/crates/bevy_ecs/src/bundle/impls.rs index ca6525247e8ec..9b67c7d4be2a8 100644 --- a/crates/bevy_ecs/src/bundle/impls.rs +++ b/crates/bevy_ecs/src/bundle/impls.rs @@ -148,11 +148,12 @@ all_tuples!( ); macro_rules! after_effect_impl { - ($($after_effect: ident),*) => { + ($(#[$meta:meta])* $($after_effect: ident),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such, the lints below may not always apply." )] + $(#[$meta])* impl<$($after_effect: BundleEffect),*> BundleEffect for ($($after_effect,)*) { #[allow( clippy::unused_unit, @@ -168,8 +169,15 @@ macro_rules! after_effect_impl { } } + $(#[$meta])* impl<$($after_effect: NoBundleEffect),*> NoBundleEffect for ($($after_effect,)*) { } } } -all_tuples!(after_effect_impl, 0, 15, P); +all_tuples!( + #[doc(fake_variadic)] + after_effect_impl, + 0, + 15, + P +); diff --git a/crates/bevy_ecs/src/bundle/info.rs b/crates/bevy_ecs/src/bundle/info.rs index 58a296a067519..4990a2ffbb004 100644 --- a/crates/bevy_ecs/src/bundle/info.rs +++ b/crates/bevy_ecs/src/bundle/info.rs @@ -72,10 +72,10 @@ pub struct BundleInfo { /// must have its storage initialized (i.e. columns created in tables, sparse set created), /// and the range (0..`explicit_components_len`) must be in the same order as the source bundle /// type writes its components in. - pub(super) contributed_component_ids: Vec, + pub(super) contributed_component_ids: Box<[ComponentId]>, /// The list of constructors for all required components indirectly contributed by this bundle. - pub(super) required_component_constructors: Vec, + pub(super) required_component_constructors: Box<[RequiredComponentConstructor]>, } impl BundleInfo { @@ -142,7 +142,7 @@ impl BundleInfo { component_ids.push(required_id); }) .map(|(_, required_component)| required_component.constructor) - .collect::>(); + .collect::>(); // SAFETY: The caller ensures that component_ids: // - is valid for the associated world @@ -150,7 +150,7 @@ impl BundleInfo { // - is in the same order as the source bundle type BundleInfo { id, - contributed_component_ids: component_ids, + contributed_component_ids: component_ids.into(), required_component_constructors: required_components, } } @@ -418,7 +418,14 @@ impl Bundles { /// Registers a new [`BundleInfo`] for a statically known type. /// /// Also registers all the components in the bundle. - pub(crate) fn register_info( + /// + /// # Safety + /// + /// `components` and `storages` must be from the same [`World`] as `self`. + /// + /// [`World`]: crate::world::World + #[deny(unsafe_op_in_unsafe_fn)] + pub(crate) unsafe fn register_info( &mut self, components: &mut ComponentsRegistrator, storages: &mut Storages, @@ -442,7 +449,14 @@ impl Bundles { /// Registers a new [`BundleInfo`], which contains both explicit and required components for a statically known type. /// /// Also registers all the components in the bundle. - pub(crate) fn register_contributed_bundle_info( + /// + /// # Safety + /// + /// `components` and `storages` must be from the same [`World`] as `self`. + /// + /// [`World`]: crate::world::World + #[deny(unsafe_op_in_unsafe_fn)] + pub(crate) unsafe fn register_contributed_bundle_info( &mut self, components: &mut ComponentsRegistrator, storages: &mut Storages, @@ -450,7 +464,10 @@ impl Bundles { if let Some(id) = self.contributed_bundle_ids.get(&TypeId::of::()).cloned() { id } else { - let explicit_bundle_id = self.register_info::(components, storages); + // SAFETY: as per the guarantees of this function, components and + // storages are from the same world as self + let explicit_bundle_id = unsafe { self.register_info::(components, storages) }; + // SAFETY: reading from `explicit_bundle_id` and creating new bundle in same time. Its valid because bundle hashmap allow this let id = unsafe { let (ptr, len) = { diff --git a/crates/bevy_ecs/src/bundle/insert.rs b/crates/bevy_ecs/src/bundle/insert.rs index 0388b5e6fd87c..ed7667c4c8183 100644 --- a/crates/bevy_ecs/src/bundle/insert.rs +++ b/crates/bevy_ecs/src/bundle/insert.rs @@ -40,9 +40,13 @@ impl<'w> BundleInserter<'w> { // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; - let bundle_id = world - .bundles - .register_info::(&mut registrator, &mut world.storages); + + // SAFETY: `registrator`, `world.bundles`, and `world.storages` all come from the same world + let bundle_id = unsafe { + world + .bundles + .register_info::(&mut registrator, &mut world.storages) + }; // SAFETY: We just ensured this bundle exists unsafe { Self::new_with_id(world, archetype_id, bundle_id, change_tick) } } diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index c0d2bd765a11a..fe8edb2eaea84 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -33,6 +33,7 @@ impl<'w> BundleRemover<'w> { /// # Safety /// Caller must ensure that `archetype_id` is valid #[inline] + #[deny(unsafe_op_in_unsafe_fn)] pub(crate) unsafe fn new( world: &'w mut World, archetype_id: ArchetypeId, @@ -41,9 +42,13 @@ impl<'w> BundleRemover<'w> { // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; - let bundle_id = world - .bundles - .register_info::(&mut registrator, &mut world.storages); + + // SAFETY: `registrator`, `world.storages`, and `world.bundles` all come from the same world. + let bundle_id = unsafe { + world + .bundles + .register_info::(&mut registrator, &mut world.storages) + }; // SAFETY: we initialized this bundle_id in `init_info`, and caller ensures archetype is valid. unsafe { Self::new_with_id(world, archetype_id, bundle_id, require_all) } } diff --git a/crates/bevy_ecs/src/bundle/spawner.rs b/crates/bevy_ecs/src/bundle/spawner.rs index 407bfda8facc4..9cc4fe0dee03c 100644 --- a/crates/bevy_ecs/src/bundle/spawner.rs +++ b/crates/bevy_ecs/src/bundle/spawner.rs @@ -29,9 +29,13 @@ impl<'w> BundleSpawner<'w> { // SAFETY: These come from the same world. `world.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut world.components, &mut world.component_ids) }; - let bundle_id = world - .bundles - .register_info::(&mut registrator, &mut world.storages); + + // SAFETY: `registrator`, `world.bundles`, and `world.storages` all come from the same world. + let bundle_id = unsafe { + world + .bundles + .register_info::(&mut registrator, &mut world.storages) + }; // SAFETY: we initialized this bundle_id in `init_info` unsafe { Self::new_with_id(world, bundle_id, change_tick) } } diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index b2e78d79c1084..2f7000357f08f 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -8,7 +8,7 @@ use derive_more::derive::From; use crate::{ archetype::Archetype, - bundle::{Bundle, BundleId, BundleRemover, InsertMode}, + bundle::{Bundle, BundleRemover, InsertMode}, change_detection::MaybeLocation, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, @@ -426,7 +426,7 @@ impl<'a> BundleScratch<'a> { relationship_hook_insert_mode: RelationshipHookMode, ) { // SAFETY: - // - All `component_ids` are from the same world as `target` entity + // - All `component_ids` are from the same world as `entity` // - All `component_data_ptrs` are valid types represented by `component_ids` unsafe { world.entity_mut(entity).insert_by_ids_internal( @@ -917,7 +917,7 @@ impl<'w> EntityClonerBuilder<'w, OptOut> { } /// Extends the list of components that shouldn't be cloned. - /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`](`crate::bundle::BundleId`), and [`IntoIterator`] yielding one of these. /// /// If component `A` is denied here and component `B` requires `A`, then `A` /// is denied as well. See [`Self::without_required_by_components`] to alter @@ -985,7 +985,7 @@ impl<'w> EntityClonerBuilder<'w, OptIn> { } /// Extends the list of components to clone. - /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`](`crate::bundle::BundleId`), and [`IntoIterator`] yielding one of these. /// /// If component `A` is allowed here and requires component `B`, then `B` /// is allowed as well. See [`Self::without_required_components`] @@ -996,7 +996,7 @@ impl<'w> EntityClonerBuilder<'w, OptIn> { } /// Extends the list of components to clone if the target does not contain them. - /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these. + /// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`](`crate::bundle::BundleId`), and [`IntoIterator`] yielding one of these. /// /// If component `A` is allowed here and requires component `B`, then `B` /// is allowed as well. See [`Self::without_required_components`] @@ -1376,7 +1376,9 @@ impl Required { } mod private { - use super::*; + use crate::{bundle::BundleId, component::ComponentId}; + use core::any::TypeId; + use derive_more::From; /// Marker trait to allow multiple blanket implementations for [`FilterableIds`]. pub trait Marker {} @@ -1387,7 +1389,7 @@ mod private { pub struct VectorType {} impl Marker for VectorType {} - /// Defines types of ids that [`EntityClonerBuilder`] can filter components by. + /// Defines types of ids that [`EntityClonerBuilder`](`super::EntityClonerBuilder`) can filter components by. #[derive(From)] pub enum FilterableId { Type(TypeId), @@ -1405,7 +1407,7 @@ mod private { } } - /// A trait to allow [`EntityClonerBuilder`] filter by any supported id type and their iterators, + /// A trait to allow [`EntityClonerBuilder`](`super::EntityClonerBuilder`) filter by any supported id type and their iterators, /// reducing the number of method permutations required for all id types. /// /// The supported id types that can be used to filter components are defined by [`FilterableId`], which allows following types: [`TypeId`], [`ComponentId`] and [`BundleId`]. diff --git a/crates/bevy_ecs/src/entity/unique_slice.rs b/crates/bevy_ecs/src/entity/unique_slice.rs index e45c3a21c06bf..26ebe8674f498 100644 --- a/crates/bevy_ecs/src/entity/unique_slice.rs +++ b/crates/bevy_ecs/src/entity/unique_slice.rs @@ -913,7 +913,7 @@ pub const unsafe fn from_raw_parts_mut<'a, T: EntityEquivalent>( /// /// # Safety /// -/// All elements in each of the casted slices must be unique. +/// All elements in each of the cast slices must be unique. pub unsafe fn cast_slice_of_unique_entity_slice<'a, 'b, T: EntityEquivalent + 'a>( slice: &'b [&'a [T]], ) -> &'b [&'a UniqueEntityEquivalentSlice] { @@ -925,7 +925,7 @@ pub unsafe fn cast_slice_of_unique_entity_slice<'a, 'b, T: EntityEquivalent + 'a /// /// # Safety /// -/// All elements in each of the casted slices must be unique. +/// All elements in each of the cast slices must be unique. pub unsafe fn cast_slice_of_unique_entity_slice_mut<'a, 'b, T: EntityEquivalent + 'a>( slice: &'b mut [&'a [T]], ) -> &'b mut [&'a UniqueEntityEquivalentSlice] { @@ -937,7 +937,7 @@ pub unsafe fn cast_slice_of_unique_entity_slice_mut<'a, 'b, T: EntityEquivalent /// /// # Safety /// -/// All elements in each of the casted slices must be unique. +/// All elements in each of the cast slices must be unique. pub unsafe fn cast_slice_of_mut_unique_entity_slice_mut<'a, 'b, T: EntityEquivalent + 'a>( slice: &'b mut [&'a mut [T]], ) -> &'b mut [&'a mut UniqueEntityEquivalentSlice] { diff --git a/crates/bevy_ecs/src/entity_disabling.rs b/crates/bevy_ecs/src/entity_disabling.rs index d60f3ab08bdf6..c2fc5ee6c53b9 100644 --- a/crates/bevy_ecs/src/entity_disabling.rs +++ b/crates/bevy_ecs/src/entity_disabling.rs @@ -44,7 +44,7 @@ //! even if they have a `Position` component, //! but `Query<&Position, With>` or `Query<(&Position, Has)>` will see them. //! -//! The [`Allows`](crate::query::Allows) query filter is designed to be used with default query filters, +//! The [`Allow`](crate::query::Allow) query filter is designed to be used with default query filters, //! and ensures that the query will include entities both with and without the specified disabling component. //! //! Entities with disabling components are still present in the [`World`] and can be accessed directly, @@ -161,9 +161,9 @@ pub struct Internal; /// To be more precise, this checks if the query's [`FilteredAccess`] contains the component, /// and if it does not, adds a [`Without`](crate::prelude::Without) filter for that component to the query. /// -/// [`Allows`](crate::query::Allows) and [`Has`](crate::prelude::Has) can be used to include entities +/// [`Allow`](crate::query::Allow) and [`Has`](crate::prelude::Has) can be used to include entities /// with and without the disabling component. -/// [`Allows`](crate::query::Allows) is a [`QueryFilter`](crate::query::QueryFilter) and will simply change +/// [`Allow`](crate::query::Allow) is a [`QueryFilter`](crate::query::QueryFilter) and will simply change /// the list of shown entities, while [`Has`](crate::prelude::Has) is a [`QueryData`](crate::query::QueryData) /// and will allow you to see if each entity has the disabling component or not. /// diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index a5924d60f4a07..2ba0de493eb7a 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -53,8 +53,8 @@ use core::{ /// # /// # let mut world = World::new(); /// # -/// world.add_observer(|trigger: On| { -/// println!("{}", trigger.message); +/// world.add_observer(|event: On| { +/// println!("{}", event.message); /// }); /// ``` /// @@ -70,8 +70,8 @@ use core::{ /// # /// # let mut world = World::new(); /// # -/// # world.add_observer(|trigger: On| { -/// # println!("{}", trigger.message); +/// # world.add_observer(|event: On| { +/// # println!("{}", event.message); /// # }); /// # /// # world.flush(); @@ -187,10 +187,10 @@ pub trait Event: Send + Sync + 'static { /// // which can then handle the event with its own observer. /// let armor_piece = world /// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) -/// .observe(|trigger: On, mut query: Query<&mut Health>| { -/// // Note: `On::target` only exists because this is an `EntityEvent`. -/// let mut health = query.get_mut(trigger.target()).unwrap(); -/// health.0 -= trigger.amount; +/// .observe(|event: On, mut query: Query<&mut Health>| { +/// // Note: `On::entity` only exists because this is an `EntityEvent`. +/// let mut health = query.get_mut(event.entity()).unwrap(); +/// health.0 -= event.amount; /// }) /// .id(); /// ``` @@ -221,10 +221,10 @@ pub trait Event: Send + Sync + 'static { /// # let enemy = world.spawn((Enemy, Health(100.0))).id(); /// # let armor_piece = world /// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) -/// # .observe(|trigger: On, mut query: Query<&mut Health>| { -/// # // Note: `On::target` only exists because this is an `EntityEvent`. -/// # let mut health = query.get_mut(trigger.target()).unwrap(); -/// # health.0 -= trigger.amount; +/// # .observe(|event: On, mut query: Query<&mut Health>| { +/// # // Note: `On::entity` only exists because this is an `EntityEvent`. +/// # let mut health = query.get_mut(event.entity()).unwrap(); +/// # health.0 -= event.amount; /// # }) /// # .id(); /// # diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 5307b3785e7cc..cb0c40913c042 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -89,7 +89,7 @@ pub mod prelude { }, name::{Name, NameOrEntity}, observer::{Observer, On, Trigger}, - query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, + query::{Added, Allow, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, resource::Resource, @@ -1946,15 +1946,27 @@ mod tests { #[derive(Bundle)] struct Simple(ComponentA); + #[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(Bundle)] struct Tuple(Simple, ComponentB); + #[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(Bundle)] struct Record { field0: Simple, field1: ComponentB, } + #[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 MyEntities { #[entities] @@ -1963,10 +1975,6 @@ mod tests { another_one: Entity, #[entities] 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." - )] something_else: String, } @@ -1981,22 +1989,42 @@ mod tests { fn clone_entities() { use crate::entity::{ComponentCloneCtx, SourceComponent}; + #[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." + )] #[derive(Component)] #[component(clone_behavior = Ignore)] struct IgnoreClone; + #[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." + )] #[derive(Component)] #[component(clone_behavior = Default)] struct DefaultClone; + #[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." + )] #[derive(Component)] #[component(clone_behavior = Custom(custom_clone))] struct CustomClone; + #[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." + )] #[derive(Component, Clone)] #[component(clone_behavior = clone::())] struct CloneFunction; + #[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." + )] fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} } diff --git a/crates/bevy_ecs/src/observer/centralized_storage.rs b/crates/bevy_ecs/src/observer/centralized_storage.rs index 544f2f1f6ade7..2e3b78a8daf83 100644 --- a/crates/bevy_ecs/src/observer/centralized_storage.rs +++ b/crates/bevy_ecs/src/observer/centralized_storage.rs @@ -76,7 +76,7 @@ impl Observers { mut world: DeferredWorld, event_key: EventKey, current_target: Option, - original_target: Option, + original_entity: Option, components: impl Iterator + Clone, data: &mut T, propagate: &mut bool, @@ -104,8 +104,8 @@ impl Observers { observer, event_key, components: components.clone().collect(), - current_target, - original_target, + entity: current_target, + original_entity, caller, }, data.into(), diff --git a/crates/bevy_ecs/src/observer/distributed_storage.rs b/crates/bevy_ecs/src/observer/distributed_storage.rs index 37ee4f4d6b83e..4610e26047b8b 100644 --- a/crates/bevy_ecs/src/observer/distributed_storage.rs +++ b/crates/bevy_ecs/src/observer/distributed_storage.rs @@ -52,8 +52,8 @@ use crate::prelude::ReflectComponent; /// message: String, /// } /// -/// world.add_observer(|trigger: On| { -/// println!("{}", trigger.event().message); +/// world.add_observer(|event: On| { +/// println!("{}", event.message); /// }); /// /// // Observers currently require a flush() to be registered. In the context of schedules, @@ -73,8 +73,8 @@ use crate::prelude::ReflectComponent; /// # #[derive(Event)] /// # struct Speak; /// // These are functionally the same: -/// world.add_observer(|trigger: On| {}); -/// world.spawn(Observer::new(|trigger: On| {})); +/// world.add_observer(|event: On| {}); +/// world.spawn(Observer::new(|event: On| {})); /// ``` /// /// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: @@ -86,7 +86,7 @@ use crate::prelude::ReflectComponent; /// # struct PrintNames; /// # #[derive(Component, Debug)] /// # struct Name; -/// world.add_observer(|trigger: On, names: Query<&Name>| { +/// world.add_observer(|event: On, names: Query<&Name>| { /// for name in &names { /// println!("{name:?}"); /// } @@ -104,7 +104,7 @@ use crate::prelude::ReflectComponent; /// # struct SpawnThing; /// # #[derive(Component, Debug)] /// # struct Thing; -/// world.add_observer(|trigger: On, mut commands: Commands| { +/// world.add_observer(|event: On, mut commands: Commands| { /// commands.spawn(Thing); /// }); /// ``` @@ -118,7 +118,7 @@ use crate::prelude::ReflectComponent; /// # struct A; /// # #[derive(Event)] /// # struct B; -/// world.add_observer(|trigger: On, mut commands: Commands| { +/// world.add_observer(|event: On, mut commands: Commands| { /// commands.trigger(B); /// }); /// ``` @@ -137,9 +137,9 @@ use crate::prelude::ReflectComponent; /// #[derive(EntityEvent)] /// struct Explode; /// -/// world.add_observer(|trigger: On, mut commands: Commands| { -/// println!("Entity {} goes BOOM!", trigger.target()); -/// commands.entity(trigger.target()).despawn(); +/// world.add_observer(|event: On, mut commands: Commands| { +/// println!("Entity {} goes BOOM!", event.entity()); +/// commands.entity(event.entity()).despawn(); /// }); /// /// world.flush(); @@ -170,12 +170,12 @@ use crate::prelude::ReflectComponent; /// # let e2 = world.spawn_empty().id(); /// # #[derive(EntityEvent)] /// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { +/// world.entity_mut(e1).observe(|event: On, mut commands: Commands| { /// println!("Boom!"); -/// commands.entity(trigger.target()).despawn(); +/// commands.entity(event.entity()).despawn(); /// }); /// -/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { +/// world.entity_mut(e2).observe(|event: On, mut commands: Commands| { /// println!("The explosion fizzles! This entity is immune!"); /// }); /// ``` @@ -192,7 +192,7 @@ use crate::prelude::ReflectComponent; /// # let entity = world.spawn_empty().id(); /// # #[derive(EntityEvent)] /// # struct Explode; -/// let mut observer = Observer::new(|trigger: On| {}); +/// let mut observer = Observer::new(|event: On| {}); /// observer.watch_entity(entity); /// world.spawn(observer); /// ``` diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 40a4ee3db6a06..fe15a67e17c87 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -25,7 +25,7 @@ //! Observers can request other data from the world, such as via a [`Query`] or [`Res`]. //! Commonly, you might want to verify that the entity that the observable event is targeting //! has a specific component, or meets some other condition. [`Query::get`] or [`Query::contains`] -//! on the [`On::target`] entity is a good way to do this. +//! on the [`On::entity`] entity is a good way to do this. //! //! [`Commands`] can also be used inside of observers. //! This can be particularly useful for triggering other observers! @@ -641,20 +641,20 @@ mod tests { world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.target()).insert(B); + commands.entity(obs.entity()).insert(B); }, ); world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.entity()).remove::(); }, ); world.add_observer( |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.entity()).remove::(); }, ); world.add_observer(|_: On, mut res: ResMut| { @@ -675,9 +675,9 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); + world.add_observer(|mut event: On| event.counter += 1); + world.add_observer(|mut event: On| event.counter += 2); + world.add_observer(|mut event: On| event.counter += 4); let mut event = EventWithData { counter: 0 }; world.trigger_ref(&mut event); @@ -688,14 +688,14 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: On| { - trigger.event_mut().counter += 1; + world.add_observer(|mut event: On| { + event.counter += 1; }); - world.add_observer(|mut trigger: On| { - trigger.event_mut().counter += 2; + world.add_observer(|mut event: On| { + event.counter += 2; }); - world.add_observer(|mut trigger: On| { - trigger.event_mut().counter += 4; + world.add_observer(|mut event: On| { + event.counter += 4; }); let mut event = EventWithData { counter: 0 }; @@ -718,7 +718,7 @@ mod tests { assert_eq!(world.query::<&A>().query(&world).count(), 1); assert_eq!( world - .query_filtered::<&Observer, Allows>() + .query_filtered::<&Observer, Allow>() .query(&world) .count(), 2 @@ -823,7 +823,7 @@ mod tests { }; world.spawn_empty().observe(system); world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.target(), Entity::PLACEHOLDER); + assert_eq!(obs.entity(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -846,7 +846,7 @@ mod tests { .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |obs: On, mut res: ResMut| { - assert_eq!(obs.target(), entity); + assert_eq!(obs.entity(), entity); res.observed("a_2"); }); @@ -1011,19 +1011,19 @@ mod tests { let child = world.spawn(ChildOf(parent)).id(); world.entity_mut(parent).observe( - move |trigger: On, mut res: ResMut| { + move |event: On, mut res: ResMut| { res.observed("parent"); - assert_eq!(trigger.target(), parent); - assert_eq!(trigger.original_target(), child); + assert_eq!(event.entity(), parent); + assert_eq!(event.original_entity(), child); }, ); world.entity_mut(child).observe( - move |trigger: On, mut res: ResMut| { + move |event: On, mut res: ResMut| { res.observed("child"); - assert_eq!(trigger.target(), child); - assert_eq!(trigger.original_target(), child); + assert_eq!(event.entity(), child); + assert_eq!(event.original_entity(), child); }, ); @@ -1099,12 +1099,10 @@ mod tests { let child = world .spawn(ChildOf(parent)) - .observe( - |mut trigger: On, mut res: ResMut| { - res.observed("child"); - trigger.propagate(false); - }, - ) + .observe(|mut event: On, mut res: ResMut| { + res.observed("child"); + event.propagate(false); + }) .id(); world.trigger_targets(EventPropagating, child); @@ -1176,12 +1174,10 @@ mod tests { let child_a = world .spawn(ChildOf(parent_a)) - .observe( - |mut trigger: On, mut res: ResMut| { - res.observed("child_a"); - trigger.propagate(false); - }, - ) + .observe(|mut event: On, mut res: ResMut| { + res.observed("child_a"); + event.propagate(false); + }) .id(); let parent_b = world @@ -1230,8 +1226,8 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: On, query: Query<&A>, mut res: ResMut| { - if query.get(trigger.target()).is_ok() { + |event: On, query: Query<&A>, mut res: ResMut| { + if query.get(event.entity()).is_ok() { res.observed("event"); } }, @@ -1249,9 +1245,9 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(trigger: On, mut commands: Commands) { + fn on_add(event: On, mut commands: Commands) { commands - .entity(trigger.target()) + .entity(event.entity()) .with_related_entities::(|rsc| { rsc.spawn_empty(); }); @@ -1327,8 +1323,8 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: On| { - assert_eq!(trigger.caller(), caller); + world.add_observer(move |event: On| { + assert_eq!(event.caller(), caller); }); world.trigger(EventA); } @@ -1341,11 +1337,11 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: On| { - assert_eq!(trigger.caller(), caller); + world.add_observer(move |event: On| { + assert_eq!(event.caller(), caller); }); - world.add_observer(move |trigger: On| { - assert_eq!(trigger.caller(), caller); + world.add_observer(move |event: On| { + assert_eq!(event.caller(), caller); }); world.commands().spawn(Component).clear(); } @@ -1360,13 +1356,11 @@ mod tests { let a_id = world.register_component::(); let b_id = world.register_component::(); - world.add_observer( - |trigger: On, mut counter: ResMut| { - for &component in trigger.components() { - *counter.0.entry(component).or_default() += 1; - } - }, - ); + world.add_observer(|event: On, mut counter: ResMut| { + for &component in event.components() { + *counter.0.entry(component).or_default() += 1; + } + }); world.trigger_targets(EventA, [a_id, b_id]); world.trigger_targets(EventA, a_id); @@ -1396,4 +1390,48 @@ mod tests { world.trigger_targets(EventA, [entities[2], entities[3]]); assert_eq!(vec!["a", "a"], world.resource::().0); } + + #[test] + fn unregister_global_observer() { + let mut world = World::new(); + let mut observer = world.add_observer(|_: On| {}); + observer.remove::(); + let id = observer.id(); + let event_key = EventA::event_key(&world).unwrap(); + assert!(!world + .observers + .get_observers_mut(event_key) + .global_observers + .contains_key(&id)); + } + + #[test] + fn unregister_entity_observer() { + let mut world = World::new(); + let entity = world.spawn_empty().id(); + let observer = Observer::new(|_: On| {}).with_entity(entity); + let mut observer = world.spawn(observer); + observer.remove::(); + let event_key = EventA::event_key(&world).unwrap(); + assert!(!world + .observers + .get_observers_mut(event_key) + .entity_observers + .contains_key(&entity)); + } + + #[test] + fn unregister_component_observer() { + let mut world = World::new(); + let a = world.register_component::(); + let observer = Observer::new(|_: On| {}).with_component(a); + let mut observer = world.spawn(observer); + observer.remove::(); + let event_key = EventA::event_key(&world).unwrap(); + assert!(!world + .observers + .get_observers_mut(event_key) + .get_component_observers() + .contains_key(&a)); + } } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index d843fb4589090..7f2536f5f8318 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -41,7 +41,7 @@ pub(super) fn observer_system_runner = On::new( + let on: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -78,7 +78,7 @@ pub(super) fn observer_system_runner On<'w, E, B> { /// #[derive(EntityEvent)] /// struct AssertEvent; /// - /// fn assert_observer(trigger: On) { - /// assert_eq!(trigger.observer(), trigger.target()); + /// fn assert_observer(event: On) { + /// assert_eq!(event.observer(), event.entity()); /// } /// /// let mut world = World::new(); @@ -109,20 +109,20 @@ impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. /// /// Note that if event propagation is enabled, this may not be the same as the original target of the event, - /// which can be accessed via [`On::original_target`]. + /// which can be accessed via [`On::original_entity`]. /// /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. - pub fn target(&self) -> Entity { - self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + pub fn entity(&self) -> Entity { + self.trigger.entity.unwrap_or(Entity::PLACEHOLDER) } /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. /// - /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// If event propagation is not enabled, this will always return the same value as [`On::entity`]. /// /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. - pub fn original_target(&self) -> Entity { - self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + pub fn original_entity(&self) -> Entity { + self.trigger.original_entity.unwrap_or(Entity::PLACEHOLDER) } /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. @@ -187,13 +187,13 @@ pub struct ObserverTrigger { pub components: SmallVec<[ComponentId; 2]>, /// The entity that the entity-event targeted, if any. /// - /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. - pub current_target: Option, + /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_entity`]. + pub entity: Option, /// The entity that the entity-event was originally targeted at, if any. /// /// If event propagation is enabled, this will be the first entity that the event was targeted at, /// even if the event was propagated to other entities. - pub original_target: Option, + pub original_entity: Option, /// The location of the source code that triggered the observer. pub caller: MaybeLocation, } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 346c131457bb7..6a0e77a632c03 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -263,10 +263,10 @@ impl Access { /// This is for components whose values are not accessed (and thus will never cause conflicts), /// but whose presence in an archetype may affect query results. /// - /// Currently, this is only used for [`Has`] and [`Allows`]. + /// Currently, this is only used for [`Has`] and [`Allow`]. /// /// [`Has`]: crate::query::Has - /// [`Allows`]: crate::query::filter::Allows + /// [`Allow`]: crate::query::filter::Allow pub fn add_archetypal(&mut self, index: ComponentId) { self.archetypal.grow_and_insert(index.index()); } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4beb4e9a62044..0e868e0be3496 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2530,6 +2530,7 @@ macro_rules! impl_tuple_query_data { } } + $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} @@ -2541,6 +2542,7 @@ macro_rules! impl_tuple_query_data { 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])* impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for ($($name,)*) { fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { ($($name::release_state($item),)*) diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index 6269fc95b8db9..dbc6483d8021e 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -569,14 +569,14 @@ all_tuples!( /// Allows a query to contain entities with the component `T`, bypassing [`DefaultQueryFilters`]. /// /// [`DefaultQueryFilters`]: crate::entity_disabling::DefaultQueryFilters -pub struct Allows(PhantomData); +pub struct Allow(PhantomData); /// SAFETY: /// `update_component_access` does not add any accesses. /// This is sound because [`QueryFilter::filter_fetch`] does not access any components. /// `update_component_access` adds an archetypal filter for `T`. /// This is sound because it doesn't affect the query -unsafe impl WorldQuery for Allows { +unsafe impl WorldQuery for Allow { type Fetch<'w> = (); type State = ComponentId; @@ -608,13 +608,13 @@ unsafe impl WorldQuery for Allows { } fn matches_component_set(_: &ComponentId, _: &impl Fn(ComponentId) -> bool) -> bool { - // Allows always matches + // Allow always matches true } } // SAFETY: WorldQuery impl performs no access at all -unsafe impl QueryFilter for Allows { +unsafe impl QueryFilter for Allow { const IS_ARCHETYPAL: bool = true; #[inline(always)] diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index dd940b88c97cc..09821a718c668 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -2186,8 +2186,8 @@ mod tests { let mut query = QueryState::>::new(&mut world); assert_eq!(3, query.iter(&world).count()); - // Allows should bypass the filter entirely - let mut query = QueryState::<(), Allows>::new(&mut world); + // Allow should bypass the filter entirely + let mut query = QueryState::<(), Allow>::new(&mut world); assert_eq!(3, query.iter(&world).count()); // Other filters should still be respected diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index dce6237f1b857..c306723f3a707 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -106,53 +106,55 @@ pub fn from_reflect_with_fallback( world: &mut World, registry: &TypeRegistry, ) -> T { - fn different_type_error(reflected: &str) -> ! { - panic!( - "The registration for the reflected `{}` trait for the type `{}` produced \ - a value of a different type", - reflected, - T::type_path(), - ); - } - - // First, try `FromReflect`. This is handled differently from the others because - // it doesn't need a subsequent `apply` and may fail. - if let Some(reflect_from_reflect) = - registry.get_type_data::(TypeId::of::()) - { + #[inline(never)] + fn type_erased( + reflected: &dyn PartialReflect, + world: &mut World, + registry: &TypeRegistry, + id: TypeId, + name: DebugName, + ) -> alloc::boxed::Box { + // First, try `FromReflect`. This is handled differently from the others because + // it doesn't need a subsequent `apply` and may fail. // If it fails it's ok, we can continue checking `Default` and `FromWorld`. - if let Some(value) = reflect_from_reflect.from_reflect(reflected) { - return value - .take::() - .unwrap_or_else(|_| different_type_error::("FromReflect")); + let (value, source) = if let Some(value) = registry + .get_type_data::(id) + .and_then(|reflect_from_reflect| reflect_from_reflect.from_reflect(reflected)) + { + (value, "FromReflect") } - } - - // Create an instance of `T` using either the reflected `Default` or `FromWorld`. - let mut value = if let Some(reflect_default) = - registry.get_type_data::(TypeId::of::()) - { - reflect_default - .default() - .take::() - .unwrap_or_else(|_| different_type_error::("Default")) - } else if let Some(reflect_from_world) = - registry.get_type_data::(TypeId::of::()) - { - reflect_from_world - .from_world(world) - .take::() - .unwrap_or_else(|_| different_type_error::("FromWorld")) - } else { - panic!( - "Couldn't create an instance of `{}` using the reflected `FromReflect`, \ - `Default` or `FromWorld` traits. Are you perhaps missing a `#[reflect(Default)]` \ - or `#[reflect(FromWorld)]`?", - // FIXME: once we have unique reflect, use `TypePath`. - DebugName::type_name::(), + // Create an instance of `T` using either the reflected `Default` or `FromWorld`. + else if let Some(reflect_default) = registry.get_type_data::(id) { + let mut value = reflect_default.default(); + value.apply(reflected); + (value, "Default") + } else if let Some(reflect_from_world) = registry.get_type_data::(id) { + let mut value = reflect_from_world.from_world(world); + value.apply(reflected); + (value, "FromWorld") + } else { + panic!( + "Couldn't create an instance of `{name}` using the reflected `FromReflect`, \ + `Default` or `FromWorld` traits. Are you perhaps missing a `#[reflect(Default)]` \ + or `#[reflect(FromWorld)]`?", + ); + }; + assert_eq!( + value.as_any().type_id(), + id, + "The registration for the reflected `{source}` trait for the type `{name}` produced \ + a value of a different type", ); - }; - - value.apply(reflected); - value + value + } + *type_erased( + reflected, + world, + registry, + TypeId::of::(), + // FIXME: once we have unique reflect, use `TypePath`. + DebugName::type_name::(), + ) + .downcast::() + .unwrap() } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index dbbe015f6b7f5..7cd539c7312e1 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -905,10 +905,10 @@ mod tests { let result_entity = world.spawn(ObserverResult::default()).id(); world.add_observer( - move |trigger: On, + move |event: On, has_relationship: Query>, mut results: Query<&mut ObserverResult>| { - let entity = trigger.target(); + let entity = event.entity(); if has_relationship.get(entity).unwrap_or(false) { results.get_mut(result_entity).unwrap().success = true; } diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index fb971f1aa7afc..1867bfbe0480d 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1147,12 +1147,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input)? && b(input)?) + Ok(a(input, data)? && b(input, data)?) } } @@ -1168,12 +1169,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input)? && b(input)?)) + Ok(!(a(input, data)? && b(input, data)?)) } } @@ -1189,12 +1191,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input)? || b(input)?)) + Ok(!(a(input, data)? || b(input, data)?)) } } @@ -1210,12 +1213,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input)? || b(input)?) + Ok(a(input, data)? || b(input, data)?) } } @@ -1231,12 +1235,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(!(a(input)? ^ b(input)?)) + Ok(!(a(input, data)? ^ b(input, data)?)) } } @@ -1252,12 +1257,13 @@ where type In = In; type Out = bool; - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, A>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, ) -> Result { - Ok(a(input)? ^ b(input)?) + Ok(a(input, data)? ^ b(input, data)?) } } diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index b0a3e95cb7044..7c2d718ed6be8 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -373,9 +373,17 @@ mod tests { b: u32, } + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It won't be constructed." + )] #[derive(ScheduleLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyTupleLabel(); + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It won't be constructed." + )] #[derive(ScheduleLabel, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyStructLabel {} @@ -473,9 +481,17 @@ mod tests { b: u32, } + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It won't be constructed." + )] #[derive(SystemSet, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyTupleSet(); + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It won't be constructed." + )] #[derive(SystemSet, Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] struct EmptyStructSet {} diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index 72bdd752a9a32..23c373490138e 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -70,7 +70,7 @@ enum SystemIdentifier { Node(NodeId), } -/// Updates to [`Stepping.schedule_states`] that will be applied at the start +/// Updates to [`Stepping::schedule_states`] that will be applied at the start /// of the next render frame enum Update { /// Set the action stepping will perform for this render frame diff --git a/crates/bevy_ecs/src/spawn.rs b/crates/bevy_ecs/src/spawn.rs index bafd8c6ad0246..b2656f8192b5b 100644 --- a/crates/bevy_ecs/src/spawn.rs +++ b/crates/bevy_ecs/src/spawn.rs @@ -215,11 +215,12 @@ impl SpawnableList for WithOneRelated { } macro_rules! spawnable_list_impl { - ($($list: ident),*) => { + ($(#[$meta:meta])* $($list: ident),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such, the lints below may not always apply." )] + $(#[$meta])* impl),*> SpawnableList for ($($list,)*) { fn spawn(self, _world: &mut World, _entity: Entity) { #[allow( @@ -242,7 +243,13 @@ macro_rules! spawnable_list_impl { } } -all_tuples!(spawnable_list_impl, 0, 12, P); +all_tuples!( + #[doc(fake_variadic)] + spawnable_list_impl, + 0, + 12, + P +); /// A [`Bundle`] that: /// 1. Contains a [`RelationshipTarget`] component (associated with the given [`Relationship`]). This reserves space for the [`SpawnableList`]. diff --git a/crates/bevy_ecs/src/storage/blob_array.rs b/crates/bevy_ecs/src/storage/blob_array.rs index 9b738a763c8ce..14406006f27aa 100644 --- a/crates/bevy_ecs/src/storage/blob_array.rs +++ b/crates/bevy_ecs/src/storage/blob_array.rs @@ -258,9 +258,8 @@ impl BlobArray { #[cfg(debug_assertions)] debug_assert_eq!(self.capacity, current_capacity.get()); if !self.is_zst() { - // SAFETY: `new_capacity` can't overflow usize - let new_layout = - unsafe { array_layout_unchecked(&self.item_layout, new_capacity.get()) }; + let new_layout = array_layout(&self.item_layout, new_capacity.get()) + .expect("array layout should be valid"); // SAFETY: // - ptr was be allocated via this allocator // - the layout used to previously allocate this array is equivalent to `array_layout(&self.item_layout, current_capacity.get())` diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 548ba82103f8a..ee79cb9e280a0 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -132,9 +132,14 @@ impl TableRow { /// [`with_capacity`]: Self::with_capacity /// [`add_column`]: Self::add_column /// [`build`]: Self::build +// +// # Safety +// The capacity of all columns is determined by that of the `entities` Vec. This means that +// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This +// means the safety invariant must be enforced even in `TableBuilder`. pub(crate) struct TableBuilder { columns: SparseSet, - capacity: usize, + entities: Vec, } impl TableBuilder { @@ -142,7 +147,7 @@ impl TableBuilder { pub fn with_capacity(capacity: usize, column_capacity: usize) -> Self { Self { columns: SparseSet::with_capacity(column_capacity), - capacity, + entities: Vec::with_capacity(capacity), } } @@ -151,7 +156,7 @@ impl TableBuilder { pub fn add_column(mut self, component_info: &ComponentInfo) -> Self { self.columns.insert( component_info.id(), - ThinColumn::with_capacity(component_info, self.capacity), + ThinColumn::with_capacity(component_info, self.entities.capacity()), ); self } @@ -161,7 +166,7 @@ impl TableBuilder { pub fn build(self) -> Table { Table { columns: self.columns.into_immutable(), - entities: Vec::with_capacity(self.capacity), + entities: self.entities, } } } @@ -178,6 +183,11 @@ impl TableBuilder { /// [structure-of-arrays]: https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_arrays /// [`Component`]: crate::component::Component /// [`World`]: crate::world::World +// +// # Safety +// The capacity of all columns is determined by that of the `entities` Vec. This means that +// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This +// means the safety invariant must be enforced even in `TableBuilder`. pub struct Table { columns: ImmutableSparseSet, entities: Vec, @@ -529,6 +539,11 @@ impl Table { /// Allocate memory for the columns in the [`Table`] /// /// The current capacity of the columns should be 0, if it's not 0, then the previous data will be overwritten and leaked. + /// + /// # Safety + /// The capacity of all columns is determined by that of the `entities` Vec. This means that + /// it must be the correct capacity to allocate, reallocate, and deallocate all columns. This + /// means the safety invariant must be enforced even in `TableBuilder`. fn alloc_columns(&mut self, new_capacity: NonZeroUsize) { // If any of these allocations trigger an unwind, the wrong capacity will be used while dropping this table - UB. // To avoid this, we use `AbortOnPanic`. If the allocation triggered a panic, the `AbortOnPanic`'s Drop impl will be @@ -544,6 +559,10 @@ impl Table { /// /// # Safety /// - `current_column_capacity` is indeed the capacity of the columns + /// + /// The capacity of all columns is determined by that of the `entities` Vec. This means that + /// it must be the correct capacity to allocate, reallocate, and deallocate all columnts. This + /// means the safety invariant must be enforced even in `TableBuilder`. unsafe fn realloc_columns( &mut self, current_column_capacity: NonZeroUsize, diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 523c904c1dc7b..937911ca834c1 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -106,7 +106,7 @@ use super::{Res, ResMut, SystemState}; /// /// The implementor must ensure that the state returned /// from [`SystemParamBuilder::build`] is valid for `P`. -/// Note that the exact safety requiremensts depend on the implementation of [`SystemParam`], +/// Note that the exact safety requirements depend on the implementation of [`SystemParam`], /// so if `Self` is not a local type then you must call [`SystemParam::init_state`] /// or another [`SystemParamBuilder::build`]. pub unsafe trait SystemParamBuilder: Sized { diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index a2a3d0d79857c..1e6d9ee30a4dd 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -36,12 +36,13 @@ use super::{IntoSystem, ReadOnlySystem, RunSystemError, System}; /// type In = (); /// type Out = bool; /// -/// fn combine( +/// fn combine( /// _input: Self::In, -/// a: impl FnOnce(A::In) -> Result, -/// b: impl FnOnce(B::In) -> Result, +/// data: &mut T, +/// a: impl FnOnce(A::In, &mut T) -> Result, +/// b: impl FnOnce(B::In, &mut T) -> Result, /// ) -> Result { -/// Ok(a(())? ^ b(())?) +/// Ok(a((), data)? ^ b((), data)?) /// } /// } /// @@ -99,10 +100,11 @@ pub trait Combine { /// the two composite systems are invoked and their outputs are combined. /// /// See the trait-level docs for [`Combine`] for an example implementation. - fn combine( + fn combine( input: ::Inner<'_>, - a: impl FnOnce(SystemIn<'_, A>) -> Result, - b: impl FnOnce(SystemIn<'_, B>) -> Result, + data: &mut T, + a: impl FnOnce(SystemIn<'_, A>, &mut T) -> Result, + b: impl FnOnce(SystemIn<'_, B>, &mut T) -> Result, ) -> Result; } @@ -153,20 +155,25 @@ where input: SystemIn<'_, Self>, world: UnsafeWorldCell, ) -> Result { + struct PrivateUnsafeWorldCell<'w>(UnsafeWorldCell<'w>); + Func::combine( input, + &mut PrivateUnsafeWorldCell(world), // SAFETY: The world accesses for both underlying systems have been registered, // so the caller will guarantee that no other systems will conflict with `a` or `b`. // If either system has `is_exclusive()`, then the combined system also has `is_exclusive`. - // Since these closures are `!Send + !Sync + !'static`, they can never be called - // in parallel, so their world accesses will not conflict with each other. - |input| unsafe { self.a.run_unsafe(input, world) }, + // Since we require a `combine` to pass in a mutable reference to `world` and that's a private type + // passed to a function as an unbound non-'static generic argument, they can never be called in parallel + // or re-entrantly because that would require forging another instance of `PrivateUnsafeWorldCell`. + // This means that the world accesses in the two closures will not conflict with each other. + |input, world| unsafe { self.a.run_unsafe(input, world.0) }, // `Self::validate_param_unsafe` already validated the first system, // but we still need to validate the second system once the first one runs. // SAFETY: See the comment above. - |input| unsafe { - self.b.validate_param_unsafe(world)?; - self.b.run_unsafe(input, world) + |input, world| unsafe { + self.b.validate_param_unsafe(world.0)?; + self.b.run_unsafe(input, world.0) }, ) } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3f3e75c37f700..bf520223af5b8 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1086,6 +1086,13 @@ impl<'w, 's> Commands<'w, 's> { /// Sends a global [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// + /// If the entity that this command targets does not exist when the command is applied, + /// the command will fail, possibly causing it to panic based on the default [error handler](crate::error) set. + /// + /// To queue this command with a specific handler, use [`EntityCommands::queue_handled`] + /// with [`entity_command::trigger(event)`](entity_command::trigger). + /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] pub fn trigger(&mut self, event: impl Event) { self.queue(command::trigger(event)); @@ -1094,6 +1101,13 @@ impl<'w, 's> Commands<'w, 's> { /// Sends an [`EntityEvent`] for the given targets. /// /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. + /// + /// If the entity that this command targets does not exist when the command is applied, + /// the command will fail, possibly causing it to panic based on the default [error handler](crate::error) set. + /// + /// To queue this command with a specific handler, use [`EntityCommands::queue_handled`] + /// with [`entity_command::trigger(event)`](entity_command::trigger). + /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] pub fn trigger_targets( &mut self, @@ -1994,6 +2008,12 @@ impl<'a> EntityCommands<'a> { /// Sends an [`EntityEvent`] targeting the entity. /// /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. + /// + /// If the entity that this command targets does not exist when the command is applied, + /// the command will fail, possibly causing it to panic based on the default error handler set. + /// To queue this command with a handler, use [`EntityCommands::queue_handled`] + /// with [`entity_command::trigger(event)`](entity_command::trigger). + /// [`EntityCommands::queue_silenced`] may also be used to ignore the error completely. #[track_caller] pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.queue(entity_command::trigger(event)) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5ee2805772129..2a6ddf4756f84 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -43,10 +43,12 @@ impl SystemMeta { pub(crate) fn new() -> Self { let name = DebugName::type_name::(); Self { + // These spans are initialized during plugin build, so we set the parent to `None` to prevent + // them from being children of the span that is measuring the plugin build time. #[cfg(feature = "trace")] - system_span: info_span!("system", name = name.clone().as_string()), + system_span: info_span!(parent: None, "system", name = name.clone().as_string()), #[cfg(feature = "trace")] - commands_span: info_span!("system_commands", name = name.clone().as_string()), + commands_span: info_span!(parent: None, "system_commands", name = name.clone().as_string()), name, flags: SystemStateFlags::empty(), last_run: Tick::new(0), @@ -68,8 +70,8 @@ impl SystemMeta { #[cfg(feature = "trace")] { let name = new_name.as_ref(); - self.system_span = info_span!("system", name = name); - self.commands_span = info_span!("system_commands", name = name); + self.system_span = info_span!(parent: None, "system", name = name); + self.commands_span = info_span!(parent: None, "system_commands", name = name); } self.name = new_name.into(); } @@ -194,6 +196,22 @@ impl SystemMeta { /// } /// }); /// ``` +/// Exclusive System: +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_ecs::system::SystemState; +/// # +/// # #[derive(BufferedEvent)] +/// # struct MyEvent; +/// # +/// fn exclusive_system(world: &mut World, system_state: &mut SystemState>) { +/// let mut event_reader = system_state.get_mut(world); +/// +/// for events in event_reader.read() { +/// println!("Hello World!"); +/// } +/// } +/// ``` pub struct SystemState { meta: SystemMeta, param_state: Param::State, diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 26c767b0514df..d971e312a7034 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -1907,14 +1907,14 @@ mod tests { schedule.add_systems(|_query: Query<&Name>| todo!()); schedule.add_systems(|_query: Query<&Name>| -> () { todo!() }); - fn obs(_trigger: On) { + fn obs(_event: On) { todo!() } world.add_observer(obs); - world.add_observer(|_trigger: On| {}); - world.add_observer(|_trigger: On| todo!()); - world.add_observer(|_trigger: On| -> () { todo!() }); + world.add_observer(|_event: On| {}); + world.add_observer(|_event: On| todo!()); + world.add_observer(|_event: On| -> () { todo!() }); fn my_command(_world: &mut World) { todo!() diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 048835f397776..177d056e48cfd 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -533,7 +533,15 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::Failed { .. }))); - let expected = "Parameter `Res` failed validation: Resource does not exist\n"; - assert!(result.unwrap_err().to_string().contains(expected)); + + let expected = "Resource does not exist"; + let actual = result.unwrap_err().to_string(); + + assert!( + actual.contains(expected), + "Expected error message to contain `{}` but got `{}`", + expected, + actual + ); } } diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index f4412d43a3cb1..576773bb70b6e 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -1005,8 +1005,15 @@ mod tests { let result = world.run_system(id); assert!(matches!(result, Err(RegisteredSystemError::Failed { .. }))); - let expected = "System returned error: Parameter `Res` failed validation: Resource does not exist\n"; - assert!(result.unwrap_err().to_string().contains(expected)); + let expected = "Resource does not exist"; + let actual = result.unwrap_err().to_string(); + + assert!( + actual.contains(expected), + "Expected error message to contain `{}` but got `{}`", + expected, + actual + ); } #[test] diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 92d2e98e5da69..9c247bcab2638 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -811,7 +811,7 @@ impl<'w> DeferredWorld<'w> { &mut self, event_key: EventKey, current_target: Option, - original_target: Option, + original_entity: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -823,7 +823,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event_key, current_target, - original_target, + original_entity, components.clone(), data, &mut propagate, @@ -851,7 +851,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event_key, Some(current_target), - original_target, + original_entity, components.clone(), data, &mut propagate, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 643c4f1a029c1..557687064c421 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -2304,7 +2304,10 @@ impl<'w> EntityWorldMut<'w> { let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.world.components, &mut self.world.component_ids) }; - let bundle_id = bundles.register_contributed_bundle_info::(&mut registrator, storages); + + // SAFETY: `storages`, `bundles` and `registrator` come from the same world. + let bundle_id = + unsafe { bundles.register_contributed_bundle_info::(&mut registrator, storages) }; // SAFETY: We just created the bundle, and the archetype is valid, since we are in it. let Some(mut remover) = (unsafe { @@ -2351,10 +2354,13 @@ impl<'w> EntityWorldMut<'w> { ComponentsRegistrator::new(&mut self.world.components, &mut self.world.component_ids) }; - let retained_bundle = self - .world - .bundles - .register_info::(&mut registrator, storages); + // SAFETY: `storages`, `bundles` and `registrator` come from the same world. + let retained_bundle = unsafe { + self.world + .bundles + .register_info::(&mut registrator, storages) + }; + // SAFETY: `retained_bundle` exists as we just initialized it. let retained_bundle_info = unsafe { self.world.bundles.get_unchecked(retained_bundle) }; let old_archetype = &mut archetypes[old_location.archetype_id]; @@ -6089,8 +6095,8 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: On, mut commands: Commands| { - commands.entity(trigger.target()).insert(TestComponent(0)); + .observe(|event: On, mut commands: Commands| { + commands.entity(event.entity()).insert(TestComponent(0)); }) .id(); @@ -6108,8 +6114,8 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer(|trigger: On, mut commands: Commands| { - commands.entity(trigger.target()).despawn(); + world.add_observer(|event: On, mut commands: Commands| { + commands.entity(event.entity()).despawn(); }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); @@ -6198,19 +6204,19 @@ mod tests { .push("OrdA hook on_remove"); } - fn ord_a_observer_on_add(_trigger: On, mut res: ResMut) { + fn ord_a_observer_on_add(_event: On, mut res: ResMut) { res.0.push("OrdA observer on_add"); } - fn ord_a_observer_on_insert(_trigger: On, mut res: ResMut) { + fn ord_a_observer_on_insert(_event: On, mut res: ResMut) { res.0.push("OrdA observer on_insert"); } - fn ord_a_observer_on_replace(_trigger: On, mut res: ResMut) { + fn ord_a_observer_on_replace(_event: On, mut res: ResMut) { res.0.push("OrdA observer on_replace"); } - fn ord_a_observer_on_remove(_trigger: On, mut res: ResMut) { + fn ord_a_observer_on_remove(_event: On, mut res: ResMut) { res.0.push("OrdA observer on_remove"); } @@ -6249,19 +6255,19 @@ mod tests { .push("OrdB hook on_remove"); } - fn ord_b_observer_on_add(_trigger: On, mut res: ResMut) { + fn ord_b_observer_on_add(_event: On, mut res: ResMut) { res.0.push("OrdB observer on_add"); } - fn ord_b_observer_on_insert(_trigger: On, mut res: ResMut) { + fn ord_b_observer_on_insert(_event: On, mut res: ResMut) { res.0.push("OrdB observer on_insert"); } - fn ord_b_observer_on_replace(_trigger: On, mut res: ResMut) { + fn ord_b_observer_on_replace(_event: On, mut res: ResMut) { res.0.push("OrdB observer on_replace"); } - fn ord_b_observer_on_remove(_trigger: On, mut res: ResMut) { + fn ord_b_observer_on_remove(_event: On, mut res: ResMut) { res.0.push("OrdB observer on_remove"); } @@ -6312,9 +6318,6 @@ mod tests { #[derive(Component, Clone, PartialEq, Debug)] struct C(u32); - #[derive(Component, Clone, PartialEq, Debug, Default)] - struct D; - let mut world = World::new(); let entity_a = world.spawn((A, B, C(5))).id(); let entity_b = world.spawn((A, C(4))).id(); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 03574331f2329..d04c28a5dfe10 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -50,7 +50,11 @@ pub enum EntityComponentError { #[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)] pub enum EntityMutableFetchError { /// The entity with the given ID does not exist. - #[error(transparent)] + #[error( + "{0}\n + If you were attempting to apply a command to this entity, + and want to handle this error gracefully, consider using `EntityCommands::queue_handled` or `queue_silenced`." + )] EntityDoesNotExist(#[from] EntityDoesNotExistError), /// The entity with the given ID was requested mutably more than once. #[error("The entity with ID {0} was requested mutably more than once")] @@ -70,3 +74,50 @@ pub enum ResourceFetchError { #[error("Cannot get access to the resource with ID {0:?} in the world as it conflicts with an on going operation.")] NoResourceAccess(ComponentId), } + +#[cfg(test)] +mod tests { + use crate::{ + prelude::*, + system::{entity_command::trigger, RunSystemOnce}, + }; + + // Inspired by https://github.com/bevyengine/bevy/issues/19623 + #[test] + fn fixing_panicking_entity_commands() { + #[derive(EntityEvent)] + struct Kill; + + #[derive(EntityEvent)] + struct FollowupEvent; + + fn despawn(event: On, mut commands: Commands) { + commands.entity(event.entity()).despawn(); + } + + fn followup(on: On, mut commands: Commands) { + // When using a simple .trigger() here, this panics because the entity has already been despawned. + // Instead, we need to use `.queue_handled` or `.queue_silenced` to avoid the panic. + commands + .entity(on.entity()) + .queue_silenced(trigger(FollowupEvent)); + } + + let mut world = World::new(); + // This test would pass if the order of these statements were swapped, + // even with panicking entity commands + world.add_observer(followup); + world.add_observer(despawn); + + // Create an entity to test these observers with + world.spawn_empty(); + + // Trigger a kill event on the entity + fn kill_everything(mut commands: Commands, query: Query) { + for id in query.iter() { + commands.entity(id).trigger(Kill); + } + } + world.run_system_once(kill_everything).unwrap(); + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 9e36484cc5141..7632e30159f7a 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2305,9 +2305,12 @@ impl World { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; - let bundle_id = self - .bundles - .register_info::(&mut registrator, &mut self.storages); + + // SAFETY: `registrator`, `self.bundles`, and `self.storages` all come from this world. + let bundle_id = unsafe { + self.bundles + .register_info::(&mut registrator, &mut self.storages) + }; let mut batch_iter = batch.into_iter(); @@ -2450,9 +2453,12 @@ impl World { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; - let bundle_id = self - .bundles - .register_info::(&mut registrator, &mut self.storages); + + // SAFETY: `registrator`, `self.bundles`, and `self.storages` all come from this world. + let bundle_id = unsafe { + self.bundles + .register_info::(&mut registrator, &mut self.storages) + }; let mut invalid_entities = Vec::::new(); let mut batch_iter = batch.into_iter(); @@ -3072,9 +3078,13 @@ impl World { // SAFETY: These come from the same world. `Self.components_registrator` can't be used since we borrow other fields too. let mut registrator = unsafe { ComponentsRegistrator::new(&mut self.components, &mut self.component_ids) }; - let id = self - .bundles - .register_info::(&mut registrator, &mut self.storages); + + // SAFETY: `registrator`, `self.storages` and `self.bundles` all come from this world. + let id = unsafe { + self.bundles + .register_info::(&mut registrator, &mut self.storages) + }; + // SAFETY: We just initialized the bundle so its id should definitely be valid. unsafe { self.bundles.get(id).debug_checked_unwrap() } } diff --git a/crates/bevy_feathers/src/alpha_pattern.rs b/crates/bevy_feathers/src/alpha_pattern.rs index 8252fb713847d..1a2732a2e57e8 100644 --- a/crates/bevy_feathers/src/alpha_pattern.rs +++ b/crates/bevy_feathers/src/alpha_pattern.rs @@ -47,7 +47,7 @@ fn on_add_alpha_pattern( mut q_material_node: Query<&mut MaterialNode>, r_material: Res, ) { - if let Ok(mut material) = q_material_node.get_mut(ev.target()) { + if let Ok(mut material) = q_material_node.get_mut(ev.entity()) { material.0 = r_material.0.clone(); } } diff --git a/crates/bevy_feathers/src/controls/button.rs b/crates/bevy_feathers/src/controls/button.rs index 68af828d93818..4ae7d7062c8e4 100644 --- a/crates/bevy_feathers/src/controls/button.rs +++ b/crates/bevy_feathers/src/controls/button.rs @@ -107,7 +107,7 @@ fn update_button_styles( ) { for (button_ent, variant, disabled, pressed, hovered, bg_color, font_color) in q_buttons.iter() { - set_button_colors( + set_button_styles( button_ent, variant, disabled, @@ -141,7 +141,7 @@ fn update_button_styles_remove( if let Ok((button_ent, variant, disabled, pressed, hovered, bg_color, font_color)) = q_buttons.get(ent) { - set_button_colors( + set_button_styles( button_ent, variant, disabled, @@ -155,7 +155,7 @@ fn update_button_styles_remove( }); } -fn set_button_colors( +fn set_button_styles( button_ent: Entity, variant: &ButtonVariant, disabled: bool, @@ -183,6 +183,11 @@ fn set_button_colors( (ButtonVariant::Primary, false) => tokens::BUTTON_PRIMARY_TEXT, }; + let cursor_shape = match disabled { + true => bevy_window::SystemCursorIcon::NotAllowed, + false => bevy_window::SystemCursorIcon::Pointer, + }; + // Change background color if bg_color.0 != bg_token { commands @@ -196,6 +201,11 @@ fn set_button_colors( .entity(button_ent) .insert(ThemeFontColor(font_color_token)); } + + // Change cursor shape + commands + .entity(button_ent) + .insert(EntityCursor::System(cursor_shape)); } /// Plugin which registers the systems for updating the button styles. diff --git a/crates/bevy_feathers/src/controls/checkbox.rs b/crates/bevy_feathers/src/controls/checkbox.rs index c28cbea5600a5..b7c5869cd449f 100644 --- a/crates/bevy_feathers/src/controls/checkbox.rs +++ b/crates/bevy_feathers/src/controls/checkbox.rs @@ -158,7 +158,7 @@ fn update_checkbox_styles( }; let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); let mark_color = q_mark.get_mut(mark_ent).unwrap(); - set_checkbox_colors( + set_checkbox_styles( checkbox_ent, outline_ent, mark_ent, @@ -213,7 +213,7 @@ fn update_checkbox_styles_remove( }; let (outline_bg, outline_border) = q_outline.get_mut(outline_ent).unwrap(); let mark_color = q_mark.get_mut(mark_ent).unwrap(); - set_checkbox_colors( + set_checkbox_styles( checkbox_ent, outline_ent, mark_ent, @@ -230,7 +230,7 @@ fn update_checkbox_styles_remove( }); } -fn set_checkbox_colors( +fn set_checkbox_styles( checkbox_ent: Entity, outline_ent: Entity, mark_ent: Entity, @@ -266,6 +266,11 @@ fn set_checkbox_colors( false => tokens::CHECKBOX_TEXT, }; + let cursor_shape = match disabled { + true => bevy_window::SystemCursorIcon::NotAllowed, + false => bevy_window::SystemCursorIcon::Pointer, + }; + // Change outline background if outline_bg.0 != outline_bg_token { commands @@ -299,6 +304,11 @@ fn set_checkbox_colors( .entity(checkbox_ent) .insert(ThemeFontColor(font_color_token)); } + + // Change cursor shape + commands + .entity(checkbox_ent) + .insert(EntityCursor::System(cursor_shape)); } /// Plugin which registers the systems for updating the checkbox styles. diff --git a/crates/bevy_feathers/src/controls/radio.rs b/crates/bevy_feathers/src/controls/radio.rs index 9b9b0d06cdd77..404ed8608f1dc 100644 --- a/crates/bevy_feathers/src/controls/radio.rs +++ b/crates/bevy_feathers/src/controls/radio.rs @@ -135,7 +135,7 @@ fn update_radio_styles( }; let outline_border = q_outline.get_mut(outline_ent).unwrap(); let mark_color = q_mark.get_mut(mark_ent).unwrap(); - set_radio_colors( + set_radio_styles( radio_ent, outline_ent, mark_ent, @@ -187,7 +187,7 @@ fn update_radio_styles_remove( }; let outline_border = q_outline.get_mut(outline_ent).unwrap(); let mark_color = q_mark.get_mut(mark_ent).unwrap(); - set_radio_colors( + set_radio_styles( radio_ent, outline_ent, mark_ent, @@ -203,7 +203,7 @@ fn update_radio_styles_remove( }); } -fn set_radio_colors( +fn set_radio_styles( radio_ent: Entity, outline_ent: Entity, mark_ent: Entity, @@ -231,6 +231,11 @@ fn set_radio_colors( false => tokens::RADIO_TEXT, }; + let cursor_shape = match disabled { + true => bevy_window::SystemCursorIcon::NotAllowed, + false => bevy_window::SystemCursorIcon::Pointer, + }; + // Change outline border if outline_border.0 != outline_border_token { commands @@ -257,6 +262,11 @@ fn set_radio_colors( .entity(radio_ent) .insert(ThemeFontColor(font_color_token)); } + + // Change cursor shape + commands + .entity(radio_ent) + .insert(EntityCursor::System(cursor_shape)); } /// Plugin which registers the systems for updating the radio styles. diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index fcea771806cd9..d618057dfc0df 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -14,7 +14,7 @@ use bevy_ecs::{ reflect::ReflectComponent, schedule::IntoScheduleConfigs, spawn::SpawnRelated, - system::{In, Query, Res}, + system::{Commands, In, Query, Res}, }; use bevy_input_focus::tab_navigation::TabIndex; use bevy_picking::PickingSystems; @@ -126,42 +126,74 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ) } -fn update_slider_colors( +fn update_slider_styles( mut q_sliders: Query< - (Has, &mut BackgroundGradient), + (Entity, Has, &mut BackgroundGradient), (With, Or<(Spawned, Added)>), >, theme: Res, + mut commands: Commands, ) { - for (disabled, mut gradient) in q_sliders.iter_mut() { - set_slider_colors(&theme, disabled, gradient.as_mut()); + for (slider_ent, disabled, mut gradient) in q_sliders.iter_mut() { + set_slider_styles( + slider_ent, + &theme, + disabled, + gradient.as_mut(), + &mut commands, + ); } } -fn update_slider_colors_remove( - mut q_sliders: Query<(Has, &mut BackgroundGradient)>, +fn update_slider_styles_remove( + mut q_sliders: Query<(Entity, Has, &mut BackgroundGradient)>, mut removed_disabled: RemovedComponents, theme: Res, + mut commands: Commands, ) { removed_disabled.read().for_each(|ent| { - if let Ok((disabled, mut gradient)) = q_sliders.get_mut(ent) { - set_slider_colors(&theme, disabled, gradient.as_mut()); + if let Ok((slider_ent, disabled, mut gradient)) = q_sliders.get_mut(ent) { + set_slider_styles( + slider_ent, + &theme, + disabled, + gradient.as_mut(), + &mut commands, + ); } }); } -fn set_slider_colors(theme: &Res<'_, UiTheme>, disabled: bool, gradient: &mut BackgroundGradient) { +fn set_slider_styles( + slider_ent: Entity, + theme: &Res<'_, UiTheme>, + disabled: bool, + gradient: &mut BackgroundGradient, + commands: &mut Commands, +) { let bar_color = theme.color(match disabled { true => tokens::SLIDER_BAR_DISABLED, false => tokens::SLIDER_BAR, }); + let bg_color = theme.color(tokens::SLIDER_BG); + + let cursor_shape = match disabled { + true => bevy_window::SystemCursorIcon::NotAllowed, + false => bevy_window::SystemCursorIcon::EwResize, + }; + if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { linear_gradient.stops[0].color = bar_color; linear_gradient.stops[1].color = bar_color; linear_gradient.stops[2].color = bg_color; linear_gradient.stops[3].color = bg_color; } + + // Change cursor shape + commands + .entity(slider_ent) + .insert(EntityCursor::System(cursor_shape)); } fn update_slider_pos( @@ -203,8 +235,8 @@ impl Plugin for SliderPlugin { app.add_systems( PreUpdate, ( - update_slider_colors, - update_slider_colors_remove, + update_slider_styles, + update_slider_styles_remove, update_slider_pos, ) .in_set(PickingSystems::Last), diff --git a/crates/bevy_feathers/src/controls/toggle_switch.rs b/crates/bevy_feathers/src/controls/toggle_switch.rs index 46872d97b0e8b..0eb48e4387878 100644 --- a/crates/bevy_feathers/src/controls/toggle_switch.rs +++ b/crates/bevy_feathers/src/controls/toggle_switch.rs @@ -114,7 +114,7 @@ fn update_switch_styles( }; // Safety: since we just checked the query, should always work. let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap(); - set_switch_colors( + set_switch_styles( switch_ent, slide_ent, disabled, @@ -162,7 +162,7 @@ fn update_switch_styles_remove( }; // Safety: since we just checked the query, should always work. let (ref mut slide_style, slide_color) = q_slide.get_mut(slide_ent).unwrap(); - set_switch_colors( + set_switch_styles( switch_ent, slide_ent, disabled, @@ -178,7 +178,7 @@ fn update_switch_styles_remove( }); } -fn set_switch_colors( +fn set_switch_styles( switch_ent: Entity, slide_ent: Entity, disabled: bool, @@ -213,6 +213,11 @@ fn set_switch_colors( false => Val::Percent(0.), }; + let cursor_shape = match disabled { + true => bevy_window::SystemCursorIcon::NotAllowed, + false => bevy_window::SystemCursorIcon::Pointer, + }; + // Change outline background if outline_bg.0 != outline_bg_token { commands @@ -238,6 +243,11 @@ fn set_switch_colors( if slide_pos != slide_style.left { slide_style.left = slide_pos; } + + // Change cursor shape + commands + .entity(switch_ent) + .insert(EntityCursor::System(cursor_shape)); } /// Plugin which registers the systems for updating the toggle switch styles. diff --git a/crates/bevy_feathers/src/cursor.rs b/crates/bevy_feathers/src/cursor.rs index 8c6b5c103b20a..4a35e40a7af20 100644 --- a/crates/bevy_feathers/src/cursor.rs +++ b/crates/bevy_feathers/src/cursor.rs @@ -51,12 +51,14 @@ impl EntityCursor { /// Compare the [`EntityCursor`] to a [`CursorIcon`] so that we can see whether or not /// the window cursor needs to be changed. pub fn eq_cursor_icon(&self, cursor_icon: &CursorIcon) -> bool { - match (self, cursor_icon) { + // If feature custom_cursor is not enabled in bevy_feathers, we can't know if it is or not + // in bevy_window. So we use the wrapper function `as_system` to let bevy_window check its own feature. + // Otherwise it is not possible to have a match that both covers all cases and doesn't have unreachable + // branches under all feature combinations. + match (self, cursor_icon, cursor_icon.as_system()) { #[cfg(feature = "custom_cursor")] - (EntityCursor::Custom(custom), CursorIcon::Custom(other)) => custom == other, - (EntityCursor::System(system), CursorIcon::System(cursor_icon)) => { - *system == *cursor_icon - } + (EntityCursor::Custom(custom), CursorIcon::Custom(other), _) => custom == other, + (EntityCursor::System(system), _, Some(cursor_icon)) => *system == *cursor_icon, _ => false, } } diff --git a/crates/bevy_feathers/src/font_styles.rs b/crates/bevy_feathers/src/font_styles.rs index 6419696bb70f4..96e76c5787ca3 100644 --- a/crates/bevy_feathers/src/font_styles.rs +++ b/crates/bevy_feathers/src/font_styles.rs @@ -50,13 +50,13 @@ pub(crate) fn on_changed_font( assets: Res, mut commands: Commands, ) { - if let Ok(style) = font_style.get(ev.target()) + if let Ok(style) = font_style.get(ev.entity()) && let Some(font) = match style.font { HandleOrPath::Handle(ref h) => Some(h.clone()), HandleOrPath::Path(ref p) => Some(assets.load::(p)), } { - commands.entity(ev.target()).insert(Propagate(TextFont { + commands.entity(ev.entity()).insert(Propagate(TextFont { font, font_size: style.font_size, ..Default::default() diff --git a/crates/bevy_feathers/src/lib.rs b/crates/bevy_feathers/src/lib.rs index ef70be1844ed0..1a4621d230ab5 100644 --- a/crates/bevy_feathers/src/lib.rs +++ b/crates/bevy_feathers/src/lib.rs @@ -18,10 +18,11 @@ //! Please report issues, submit fixes and propose changes. //! Thanks for stress-testing; let's build something better together. -use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate, Update}; +use bevy_app::{HierarchyPropagatePlugin, Plugin, PostUpdate, PropagateSet}; use bevy_asset::embedded_asset; -use bevy_ecs::query::With; +use bevy_ecs::{query::With, schedule::IntoScheduleConfigs}; use bevy_text::{TextColor, TextFont}; +use bevy_ui::UiSystems; use bevy_ui_render::UiMaterialPlugin; use crate::{ @@ -63,11 +64,18 @@ impl Plugin for FeathersPlugin { app.add_plugins(( ControlsPlugin, CursorIconPlugin, - HierarchyPropagatePlugin::>::new(Update), - HierarchyPropagatePlugin::>::new(Update), + HierarchyPropagatePlugin::>::new(PostUpdate), + HierarchyPropagatePlugin::>::new(PostUpdate), UiMaterialPlugin::::default(), )); + // This needs to run in UiSystems::Propagate so the fonts are up-to-date for `measure_text_system` + // and `detect_text_needs_rerender` in UiSystems::Content + app.configure_sets( + PostUpdate, + PropagateSet::::default().in_set(UiSystems::Propagate), + ); + app.insert_resource(DefaultCursor(EntityCursor::System( bevy_window::SystemCursorIcon::Default, ))); diff --git a/crates/bevy_feathers/src/theme.rs b/crates/bevy_feathers/src/theme.rs index 1bb8f08e61825..1fd095b764c8e 100644 --- a/crates/bevy_feathers/src/theme.rs +++ b/crates/bevy_feathers/src/theme.rs @@ -109,7 +109,7 @@ pub(crate) fn on_changed_background( theme: Res, ) { // Update background colors where the design token has changed. - if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.target()) { + if let Ok((mut bg, theme_bg)) = q_background.get_mut(ev.entity()) { bg.0 = theme.color(theme_bg.0); } } @@ -120,7 +120,7 @@ pub(crate) fn on_changed_border( theme: Res, ) { // Update background colors where the design token has changed. - if let Ok((mut border, theme_border)) = q_border.get_mut(ev.target()) { + if let Ok((mut border, theme_border)) = q_border.get_mut(ev.entity()) { border.set_all(theme.color(theme_border.0)); } } @@ -133,10 +133,10 @@ pub(crate) fn on_changed_font_color( theme: Res, mut commands: Commands, ) { - if let Ok(token) = font_color.get(ev.target()) { + if let Ok(token) = font_color.get(ev.entity()) { let color = theme.color(token.0); commands - .entity(ev.target()) + .entity(ev.entity()) .insert(Propagate(TextColor(color))); } } diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index e5c0dc7058d88..1c2f5324855cd 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -17,6 +17,7 @@ bevy_render = ["dep:bevy_render", "bevy_core_pipeline"] # Bevy bevy_pbr = { path = "../bevy_pbr", version = "0.17.0-dev", optional = true } bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev", optional = true } +bevy_sprite_render = { path = "../bevy_sprite_render", version = "0.17.0-dev", optional = true } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_light = { path = "../bevy_light", version = "0.17.0-dev" } diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index fd7f8bfcc1572..48cd53c4e1420 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -29,7 +29,7 @@ use bevy_render::{ }; use bevy_render::{sync_world::MainEntity, RenderStartup}; use bevy_shader::Shader; -use bevy_sprite::{ +use bevy_sprite_render::{ init_mesh_2d_pipeline, Mesh2dPipeline, Mesh2dPipelineKey, SetMesh2dViewBindGroup, }; use bevy_utils::default; @@ -53,9 +53,11 @@ impl Plugin for LineGizmo2dPlugin { Render, GizmoRenderSystems::QueueLineGizmos2d .in_set(RenderSystems::Queue) - .ambiguous_with(bevy_sprite::queue_sprites) + .ambiguous_with(bevy_sprite_render::queue_sprites) .ambiguous_with( - bevy_sprite::queue_material2d_meshes::, + bevy_sprite_render::queue_material2d_meshes::< + bevy_sprite_render::ColorMaterial, + >, ), ) .add_systems( diff --git a/crates/bevy_gizmos/src/primitives/dim2.rs b/crates/bevy_gizmos/src/primitives/dim2.rs index 9535c28fbd0ab..41b5e0a60a056 100644 --- a/crates/bevy_gizmos/src/primitives/dim2.rs +++ b/crates/bevy_gizmos/src/primitives/dim2.rs @@ -7,9 +7,9 @@ use super::helpers::*; use bevy_color::Color; use bevy_math::{ primitives::{ - Annulus, Arc2d, BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, CircularSector, - CircularSegment, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, - RegularPolygon, Rhombus, Segment2d, Triangle2d, + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Polygon, Polyline2d, Primitive2d, Rectangle, RegularPolygon, Rhombus, Segment2d, + Triangle2d, }, Dir2, Isometry2d, Rot2, Vec2, }; @@ -648,7 +648,7 @@ where // polyline 2d -impl GizmoPrimitive2d> for GizmoBuffer +impl GizmoPrimitive2d for GizmoBuffer where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, @@ -660,42 +660,7 @@ where fn primitive_2d( &mut self, - primitive: &Polyline2d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - self.linestrip_2d( - primitive - .vertices - .iter() - .copied() - .map(|vec2| isometry * vec2), - color, - ); - } -} - -// boxed polyline 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &BoxedPolyline2d, + primitive: &Polyline2d, isometry: impl Into, color: impl Into, ) -> Self::Output<'_> { @@ -784,7 +749,7 @@ where // polygon 2d -impl GizmoPrimitive2d> for GizmoBuffer +impl GizmoPrimitive2d for GizmoBuffer where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, @@ -796,7 +761,7 @@ where fn primitive_2d( &mut self, - primitive: &Polygon, + primitive: &Polygon, isometry: impl Into, color: impl Into, ) -> Self::Output<'_> { @@ -827,49 +792,6 @@ where } } -// boxed polygon 2d - -impl GizmoPrimitive2d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_2d( - &mut self, - primitive: &BoxedPolygon, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - - let closing_point = { - let first = primitive.vertices.first(); - (primitive.vertices.last() != first) - .then_some(first) - .flatten() - .cloned() - }; - self.linestrip_2d( - primitive - .vertices - .iter() - .copied() - .chain(closing_point) - .map(|vec2| isometry * vec2), - color, - ); - } -} - // regular polygon 2d impl GizmoPrimitive2d for GizmoBuffer diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 898850ddea901..ca1172316b2c6 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -5,8 +5,8 @@ use super::helpers::*; use bevy_color::Color; use bevy_math::{ primitives::{ - BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, - Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, + Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, Triangle3d, }, Dir3, Isometry3d, Quat, UVec2, Vec2, Vec3, }; @@ -235,7 +235,7 @@ where // polyline 3d -impl GizmoPrimitive3d> for GizmoBuffer +impl GizmoPrimitive3d for GizmoBuffer where Config: GizmoConfigGroup, Clear: 'static + Send + Sync, @@ -247,34 +247,7 @@ where fn primitive_3d( &mut self, - primitive: &Polyline3d, - isometry: impl Into, - color: impl Into, - ) -> Self::Output<'_> { - if !self.enabled { - return; - } - - let isometry = isometry.into(); - self.linestrip(primitive.vertices.map(|vec3| isometry * vec3), color); - } -} - -// boxed polyline 3d - -impl GizmoPrimitive3d for GizmoBuffer -where - Config: GizmoConfigGroup, - Clear: 'static + Send + Sync, -{ - type Output<'a> - = () - where - Self: 'a; - - fn primitive_3d( - &mut self, - primitive: &BoxedPolyline3d, + primitive: &Polyline3d, isometry: impl Into, color: impl Into, ) -> Self::Output<'_> { @@ -284,11 +257,7 @@ where let isometry = isometry.into(); self.linestrip( - primitive - .vertices - .iter() - .copied() - .map(|vec3| isometry * vec3), + primitive.vertices.iter().map(|vec3| isometry * *vec3), color, ); } diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index c8b486f41d13d..1d3d2e96ea9dc 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -15,7 +15,6 @@ pbr_multi_layer_material_textures = [ ] pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"] pbr_specular_textures = ["bevy_pbr/pbr_specular_textures"] -gltf_convert_coordinates_default = [] [dependencies] # bevy @@ -23,7 +22,6 @@ bevy_animation = { path = "../bevy_animation", version = "0.17.0-dev", optional bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_light = { path = "../bevy_light", version = "0.17.0-dev" } @@ -64,6 +62,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.140" smallvec = { version = "1", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } + +[dev-dependencies] bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } [lints] diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index c53e1a6b7617e..96b9d7f1d917c 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -159,20 +159,19 @@ pub struct GltfPlugin { /// Can be modified with the [`DefaultGltfImageSampler`] resource. pub default_sampler: ImageSamplerDescriptor, - /// Whether to convert glTF coordinates to Bevy's coordinate system by default. - /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system - /// such that objects looking forward in glTF will also look forward in Bevy. + /// _CAUTION: This is an experimental feature with [known issues](https://github.com/bevyengine/bevy/issues/20621). Behavior may change in future versions._ /// - /// The exact coordinate system conversion is as follows: - /// - glTF: - /// - forward: Z - /// - up: Y - /// - right: -X - /// - Bevy: - /// - forward: -Z - /// - up: Y - /// - right: X - pub convert_coordinates: bool, + /// How to convert glTF coordinates on import. Assuming glTF cameras, glTF lights, and glTF meshes had global identity transforms, + /// their Bevy [`Transform::forward`](bevy_transform::components::Transform::forward) will be pointing in the following global directions: + /// - When set to `false` + /// - glTF cameras and glTF lights: global -Z, + /// - glTF models: global +Z. + /// - When set to `true` + /// - glTF cameras and glTF lights: global +Z, + /// - glTF models: global -Z. + /// + /// The default is `false`. + pub use_model_forward_direction: bool, /// Registry for custom vertex attributes. /// @@ -185,7 +184,7 @@ impl Default for GltfPlugin { GltfPlugin { default_sampler: ImageSamplerDescriptor::linear(), custom_vertex_attributes: HashMap::default(), - convert_coordinates: cfg!(feature = "gltf_convert_coordinates_default"), + use_model_forward_direction: false, } } } @@ -235,7 +234,7 @@ impl Plugin for GltfPlugin { supported_compressed_formats, custom_vertex_attributes: self.custom_vertex_attributes.clone(), default_sampler, - default_convert_coordinates: self.convert_coordinates, + default_use_model_forward_direction: self.use_model_forward_direction, }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 50a6e42b39f0c..8aa7db8c34e6f 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2,7 +2,6 @@ mod extensions; mod gltf_ext; use alloc::sync::Arc; -use bevy_log::warn_once; use std::{ io::Error, path::{Path, PathBuf}, @@ -35,7 +34,7 @@ use bevy_math::{Mat4, Vec3}; use bevy_mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, Mesh3d, MeshVertexAttribute, PrimitiveTopology, VertexAttributeValues, + Indices, Mesh, Mesh3d, MeshVertexAttribute, PrimitiveTopology, }; #[cfg(feature = "pbr_transmission_textures")] use bevy_pbr::UvChannel; @@ -148,20 +147,17 @@ pub struct GltfLoader { pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, /// Arc to default [`ImageSamplerDescriptor`]. pub default_sampler: Arc>, - /// Whether to convert glTF coordinates to Bevy's coordinate system by default. - /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system - /// such that objects looking forward in glTF will also look forward in Bevy. + /// How to convert glTF coordinates on import. Assuming glTF cameras, glTF lights, and glTF meshes had global identity transforms, + /// their Bevy [`Transform::forward`](bevy_transform::components::Transform::forward) will be pointing in the following global directions: + /// - When set to `false` + /// - glTF cameras and glTF lights: global -Z, + /// - glTF models: global +Z. + /// - When set to `true` + /// - glTF cameras and glTF lights: global +Z, + /// - glTF models: global -Z. /// - /// The exact coordinate system conversion is as follows: - /// - glTF: - /// - forward: Z - /// - up: Y - /// - right: -X - /// - Bevy: - /// - forward: -Z - /// - up: Y - /// - right: X - pub default_convert_coordinates: bool, + /// The default is `false`. + pub default_use_model_forward_direction: bool, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -203,23 +199,19 @@ pub struct GltfLoaderSettings { pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, - /// Overrides the default glTF coordinate conversion setting. + /// _CAUTION: This is an experimental feature with [known issues](https://github.com/bevyengine/bevy/issues/20621). Behavior may change in future versions._ /// - /// If set to `Some(true)`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system - /// such that objects looking forward in glTF will also look forward in Bevy. + /// How to convert glTF coordinates on import. Assuming glTF cameras, glTF lights, and glTF meshes had global unit transforms, + /// their Bevy [`Transform::forward`](bevy_transform::components::Transform::forward) will be pointing in the following global directions: + /// - When set to `false` + /// - glTF cameras and glTF lights: global -Z, + /// - glTF models: global +Z. + /// - When set to `true` + /// - glTF cameras and glTF lights: global +Z, + /// - glTF models: global -Z. /// - /// The exact coordinate system conversion is as follows: - /// - glTF: - /// - forward: Z - /// - up: Y - /// - right: -X - /// - Bevy: - /// - forward: -Z - /// - up: Y - /// - right: X - /// - /// If `None`, uses the global default set by [`GltfPlugin::convert_coordinates`](crate::GltfPlugin::convert_coordinates). - pub convert_coordinates: Option, + /// If `None`, uses the global default set by [`GltfPlugin::use_model_forward_direction`](crate::GltfPlugin::use_model_forward_direction). + pub use_model_forward_direction: Option, } impl Default for GltfLoaderSettings { @@ -232,7 +224,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, - convert_coordinates: None, + use_model_forward_direction: None, } } } @@ -272,20 +264,9 @@ impl GltfLoader { paths }; - let convert_coordinates = match settings.convert_coordinates { + let convert_coordinates = match settings.use_model_forward_direction { Some(convert_coordinates) => convert_coordinates, - None => { - let convert_by_default = loader.default_convert_coordinates; - if !convert_by_default && !cfg!(feature = "gltf_convert_coordinates_default") { - warn_once!( - "Starting from Bevy 0.18, by default all imported glTF models will be rotated by 180 degrees around the Y axis to align with Bevy's coordinate system. \ - You are currently importing glTF files using the old behavior. Consider opting-in to the new import behavior by enabling the `gltf_convert_coordinates_default` feature. \ - If you encounter any issues please file a bug! \ - If you want to continue using the old behavior going forward (even when the default changes in 0.18), manually set the corresponding option in the `GltfPlugin` or `GltfLoaderSettings`. See the migration guide for more details." - ); - } - convert_by_default - } + None => loader.default_use_model_forward_direction, }; #[cfg(feature = "bevy_animation")] @@ -733,7 +714,12 @@ impl GltfLoader { primitive: primitive.index(), }; let morph_target_image = MorphTargetImage::new( - morph_target_reader.map(PrimitiveMorphAttributesIter), + morph_target_reader.map(|i| PrimitiveMorphAttributesIter { + convert_coordinates, + positions: i.0, + normals: i.1, + tangents: i.2, + }), mesh.count_vertices(), RenderAssetUsages::default(), )?; @@ -771,26 +757,22 @@ impl GltfLoader { } } - if let Some(vertex_attribute) = reader - .read_tangents() - .map(|v| VertexAttributeValues::Float32x4(v.collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); - } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() + if !mesh.contains_attribute(Mesh::ATTRIBUTE_TANGENT) + && mesh.contains_attribute(Mesh::ATTRIBUTE_NORMAL) && needs_tangents(&primitive.material()) { 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 - ); + "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name + ); let generate_tangents_span = info_span!("generate_tangents", name = file_name); 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 + ); } }); } @@ -1564,10 +1546,19 @@ fn load_node( // > the accessors of the original primitive. mesh_entity.insert(MeshMorphWeights::new(weights).unwrap()); } - mesh_entity.insert(Aabb::from_min_max( - Vec3::from_slice(&bounds.min), - Vec3::from_slice(&bounds.max), - )); + + let mut bounds_min = Vec3::from_slice(&bounds.min); + let mut bounds_max = Vec3::from_slice(&bounds.max); + + if convert_coordinates { + let converted_min = bounds_min.convert_coordinates(); + let converted_max = bounds_max.convert_coordinates(); + + bounds_min = converted_min.min(converted_max); + bounds_max = converted_min.max(converted_max); + } + + mesh_entity.insert(Aabb::from_min_max(bounds_min, bounds_max)); if let Some(extras) = primitive.extras() { mesh_entity.insert(GltfExtras { @@ -1839,30 +1830,39 @@ impl ImageOrPath { } } -struct PrimitiveMorphAttributesIter<'s>( - pub ( - Option>, - Option>, - Option>, - ), -); +struct PrimitiveMorphAttributesIter<'s> { + convert_coordinates: bool, + positions: Option>, + normals: Option>, + tangents: Option>, +} impl<'s> Iterator for PrimitiveMorphAttributesIter<'s> { type Item = MorphAttributes; fn next(&mut self) -> Option { - let position = self.0 .0.as_mut().and_then(Iterator::next); - let normal = self.0 .1.as_mut().and_then(Iterator::next); - let tangent = self.0 .2.as_mut().and_then(Iterator::next); + let position = self.positions.as_mut().and_then(Iterator::next); + let normal = self.normals.as_mut().and_then(Iterator::next); + let tangent = self.tangents.as_mut().and_then(Iterator::next); if position.is_none() && normal.is_none() && tangent.is_none() { return None; } - Some(MorphAttributes { + let mut attributes = MorphAttributes { position: position.map(Into::into).unwrap_or(Vec3::ZERO), normal: normal.map(Into::into).unwrap_or(Vec3::ZERO), tangent: tangent.map(Into::into).unwrap_or(Vec3::ZERO), - }) + }; + + if self.convert_coordinates { + attributes = MorphAttributes { + position: attributes.position.convert_coordinates(), + normal: attributes.normal.convert_coordinates(), + tangent: attributes.tangent.convert_coordinates(), + } + } + + Some(attributes) } } diff --git a/crates/bevy_image/src/basis.rs b/crates/bevy_image/src/basis.rs index 396129e678634..c88edb1fdc7bc 100644 --- a/crates/bevy_image/src/basis.rs +++ b/crates/bevy_image/src/basis.rs @@ -45,7 +45,7 @@ pub fn basis_buffer_to_image( let image_count = transcoder.image_count(buffer); let texture_type = transcoder.basis_texture_type(buffer); - if texture_type == BasisTextureType::TextureTypeCubemapArray && image_count % 6 != 0 { + if texture_type == BasisTextureType::TextureTypeCubemapArray && !image_count.is_multiple_of(6) { return Err(TextureError::InvalidData(format!( "Basis file with cube map array texture with non-modulo 6 number of images: {image_count}", ))); diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index c2adc2c029116..bcae2f238060d 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -69,6 +69,7 @@ impl AssetSaver for CompressedImageSaver { is_srgb, sampler: image.sampler.clone(), asset_usage: image.asset_usage, + texture_format: None, }) } } diff --git a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs index 3e1c8fd5423ec..016ae96454666 100644 --- a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs @@ -1,4 +1,4 @@ -use crate::{Image, TextureAtlasLayout, TextureFormatPixelInfo as _}; +use crate::{Image, TextureAccessError, TextureAtlasLayout, TextureFormatPixelInfo as _}; use bevy_asset::RenderAssetUsages; use bevy_math::{URect, UVec2}; use guillotiere::{size2, Allocation, AtlasAllocator}; @@ -18,6 +18,9 @@ pub enum DynamicTextureAtlasBuilderError { /// Attempted to add an uninitialized texture to an atlas #[error("cannot add uninitialized texture to atlas")] UninitializedSourceTexture, + /// A texture access error occurred + #[error("texture access error: {0}")] + TextureAccess(#[from] TextureAccessError), } /// Helper utility to update [`TextureAtlasLayout`] on the fly. @@ -90,7 +93,7 @@ impl DynamicTextureAtlasBuilder { rect.max.y -= self.padding as i32; let atlas_width = atlas_texture.width() as usize; let rect_width = rect.width() as usize; - let format_size = atlas_texture.texture_descriptor.format.pixel_size(); + let format_size = atlas_texture.texture_descriptor.format.pixel_size()?; let Some(ref mut atlas_data) = atlas_texture.data else { return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas); diff --git a/crates/bevy_image/src/exr_texture_loader.rs b/crates/bevy_image/src/exr_texture_loader.rs index d9b89dd21e152..9cbf315bb4f88 100644 --- a/crates/bevy_image/src/exr_texture_loader.rs +++ b/crates/bevy_image/src/exr_texture_loader.rs @@ -1,4 +1,4 @@ -use crate::{Image, TextureFormatPixelInfo}; +use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; use image::ImageDecoder; use serde::{Deserialize, Serialize}; @@ -25,6 +25,8 @@ pub enum ExrTextureLoaderError { Io(#[from] std::io::Error), #[error(transparent)] ImageError(#[from] image::ImageError), + #[error("Texture access error: {0}")] + TextureAccess(#[from] TextureAccessError), } impl AssetLoader for ExrTextureLoader { @@ -40,7 +42,7 @@ impl AssetLoader for ExrTextureLoader { ) -> Result { let format = TextureFormat::Rgba32Float; debug_assert_eq!( - format.pixel_size(), + format.pixel_size()?, 4 * 4, "Format should have 32bit x 4 size" ); diff --git a/crates/bevy_image/src/hdr_texture_loader.rs b/crates/bevy_image/src/hdr_texture_loader.rs index 3177e94865809..83e9df3b3d807 100644 --- a/crates/bevy_image/src/hdr_texture_loader.rs +++ b/crates/bevy_image/src/hdr_texture_loader.rs @@ -1,4 +1,4 @@ -use crate::{Image, TextureFormatPixelInfo}; +use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::RenderAssetUsages; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; use image::DynamicImage; @@ -22,6 +22,8 @@ pub enum HdrTextureLoaderError { Io(#[from] std::io::Error), #[error("Could not extract image: {0}")] Image(#[from] image::ImageError), + #[error("Texture access error: {0}")] + TextureAccess(#[from] TextureAccessError), } impl AssetLoader for HdrTextureLoader { @@ -35,11 +37,8 @@ impl AssetLoader for HdrTextureLoader { _load_context: &mut LoadContext<'_>, ) -> Result { let format = TextureFormat::Rgba32Float; - debug_assert_eq!( - format.pixel_size(), - 4 * 4, - "Format should have 32bit x 4 size" - ); + let pixel_size = format.pixel_size()?; + debug_assert_eq!(pixel_size, 4 * 4, "Format should have 32bit x 4 size"); let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; @@ -49,7 +48,7 @@ impl AssetLoader for HdrTextureLoader { let image_buffer = dynamic_image .as_rgb32f() .expect("HDR Image format should be Rgb32F"); - let mut rgba_data = Vec::with_capacity(image_buffer.pixels().len() * format.pixel_size()); + let mut rgba_data = Vec::with_capacity(image_buffer.pixels().len() * pixel_size); for rgb in image_buffer.pixels() { let alpha = 1.0f32; diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index c3c248dc4d0c5..9a6fcc63b8a7e 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1,15 +1,18 @@ +use crate::ImageLoader; + #[cfg(feature = "basis-universal")] use super::basis::*; #[cfg(feature = "dds")] use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; +use bevy_app::{App, Plugin}; #[cfg(not(feature = "bevy_reflect"))] use bevy_reflect::TypePath; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_asset::{Asset, RenderAssetUsages}; +use bevy_asset::{uuid_handle, Asset, AssetApp, Assets, Handle, RenderAssetUsages}; use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; use bevy_ecs::resource::Resource; use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; @@ -35,6 +38,84 @@ impl BevyDefault for TextureFormat { } } +/// A handle to a 1 x 1 transparent white image. +/// +/// Like [`Handle::default`], this is a handle to a fallback image asset. +/// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image. +// Number randomly selected by fair WolframAlpha query. Totally arbitrary. +pub const TRANSPARENT_IMAGE_HANDLE: Handle = + uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); + +/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU. +pub struct ImagePlugin { + /// The default image sampler to use when [`ImageSampler`] is set to `Default`. + pub default_sampler: ImageSamplerDescriptor, +} + +impl Default for ImagePlugin { + fn default() -> Self { + ImagePlugin::default_linear() + } +} + +impl ImagePlugin { + /// Creates image settings with linear sampling by default. + pub fn default_linear() -> ImagePlugin { + ImagePlugin { + default_sampler: ImageSamplerDescriptor::linear(), + } + } + + /// Creates image settings with nearest sampling by default. + pub fn default_nearest() -> ImagePlugin { + ImagePlugin { + default_sampler: ImageSamplerDescriptor::nearest(), + } + } +} + +impl Plugin for ImagePlugin { + fn build(&self, app: &mut App) { + #[cfg(feature = "exr")] + app.init_asset_loader::(); + + #[cfg(feature = "hdr")] + app.init_asset_loader::(); + + app.init_asset::(); + #[cfg(feature = "bevy_reflect")] + app.register_asset_reflect::(); + + let mut image_assets = app.world_mut().resource_mut::>(); + + image_assets + .insert(&Handle::default(), Image::default()) + .unwrap(); + image_assets + .insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()) + .unwrap(); + + #[cfg(feature = "compressed_image_saver")] + if let Some(processor) = app + .world() + .get_resource::() + { + processor.register_processor::, + crate::CompressedImageSaver, + >>(crate::CompressedImageSaver.into()); + processor.set_default_processor::, + crate::CompressedImageSaver, + >>("png"); + } + + app.preregister_asset_loader::(ImageLoader::SUPPORTED_FILE_EXTENSIONS); + } +} + pub const TEXTURE_ASSET_INDEX: u64 = 0; pub const SAMPLER_ASSET_INDEX: u64 = 1; @@ -751,7 +832,14 @@ impl Default for Image { /// default is a 1x1x1 all '1.0' texture fn default() -> Self { let mut image = Image::default_uninit(); - image.data = Some(vec![255; image.texture_descriptor.format.pixel_size()]); + image.data = Some(vec![ + 255; + image + .texture_descriptor + .format + .pixel_size() + .unwrap_or(0) + ]); image } } @@ -769,11 +857,13 @@ impl Image { format: TextureFormat, asset_usage: RenderAssetUsages, ) -> Self { - debug_assert_eq!( - size.volume() * format.pixel_size(), - data.len(), - "Pixel data, size and format have to match", - ); + if let Ok(pixel_size) = format.pixel_size() { + debug_assert_eq!( + size.volume() * pixel_size, + data.len(), + "Pixel data, size and format have to match", + ); + } let mut image = Image::new_uninit(size, dimension, format, asset_usage); image.data = Some(data); image @@ -816,7 +906,9 @@ impl Image { // when constructing a transparent color from bytes. // If this changes, this function will need to be updated. let format = TextureFormat::bevy_default(); - debug_assert!(format.pixel_size() == 4); + if let Ok(pixel_size) = format.pixel_size() { + debug_assert!(pixel_size == 4); + } let data = vec![255, 255, 255, 0]; Image::new( Extent3d::default(), @@ -848,19 +940,25 @@ impl Image { format: TextureFormat, asset_usage: RenderAssetUsages, ) -> Self { - let byte_len = format.pixel_size() * size.volume(); - debug_assert_eq!( - pixel.len() % format.pixel_size(), - 0, - "Must not have incomplete pixel data (pixel size is {}B).", - format.pixel_size(), - ); - debug_assert!( - pixel.len() <= byte_len, - "Fill data must fit within pixel buffer (expected {byte_len}B).", - ); - let data = pixel.iter().copied().cycle().take(byte_len).collect(); - Image::new(size, dimension, data, format, asset_usage) + let mut image = Image::new_uninit(size, dimension, format, asset_usage); + if let Ok(pixel_size) = image.texture_descriptor.format.pixel_size() + && pixel_size > 0 + { + let byte_len = pixel_size * size.volume(); + debug_assert_eq!( + pixel.len() % pixel_size, + 0, + "Must not have incomplete pixel data (pixel size is {}B).", + pixel_size, + ); + debug_assert!( + pixel.len() <= byte_len, + "Fill data must fit within pixel buffer (expected {byte_len}B).", + ); + let data = pixel.iter().copied().cycle().take(byte_len).collect(); + image.data = Some(data); + } + image } /// Create a new zero-filled image with a given size, which can be rendered to. @@ -893,7 +991,12 @@ impl Image { | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT; // Fill with zeroes - let data = vec![0; format.pixel_size() * size.volume()]; + let data = vec![ + 0; + format.pixel_size().expect( + "Failed to create Image: can't get pixel size for this TextureFormat" + ) * size.volume() + ]; Image { data: Some(data), @@ -953,11 +1056,10 @@ impl Image { /// If you need to keep pixel data intact, use [`Image::resize_in_place`]. pub fn resize(&mut self, size: Extent3d) { self.texture_descriptor.size = size; - if let Some(ref mut data) = self.data { - data.resize( - size.volume() * self.texture_descriptor.format.pixel_size(), - 0, - ); + if let Some(ref mut data) = self.data + && let Ok(pixel_size) = self.texture_descriptor.format.pixel_size() + { + data.resize(size.volume() * pixel_size, 0); } } @@ -983,43 +1085,44 @@ impl Image { /// /// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`]. pub fn resize_in_place(&mut self, new_size: Extent3d) { - let old_size = self.texture_descriptor.size; - let pixel_size = self.texture_descriptor.format.pixel_size(); - let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume(); - self.texture_descriptor.size = new_size; + if let Ok(pixel_size) = self.texture_descriptor.format.pixel_size() { + let old_size = self.texture_descriptor.size; + let byte_len = pixel_size * new_size.volume(); + self.texture_descriptor.size = new_size; - let Some(ref mut data) = self.data else { - self.copy_on_resize = true; - return; - }; + let Some(ref mut data) = self.data else { + self.copy_on_resize = true; + return; + }; - let mut new: Vec = vec![0; byte_len]; + let mut new: Vec = vec![0; byte_len]; - let copy_width = old_size.width.min(new_size.width) as usize; - let copy_height = old_size.height.min(new_size.height) as usize; - let copy_depth = old_size - .depth_or_array_layers - .min(new_size.depth_or_array_layers) as usize; + let copy_width = old_size.width.min(new_size.width) as usize; + let copy_height = old_size.height.min(new_size.height) as usize; + let copy_depth = old_size + .depth_or_array_layers + .min(new_size.depth_or_array_layers) as usize; - let old_row_stride = old_size.width as usize * pixel_size; - let old_layer_stride = old_size.height as usize * old_row_stride; + let old_row_stride = old_size.width as usize * pixel_size; + let old_layer_stride = old_size.height as usize * old_row_stride; - let new_row_stride = new_size.width as usize * pixel_size; - let new_layer_stride = new_size.height as usize * new_row_stride; + let new_row_stride = new_size.width as usize * pixel_size; + let new_layer_stride = new_size.height as usize * new_row_stride; - for z in 0..copy_depth { - for y in 0..copy_height { - let old_offset = z * old_layer_stride + y * old_row_stride; - let new_offset = z * new_layer_stride + y * new_row_stride; + for z in 0..copy_depth { + for y in 0..copy_height { + let old_offset = z * old_layer_stride + y * old_row_stride; + let new_offset = z * new_layer_stride + y * new_row_stride; - let old_range = (old_offset)..(old_offset + copy_width * pixel_size); - let new_range = (new_offset)..(new_offset + copy_width * pixel_size); + let old_range = (old_offset)..(old_offset + copy_width * pixel_size); + let new_range = (new_offset)..(new_offset + copy_width * pixel_size); - new[new_range].copy_from_slice(&data[old_range]); + new[new_range].copy_from_slice(&data[old_range]); + } } - } - self.data = Some(new); + self.data = Some(new); + } } /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets @@ -1151,7 +1254,7 @@ impl Image { let height = self.texture_descriptor.size.height; let depth = self.texture_descriptor.size.depth_or_array_layers; - let pixel_size = self.texture_descriptor.format.pixel_size(); + let pixel_size = self.texture_descriptor.format.pixel_size().ok()?; let pixel_offset = match self.texture_descriptor.dimension { TextureDimension::D3 | TextureDimension::D2 => { if coords.x >= width || coords.y >= height || coords.z >= depth { @@ -1173,7 +1276,7 @@ impl Image { /// Get a reference to the data bytes where a specific pixel's value is stored #[inline(always)] pub fn pixel_bytes(&self, coords: UVec3) -> Option<&[u8]> { - let len = self.texture_descriptor.format.pixel_size(); + let len = self.texture_descriptor.format.pixel_size().ok()?; let data = self.data.as_ref()?; self.pixel_data_offset(coords) .map(|start| &data[start..(start + len)]) @@ -1182,7 +1285,7 @@ impl Image { /// Get a mutable reference to the data bytes where a specific pixel's value is stored #[inline(always)] pub fn pixel_bytes_mut(&mut self, coords: UVec3) -> Option<&mut [u8]> { - let len = self.texture_descriptor.format.pixel_size(); + let len = self.texture_descriptor.format.pixel_size().ok()?; let offset = self.pixel_data_offset(coords); let data = self.data.as_mut()?; offset.map(|start| &mut data[start..(start + len)]) @@ -1722,15 +1825,16 @@ impl Volume for Extent3d { /// Extends the wgpu [`TextureFormat`] with information about the pixel. pub trait TextureFormatPixelInfo { /// Returns the size of a pixel in bytes of the format. - fn pixel_size(&self) -> usize; + /// error with `TextureAccessError::UnsupportedTextureFormat` if the format is compressed. + fn pixel_size(&self) -> Result; } impl TextureFormatPixelInfo for TextureFormat { - fn pixel_size(&self) -> usize { + fn pixel_size(&self) -> Result { let info = self; match info.block_dimensions() { - (1, 1) => info.block_copy_size(None).unwrap() as usize, - _ => panic!("Using pixel_size for compressed textures is invalid"), + (1, 1) => Ok(info.block_copy_size(None).unwrap() as usize), + _ => Err(TextureAccessError::UnsupportedTextureFormat(*self)), } } } diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index fe086db674a4e..91d03cb9d1116 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -99,8 +99,15 @@ pub enum ImageFormatSetting { /// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { - /// How to determine the image's format. + /// How to determine the image's container format. pub format: ImageFormatSetting, + /// Forcibly use a specific [`wgpu_types::TextureFormat`]. + /// Useful to control how data is handled when used + /// in a shader. + /// Ex: data that would be `R16Uint` that needs to + /// be sampled as a float using `R16Snorm`. + #[serde(skip)] + pub texture_format: Option, /// Specifies whether image data is linear /// or in sRGB space when this is not determined by /// the image format. @@ -117,6 +124,7 @@ impl Default for ImageLoaderSettings { fn default() -> Self { Self { format: ImageFormatSetting::default(), + texture_format: None, is_srgb: true, sampler: ImageSampler::Default, asset_usage: RenderAssetUsages::default(), @@ -176,6 +184,12 @@ impl AssetLoader for ImageLoader { settings.sampler.clone(), settings.asset_usage, ) + .map(|mut image| { + if let Some(format) = settings.texture_format { + image.texture_descriptor.format = format; + } + image + }) .map_err(|err| FileTextureError { error: err, path: format!("{}", load_context.path().display()), diff --git a/crates/bevy_image/src/image_texture_conversion.rs b/crates/bevy_image/src/image_texture_conversion.rs index 1eb3b78b1eaff..c6fb6e33e10b3 100644 --- a/crates/bevy_image/src/image_texture_conversion.rs +++ b/crates/bevy_image/src/image_texture_conversion.rs @@ -108,8 +108,9 @@ impl Image { height = image.height(); format = TextureFormat::Rgba32Float; - let mut local_data = - Vec::with_capacity(width as usize * height as usize * format.pixel_size()); + let mut local_data = Vec::with_capacity( + width as usize * height as usize * format.pixel_size().unwrap_or(0), + ); for pixel in image.into_raw().chunks_exact(3) { // TODO: use the array_chunks method once stabilized diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index be9f8ecc5543b..74e8c0caadbda 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -43,12 +43,13 @@ pub fn ktx2_buffer_to_image( let depth = depth.max(1); // Handle supercompression - let mut levels = Vec::new(); + let mut levels: Vec>; if let Some(supercompression_scheme) = supercompression_scheme { - for (level_index, level) in ktx2.levels().enumerate() { - match supercompression_scheme { - #[cfg(feature = "flate2")] - SupercompressionScheme::ZLIB => { + match supercompression_scheme { + #[cfg(feature = "flate2")] + SupercompressionScheme::ZLIB => { + levels = Vec::with_capacity(ktx2.levels().len()); + for (level_index, level) in ktx2.levels().enumerate() { let mut decoder = flate2::bufread::ZlibDecoder::new(level.data); let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed).map_err(|err| { @@ -58,8 +59,11 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } - #[cfg(all(feature = "zstd_rust", not(feature = "zstd_c")))] - SupercompressionScheme::Zstandard => { + } + #[cfg(all(feature = "zstd_rust", not(feature = "zstd_c")))] + SupercompressionScheme::Zstandard => { + levels = Vec::with_capacity(ktx2.levels().len()); + for (level_index, level) in ktx2.levels().enumerate() { let mut cursor = std::io::Cursor::new(level.data); let mut decoder = ruzstd::decoding::StreamingDecoder::new(&mut cursor) .map_err(|err| TextureError::SuperDecompressionError(err.to_string()))?; @@ -71,19 +75,22 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } - #[cfg(feature = "zstd_c")] - SupercompressionScheme::Zstandard => { + } + #[cfg(feature = "zstd_c")] + SupercompressionScheme::Zstandard => { + levels = Vec::with_capacity(ktx2.levels().len()); + for (level_index, level) in ktx2.levels().enumerate() { levels.push(zstd::decode_all(level.data).map_err(|err| { TextureError::SuperDecompressionError(format!( "Failed to decompress {supercompression_scheme:?} for mip {level_index}: {err:?}", )) })?); } - _ => { - return Err(TextureError::SuperDecompressionError(format!( - "Unsupported supercompression scheme: {supercompression_scheme:?}", - ))); - } + } + _ => { + return Err(TextureError::SuperDecompressionError(format!( + "Unsupported supercompression scheme: {supercompression_scheme:?}", + ))); } } } else { diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 3ab8ff91fe33f..d17e8e092b915 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -6,7 +6,7 @@ pub mod prelude { pub use crate::{ dynamic_texture_atlas_builder::DynamicTextureAtlasBuilder, texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources}, - BevyDefault as _, Image, ImageFormat, TextureAtlasBuilder, TextureError, + BevyDefault as _, Image, ImageFormat, ImagePlugin, TextureAtlasBuilder, TextureError, }; } diff --git a/crates/bevy_image/src/texture_atlas_builder.rs b/crates/bevy_image/src/texture_atlas_builder.rs index 2f23331c8cb2b..98da24ac41611 100644 --- a/crates/bevy_image/src/texture_atlas_builder.rs +++ b/crates/bevy_image/src/texture_atlas_builder.rs @@ -9,7 +9,7 @@ use thiserror::Error; use tracing::{debug, error, warn}; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; -use crate::{Image, TextureFormatPixelInfo}; +use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use crate::{TextureAtlasLayout, TextureAtlasSources}; #[derive(Debug, Error)] @@ -24,6 +24,9 @@ pub enum TextureAtlasBuilderError { /// Attempted to add an uninitialized texture to an atlas #[error("cannot add uninitialized texture to atlas")] UninitializedSourceTexture, + /// A texture access error occurred + #[error("texture access error: {0}")] + TextureAccess(#[from] TextureAccessError), } #[derive(Debug)] @@ -117,7 +120,7 @@ impl<'a> TextureAtlasBuilder<'a> { let rect_x = packed_location.x() as usize; let rect_y = packed_location.y() as usize; let atlas_width = atlas_texture.width() as usize; - let format_size = atlas_texture.texture_descriptor.format.pixel_size(); + let format_size = atlas_texture.texture_descriptor.format.pixel_size()?; let Some(ref mut atlas_data) = atlas_texture.data else { return Err(TextureAtlasBuilderError::UninitializedAtlas); @@ -243,7 +246,7 @@ impl<'a> TextureAtlasBuilder<'a> { TextureDimension::D2, vec![ 0; - self.format.pixel_size() * (current_width * current_height) as usize + self.format.pixel_size()? * (current_width * current_height) as usize ], self.format, RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 3d1f8851350d8..9809a7627bd79 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -419,11 +419,11 @@ mod tests { struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: On>, + event: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(trigger.target()) { - if let Key::Character(c) = &trigger.input.logical_key { + if let Ok(mut gather) = query.get_mut(event.entity()) { + if let Key::Character(c) = &event.input.logical_key { gather.0.push_str(c.as_str()); } } diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index e8eb6d85ce0dc..2599054313b65 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -322,14 +322,14 @@ pub(crate) fn acquire_focus( mut focus: ResMut, ) { // If the entity has a TabIndex - if focusable.contains(ev.target()) { + if focusable.contains(ev.entity()) { // Stop and focus it ev.propagate(false); // Don't mutate unless we need to, for change detection - if focus.0 != Some(ev.target()) { - focus.0 = Some(ev.target()); + if focus.0 != Some(ev.entity()) { + focus.0 = Some(ev.entity()); } - } else if windows.contains(ev.target()) { + } else if windows.contains(ev.entity()) { // Stop and clear focus ev.propagate(false); // Don't mutate unless we need to, for change detection @@ -366,7 +366,7 @@ fn click_to_focus( // for every ancestor, but only for the original entity. Also, users may want to stop // propagation on the pointer event at some point along the bubbling chain, so we need our // own dedicated event whose propagation we can control. - if ev.target() == ev.original_target() { + if ev.entity() == ev.original_entity() { // Clicking hides focus if focus_visible.0 { focus_visible.0 = false; @@ -374,7 +374,7 @@ fn click_to_focus( // Search for a focusable parent entity, defaulting to window if none. if let Ok(window) = windows.single() { commands - .entity(ev.target()) + .entity(ev.entity()) .trigger(AcquireFocus { window }); } } @@ -387,14 +387,14 @@ fn click_to_focus( /// /// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings. pub fn handle_tab_navigation( - mut trigger: On>, + mut event: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, keys: Res>, ) { // Tab navigation. - let key_event = &trigger.event().input; + let key_event = &event.input; if key_event.key_code == KeyCode::Tab && key_event.state == ButtonState::Pressed && !key_event.repeat @@ -410,7 +410,7 @@ pub fn handle_tab_navigation( match maybe_next { Ok(next) => { - trigger.propagate(false); + event.propagate(false); focus.set(next); visible.0 = true; } @@ -418,7 +418,7 @@ pub fn handle_tab_navigation( 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); + event.propagate(false); focus.set(new_focus); visible.0 = true; } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index c662a4858dad8..babb95a4dac1c 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -14,7 +14,7 @@ trace = [ "bevy_app/trace", "bevy_asset?/trace", "bevy_core_pipeline?/trace", - "bevy_anti_aliasing?/trace", + "bevy_anti_alias?/trace", "bevy_ecs/trace", "bevy_log/trace", "bevy_pbr?/trace", @@ -23,22 +23,13 @@ trace = [ ] trace_chrome = ["bevy_log/tracing-chrome"] trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy"] -trace_tracy_memory = ["bevy_log/trace_tracy_memory"] +trace_tracy_memory = ["bevy_log/trace_tracy_memory", "trace", "trace_tracy"] detailed_trace = ["bevy_ecs/detailed_trace", "bevy_render?/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] # Enables compressed KTX2 UASTC texture output on the asset processor -compressed_image_saver = [ - "bevy_image/compressed_image_saver", - "bevy_render/compressed_image_saver", -] - -# Texture formats that have specific rendering support (HDR enabled by default) -basis-universal = ["bevy_image/basis-universal", "bevy_render/basis-universal"] -exr = ["bevy_image/exr", "bevy_render/exr"] -hdr = ["bevy_image/hdr", "bevy_render/hdr"] -ktx2 = ["bevy_image/ktx2", "bevy_render/ktx2"] +compressed_image_saver = ["bevy_image/compressed_image_saver"] # For ktx2 supercompression zlib = ["bevy_image/zlib"] @@ -46,7 +37,8 @@ zstd = ["bevy_image/zstd"] zstd_rust = ["bevy_image/zstd_rust"] zstd_c = ["bevy_image/zstd_c"] -# Image format support (PNG enabled by default) +# Image format support (HDR and PNG enabled by default) +basis-universal = ["bevy_image/basis-universal"] bmp = ["bevy_image/bmp"] ff = ["bevy_image/ff"] gif = ["bevy_image/gif"] @@ -59,6 +51,9 @@ tga = ["bevy_image/tga"] tiff = ["bevy_image/tiff"] webp = ["bevy_image/webp"] dds = ["bevy_image/dds"] +exr = ["bevy_image/exr"] +hdr = ["bevy_image/hdr"] +ktx2 = ["bevy_image/ktx2"] # Enable SPIR-V passthrough spirv_shader_passthrough = ["bevy_render/spirv_shader_passthrough"] @@ -67,13 +62,30 @@ spirv_shader_passthrough = ["bevy_render/spirv_shader_passthrough"] # TODO: When wgpu switches to DirectX 12 instead of Vulkan by default on windows, make this a default feature statically-linked-dxc = ["bevy_render/statically-linked-dxc"] +# Forces the wgpu instance to be initialized using the raw Vulkan HAL, enabling additional configuration +raw_vulkan_init = ["bevy_render/raw_vulkan_init"] + # Include tonemapping LUT KTX2 files. -tonemapping_luts = ["bevy_core_pipeline/tonemapping_luts"] -# Include Bluenoise texture for environment map generation. -bluenoise_texture = ["bevy_pbr?/bluenoise_texture"] +tonemapping_luts = [ + "bevy_core_pipeline?/tonemapping_luts", + "ktx2", + "bevy_image/zstd", +] # Include SMAA LUT KTX2 Files -smaa_luts = ["bevy_anti_aliasing/smaa_luts"] +smaa_luts = ["bevy_anti_alias?/smaa_luts", "ktx2", "bevy_image/zstd"] + +# Include Bluenoise texture for environment map generation. +bluenoise_texture = ["bevy_pbr?/bluenoise_texture", "ktx2", "bevy_image/zstd"] + +# NVIDIA Deep Learning Super Sampling +dlss = ["bevy_anti_alias/dlss", "bevy_solari?/dlss"] + +# Forcibly disable DLSS so that cargo build --all-features works without the DLSS SDK being installed. Not meant for users. +force_disable_dlss = [ + "bevy_anti_alias?/force_disable_dlss", + "bevy_solari?/force_disable_dlss", +] # Audio format support (vorbis is enabled by default) flac = ["bevy_audio/flac"] @@ -89,11 +101,14 @@ symphonia-wav = ["bevy_audio/symphonia-wav"] # Shader formats shader_format_glsl = [ - "bevy_render/shader_format_glsl", + "bevy_shader/shader_format_glsl", "bevy_pbr?/shader_format_glsl", ] -shader_format_spirv = ["bevy_render/shader_format_spirv"] -shader_format_wesl = ["bevy_render/shader_format_wesl"] +shader_format_spirv = [ + "bevy_shader/shader_format_spirv", + "bevy_render?/shader_format_spirv", +] +shader_format_wesl = ["bevy_shader/shader_format_wesl"] serialize = [ "bevy_a11y?/serialize", @@ -167,48 +182,76 @@ pbr_specular_textures = [ # Optimise for WebGL2 webgl = [ "bevy_core_pipeline?/webgl", - "bevy_anti_aliasing?/webgl", + "bevy_anti_alias?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl", "bevy_gizmos?/webgl", - "bevy_sprite?/webgl", + "bevy_sprite_render?/webgl", ] webgpu = [ "bevy_core_pipeline?/webgpu", - "bevy_anti_aliasing?/webgpu", + "bevy_anti_alias?/webgpu", "bevy_pbr?/webgpu", "bevy_render?/webgpu", "bevy_gizmos?/webgpu", - "bevy_sprite?/webgpu", + "bevy_sprite_render?/webgpu", ] -# enable systems that allow for automated testing on CI +# Enable systems that allow for automated testing on CI 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"] +animation = [ + "bevy_animation", + "bevy_mesh", + "bevy_color", + "bevy_gltf?/bevy_animation", +] -bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite", "bevy_image"] +bevy_shader = ["dep:bevy_shader"] +bevy_image = ["dep:bevy_image", "bevy_color", "bevy_asset"] +bevy_sprite = ["dep:bevy_sprite", "bevy_camera", "bevy_gizmos?/bevy_sprite"] +bevy_text = [ + "dep:bevy_text", + "bevy_image", + "bevy_sprite?/bevy_text", + "bevy_sprite_render?/bevy_text", +] +bevy_ui = ["dep:bevy_ui", "bevy_text", "bevy_sprite"] +bevy_mesh = ["dep:bevy_mesh", "bevy_image"] +bevy_window = ["dep:bevy_window", "dep:bevy_a11y", "bevy_image"] +bevy_winit = ["dep:bevy_winit", "bevy_window"] +bevy_camera = ["dep:bevy_camera", "bevy_mesh", "bevy_window"] +bevy_scene = ["dep:bevy_scene", "bevy_asset"] +bevy_light = ["dep:bevy_light", "bevy_camera"] +bevy_render = [ + "dep:bevy_render", + "bevy_camera", + "bevy_shader", + "bevy_color/wgpu-types", + "bevy_color/encase", + "bevy_gizmos?/bevy_render", +] +bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_render"] +bevy_anti_alias = ["dep:bevy_anti_alias", "bevy_core_pipeline"] +bevy_post_process = ["dep:bevy_post_process", "bevy_core_pipeline"] bevy_pbr = [ "dep:bevy_pbr", - "bevy_gizmos?/bevy_pbr", "bevy_light", - "bevy_render", + "bevy_core_pipeline", + "bevy_gizmos?/bevy_pbr", ] -bevy_window = ["dep:bevy_window", "dep:bevy_a11y"] -bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_image"] -bevy_anti_aliasing = ["dep:bevy_anti_aliasing", "bevy_image"] -bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"] -bevy_gltf = ["dep:bevy_gltf", "bevy_image"] -bevy_ui = ["dep:bevy_ui", "bevy_image"] -bevy_ui_render = ["dep:bevy_ui_render"] -bevy_shader = ["dep:bevy_shader"] -bevy_image = ["dep:bevy_image"] - -bevy_mesh = ["dep:bevy_mesh", "bevy_image"] -bevy_camera = ["dep:bevy_camera", "bevy_mesh"] -bevy_light = ["dep:bevy_light", "bevy_camera"] +bevy_sprite_render = [ + "dep:bevy_sprite_render", + "bevy_sprite", + "bevy_core_pipeline", + "bevy_gizmos?/bevy_sprite_render", +] +bevy_ui_render = ["dep:bevy_ui_render", "bevy_sprite_render", "bevy_ui"] +bevy_solari = ["dep:bevy_solari", "bevy_pbr"] +bevy_gizmos = ["dep:bevy_gizmos", "bevy_camera"] +bevy_gltf = ["dep:bevy_gltf", "bevy_scene", "bevy_pbr"] # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -220,17 +263,6 @@ android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"] # screen readers and forks.) accesskit_unix = ["bevy_winit/accesskit_unix"] -bevy_text = ["dep:bevy_text", "bevy_image"] - -bevy_render = [ - "dep:bevy_render", - "bevy_gizmos?/bevy_render", - "bevy_camera", - "bevy_shader", - "bevy_color/wgpu-types", - "bevy_color/encase", -] - # Enable assertions to check the validity of parameters passed to glam glam_assert = ["bevy_math/glam_assert"] @@ -239,6 +271,15 @@ debug_glam_assert = ["bevy_math/debug_glam_assert"] default_font = ["bevy_text?/default_font"] +# Enables downloading assets from HTTP sources +http = ["bevy_asset?/http"] + +# Enables downloading assets from HTTPS sources +https = ["bevy_asset?/https"] + +# Enable caching downloaded assets on the filesystem. NOTE: this cache currently never invalidates entries! +web_asset_cache = ["bevy_asset?/web_asset_cache"] + # Enables the built-in asset processor for processed assets. asset_processor = ["bevy_asset?/asset_processor"] @@ -383,10 +424,6 @@ web = ["bevy_app/web", "bevy_platform/web", "bevy_reflect/web"] hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] -gltf_convert_coordinates_default = [ - "bevy_gltf?/gltf_convert_coordinates_default", -] - debug = ["bevy_utils/debug"] [dependencies] @@ -438,8 +475,9 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.17.0-dev", "bevy_reflect", ] } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.17.0-dev" } +bevy_post_process = { path = "../bevy_post_process", optional = true, version = "0.17.0-dev" } bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.17.0-dev" } -bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.17.0-dev" } +bevy_anti_alias = { path = "../bevy_anti_alias", optional = true, version = "0.17.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" } bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false } @@ -460,6 +498,7 @@ bevy_render = { path = "../bevy_render", optional = true, version = "0.17.0-dev" bevy_scene = { path = "../bevy_scene", optional = true, version = "0.17.0-dev" } bevy_solari = { path = "../bevy_solari", optional = true, version = "0.17.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.17.0-dev" } +bevy_sprite_render = { path = "../bevy_sprite_render", optional = true, version = "0.17.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.17.0-dev", default-features = false, features = [ "bevy_app", "bevy_reflect", diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index cdb59921dcc74..4467da12f4f11 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -21,27 +21,43 @@ plugin_group! { #[cfg(feature = "std")] #[custom(cfg(any(all(unix, not(target_os = "horizon")), windows)))] bevy_app:::TerminalCtrlCHandlerPlugin, + // NOTE: Load this before AssetPlugin to properly register http asset sources. + #[cfg(feature = "bevy_asset")] + #[custom(cfg(any(feature = "http", feature = "https")))] + bevy_asset::io::web:::WebAssetPlugin, #[cfg(feature = "bevy_asset")] bevy_asset:::AssetPlugin, #[cfg(feature = "bevy_scene")] bevy_scene:::ScenePlugin, #[cfg(feature = "bevy_winit")] bevy_winit:::WinitPlugin, + #[custom(cfg(all(feature = "dlss", not(feature = "force_disable_dlss"))))] + bevy_anti_alias::dlss:::DlssInitPlugin, #[cfg(feature = "bevy_render")] bevy_render:::RenderPlugin, // NOTE: Load this after renderer initialization so that it knows about the supported // compressed texture formats. - #[cfg(feature = "bevy_render")] - bevy_render::texture:::ImagePlugin, + #[cfg(feature = "bevy_image")] + bevy_image:::ImagePlugin, + #[cfg(feature = "bevy_mesh")] + bevy_mesh:::MeshPlugin, + #[cfg(feature = "bevy_camera")] + bevy_camera:::CameraPlugin, + #[cfg(feature = "bevy_light")] + bevy_light:::LightPlugin, #[cfg(feature = "bevy_render")] #[custom(cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded")))] bevy_render::pipelined_rendering:::PipelinedRenderingPlugin, #[cfg(feature = "bevy_core_pipeline")] bevy_core_pipeline:::CorePipelinePlugin, - #[cfg(feature = "bevy_anti_aliasing")] - bevy_anti_aliasing:::AntiAliasingPlugin, + #[cfg(feature = "bevy_post_process")] + bevy_post_process:::PostProcessPlugin, + #[cfg(feature = "bevy_anti_alias")] + bevy_anti_alias:::AntiAliasPlugin, #[cfg(feature = "bevy_sprite")] bevy_sprite:::SpritePlugin, + #[cfg(feature = "bevy_sprite_render")] + bevy_sprite_render:::SpriteRenderPlugin, #[cfg(feature = "bevy_text")] bevy_text:::TextPlugin, #[cfg(feature = "bevy_ui")] diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 67b1e465e715f..78c8ba2b221f5 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -20,8 +20,8 @@ pub use bevy_a11y as a11y; pub use bevy_android as android; #[cfg(feature = "bevy_animation")] pub use bevy_animation as animation; -#[cfg(feature = "bevy_anti_aliasing")] -pub use bevy_anti_aliasing as anti_aliasing; +#[cfg(feature = "bevy_anti_alias")] +pub use bevy_anti_alias as anti_alias; pub use bevy_app as app; #[cfg(feature = "bevy_asset")] pub use bevy_asset as asset; @@ -64,6 +64,8 @@ pub use bevy_pbr as pbr; #[cfg(feature = "bevy_picking")] pub use bevy_picking as picking; pub use bevy_platform as platform; +#[cfg(feature = "bevy_post_process")] +pub use bevy_post_process as post_process; pub use bevy_ptr as ptr; pub use bevy_reflect as reflect; #[cfg(feature = "bevy_remote")] @@ -78,6 +80,8 @@ pub use bevy_shader as shader; pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; +#[cfg(feature = "bevy_sprite_render")] +pub use bevy_sprite_render as sprite_render; #[cfg(feature = "bevy_state")] pub use bevy_state as state; pub use bevy_tasks as tasks; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index e1be8d70cebb3..ea4a306d0a6d6 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -67,6 +67,10 @@ pub use crate::scene::prelude::*; #[cfg(feature = "bevy_sprite")] pub use crate::sprite::prelude::*; +#[doc(hidden)] +#[cfg(feature = "bevy_sprite_render")] +pub use crate::sprite_render::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_text")] pub use crate::text::prelude::*; diff --git a/crates/bevy_light/src/cluster/assign.rs b/crates/bevy_light/src/cluster/assign.rs index 20a40104edf0f..e629ccf7db88e 100644 --- a/crates/bevy_light/src/cluster/assign.rs +++ b/crates/bevy_light/src/cluster/assign.rs @@ -328,10 +328,10 @@ pub(crate) fn assign_objects_to_clusters( let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size); - let world_from_view = camera_transform.to_matrix(); + let world_from_view = camera_transform.affine(); let view_from_world_scale = camera_transform.compute_transform().scale.recip(); let view_from_world_scale_max = view_from_world_scale.abs().max_element(); - let view_from_world = world_from_view.inverse(); + let view_from_world = Mat4::from(world_from_view.inverse()); let is_orthographic = camera.clip_from_view().w_axis.w == 1.0; let far_z = match config.far_z_mode() { diff --git a/crates/bevy_light/src/directional_light.rs b/crates/bevy_light/src/directional_light.rs index 39e52fb5f02e8..b7e97a3a78536 100644 --- a/crates/bevy_light/src/directional_light.rs +++ b/crates/bevy_light/src/directional_light.rs @@ -240,3 +240,53 @@ pub fn update_directional_light_frusta( .collect(); } } + +/// Add to a [`DirectionalLight`] to control rendering of the visible solar disk in the sky. +/// Affects only the disk’s appearance, not the light’s illuminance or shadows. +/// Requires a `bevy::pbr::Atmosphere` component on a [`Camera3d`](bevy_camera::Camera3d) to have any effect. +/// +/// By default, the atmosphere is rendered with [`SunDisk::EARTH`], which approximates the +/// apparent size and brightness of the Sun as seen from Earth. You can also disable the sun +/// disk entirely with [`SunDisk::OFF`]. +#[derive(Component, Clone)] +#[require(DirectionalLight)] +pub struct SunDisk { + /// The angular size (diameter) of the sun disk in radians, as observed from the scene. + pub angular_size: f32, + /// Multiplier for the brightness of the sun disk. + /// + /// `0.0` disables the disk entirely (atmospheric scattering still occurs), + /// `1.0` is the default physical intensity, and values `>1.0` overexpose it. + pub intensity: f32, +} + +impl SunDisk { + /// Earth-like parameters for the sun disk. + /// + /// Uses the mean apparent size (~32 arcminutes) of the Sun at 1 AU distance + /// with default intensity. + pub const EARTH: SunDisk = SunDisk { + angular_size: 0.00930842, + intensity: 1.0, + }; + + /// No visible sun disk. + /// + /// Keeps scattering and directional light illumination, but hides the disk itself. + pub const OFF: SunDisk = SunDisk { + angular_size: 0.0, + intensity: 0.0, + }; +} + +impl Default for SunDisk { + fn default() -> Self { + Self::EARTH + } +} + +impl Default for &SunDisk { + fn default() -> Self { + &SunDisk::EARTH + } +} diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index 324de212fab72..b4904d386ba87 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -27,7 +27,10 @@ use cluster::{ mod ambient_light; pub use ambient_light::AmbientLight; mod probe; -pub use probe::{EnvironmentMapLight, GeneratedEnvironmentMapLight, IrradianceVolume, LightProbe}; +pub use probe::{ + AtmosphereEnvironmentMapLight, EnvironmentMapLight, GeneratedEnvironmentMapLight, + IrradianceVolume, LightProbe, +}; mod volumetric; pub use volumetric::{FogVolume, VolumetricFog, VolumetricLight}; pub mod cascade; @@ -45,7 +48,7 @@ pub use spot_light::{ mod directional_light; pub use directional_light::{ update_directional_light_frusta, DirectionalLight, DirectionalLightShadowMap, - DirectionalLightTexture, + DirectionalLightTexture, SunDisk, }; /// The light prelude. @@ -124,6 +127,7 @@ pub mod light_consts { } } +#[derive(Default)] pub struct LightPlugin; impl Plugin for LightPlugin { diff --git a/crates/bevy_light/src/point_light.rs b/crates/bevy_light/src/point_light.rs index 39f98d448aeee..714920c09e70f 100644 --- a/crates/bevy_light/src/point_light.rs +++ b/crates/bevy_light/src/point_light.rs @@ -232,7 +232,7 @@ pub fn update_point_light_frusta( for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); + let clip_from_world = clip_from_view * world_from_view.compute_affine().inverse(); *frustum = Frustum::from_clip_from_world_custom_far( &clip_from_world, diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index 11d316c8751c1..213a02b4a0619 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -2,7 +2,7 @@ use bevy_asset::Handle; use bevy_camera::visibility::Visibility; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::Quat; +use bevy_math::{Quat, UVec2}; use bevy_reflect::prelude::*; use bevy_transform::components::Transform; @@ -140,6 +140,39 @@ impl Default for GeneratedEnvironmentMapLight { } } +/// Lets the atmosphere contribute environment lighting (reflections and ambient diffuse) to your scene. +/// +/// Attach this to a [`Camera3d`](bevy_camera::Camera3d) to light the entire view, or to a +/// [`LightProbe`] to light only a specific region. +/// Behind the scenes, this generates an environment map from the atmosphere for image-based lighting +/// and inserts a corresponding [`GeneratedEnvironmentMapLight`]. +/// +/// For HDRI-based lighting, use a preauthored [`EnvironmentMapLight`] or filter one at runtime with +/// [`GeneratedEnvironmentMapLight`]. +#[derive(Component, Clone)] +pub struct AtmosphereEnvironmentMapLight { + /// Controls how bright the atmosphere's environment lighting is. + /// Increase this value to brighten reflections and ambient diffuse lighting. + /// + /// The default is `1.0` so that the generated environment lighting matches + /// the light intensity of the atmosphere in the scene. + pub intensity: f32, + /// Whether the diffuse contribution should affect meshes that already have lightmaps. + pub affects_lightmapped_mesh_diffuse: bool, + /// Cubemap resolution in pixels (must be a power-of-two). + pub size: UVec2, +} + +impl Default for AtmosphereEnvironmentMapLight { + fn default() -> Self { + Self { + intensity: 1.0, + affects_lightmapped_mesh_diffuse: true, + size: UVec2::new(512, 512), + } + } +} + /// The component that defines an irradiance volume. /// /// See `bevy_pbr::irradiance_volume` for detailed information. diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index 4b7481629a162..e4bc5696ac809 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -6,7 +6,7 @@ use bevy_camera::{ use bevy_color::Color; use bevy_ecs::prelude::*; use bevy_image::Image; -use bevy_math::{Dir3, Mat3, Mat4, Vec3}; +use bevy_math::{Affine3A, Dir3, Mat3, Mat4, Vec3}; use bevy_reflect::prelude::*; use bevy_transform::components::{GlobalTransform, Transform}; @@ -176,14 +176,12 @@ pub fn orthonormalize(z_basis: Dir3) -> Mat3 { /// Constructs a right-handed orthonormal basis with translation, using only the forward direction and translation of a given [`GlobalTransform`]. /// /// This is a version of [`orthonormalize`] which also includes translation. -pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Mat4 { +pub fn spot_light_world_from_view(transform: &GlobalTransform) -> Affine3A { // the matrix z_local (opposite of transform.forward()) let fwd_dir = transform.back(); let basis = orthonormalize(fwd_dir); - let mut mat = Mat4::from_mat3(basis); - mat.w_axis = transform.translation().extend(1.0); - mat + Affine3A::from_mat3_translation(basis, transform.translation()) } pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 3dcfa277942f1..76e4ec7a05674 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -20,7 +20,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } # other -tracing-subscriber = { version = "0.3.1", features = [ +tracing-subscriber = { version = "0.3.20", features = [ "registry", "env-filter", ] } diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 5f11ad5233dc4..3a8f9a742edfc 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -127,7 +127,7 @@ impl BoundingVolume for Aabb2d { #[inline(always)] fn visible_area(&self) -> f32 { - let b = self.max - self.min; + let b = (self.max - self.min).max(Vec2::ZERO); b.x * b.y } diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index f55f40ddc6c87..a6b795c24a54d 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -4,16 +4,15 @@ use crate::{ bounding::BoundingVolume, ops, primitives::{ - Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, ConvexPolygon, Ellipse, - Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Rhombus, Segment2d, - Triangle2d, + Annulus, Arc2d, Capsule2d, Circle, CircularSector, CircularSegment, Ellipse, Line2d, + Plane2d, Rectangle, RegularPolygon, Rhombus, Segment2d, Triangle2d, }, Dir2, Isometry2d, Mat2, Rot2, Vec2, }; use core::f32::consts::{FRAC_PI_2, PI, TAU}; #[cfg(feature = "alloc")] -use crate::primitives::{BoxedPolygon, BoxedPolyline2d}; +use crate::primitives::{ConvexPolygon, Polygon, Polyline2d}; use smallvec::SmallVec; @@ -279,18 +278,8 @@ impl Bounded2d for Segment2d { } } -impl Bounded2d for Polyline2d { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, &self.vertices) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, &self.vertices) - } -} - #[cfg(feature = "alloc")] -impl Bounded2d for BoxedPolyline2d { +impl Bounded2d for Polyline2d { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(isometry, &self.vertices) } @@ -366,7 +355,8 @@ impl Bounded2d for Rectangle { } } -impl Bounded2d for Polygon { +#[cfg(feature = "alloc")] +impl Bounded2d for Polygon { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(isometry, &self.vertices) } @@ -376,24 +366,14 @@ impl Bounded2d for Polygon { } } -impl Bounded2d for ConvexPolygon { - fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, self.vertices().as_slice()) - } - - fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, self.vertices().as_slice()) - } -} - #[cfg(feature = "alloc")] -impl Bounded2d for BoxedPolygon { +impl Bounded2d for ConvexPolygon { fn aabb_2d(&self, isometry: impl Into) -> Aabb2d { - Aabb2d::from_point_cloud(isometry, &self.vertices) + Aabb2d::from_point_cloud(isometry, self.vertices()) } fn bounding_circle(&self, isometry: impl Into) -> BoundingCircle { - BoundingCircle::from_point_cloud(isometry, &self.vertices) + BoundingCircle::from_point_cloud(isometry, self.vertices()) } } @@ -908,7 +888,7 @@ mod tests { #[test] fn polyline() { - let polyline = Polyline2d::<4>::new([ + let polyline = Polyline2d::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, @@ -981,7 +961,7 @@ mod tests { #[test] fn polygon() { - let polygon = Polygon::<4>::new([ + let polygon = Polygon::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, diff --git a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs index 607d0f27464f3..8698504bcd882 100644 --- a/crates/bevy_math/src/bounding/bounded3d/extrusion.rs +++ b/crates/bevy_math/src/bounding/bounded3d/extrusion.rs @@ -6,14 +6,14 @@ use crate::{ bounding::{BoundingCircle, BoundingVolume}, ops, primitives::{ - Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Polygon, Polyline2d, Primitive2d, - Rectangle, RegularPolygon, Segment2d, Triangle2d, + Capsule2d, Cuboid, Cylinder, Ellipse, Extrusion, Line2d, Primitive2d, Rectangle, + RegularPolygon, Segment2d, Triangle2d, }, Isometry2d, Isometry3d, Quat, Rot2, }; #[cfg(feature = "alloc")] -use crate::primitives::{BoxedPolygon, BoxedPolyline2d}; +use crate::primitives::{Polygon, Polyline2d}; use crate::{bounding::Bounded2d, primitives::Circle}; @@ -96,19 +96,8 @@ impl BoundedExtrusion for Segment2d { } } -impl BoundedExtrusion for Polyline2d { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = - Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter()); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - #[cfg(feature = "alloc")] -impl BoundedExtrusion for BoxedPolyline2d { +impl BoundedExtrusion for Polyline2d { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { let isometry = isometry.into(); let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); @@ -137,19 +126,8 @@ impl BoundedExtrusion for Rectangle { } } -impl BoundedExtrusion for Polygon { - fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { - let isometry = isometry.into(); - let aabb = - Aabb3d::from_point_cloud(isometry, self.vertices.map(|v| v.extend(0.)).into_iter()); - let depth = isometry.rotation * Vec3A::new(0., 0., half_depth); - - aabb.grow(depth.abs()) - } -} - #[cfg(feature = "alloc")] -impl BoundedExtrusion for BoxedPolygon { +impl BoundedExtrusion for Polygon { fn extrusion_aabb_3d(&self, half_depth: f32, isometry: impl Into) -> Aabb3d { let isometry = isometry.into(); let aabb = Aabb3d::from_point_cloud(isometry, self.vertices.iter().map(|v| v.extend(0.))); @@ -367,7 +345,7 @@ mod tests { #[test] fn polyline() { - let polyline = Polyline2d::<4>::new([ + let polyline = Polyline2d::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, @@ -413,7 +391,7 @@ mod tests { #[test] fn polygon() { - let polygon = Polygon::<4>::new([ + let polygon = Polygon::new([ Vec2::ONE, Vec2::new(-1.0, 1.0), Vec2::NEG_ONE, diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index ca3b3597984d9..6769d61dab383 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -136,7 +136,7 @@ impl BoundingVolume for Aabb3d { #[inline(always)] fn visible_area(&self) -> f32 { - let b = self.max - self.min; + let b = (self.max - self.min).max(Vec3A::ZERO); b.x * (b.y + b.z) + b.y * b.z } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index ebfd0266e81d0..7cf118e4d97e8 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -4,14 +4,14 @@ use crate::{ bounding::{Bounded2d, BoundingCircle, BoundingVolume}, ops, primitives::{ - Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, - Segment3d, Sphere, Torus, Triangle2d, Triangle3d, + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Segment3d, + Sphere, Torus, Triangle2d, Triangle3d, }, Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A, }; #[cfg(feature = "alloc")] -use crate::primitives::BoxedPolyline3d; +use crate::primitives::Polyline3d; use super::{Aabb3d, Bounded3d, BoundingSphere}; @@ -86,18 +86,8 @@ impl Bounded3d for Segment3d { } } -impl Bounded3d for Polyline3d { - fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { - Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) - } - - fn bounding_sphere(&self, isometry: impl Into) -> BoundingSphere { - BoundingSphere::from_point_cloud(isometry, &self.vertices) - } -} - #[cfg(feature = "alloc")] -impl Bounded3d for BoxedPolyline3d { +impl Bounded3d for Polyline3d { fn aabb_3d(&self, isometry: impl Into) -> Aabb3d { Aabb3d::from_point_cloud(isometry, self.vertices.iter().copied()) } @@ -471,7 +461,7 @@ mod tests { #[test] fn polyline() { - let polyline = Polyline3d::<4>::new([ + let polyline = Polyline3d::new([ Vec3::ONE, Vec3::new(-1.0, 1.0, 1.0), Vec3::NEG_ONE, diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 055002c9bbcbe..48a2b383b95eb 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -91,6 +91,7 @@ pub struct FunctionCurve { pub(crate) domain: Interval, #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] pub(crate) f: F, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -192,6 +193,7 @@ pub struct MapCurve { pub(crate) preimage: C, #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] pub(crate) f: F, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData<(fn() -> S, fn(S) -> T)>, } @@ -289,6 +291,7 @@ pub struct ReparamCurve { pub(crate) base: C, #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] pub(crate) f: F, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -383,6 +386,7 @@ pub struct LinearReparamCurve { pub(crate) base: C, /// Invariants: This interval must always be bounded. pub(crate) new_domain: Interval, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -416,6 +420,7 @@ where pub struct CurveReparamCurve { pub(crate) base: C, pub(crate) reparam_curve: D, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -448,6 +453,7 @@ where )] pub struct GraphCurve { pub(crate) base: C, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -480,6 +486,7 @@ pub struct ZipCurve { pub(crate) domain: Interval, pub(crate) first: C, pub(crate) second: D, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData (S, T)>, } @@ -520,6 +527,7 @@ where pub struct ChainCurve { pub(crate) first: C, pub(crate) second: D, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -569,6 +577,7 @@ where )] pub struct ReverseCurve { pub(crate) curve: C, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -611,6 +620,7 @@ where pub struct RepeatCurve { pub(crate) domain: Interval, pub(crate) curve: C, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -669,6 +679,7 @@ where )] pub struct ForeverCurve { pub(crate) curve: C, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -723,6 +734,7 @@ where )] pub struct PingPongCurve { pub(crate) curve: C, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } @@ -780,6 +792,7 @@ pub struct ContinuationCurve { pub(crate) second: D, // cache the offset in the curve directly to prevent triple sampling for every sample we make pub(crate) offset: T, + #[cfg_attr(feature = "serialize", serde(skip))] #[cfg_attr(feature = "bevy_reflect", reflect(ignore, clone))] pub(crate) _phantom: PhantomData T>, } diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index 6b9c33ac8afde..a5f880b103e03 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -534,7 +534,7 @@ pub enum EaseFunction { /// #[doc = include_str!("../../images/easefunction/SmoothStepOut.svg")] SmoothStepOut, - /// `f(t) = 2t³ + 3t²` + /// `f(t) = 3t² - 2t³` /// /// This is the Hermite interpolator for /// - f(0) = 0 @@ -794,7 +794,7 @@ pub struct SmoothStepInCurve; #[derive(Copy, Clone)] pub struct SmoothStepOutCurve; -/// `f(t) = 2t³ + 3t²` +/// `f(t) = 3t² - 2t³` /// /// This is the Hermite interpolator for /// - f(0) = 0 diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 76da9555fa5f3..0f3b4bcc26153 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,5 +1,6 @@ use core::f32::consts::{FRAC_1_SQRT_2, FRAC_PI_2, FRAC_PI_3, PI}; use derive_more::derive::From; +#[cfg(feature = "alloc")] use thiserror::Error; use super::{Measured2d, Primitive2d, WindingOrder}; @@ -17,7 +18,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; #[cfg(feature = "alloc")] -use alloc::{boxed::Box, vec::Vec}; +use alloc::vec::Vec; /// A circle primitive, representing the set of points some distance from the origin #[derive(Clone, Copy, Debug, PartialEq)] @@ -54,7 +55,7 @@ impl Circle { /// Get the diameter of the circle #[inline(always)] - pub fn diameter(&self) -> f32 { + pub const fn diameter(&self) -> f32 { 2.0 * self.radius } @@ -141,13 +142,13 @@ impl Default for Arc2d { impl Arc2d { /// Create a new [`Arc2d`] from a `radius` and a `half_angle` #[inline(always)] - pub fn new(radius: f32, half_angle: f32) -> Self { + pub const fn new(radius: f32, half_angle: f32) -> Self { Self { radius, half_angle } } /// Create a new [`Arc2d`] from a `radius` and an `angle` in radians #[inline(always)] - pub fn from_radians(radius: f32, angle: f32) -> Self { + pub const fn from_radians(radius: f32, angle: f32) -> Self { Self { radius, half_angle: angle / 2.0, @@ -156,7 +157,7 @@ impl Arc2d { /// Create a new [`Arc2d`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, angle: f32) -> Self { + pub const fn from_degrees(radius: f32, angle: f32) -> Self { Self { radius, half_angle: angle.to_radians() / 2.0, @@ -167,7 +168,7 @@ impl Arc2d { /// /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_turns(radius: f32, fraction: f32) -> Self { + pub const fn from_turns(radius: f32, fraction: f32) -> Self { Self { radius, half_angle: fraction * PI, @@ -176,13 +177,13 @@ impl Arc2d { /// Get the angle of the arc #[inline(always)] - pub fn angle(&self) -> f32 { + pub const fn angle(&self) -> f32 { self.half_angle * 2.0 } /// Get the length of the arc #[inline(always)] - pub fn length(&self) -> f32 { + pub const fn length(&self) -> f32 { self.angle() * self.radius } @@ -255,7 +256,7 @@ impl Arc2d { /// /// **Note:** This is not the negation of [`is_major`](Self::is_major): an exact semicircle is both major and minor. #[inline(always)] - pub fn is_minor(&self) -> bool { + pub const fn is_minor(&self) -> bool { self.half_angle <= FRAC_PI_2 } @@ -263,7 +264,7 @@ impl Arc2d { /// /// **Note:** This is not the negation of [`is_minor`](Self::is_minor): an exact semicircle is both major and minor. #[inline(always)] - pub fn is_major(&self) -> bool { + pub const fn is_major(&self) -> bool { self.half_angle >= FRAC_PI_2 } } @@ -321,51 +322,59 @@ impl Measured2d for CircularSector { impl CircularSector { /// Create a new [`CircularSector`] from a `radius` and an `angle` #[inline(always)] - pub fn new(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::new(radius, angle)) + pub const fn new(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::new(radius, angle), + } } /// Create a new [`CircularSector`] from a `radius` and an `angle` in radians. #[inline(always)] - pub fn from_radians(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::from_radians(radius, angle)) + pub const fn from_radians(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_radians(radius, angle), + } } /// Create a new [`CircularSector`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::from_degrees(radius, angle)) + pub const fn from_degrees(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_degrees(radius, angle), + } } /// Create a new [`CircularSector`] from a `radius` and a number of `turns` of a circle. /// /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_turns(radius: f32, fraction: f32) -> Self { - Self::from(Arc2d::from_turns(radius, fraction)) + pub const fn from_turns(radius: f32, fraction: f32) -> Self { + Self { + arc: Arc2d::from_turns(radius, fraction), + } } /// Get half the angle of the sector #[inline(always)] - pub fn half_angle(&self) -> f32 { + pub const fn half_angle(&self) -> f32 { self.arc.half_angle } /// Get the angle of the sector #[inline(always)] - pub fn angle(&self) -> f32 { + pub const fn angle(&self) -> f32 { self.arc.angle() } /// Get the radius of the sector #[inline(always)] - pub fn radius(&self) -> f32 { + pub const fn radius(&self) -> f32 { self.arc.radius } /// Get the length of the arc defining the sector #[inline(always)] - pub fn arc_length(&self) -> f32 { + pub const fn arc_length(&self) -> f32 { self.arc.length() } @@ -461,51 +470,59 @@ impl Measured2d for CircularSegment { impl CircularSegment { /// Create a new [`CircularSegment`] from a `radius`, and an `angle` #[inline(always)] - pub fn new(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::new(radius, angle)) + pub const fn new(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::new(radius, angle), + } } /// Create a new [`CircularSegment`] from a `radius` and an `angle` in radians. #[inline(always)] - pub fn from_radians(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::from_radians(radius, angle)) + pub const fn from_radians(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_radians(radius, angle), + } } /// Create a new [`CircularSegment`] from a `radius` and an `angle` in degrees. #[inline(always)] - pub fn from_degrees(radius: f32, angle: f32) -> Self { - Self::from(Arc2d::from_degrees(radius, angle)) + pub const fn from_degrees(radius: f32, angle: f32) -> Self { + Self { + arc: Arc2d::from_degrees(radius, angle), + } } /// Create a new [`CircularSegment`] from a `radius` and a number of `turns` of a circle. /// /// For instance, `0.5` turns is a semicircle. #[inline(always)] - pub fn from_turns(radius: f32, fraction: f32) -> Self { - Self::from(Arc2d::from_turns(radius, fraction)) + pub const fn from_turns(radius: f32, fraction: f32) -> Self { + Self { + arc: Arc2d::from_turns(radius, fraction), + } } /// Get the half-angle of the segment #[inline(always)] - pub fn half_angle(&self) -> f32 { + pub const fn half_angle(&self) -> f32 { self.arc.half_angle } /// Get the angle of the segment #[inline(always)] - pub fn angle(&self) -> f32 { + pub const fn angle(&self) -> f32 { self.arc.angle() } /// Get the radius of the segment #[inline(always)] - pub fn radius(&self) -> f32 { + pub const fn radius(&self) -> f32 { self.arc.radius } /// Get the length of the arc defining the segment #[inline(always)] - pub fn arc_length(&self) -> f32 { + pub const fn arc_length(&self) -> f32 { self.arc.length() } @@ -820,9 +837,9 @@ impl Ellipse { /// /// `size.x` is the diameter along the X axis, and `size.y` is the diameter along the Y axis. #[inline(always)] - pub fn from_size(size: Vec2) -> Self { + pub const fn from_size(size: Vec2) -> Self { Self { - half_size: size / 2.0, + half_size: Vec2::new(size.x / 2.0, size.y / 2.0), } } @@ -970,13 +987,13 @@ impl Annulus { /// Get the diameter of the annulus #[inline(always)] - pub fn diameter(&self) -> f32 { + pub const fn diameter(&self) -> f32 { self.outer_circle.diameter() } /// Get the thickness of the annulus #[inline(always)] - pub fn thickness(&self) -> f32 { + pub const fn thickness(&self) -> f32 { self.outer_circle.radius - self.inner_circle.radius } @@ -1058,7 +1075,7 @@ impl Default for Rhombus { impl Rhombus { /// Create a new `Rhombus` from a vertical and horizontal diagonal sizes. #[inline(always)] - pub fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { + pub const fn new(horizontal_diagonal: f32, vertical_diagonal: f32) -> Self { Self { half_diagonals: Vec2::new(horizontal_diagonal / 2.0, vertical_diagonal / 2.0), } @@ -1066,7 +1083,7 @@ impl Rhombus { /// Create a new `Rhombus` from a side length with all inner angles equal. #[inline(always)] - pub fn from_side(side: f32) -> Self { + pub const fn from_side(side: f32) -> Self { Self { half_diagonals: Vec2::splat(side * FRAC_1_SQRT_2), } @@ -1074,7 +1091,7 @@ impl Rhombus { /// Create a new `Rhombus` from a given inradius with all inner angles equal. #[inline(always)] - pub fn from_inradius(inradius: f32) -> Self { + pub const fn from_inradius(inradius: f32) -> Self { let half_diagonal = inradius * 2.0 / core::f32::consts::SQRT_2; Self { half_diagonals: Vec2::new(half_diagonal, half_diagonal), @@ -1090,7 +1107,7 @@ impl Rhombus { /// Get the radius of the circumcircle on which all vertices /// of the rhombus lie #[inline(always)] - pub fn circumradius(&self) -> f32 { + pub const fn circumradius(&self) -> f32 { self.half_diagonals.x.max(self.half_diagonals.y) } @@ -1246,10 +1263,9 @@ pub struct Segment2d { impl Primitive2d for Segment2d {} impl Default for Segment2d { - /// Returns the default [`Segment2d`] with endpoints at `(0.0, 0.0)` and `(1.0, 0.0)`. fn default() -> Self { Self { - vertices: [Vec2::new(0.0, 0.0), Vec2::new(1.0, 0.0)], + vertices: [Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)], } } } @@ -1299,13 +1315,13 @@ impl Segment2d { /// Get the position of the first endpoint of the line segment. #[inline(always)] - pub fn point1(&self) -> Vec2 { + pub const fn point1(&self) -> Vec2 { self.vertices[0] } /// Get the position of the second endpoint of the line segment. #[inline(always)] - pub fn point2(&self) -> Vec2 { + pub const fn point2(&self) -> Vec2 { self.vertices[1] } @@ -1538,8 +1554,7 @@ impl From<(Vec2, Vec2)> for Segment2d { } /// A series of connected line segments in 2D space. -/// -/// For a version without generics: [`BoxedPolyline2d`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -1551,63 +1566,53 @@ impl From<(Vec2, Vec2)> for Segment2d { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub struct Polyline2d { +pub struct Polyline2d { /// The vertices of the polyline - #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] - pub vertices: [Vec2; N], + pub vertices: Vec, } -impl Primitive2d for Polyline2d {} +#[cfg(feature = "alloc")] +impl Primitive2d for Polyline2d {} -impl FromIterator for Polyline2d { +#[cfg(feature = "alloc")] +impl FromIterator for Polyline2d { fn from_iter>(iter: I) -> Self { - let mut vertices: [Vec2; N] = [Vec2::ZERO; N]; - - for (index, i) in iter.into_iter().take(N).enumerate() { - vertices[index] = i; + Self { + vertices: iter.into_iter().collect(), } - Self { vertices } - } -} - -impl Polyline2d { - /// Create a new `Polyline2d` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) } } -/// A series of connected line segments in 2D space, allocated on the heap -/// in a `Box<[Vec2]>`. -/// -/// For a version without alloc: [`Polyline2d`] -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct BoxedPolyline2d { - /// The vertices of the polyline - pub vertices: Box<[Vec2]>, -} - -#[cfg(feature = "alloc")] -impl Primitive2d for BoxedPolyline2d {} - #[cfg(feature = "alloc")] -impl FromIterator for BoxedPolyline2d { - fn from_iter>(iter: I) -> Self { - let vertices: Vec = iter.into_iter().collect(); +impl Default for Polyline2d { + fn default() -> Self { Self { - vertices: vertices.into_boxed_slice(), + vertices: Vec::from([Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)]), } } } #[cfg(feature = "alloc")] -impl BoxedPolyline2d { - /// Create a new `BoxedPolyline2d` from its vertices +impl Polyline2d { + /// Create a new `Polyline2d` from its vertices pub fn new(vertices: impl IntoIterator) -> Self { Self::from_iter(vertices) } + + /// Create a new `Polyline2d` from two endpoints with subdivision points. + /// `subdivisions = 0` creates a simple line with just start and end points. + /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. + pub fn with_subdivisions(start: Vec2, end: Vec2, subdivisions: usize) -> Self { + let total_vertices = subdivisions + 2; + let mut vertices = Vec::with_capacity(total_vertices); + + let step = (end - start) / (subdivisions + 1) as f32; + for i in 0..total_vertices { + vertices.push(start + step * i as f32); + } + + Self { vertices } + } } /// A triangle in 2D space @@ -1814,15 +1819,15 @@ impl Default for Rectangle { impl Rectangle { /// Create a new `Rectangle` from a full width and height #[inline(always)] - pub fn new(width: f32, height: f32) -> Self { + pub const fn new(width: f32, height: f32) -> Self { Self::from_size(Vec2::new(width, height)) } /// Create a new `Rectangle` from a given full size #[inline(always)] - pub fn from_size(size: Vec2) -> Self { + pub const fn from_size(size: Vec2) -> Self { Self { - half_size: size / 2.0, + half_size: Vec2::new(size.x / 2.0, size.y / 2.0), } } @@ -1837,7 +1842,7 @@ impl Rectangle { /// Create a `Rectangle` from a single length. /// The resulting `Rectangle` will be the same size in every direction. #[inline(always)] - pub fn from_length(length: f32) -> Self { + pub const fn from_length(length: f32) -> Self { Self { half_size: Vec2::splat(length / 2.0), } @@ -1875,8 +1880,7 @@ impl Measured2d for Rectangle { } /// A polygon with N vertices. -/// -/// For a version without generics: [`BoxedPolygon`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -1888,26 +1892,25 @@ impl Measured2d for Rectangle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub struct Polygon { +pub struct Polygon { /// The vertices of the `Polygon` - #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] - pub vertices: [Vec2; N], + pub vertices: Vec, } -impl Primitive2d for Polygon {} +#[cfg(feature = "alloc")] +impl Primitive2d for Polygon {} -impl FromIterator for Polygon { +#[cfg(feature = "alloc")] +impl FromIterator for Polygon { fn from_iter>(iter: I) -> Self { - let mut vertices: [Vec2; N] = [Vec2::ZERO; N]; - - for (index, i) in iter.into_iter().take(N).enumerate() { - vertices[index] = i; + Self { + vertices: iter.into_iter().collect(), } - Self { vertices } } } -impl Polygon { +#[cfg(feature = "alloc")] +impl Polygon { /// Create a new `Polygon` from its vertices pub fn new(vertices: impl IntoIterator) -> Self { Self::from_iter(vertices) @@ -1923,8 +1926,9 @@ impl Polygon { } } -impl From> for Polygon { - fn from(val: ConvexPolygon) -> Self { +#[cfg(feature = "alloc")] +impl From for Polygon { + fn from(val: ConvexPolygon) -> Self { Polygon { vertices: val.vertices, } @@ -1932,6 +1936,7 @@ impl From> for Polygon { } /// A convex polygon with `N` vertices. +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -1943,15 +1948,16 @@ impl From> for Polygon { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub struct ConvexPolygon { +pub struct ConvexPolygon { /// The vertices of the [`ConvexPolygon`]. - #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] - vertices: [Vec2; N], + vertices: Vec, } -impl Primitive2d for ConvexPolygon {} +#[cfg(feature = "alloc")] +impl Primitive2d for ConvexPolygon {} /// An error that happens when creating a [`ConvexPolygon`]. +#[cfg(feature = "alloc")] #[derive(Error, Debug, Clone)] pub enum ConvexPolygonError { /// The created polygon is not convex. @@ -1959,7 +1965,8 @@ pub enum ConvexPolygonError { Concave, } -impl ConvexPolygon { +#[cfg(feature = "alloc")] +impl ConvexPolygon { fn triangle_winding_order( &self, a_index: usize, @@ -1977,11 +1984,12 @@ impl ConvexPolygon { /// # Errors /// /// Returns [`ConvexPolygonError::Concave`] if the `vertices` do not form a convex polygon. - pub fn new(vertices: [Vec2; N]) -> Result { + pub fn new(vertices: impl IntoIterator) -> Result { let polygon = Self::new_unchecked(vertices); - let ref_winding_order = polygon.triangle_winding_order(N - 1, 0, 1); - for i in 1..N { - let winding_order = polygon.triangle_winding_order(i - 1, i, (i + 1) % N); + let len = polygon.vertices.len(); + let ref_winding_order = polygon.triangle_winding_order(len - 1, 0, 1); + for i in 1..len { + let winding_order = polygon.triangle_winding_order(i - 1, i, (i + 1) % len); if winding_order != ref_winding_order { return Err(ConvexPolygonError::Concave); } @@ -1992,66 +2000,28 @@ impl ConvexPolygon { /// Create a [`ConvexPolygon`] from its `vertices`, without checks. /// Use this version only if you know that the `vertices` make up a convex polygon. #[inline(always)] - pub fn new_unchecked(vertices: [Vec2; N]) -> Self { - Self { vertices } + pub fn new_unchecked(vertices: impl IntoIterator) -> Self { + Self { + vertices: vertices.into_iter().collect(), + } } /// Get the vertices of this polygon #[inline(always)] - pub fn vertices(&self) -> &[Vec2; N] { + pub fn vertices(&self) -> &[Vec2] { &self.vertices } } -impl TryFrom> for ConvexPolygon { +#[cfg(feature = "alloc")] +impl TryFrom for ConvexPolygon { type Error = ConvexPolygonError; - fn try_from(val: Polygon) -> Result { + fn try_from(val: Polygon) -> Result { ConvexPolygon::new(val.vertices) } } -/// A polygon with a variable number of vertices, allocated on the heap -/// in a `Box<[Vec2]>`. -/// -/// For a version without alloc: [`Polygon`] -#[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct BoxedPolygon { - /// The vertices of the `BoxedPolygon` - pub vertices: Box<[Vec2]>, -} - -#[cfg(feature = "alloc")] -impl Primitive2d for BoxedPolygon {} - -#[cfg(feature = "alloc")] -impl FromIterator for BoxedPolygon { - fn from_iter>(iter: I) -> Self { - let vertices: Vec = iter.into_iter().collect(); - Self { - vertices: vertices.into_boxed_slice(), - } - } -} - -#[cfg(feature = "alloc")] -impl BoxedPolygon { - /// Create a new `BoxedPolygon` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) - } - - /// Tests if the polygon is simple. - /// - /// A polygon is simple if it is not self intersecting and not self tangent. - /// As such, no two edges of the polygon may cross each other and each vertex must not lie on another edge. - pub fn is_simple(&self) -> bool { - is_polygon_simple(&self.vertices) - } -} - /// A polygon centered on the origin where all vertices lie on a circle, equally far apart. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -2091,7 +2061,7 @@ impl RegularPolygon { /// /// Panics if `circumradius` is negative #[inline(always)] - pub fn new(circumradius: f32, sides: u32) -> Self { + pub const fn new(circumradius: f32, sides: u32) -> Self { assert!( circumradius.is_sign_positive(), "polygon has a negative radius" @@ -2109,7 +2079,7 @@ impl RegularPolygon { /// Get the radius of the circumcircle on which all vertices /// of the regular polygon lie #[inline(always)] - pub fn circumradius(&self) -> f32 { + pub const fn circumradius(&self) -> f32 { self.circumcircle.radius } @@ -2133,7 +2103,7 @@ impl RegularPolygon { /// This is the angle formed by two adjacent sides with points /// within the angle being in the interior of the polygon #[inline(always)] - pub fn internal_angle_degrees(&self) -> f32 { + pub const fn internal_angle_degrees(&self) -> f32 { (self.sides - 2) as f32 / self.sides as f32 * 180.0 } @@ -2142,7 +2112,7 @@ impl RegularPolygon { /// This is the angle formed by two adjacent sides with points /// within the angle being in the interior of the polygon #[inline(always)] - pub fn internal_angle_radians(&self) -> f32 { + pub const fn internal_angle_radians(&self) -> f32 { (self.sides - 2) as f32 * PI / self.sides as f32 } @@ -2151,7 +2121,7 @@ impl RegularPolygon { /// This is the angle formed by two adjacent sides with points /// within the angle being in the exterior of the polygon #[inline(always)] - pub fn external_angle_degrees(&self) -> f32 { + pub const fn external_angle_degrees(&self) -> f32 { 360.0 / self.sides as f32 } @@ -2160,7 +2130,7 @@ impl RegularPolygon { /// This is the angle formed by two adjacent sides with points /// within the angle being in the exterior of the polygon #[inline(always)] - pub fn external_angle_radians(&self) -> f32 { + pub const fn external_angle_radians(&self) -> f32 { 2.0 * PI / self.sides as f32 } @@ -2234,7 +2204,7 @@ impl Default for Capsule2d { impl Capsule2d { /// Create a new `Capsule2d` from a radius and length - pub fn new(radius: f32, length: f32) -> Self { + pub const fn new(radius: f32, length: f32) -> Self { Self { radius, half_length: length / 2.0, @@ -2243,7 +2213,7 @@ impl Capsule2d { /// Get the part connecting the semicircular ends of the capsule as a [`Rectangle`] #[inline] - pub fn to_inner_rectangle(&self) -> Rectangle { + pub const fn to_inner_rectangle(&self) -> Rectangle { Rectangle::new(self.radius * 2.0, self.half_length * 2.0) } } diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 76021f87f3e7b..e40e0e2f1fe5a 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -13,7 +13,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use glam::Quat; #[cfg(feature = "alloc")] -use alloc::{boxed::Box, vec::Vec}; +use alloc::vec::Vec; /// A sphere primitive, representing the set of all points some distance from the origin #[derive(Clone, Copy, Debug, PartialEq)] @@ -378,10 +378,9 @@ pub struct Segment3d { impl Primitive3d for Segment3d {} impl Default for Segment3d { - /// Returns the default [`Segment3d`] with endpoints at `(0.0, 0.0, 0.0)` and `(1.0, 0.0, 0.0)`. fn default() -> Self { Self { - vertices: [Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0)], + vertices: [Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)], } } } @@ -606,8 +605,7 @@ impl From<(Vec3, Vec3)> for Segment3d { } /// A series of connected line segments in 3D space. -/// -/// For a version without generics: [`BoxedPolyline3d`] +#[cfg(feature = "alloc")] #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -619,62 +617,50 @@ impl From<(Vec3, Vec3)> for Segment3d { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -pub struct Polyline3d { +pub struct Polyline3d { /// The vertices of the polyline - #[cfg_attr(feature = "serialize", serde(with = "super::serde::array"))] - pub vertices: [Vec3; N], + pub vertices: Vec, } -impl Primitive3d for Polyline3d {} +#[cfg(feature = "alloc")] +impl Primitive3d for Polyline3d {} -impl FromIterator for Polyline3d { +#[cfg(feature = "alloc")] +impl FromIterator for Polyline3d { fn from_iter>(iter: I) -> Self { - let mut vertices: [Vec3; N] = [Vec3::ZERO; N]; - - for (index, i) in iter.into_iter().take(N).enumerate() { - vertices[index] = i; + Self { + vertices: iter.into_iter().collect(), } - Self { vertices } } } -impl Polyline3d { - /// Create a new `Polyline3d` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) +#[cfg(feature = "alloc")] +impl Default for Polyline3d { + fn default() -> Self { + Self::new([Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0)]) } } -/// A series of connected line segments in 3D space, allocated on the heap -/// in a `Box<[Vec3]>`. -/// -/// For a version without alloc: [`Polyline3d`] #[cfg(feature = "alloc")] -#[derive(Clone, Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct BoxedPolyline3d { - /// The vertices of the polyline - pub vertices: Box<[Vec3]>, -} +impl Polyline3d { + /// Create a new `Polyline3d` from its vertices + pub fn new(vertices: impl IntoIterator) -> Self { + Self::from_iter(vertices) + } -#[cfg(feature = "alloc")] -impl Primitive3d for BoxedPolyline3d {} + /// Create a new `Polyline3d` from two endpoints with subdivision points. + /// `subdivisions = 0` creates a simple line with just start and end points. + /// `subdivisions = 1` adds one point in the middle, creating 2 segments, etc. + pub fn with_subdivisions(start: Vec3, end: Vec3, subdivisions: usize) -> Self { + let total_vertices = subdivisions + 2; + let mut vertices = Vec::with_capacity(total_vertices); -#[cfg(feature = "alloc")] -impl FromIterator for BoxedPolyline3d { - fn from_iter>(iter: I) -> Self { - let vertices: Vec = iter.into_iter().collect(); - Self { - vertices: vertices.into_boxed_slice(), + let step = (end - start) / (subdivisions + 1) as f32; + for i in 0..total_vertices { + vertices.push(start + step * i as f32); } - } -} -#[cfg(feature = "alloc")] -impl BoxedPolyline3d { - /// Create a new `BoxedPolyline3d` from its vertices - pub fn new(vertices: impl IntoIterator) -> Self { - Self::from_iter(vertices) + Self { vertices } } } @@ -1311,14 +1297,16 @@ impl Triangle3d { let ca = a - c; let mut largest_side_points = (a, b); - let mut largest_side_length = ab.length(); + let mut largest_side_length = ab.length_squared(); - if bc.length() > largest_side_length { + let bc_length = bc.length_squared(); + if bc_length > largest_side_length { largest_side_points = (b, c); - largest_side_length = bc.length(); + largest_side_length = bc_length; } - if ca.length() > largest_side_length { + let ca_length = ca.length_squared(); + if ca_length > largest_side_length { largest_side_points = (a, c); } diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs index b5c6644b13cc0..625a359dd711d 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_math/src/primitives/mod.rs @@ -7,8 +7,6 @@ pub use dim2::*; mod dim3; pub use dim3::*; mod polygon; -#[cfg(feature = "serialize")] -mod serde; /// A marker trait for 2D primitives pub trait Primitive2d {} diff --git a/crates/bevy_math/src/primitives/serde.rs b/crates/bevy_math/src/primitives/serde.rs deleted file mode 100644 index a1b678132ee42..0000000000000 --- a/crates/bevy_math/src/primitives/serde.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! This module defines serialization/deserialization for const generic arrays. -//! Unlike serde's default behavior, it supports arbitrarily large arrays. -//! The code is based on this github comment: -//! - -pub(crate) mod array { - use core::marker::PhantomData; - use serde::{ - de::{SeqAccess, Visitor}, - ser::SerializeTuple, - Deserialize, Deserializer, Serialize, Serializer, - }; - - pub fn serialize( - data: &[T; N], - ser: S, - ) -> Result { - let mut s = ser.serialize_tuple(N)?; - for item in data { - s.serialize_element(item)?; - } - s.end() - } - - struct GenericArrayVisitor(PhantomData); - - impl<'de, T, const N: usize> Visitor<'de> for GenericArrayVisitor - where - T: Deserialize<'de>, - { - type Value = [T; N]; - - fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { - formatter.write_fmt(format_args!("an array of length {N}")) - } - - #[inline] - fn visit_seq(self, mut seq: A) -> Result - where - A: SeqAccess<'de>, - { - let mut data = [const { Option::::None }; N]; - - for element in data.iter_mut() { - match (seq.next_element())? { - Some(val) => *element = Some(val), - None => return Err(serde::de::Error::invalid_length(N, &self)), - } - } - - let data = data.map(|value| match value { - Some(value) => value, - None => unreachable!(), - }); - - Ok(data) - } - } - - pub fn deserialize<'de, D, T, const N: usize>(deserializer: D) -> Result<[T; N], D::Error> - where - D: Deserializer<'de>, - T: Deserialize<'de>, - { - deserializer.deserialize_tuple(N, GenericArrayVisitor::(PhantomData)) - } -} diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs index 1320f6363a784..bcaf7f63b0dec 100644 --- a/crates/bevy_math/src/rotation2d.rs +++ b/crates/bevy_math/src/rotation2d.rs @@ -12,7 +12,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// A counterclockwise 2D rotation. +/// A 2D rotation. /// /// # Example /// @@ -21,7 +21,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// # use bevy_math::{Rot2, Vec2}; /// use std::f32::consts::PI; /// -/// // Create rotations from radians or degrees +/// // Create rotations from counterclockwise angles in radians or degrees /// let rotation1 = Rot2::radians(PI / 2.0); /// let rotation2 = Rot2::degrees(45.0); /// @@ -50,11 +50,11 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; )] #[doc(alias = "rotation", alias = "rotation2d", alias = "rotation_2d")] pub struct Rot2 { - /// The cosine of the rotation angle in radians. + /// The cosine of the rotation angle. /// /// This is the real part of the unit complex number representing the rotation. pub cos: f32, - /// The sine of the rotation angle in radians. + /// The sine of the rotation angle. /// /// This is the imaginary part of the unit complex number representing the rotation. pub sin: f32, @@ -68,46 +68,60 @@ impl Default for Rot2 { impl Rot2 { /// No rotation. + /// Also equals a full turn that returns back to its original position. + /// ``` + /// # use approx::assert_relative_eq; + /// # use bevy_math::Rot2; + /// #[cfg(feature = "approx")] + /// assert_relative_eq!(Rot2::IDENTITY, Rot2::degrees(360.0), epsilon = 2e-7); + /// ``` pub const IDENTITY: Self = Self { cos: 1.0, sin: 0.0 }; /// A rotation of π radians. + /// Corresponds to a half-turn. pub const PI: Self = Self { cos: -1.0, sin: 0.0, }; /// A counterclockwise rotation of π/2 radians. + /// Corresponds to a counterclockwise quarter-turn. pub const FRAC_PI_2: Self = Self { cos: 0.0, sin: 1.0 }; /// A counterclockwise rotation of π/3 radians. + /// Corresponds to a counterclockwise turn by 60°. pub const FRAC_PI_3: Self = Self { cos: 0.5, sin: 0.866_025_4, }; /// A counterclockwise rotation of π/4 radians. + /// Corresponds to a counterclockwise turn by 45°. pub const FRAC_PI_4: Self = Self { cos: core::f32::consts::FRAC_1_SQRT_2, sin: core::f32::consts::FRAC_1_SQRT_2, }; /// A counterclockwise rotation of π/6 radians. + /// Corresponds to a counterclockwise turn by 30°. pub const FRAC_PI_6: Self = Self { cos: 0.866_025_4, sin: 0.5, }; /// A counterclockwise rotation of π/8 radians. + /// Corresponds to a counterclockwise turn by 22.5°. pub const FRAC_PI_8: Self = Self { cos: 0.923_879_5, sin: 0.382_683_43, }; /// Creates a [`Rot2`] from a counterclockwise angle in radians. + /// A negative argument corresponds to a clockwise rotation. /// /// # Note /// - /// The input rotation will always be clamped to the range `(-π, π]` by design. + /// Angles larger than or equal to 2π (in either direction) loop around to smaller rotations, since a full rotation returns an object to its starting orientation. /// /// # Example /// @@ -124,6 +138,10 @@ impl Rot2 { /// let rot3 = Rot2::radians(PI); /// #[cfg(feature = "approx")] /// assert_relative_eq!(rot1 * rot1, rot3); + /// + /// // A rotation by 3π and 1π are the same + /// #[cfg(feature = "approx")] + /// assert_relative_eq!(Rot2::radians(3.0 * PI), Rot2::radians(PI)); /// ``` #[inline] pub fn radians(radians: f32) -> Self { @@ -132,16 +150,17 @@ impl Rot2 { } /// Creates a [`Rot2`] from a counterclockwise angle in degrees. + /// A negative argument corresponds to a clockwise rotation. /// /// # Note /// - /// The input rotation will always be clamped to the range `(-180°, 180°]` by design. + /// Angles larger than or equal to 360° (in either direction) loop around to smaller rotations, since a full rotation returns an object to its starting orientation. /// /// # Example /// /// ``` /// # use bevy_math::Rot2; - /// # use approx::assert_relative_eq; + /// # use approx::{assert_relative_eq, assert_abs_diff_eq}; /// /// let rot1 = Rot2::degrees(270.0); /// let rot2 = Rot2::degrees(-90.0); @@ -151,6 +170,10 @@ impl Rot2 { /// let rot3 = Rot2::degrees(180.0); /// #[cfg(feature = "approx")] /// assert_relative_eq!(rot1 * rot1, rot3); + /// + /// // A rotation by 365° and 5° are the same + /// #[cfg(feature = "approx")] + /// assert_abs_diff_eq!(Rot2::degrees(365.0), Rot2::degrees(5.0), epsilon = 2e-7); /// ``` #[inline] pub fn degrees(degrees: f32) -> Self { @@ -158,10 +181,11 @@ impl Rot2 { } /// Creates a [`Rot2`] from a counterclockwise fraction of a full turn of 360 degrees. + /// A negative argument corresponds to a clockwise rotation. /// /// # Note /// - /// The input rotation will always be clamped to the range `(-50%, 50%]` by design. + /// Angles larger than or equal to 1 turn (in either direction) loop around to smaller rotations, since a full rotation returns an object to its starting orientation. /// /// # Example /// @@ -177,13 +201,17 @@ impl Rot2 { /// let rot3 = Rot2::turn_fraction(0.5); /// #[cfg(feature = "approx")] /// assert_relative_eq!(rot1 * rot1, rot3); + /// + /// // A rotation by 1.5 turns and 0.5 turns are the same + /// #[cfg(feature = "approx")] + /// assert_relative_eq!(Rot2::turn_fraction(1.5), Rot2::turn_fraction(0.5)); /// ``` #[inline] pub fn turn_fraction(fraction: f32) -> Self { Self::radians(TAU * fraction) } - /// Creates a [`Rot2`] from the sine and cosine of an angle in radians. + /// Creates a [`Rot2`] from the sine and cosine of an angle. /// /// The rotation is only valid if `sin * sin + cos * cos == 1.0`. /// @@ -200,25 +228,25 @@ impl Rot2 { rotation } - /// Returns the rotation in radians in the `(-pi, pi]` range. + /// Returns a corresponding rotation angle in radians in the `(-pi, pi]` range. #[inline] pub fn as_radians(self) -> f32 { ops::atan2(self.sin, self.cos) } - /// Returns the rotation in degrees in the `(-180, 180]` range. + /// Returns a corresponding rotation angle in degrees in the `(-180, 180]` range. #[inline] pub fn as_degrees(self) -> f32 { self.as_radians().to_degrees() } - /// Returns the rotation as a fraction of a full 360 degree turn. + /// Returns a corresponding rotation angle as a fraction of a full 360 degree turn in the `(-0.5, 0.5]` range. #[inline] pub fn as_turn_fraction(self) -> f32 { self.as_radians() / TAU } - /// Returns the sine and cosine of the rotation angle in radians. + /// Returns the sine and cosine of the rotation angle. #[inline] pub const fn sin_cos(self) -> (f32, f32) { (self.sin, self.cos) @@ -446,7 +474,7 @@ impl From for Rot2 { impl From for Mat2 { /// Creates a [`Mat2`] rotation matrix from a [`Rot2`]. fn from(rot: Rot2) -> Self { - Mat2::from_cols_array(&[rot.cos, -rot.sin, rot.sin, rot.cos]) + Mat2::from_cols_array(&[rot.cos, rot.sin, -rot.sin, rot.cos]) } } @@ -518,7 +546,7 @@ mod tests { use approx::assert_relative_eq; - use crate::{ops, Dir2, Rot2, Vec2}; + use crate::{ops, Dir2, Mat2, Rot2, Vec2}; #[test] fn creation() { @@ -721,4 +749,20 @@ mod tests { assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 90.0); assert_eq!(ops::abs(rot1.slerp(rot2, 1.0).as_degrees()), 180.0); } + + #[test] + fn rotation_matrix() { + let rotation = Rot2::degrees(90.0); + let matrix: Mat2 = rotation.into(); + + // Check that the matrix is correct. + assert_relative_eq!(matrix.x_axis, Vec2::Y); + assert_relative_eq!(matrix.y_axis, Vec2::NEG_X); + + // Check that the matrix rotates vectors correctly. + assert_relative_eq!(matrix * Vec2::X, Vec2::Y); + assert_relative_eq!(matrix * Vec2::Y, Vec2::NEG_X); + assert_relative_eq!(matrix * Vec2::NEG_X, Vec2::NEG_Y); + assert_relative_eq!(matrix * Vec2::NEG_Y, Vec2::X); + } } diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index 94dc06427bdd6..31b92b9beb28d 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } diff --git a/crates/bevy_mesh/src/lib.rs b/crates/bevy_mesh/src/lib.rs index c97fb21422a6e..6cb0fbe402b88 100644 --- a/crates/bevy_mesh/src/lib.rs +++ b/crates/bevy_mesh/src/lib.rs @@ -12,7 +12,9 @@ pub mod morph; pub mod primitives; pub mod skinning; mod vertex; -use bevy_ecs::schedule::SystemSet; +use bevy_app::{App, Plugin, PostUpdate}; +use bevy_asset::{AssetApp, AssetEventSystems}; +use bevy_ecs::schedule::{IntoScheduleConfigs, SystemSet}; use bitflags::bitflags; pub use components::*; pub use index::*; @@ -43,6 +45,20 @@ bitflags! { } } +#[derive(Default)] +pub struct MeshPlugin; + +impl Plugin for MeshPlugin { + fn build(&self, app: &mut App) { + app.init_asset::() + .register_asset_reflect::() + .add_systems( + PostUpdate, + mark_3d_meshes_as_changed_if_their_assets_changed.before(AssetEventSystems), + ); + } +} + impl BaseMeshPipelineKey { pub const PRIMITIVE_TOPOLOGY_MASK_BITS: u64 = 0b111; pub const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u64 = @@ -71,4 +87,4 @@ impl BaseMeshPipelineKey { /// `bevy_render::mesh::inherit_weights` runs in this `SystemSet` #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub struct InheritWeights; +pub struct InheritWeightSystems; diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index ea82c8a7445ae..da7f2db305c20 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -33,8 +33,8 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// or by converting a [primitive](bevy_math::primitives) using [`into`](Into). /// It is also possible to create one manually. They can be edited after creation. /// -/// Meshes can be rendered with a `Mesh2d` and `MeshMaterial2d` -/// or `Mesh3d` and `MeshMaterial3d` for 2D and 3D respectively. +/// Meshes can be rendered with a [`Mesh2d`](crate::Mesh2d) and `MeshMaterial2d` +/// or [`Mesh3d`](crate::Mesh3d) and `MeshMaterial3d` for 2D and 3D respectively. /// /// A [`Mesh`] in Bevy is equivalent to a "primitive" in the glTF format, for a /// glTF Mesh representation, see `GltfMesh`. @@ -78,7 +78,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// ``` /// /// You can see how it looks like [here](https://github.com/bevyengine/bevy/blob/main/assets/docs/Mesh.png), -/// used in a `Mesh3d` with a square bevy logo texture, with added axis, points, +/// used in a [`Mesh3d`](crate::Mesh3d) with a square bevy logo texture, with added axis, points, /// lines and text for clarity. /// /// ## Other examples @@ -606,7 +606,7 @@ impl Mesh { match topology { PrimitiveTopology::TriangleList => { // Early return if the index count doesn't match - if indices.len() % 3 != 0 { + if !indices.len().is_multiple_of(3) { return Err(MeshWindingInvertError::AbruptIndicesEnd); } for chunk in indices.chunks_mut(3) { @@ -620,7 +620,7 @@ impl Mesh { } PrimitiveTopology::LineList => { // Early return if the index count doesn't match - if indices.len() % 2 != 0 { + if !indices.len().is_multiple_of(2) { return Err(MeshWindingInvertError::AbruptIndicesEnd); } indices.reverse(); diff --git a/crates/bevy_mesh/src/morph.rs b/crates/bevy_mesh/src/morph.rs index fdeeeacc31198..4b6d2f574323d 100644 --- a/crates/bevy_mesh/src/morph.rs +++ b/crates/bevy_mesh/src/morph.rs @@ -97,7 +97,7 @@ impl MorphTargetImage { } } -/// Controls the [morph targets] for all child `Mesh3d` entities. In most cases, [`MorphWeights`] should be considered +/// Controls the [morph targets] for all child [`Mesh3d`](crate::Mesh3d) entities. In most cases, [`MorphWeights`] should be considered /// the "source of truth" when writing morph targets for meshes. However you can choose to write child [`MeshMorphWeights`] /// if your situation requires more granularity. Just note that if you set [`MorphWeights`], it will overwrite child /// [`MeshMorphWeights`] values. @@ -105,9 +105,9 @@ impl MorphTargetImage { /// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets /// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material). /// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then -/// synchronized to child `Mesh3d` / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). +/// synchronized to child [`Mesh3d`](crate::Mesh3d) / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). /// -/// Add this to the parent of one or more [`Entities`](`Entity`) with a `Mesh3d` with a [`MeshMorphWeights`]. +/// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Mesh3d`](crate::Mesh3d) with a [`MeshMorphWeights`]. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] @@ -132,7 +132,7 @@ impl MorphWeights { first_mesh, }) } - /// The first child `Mesh3d` primitive controlled by these weights. + /// The first child [`Mesh3d`](crate::Mesh3d) primitive controlled by these weights. /// This can be used to look up metadata information such as [`Mesh::morph_target_names`]. pub fn first_mesh(&self) -> Option<&Handle> { self.first_mesh.as_ref() @@ -152,7 +152,7 @@ impl MorphWeights { /// /// See [`MorphWeights`] for more details on Bevy's morph target implementation. /// -/// Add this to an [`Entity`] with a `Mesh3d` with a [`MorphAttributes`] set +/// Add this to an [`Entity`] with a [`Mesh3d`](crate::Mesh3d) with a [`MorphAttributes`] set /// to control individual weights of each morph target. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation diff --git a/crates/bevy_mesh/src/primitives/dim2.rs b/crates/bevy_mesh/src/primitives/dim2.rs index 172cb152074d8..dc2e99efad4f9 100644 --- a/crates/bevy_mesh/src/primitives/dim2.rs +++ b/crates/bevy_mesh/src/primitives/dim2.rs @@ -4,6 +4,7 @@ use crate::{primitives::dim3::triangle3d, Indices, Mesh, PerimeterSegment}; use bevy_asset::RenderAssetUsages; use super::{Extrudable, MeshBuilder, Meshable}; +use bevy_math::prelude::Polyline2d; use bevy_math::{ ops, primitives::{ @@ -407,31 +408,32 @@ impl From for Mesh { /// /// 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()`]. -#[derive(Clone, Copy, Debug, Reflect)] +#[derive(Clone, Debug, Reflect)] #[reflect(Debug, Clone)] -pub struct ConvexPolygonMeshBuilder { - pub vertices: [Vec2; N], +pub struct ConvexPolygonMeshBuilder { + pub vertices: Vec, } -impl Meshable for ConvexPolygon { - type Output = ConvexPolygonMeshBuilder; +impl Meshable for ConvexPolygon { + type Output = ConvexPolygonMeshBuilder; fn mesh(&self) -> Self::Output { Self::Output { - vertices: *self.vertices(), + vertices: self.vertices().to_vec(), } } } -impl MeshBuilder for ConvexPolygonMeshBuilder { +impl MeshBuilder for ConvexPolygonMeshBuilder { fn build(&self) -> Mesh { - let mut indices = Vec::with_capacity((N - 2) * 3); - let mut positions = Vec::with_capacity(N); + let len = self.vertices.len(); + let mut indices = Vec::with_capacity((len - 2) * 3); + let mut positions = Vec::with_capacity(len); - for vertex in self.vertices { + for vertex in &self.vertices { positions.push([vertex.x, vertex.y, 0.0]); } - for i in 2..N as u32 { + for i in 2..len as u32 { indices.extend_from_slice(&[0, i - 1, i]); } Mesh::new( @@ -443,16 +445,16 @@ impl MeshBuilder for ConvexPolygonMeshBuilder { } } -impl Extrudable for ConvexPolygonMeshBuilder { +impl Extrudable for ConvexPolygonMeshBuilder { fn perimeter(&self) -> Vec { vec![PerimeterSegment::Flat { - indices: (0..N as u32).chain([0]).collect(), + indices: (0..self.vertices.len() as u32).chain([0]).collect(), }] } } -impl From> for Mesh { - fn from(polygon: ConvexPolygon) -> Self { +impl From for Mesh { + fn from(polygon: ConvexPolygon) -> Self { polygon.mesh().build() } } @@ -676,6 +678,50 @@ impl From for Mesh { } } +/// A builder used for creating a [`Mesh`] with a [`Polyline2d`] shape. +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Polyline2dMeshBuilder { + polyline: Polyline2d, +} + +impl MeshBuilder for Polyline2dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self + .polyline + .vertices + .iter() + .map(|v| v.extend(0.0)) + .collect(); + + let indices = Indices::U32( + (0..self.polyline.vertices.len() as u32 - 1) + .flat_map(|i| [i, i + 1]) + .collect(), + ); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + } +} + +impl Meshable for Polyline2d { + type Output = Polyline2dMeshBuilder; + + fn mesh(&self) -> Self::Output { + Polyline2dMeshBuilder { + polyline: self.clone(), + } + } +} + +impl From for Mesh { + fn from(polyline: Polyline2d) -> Self { + polyline.mesh().build() + } +} + /// A builder for creating a [`Mesh`] with an [`Annulus`] shape. #[derive(Clone, Copy, Debug, Reflect)] #[reflect(Default, Debug, Clone)] @@ -1111,7 +1157,7 @@ impl MeshBuilder for Capsule2dMeshBuilder { // If the vertex count is even, offset starting angle of top semicircle by half a step // to position the vertices evenly. - let start_angle = if vertex_count % 2 == 0 { + let start_angle = if vertex_count.is_multiple_of(2) { step / 2.0 } else { 0.0 diff --git a/crates/bevy_mesh/src/primitives/dim3/mod.rs b/crates/bevy_mesh/src/primitives/dim3/mod.rs index a27d0a1bfb259..21cd80b3a420f 100644 --- a/crates/bevy_mesh/src/primitives/dim3/mod.rs +++ b/crates/bevy_mesh/src/primitives/dim3/mod.rs @@ -4,6 +4,7 @@ mod conical_frustum; mod cuboid; mod cylinder; mod plane; +mod polyline3d; mod segment3d; mod sphere; mod tetrahedron; diff --git a/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs new file mode 100644 index 0000000000000..4d13112579f09 --- /dev/null +++ b/crates/bevy_mesh/src/primitives/dim3/polyline3d.rs @@ -0,0 +1,43 @@ +use crate::{Indices, Mesh, MeshBuilder, Meshable, PrimitiveTopology}; +use bevy_asset::RenderAssetUsages; +use bevy_math::primitives::Polyline3d; +use bevy_reflect::prelude::*; + +/// A builder used for creating a [`Mesh`] with a [`Polyline3d`] shape. +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Default, Debug, Clone)] +pub struct Polyline3dMeshBuilder { + polyline: Polyline3d, +} + +impl MeshBuilder for Polyline3dMeshBuilder { + fn build(&self) -> Mesh { + let positions: Vec<_> = self.polyline.vertices.clone(); + + let indices = Indices::U32( + (0..self.polyline.vertices.len() as u32 - 1) + .flat_map(|i| [i, i + 1]) + .collect(), + ); + + Mesh::new(PrimitiveTopology::LineList, RenderAssetUsages::default()) + .with_inserted_indices(indices) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + } +} + +impl Meshable for Polyline3d { + type Output = Polyline3dMeshBuilder; + + fn mesh(&self) -> Self::Output { + Polyline3dMeshBuilder { + polyline: self.clone(), + } + } +} + +impl From for Mesh { + fn from(polyline: Polyline3d) -> Self { + polyline.mesh().build() + } +} diff --git a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs index 3c892b424277d..d032285283afb 100644 --- a/crates/bevy_mesh/src/primitives/dim3/segment3d.rs +++ b/crates/bevy_mesh/src/primitives/dim3/segment3d.rs @@ -29,6 +29,12 @@ impl Meshable for Segment3d { } } +impl From for Mesh { + fn from(segment: Segment3d) -> Self { + segment.mesh().build() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 40f2bee2eceb6..a5d06399ee3d3 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -18,8 +18,8 @@ experimental_pbr_pcss = ["bevy_light/experimental_pbr_pcss"] pbr_specular_textures = [] pbr_clustered_decals = [] pbr_light_textures = [] -bluenoise_texture = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] -shader_format_glsl = ["bevy_render/shader_format_glsl"] +bluenoise_texture = ["bevy_image/ktx2", "bevy_image/zstd"] +shader_format_glsl = ["bevy_shader/shader_format_glsl"] trace = ["bevy_render/trace"] # Enables the meshlet renderer for dense high-poly scenes (experimental) meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_tasks"] @@ -47,9 +47,7 @@ bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } -bevy_render = { path = "../bevy_render", features = [ - "bevy_light", -], version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } diff --git a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl index f7ba0ecb60cdc..0201165edaf45 100644 --- a/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/aerial_view_lut.wgsl @@ -6,8 +6,8 @@ functions::{ sample_transmittance_lut, sample_atmosphere, rayleigh, henyey_greenstein, sample_multiscattering_lut, AtmosphereSample, sample_local_inscattering, - get_local_r, get_local_up, view_radius, uv_to_ndc, max_atmosphere_distance, - uv_to_ray_direction, MIDPOINT_RATIO + uv_to_ndc, max_atmosphere_distance, uv_to_ray_direction, + MIDPOINT_RATIO, get_view_position }, } } @@ -22,8 +22,9 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let uv = (vec2(idx.xy) + 0.5) / vec2(settings.aerial_view_lut_size.xy); let ray_dir = uv_to_ray_direction(uv); - let r = view_radius(); - let mu = ray_dir.y; + let world_pos = get_view_position(); + + let r = length(world_pos); let t_max = settings.aerial_view_lut_max_distance; var prev_t = 0.0; @@ -36,15 +37,16 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let dt = (t_i - prev_t); prev_t = t_i; - let local_r = get_local_r(r, mu, t_i); - let local_up = get_local_up(r, t_i, ray_dir.xyz); + let sample_pos = world_pos + ray_dir * t_i; + let local_r = length(sample_pos); + let local_up = normalize(sample_pos); let local_atmosphere = sample_atmosphere(local_r); let sample_optical_depth = local_atmosphere.extinction * dt; let sample_transmittance = exp(-sample_optical_depth); // evaluate one segment of the integral - var inscattering = sample_local_inscattering(local_atmosphere, ray_dir.xyz, local_r, local_up); + var inscattering = sample_local_inscattering(local_atmosphere, ray_dir, sample_pos); // Analytical integration of the single scattering term in the radiance transfer equation let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction; diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs new file mode 100644 index 0000000000000..ce8370f543d93 --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -0,0 +1,332 @@ +use crate::{ + resources::{ + AtmosphereSamplers, AtmosphereTextures, AtmosphereTransform, AtmosphereTransforms, + AtmosphereTransformsOffset, + }, + GpuAtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, +}; +use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages}; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::{QueryState, With, Without}, + resource::Resource, + system::{lifetimeless::Read, Commands, Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_image::Image; +use bevy_light::{AtmosphereEnvironmentMapLight, GeneratedEnvironmentMapLight}; +use bevy_math::{Quat, UVec2}; +use bevy_render::{ + extract_component::{ComponentUniforms, DynamicUniformIndex, ExtractComponent}, + render_asset::RenderAssets, + render_graph::{Node, NodeRunError, RenderGraphContext}, + render_resource::{binding_types::*, *}, + renderer::{RenderContext, RenderDevice}, + texture::{CachedTexture, GpuImage}, + view::{ViewUniform, ViewUniformOffset, ViewUniforms}, +}; +use bevy_utils::default; +use tracing::warn; + +use super::Atmosphere; + +// Render world representation of an environment map light for the atmosphere +#[derive(Component, ExtractComponent, Clone)] +pub struct AtmosphereEnvironmentMap { + pub environment_map: Handle, + pub size: UVec2, +} + +#[derive(Component)] +pub struct AtmosphereProbeTextures { + pub environment: TextureView, + pub transmittance_lut: CachedTexture, + pub multiscattering_lut: CachedTexture, + pub sky_view_lut: CachedTexture, + pub aerial_view_lut: CachedTexture, +} + +#[derive(Component)] +pub(crate) struct AtmosphereProbeBindGroups { + pub environment: BindGroup, +} + +#[derive(Resource)] +pub struct AtmosphereProbeLayouts { + pub environment: BindGroupLayout, +} + +#[derive(Resource)] +pub struct AtmosphereProbePipeline { + pub environment: CachedComputePipelineId, +} + +pub fn init_atmosphere_probe_layout(mut commands: Commands, render_device: Res) { + let environment = render_device.create_bind_group_layout( + "environment_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + uniform_buffer::(true), + texture_2d(TextureSampleType::Float { filterable: true }), //transmittance lut and sampler + sampler(SamplerBindingType::Filtering), + texture_2d(TextureSampleType::Float { filterable: true }), //multiscattering lut and sampler + sampler(SamplerBindingType::Filtering), + texture_2d(TextureSampleType::Float { filterable: true }), //sky view lut and sampler + sampler(SamplerBindingType::Filtering), + texture_3d(TextureSampleType::Float { filterable: true }), //aerial view lut ans sampler + sampler(SamplerBindingType::Filtering), + texture_storage_2d_array( + // output 2D array texture + TextureFormat::Rgba16Float, + StorageTextureAccess::WriteOnly, + ), + ), + ), + ); + + commands.insert_resource(AtmosphereProbeLayouts { environment }); +} + +pub(super) fn prepare_atmosphere_probe_bind_groups( + probes: Query<(Entity, &AtmosphereProbeTextures), With>, + render_device: Res, + layouts: Res, + samplers: Res, + view_uniforms: Res, + lights_uniforms: Res, + atmosphere_transforms: Res, + atmosphere_uniforms: Res>, + settings_uniforms: Res>, + mut commands: Commands, +) { + for (entity, textures) in &probes { + let environment = render_device.create_bind_group( + "environment_bind_group", + &layouts.environment, + &BindGroupEntries::sequential(( + atmosphere_uniforms.binding().unwrap(), + settings_uniforms.binding().unwrap(), + atmosphere_transforms.uniforms().binding().unwrap(), + view_uniforms.uniforms.binding().unwrap(), + lights_uniforms.view_gpu_lights.binding().unwrap(), + &textures.transmittance_lut.default_view, + &samplers.transmittance_lut, + &textures.multiscattering_lut.default_view, + &samplers.multiscattering_lut, + &textures.sky_view_lut.default_view, + &samplers.sky_view_lut, + &textures.aerial_view_lut.default_view, + &samplers.aerial_view_lut, + &textures.environment, + )), + ); + + commands + .entity(entity) + .insert(AtmosphereProbeBindGroups { environment }); + } +} + +pub(super) fn prepare_probe_textures( + view_textures: Query<&AtmosphereTextures, With>, + probes: Query< + (Entity, &AtmosphereEnvironmentMap), + ( + With, + Without, + ), + >, + gpu_images: Res>, + mut commands: Commands, +) { + for (probe, render_env_map) in &probes { + let environment = gpu_images.get(&render_env_map.environment_map).unwrap(); + // create a cube view + let environment_view = environment.texture.create_view(&TextureViewDescriptor { + dimension: Some(TextureViewDimension::D2Array), + ..Default::default() + }); + // Get the first view entity's textures to borrow + if let Some(view_textures) = view_textures.iter().next() { + commands.entity(probe).insert(AtmosphereProbeTextures { + environment: environment_view, + transmittance_lut: view_textures.transmittance_lut.clone(), + multiscattering_lut: view_textures.multiscattering_lut.clone(), + sky_view_lut: view_textures.sky_view_lut.clone(), + aerial_view_lut: view_textures.aerial_view_lut.clone(), + }); + } + } +} + +pub fn init_atmosphere_probe_pipeline( + pipeline_cache: Res, + layouts: Res, + asset_server: Res, + mut commands: Commands, +) { + let environment = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("environment_pipeline".into()), + layout: vec![layouts.environment.clone()], + shader: load_embedded_asset!(asset_server.as_ref(), "environment.wgsl"), + ..default() + }); + commands.insert_resource(AtmosphereProbePipeline { environment }); +} + +// Ensure power-of-two dimensions to avoid edge update issues on cubemap faces +pub fn validate_environment_map_size(size: UVec2) -> UVec2 { + let new_size = UVec2::new( + size.x.max(1).next_power_of_two(), + size.y.max(1).next_power_of_two(), + ); + if new_size != size { + warn!( + "Non-power-of-two AtmosphereEnvironmentMapLight size {}, correcting to {new_size}", + size + ); + } + new_size +} + +pub fn prepare_atmosphere_probe_components( + probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without,)>, + mut commands: Commands, + mut images: ResMut>, +) { + for (entity, env_map_light) in &probes { + // Create a cubemap image in the main world that we can reference + let new_size = validate_environment_map_size(env_map_light.size); + let mut environment_image = Image::new_fill( + Extent3d { + width: new_size.x, + height: new_size.y, + depth_or_array_layers: 6, + }, + TextureDimension::D2, + &[0; 8], + TextureFormat::Rgba16Float, + RenderAssetUsages::all(), + ); + + environment_image.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(TextureViewDimension::Cube), + ..Default::default() + }); + + environment_image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING + | TextureUsages::STORAGE_BINDING + | TextureUsages::COPY_SRC; + + // Add the image to assets to get a handle + let environment_handle = images.add(environment_image); + + commands.entity(entity).insert(AtmosphereEnvironmentMap { + environment_map: environment_handle.clone(), + size: new_size, + }); + + commands + .entity(entity) + .insert(GeneratedEnvironmentMapLight { + environment_map: environment_handle, + intensity: env_map_light.intensity, + rotation: Quat::IDENTITY, + affects_lightmapped_mesh_diffuse: env_map_light.affects_lightmapped_mesh_diffuse, + }); + } +} + +pub(super) struct EnvironmentNode { + main_view_query: QueryState<( + Read>, + Read>, + Read, + Read, + Read, + )>, + probe_query: QueryState<( + Read, + Read, + )>, +} + +impl FromWorld for EnvironmentNode { + fn from_world(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + probe_query: QueryState::new(world), + } + } +} + +impl Node for EnvironmentNode { + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + self.probe_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let pipelines = world.resource::(); + let view_entity = graph.view_entity(); + + let Some(environment_pipeline) = pipeline_cache.get_compute_pipeline(pipelines.environment) + else { + return Ok(()); + }; + + let (Ok(( + atmosphere_uniforms_offset, + settings_uniforms_offset, + atmosphere_transforms_offset, + view_uniforms_offset, + lights_uniforms_offset, + )),) = (self.main_view_query.get_manual(world, view_entity),) + else { + return Ok(()); + }; + + for (bind_groups, env_map_light) in self.probe_query.iter_manual(world) { + let mut pass = + render_context + .command_encoder() + .begin_compute_pass(&ComputePassDescriptor { + label: Some("environment_pass"), + timestamp_writes: None, + }); + + pass.set_pipeline(environment_pipeline); + pass.set_bind_group( + 0, + &bind_groups.environment, + &[ + atmosphere_uniforms_offset.index(), + settings_uniforms_offset.index(), + atmosphere_transforms_offset.index(), + view_uniforms_offset.offset, + lights_uniforms_offset.offset, + ], + ); + + pass.dispatch_workgroups( + env_map_light.size.x / 8, + env_map_light.size.y / 8, + 6, // 6 cubemap faces + ); + } + + Ok(()) + } +} diff --git a/crates/bevy_pbr/src/atmosphere/environment.wgsl b/crates/bevy_pbr/src/atmosphere/environment.wgsl new file mode 100644 index 0000000000000..3e96b41120f6e --- /dev/null +++ b/crates/bevy_pbr/src/atmosphere/environment.wgsl @@ -0,0 +1,39 @@ +#import bevy_pbr::{ + atmosphere::{ + functions::{direction_world_to_atmosphere, sample_sky_view_lut, get_view_position}, + }, + utils::sample_cube_dir +} + +@group(0) @binding(13) var output: texture_storage_2d_array; + +@compute @workgroup_size(8, 8, 1) +fn main(@builtin(global_invocation_id) global_id: vec3) { + let dimensions = textureDimensions(output); + let slice_index = global_id.z; + + if (global_id.x >= dimensions.x || global_id.y >= dimensions.y || slice_index >= 6u) { + return; + } + + // Calculate normalized UV coordinates for this pixel + let uv = vec2( + (f32(global_id.x) + 0.5) / f32(dimensions.x), + (f32(global_id.y) + 0.5) / f32(dimensions.y) + ); + + var ray_dir_ws = sample_cube_dir(uv, slice_index); + + // invert the z direction to account for cubemaps being lefthanded + ray_dir_ws.z = -ray_dir_ws.z; + + let world_pos = get_view_position(); + let r = length(world_pos); + let up = normalize(world_pos); + + let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz, up); + let inscattering = sample_sky_view_lut(r, ray_dir_as); + let color = vec4(inscattering, 1.0); + + textureStore(output, vec2(global_id.xy), i32(slice_index), color); +} \ No newline at end of file diff --git a/crates/bevy_pbr/src/atmosphere/functions.wgsl b/crates/bevy_pbr/src/atmosphere/functions.wgsl index c1f02fc921c88..e63f22d4d58cc 100644 --- a/crates/bevy_pbr/src/atmosphere/functions.wgsl +++ b/crates/bevy_pbr/src/atmosphere/functions.wgsl @@ -1,6 +1,6 @@ #define_import_path bevy_pbr::atmosphere::functions -#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2} +#import bevy_render::maths::{PI, HALF_PI, PI_2, fast_acos, fast_acos_4, fast_atan2, ray_sphere_intersect} #import bevy_pbr::atmosphere::{ types::Atmosphere, @@ -36,12 +36,12 @@ // CONSTANTS - const FRAC_PI: f32 = 0.3183098862; // 1 / π const FRAC_2_PI: f32 = 0.15915494309; // 1 / (2π) const FRAC_3_16_PI: f32 = 0.0596831036594607509; // 3 / (16π) const FRAC_4_PI: f32 = 0.07957747154594767; // 1 / (4π) const ROOT_2: f32 = 1.41421356; // √2 +const EPSILON: f32 = 1.0; // 1 meter // During raymarching, each segment is sampled at a single point. This constant determines // where in the segment that sample is taken (0.0 = start, 0.5 = middle, 1.0 = end). @@ -243,7 +243,9 @@ fn sample_atmosphere(r: f32) -> AtmosphereSample { } /// evaluates L_scat, equation 3 in the paper, which gives the total single-order scattering towards the view at a single point -fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3, local_r: f32, local_up: vec3) -> vec3 { +fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3, world_pos: vec3) -> vec3 { + let local_r = length(world_pos); + let local_up = normalize(world_pos); var inscattering = vec3(0.0); for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { let light = &lights.directional_lights[light_i]; @@ -272,24 +274,28 @@ fn sample_local_inscattering(local_atmosphere: AtmosphereSample, ray_dir: vec3) -> vec3 { - let r = view_radius(); - let mu_view = ray_dir_ws.y; + let view_pos = get_view_position(); + let r = length(view_pos); + let up = normalize(view_pos); + let mu_view = dot(ray_dir_ws, up); let shadow_factor = f32(!ray_intersects_ground(r, mu_view)); var sun_radiance = vec3(0.0); for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { let light = &lights.directional_lights[light_i]; let neg_LdotV = dot((*light).direction_to_light, ray_dir_ws); - let angle_to_sun = fast_acos(neg_LdotV); - let pixel_size = fwidth(angle_to_sun); - let factor = smoothstep(0.0, -pixel_size * ROOT_2, angle_to_sun - SUN_ANGULAR_SIZE * 0.5); - let sun_solid_angle = (SUN_ANGULAR_SIZE * SUN_ANGULAR_SIZE) * 4.0 * FRAC_PI; - sun_radiance += ((*light).color.rgb / sun_solid_angle) * factor * shadow_factor; + let angle_to_sun = fast_acos(clamp(neg_LdotV, -1.0, 1.0)); + let w = max(0.5 * fwidth(angle_to_sun), 1e-6); + let sun_angular_size = (*light).sun_disk_angular_size; + let sun_intensity = (*light).sun_disk_intensity; + if sun_angular_size > 0.0 && sun_intensity > 0.0 { + let factor = 1 - smoothstep(sun_angular_size * 0.5 - w, sun_angular_size * 0.5 + w, angle_to_sun); + let sun_solid_angle = (sun_angular_size * sun_angular_size) * 0.25 * PI; + sun_radiance += ((*light).color.rgb / sun_solid_angle) * sun_intensity * factor * shadow_factor; + } } return sun_radiance; } @@ -303,9 +309,21 @@ fn max_atmosphere_distance(r: f32, mu: f32) -> f32 { return mix(t_top, t_bottom, f32(hits)); } -/// Assuming y=0 is the planet ground, returns the view radius in meters -fn view_radius() -> f32 { - return view.world_position.y * settings.scene_units_to_m + atmosphere.bottom_radius; +/// Returns the observer's position in the atmosphere +fn get_view_position() -> vec3 { + var world_pos = view.world_position * settings.scene_units_to_m + vec3(0.0, atmosphere.bottom_radius, 0.0); + + // If the camera is underground, clamp it to the ground surface along the local up. + let r = length(world_pos); + // Nudge r above ground to avoid sqrt cancellation, zero-length segments where + // r is equal to bottom_radius, which show up as black pixels + let min_radius = atmosphere.bottom_radius + EPSILON; + if r < min_radius { + let up = normalize(world_pos); + world_pos = up * min_radius; + } + + return world_pos; } // We assume the `up` vector at the view position is the y axis, since the world is locally flat/level. @@ -332,9 +350,16 @@ fn ndc_to_uv(ndc: vec2) -> vec2 { } /// Converts a direction in world space to atmosphere space -fn direction_world_to_atmosphere(dir_ws: vec3) -> vec3 { - let dir_as = atmosphere_transforms.atmosphere_from_world * vec4(dir_ws, 0.0); - return dir_as.xyz; +fn direction_world_to_atmosphere(dir_ws: vec3, up: vec3) -> vec3 { + // Camera forward in world space (-Z in view to world transform) + let forward_ws = (view.world_from_view * vec4(0.0, 0.0, -1.0, 0.0)).xyz; + let tangent_z = normalize(up * dot(forward_ws, up) - forward_ws); + let tangent_x = cross(up, tangent_z); + return vec3( + dot(dir_ws, tangent_x), + dot(dir_ws, up), + dot(dir_ws, tangent_z), + ); } /// Converts a direction in atmosphere space to world space @@ -344,8 +369,8 @@ fn direction_atmosphere_to_world(dir_as: vec3) -> vec3 { } // Modified from skybox.wgsl. For this pass we don't need to apply a separate sky transform or consider camera viewport. -// w component is the cosine of the view direction with the view forward vector, to correct step distance at the edges of the viewport -fn uv_to_ray_direction(uv: vec2) -> vec4 { +// Returns a normalized ray direction in world space. +fn uv_to_ray_direction(uv: vec2) -> vec3 { // Using world positions of the fragment and camera to calculate a ray direction // breaks down at large translations. This code only needs to know the ray direction. // The ray direction is along the direction from the camera to the fragment position. @@ -367,7 +392,7 @@ fn uv_to_ray_direction(uv: vec2) -> vec4 { // the translations from the view matrix. let ray_direction = (view.world_from_view * vec4(view_ray_direction, 0.0)).xyz; - return vec4(normalize(ray_direction), -view_ray_direction.z); + return normalize(ray_direction); } fn zenith_azimuth_to_ray_dir(zenith: f32, azimuth: f32) -> vec3 { @@ -377,3 +402,127 @@ fn zenith_azimuth_to_ray_dir(zenith: f32, azimuth: f32) -> vec3 { let cos_azimuth = cos(azimuth); return vec3(sin_azimuth * sin_zenith, mu, -cos_azimuth * sin_zenith); } + +struct RaymarchSegment { + start: f32, + end: f32, +} + +fn get_raymarch_segment(r: f32, mu: f32) -> RaymarchSegment { + // Get both intersection points with atmosphere + let atmosphere_intersections = ray_sphere_intersect(r, mu, atmosphere.top_radius); + let ground_intersections = ray_sphere_intersect(r, mu, atmosphere.bottom_radius); + + var segment: RaymarchSegment; + + if r < atmosphere.bottom_radius { + // Inside planet - start from bottom of atmosphere + segment.start = ground_intersections.y; // Use second intersection point with ground + segment.end = atmosphere_intersections.y; + } else if r < atmosphere.top_radius { + // Inside atmosphere + segment.start = 0.0; + segment.end = select(atmosphere_intersections.y, ground_intersections.x, ray_intersects_ground(r, mu)); + } else { + // Outside atmosphere + if atmosphere_intersections.x < 0.0 { + // No intersection with atmosphere + return segment; + } + // Start at atmosphere entry, end at exit or ground + segment.start = atmosphere_intersections.x; + segment.end = select(atmosphere_intersections.y, ground_intersections.x, ray_intersects_ground(r, mu)); + } + + return segment; +} + +struct RaymarchResult { + inscattering: vec3, + transmittance: vec3, +} + +fn raymarch_atmosphere( + pos: vec3, + ray_dir: vec3, + t_max: f32, + max_samples: u32, + uv: vec2, + ground: bool +) -> RaymarchResult { + let r = length(pos); + let up = normalize(pos); + let mu = dot(ray_dir, up); + + // Optimization: Reduce sample count at close proximity to the scene + let sample_count = mix(1.0, f32(max_samples), saturate(t_max * 0.01)); + + let segment = get_raymarch_segment(r, mu); + let t_start = segment.start; + var t_end = segment.end; + + t_end = min(t_end, t_max); + let t_total = t_end - t_start; + + var result: RaymarchResult; + result.inscattering = vec3(0.0); + result.transmittance = vec3(1.0); + + // Skip if invalid segment + if t_total <= 0.0 { + return result; + } + + var prev_t = t_start; + var optical_depth = vec3(0.0); + for (var s = 0.0; s < sample_count; s += 1.0) { + // Linear distribution from atmosphere entry to exit/ground + let t_i = t_start + t_total * (s + MIDPOINT_RATIO) / sample_count; + let dt_i = (t_i - prev_t); + prev_t = t_i; + + let sample_pos = pos + ray_dir * t_i; + let local_r = length(sample_pos); + let local_up = normalize(sample_pos); + let local_atmosphere = sample_atmosphere(local_r); + + let sample_optical_depth = local_atmosphere.extinction * dt_i; + optical_depth += sample_optical_depth; + let sample_transmittance = exp(-sample_optical_depth); + + let inscattering = sample_local_inscattering( + local_atmosphere, + ray_dir, + sample_pos + ); + + let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction; + result.inscattering += result.transmittance * s_int; + + result.transmittance *= sample_transmittance; + if all(result.transmittance < vec3(0.001)) { + break; + } + } + + // include reflected luminance from planet ground + if ground && ray_intersects_ground(r, mu) { + for (var light_i: u32 = 0u; light_i < lights.n_directional_lights; light_i++) { + let light = &lights.directional_lights[light_i]; + let light_dir = (*light).direction_to_light; + let light_color = (*light).color.rgb; + let transmittance_to_ground = exp(-optical_depth); + // position on the sphere and get the sphere normal (up) + let sphere_point = pos + ray_dir * t_end; + let sphere_normal = normalize(sphere_point); + let mu_light = dot(light_dir, sphere_normal); + let transmittance_to_light = sample_transmittance_lut(0.0, mu_light); + let light_luminance = transmittance_to_light * max(mu_light, 0.0) * light_color; + // Normalized Lambert BRDF + let ground_luminance = transmittance_to_ground * atmosphere.ground_albedo / PI; + result.inscattering += ground_luminance * light_luminance; + } + } + + return result; +} \ No newline at end of file diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index e7b7e55c6d941..608cf02314bf4 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -33,10 +33,11 @@ //! //! [Unreal Engine Implementation]: https://github.com/sebh/UnrealEngineSkyAtmosphere +mod environment; mod node; pub mod resources; -use bevy_app::{App, Plugin}; +use bevy_app::{App, Plugin, Update}; use bevy_asset::embedded_asset; use bevy_camera::Camera3d; use bevy_core_pipeline::core_3d::graph::Node3d; @@ -52,6 +53,7 @@ use bevy_render::{ extract_component::UniformComponentPlugin, render_resource::{DownlevelFlags, ShaderType, SpecializedRenderPipelines}, view::Hdr, + RenderStartup, }; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, @@ -63,6 +65,11 @@ use bevy_render::{ use bevy_core_pipeline::core_3d::graph::Core3d; use bevy_shader::load_shader_library; +use environment::{ + init_atmosphere_probe_layout, init_atmosphere_probe_pipeline, + prepare_atmosphere_probe_bind_groups, prepare_atmosphere_probe_components, + prepare_probe_textures, AtmosphereEnvironmentMap, EnvironmentNode, +}; use resources::{ prepare_atmosphere_transforms, queue_render_sky_pipelines, AtmosphereTransforms, RenderSkyBindGroupLayouts, @@ -92,13 +99,16 @@ impl Plugin for AtmospherePlugin { embedded_asset!(app, "sky_view_lut.wgsl"); embedded_asset!(app, "aerial_view_lut.wgsl"); embedded_asset!(app, "render_sky.wgsl"); + embedded_asset!(app, "environment.wgsl"); app.add_plugins(( ExtractComponentPlugin::::default(), - ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), + ExtractComponentPlugin::::default(), UniformComponentPlugin::::default(), - UniformComponentPlugin::::default(), - )); + UniformComponentPlugin::::default(), + )) + .add_systems(Update, prepare_atmosphere_probe_components); } fn finish(&self, app: &mut App) { @@ -133,12 +143,20 @@ impl Plugin for AtmospherePlugin { .init_resource::() .init_resource::() .init_resource::>() + .add_systems( + RenderStartup, + (init_atmosphere_probe_layout, init_atmosphere_probe_pipeline).chain(), + ) .add_systems( Render, ( configure_camera_depth_usages.in_set(RenderSystems::ManageViews), queue_render_sky_pipelines.in_set(RenderSystems::Queue), prepare_atmosphere_textures.in_set(RenderSystems::PrepareResources), + prepare_probe_textures + .in_set(RenderSystems::PrepareResources) + .after(prepare_atmosphere_textures), + prepare_atmosphere_probe_bind_groups.in_set(RenderSystems::PrepareBindGroups), prepare_atmosphere_transforms.in_set(RenderSystems::PrepareResources), prepare_atmosphere_bind_groups.in_set(RenderSystems::PrepareBindGroups), ), @@ -160,6 +178,7 @@ impl Plugin for AtmospherePlugin { Core3d, AtmosphereNode::RenderSky, ) + .add_render_graph_node::(Core3d, AtmosphereNode::Environment) .add_render_graph_edges( Core3d, ( @@ -331,7 +350,7 @@ impl ExtractComponent for Atmosphere { /// The aerial-view lut is a 3d LUT fit to the view frustum, which stores the luminance /// scattered towards the camera at each point (RGB channels), alongside the average /// transmittance to that point (A channel). -#[derive(Clone, Component, Reflect, ShaderType)] +#[derive(Clone, Component, Reflect)] #[reflect(Clone, Default)] pub struct AtmosphereSettings { /// The size of the transmittance LUT @@ -377,6 +396,13 @@ pub struct AtmosphereSettings { /// A conversion factor between scene units and meters, used to /// ensure correctness at different length scales. pub scene_units_to_m: f32, + + /// The number of points to sample for each fragment when the using + /// ray marching to render the sky + pub sky_max_samples: u32, + + /// The rendering method to use for the atmosphere. + pub rendering_method: AtmosphereMode, } impl Default for AtmosphereSettings { @@ -393,19 +419,65 @@ impl Default for AtmosphereSettings { aerial_view_lut_samples: 10, aerial_view_lut_max_distance: 3.2e4, scene_units_to_m: 1.0, + sky_max_samples: 16, + rendering_method: AtmosphereMode::LookupTexture, + } + } +} + +#[derive(Clone, Component, Reflect, ShaderType)] +#[reflect(Default)] +pub struct GpuAtmosphereSettings { + pub transmittance_lut_size: UVec2, + pub multiscattering_lut_size: UVec2, + pub sky_view_lut_size: UVec2, + pub aerial_view_lut_size: UVec3, + pub transmittance_lut_samples: u32, + pub multiscattering_lut_dirs: u32, + pub multiscattering_lut_samples: u32, + pub sky_view_lut_samples: u32, + pub aerial_view_lut_samples: u32, + pub aerial_view_lut_max_distance: f32, + pub scene_units_to_m: f32, + pub sky_max_samples: u32, + pub rendering_method: u32, +} + +impl Default for GpuAtmosphereSettings { + fn default() -> Self { + AtmosphereSettings::default().into() + } +} + +impl From for GpuAtmosphereSettings { + fn from(s: AtmosphereSettings) -> Self { + Self { + transmittance_lut_size: s.transmittance_lut_size, + multiscattering_lut_size: s.multiscattering_lut_size, + sky_view_lut_size: s.sky_view_lut_size, + aerial_view_lut_size: s.aerial_view_lut_size, + transmittance_lut_samples: s.transmittance_lut_samples, + multiscattering_lut_dirs: s.multiscattering_lut_dirs, + multiscattering_lut_samples: s.multiscattering_lut_samples, + sky_view_lut_samples: s.sky_view_lut_samples, + aerial_view_lut_samples: s.aerial_view_lut_samples, + aerial_view_lut_max_distance: s.aerial_view_lut_max_distance, + scene_units_to_m: s.scene_units_to_m, + sky_max_samples: s.sky_max_samples, + rendering_method: s.rendering_method as u32, } } } -impl ExtractComponent for AtmosphereSettings { +impl ExtractComponent for GpuAtmosphereSettings { type QueryData = Read; type QueryFilter = (With, With); - type Out = AtmosphereSettings; + type Out = GpuAtmosphereSettings; fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { - Some(item.clone()) + Some(item.clone().into()) } } @@ -416,3 +488,23 @@ fn configure_camera_depth_usages( camera.depth_texture_usages.0 |= TextureUsages::TEXTURE_BINDING.bits(); } } + +/// Selects how the atmosphere is rendered. Choose based on scene scale and +/// volumetric shadow quality, and based on performance needs. +#[repr(u32)] +#[derive(Clone, Default, Reflect, Copy)] +pub enum AtmosphereMode { + /// High-performance solution tailored to scenes that are mostly inside of the atmosphere. + /// Uses a set of lookup textures to approximate scattering integration. + /// Slightly less accurate for very long-distance/space views (lighting precision + /// tapers as the camera moves far from the scene origin) and for sharp volumetric + /// (cloud/fog) shadows. + #[default] + LookupTexture = 0, + /// Slower, more accurate rendering method for any type of scene. + /// Integrates the scattering numerically with raymarching and produces sharp volumetric + /// (cloud/fog) shadows. + /// Best for cinematic shots, planets seen from orbit, and scenes requiring + /// accurate long-distance lighting. + Raymarched = 1, +} diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 93c1a33ae9db2..13734ab07a980 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -16,13 +16,14 @@ use super::{ AtmosphereBindGroups, AtmosphereLutPipelines, AtmosphereTransformsOffset, RenderSkyPipelineId, }, - Atmosphere, AtmosphereSettings, + Atmosphere, GpuAtmosphereSettings, }; #[derive(PartialEq, Eq, Debug, Copy, Clone, Hash, RenderLabel)] pub enum AtmosphereNode { RenderLuts, RenderSky, + Environment, } #[derive(Default)] @@ -30,10 +31,10 @@ pub(super) struct AtmosphereLutsNode {} impl ViewNode for AtmosphereLutsNode { type ViewQuery = ( - Read, + Read, Read, Read>, - Read>, + Read>, Read, Read, Read, @@ -167,7 +168,7 @@ impl ViewNode for RenderSkyNode { Read, Read, Read>, - Read>, + Read>, Read, Read, Read, diff --git a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl index f8298272caa69..0e0d5485c963b 100644 --- a/crates/bevy_pbr/src/atmosphere/render_sky.wgsl +++ b/crates/bevy_pbr/src/atmosphere/render_sky.wgsl @@ -2,12 +2,13 @@ enable dual_source_blending; #import bevy_pbr::atmosphere::{ types::{Atmosphere, AtmosphereSettings}, - bindings::{atmosphere, view, atmosphere_transforms}, + bindings::{atmosphere, view, atmosphere_transforms, settings}, functions::{ sample_transmittance_lut, sample_transmittance_lut_segment, sample_sky_view_lut, direction_world_to_atmosphere, uv_to_ray_direction, uv_to_ndc, sample_aerial_view_lut, - view_radius, sample_sun_radiance, ndc_to_camera_dist + sample_sun_radiance, ndc_to_camera_dist, raymarch_atmosphere, + get_view_position, max_atmosphere_distance }, }; #import bevy_render::view::View; @@ -34,24 +35,43 @@ fn main(in: FullscreenVertexOutput) -> RenderSkyOutput { let depth = textureLoad(depth_texture, vec2(in.position.xy), 0); let ray_dir_ws = uv_to_ray_direction(in.uv); - let r = view_radius(); - let mu = ray_dir_ws.y; + let world_pos = get_view_position(); + let r = length(world_pos); + let up = normalize(world_pos); + let mu = dot(ray_dir_ws, up); + let max_samples = settings.sky_max_samples; + let should_raymarch = settings.rendering_method == 1u; var transmittance: vec3; var inscattering: vec3; - let sun_radiance = sample_sun_radiance(ray_dir_ws.xyz); + let sun_radiance = sample_sun_radiance(ray_dir_ws); if depth == 0.0 { - let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws.xyz); + let ray_dir_as = direction_world_to_atmosphere(ray_dir_ws, up); transmittance = sample_transmittance_lut(r, mu); - inscattering += sample_sky_view_lut(r, ray_dir_as); - inscattering += sun_radiance * transmittance * view.exposure; + inscattering = sample_sky_view_lut(r, ray_dir_as); + if should_raymarch { + let t_max = max_atmosphere_distance(r, mu); + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t_max, max_samples, in.uv, true); + inscattering = result.inscattering; + transmittance = result.transmittance; + } + inscattering += sun_radiance * transmittance; } else { let t = ndc_to_camera_dist(vec3(uv_to_ndc(in.uv), depth)); inscattering = sample_aerial_view_lut(in.uv, t); transmittance = sample_transmittance_lut_segment(r, mu, t); + if should_raymarch { + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t, max_samples, in.uv, false); + inscattering = result.inscattering; + transmittance = result.transmittance; + } } + + // exposure compensation + inscattering *= view.exposure; + #ifdef DUAL_SOURCE_BLENDING return RenderSkyOutput(vec4(inscattering, 0.0), vec4(transmittance, 1.0)); #else diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 9e3a75d4b6921..fe487975e8d6f 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ world::{FromWorld, World}, }; use bevy_image::ToExtents; -use bevy_math::{Mat4, Vec3}; +use bevy_math::{Affine3A, Mat4, Vec3A}; use bevy_render::{ extract_component::ComponentUniforms, render_resource::{binding_types::*, *}, @@ -22,7 +22,7 @@ use bevy_render::{ use bevy_shader::Shader; use bevy_utils::default; -use super::{Atmosphere, AtmosphereSettings}; +use super::{Atmosphere, GpuAtmosphereSettings}; #[derive(Resource)] pub(crate) struct AtmosphereBindGroupLayouts { @@ -49,7 +49,7 @@ impl FromWorld for AtmosphereBindGroupLayouts { ShaderStages::COMPUTE, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), ( // transmittance lut storage texture 13, @@ -68,7 +68,7 @@ impl FromWorld for AtmosphereBindGroupLayouts { ShaderStages::COMPUTE, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler (6, sampler(SamplerBindingType::Filtering)), ( @@ -89,7 +89,7 @@ impl FromWorld for AtmosphereBindGroupLayouts { ShaderStages::COMPUTE, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), @@ -114,7 +114,7 @@ impl FromWorld for AtmosphereBindGroupLayouts { ShaderStages::COMPUTE, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler @@ -151,12 +151,14 @@ impl FromWorld for RenderSkyBindGroupLayouts { ShaderStages::FRAGMENT, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler (6, sampler(SamplerBindingType::Filtering)), + (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler + (8, sampler(SamplerBindingType::Filtering)), (9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler (10, sampler(SamplerBindingType::Filtering)), ( @@ -180,12 +182,14 @@ impl FromWorld for RenderSkyBindGroupLayouts { ShaderStages::FRAGMENT, ( (0, uniform_buffer::(true)), - (1, uniform_buffer::(true)), + (1, uniform_buffer::(true)), (2, uniform_buffer::(true)), (3, uniform_buffer::(true)), (4, uniform_buffer::(true)), (5, texture_2d(TextureSampleType::Float { filterable: true })), //transmittance lut and sampler (6, sampler(SamplerBindingType::Filtering)), + (7, texture_2d(TextureSampleType::Float { filterable: true })), //multiscattering lut and sampler + (8, sampler(SamplerBindingType::Filtering)), (9, texture_2d(TextureSampleType::Float { filterable: true })), //sky view lut and sampler (10, sampler(SamplerBindingType::Filtering)), ( @@ -410,7 +414,7 @@ pub struct AtmosphereTextures { } pub(super) fn prepare_atmosphere_textures( - views: Query<(Entity, &AtmosphereSettings), With>, + views: Query<(Entity, &GpuAtmosphereSettings), With>, render_device: Res, mut texture_cache: ResMut, mut commands: Commands, @@ -498,7 +502,6 @@ impl AtmosphereTransforms { #[derive(ShaderType)] pub struct AtmosphereTransform { world_from_atmosphere: Mat4, - atmosphere_from_world: Mat4, } #[derive(Component)] @@ -530,28 +533,23 @@ pub(super) fn prepare_atmosphere_transforms( }; for (entity, view) in &views { - let world_from_view = view.world_from_view.to_matrix(); - let camera_z = world_from_view.z_axis.truncate(); - let camera_y = world_from_view.y_axis.truncate(); + let world_from_view = view.world_from_view.affine(); + let camera_z = world_from_view.matrix3.z_axis; + let camera_y = world_from_view.matrix3.y_axis; let atmo_z = camera_z .with_y(0.0) .try_normalize() .unwrap_or_else(|| camera_y.with_y(0.0).normalize()); - let atmo_y = Vec3::Y; + let atmo_y = Vec3A::Y; let atmo_x = atmo_y.cross(atmo_z).normalize(); - let world_from_atmosphere = Mat4::from_cols( - atmo_x.extend(0.0), - atmo_y.extend(0.0), - atmo_z.extend(0.0), - world_from_view.w_axis, - ); + let world_from_atmosphere = + Affine3A::from_cols(atmo_x, atmo_y, atmo_z, world_from_view.translation); - let atmosphere_from_world = world_from_atmosphere.inverse(); + let world_from_atmosphere = Mat4::from(world_from_atmosphere); commands.entity(entity).insert(AtmosphereTransformsOffset { index: writer.write(&AtmosphereTransform { world_from_atmosphere, - atmosphere_from_world, }), }); } @@ -579,7 +577,7 @@ pub(super) fn prepare_atmosphere_bind_groups( lights_uniforms: Res, atmosphere_transforms: Res, atmosphere_uniforms: Res>, - settings_uniforms: Res>, + settings_uniforms: Res>, mut commands: Commands, ) { @@ -681,6 +679,8 @@ pub(super) fn prepare_atmosphere_bind_groups( (4, lights_binding.clone()), (5, &textures.transmittance_lut.default_view), (6, &samplers.transmittance_lut), + (7, &textures.multiscattering_lut.default_view), + (8, &samplers.multiscattering_lut), (9, &textures.sky_view_lut.default_view), (10, &samplers.sky_view_lut), (11, &textures.aerial_view_lut.default_view), diff --git a/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl index cf3d95b173c42..5bb7b9417df98 100644 --- a/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl +++ b/crates/bevy_pbr/src/atmosphere/sky_view_lut.wgsl @@ -4,11 +4,11 @@ types::{Atmosphere, AtmosphereSettings}, bindings::{atmosphere, view, settings}, functions::{ - sample_atmosphere, get_local_up, AtmosphereSample, - sample_local_inscattering, get_local_r, view_radius, + sample_atmosphere, AtmosphereSample, + sample_local_inscattering, get_view_position, max_atmosphere_distance, direction_atmosphere_to_world, sky_view_lut_uv_to_zenith_azimuth, zenith_azimuth_to_ray_dir, - MIDPOINT_RATIO + MIDPOINT_RATIO, raymarch_atmosphere, EPSILON }, } } @@ -26,47 +26,19 @@ fn main(@builtin(global_invocation_id) idx: vec3) { let uv = vec2(idx.xy) / vec2(settings.sky_view_lut_size); - let r = view_radius(); + let cam_pos = get_view_position(); + let r = length(cam_pos); var zenith_azimuth = sky_view_lut_uv_to_zenith_azimuth(r, uv); let ray_dir_as = zenith_azimuth_to_ray_dir(zenith_azimuth.x, zenith_azimuth.y); let ray_dir_ws = direction_atmosphere_to_world(ray_dir_as); - let mu = ray_dir_ws.y; + let world_pos = vec3(0.0, r, 0.0); + let up = normalize(world_pos); + let mu = dot(ray_dir_ws, up); let t_max = max_atmosphere_distance(r, mu); - let sample_count = mix(1.0, f32(settings.sky_view_lut_samples), clamp(t_max * 0.01, 0.0, 1.0)); - var total_inscattering = vec3(0.0); - var throughput = vec3(1.0); - var prev_t = 0.0; - for (var s = 0.0; s < sample_count; s += 1.0) { - let t_i = t_max * (s + MIDPOINT_RATIO) / sample_count; - let dt_i = (t_i - prev_t); - prev_t = t_i; + let result = raymarch_atmosphere(world_pos, ray_dir_ws, t_max, settings.sky_view_lut_samples, uv, true); - let local_r = get_local_r(r, mu, t_i); - let local_up = get_local_up(r, t_i, ray_dir_ws); - let local_atmosphere = sample_atmosphere(local_r); - - let sample_optical_depth = local_atmosphere.extinction * dt_i; - let sample_transmittance = exp(-sample_optical_depth); - - let inscattering = sample_local_inscattering( - local_atmosphere, - ray_dir_ws, - local_r, - local_up - ); - - // Analytical integration of the single scattering term in the radiance transfer equation - let s_int = (inscattering - inscattering * sample_transmittance) / local_atmosphere.extinction; - total_inscattering += throughput * s_int; - - throughput *= sample_transmittance; - if all(throughput < vec3(0.001)) { - break; - } - } - - textureStore(sky_view_lut_out, idx.xy, vec4(total_inscattering, 1.0)); + textureStore(sky_view_lut_out, idx.xy, vec4(result.inscattering, 1.0)); } diff --git a/crates/bevy_pbr/src/atmosphere/types.wgsl b/crates/bevy_pbr/src/atmosphere/types.wgsl index 78e9e9a717192..f9207dd7228c5 100644 --- a/crates/bevy_pbr/src/atmosphere/types.wgsl +++ b/crates/bevy_pbr/src/atmosphere/types.wgsl @@ -34,6 +34,8 @@ struct AtmosphereSettings { aerial_view_lut_samples: u32, aerial_view_lut_max_distance: f32, scene_units_to_m: f32, + sky_max_samples: u32, + rendering_method: u32, } @@ -41,5 +43,4 @@ struct AtmosphereSettings { // so the horizon stays a horizontal line in our luts struct AtmosphereTransforms { world_from_atmosphere: mat4x4, - atmosphere_from_world: mat4x4, } diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index 27e8c2d5ec8da..857969510bd05 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -25,20 +25,20 @@ use bevy_ecs::{ query::With, resource::Resource, schedule::IntoScheduleConfigs as _, - system::{Query, Res, ResMut}, + system::{Commands, Local, Query, Res, ResMut}, }; use bevy_image::Image; use bevy_light::{ClusteredDecal, DirectionalLightTexture, PointLightTexture, SpotLightTexture}; use bevy_math::Mat4; use bevy_platform::collections::HashMap; use bevy_render::{ - extract_component::ExtractComponentPlugin, render_asset::RenderAssets, render_resource::{ binding_types, BindGroupLayoutEntryBuilder, Buffer, BufferUsages, RawBufferVec, Sampler, SamplerBindingType, ShaderType, TextureSampleType, TextureView, }, renderer::{RenderAdapter, RenderDevice, RenderQueue}, + sync_component::SyncComponentPlugin, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, @@ -142,7 +142,7 @@ impl Plugin for ClusteredDecalPlugin { fn build(&self, app: &mut App) { load_shader_library!(app, "clustered.wgsl"); - app.add_plugins(ExtractComponentPlugin::::default()); + app.add_plugins(SyncComponentPlugin::::default()); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -151,7 +151,7 @@ impl Plugin for ClusteredDecalPlugin { render_app .init_resource::() .init_resource::() - .add_systems(ExtractSchedule, extract_decals) + .add_systems(ExtractSchedule, (extract_decals, extract_clustered_decal)) .add_systems( Render, prepare_decals @@ -165,6 +165,21 @@ impl Plugin for ClusteredDecalPlugin { } } +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type ClusteredDecal +fn extract_clustered_decal( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + /// The GPU data structure that stores information about each decal. #[derive(Clone, Copy, Default, ShaderType, Pod, Zeroable)] #[repr(C)] diff --git a/crates/bevy_pbr/src/extended_material.rs b/crates/bevy_pbr/src/extended_material.rs index 4db6276e2c452..585039263100b 100644 --- a/crates/bevy_pbr/src/extended_material.rs +++ b/crates/bevy_pbr/src/extended_material.rs @@ -147,7 +147,7 @@ where } } -#[derive(bytemuck::Pod, bytemuck::Zeroable, Copy, Clone, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, PartialEq, Eq, Hash)] #[repr(C, packed)] pub struct MaterialExtensionBindGroupData { pub base: B, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 3cad4b859d546..a0bf22ba22c23 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -48,8 +48,8 @@ use bevy_color::{Color, LinearRgba}; pub use atmosphere::*; use bevy_light::{ - AmbientLight, DirectionalLight, LightPlugin, PointLight, ShadowFilteringMethod, - SimulationLightSystems, SpotLight, + AmbientLight, DirectionalLight, PointLight, ShadowFilteringMethod, SimulationLightSystems, + SpotLight, }; use bevy_shader::{load_shader_library, ShaderRef}; pub use cluster::*; @@ -134,7 +134,6 @@ use bevy_image::{Image, ImageSampler}; use bevy_render::{ alpha::AlphaMode, camera::sort_cameras, - extract_component::ExtractComponentPlugin, extract_resource::ExtractResourcePlugin, render_graph::RenderGraph, render_resource::{ @@ -229,13 +228,11 @@ impl Plugin for PbrPlugin { ..Default::default() }, ScreenSpaceAmbientOcclusionPlugin, - ExtractResourcePlugin::::default(), FogPlugin, ExtractResourcePlugin::::default(), - ExtractComponentPlugin::::default(), + SyncComponentPlugin::::default(), LightmapPlugin, LightProbePlugin, - LightPlugin, GpuMeshPreprocessPlugin { use_gpu_instance_buffer_builder: self.use_gpu_instance_buffer_builder, }, @@ -248,7 +245,7 @@ impl Plugin for PbrPlugin { SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), SyncComponentPlugin::::default(), - ExtractComponentPlugin::::default(), + SyncComponentPlugin::::default(), )) .add_plugins(AtmospherePlugin) .configure_sets( @@ -325,6 +322,9 @@ impl Plugin for PbrPlugin { ( extract_clusters, extract_lights, + extract_ambient_light_resource, + extract_ambient_light, + extract_shadow_filtering_method, late_sweep_material_instances, ), ) diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 1ed87319ef639..c6249a0fada88 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -23,12 +23,12 @@ use bevy_render::{ extract_instances::ExtractInstancesPlugin, render_asset::RenderAssets, render_resource::{DynamicUniformBuffer, Sampler, ShaderType, TextureView}, - renderer::{RenderAdapter, RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}, settings::WgpuFeatures, sync_world::RenderEntity, texture::{FallbackImage, GpuImage}, view::ExtractedView, - Extract, ExtractSchedule, Render, RenderApp, RenderSystems, + Extract, ExtractSchedule, Render, RenderApp, RenderSystems, WgpuWrapper, }; use bevy_shader::load_shader_library; use bevy_transform::{components::Transform, prelude::GlobalTransform}; @@ -143,7 +143,7 @@ where C: LightProbeComponent, { // The transform from world space to light probe space. - light_from_world: Mat4, + light_from_world: Affine3A, // The transform from light probe space to world space. world_from_light: Affine3A, @@ -543,7 +543,7 @@ where ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { world_from_light: light_probe_transform.affine(), - light_from_world: light_probe_transform.to_matrix().inverse(), + light_from_world: light_probe_transform.affine().inverse(), asset_id: id, intensity: environment_map.intensity(), affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), @@ -633,7 +633,7 @@ where // Transpose the inverse transform to compress the structure on the // GPU (from 4 `Vec4`s to 3 `Vec4`s). The shader will transpose it // to recover the original inverse transform. - let light_from_world_transposed = light_probe.light_from_world.transpose(); + let light_from_world_transposed = Mat4::from(light_probe.light_from_world).transpose(); // Write in the light probe data. self.render_light_probes.push(RenderLightProbe { @@ -718,8 +718,10 @@ pub(crate) fn binding_arrays_are_usable( render_device: &RenderDevice, render_adapter: &RenderAdapter, ) -> bool { + let adapter_info = RenderAdapterInfo(WgpuWrapper::new(render_adapter.get_info())); + !cfg!(feature = "shader_format_glsl") - && bevy_render::get_adreno_model(render_adapter).is_none_or(|model| model > 610) + && bevy_render::get_adreno_model(&adapter_info).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/material.rs b/crates/bevy_pbr/src/material.rs index 059c37db0cb83..ec46bd4f9fd16 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -57,7 +57,8 @@ use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap} use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities}; use bevy_shader::{Shader, ShaderDefVal}; use bevy_utils::Parallel; -use core::any::TypeId; +use core::any::{Any, TypeId}; +use core::hash::{BuildHasher, Hasher}; use core::{hash::Hash, marker::PhantomData}; use smallvec::SmallVec; use tracing::error; @@ -432,7 +433,7 @@ pub struct MaterialPipelineKey { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ErasedMaterialPipelineKey { pub mesh_key: MeshPipelineKey, - pub material_key: SmallVec<[u8; 8]>, + pub material_key: ErasedMaterialKey, pub type_id: TypeId, } @@ -1290,6 +1291,85 @@ pub struct DeferredDrawFunction; #[derive(DrawFunctionLabel, Debug, Hash, PartialEq, Eq, Clone, Default)] pub struct ShadowsDrawFunction; +#[derive(Debug)] +pub struct ErasedMaterialKey { + type_id: TypeId, + hash: u64, + value: Box, + vtable: Arc, +} + +#[derive(Debug)] +pub struct ErasedMaterialKeyVTable { + clone_fn: fn(&dyn Any) -> Box, + partial_eq_fn: fn(&dyn Any, &dyn Any) -> bool, +} + +impl ErasedMaterialKey { + pub fn new(material_key: T) -> Self + where + T: Clone + Hash + PartialEq + Send + Sync + 'static, + { + let type_id = TypeId::of::(); + let hash = FixedHasher::hash_one(&FixedHasher, &material_key); + + fn clone(any: &dyn Any) -> Box { + Box::new(any.downcast_ref::().unwrap().clone()) + } + fn partial_eq(a: &dyn Any, b: &dyn Any) -> bool { + a.downcast_ref::().unwrap() == b.downcast_ref::().unwrap() + } + + Self { + type_id, + hash, + value: Box::new(material_key), + vtable: Arc::new(ErasedMaterialKeyVTable { + clone_fn: clone::, + partial_eq_fn: partial_eq::, + }), + } + } + + pub fn to_key(&self) -> T { + debug_assert_eq!(self.type_id, TypeId::of::()); + self.value.downcast_ref::().unwrap().clone() + } +} + +impl PartialEq for ErasedMaterialKey { + fn eq(&self, other: &Self) -> bool { + self.type_id == other.type_id + && (self.vtable.partial_eq_fn)(self.value.as_ref(), other.value.as_ref()) + } +} + +impl Eq for ErasedMaterialKey {} + +impl Clone for ErasedMaterialKey { + fn clone(&self) -> Self { + Self { + type_id: self.type_id, + hash: self.hash, + value: (self.vtable.clone_fn)(self.value.as_ref()), + vtable: self.vtable.clone(), + } + } +} + +impl Hash for ErasedMaterialKey { + fn hash(&self, state: &mut H) { + self.type_id.hash(state); + self.hash.hash(state); + } +} + +impl Default for ErasedMaterialKey { + fn default() -> Self { + Self::new(()) + } +} + /// Common [`Material`] properties, calculated for a specific material instance. #[derive(Default)] pub struct MaterialProperties { @@ -1332,7 +1412,7 @@ pub struct MaterialProperties { >, /// The key for this material, typically a bitfield of flags that are used to modify /// the pipeline descriptor used for this material. - pub material_key: SmallVec<[u8; 8]>, + pub material_key: ErasedMaterialKey, /// Whether shadows are enabled for this material pub shadows_enabled: bool, /// Whether prepass is enabled for this material @@ -1395,7 +1475,7 @@ pub struct PreparedMaterial { // orphan rules T_T impl ErasedRenderAsset for MeshMaterial3d where - M::Data: Clone, + M::Data: PartialEq + Eq + Hash + Clone, { type SourceAsset = M; type ErasedAsset = PreparedMaterial; @@ -1556,21 +1636,24 @@ where let bindless = material_uses_bindless_resources::(render_device); let bind_group_data = material.bind_group_data(); - let material_key = SmallVec::from(bytemuck::bytes_of(&bind_group_data)); + let material_key = ErasedMaterialKey::new(bind_group_data); fn specialize( pipeline: &MaterialPipeline, descriptor: &mut RenderPipelineDescriptor, mesh_layout: &MeshVertexBufferLayoutRef, erased_key: ErasedMaterialPipelineKey, - ) -> Result<(), SpecializedMeshPipelineError> { - let material_key = bytemuck::from_bytes(erased_key.material_key.as_slice()); + ) -> Result<(), SpecializedMeshPipelineError> + where + M::Data: Hash + Clone, + { + let material_key = erased_key.material_key.to_key(); M::specialize( pipeline, descriptor, mesh_layout, MaterialPipelineKey { mesh_key: erased_key.mesh_key, - bind_group_data: *material_key, + bind_group_data: material_key, }, ) } diff --git a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs index e8f4669227a0b..bae7b670b7de2 100644 --- a/crates/bevy_pbr/src/meshlet/persistent_buffer.rs +++ b/crates/bevy_pbr/src/meshlet/persistent_buffer.rs @@ -39,7 +39,7 @@ impl PersistentGpuBuffer { /// Queue an item of type T to be added to the buffer, returning the byte range within the buffer that it will be located at. pub fn queue_write(&mut self, data: T, metadata: T::Metadata) -> Range { let data_size = data.size_in_bytes() as u64; - debug_assert!(data_size % COPY_BUFFER_ALIGNMENT == 0); + debug_assert!(data_size.is_multiple_of(COPY_BUFFER_ALIGNMENT)); if let Ok(buffer_slice) = self.allocation_planner.allocate_range(data_size) { self.write_queue .push((data, metadata, buffer_slice.clone())); diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index dd664c1a2da4f..2d19f863efc7e 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -78,16 +78,25 @@ pub struct StandardMaterial { /// /// The default emissive color is [`LinearRgba::BLACK`], which doesn't add anything to the material color. /// - /// To increase emissive strength, channel values for `emissive` + /// Emissive strength is controlled by the value of the color channels, + /// while the hue is controlled by their relative values. + /// + /// As a result, channel values for `emissive` /// colors can exceed `1.0`. For instance, a `base_color` of /// `LinearRgba::rgb(1.0, 0.0, 0.0)` represents the brightest /// red for objects that reflect light, but an emissive color /// like `LinearRgba::rgb(1000.0, 0.0, 0.0)` can be used to create /// intensely bright red emissive effects. /// + /// This results in a final luminance value when multiplied + /// by the value of the greyscale emissive texture (which ranges from 0 for black to 1 for white). + /// Luminance is a measure of the amount of light emitted per unit area, + /// and can be thought of as the "brightness" of the effect. + /// In Bevy, we treat these luminance values as the physical units of cd/m², aka nits. + /// /// Increasing the emissive strength of the color will impact visual effects /// like bloom, but it's important to note that **an emissive material won't - /// light up surrounding areas like a light source**, + /// typically light up surrounding areas like a light source**, /// it just adds a value to the color seen on screen. pub emissive: LinearRgba, @@ -1201,7 +1210,7 @@ impl AsBindGroupShaderType for StandardMaterial { bitflags! { /// The pipeline key for `StandardMaterial`, packed into 64 bits. #[repr(C)] - #[derive(Clone, Copy, PartialEq, Eq, Hash, bytemuck::Pod, bytemuck::Zeroable)] + #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct StandardMaterialKey: u64 { const CULL_FRONT = 0x000001; const CULL_BACK = 0x000002; diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index a06d15594d29f..69113359f5808 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -21,7 +21,7 @@ use bevy_ecs::{ SystemParamItem, }, }; -use bevy_math::{Affine3A, Vec4}; +use bevy_math::{Affine3A, Mat4, Vec4}; use bevy_mesh::{Mesh, Mesh3d, MeshVertexBufferLayoutRef}; use bevy_render::{ alpha::AlphaMode, @@ -201,15 +201,15 @@ pub fn update_previous_view_data( query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, ) { for (entity, camera, camera_transform) in &query { - let world_from_view = camera_transform.to_matrix(); - let view_from_world = world_from_view.inverse(); + let world_from_view = camera_transform.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); let view_from_clip = camera.clip_from_view().inverse(); commands.entity(entity).try_insert(PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view() * view_from_world, clip_from_view: camera.clip_from_view(), - world_from_clip: world_from_view * view_from_clip, + world_from_clip: Mat4::from(world_from_view) * view_from_clip, view_from_clip, }); } @@ -672,15 +672,15 @@ pub fn prepare_previous_view_uniforms( let prev_view_data = match maybe_previous_view_uniforms { Some(previous_view) => previous_view.clone(), None => { - let world_from_view = camera.world_from_view.to_matrix(); - let view_from_world = world_from_view.inverse(); + let world_from_view = camera.world_from_view.affine(); + let view_from_world = Mat4::from(world_from_view.inverse()); let view_from_clip = camera.clip_from_view.inverse(); PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view * view_from_world, clip_from_view: camera.clip_from_view, - world_from_clip: world_from_view * view_from_clip, + world_from_clip: Mat4::from(world_from_view) * view_from_clip, view_from_clip, } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 41792b38a62a1..155f2b2f2b9e9 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -22,10 +22,11 @@ use bevy_ecs::{ use bevy_light::cascade::Cascade; use bevy_light::cluster::assign::{calculate_cluster_factors, ClusterableObjectType}; use bevy_light::cluster::GlobalVisibleClusterableObjects; +use bevy_light::SunDisk; use bevy_light::{ spot_light_clip_from_view, spot_light_world_from_view, AmbientLight, CascadeShadowConfig, Cascades, DirectionalLight, DirectionalLightShadowMap, NotShadowCaster, PointLight, - PointLightShadowMap, SpotLight, VolumetricLight, + PointLightShadowMap, ShadowFilteringMethod, SpotLight, VolumetricLight, }; use bevy_math::{ops, Mat4, UVec4, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_platform::collections::{HashMap, HashSet}; @@ -103,6 +104,8 @@ pub struct ExtractedDirectionalLight { pub soft_shadow_size: Option, /// True if this light is using two-phase occlusion culling. pub occlusion_culling: bool, + pub sun_disk_angular_size: f32, + pub sun_disk_intensity: f32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -138,6 +141,8 @@ pub struct GpuDirectionalLight { cascades_overlap_proportion: f32, depth_texture_base_index: u32, decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, } // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! @@ -224,6 +229,54 @@ pub fn init_shadow_samplers(mut commands: Commands, render_device: Res, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, *query_item)); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractResource on foreign type AmbientLight +pub fn extract_ambient_light_resource( + mut commands: Commands, + main_resource: Extract>>, + target_resource: Option>, +) { + if let Some(main_resource) = main_resource.as_ref() { + if let Some(mut target_resource) = target_resource { + if main_resource.is_changed() { + *target_resource = (*main_resource).clone(); + } + } else { + commands.insert_resource((*main_resource).clone()); + } + } +} + +// This is needed because of the orphan rule not allowing implementing +// foreign trait ExtractComponent on foreign type AmbientLight +pub fn extract_ambient_light( + mut commands: Commands, + mut previous_len: Local, + query: Extract>, +) { + let mut values = Vec::with_capacity(*previous_len); + for (entity, query_item) in &query { + values.push((entity, query_item.clone())); + } + *previous_len = values.len(); + commands.try_insert_batch(values); +} + pub fn extract_lights( mut commands: Commands, point_light_shadow_map: Extract>, @@ -279,6 +332,7 @@ pub fn extract_lights( Option<&RenderLayers>, Option<&VolumetricLight>, Has, + Option<&SunDisk>, ), Without, >, @@ -460,6 +514,7 @@ pub fn extract_lights( maybe_layers, volumetric_light, occlusion_culling, + sun_disk, ) in &directional_lights { if !view_visibility.get() { @@ -526,6 +581,8 @@ pub fn extract_lights( frusta: extracted_frusta, render_layers: maybe_layers.unwrap_or_default().clone(), occlusion_culling, + sun_disk_angular_size: sun_disk.unwrap_or_default().angular_size, + sun_disk_intensity: sun_disk.unwrap_or_default().intensity, }, RenderCascadesVisibleEntities { entities: cascade_visible_entities, @@ -557,30 +614,30 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - trigger: On, + event: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(event.entity()) { v.insert(LightViewEntities::default()); } } /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - trigger: On, + event: On, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(event.entity()) { v.try_remove::(); } } pub(crate) fn remove_light_view_entities( - trigger: On, + event: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { - if let Ok(entities) = query.get(trigger.target()) { + if let Ok(entities) = query.get(event.entity()) { for v in entities.0.values() { for e in v.iter().copied() { if let Ok(mut v) = commands.get_entity(e) { @@ -1152,6 +1209,8 @@ pub fn prepare_lights( num_cascades: num_cascades as u32, cascades_overlap_proportion: light.cascade_shadow_config.overlap_proportion, depth_texture_base_index: num_directional_cascades_enabled_for_this_view as u32, + sun_disk_angular_size: light.sun_disk_angular_size, + sun_disk_intensity: light.sun_disk_intensity, decal_index: decals .as_ref() .and_then(|decals| decals.get(*light_entity)) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index b927b78f6fa38..2281366b3757c 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1822,17 +1822,18 @@ impl FromWorld for MeshPipeline { } }; - let format_size = image.texture_descriptor.format.pixel_size(); - render_queue.write_texture( - texture.as_image_copy(), - image.data.as_ref().expect("Image was created without data"), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(image.width() * format_size as u32), - rows_per_image: None, - }, - image.texture_descriptor.size, - ); + if let Ok(format_size) = image.texture_descriptor.format.pixel_size() { + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image was created without data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + } let texture_view = texture.create_view(&TextureViewDescriptor::default()); GpuImage { diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index aaf9d0ef7d7e1..19f87b37946a4 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -41,6 +41,8 @@ struct DirectionalLight { cascades_overlap_proportion: f32, depth_texture_base_index: u32, decal_index: u32, + sun_disk_angular_size: f32, + sun_disk_intensity: f32, }; const DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u << 0u; diff --git a/crates/bevy_pbr/src/render/morph.rs b/crates/bevy_pbr/src/render/morph.rs index b2dc90af02294..75ddb03414682 100644 --- a/crates/bevy_pbr/src/render/morph.rs +++ b/crates/bevy_pbr/src/render/morph.rs @@ -75,7 +75,7 @@ pub fn prepare_morphs( } const fn can_align(step: usize, target: usize) -> bool { - step % target == 0 || target % step == 0 + step.is_multiple_of(target) || target.is_multiple_of(step) } const WGPU_MIN_ALIGN: usize = 256; diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 3c69c4405f984..a78abcbf52075 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -120,6 +120,8 @@ fn pbr_input_from_standard_material( let uv_transform = pbr_bindings::material.uv_transform; #endif // BINDLESS +pbr_input.material.uv_transform = uv_transform; + #ifdef VERTEX_UVS_A var uv = (uv_transform * vec3(in.uv, 1.0)).xy; #endif diff --git a/crates/bevy_pbr/src/render/view_transformations.wgsl b/crates/bevy_pbr/src/render/view_transformations.wgsl index d21a1d534c3c3..4fe44d48dbd21 100644 --- a/crates/bevy_pbr/src/render/view_transformations.wgsl +++ b/crates/bevy_pbr/src/render/view_transformations.wgsl @@ -22,6 +22,10 @@ /// Perspective projection: 0.0 is inf far away /// Orthographic projection: 0.0 is far clipping plane +/// Clip space: +/// This is NDC before the perspective divide, still in homogenous coordinate space. +/// Dividing a clip space point by its w component yields a point in NDC space. + /// UV space: /// 0.0, 0.0 is the top left /// 1.0, 1.0 is the bottom right diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 7d1a55700e429..0c558fe2e804d 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ }; use bevy_image::{BevyDefault, Image}; use bevy_light::{FogVolume, VolumetricFog, VolumetricLight}; -use bevy_math::{vec4, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles as _}; +use bevy_math::{vec4, Affine3A, Mat4, Vec3, Vec3A, Vec4}; use bevy_mesh::{Mesh, MeshVertexBufferLayoutRef}; use bevy_render::{ diagnostic::RecordDiagnostics, @@ -698,12 +698,12 @@ pub fn prepare_volumetric_fog_uniforms( fog_volumes: Query<(Entity, &FogVolume, &GlobalTransform)>, render_device: Res, render_queue: Res, - mut local_from_world_matrices: Local>, + mut local_from_world_matrices: Local>, ) { // Do this up front to avoid O(n^2) matrix inversion. local_from_world_matrices.clear(); for (_, _, fog_transform) in fog_volumes.iter() { - local_from_world_matrices.push(fog_transform.to_matrix().inverse()); + local_from_world_matrices.push(fog_transform.affine().inverse()); } let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); @@ -715,7 +715,7 @@ pub fn prepare_volumetric_fog_uniforms( }; for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { - let world_from_view = extracted_view.world_from_view.to_matrix(); + let world_from_view = extracted_view.world_from_view.affine(); let mut view_fog_volumes = vec![]; @@ -736,7 +736,9 @@ pub fn prepare_volumetric_fog_uniforms( ); // Calculate the radius of the sphere that bounds the fog volume. - let bounding_radius = (Mat3A::from_mat4(view_from_local) * Vec3A::splat(0.5)).length(); + let bounding_radius = view_from_local + .transform_vector3a(Vec3A::splat(0.5)) + .length(); // Write out our uniform. let uniform_buffer_offset = writer.write(&VolumetricFogUniform { @@ -788,9 +790,8 @@ pub fn prepare_view_depth_textures_for_volumetric_fog( } } -fn get_far_planes(view_from_local: &Mat4) -> [Vec4; 3] { +fn get_far_planes(view_from_local: &Affine3A) -> [Vec4; 3] { let (mut far_planes, mut next_index) = ([Vec4::ZERO; 3], 0); - let view_from_normal_local = Mat3A::from_mat4(*view_from_local); for &local_normal in &[ Vec3A::X, @@ -800,13 +801,15 @@ fn get_far_planes(view_from_local: &Mat4) -> [Vec4; 3] { Vec3A::Z, Vec3A::NEG_Z, ] { - let view_normal = (view_from_normal_local * local_normal).normalize_or_zero(); + let view_normal = view_from_local + .transform_vector3a(local_normal) + .normalize_or_zero(); if view_normal.z <= 0.0 { continue; } - let view_position = *view_from_local * (-local_normal * 0.5).extend(1.0); - let plane_coords = view_normal.extend(-view_normal.dot(view_position.xyz().into())); + let view_position = view_from_local.transform_point3a(-local_normal * 0.5); + let plane_coords = view_normal.extend(-view_normal.dot(view_position)); far_planes[next_index] = plane_coords; next_index += 1; @@ -846,8 +849,9 @@ impl VolumetricFogBindGroupLayoutKey { /// Given the transform from the view to the 1×1×1 cube in local fog volume /// space, returns true if the camera is inside the volume. -fn camera_is_inside_fog_volume(local_from_view: &Mat4) -> bool { - Vec3A::from(local_from_view.col(3).xyz()) +fn camera_is_inside_fog_volume(local_from_view: &Affine3A) -> bool { + local_from_view + .translation .abs() .cmple(Vec3A::splat(0.5)) .all() @@ -858,10 +862,10 @@ fn camera_is_inside_fog_volume(local_from_view: &Mat4) -> bool { fn calculate_fog_volume_clip_from_local_transforms( interior: bool, clip_from_view: &Mat4, - view_from_local: &Mat4, + view_from_local: &Affine3A, ) -> Mat4 { if !interior { - return *clip_from_view * *view_from_local; + return *clip_from_view * Mat4::from(*view_from_local); } // If the camera is inside the fog volume, then we'll be rendering a full diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index dc09d8d37777f..ff732d7887548 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -45,7 +45,7 @@ use bevy_render::{ SetItemPipeline, TrackedRenderPass, ViewBinnedRenderPhases, }, render_resource::*, - renderer::RenderContext, + renderer::{RenderContext, RenderDevice}, sync_world::{MainEntity, MainEntityHashMap}, view::{ ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RenderVisibleEntities, @@ -55,7 +55,7 @@ use bevy_render::{ }; use bevy_shader::Shader; use core::{hash::Hash, ops::Range}; -use tracing::error; +use tracing::{error, warn}; /// A [`Plugin`] that draws wireframes. /// @@ -108,11 +108,23 @@ impl Plugin for WireframePlugin { .after(AssetEventSystems) .run_if(resource_exists::), ); + } + fn finish(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + let required_features = WgpuFeatures::POLYGON_MODE_LINE | WgpuFeatures::PUSH_CONSTANTS; + let render_device = render_app.world().resource::(); + if !render_device.features().contains(required_features) { + warn!( + "WireframePlugin not loaded. GPU lacks support for required features: {:?}.", + required_features + ); + return; + } + render_app .init_resource::() .init_resource::() diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 30e403a1129d8..26c562c98ce1a 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -99,7 +99,7 @@ impl PointerHits { #[reflect(Clone, PartialEq)] pub struct HitData { /// The camera entity used to detect this hit. Useful when you need to find the ray that was - /// casted for this hit when using a raycasting backend. + /// cast for this hit when using a raycasting backend. pub camera: Entity, /// `depth` only needs to be self-consistent with other [`PointerHits`]s using the same /// [`RenderTarget`](bevy_camera::RenderTarget). However, it is recommended to use the diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index b2f603562d2fe..97e6bfad3b56d 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: On>| { +//! .observe(|event: On>| { //! println!("I am being hovered over"); //! }); //! ``` @@ -141,7 +141,7 @@ pub struct Cancel { pub hit: HitData, } -/// Fires when a pointer crosses into the bounds of the `target` entity. +/// Fires when a pointer crosses into the bounds of the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Over { @@ -149,7 +149,7 @@ pub struct Over { pub hit: HitData, } -/// Fires when a pointer crosses out of the bounds of the `target` entity. +/// Fires when a pointer crosses out of the bounds of the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Out { @@ -157,7 +157,7 @@ pub struct Out { pub hit: HitData, } -/// Fires when a pointer button is pressed over the `target` entity. +/// Fires when a pointer button is pressed over the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Press { @@ -167,7 +167,7 @@ pub struct Press { pub hit: HitData, } -/// Fires when a pointer button is released over the `target` entity. +/// Fires when a pointer button is released over the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Release { @@ -178,7 +178,7 @@ pub struct Release { } /// Fires when a pointer sends a pointer pressed event followed by a pointer released event, with the same -/// `target` entity for both events. +/// [target entity](On::entity) for both events. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Click { @@ -190,7 +190,7 @@ pub struct Click { pub duration: Duration, } -/// Fires while a pointer is moving over the `target` entity. +/// Fires while a pointer is moving over the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Move { @@ -205,7 +205,7 @@ pub struct Move { pub delta: Vec2, } -/// Fires when the `target` entity receives a pointer pressed event followed by a pointer move event. +/// Fires when the [target entity](On::entity) receives a pointer pressed event followed by a pointer move event. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragStart { @@ -215,7 +215,7 @@ pub struct DragStart { pub hit: HitData, } -/// Fires while the `target` entity is being dragged. +/// Fires while the [target entity](On::entity) is being dragged. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Drag { @@ -237,7 +237,7 @@ pub struct Drag { pub delta: Vec2, } -/// Fires when a pointer is dragging the `target` entity and a pointer released event is received. +/// Fires when a pointer is dragging the [target entity](On::entity) and a pointer released event is received. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragEnd { @@ -252,49 +252,49 @@ pub struct DragEnd { pub distance: Vec2, } -/// Fires when a pointer dragging the `dragged` entity enters the `target` entity. +/// Fires when a pointer dragging the `dragged` entity enters the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragEnter { /// Pointer button pressed to enter drag. pub button: PointerButton, - /// The entity that was being dragged when the pointer entered the `target` entity. + /// The entity that was being dragged when the pointer entered the [target entity](On::entity). pub dragged: Entity, /// Information about the picking intersection. pub hit: HitData, } -/// Fires while the `dragged` entity is being dragged over the `target` entity. +/// Fires while the `dragged` entity is being dragged over the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragOver { /// Pointer button pressed while dragging over. pub button: PointerButton, - /// The entity that was being dragged when the pointer was over the `target` entity. + /// The entity that was being dragged when the pointer was over the [target entity](On::entity). pub dragged: Entity, /// Information about the picking intersection. pub hit: HitData, } -/// Fires when a pointer dragging the `dragged` entity leaves the `target` entity. +/// Fires when a pointer dragging the `dragged` entity leaves the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragLeave { /// Pointer button pressed while leaving drag. pub button: PointerButton, - /// The entity that was being dragged when the pointer left the `target` entity. + /// The entity that was being dragged when the pointer left the [target entity](On::entity). pub dragged: Entity, /// Information about the latest prior picking intersection. pub hit: HitData, } -/// Fires when a pointer drops the `dropped` entity onto the `target` entity. +/// Fires when a pointer drops the `dropped` entity onto the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct DragDrop { /// Pointer button released to drop. pub button: PointerButton, - /// The entity that was dropped onto the `target` entity. + /// The entity that was dropped onto the [target entity](On::entity). pub dropped: Entity, /// Information about the picking intersection. pub hit: HitData, @@ -322,7 +322,7 @@ pub struct DragEntry { pub latest_pos: Vec2, } -/// Fires while a pointer is scrolling over the `target` entity. +/// Fires while a pointer is scrolling over the [target entity](On::entity). #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] pub struct Scroll { diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 7a0e1dfb2b82d..266c0971822f9 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -17,12 +17,11 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: On>| { -//! println!("I was just clicked!"); -//! // Get the underlying pointer event data -//! let click_event: &Pointer = trigger.event(); +//! .observe(|mut event: On>| { +//! // Read the underlying pointer event data +//! println!("Pointer {:?} was just clicked!", event.pointer_id); //! // Stop the event from bubbling up the entity hierarchy -//! trigger.propagate(false); +//! event.propagate(false); //! }); //! ``` //! @@ -54,16 +53,15 @@ //! commands.spawn(Transform::default()) //! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: -//! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { -//! let mut transform = transforms.get_mut(trigger.target()).unwrap(); -//! let drag = trigger.event(); -//! transform.rotate_local_y(drag.delta.x / 50.0); +//! .observe(|event: On>, mut transforms: Query<&mut Transform>| { +//! let mut transform = transforms.get_mut(event.entity()).unwrap(); +//! transform.rotate_local_y(event.delta.x / 50.0); //! }) -//! .observe(|trigger: On>, mut commands: Commands| { -//! println!("Entity {} goes BOOM!", trigger.target()); -//! commands.entity(trigger.target()).despawn(); +//! .observe(|event: On>, mut commands: Commands| { +//! println!("Entity {} goes BOOM!", event.entity()); +//! commands.entity(event.entity()).despawn(); //! }) -//! .observe(|trigger: On>, mut events: EventWriter| { +//! .observe(|event: On>, mut events: EventWriter| { //! events.write(Greeting); //! }); //! } diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index d521fe1213a6d..24c6effab046c 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -1,4 +1,4 @@ -use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec2, Vec3, Vec3A}; +use bevy_math::{bounding::Aabb3d, Affine3A, Dir3, Ray3d, Vec2, Vec3, Vec3A}; use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; use bevy_reflect::Reflect; @@ -38,7 +38,7 @@ pub struct RayTriangleHit { /// Casts a ray on a mesh, and returns the intersection. pub(super) fn ray_intersection_over_mesh( mesh: &Mesh, - transform: &Mat4, + transform: &Affine3A, ray: Ray3d, cull: Backfaces, ) -> Option { @@ -74,7 +74,7 @@ pub(super) fn ray_intersection_over_mesh( /// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. pub fn ray_mesh_intersection( ray: Ray3d, - mesh_transform: &Mat4, + mesh_transform: &Affine3A, positions: &[[f32; 3]], vertex_normals: Option<&[[f32; 3]]>, indices: Option<&[I]>, @@ -285,7 +285,11 @@ fn ray_triangle_intersection( // In our case, the ray is transformed to model space, which could involve scaling. /// Checks if the ray intersects with the AABB of a mesh, returning the distance to the point of intersection. /// The distance is zero if the ray starts inside the AABB. -pub fn ray_aabb_intersection_3d(ray: Ray3d, aabb: &Aabb3d, model_to_world: &Mat4) -> Option { +pub fn ray_aabb_intersection_3d( + ray: Ray3d, + aabb: &Aabb3d, + model_to_world: &Affine3A, +) -> Option { // Transform the ray to model space let world_to_model = model_to_world.inverse(); let ray_direction: Vec3A = world_to_model.transform_vector3a((*ray.direction).into()); @@ -351,7 +355,7 @@ mod tests { #[test] fn ray_mesh_intersection_simple() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = None; @@ -373,7 +377,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -395,7 +399,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -418,7 +422,7 @@ mod tests { #[test] fn ray_mesh_intersection_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -441,7 +445,7 @@ mod tests { #[test] fn ray_mesh_intersection_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = None; @@ -463,7 +467,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -485,7 +489,7 @@ mod tests { #[test] fn ray_mesh_intersection_not_enough_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0]); @@ -507,7 +511,7 @@ mod tests { #[test] fn ray_mesh_intersection_bad_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.affine(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 3]); 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 b1ba933976c28..5fbcc370e1e98 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -27,7 +27,7 @@ use tracing::*; #[derive(Clone, Copy, Reflect)] #[reflect(Clone)] pub enum RayCastVisibility { - /// Completely ignore visibility checks. Hidden items can still be ray casted against. + /// Completely ignore visibility checks. Hidden items can still be ray cast against. Any, /// Only cast rays against entities that are visible in the hierarchy. See [`Visibility`](bevy_camera::visibility::Visibility). Visible, @@ -236,7 +236,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { && let Some(distance) = ray_aabb_intersection_3d( ray, &Aabb3d::new(aabb.center, aabb.half_extents), - &transform.to_matrix(), + &transform.affine(), ) { aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); @@ -290,7 +290,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { // Perform the actual ray cast. let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.to_matrix(); + let transform = transform.affine(); let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 5f95af866794d..f173c896248b0 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -66,8 +66,8 @@ spin = { version = "0.10.0", default-features = false, features = [ "lazy", "barrier", ] } -foldhash = { version = "0.1.3", default-features = false } -hashbrown = { version = "0.15.1", features = [ +foldhash = { version = "0.2.0", default-features = false } +hashbrown = { version = "0.16.0", features = [ "equivalent", "raw-entry", ], optional = true, default-features = false } diff --git a/crates/bevy_platform/src/hash.rs b/crates/bevy_platform/src/hash.rs index 3b1a836ecf83d..66814bc583623 100644 --- a/crates/bevy_platform/src/hash.rs +++ b/crates/bevy_platform/src/hash.rs @@ -22,7 +22,7 @@ const FIXED_HASHER: FixedState = #[derive(Copy, Clone, Default, Debug)] pub struct FixedHasher; impl BuildHasher for FixedHasher { - type Hasher = DefaultHasher; + type Hasher = DefaultHasher<'static>; #[inline] fn build_hasher(&self) -> Self::Hasher { diff --git a/crates/bevy_post_process/Cargo.toml b/crates/bevy_post_process/Cargo.toml new file mode 100644 index 0000000000000..6610758567f06 --- /dev/null +++ b/crates/bevy_post_process/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "bevy_post_process" +version = "0.17.0-dev" +edition = "2024" +description = "Provides post process effects for Bevy Engine." +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] +trace = [] +webgl = [] +webgpu = [] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_shader = { path = "../bevy_shader", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ + "std", + "serialize", +] } + +bitflags = "2.3" +radsort = "0.1" +nonmax = "0.5" +smallvec = { version = "1", default-features = false } +thiserror = { version = "2", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_post_process/LICENSE-APACHE b/crates/bevy_post_process/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_post_process/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_post_process/LICENSE-MIT b/crates/bevy_post_process/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_post_process/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_post_process/README.md b/crates/bevy_post_process/README.md new file mode 100644 index 0000000000000..922c9523817ca --- /dev/null +++ b/crates/bevy_post_process/README.md @@ -0,0 +1,7 @@ +# Bevy Post Process + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_core_pipeline.svg)](https://crates.io/crates/bevy_core_pipeline) +[![Downloads](https://img.shields.io/crates/d/bevy_core_pipeline.svg)](https://crates.io/crates/bevy_core_pipeline) +[![Docs](https://docs.rs/bevy_core_pipeline/badge.svg)](https://docs.rs/bevy_core_pipeline/latest/bevy_core_pipeline/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl b/crates/bevy_post_process/src/auto_exposure/auto_exposure.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/auto_exposure/auto_exposure.wgsl rename to crates/bevy_post_process/src/auto_exposure/auto_exposure.wgsl diff --git a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs b/crates/bevy_post_process/src/auto_exposure/buffers.rs similarity index 100% rename from crates/bevy_core_pipeline/src/auto_exposure/buffers.rs rename to crates/bevy_post_process/src/auto_exposure/buffers.rs diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs similarity index 99% rename from crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs rename to crates/bevy_post_process/src/auto_exposure/compensation_curve.rs index 7c2036fa72153..c7c4fcbecb174 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs @@ -86,7 +86,7 @@ impl AutoExposureCompensationCurve { /// # use bevy_asset::prelude::*; /// # use bevy_math::vec2; /// # use bevy_math::cubic_splines::*; - /// # use bevy_core_pipeline::auto_exposure::AutoExposureCompensationCurve; + /// # use bevy_post_process::auto_exposure::AutoExposureCompensationCurve; /// # let mut compensation_curves = Assets::::default(); /// let curve: Handle = compensation_curves.add( /// AutoExposureCompensationCurve::from_curve(LinearSpline::new([ diff --git a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs b/crates/bevy_post_process/src/auto_exposure/mod.rs similarity index 95% rename from crates/bevy_core_pipeline/src/auto_exposure/mod.rs rename to crates/bevy_post_process/src/auto_exposure/mod.rs index f054cd3d3a3d9..d8975aa4ec9a9 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/mod.rs +++ b/crates/bevy_post_process/src/auto_exposure/mod.rs @@ -24,12 +24,10 @@ use node::AutoExposureNode; use pipeline::{AutoExposurePass, AutoExposurePipeline, ViewAutoExposurePipeline}; pub use settings::AutoExposure; -use crate::{ - auto_exposure::{ - compensation_curve::GpuAutoExposureCompensationCurve, pipeline::init_auto_exposure_pipeline, - }, - core_3d::graph::{Core3d, Node3d}, +use crate::auto_exposure::{ + compensation_curve::GpuAutoExposureCompensationCurve, pipeline::init_auto_exposure_pipeline, }; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; /// Plugin for the auto exposure feature. /// diff --git a/crates/bevy_core_pipeline/src/auto_exposure/node.rs b/crates/bevy_post_process/src/auto_exposure/node.rs similarity index 100% rename from crates/bevy_core_pipeline/src/auto_exposure/node.rs rename to crates/bevy_post_process/src/auto_exposure/node.rs diff --git a/crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs b/crates/bevy_post_process/src/auto_exposure/pipeline.rs similarity index 100% rename from crates/bevy_core_pipeline/src/auto_exposure/pipeline.rs rename to crates/bevy_post_process/src/auto_exposure/pipeline.rs diff --git a/crates/bevy_core_pipeline/src/auto_exposure/settings.rs b/crates/bevy_post_process/src/auto_exposure/settings.rs similarity index 100% rename from crates/bevy_core_pipeline/src/auto_exposure/settings.rs rename to crates/bevy_post_process/src/auto_exposure/settings.rs diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_post_process/src/bloom/bloom.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/bloom/bloom.wgsl rename to crates/bevy_post_process/src/bloom/bloom.wgsl diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs similarity index 99% rename from crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs rename to crates/bevy_post_process/src/bloom/downsampling_pipeline.rs index 6d6fc1d4dbd2d..2e66e1d25dbaf 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_post_process/src/bloom/downsampling_pipeline.rs @@ -1,4 +1,4 @@ -use crate::FullscreenShader; +use bevy_core_pipeline::FullscreenShader; use super::{Bloom, BLOOM_TEXTURE_FORMAT}; use bevy_asset::{load_embedded_asset, AssetServer, Handle}; diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_post_process/src/bloom/mod.rs similarity index 99% rename from crates/bevy_core_pipeline/src/bloom/mod.rs rename to crates/bevy_post_process/src/bloom/mod.rs index f07675fad36c7..e0f2424ec2eb5 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_post_process/src/bloom/mod.rs @@ -5,17 +5,17 @@ mod upsampling_pipeline; use bevy_image::ToExtents; pub use settings::{Bloom, BloomCompositeMode, BloomPrefilter}; -use crate::{ - bloom::{ - downsampling_pipeline::init_bloom_downsampling_pipeline, - upsampling_pipeline::init_bloom_upscaling_pipeline, - }, - core_2d::graph::{Core2d, Node2d}, - core_3d::graph::{Core3d, Node3d}, +use crate::bloom::{ + downsampling_pipeline::init_bloom_downsampling_pipeline, + upsampling_pipeline::init_bloom_upscaling_pipeline, }; use bevy_app::{App, Plugin}; use bevy_asset::embedded_asset; use bevy_color::{Gray, LinearRgba}; +use bevy_core_pipeline::{ + core_2d::graph::{Core2d, Node2d}, + core_3d::graph::{Core3d, Node3d}, +}; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_math::{ops, UVec2}; use bevy_render::{ @@ -43,6 +43,7 @@ use upsampling_pipeline::{ const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Ufloat; +#[derive(Default)] pub struct BloomPlugin; impl Plugin for BloomPlugin { diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_post_process/src/bloom/settings.rs similarity index 99% rename from crates/bevy_core_pipeline/src/bloom/settings.rs rename to crates/bevy_post_process/src/bloom/settings.rs index 1d96d9cbb0c10..39457dac8efd2 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_post_process/src/bloom/settings.rs @@ -23,7 +23,7 @@ use bevy_render::{extract_component::ExtractComponent, view::Hdr}; /// Often used in conjunction with `bevy_pbr::StandardMaterial::emissive` for 3d meshes. /// /// Bloom is best used alongside a tonemapping function that desaturates bright colors, -/// such as [`crate::tonemapping::Tonemapping::TonyMcMapface`]. +/// such as [`bevy_core_pipeline::tonemapping::Tonemapping::TonyMcMapface`]. /// /// Bevy's implementation uses a parametric curve to blend between a set of /// blurred (lower frequency) images generated from the camera's view. diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs similarity index 99% rename from crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs rename to crates/bevy_post_process/src/bloom/upsampling_pipeline.rs index c528ffd9b4243..775ca55fb3529 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_post_process/src/bloom/upsampling_pipeline.rs @@ -1,4 +1,4 @@ -use crate::FullscreenShader; +use bevy_core_pipeline::FullscreenShader; use super::{ downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT, diff --git a/crates/bevy_core_pipeline/src/dof/dof.wgsl b/crates/bevy_post_process/src/dof/dof.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/dof/dof.wgsl rename to crates/bevy_post_process/src/dof/dof.wgsl diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_post_process/src/dof/mod.rs similarity index 99% rename from crates/bevy_core_pipeline/src/dof/mod.rs rename to crates/bevy_post_process/src/dof/mod.rs index 7f6a84beccae6..9b0077d8569a3 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_post_process/src/dof/mod.rs @@ -63,7 +63,7 @@ use bevy_utils::{default, once}; use smallvec::SmallVec; use tracing::{info, warn}; -use crate::{ +use bevy_core_pipeline::{ core_3d::{ graph::{Core3d, Node3d}, DEPTH_TEXTURE_SAMPLING_SUPPORTED, @@ -72,6 +72,7 @@ use crate::{ }; /// A plugin that adds support for the depth of field effect to Bevy. +#[derive(Default)] pub struct DepthOfFieldPlugin; /// A component that enables a [depth of field] postprocessing effect when attached to a [`Camera3d`], diff --git a/crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl b/crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/post_process/chromatic_aberration.wgsl rename to crates/bevy_post_process/src/effect_stack/chromatic_aberration.wgsl diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_post_process/src/effect_stack/mod.rs similarity index 99% rename from crates/bevy_core_pipeline/src/post_process/mod.rs rename to crates/bevy_post_process/src/effect_stack/mod.rs index 337e9ad7802d5..8ad769efaa53a 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_post_process/src/effect_stack/mod.rs @@ -44,7 +44,7 @@ use bevy_render::{ use bevy_shader::{load_shader_library, Shader}; use bevy_utils::prelude::default; -use crate::{ +use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, FullscreenShader, @@ -71,7 +71,8 @@ struct DefaultChromaticAberrationLut(Handle); /// effects. /// /// Currently, this only consists of chromatic aberration. -pub struct PostProcessingPlugin; +#[derive(Default)] +pub struct EffectStackPlugin; /// Adds colored fringes to the edges of objects in the scene. /// @@ -183,7 +184,7 @@ pub struct PostProcessingUniformBufferOffsets { #[derive(Default)] pub struct PostProcessingNode; -impl Plugin for PostProcessingPlugin { +impl Plugin for EffectStackPlugin { fn build(&self, app: &mut App) { load_shader_library!(app, "chromatic_aberration.wgsl"); diff --git a/crates/bevy_core_pipeline/src/post_process/post_process.wgsl b/crates/bevy_post_process/src/effect_stack/post_process.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/post_process/post_process.wgsl rename to crates/bevy_post_process/src/effect_stack/post_process.wgsl diff --git a/crates/bevy_post_process/src/lib.rs b/crates/bevy_post_process/src/lib.rs new file mode 100644 index 0000000000000..05241bc4c7960 --- /dev/null +++ b/crates/bevy_post_process/src/lib.rs @@ -0,0 +1,36 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] + +pub mod auto_exposure; +pub mod bloom; +pub mod dof; +pub mod effect_stack; +pub mod motion_blur; +pub mod msaa_writeback; + +use crate::{ + bloom::BloomPlugin, dof::DepthOfFieldPlugin, effect_stack::EffectStackPlugin, + motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, +}; +use bevy_app::{App, Plugin}; + +/// Adds bloom, motion blur, depth of field, and chromatic aberration support. +#[derive(Default)] +pub struct PostProcessPlugin; + +impl Plugin for PostProcessPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + MsaaWritebackPlugin, + BloomPlugin, + MotionBlurPlugin, + DepthOfFieldPlugin, + EffectStackPlugin, + )); + } +} diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_post_process/src/motion_blur/mod.rs similarity index 98% rename from crates/bevy_core_pipeline/src/motion_blur/mod.rs rename to crates/bevy_post_process/src/motion_blur/mod.rs index ff2a77ff7a29f..9026fb7e813ad 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_post_process/src/motion_blur/mod.rs @@ -2,13 +2,13 @@ //! //! Add the [`MotionBlur`] component to a camera to enable motion blur. -use crate::{ - core_3d::graph::{Core3d, Node3d}, - prepass::{DepthPrepass, MotionVectorPrepass}, -}; use bevy_app::{App, Plugin}; use bevy_asset::embedded_asset; use bevy_camera::Camera; +use bevy_core_pipeline::{ + core_3d::graph::{Core3d, Node3d}, + prepass::{DepthPrepass, MotionVectorPrepass}, +}; use bevy_ecs::{ component::Component, query::{QueryItem, With}, @@ -47,7 +47,7 @@ pub mod pipeline; /// camera. /// /// ``` -/// # use bevy_core_pipeline::motion_blur::MotionBlur; +/// # use bevy_post_process::motion_blur::MotionBlur; /// # use bevy_camera::Camera3d; /// # use bevy_ecs::prelude::*; /// # fn test(mut commands: Commands) { @@ -128,6 +128,7 @@ pub struct MotionBlurUniform { } /// Adds support for per-object motion blur to the app. See [`MotionBlur`] for details. +#[derive(Default)] pub struct MotionBlurPlugin; impl Plugin for MotionBlurPlugin { fn build(&self, app: &mut App) { diff --git a/crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl b/crates/bevy_post_process/src/motion_blur/motion_blur.wgsl similarity index 100% rename from crates/bevy_core_pipeline/src/motion_blur/motion_blur.wgsl rename to crates/bevy_post_process/src/motion_blur/motion_blur.wgsl diff --git a/crates/bevy_core_pipeline/src/motion_blur/node.rs b/crates/bevy_post_process/src/motion_blur/node.rs similarity index 98% rename from crates/bevy_core_pipeline/src/motion_blur/node.rs rename to crates/bevy_post_process/src/motion_blur/node.rs index 00e4d4df873d5..5ac04df141612 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/node.rs +++ b/crates/bevy_post_process/src/motion_blur/node.rs @@ -12,7 +12,7 @@ use bevy_render::{ view::{Msaa, ViewTarget}, }; -use crate::prepass::ViewPrepassTextures; +use bevy_core_pipeline::prepass::ViewPrepassTextures; use super::{ pipeline::{MotionBlurPipeline, MotionBlurPipelineId}, diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_post_process/src/motion_blur/pipeline.rs similarity index 99% rename from crates/bevy_core_pipeline/src/motion_blur/pipeline.rs rename to crates/bevy_post_process/src/motion_blur/pipeline.rs index 80b08dd920287..dc9dbe8f762f3 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_post_process/src/motion_blur/pipeline.rs @@ -1,5 +1,5 @@ -use crate::FullscreenShader; use bevy_asset::{load_embedded_asset, AssetServer, Handle}; +use bevy_core_pipeline::FullscreenShader; use bevy_ecs::{ component::Component, entity::Entity, diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_post_process/src/msaa_writeback.rs similarity index 99% rename from crates/bevy_core_pipeline/src/msaa_writeback.rs rename to crates/bevy_post_process/src/msaa_writeback.rs index 15f5a03c5ac6b..ddc44896fd20e 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_post_process/src/msaa_writeback.rs @@ -1,10 +1,10 @@ -use crate::{ +use bevy_app::{App, Plugin}; +use bevy_color::LinearRgba; +use bevy_core_pipeline::{ blit::{BlitPipeline, BlitPipelineKey}, core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, }; -use bevy_app::{App, Plugin}; -use bevy_color::LinearRgba; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::ExtractedCamera, @@ -18,6 +18,7 @@ use bevy_render::{ /// This enables "msaa writeback" support for the `core_2d` and `core_3d` pipelines, which can be enabled on cameras /// using [`bevy_camera::Camera::msaa_writeback`]. See the docs on that field for more information. +#[derive(Default)] pub struct MsaaWritebackPlugin; impl Plugin for MsaaWritebackPlugin { diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index d40fbbf962481..e74b3c782fd4d 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -99,8 +99,8 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # used by bevy-utils, but it also needs reflect impls -foldhash = { version = "0.1.3", default-features = false } -hashbrown = { version = "0.15.1", optional = true, default-features = false } +foldhash = { version = "0.2.0", default-features = false } +hashbrown = { version = "0.16.0", optional = true, default-features = false } # other erased-serde = { version = "0.4", default-features = false, features = [ diff --git a/crates/bevy_reflect/README.md b/crates/bevy_reflect/README.md index 8a145c7a67955..fb4669ed7442f 100644 --- a/crates/bevy_reflect/README.md +++ b/crates/bevy_reflect/README.md @@ -160,7 +160,7 @@ println!("{}", my_trait.do_thing()); // This works because the #[reflect(MyTrait)] we put on MyType informed the Reflect derive to insert a new instance // of ReflectDoThing into MyType's registration. The instance knows how to cast &dyn Reflect to &dyn DoThing, because it -// knows that &dyn Reflect should first be downcasted to &MyType, which can then be safely casted to &dyn DoThing +// knows that &dyn Reflect should first be downcast to &MyType, which can then be safely cast to &dyn DoThing ``` ## Why make this? diff --git a/crates/bevy_reflect/derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs index a0e6e444d32a9..bf7867903e84c 100644 --- a/crates/bevy_reflect/derive/src/from_reflect.rs +++ b/crates/bevy_reflect/derive/src/from_reflect.rs @@ -27,16 +27,31 @@ pub(crate) fn impl_opaque(meta: &ReflectMeta) -> proc_macro2::TokenStream { let bevy_reflect_path = meta.bevy_reflect_path(); let (impl_generics, ty_generics, where_clause) = type_path.generics().split_for_impl(); let where_from_reflect_clause = WhereClauseOptions::new(meta).extend_where_clause(where_clause); - quote! { - impl #impl_generics #bevy_reflect_path::FromReflect for #type_path #ty_generics #where_from_reflect_clause { - fn from_reflect(reflect: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { - #FQOption::Some( + + let downcast = match meta.remote_ty() { + Some(remote) => { + let remote_ty = remote.type_path(); + quote! { + ::into_wrapper( #FQClone::clone( - ::try_downcast_ref::<#type_path #ty_generics>(reflect)? + ::try_downcast_ref::<#remote_ty>(reflect)? ) ) } } + None => quote! { + #FQClone::clone( + ::try_downcast_ref::<#type_path #ty_generics>(reflect)? + ) + }, + }; + + quote! { + impl #impl_generics #bevy_reflect_path::FromReflect for #type_path #ty_generics #where_from_reflect_clause { + fn from_reflect(reflect: &dyn #bevy_reflect_path::PartialReflect) -> #FQOption { + #FQOption::Some(#downcast) + } + } } } diff --git a/crates/bevy_reflect/src/impls/foldhash.rs b/crates/bevy_reflect/src/impls/foldhash.rs index 1b0452d433603..e51fd1267064b 100644 --- a/crates/bevy_reflect/src/impls/foldhash.rs +++ b/crates/bevy_reflect/src/impls/foldhash.rs @@ -1,8 +1,8 @@ use crate::impl_type_path; -impl_type_path!(::foldhash::fast::FoldHasher); +impl_type_path!(::foldhash::fast::FoldHasher<'a>); impl_type_path!(::foldhash::fast::FixedState); impl_type_path!(::foldhash::fast::RandomState); -impl_type_path!(::foldhash::quality::FoldHasher); +impl_type_path!(::foldhash::quality::FoldHasher<'a>); impl_type_path!(::foldhash::quality::FixedState); impl_type_path!(::foldhash::quality::RandomState); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 4e2592daaeb69..477c222a345ce 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -59,7 +59,7 @@ //! This means values implementing `PartialReflect` can be dynamically constructed and introspected. //! * The `Reflect` trait, however, ensures that the interface exposed by `PartialReflect` //! on types which additionally implement `Reflect` mirrors the structure of a single Rust type. -//! * This means `dyn Reflect` trait objects can be directly downcasted to concrete types, +//! * This means `dyn Reflect` trait objects can be directly downcast to concrete types, //! where `dyn PartialReflect` trait object cannot. //! * `Reflect`, since it provides a stronger type-correctness guarantee, //! is the trait used to interact with [the type registry]. @@ -516,8 +516,8 @@ //! //! | Default | Dependencies | //! | :-----: | :-------------------------------: | -//! | ✅ | [`bevy_reflect_derive/auto_register_inventory`] | -//! | ❌ | [`bevy_reflect_derive/auto_register_static`] | +//! | ✅ | `bevy_reflect_derive/auto_register_inventory` | +//! | ❌ | `bevy_reflect_derive/auto_register_static` | //! //! These features enable automatic registration of types that derive [`Reflect`]. //! @@ -740,6 +740,14 @@ pub mod __macro_exports { pub mod auto_register { pub use super::*; + #[cfg(all( + not(feature = "auto_register_inventory"), + not(feature = "auto_register_static") + ))] + compile_error!( + "Choosing a backend is required for automatic reflect registration. Please enable either the \"auto_register_inventory\" or the \"auto_register_static\" feature." + ); + /// inventory impl #[cfg(all( not(feature = "auto_register_static"), @@ -817,6 +825,7 @@ pub mod __macro_exports { } } + #[cfg(any(feature = "auto_register_static", feature = "auto_register_inventory"))] pub use __automatic_type_registration_impl::*; } } @@ -3460,6 +3469,41 @@ bevy_reflect::tests::Test { ); } + // https://github.com/bevyengine/bevy/issues/19017 + #[test] + fn should_serialize_opaque_remote_type() { + mod external_crate { + use serde::{Deserialize, Serialize}; + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + pub struct Vector2(pub [T; 2]); + } + + #[reflect_remote(external_crate::Vector2)] + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + #[reflect(Serialize, Deserialize)] + #[reflect(opaque)] + struct Vector2Wrapper([i32; 2]); + + #[derive(Reflect, Debug, PartialEq)] + struct Point(#[reflect(remote = Vector2Wrapper)] external_crate::Vector2); + + let point = Point(external_crate::Vector2([1, 2])); + + let mut registry = TypeRegistry::new(); + registry.register::(); + registry.register::(); + + let serializer = ReflectSerializer::new(&point, ®istry); + let serialized = ron::to_string(&serializer).unwrap(); + assert_eq!(serialized, r#"{"bevy_reflect::tests::Point":((((1,2))))}"#); + + let mut deserializer = Deserializer::from_str(&serialized).unwrap(); + let reflect_deserializer = ReflectDeserializer::new(®istry); + let deserialized = reflect_deserializer.deserialize(&mut deserializer).unwrap(); + let point = ::from_reflect(&*deserialized).unwrap(); + assert_eq!(point, Point(external_crate::Vector2([1, 2]))); + } + #[cfg(feature = "auto_register")] mod auto_register_reflect { use super::*; diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 1c86d8d4d2841..8dce19a925e99 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -546,9 +546,9 @@ impl dyn Reflect { /// otherwise. /// /// The underlying value is the concrete type that is stored in this `dyn` object; - /// it can be downcasted to. In the case that this underlying value "represents" + /// it can be downcast to. In the case that this underlying value "represents" /// a different type, like the Dynamic\*\*\* types do, you can call `represents` - /// to determine what type they represent. Represented types cannot be downcasted + /// to determine what type they represent. Represented types cannot be downcast /// to, but you can use [`FromReflect`] to create a value of the represented type from them. /// /// For remote types, `T` should be the type itself rather than the wrapper type. diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index cb0bf0f097121..42d8d8be79fb7 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -303,6 +303,6 @@ impl Default for GenericTypeCell { /// /// [`Reflect::reflect_hash`]: crate::Reflect #[inline] -pub fn reflect_hasher() -> DefaultHasher { +pub fn reflect_hasher() -> DefaultHasher<'static> { FixedHasher.build_hasher() } diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 899ac8b846cad..371f763ccd3ea 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -10,7 +10,7 @@ keywords = ["bevy"] [features] default = ["http", "bevy_asset"] -http = ["dep:async-io", "dep:smol-hyper"] +http = ["dep:async-io", "dep:smol-hyper", "bevy_tasks/async-io"] bevy_asset = ["dep:bevy_asset"] [dependencies] diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 68169469df7de..b8e5813177067 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -14,7 +14,7 @@ //! //! ```json //! { -//! "method": world.get_components", +//! "method": "world.get_components", //! "id": 0, //! "params": { //! "entity": 4294967298, diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 276f91e793bf4..d2f3e1e55cf71 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -21,20 +21,9 @@ keywords = ["bevy"] # wgpu-types = { git = "https://github.com/gfx-rs/wgpu", rev = "..." } decoupled_naga = ["bevy_shader/decoupled_naga"] -# Enables compressed KTX2 UASTC texture output on the asset processor -compressed_image_saver = ["bevy_image/compressed_image_saver"] - -# Texture formats (require more than just image support) -basis-universal = ["bevy_image/basis-universal"] -exr = ["bevy_image/exr"] -hdr = ["bevy_image/hdr"] -ktx2 = ["bevy_image/ktx2"] - multi_threaded = ["bevy_tasks/multi_threaded"] -shader_format_glsl = ["bevy_shader/shader_format_glsl"] shader_format_spirv = ["bevy_shader/shader_format_spirv", "wgpu/spirv"] -shader_format_wesl = ["bevy_shader/shader_format_wesl"] # Enable SPIR-V shader passthrough spirv_shader_passthrough = ["wgpu/spirv"] @@ -43,11 +32,16 @@ spirv_shader_passthrough = ["wgpu/spirv"] # TODO: When wgpu switches to DirectX 12 instead of Vulkan by default on windows, make this a default feature statically-linked-dxc = ["wgpu/static-dxc"] +# Forces the wgpu instance to be initialized using the raw Vulkan HAL, enabling additional configuration +raw_vulkan_init = ["wgpu/vulkan"] + trace = ["profiling"] tracing-tracy = ["dep:tracy-client"] ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] +vulkan-portability = ["wgpu/vulkan-portability"] +gles = ["wgpu/gles"] detailed_trace = [] ## Adds serialization support through `serde`. serialize = ["bevy_mesh/serialize"] @@ -76,7 +70,6 @@ bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_shader = { path = "../bevy_shader", version = "0.17.0-dev" } -bevy_light = { path = "../bevy_light", optional = true, version = "0.17.0-dev" } bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", "serialize", @@ -95,7 +88,6 @@ wgpu = { version = "26", default-features = false, features = [ "dx12", "metal", "vulkan", - "gles", "naga-ir", "fragile-send-sync-non-atomic-wasm", ] } diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 35671a84e0ef9..6598c98595024 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -33,9 +33,10 @@ use crate::{ ViewSortedRenderPhases, }, render_resource::{Buffer, GpuArrayBufferable, RawBufferVec, UninitBufferVec}, - renderer::{RenderAdapter, RenderDevice, RenderQueue}, + renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}, sync_world::MainEntity, view::{ExtractedView, NoIndirectDrawing, RetainedViewEntity}, + wgpu_wrapper::WgpuWrapper, Render, RenderApp, RenderDebugFlags, RenderSystems, }; @@ -1104,9 +1105,9 @@ impl FromWorld for GpuPreprocessingSupport { // - We filter out Adreno 730 and earlier GPUs (except 720, as it's newer // than 730). // - We filter out Mali GPUs with driver versions lower than 48. - fn is_non_supported_android_device(adapter: &RenderAdapter) -> bool { - crate::get_adreno_model(adapter).is_some_and(|model| model != 720 && model <= 730) - || crate::get_mali_driver_version(adapter).is_some_and(|version| version < 48) + fn is_non_supported_android_device(adapter_info: &RenderAdapterInfo) -> bool { + crate::get_adreno_model(adapter_info).is_some_and(|model| model != 720 && model <= 730) + || crate::get_mali_driver_version(adapter_info).is_some_and(|version| version < 48) } let culling_feature_support = device.features().contains( @@ -1127,8 +1128,11 @@ impl FromWorld for GpuPreprocessingSupport { .flags .contains(DownlevelFlags::COMPUTE_SHADERS); + let adapter_info = RenderAdapterInfo(WgpuWrapper::new(adapter.get_info())); + let max_supported_mode = if device.limits().max_compute_workgroup_size_x == 0 - || is_non_supported_android_device(adapter) + || is_non_supported_android_device(&adapter_info) + || adapter_info.backend == wgpu::Backend::Gl { info!( "GPU preprocessing is not supported on this device. \ diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 8fb0dc452bf74..085e7733ad397 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -20,14 +20,15 @@ use bevy_camera::{ primitives::Frustum, visibility::{self, RenderLayers, VisibleEntities}, Camera, Camera2d, Camera3d, CameraMainTextureUsages, CameraOutputMode, CameraUpdateSystems, - ClearColor, ClearColorConfig, Exposure, NormalizedRenderTarget, Projection, RenderTargetInfo, - Viewport, + ClearColor, ClearColorConfig, Exposure, ManualTextureViewHandle, NormalizedRenderTarget, + Projection, RenderTargetInfo, Viewport, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::{ContainsEntity, Entity}, + error::BevyError, event::EventReader, lifecycle::HookContext, prelude::With, @@ -39,7 +40,7 @@ use bevy_ecs::{ world::DeferredWorld, }; use bevy_image::Image; -use bevy_math::{vec2, Mat4, URect, UVec2, UVec4, Vec2}; +use bevy_math::{uvec2, vec2, Mat4, URect, UVec2, UVec4, Vec2}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::prelude::*; use bevy_transform::components::GlobalTransform; @@ -59,7 +60,6 @@ impl Plugin for CameraPlugin { .add_plugins(( ExtractResourcePlugin::::default(), ExtractComponentPlugin::::default(), - bevy_camera::CameraPlugin, )) .add_systems(PostStartup, camera_system.in_set(CameraUpdateSystems)) .add_systems( @@ -167,7 +167,7 @@ pub trait NormalizedRenderTargetExt { resolutions: impl IntoIterator, images: &Assets, manual_texture_views: &ManualTextureViews, - ) -> Option; + ) -> Result; // Check if this render target is contained in the given changed windows or images. fn is_changed( @@ -194,6 +194,7 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { NormalizedRenderTarget::TextureView(id) => { manual_texture_views.get(id).map(|tex| &tex.texture_view) } + NormalizedRenderTarget::None { .. } => None, } } @@ -214,6 +215,7 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { NormalizedRenderTarget::TextureView(id) => { manual_texture_views.get(id).map(|tex| tex.format) } + NormalizedRenderTarget::None { .. } => None, } } @@ -222,7 +224,7 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { resolutions: impl IntoIterator, images: &Assets, manual_texture_views: &ManualTextureViews, - ) -> Option { + ) -> Result { match self { NormalizedRenderTarget::Window(window_ref) => resolutions .into_iter() @@ -230,20 +232,30 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { .map(|(_, window)| RenderTargetInfo { physical_size: window.physical_size(), scale_factor: window.resolution.scale_factor(), + }) + .ok_or(MissingRenderTargetInfoError::Window { + window: window_ref.entity(), }), - NormalizedRenderTarget::Image(image_target) => { - let image = images.get(&image_target.handle)?; - Some(RenderTargetInfo { + NormalizedRenderTarget::Image(image_target) => images + .get(&image_target.handle) + .map(|image| RenderTargetInfo { physical_size: image.size(), scale_factor: image_target.scale_factor.0, }) - } - NormalizedRenderTarget::TextureView(id) => { - manual_texture_views.get(id).map(|tex| RenderTargetInfo { + .ok_or(MissingRenderTargetInfoError::Image { + image: image_target.handle.id(), + }), + NormalizedRenderTarget::TextureView(id) => manual_texture_views + .get(id) + .map(|tex| RenderTargetInfo { physical_size: tex.size, scale_factor: 1.0, }) - } + .ok_or(MissingRenderTargetInfoError::TextureView { texture_view: *id }), + NormalizedRenderTarget::None { width, height } => Ok(RenderTargetInfo { + physical_size: uvec2(*width, *height), + scale_factor: 1.0, + }), } } @@ -261,10 +273,23 @@ impl NormalizedRenderTargetExt for NormalizedRenderTarget { changed_image_handles.contains(&image_target.handle.id()) } NormalizedRenderTarget::TextureView(_) => true, + NormalizedRenderTarget::None { .. } => false, } } } +#[derive(Debug, thiserror::Error)] +pub enum MissingRenderTargetInfoError { + #[error("RenderTarget::Window missing ({window:?}): Make sure the provided entity has a Window component.")] + Window { window: Entity }, + #[error("RenderTarget::Image missing ({image:?}): Make sure the Image's usages include RenderAssetUsages::MAIN_WORLD.")] + Image { image: AssetId }, + #[error("RenderTarget::TextureView missing ({texture_view:?}): make sure the texture view handle was not removed.")] + TextureView { + texture_view: ManualTextureViewHandle, + }, +} + /// 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 @@ -287,7 +312,7 @@ pub fn camera_system( images: Res>, manual_texture_views: Res, mut cameras: Query<(&mut Camera, &mut Projection)>, -) { +) -> Result<(), BevyError> { let primary_window = primary_window.iter().next(); let mut changed_window_ids = >::default(); @@ -320,25 +345,23 @@ pub fn camera_system( || camera.computed.old_viewport_size != viewport_size || camera.computed.old_sub_camera_view != camera.sub_camera_view) { - let new_computed_target_info = - normalized_target.get_render_target_info(windows, &images, &manual_texture_views); + let new_computed_target_info = normalized_target.get_render_target_info( + windows, + &images, + &manual_texture_views, + )?; // Check for the scale factor changing, and resize the viewport if needed. // This can happen when the window is moved between monitors with different DPIs. // Without this, the viewport will take a smaller portion of the window moved to // a higher DPI monitor. if normalized_target.is_changed(&scale_factor_changed_window_ids, &HashSet::default()) - && let (Some(new_scale_factor), Some(old_scale_factor)) = ( - new_computed_target_info - .as_ref() - .map(|info| info.scale_factor), - camera - .computed - .target_info - .as_ref() - .map(|info| info.scale_factor), - ) + && let Some(old_scale_factor) = camera + .computed + .target_info + .as_ref() + .map(|info| info.scale_factor) { - let resize_factor = new_scale_factor / old_scale_factor; + let resize_factor = new_computed_target_info.scale_factor / old_scale_factor; if let Some(ref mut viewport) = camera.viewport { let resize = |vec: UVec2| (vec.as_vec2() * resize_factor).as_uvec2(); viewport.physical_position = resize(viewport.physical_position); @@ -350,12 +373,9 @@ pub fn camera_system( // arguments due to a sudden change on the window size to a lower value. // If the size of the window is lower, the viewport will match that lower value. if let Some(viewport) = &mut camera.viewport { - let target_info = &new_computed_target_info; - if let Some(target) = target_info { - viewport.clamp_to_size(target.physical_size); - } + viewport.clamp_to_size(new_computed_target_info.physical_size); } - camera.computed.target_info = new_computed_target_info; + camera.computed.target_info = Some(new_computed_target_info); if let Some(size) = camera.logical_viewport_size() && size.x != 0.0 && size.y != 0.0 @@ -376,6 +396,7 @@ pub fn camera_system( camera.computed.old_sub_camera_view = camera.sub_camera_view; } } + Ok(()) } #[derive(Component, Debug)] diff --git a/crates/bevy_render/src/diagnostic/mod.rs b/crates/bevy_render/src/diagnostic/mod.rs index 197b9f4e7f2a3..78915a2413416 100644 --- a/crates/bevy_render/src/diagnostic/mod.rs +++ b/crates/bevy_render/src/diagnostic/mod.rs @@ -17,7 +17,7 @@ use self::internal::{ sync_diagnostics, DiagnosticsRecorder, Pass, RenderDiagnosticsMutex, WriteTimestamp, }; -use super::{RenderDevice, RenderQueue}; +use crate::renderer::{RenderDevice, RenderQueue}; /// Enables collecting render diagnostics, such as CPU/GPU elapsed time per render pass, /// as well as pipeline statistics (number of primitives, number of shader invocations, etc). diff --git a/crates/bevy_render/src/extract_impls.rs b/crates/bevy_render/src/extract_impls.rs deleted file mode 100644 index 87b854363abea..0000000000000 --- a/crates/bevy_render/src/extract_impls.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! This module exists because of the orphan rule - -use bevy_ecs::query::QueryItem; -use bevy_light::{cluster::ClusteredDecal, AmbientLight, ShadowFilteringMethod}; - -use crate::{extract_component::ExtractComponent, extract_resource::ExtractResource}; - -impl ExtractComponent for ClusteredDecal { - type QueryData = &'static Self; - type QueryFilter = (); - type Out = Self; - - fn extract_component(item: QueryItem) -> Option { - Some(item.clone()) - } -} -impl ExtractResource for AmbientLight { - type Source = Self; - - fn extract_resource(source: &Self::Source) -> Self { - source.clone() - } -} -impl ExtractComponent for AmbientLight { - type QueryData = &'static Self; - type QueryFilter = (); - type Out = Self; - - fn extract_component(item: QueryItem) -> Option { - Some(item.clone()) - } -} -impl ExtractComponent for ShadowFilteringMethod { - type QueryData = &'static Self; - type QueryFilter = (); - type Out = Self; - - fn extract_component(item: QueryItem) -> Option { - Some(*item) - } -} diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index 38934e274eabd..712479dd5326c 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -257,14 +257,13 @@ fn prepare_buffers( for (entity, readback) in handles.iter() { match readback { Readback::Texture(image) => { - if let Some(gpu_image) = gpu_images.get(image) { + if let Some(gpu_image) = gpu_images.get(image) + && let Ok(pixel_size) = gpu_image.texture_format.pixel_size() + { let layout = layout_data(gpu_image.size, gpu_image.texture_format); let buffer = buffer_pool.get( &render_device, - get_aligned_size( - gpu_image.size, - gpu_image.texture_format.pixel_size() as u32, - ) as u64, + get_aligned_size(gpu_image.size, pixel_size as u32) as u64, ); let (tx, rx) = async_channel::bounded(1); readbacks.requested.push(GpuReadback { @@ -384,15 +383,19 @@ pub(crate) const fn get_aligned_size(extent: Extent3d, pixel_size: u32) -> u32 { pub(crate) fn layout_data(extent: Extent3d, format: TextureFormat) -> TexelCopyBufferLayout { TexelCopyBufferLayout { bytes_per_row: if extent.height > 1 || extent.depth_or_array_layers > 1 { - // 1 = 1 row - Some(get_aligned_size( - Extent3d { - width: extent.width, - height: 1, - depth_or_array_layers: 1, - }, - format.pixel_size() as u32, - )) + if let Ok(pixel_size) = format.pixel_size() { + // 1 = 1 row + Some(get_aligned_size( + Extent3d { + width: extent.width, + height: 1, + depth_or_array_layers: 1, + }, + pixel_size as u32, + )) + } else { + None + } } else { None }, diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 82d55d9baf5ed..8c54629fbe0b0 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -42,8 +42,6 @@ pub mod diagnostic; pub mod erased_render_asset; pub mod experimental; pub mod extract_component; -#[cfg(feature = "bevy_light")] -mod extract_impls; pub mod extract_instances; mod extract_param; pub mod extract_resource; @@ -72,11 +70,8 @@ mod wgpu_wrapper; pub mod prelude { #[doc(hidden)] pub use crate::{ - alpha::AlphaMode, - camera::NormalizedRenderTargetExt as _, - texture::{ImagePlugin, ManualTextureViews}, - view::Msaa, - ExtractSchedule, + alpha::AlphaMode, camera::NormalizedRenderTargetExt as _, texture::ManualTextureViews, + view::Msaa, ExtractSchedule, }; } @@ -88,9 +83,10 @@ use crate::{ mesh::{MeshPlugin, MorphPlugin, RenderMesh}, render_asset::prepare_assets, render_resource::{init_empty_bind_group_layout, PipelineCache}, - renderer::{render_system, RenderInstance}, + renderer::{render_system, RenderAdapterInfo}, settings::RenderCreation, storage::StoragePlugin, + texture::TexturePlugin, view::{ViewPlugin, WindowRenderPlugin}, }; use alloc::sync::Arc; @@ -113,11 +109,9 @@ use render_asset::{ extract_render_asset_bytes_per_frame, reset_render_asset_bytes_per_frame, RenderAssetBytesPerFrame, RenderAssetBytesPerFrameLimiter, }; -use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use std::sync::Mutex; use sync_world::{despawn_temporary_render_entities, entity_sync_system, SyncWorldPlugin}; -use tracing::debug; pub use wgpu_wrapper::WgpuWrapper; /// Contains the default Bevy rendering backend based on wgpu. @@ -332,76 +326,29 @@ impl Plugin for RenderPlugin { .single(app.world()) .ok() .cloned(); + let settings = render_creation.clone(); + + #[cfg(feature = "raw_vulkan_init")] + let raw_vulkan_init_settings = app + .world_mut() + .get_resource::() + .cloned() + .unwrap_or_default(); + let async_renderer = async move { - let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + let render_resources = renderer::initialize_renderer( backends, - flags: settings.instance_flags, - memory_budget_thresholds: settings.instance_memory_budget_thresholds, - backend_options: wgpu::BackendOptions { - gl: wgpu::GlBackendOptions { - gles_minor_version: settings.gles3_minor_version, - fence_behavior: wgpu::GlFenceBehavior::Normal, - }, - dx12: wgpu::Dx12BackendOptions { - shader_compiler: settings.dx12_shader_compiler.clone(), - }, - noop: wgpu::NoopBackendOptions { enable: false }, - }, - }); - - let surface = primary_window.and_then(|wrapper| { - let maybe_handle = wrapper.0.lock().expect( - "Couldn't get the window handle in time for renderer initialization", - ); - if let Some(wrapper) = maybe_handle.as_ref() { - // SAFETY: Plugins should be set up on the main thread. - let handle = unsafe { wrapper.get_handle() }; - Some( - instance - .create_surface(handle) - .expect("Failed to create wgpu surface"), - ) - } else { - None - } - }); - - let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK_ADAPTER") - .map_or(settings.force_fallback_adapter, |v| { - !(v.is_empty() || v == "0" || v == "false") - }); - - let desired_adapter_name = std::env::var("WGPU_ADAPTER_NAME") - .as_deref() - .map_or(settings.adapter_name.clone(), |x| Some(x.to_lowercase())); - - let request_adapter_options = wgpu::RequestAdapterOptions { - power_preference: settings.power_preference, - compatible_surface: surface.as_ref(), - force_fallback_adapter, - }; - - let (device, queue, adapter_info, render_adapter) = - renderer::initialize_renderer( - &instance, - &settings, - &request_adapter_options, - desired_adapter_name, - ) - .await; - debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); - debug!("Configured wgpu adapter Features: {:#?}", device.features()); - let mut future_render_resources_inner = - future_render_resources_wrapper.lock().unwrap(); - *future_render_resources_inner = Some(RenderResources( - device, - queue, - adapter_info, - render_adapter, - RenderInstance(Arc::new(WgpuWrapper::new(instance))), - )); + primary_window, + &settings, + #[cfg(feature = "raw_vulkan_init")] + raw_vulkan_init_settings, + ) + .await; + + *future_render_resources_wrapper.lock().unwrap() = Some(render_resources); }; + // In wasm, spawn a task and detach it for execution #[cfg(target_arch = "wasm32")] bevy_tasks::IoTaskPool::get() @@ -424,6 +371,7 @@ impl Plugin for RenderPlugin { MeshPlugin, GlobalsPlugin, MorphPlugin, + TexturePlugin, BatchingPlugin { debug_flags: self.debug_flags, }, @@ -463,8 +411,9 @@ impl Plugin for RenderPlugin { if let Some(future_render_resources) = app.world_mut().remove_resource::() { - let RenderResources(device, queue, adapter_info, render_adapter, instance) = - future_render_resources.0.lock().unwrap().take().unwrap(); + let render_resources = future_render_resources.0.lock().unwrap().take().unwrap(); + let RenderResources(device, queue, adapter_info, render_adapter, instance, ..) = + render_resources; let compressed_image_format_support = CompressedImageFormatSupport( CompressedImageFormats::from_features(device.features()), @@ -478,6 +427,13 @@ impl Plugin for RenderPlugin { let render_app = app.sub_app_mut(RenderApp); + #[cfg(feature = "raw_vulkan_init")] + { + let additional_vulkan_features: renderer::raw_vulkan_init::AdditionalVulkanFeatures = + render_resources.5; + render_app.insert_resource(additional_vulkan_features); + } + render_app .insert_resource(instance) .insert_resource(PipelineCache::new( @@ -590,16 +546,15 @@ fn apply_extract_commands(render_world: &mut World) { }); } -/// If the [`RenderAdapter`] is a Qualcomm Adreno, returns its model number. +/// If the [`RenderAdapterInfo`] is a Qualcomm Adreno, returns its model number. /// /// This lets us work around hardware bugs. -pub fn get_adreno_model(adapter: &RenderAdapter) -> Option { +pub fn get_adreno_model(adapter_info: &RenderAdapterInfo) -> Option { if !cfg!(target_os = "android") { return None; } - let adapter_name = adapter.get_info().name; - let adreno_model = adapter_name.strip_prefix("Adreno (TM) ")?; + let adreno_model = adapter_info.name.strip_prefix("Adreno (TM) ")?; // Take suffixes into account (like Adreno 642L). Some( @@ -611,16 +566,15 @@ pub fn get_adreno_model(adapter: &RenderAdapter) -> Option { } /// Get the Mali driver version if the adapter is a Mali GPU. -pub fn get_mali_driver_version(adapter: &RenderAdapter) -> Option { +pub fn get_mali_driver_version(adapter_info: &RenderAdapterInfo) -> Option { if !cfg!(target_os = "android") { return None; } - let driver_name = adapter.get_info().name; - if !driver_name.contains("Mali") { + if !adapter_info.name.contains("Mali") { return None; } - let driver_info = adapter.get_info().driver_info; + let driver_info = &adapter_info.driver_info; if let Some(start_pos) = driver_info.find("v1.r") && let Some(end_pos) = driver_info[start_pos..].find('p') { diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index a5c556f9f1104..40dfebf892d54 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -99,6 +99,32 @@ fn sphere_intersects_plane_half_space( return dot(plane, sphere_center) + sphere_radius > 0.0; } +// Returns the distances along the ray to its intersections with a sphere +// centered at the origin. +// +// r: distance from the sphere center to the ray origin +// mu: cosine of the zenith angle +// sphere_radius: radius of the sphere +// +// Returns vec2(t0, t1). If there is no intersection, returns vec2(-1.0). +fn ray_sphere_intersect(r: f32, mu: f32, sphere_radius: f32) -> vec2 { + let discriminant = r * r * (mu * mu - 1.0) + sphere_radius * sphere_radius; + + // No intersection + if discriminant < 0.0 { + return vec2(-1.0); + } + + let q = -r * mu; + let sqrt_discriminant = sqrt(discriminant); + + // Return both intersection distances + return vec2( + q - sqrt_discriminant, + q + sqrt_discriminant + ); +} + // pow() but safe for NaNs/negatives fn powsafe(color: vec3, power: f32) -> vec3 { return pow(abs(color), vec3(power)) * sign(color); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index d18c3c77cf2a3..f9a834add2649 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -44,7 +44,7 @@ pub struct MeshAllocatorPlugin; /// rebinding. This resource manages these buffers. /// /// Within each slab, or hardware buffer, the underlying allocation algorithm is -/// [`offset-allocator`], a Rust port of Sebastian Aaltonen's hard-real-time C++ +/// [`offset_allocator`], a Rust port of Sebastian Aaltonen's hard-real-time C++ /// `OffsetAllocator`. Slabs start small and then grow as their contents fill /// up, up to a maximum size limit. To reduce fragmentation, vertex and index /// buffers that are too large bypass this system and receive their own buffers. diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index a2bbfec58c3c1..d8c7bc63e1ec4 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -7,8 +7,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin, PostUpdate}; -use bevy_asset::{AssetApp, AssetEventSystems, AssetId, RenderAssetUsages}; -use bevy_camera::visibility::VisibilitySystems; +use bevy_asset::{AssetApp, AssetId, RenderAssetUsages}; use bevy_ecs::{ prelude::*, system::{ @@ -25,18 +24,10 @@ pub struct MeshPlugin; impl Plugin for MeshPlugin { fn build(&self, app: &mut App) { - app.init_asset::() - .init_asset::() - .register_asset_reflect::() + app.init_asset::() // 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready .add_plugins(RenderAssetPlugin::::default()) - .add_plugins(MeshAllocatorPlugin) - .add_systems( - PostUpdate, - mark_3d_meshes_as_changed_if_their_assets_changed - .ambiguous_with(VisibilitySystems::CalculateBounds) - .before(AssetEventSystems), - ); + .add_plugins(MeshAllocatorPlugin); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -51,7 +42,7 @@ impl Plugin for MeshPlugin { pub struct MorphPlugin; impl Plugin for MorphPlugin { fn build(&self, app: &mut App) { - app.add_systems(PostUpdate, inherit_weights.in_set(InheritWeights)); + app.add_systems(PostUpdate, inherit_weights.in_set(InheritWeightSystems)); } } diff --git a/crates/bevy_render/src/render_graph/camera_driver_node.rs b/crates/bevy_render/src/render_graph/camera_driver_node.rs index dff7eaed25787..5a8dc581625ec 100644 --- a/crates/bevy_render/src/render_graph/camera_driver_node.rs +++ b/crates/bevy_render/src/render_graph/camera_driver_node.rs @@ -63,7 +63,7 @@ impl Node for CameraDriverNode { // wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()` // This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted). for (id, window) in world.resource::().iter() { - if camera_windows.contains(id) { + if camera_windows.contains(id) && render_context.has_commands() { continue; } diff --git a/crates/bevy_render/src/render_phase/rangefinder.rs b/crates/bevy_render/src/render_phase/rangefinder.rs index 4222ee134b02f..0a93651a41671 100644 --- a/crates/bevy_render/src/render_phase/rangefinder.rs +++ b/crates/bevy_render/src/render_phase/rangefinder.rs @@ -1,4 +1,4 @@ -use bevy_math::{Mat4, Vec3, Vec4}; +use bevy_math::{Affine3A, Mat4, Vec3, Vec4}; /// A distance calculator for the draw order of [`PhaseItem`](crate::render_phase::PhaseItem)s. pub struct ViewRangefinder3d { @@ -7,11 +7,11 @@ pub struct ViewRangefinder3d { impl ViewRangefinder3d { /// Creates a 3D rangefinder for a view matrix. - pub fn from_world_from_view(world_from_view: &Mat4) -> ViewRangefinder3d { + pub fn from_world_from_view(world_from_view: &Affine3A) -> ViewRangefinder3d { let view_from_world = world_from_view.inverse(); ViewRangefinder3d { - view_from_world_row_2: view_from_world.row(2), + view_from_world_row_2: Mat4::from(view_from_world).row(2), } } @@ -35,11 +35,11 @@ impl ViewRangefinder3d { #[cfg(test)] mod tests { use super::ViewRangefinder3d; - use bevy_math::{Mat4, Vec3}; + use bevy_math::{Affine3A, Mat4, Vec3}; #[test] fn distance() { - let view_matrix = Mat4::from_translation(Vec3::new(0.0, 0.0, -1.0)); + let view_matrix = Affine3A::from_translation(Vec3::new(0.0, 0.0, -1.0)); let rangefinder = ViewRangefinder3d::from_world_from_view(&view_matrix); assert_eq!(rangefinder.distance(&Mat4::IDENTITY), 1.0); assert_eq!( diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 3dfd8881ce484..de129ff880d4c 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -482,26 +482,24 @@ impl Deref for BindGroup { /// } /// /// // Materials keys are intended to be small, cheap to hash, and -/// // uniquely identify a specific material permutation, which -/// // is why they are required to be `bytemuck::Pod` and `bytemuck::Zeroable` -/// // when using the `AsBindGroup` derive macro. +/// // uniquely identify a specific material permutation. /// #[repr(C)] -/// #[derive(Copy, Clone, Hash, Eq, PartialEq, bytemuck::Pod, bytemuck::Zeroable)] +/// #[derive(Copy, Clone, Hash, Eq, PartialEq)] /// struct CoolMaterialKey { -/// is_shaded: u32, +/// is_shaded: bool, /// } /// /// impl From<&CoolMaterial> for CoolMaterialKey { /// fn from(material: &CoolMaterial) -> CoolMaterialKey { /// CoolMaterialKey { -/// is_shaded: material.is_shaded as u32, +/// is_shaded: material.is_shaded, /// } /// } /// } /// ``` pub trait AsBindGroup { /// Data that will be stored alongside the "prepared" bind group. - type Data: bytemuck::Pod + bytemuck::Zeroable + Send + Sync; + type Data: Send + Sync; type Param: SystemParam + 'static; @@ -699,6 +697,10 @@ mod test { #[test] fn texture_visibility() { + #[expect( + dead_code, + reason = "This is a derive macro compilation test. It will not be constructed." + )] #[derive(AsBindGroup)] pub struct TextureVisibilityTest { #[texture(0, visibility(all))] diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 847bb46f498af..274aa111434f6 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -244,6 +244,12 @@ pub struct DynamicBindGroupEntries<'b> { entries: Vec>, } +impl<'b> Default for DynamicBindGroupEntries<'b> { + fn default() -> Self { + Self::new() + } +} + impl<'b> DynamicBindGroupEntries<'b> { pub fn sequential(entries: impl IntoBindingArray<'b, N>) -> Self { Self { diff --git a/crates/bevy_render/src/render_resource/bind_group_layout.rs b/crates/bevy_render/src/render_resource/bind_group_layout.rs index 1ad9f70713b3f..e4fbfb448fbe8 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout.rs @@ -12,7 +12,7 @@ define_atomic_id!(BindGroupLayoutId); /// which can be cloned as needed to workaround lifetime management issues. It may be converted /// from and dereferences to wgpu's [`BindGroupLayout`](wgpu::BindGroupLayout). /// -/// Can be created via [`RenderDevice::create_bind_group_layout`](crate::RenderDevice::create_bind_group_layout). +/// Can be created via [`RenderDevice::create_bind_group_layout`](crate::renderer::RenderDevice::create_bind_group_layout). #[derive(Clone, Debug)] pub struct BindGroupLayout { id: BindGroupLayoutId, diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index e51f78cbc13e4..acd6a15032487 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -9,6 +9,7 @@ use encase::{ internal::{WriteInto, Writer}, ShaderType, }; +use thiserror::Error; use wgpu::{BindingResource, BufferAddress, BufferUsages}; use super::GpuArrayBufferable; @@ -186,25 +187,30 @@ impl RawBufferVec { /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] /// and the provided [`RenderQueue`]. /// - /// Before queuing the write, a [`reserve`](RawBufferVec::reserve) operation - /// is executed. + /// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will + /// return an error. You'll need to either reserve a new buffer which will lose data on the GPU + /// or create a new buffer and copy the old data to it. /// /// This will only write the data contained in the given range. It is useful if you only want /// to update a part of the buffer. pub fn write_buffer_range( &mut self, - device: &RenderDevice, render_queue: &RenderQueue, range: core::ops::Range, - ) { + ) -> Result<(), WriteBufferRangeError> { if self.values.is_empty() { - return; + return Err(WriteBufferRangeError::NoValuesToUpload); + } + if range.end > self.item_size * self.capacity { + return Err(WriteBufferRangeError::RangeBiggerThanBuffer); } - self.reserve(self.values.len(), device); if let Some(buffer) = &self.buffer { // Cast only the bytes we need to write let bytes: &[u8] = must_cast_slice(&self.values[range.start..range.end]); render_queue.write_buffer(buffer, (range.start * self.item_size) as u64, bytes); + Ok(()) + } else { + Err(WriteBufferRangeError::BufferNotInitialized) } } @@ -417,25 +423,30 @@ where /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] /// and the provided [`RenderQueue`]. /// - /// Before queuing the write, a [`reserve`](BufferVec::reserve) operation - /// is executed. + /// If the buffer is not initialized on the GPU or the range is bigger than the capacity it will + /// return an error. You'll need to either reserve a new buffer which will lose data on the GPU + /// or create a new buffer and copy the old data to it. /// /// This will only write the data contained in the given range. It is useful if you only want /// to update a part of the buffer. pub fn write_buffer_range( &mut self, - device: &RenderDevice, render_queue: &RenderQueue, range: core::ops::Range, - ) { + ) -> Result<(), WriteBufferRangeError> { if self.data.is_empty() { - return; + return Err(WriteBufferRangeError::NoValuesToUpload); } let item_size = u64::from(T::min_size()) as usize; - self.reserve(self.data.len() / item_size, device); + if range.end > item_size * self.capacity { + return Err(WriteBufferRangeError::RangeBiggerThanBuffer); + } if let Some(buffer) = &self.buffer { let bytes = &self.data[range.start..range.end]; render_queue.write_buffer(buffer, (range.start * item_size) as u64, bytes); + Ok(()) + } else { + Err(WriteBufferRangeError::BufferNotInitialized) } } @@ -561,3 +572,16 @@ where } } } + +/// Error returned when `write_buffer_range` fails +/// +/// See [`RawBufferVec::write_buffer_range`] [`BufferVec::write_buffer_range`] +#[derive(Debug, Eq, PartialEq, Copy, Clone, Error)] +pub enum WriteBufferRangeError { + #[error("the range is bigger than the capacity of the buffer")] + RangeBiggerThanBuffer, + #[error("the gpu buffer is not initialized")] + BufferNotInitialized, + #[error("there are no values to upload")] + NoValuesToUpload, +} diff --git a/crates/bevy_render/src/render_resource/specializer.rs b/crates/bevy_render/src/render_resource/specializer.rs index 7f2d12dac5bcb..2dc52577db690 100644 --- a/crates/bevy_render/src/render_resource/specializer.rs +++ b/crates/bevy_render/src/render_resource/specializer.rs @@ -245,7 +245,8 @@ impl Specializer for PhantomData< } macro_rules! impl_specialization_key_tuple { - ($($T:ident),*) => { + ($(#[$meta:meta])* $($T:ident),*) => { + $(#[$meta])* impl <$($T: SpecializerKey),*> SpecializerKey for ($($T,)*) { const IS_CANONICAL: bool = true $(&& <$T as SpecializerKey>::IS_CANONICAL)*; type Canonical = ($(Canonical<$T>,)*); @@ -253,8 +254,13 @@ macro_rules! impl_specialization_key_tuple { }; } -// TODO: How to we fake_variadics this? -all_tuples!(impl_specialization_key_tuple, 0, 12, T); +all_tuples!( + #[doc(fake_variadic)] + impl_specialization_key_tuple, + 0, + 12, + T +); /// A cache for variants of a resource type created by a specializer. /// At most one resource will be created for each key. diff --git a/crates/bevy_render/src/render_resource/texture.rs b/crates/bevy_render/src/render_resource/texture.rs index 035e1ecca3183..e7be4177ef02b 100644 --- a/crates/bevy_render/src/render_resource/texture.rs +++ b/crates/bevy_render/src/render_resource/texture.rs @@ -160,7 +160,7 @@ impl Deref for Sampler { /// A rendering resource for the default image sampler which is set during renderer /// initialization. /// -/// The [`ImagePlugin`](crate::texture::ImagePlugin) can be set during app initialization to change the default +/// The [`ImagePlugin`](bevy_image::ImagePlugin) can be set during app initialization to change the default /// image sampler. #[derive(Resource, Debug, Clone, Deref, DerefMut)] pub struct DefaultImageSampler(pub(crate) Sampler); diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index e06053cfd3cc0..516f48e843849 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -1,28 +1,29 @@ mod graph_runner; +#[cfg(feature = "raw_vulkan_init")] +pub mod raw_vulkan_init; mod render_device; -use crate::WgpuWrapper; -use bevy_derive::{Deref, DerefMut}; -#[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] -use bevy_tasks::ComputeTaskPool; pub use graph_runner::*; pub use render_device::*; -use tracing::{debug, error, info, info_span, warn}; use crate::{ diagnostic::{internal::DiagnosticsRecorder, RecordDiagnostics}, render_graph::RenderGraph, render_phase::TrackedRenderPass, render_resource::RenderPassDescriptor, - settings::{WgpuSettings, WgpuSettingsPriority}, + settings::{RenderResources, WgpuSettings, WgpuSettingsPriority}, view::{ExtractedWindows, ViewTarget}, + WgpuWrapper, }; use alloc::sync::Arc; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemState}; use bevy_platform::time::Instant; use bevy_time::TimeSender; +use bevy_window::RawHandleWrapperHolder; +use tracing::{debug, error, info, info_span, warn}; use wgpu::{ - Adapter, AdapterInfo, CommandBuffer, CommandEncoder, DeviceType, Instance, Queue, + Adapter, AdapterInfo, Backends, CommandBuffer, CommandEncoder, DeviceType, Instance, Queue, RequestAdapterOptions, Trace, }; @@ -171,15 +172,76 @@ fn find_adapter_by_name( /// Initializes the renderer by retrieving and preparing the GPU instance, device and queue /// for the specified backend. pub async fn initialize_renderer( - instance: &Instance, + backends: Backends, + primary_window: Option, options: &WgpuSettings, - request_adapter_options: &RequestAdapterOptions<'_, '_>, - desired_adapter_name: Option, -) -> (RenderDevice, RenderQueue, RenderAdapterInfo, RenderAdapter) { + #[cfg(feature = "raw_vulkan_init")] + raw_vulkan_init_settings: raw_vulkan_init::RawVulkanInitSettings, +) -> RenderResources { + let instance_descriptor = wgpu::InstanceDescriptor { + backends, + flags: options.instance_flags, + memory_budget_thresholds: options.instance_memory_budget_thresholds, + backend_options: wgpu::BackendOptions { + gl: wgpu::GlBackendOptions { + gles_minor_version: options.gles3_minor_version, + fence_behavior: wgpu::GlFenceBehavior::Normal, + }, + dx12: wgpu::Dx12BackendOptions { + shader_compiler: options.dx12_shader_compiler.clone(), + }, + noop: wgpu::NoopBackendOptions { enable: false }, + }, + }; + + #[cfg(not(feature = "raw_vulkan_init"))] + let instance = Instance::new(&instance_descriptor); + #[cfg(feature = "raw_vulkan_init")] + let mut additional_vulkan_features = raw_vulkan_init::AdditionalVulkanFeatures::default(); + #[cfg(feature = "raw_vulkan_init")] + let instance = raw_vulkan_init::create_raw_vulkan_instance( + &instance_descriptor, + &raw_vulkan_init_settings, + &mut additional_vulkan_features, + ); + + let surface = primary_window.and_then(|wrapper| { + let maybe_handle = wrapper + .0 + .lock() + .expect("Couldn't get the window handle in time for renderer initialization"); + if let Some(wrapper) = maybe_handle.as_ref() { + // SAFETY: Plugins should be set up on the main thread. + let handle = unsafe { wrapper.get_handle() }; + Some( + instance + .create_surface(handle) + .expect("Failed to create wgpu surface"), + ) + } else { + None + } + }); + + let force_fallback_adapter = std::env::var("WGPU_FORCE_FALLBACK_ADAPTER") + .map_or(options.force_fallback_adapter, |v| { + !(v.is_empty() || v == "0" || v == "false") + }); + + let desired_adapter_name = std::env::var("WGPU_ADAPTER_NAME") + .as_deref() + .map_or(options.adapter_name.clone(), |x| Some(x.to_lowercase())); + + let request_adapter_options = RequestAdapterOptions { + power_preference: options.power_preference, + compatible_surface: surface.as_ref(), + force_fallback_adapter, + }; + #[cfg(not(target_family = "wasm"))] let mut selected_adapter = desired_adapter_name.and_then(|adapter_name| { find_adapter_by_name( - instance, + &instance, options, request_adapter_options.compatible_surface, &adapter_name, @@ -198,7 +260,10 @@ pub async fn initialize_renderer( "Searching for adapter with options: {:?}", request_adapter_options ); - selected_adapter = instance.request_adapter(request_adapter_options).await.ok(); + selected_adapter = instance + .request_adapter(&request_adapter_options) + .await + .ok(); } let adapter = selected_adapter.expect(GPU_NOT_FOUND_ERROR_MESSAGE); @@ -364,24 +429,39 @@ pub async fn initialize_renderer( }; } - let (device, queue) = adapter - .request_device(&wgpu::DeviceDescriptor { - label: options.device_label.as_ref().map(AsRef::as_ref), - required_features: features, - required_limits: limits, - memory_hints: options.memory_hints.clone(), - // See https://github.com/gfx-rs/wgpu/issues/5974 - trace: Trace::Off, - }) - .await - .unwrap(); - let queue = Arc::new(WgpuWrapper::new(queue)); - let adapter = Arc::new(WgpuWrapper::new(adapter)); - ( + let device_descriptor = wgpu::DeviceDescriptor { + label: options.device_label.as_ref().map(AsRef::as_ref), + required_features: features, + required_limits: limits, + memory_hints: options.memory_hints.clone(), + // See https://github.com/gfx-rs/wgpu/issues/5974 + trace: Trace::Off, + }; + + #[cfg(not(feature = "raw_vulkan_init"))] + let (device, queue) = adapter.request_device(&device_descriptor).await.unwrap(); + + #[cfg(feature = "raw_vulkan_init")] + let (device, queue) = raw_vulkan_init::create_raw_device( + &adapter, + &device_descriptor, + &raw_vulkan_init_settings, + &mut additional_vulkan_features, + ) + .await + .unwrap(); + + debug!("Configured wgpu adapter Limits: {:#?}", device.limits()); + debug!("Configured wgpu adapter Features: {:#?}", device.features()); + + RenderResources( RenderDevice::from(device), - RenderQueue(queue), + RenderQueue(Arc::new(WgpuWrapper::new(queue))), RenderAdapterInfo(WgpuWrapper::new(adapter_info)), - RenderAdapter(adapter), + RenderAdapter(Arc::new(WgpuWrapper::new(adapter))), + RenderInstance(Arc::new(WgpuWrapper::new(instance))), + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features, ) } @@ -429,6 +509,10 @@ impl<'w> RenderContext<'w> { }) } + pub(crate) fn has_commands(&mut self) -> bool { + self.command_encoder.is_some() || !self.command_buffer_queue.is_empty() + } + /// Creates a new [`TrackedRenderPass`] for the context, /// configured using the provided `descriptor`. pub fn begin_tracked_render_pass<'a>( @@ -499,22 +583,24 @@ impl<'w> RenderContext<'w> { #[cfg(not(all(target_arch = "wasm32", target_feature = "atomics")))] { - let mut task_based_command_buffers = ComputeTaskPool::get().scope(|task_pool| { - for (i, queued_command_buffer) in self.command_buffer_queue.into_iter().enumerate() - { - match queued_command_buffer { - QueuedCommandBuffer::Ready(command_buffer) => { - command_buffers.push((i, command_buffer)); - } - QueuedCommandBuffer::Task(command_buffer_generation_task) => { - let render_device = self.render_device.clone(); - task_pool.spawn(async move { - (i, command_buffer_generation_task(render_device)) - }); + let mut task_based_command_buffers = + bevy_tasks::ComputeTaskPool::get().scope(|task_pool| { + for (i, queued_command_buffer) in + self.command_buffer_queue.into_iter().enumerate() + { + match queued_command_buffer { + QueuedCommandBuffer::Ready(command_buffer) => { + command_buffers.push((i, command_buffer)); + } + QueuedCommandBuffer::Task(command_buffer_generation_task) => { + let render_device = self.render_device.clone(); + task_pool.spawn(async move { + (i, command_buffer_generation_task(render_device)) + }); + } } } - } - }); + }); command_buffers.append(&mut task_based_command_buffers); } diff --git a/crates/bevy_render/src/renderer/raw_vulkan_init.rs b/crates/bevy_render/src/renderer/raw_vulkan_init.rs new file mode 100644 index 0000000000000..973056956cb09 --- /dev/null +++ b/crates/bevy_render/src/renderer/raw_vulkan_init.rs @@ -0,0 +1,148 @@ +use alloc::sync::Arc; +use bevy_ecs::resource::Resource; +use bevy_platform::collections::HashSet; +use core::any::{Any, TypeId}; +use thiserror::Error; +use wgpu::{ + hal::api::Vulkan, Adapter, Device, DeviceDescriptor, Instance, InstanceDescriptor, Queue, +}; + +/// When the `raw_vulkan_init` feature is enabled, these settings will be used to configure the raw vulkan instance. +#[derive(Resource, Default, Clone)] +pub struct RawVulkanInitSettings { + // SAFETY: this must remain private to ensure that registering callbacks is unsafe + create_instance_callbacks: Vec< + Arc< + dyn Fn( + &mut wgpu::hal::vulkan::CreateInstanceCallbackArgs, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync, + >, + >, + // SAFETY: this must remain private to ensure that registering callbacks is unsafe + create_device_callbacks: Vec< + Arc< + dyn Fn( + &mut wgpu::hal::vulkan::CreateDeviceCallbackArgs, + &wgpu::hal::vulkan::Adapter, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync, + >, + >, +} + +impl RawVulkanInitSettings { + /// Adds a new Vulkan create instance callback. See [`wgpu::hal::vulkan::Instance::init_with_callback`] for details. + /// + /// # Safety + /// - Callback must not remove features. + /// - Callback must not change anything to what the instance does not support. + pub unsafe fn add_create_instance_callback( + &mut self, + callback: impl Fn(&mut wgpu::hal::vulkan::CreateInstanceCallbackArgs, &mut AdditionalVulkanFeatures) + + Send + + Sync + + 'static, + ) { + self.create_instance_callbacks.push(Arc::new(callback)); + } + + /// Adds a new Vulkan create device callback. See [`wgpu::hal::vulkan::Adapter::open_with_callback`] for details. + /// + /// # Safety + /// - Callback must not remove features. + /// - Callback must not change anything to what the device does not support. + pub unsafe fn add_create_device_callback( + &mut self, + callback: impl Fn( + &mut wgpu::hal::vulkan::CreateDeviceCallbackArgs, + &wgpu::hal::vulkan::Adapter, + &mut AdditionalVulkanFeatures, + ) + Send + + Sync + + 'static, + ) { + self.create_device_callbacks.push(Arc::new(callback)); + } +} + +pub(crate) fn create_raw_vulkan_instance( + instance_descriptor: &InstanceDescriptor, + settings: &RawVulkanInitSettings, + additional_features: &mut AdditionalVulkanFeatures, +) -> Instance { + // SAFETY: Registering callbacks is unsafe. Callback authors promise not to remove features + // or change the instance to something it does not support + unsafe { + wgpu::hal::vulkan::Instance::init_with_callback( + &wgpu::hal::InstanceDescriptor { + name: "wgpu", + flags: instance_descriptor.flags, + memory_budget_thresholds: instance_descriptor.memory_budget_thresholds, + backend_options: instance_descriptor.backend_options.clone(), + }, + Some(Box::new(|mut args| { + for callback in &settings.create_instance_callbacks { + (callback)(&mut args, additional_features); + } + })), + ) + .map(|raw_instance| Instance::from_hal::(raw_instance)) + .unwrap_or_else(|_| Instance::new(instance_descriptor)) + } +} + +pub(crate) async fn create_raw_device( + adapter: &Adapter, + device_descriptor: &DeviceDescriptor<'_>, + settings: &RawVulkanInitSettings, + additional_features: &mut AdditionalVulkanFeatures, +) -> Result<(Device, Queue), CreateRawVulkanDeviceError> { + // SAFETY: Registering callbacks is unsafe. Callback authors promise not to remove features + // or change the adapter to something it does not support + unsafe { + let Some(raw_adapter) = adapter.as_hal::() else { + return Ok(adapter.request_device(device_descriptor).await?); + }; + let open_device = raw_adapter.open_with_callback( + device_descriptor.required_features, + &device_descriptor.memory_hints, + Some(Box::new(|mut args| { + for callback in &settings.create_device_callbacks { + (callback)(&mut args, &raw_adapter, additional_features); + } + })), + )?; + + Ok(adapter.create_device_from_hal::(open_device, device_descriptor)?) + } +} + +#[derive(Error, Debug)] +pub(crate) enum CreateRawVulkanDeviceError { + #[error(transparent)] + RequestDeviceError(#[from] wgpu::RequestDeviceError), + #[error(transparent)] + DeviceError(#[from] wgpu::hal::DeviceError), +} + +/// A list of additional Vulkan features that are supported by the current wgpu instance / adapter. This is populated +/// by callbacks defined in [`RawVulkanInitSettings`] +#[derive(Resource, Default, Clone)] +pub struct AdditionalVulkanFeatures(HashSet); + +impl AdditionalVulkanFeatures { + pub fn insert(&mut self) { + self.0.insert(TypeId::of::()); + } + + pub fn has(&self) -> bool { + self.0.contains(&TypeId::of::()) + } + + pub fn remove(&mut self) { + self.0.remove(&TypeId::of::()); + } +} diff --git a/crates/bevy_render/src/settings.rs b/crates/bevy_render/src/settings.rs index 411a21ceeb0bc..c3b284ecc13b4 100644 --- a/crates/bevy_render/src/settings.rs +++ b/crates/bevy_render/src/settings.rs @@ -27,8 +27,8 @@ pub enum WgpuSettingsPriority { /// [`Backends::VULKAN`](Backends::VULKAN) are enabled by default for non-web and the best choice /// is automatically selected. Web using the `webgl` feature uses [`Backends::GL`](Backends::GL). /// NOTE: If you want to use [`Backends::GL`](Backends::GL) in a native app on `Windows` and/or `macOS`, you must -/// use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle). This is because wgpu requires EGL to -/// create a GL context without a window and only ANGLE supports that. +/// use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and enable the `gles` feature. This is +/// because wgpu requires EGL to create a GL context without a window and only ANGLE supports that. #[derive(Clone)] pub struct WgpuSettings { pub device_label: Option>, @@ -151,6 +151,8 @@ pub struct RenderResources( pub RenderAdapterInfo, pub RenderAdapter, pub RenderInstance, + #[cfg(feature = "raw_vulkan_init")] + pub crate::renderer::raw_vulkan_init::AdditionalVulkanFeatures, ); /// An enum describing how the renderer will initialize resources. This is used when creating the [`RenderPlugin`](crate::RenderPlugin). @@ -173,8 +175,19 @@ impl RenderCreation { adapter_info: RenderAdapterInfo, adapter: RenderAdapter, instance: RenderInstance, + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features: crate::renderer::raw_vulkan_init::AdditionalVulkanFeatures, ) -> Self { - RenderResources(device, queue, adapter_info, adapter, instance).into() + RenderResources( + device, + queue, + adapter_info, + adapter, + instance, + #[cfg(feature = "raw_vulkan_init")] + additional_vulkan_features, + ) + .into() } } diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index 77643ba809052..f5cdff8fba38b 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -94,15 +94,15 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + |event: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(event.entity())); }, ); app.add_observer( - |trigger: On, + |event: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(event.entity()) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -526,15 +526,15 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: On, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + |event: On, mut pending: ResMut| { + pending.push(EntityRecord::Added(event.entity())); }, ); main_world.add_observer( - |trigger: On, + |event: On, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(event.entity()) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index 8a3ff801ecf1d..095f27eecec69 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -89,7 +89,7 @@ fn fallback_image_new( let image_dimension = dimension.compatible_texture_dimension(); let mut image = if create_texture_with_data { - let data = vec![value; format.pixel_size()]; + let data = vec![value; format.pixel_size().unwrap_or(0)]; Image::new_fill( extents, image_dimension, diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 133a4b207e2fe..3d451b5d351df 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -5,14 +5,7 @@ mod texture_attachment; mod texture_cache; pub use crate::render_resource::DefaultImageSampler; -#[cfg(feature = "compressed_image_saver")] -use bevy_image::CompressedImageSaver; -#[cfg(feature = "hdr")] -use bevy_image::HdrTextureLoader; -use bevy_image::{ - CompressedImageFormatSupport, CompressedImageFormats, Image, ImageLoader, - ImageSamplerDescriptor, -}; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageLoader, ImagePlugin}; pub use fallback_image::*; pub use gpu_image::*; pub use manual_texture_view::*; @@ -24,103 +17,26 @@ use crate::{ renderer::RenderDevice, Render, RenderApp, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::{uuid_handle, AssetApp, Assets, Handle}; +use bevy_asset::AssetApp; use bevy_ecs::prelude::*; use tracing::warn; -/// A handle to a 1 x 1 transparent white image. -/// -/// Like [`Handle::default`], this is a handle to a fallback image asset. -/// While that handle points to an opaque white 1 x 1 image, this handle points to a transparent 1 x 1 white image. -// Number randomly selected by fair WolframAlpha query. Totally arbitrary. -pub const TRANSPARENT_IMAGE_HANDLE: Handle = - uuid_handle!("d18ad97e-a322-4981-9505-44c59a4b5e46"); - -// TODO: replace Texture names with Image names? -/// Adds the [`Image`] as an asset and makes sure that they are extracted and prepared for the GPU. -pub struct ImagePlugin { - /// The default image sampler to use when [`bevy_image::ImageSampler`] is set to `Default`. - pub default_sampler: ImageSamplerDescriptor, -} - -impl Default for ImagePlugin { - fn default() -> Self { - ImagePlugin::default_linear() - } -} - -impl ImagePlugin { - /// Creates image settings with linear sampling by default. - pub fn default_linear() -> ImagePlugin { - ImagePlugin { - default_sampler: ImageSamplerDescriptor::linear(), - } - } - - /// Creates image settings with nearest sampling by default. - pub fn default_nearest() -> ImagePlugin { - ImagePlugin { - default_sampler: ImageSamplerDescriptor::nearest(), - } - } -} +#[derive(Default)] +pub struct TexturePlugin; -impl Plugin for ImagePlugin { +impl Plugin for TexturePlugin { fn build(&self, app: &mut App) { - #[cfg(feature = "exr")] - { - app.init_asset_loader::(); - } - - #[cfg(feature = "hdr")] - { - app.init_asset_loader::(); - } - app.add_plugins(( RenderAssetPlugin::::default(), ExtractResourcePlugin::::default(), )) - .init_resource::() - .init_asset::() - .register_asset_reflect::(); - - let mut image_assets = app.world_mut().resource_mut::>(); - - image_assets - .insert(&Handle::default(), Image::default()) - .unwrap(); - image_assets - .insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()) - .unwrap(); - - #[cfg(feature = "compressed_image_saver")] - if let Some(processor) = app - .world() - .get_resource::() - { - processor.register_processor::, - CompressedImageSaver, - >>(CompressedImageSaver.into()); - processor.set_default_processor::, - CompressedImageSaver, - >>("png"); - } - + .init_resource::(); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.init_resource::().add_systems( Render, update_texture_cache_system.in_set(RenderSystems::Cleanup), ); } - - if !ImageLoader::SUPPORTED_FILE_EXTENSIONS.is_empty() { - app.preregister_asset_loader::(ImageLoader::SUPPORTED_FILE_EXTENSIONS); - } } fn finish(&self, app: &mut App) { @@ -137,11 +53,14 @@ impl Plugin for ImagePlugin { app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); } + let default_sampler = app.get_added_plugins::()[0] + .default_sampler + .clone(); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { let default_sampler = { let device = render_app.world().resource::(); - device.create_sampler(&self.default_sampler.as_wgpu()) + device.create_sampler(&default_sampler.as_wgpu()) }; render_app .insert_resource(DefaultImageSampler(default_sampler)) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 04482066f6d4b..ea95b7fad725b 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -307,7 +307,7 @@ pub struct ExtractedView { impl ExtractedView { /// Creates a 3D rangefinder for a view pub fn rangefinder3d(&self) -> ViewRangefinder3d { - ViewRangefinder3d::from_world_from_view(&self.world_from_view.to_matrix()) + ViewRangefinder3d::from_world_from_view(&self.world_from_view.affine()) } } diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 0d0efb75b556e..23ded53f40f91 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -84,6 +84,10 @@ struct View { /// Perspective projection: 0.0 is inf far away /// Orthographic projection: 0.0 is far clipping plane +/// Clip space: +/// This is NDC before the perspective divide, still in homogenous coordinate space. +/// Dividing a clip space point by its w component yields a point in NDC space. + /// UV space: /// 0.0, 0.0 is the top left /// 1.0, 1.0 is the bottom right diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index e682829bf4a07..ed2367d27b8e9 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -205,10 +205,10 @@ impl WindowSurfaces { /// `DirectX 11` is not supported by wgpu 0.12 and so if your GPU/drivers do not support Vulkan, /// it may be that a software renderer called "Microsoft Basic Render Driver" using `DirectX 12` /// will be chosen and performance will be very poor. This is visible in a log message that is -/// output during renderer initialization. Future versions of wgpu will support `DirectX 11`, but -/// 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. +/// output during renderer initialization. +/// Another alternative is to try to use [`ANGLE`](https://github.com/gfx-rs/wgpu#angle) and +/// [`Backends::GL`](crate::settings::Backends::GL) with the `gles` feature enabled if your +/// GPU/drivers support `OpenGL 4.3` / `OpenGL ES 3.0` or later. pub fn prepare_windows( mut windows: ResMut, mut window_surfaces: ResMut, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index b87d76252c557..19311f796d671 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -124,8 +124,8 @@ struct RenderScreenshotsSender(Sender<(Entity, Image)>); /// Saves the captured screenshot to disk at the provided path. pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); - move |trigger| { - let img = trigger.event().deref().clone(); + move |event| { + let img = event.0.clone(); match img.try_into_dynamic() { Ok(dyn_img) => match image::ImageFormat::from_path(&path) { Ok(format) => { @@ -336,6 +336,9 @@ fn prepare_screenshots( OutputColorAttachment::new(texture_view.clone(), format.add_srgb_suffix()), ); } + NormalizedRenderTarget::None { .. } => { + // Nothing to screenshot! + } } } } @@ -363,7 +366,7 @@ fn prepare_screenshot_state( let texture_view = texture.create_view(&Default::default()); let buffer = render_device.create_buffer(&wgpu::BufferDescriptor { label: Some("screenshot-transfer-buffer"), - size: gpu_readback::get_aligned_size(size, format.pixel_size() as u32) as u64, + size: gpu_readback::get_aligned_size(size, format.pixel_size().unwrap_or(0) as u32) as u64, usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, mapped_at_creation: false, }); @@ -558,6 +561,9 @@ pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEnc texture_view, ); } + NormalizedRenderTarget::None { .. } => { + // Nothing to screenshot! + } }; } } @@ -623,7 +629,9 @@ pub(crate) fn collect_screenshots(world: &mut World) { let width = prepared.size.width; let height = prepared.size.height; let texture_format = prepared.texture.format(); - let pixel_size = texture_format.pixel_size(); + let Ok(pixel_size) = texture_format.pixel_size() else { + continue; + }; let buffer = prepared.buffer.clone(); let finish = async move { diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index e6740090c65a4..f9ef1616ee4e6 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -56,7 +56,7 @@ impl DynamicScene { DynamicSceneBuilder::from_world(world) .extract_entities( // we do this instead of a query, in order to completely sidestep default query filters. - // while we could use `Allows<_>`, this wouldn't account for custom disabled components + // while we could use `Allow<_>`, this wouldn't account for custom disabled components world .archetypes() .iter() diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 07b5da4ec580c..8908fc466ff64 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -122,7 +122,7 @@ mod tests { entity::Entity, entity_disabling::Internal, hierarchy::{ChildOf, Children}, - query::Allows, + query::Allow, reflect::{AppTypeRegistry, ReflectComponent}, world::World, }; @@ -196,6 +196,10 @@ mod tests { .unwrap(); app.update(); + // TODO: multiple updates to avoid debounced asset events. See comment on SceneSpawner::debounced_scene_asset_events + app.update(); + app.update(); + app.update(); let child_root = app .world() @@ -307,7 +311,7 @@ mod tests { .insert_resource(world.resource::().clone()); let entities: Vec = scene .world - .query_filtered::>() + .query_filtered::>() .iter(&scene.world) .collect(); DynamicSceneBuilder::from_world(&scene.world) @@ -336,6 +340,10 @@ mod tests { .unwrap(); app.update(); + // TODO: multiple updates to avoid debounced asset events. See comment on SceneSpawner::debounced_scene_asset_events + app.update(); + app.update(); + app.update(); let child_root = app .world() diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 386e81080a106..f3a9ab7e149de 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -82,7 +82,18 @@ pub struct SceneSpawner { pub(crate) spawned_dynamic_scenes: HashMap, HashSet>, spawned_instances: HashMap, scene_asset_event_reader: EventCursor>, + // TODO: temp fix for https://github.com/bevyengine/bevy/issues/12756 effect on scenes + // To handle scene hot reloading, they are unloaded/reloaded on asset modifications. + // When loading several subassets of a scene as is common with gltf, they each trigger a complete asset load, + // and each will trigger either a created or modified event for the parent asset. This causes the scene to be + // unloaded, losing its initial setup, and reloaded without it. + // Debouncing scene asset events let us ignore events that happen less than SCENE_ASSET_AGE_THRESHOLD frames + // apart and not reload the scene in those cases as it's unlikely to be an actual asset change. + debounced_scene_asset_events: HashMap, u32>, dynamic_scene_asset_event_reader: EventCursor>, + // TODO: temp fix for https://github.com/bevyengine/bevy/issues/12756 effect on scenes + // See debounced_scene_asset_events + debounced_dynamic_scene_asset_events: HashMap, u32>, scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, dynamic_scenes_to_spawn: Vec<(Handle, InstanceId, Option)>, scenes_to_despawn: Vec>, @@ -552,10 +563,21 @@ pub fn scene_spawner_system(world: &mut World) { .scene_asset_event_reader .read(scene_asset_events) { - if let AssetEvent::Modified { id } = event - && scene_spawner.spawned_scenes.contains_key(id) - { - updated_spawned_scenes.push(*id); + match event { + AssetEvent::Added { id } => { + scene_spawner.debounced_scene_asset_events.insert(*id, 0); + } + AssetEvent::Modified { id } => { + if scene_spawner + .debounced_scene_asset_events + .insert(*id, 0) + .is_none() + && scene_spawner.spawned_scenes.contains_key(id) + { + updated_spawned_scenes.push(*id); + } + } + _ => {} } } let mut updated_spawned_dynamic_scenes = Vec::new(); @@ -563,10 +585,23 @@ pub fn scene_spawner_system(world: &mut World) { .dynamic_scene_asset_event_reader .read(dynamic_scene_asset_events) { - if let AssetEvent::Modified { id } = event - && scene_spawner.spawned_dynamic_scenes.contains_key(id) - { - updated_spawned_dynamic_scenes.push(*id); + match event { + AssetEvent::Added { id } => { + scene_spawner + .debounced_dynamic_scene_asset_events + .insert(*id, 0); + } + AssetEvent::Modified { id } => { + if scene_spawner + .debounced_dynamic_scene_asset_events + .insert(*id, 0) + .is_none() + && scene_spawner.spawned_dynamic_scenes.contains_key(id) + { + updated_spawned_dynamic_scenes.push(*id); + } + } + _ => {} } } @@ -582,6 +617,40 @@ pub fn scene_spawner_system(world: &mut World) { .update_spawned_dynamic_scenes(world, &updated_spawned_dynamic_scenes) .unwrap(); scene_spawner.trigger_scene_ready_events(world); + + const SCENE_ASSET_AGE_THRESHOLD: u32 = 2; + for asset_id in scene_spawner.debounced_scene_asset_events.clone().keys() { + let age = scene_spawner + .debounced_scene_asset_events + .get(asset_id) + .unwrap(); + if *age > SCENE_ASSET_AGE_THRESHOLD { + scene_spawner.debounced_scene_asset_events.remove(asset_id); + } else { + scene_spawner + .debounced_scene_asset_events + .insert(*asset_id, *age + 1); + } + } + for asset_id in scene_spawner + .debounced_dynamic_scene_asset_events + .clone() + .keys() + { + let age = scene_spawner + .debounced_dynamic_scene_asset_events + .get(asset_id) + .unwrap(); + if *age > SCENE_ASSET_AGE_THRESHOLD { + scene_spawner + .debounced_dynamic_scene_asset_events + .remove(asset_id); + } else { + scene_spawner + .debounced_dynamic_scene_asset_events + .insert(*asset_id, *age + 1); + } + } }); } @@ -811,21 +880,21 @@ mod tests { fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( - move |trigger: On, + move |event: On, scene_spawner: Res, mut trigger_count: ResMut| { assert_eq!( - trigger.event().instance_id, + event.event().instance_id, scene_id, "`SceneInstanceReady` contains the wrong `InstanceId`" ); assert_eq!( - trigger.target(), + event.entity(), scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); assert!( - scene_spawner.instance_is_ready(trigger.event().instance_id), + scene_spawner.instance_is_ready(event.event().instance_id), "`InstanceId` is not ready" ); trigger_count.0 += 1; diff --git a/crates/bevy_shader/Cargo.toml b/crates/bevy_shader/Cargo.toml index f5df411739f35..0be59145d8d63 100644 --- a/crates/bevy_shader/Cargo.toml +++ b/crates/bevy_shader/Cargo.toml @@ -10,7 +10,6 @@ keywords = ["bevy", "shader"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" } diff --git a/crates/bevy_shader/src/shader.rs b/crates/bevy_shader/src/shader.rs index 3470e8c617e09..932de7b98ceff 100644 --- a/crates/bevy_shader/src/shader.rs +++ b/crates/bevy_shader/src/shader.rs @@ -429,8 +429,10 @@ impl ShaderImport { } /// A reference to a shader asset. +#[derive(Default)] pub enum ShaderRef { /// Use the "default" shader for the current context. + #[default] Default, /// A handle to a shader stored in the [`Assets`](bevy_asset::Assets) resource Handle(Handle), diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml index 1b80c65c8a8c9..473d189ee19a7 100644 --- a/crates/bevy_solari/Cargo.toml +++ b/crates/bevy_solari/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["bevy"] [dependencies] # bevy +bevy_anti_alias = { path = "../bevy_anti_alias", version = "0.17.0-dev" } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } @@ -36,6 +37,10 @@ bytemuck = { version = "1" } derive_more = { version = "2", default-features = false, features = ["from"] } tracing = { version = "0.1", default-features = false, features = ["std"] } +[features] +dlss = ["bevy_anti_alias/dlss"] +force_disable_dlss = ["bevy_anti_alias/force_disable_dlss"] + [lints] workspace = true diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs index 0ad14fd13315d..6aaea2180fe0b 100644 --- a/crates/bevy_solari/src/lib.rs +++ b/crates/bevy_solari/src/lib.rs @@ -47,7 +47,7 @@ impl PluginGroup for SolariPlugins { } impl SolariPlugins { - /// [`WgpuFeatures`] required for this plugin to function. + /// [`WgpuFeatures`] required for these plugins to function. pub fn required_wgpu_features() -> WgpuFeatures { WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE | WgpuFeatures::EXPERIMENTAL_RAY_QUERY diff --git a/crates/bevy_solari/src/realtime/mod.rs b/crates/bevy_solari/src/realtime/mod.rs index bd810d459e7ab..04e3528e63004 100644 --- a/crates/bevy_solari/src/realtime/mod.rs +++ b/crates/bevy_solari/src/realtime/mod.rs @@ -39,6 +39,9 @@ impl Plugin for SolariLightingPlugin { embedded_asset!(app, "world_cache_compact.wgsl"); embedded_asset!(app, "world_cache_update.wgsl"); + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + embedded_asset!(app, "resolve_dlss_rr_textures.wgsl"); + app.insert_resource(DefaultOpaqueRendererMethod::deferred()); } @@ -54,6 +57,7 @@ impl Plugin for SolariLightingPlugin { ); return; } + render_app .add_systems(ExtractSchedule, extract_solari_lighting) .add_systems( @@ -66,7 +70,11 @@ impl Plugin for SolariLightingPlugin { ) .add_render_graph_edges( Core3d, - (Node3d::EndMainPass, node::graph::SolariLightingNode), + ( + Node3d::EndPrepasses, + node::graph::SolariLightingNode, + Node3d::EndMainPass, + ), ); } } diff --git a/crates/bevy_solari/src/realtime/node.rs b/crates/bevy_solari/src/realtime/node.rs index 02ec72e729bab..1de282ed4192e 100644 --- a/crates/bevy_solari/src/realtime/node.rs +++ b/crates/bevy_solari/src/realtime/node.rs @@ -3,6 +3,8 @@ use super::{ SolariLighting, }; use crate::scene::RaytracingSceneBindings; +#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] +use bevy_anti_alias::dlss::ViewDlssRayReconstructionTextures; use bevy_asset::{load_embedded_asset, Handle}; use bevy_core_pipeline::prepass::{ PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms, ViewPrepassTextures, @@ -40,6 +42,8 @@ pub mod graph { pub struct SolariLightingNode { bind_group_layout: BindGroupLayout, bind_group_layout_world_cache_active_cells_dispatch: BindGroupLayout, + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + bind_group_layout_resolve_dlss_rr_textures: BindGroupLayout, decay_world_cache_pipeline: CachedComputePipelineId, compact_world_cache_single_block_pipeline: CachedComputePipelineId, compact_world_cache_blocks_pipeline: CachedComputePipelineId, @@ -51,9 +55,12 @@ pub struct SolariLightingNode { di_spatial_and_shade_pipeline: CachedComputePipelineId, gi_initial_and_temporal_pipeline: CachedComputePipelineId, gi_spatial_and_shade_pipeline: CachedComputePipelineId, + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + resolve_dlss_rr_textures_pipeline: CachedComputePipelineId, } impl ViewNode for SolariLightingNode { + #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))] type ViewQuery = ( &'static SolariLighting, &'static SolariLightingResources, @@ -62,12 +69,22 @@ impl ViewNode for SolariLightingNode { &'static ViewUniformOffset, &'static PreviousViewUniformOffset, ); + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + type ViewQuery = ( + &'static SolariLighting, + &'static SolariLightingResources, + &'static ViewTarget, + &'static ViewPrepassTextures, + &'static ViewUniformOffset, + &'static PreviousViewUniformOffset, + Option<&'static ViewDlssRayReconstructionTextures>, + ); fn run( &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext, - ( + #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))] ( solari_lighting, solari_lighting_resources, view_target, @@ -75,6 +92,15 @@ impl ViewNode for SolariLightingNode { view_uniform_offset, previous_view_uniform_offset, ): QueryItem, + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] ( + solari_lighting, + solari_lighting_resources, + view_target, + view_prepass_textures, + view_uniform_offset, + previous_view_uniform_offset, + view_dlss_rr_textures, + ): QueryItem, world: &World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -123,6 +149,12 @@ impl ViewNode for SolariLightingNode { else { return Ok(()); }; + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + let Some(resolve_dlss_rr_textures_pipeline) = + pipeline_cache.get_compute_pipeline(self.resolve_dlss_rr_textures_pipeline) + else { + return Ok(()); + }; let s = solari_lighting_resources; let bind_group = render_context.render_device().create_bind_group( @@ -160,6 +192,19 @@ impl ViewNode for SolariLightingNode { &self.bind_group_layout_world_cache_active_cells_dispatch, &BindGroupEntries::single(s.world_cache_active_cells_dispatch.as_entire_binding()), ); + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + let bind_group_resolve_dlss_rr_textures = view_dlss_rr_textures.map(|d| { + render_context.render_device().create_bind_group( + "solari_lighting_bind_group_resolve_dlss_rr_textures", + &self.bind_group_layout_resolve_dlss_rr_textures, + &BindGroupEntries::sequential(( + &d.diffuse_albedo.default_view, + &d.specular_albedo.default_view, + &d.normal_roughness.default_view, + &d.specular_motion_vectors.default_view, + )), + ) + }); // Choice of number here is arbitrary let frame_index = frame_count.0.wrapping_mul(5782582); @@ -185,6 +230,14 @@ impl ViewNode for SolariLightingNode { previous_view_uniform_offset.offset, ], ); + + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + if let Some(bind_group_resolve_dlss_rr_textures) = bind_group_resolve_dlss_rr_textures { + pass.set_bind_group(2, &bind_group_resolve_dlss_rr_textures, &[]); + pass.set_pipeline(resolve_dlss_rr_textures_pipeline); + pass.dispatch_workgroups(dx, dy, 1); + } + pass.set_bind_group(2, &bind_group_world_cache_active_cells_dispatch, &[]); pass.set_pipeline(decay_world_cache_pipeline); @@ -333,6 +386,20 @@ impl FromWorld for SolariLightingNode { ), ); + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + let bind_group_layout_resolve_dlss_rr_textures = render_device.create_bind_group_layout( + "solari_lighting_bind_group_layout_resolve_dlss_rr_textures", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::Rgba8Unorm, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::Rgba8Unorm, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::Rgba16Float, StorageTextureAccess::WriteOnly), + texture_storage_2d(TextureFormat::Rg16Float, StorageTextureAccess::WriteOnly), + ), + ), + ); + let create_pipeline = |label: &'static str, entry_point: &'static str, shader: Handle, @@ -370,6 +437,9 @@ impl FromWorld for SolariLightingNode { bind_group_layout: bind_group_layout.clone(), bind_group_layout_world_cache_active_cells_dispatch: bind_group_layout_world_cache_active_cells_dispatch.clone(), + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + bind_group_layout_resolve_dlss_rr_textures: bind_group_layout_resolve_dlss_rr_textures + .clone(), decay_world_cache_pipeline: create_pipeline( "solari_lighting_decay_world_cache_pipeline", "decay_world_cache", @@ -447,6 +517,14 @@ impl FromWorld for SolariLightingNode { None, vec![], ), + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + resolve_dlss_rr_textures_pipeline: create_pipeline( + "solari_lighting_resolve_dlss_rr_textures_pipeline", + "resolve_dlss_rr_textures", + load_embedded_asset!(world, "resolve_dlss_rr_textures.wgsl"), + Some(&bind_group_layout_resolve_dlss_rr_textures), + vec![], + ), } } } diff --git a/crates/bevy_solari/src/realtime/prepare.rs b/crates/bevy_solari/src/realtime/prepare.rs index 68e8af9dd26b4..e4d13d3c72425 100644 --- a/crates/bevy_solari/src/realtime/prepare.rs +++ b/crates/bevy_solari/src/realtime/prepare.rs @@ -1,6 +1,12 @@ use super::SolariLighting; +#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] +use bevy_anti_alias::dlss::{ + Dlss, DlssRayReconstructionFeature, ViewDlssRayReconstructionTextures, +}; use bevy_camera::MainPassResolutionOverride; use bevy_core_pipeline::{core_3d::CORE_3D_DEPTH_FORMAT, deferred::DEFERRED_PREPASS_FORMAT}; +#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] +use bevy_ecs::query::Has; use bevy_ecs::{ component::Component, entity::Entity, @@ -9,6 +15,8 @@ use bevy_ecs::{ }; use bevy_image::ToExtents; use bevy_math::UVec2; +#[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] +use bevy_render::texture::CachedTexture; use bevy_render::{ camera::ExtractedCamera, render_resource::{ @@ -58,19 +66,35 @@ pub struct SolariLightingResources { } pub fn prepare_solari_lighting_resources( - query: Query< + #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))] query: Query< + ( + Entity, + &ExtractedCamera, + Option<&SolariLightingResources>, + Option<&MainPassResolutionOverride>, + ), + With, + >, + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] query: Query< ( Entity, &ExtractedCamera, Option<&SolariLightingResources>, Option<&MainPassResolutionOverride>, + Has>, ), With, >, render_device: Res, mut commands: Commands, ) { - for (entity, camera, solari_lighting_resources, resolution_override) in &query { + for query_item in &query { + #[cfg(any(not(feature = "dlss"), feature = "force_disable_dlss"))] + let (entity, camera, solari_lighting_resources, resolution_override) = query_item; + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + let (entity, camera, solari_lighting_resources, resolution_override, has_dlss_rr) = + query_item; + let Some(mut view_size) = camera.physical_viewport_size else { continue; }; @@ -241,5 +265,80 @@ pub fn prepare_solari_lighting_resources( world_cache_active_cells_dispatch, view_size, }); + + #[cfg(all(feature = "dlss", not(feature = "force_disable_dlss")))] + if has_dlss_rr { + let diffuse_albedo = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_diffuse_albedo"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }); + let diffuse_albedo_view = diffuse_albedo.create_view(&TextureViewDescriptor::default()); + + let specular_albedo = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_specular_albedo"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }); + let specular_albedo_view = + specular_albedo.create_view(&TextureViewDescriptor::default()); + + let normal_roughness = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_normal_roughness"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Float, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }); + let normal_roughness_view = + normal_roughness.create_view(&TextureViewDescriptor::default()); + + let specular_motion_vectors = render_device.create_texture(&TextureDescriptor { + label: Some("solari_lighting_specular_motion_vectors"), + size: view_size.to_extents(), + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rg16Float, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::STORAGE_BINDING, + view_formats: &[], + }); + let specular_motion_vectors_view = + specular_motion_vectors.create_view(&TextureViewDescriptor::default()); + + commands + .entity(entity) + .insert(ViewDlssRayReconstructionTextures { + diffuse_albedo: CachedTexture { + texture: diffuse_albedo, + default_view: diffuse_albedo_view, + }, + specular_albedo: CachedTexture { + texture: specular_albedo, + default_view: specular_albedo_view, + }, + normal_roughness: CachedTexture { + texture: normal_roughness, + default_view: normal_roughness_view, + }, + specular_motion_vectors: CachedTexture { + texture: specular_motion_vectors, + default_view: specular_motion_vectors_view, + }, + }); + } } } diff --git a/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl b/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl new file mode 100644 index 0000000000000..007498968bfb0 --- /dev/null +++ b/crates/bevy_solari/src/realtime/resolve_dlss_rr_textures.wgsl @@ -0,0 +1,28 @@ +#import bevy_pbr::pbr_deferred_types::unpack_24bit_normal +#import bevy_pbr::utils::octahedral_decode +#import bevy_render::view::View + +@group(1) @binding(7) var gbuffer: texture_2d; +@group(1) @binding(12) var view: View; + +@group(2) @binding(0) var diffuse_albedo: texture_storage_2d; +@group(2) @binding(1) var specular_albedo: texture_storage_2d; +@group(2) @binding(2) var normal_roughness: texture_storage_2d; +@group(2) @binding(3) var specular_motion_vectors: texture_storage_2d; + +@compute @workgroup_size(8, 8, 1) +fn resolve_dlss_rr_textures(@builtin(global_invocation_id) global_id: vec3) { + let pixel_id = global_id.xy; + if any(pixel_id >= vec2u(view.main_pass_viewport.zw)) { return; } + + let gpixel = textureLoad(gbuffer, pixel_id, 0); + let base_rough = unpack4x8unorm(gpixel.r); + let base_color = pow(base_rough.rgb, vec3(2.2)); + let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let perceptual_roughness = base_rough.a; + + textureStore(diffuse_albedo, pixel_id, vec4(base_color, 0.0)); + textureStore(specular_albedo, pixel_id, vec4(0.0)); // TODO + textureStore(normal_roughness, pixel_id, vec4(world_normal, perceptual_roughness)); + textureStore(specular_motion_vectors, pixel_id, vec4(0.0)); // TODO +} diff --git a/crates/bevy_solari/src/realtime/restir_gi.wgsl b/crates/bevy_solari/src/realtime/restir_gi.wgsl index f4043b685b6d9..66ececed93a16 100644 --- a/crates/bevy_solari/src/realtime/restir_gi.wgsl +++ b/crates/bevy_solari/src/realtime/restir_gi.wgsl @@ -3,7 +3,6 @@ #import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance #import bevy_pbr::pbr_deferred_types::unpack_24bit_normal #import bevy_pbr::prepass_bindings::PreviousViewUniforms -#import bevy_pbr::rgb9e5::rgb9e5_to_vec3_ #import bevy_pbr::utils::{rand_f, sample_uniform_hemisphere, uniform_hemisphere_inverse_pdf, sample_disk, octahedral_decode} #import bevy_render::maths::PI #import bevy_render::view::View @@ -25,7 +24,7 @@ struct PushConstants { frame_index: u32, reset: u32 } var constants: PushConstants; const SPATIAL_REUSE_RADIUS_PIXELS = 30.0; -const CONFIDENCE_WEIGHT_CAP = 30.0; +const CONFIDENCE_WEIGHT_CAP = 8.0; @compute @workgroup_size(8, 8, 1) fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { @@ -42,12 +41,15 @@ fn initial_and_temporal(@builtin(global_invocation_id) global_id: vec3) { let gpixel = textureLoad(gbuffer, global_id.xy, 0); let world_position = reconstruct_world_position(global_id.xy, depth); let world_normal = octahedral_decode(unpack_24bit_normal(gpixel.a)); + let base_color = pow(unpack4x8unorm(gpixel.r).rgb, vec3(2.2)); + let diffuse_brdf = base_color / PI; let initial_reservoir = generate_initial_reservoir(world_position, world_normal, &rng); - let temporal_reservoir = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); - let combined_reservoir = merge_reservoirs(initial_reservoir, temporal_reservoir, &rng); + let temporal = load_temporal_reservoir(global_id.xy, depth, world_position, world_normal); + let merge_result = merge_reservoirs(initial_reservoir, world_position, world_normal, diffuse_brdf, + temporal.reservoir, temporal.world_position, temporal.world_normal, temporal.diffuse_brdf, &rng); - gi_reservoirs_b[pixel_index] = combined_reservoir; + gi_reservoirs_b[pixel_index] = merge_result.merged_reservoir; } @compute @workgroup_size(8, 8, 1) @@ -70,7 +72,7 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { let input_reservoir = gi_reservoirs_b[pixel_index]; let spatial = load_spatial_reservoir(global_id.xy, depth, world_position, world_normal, &rng); - let merge_result = merge_reservoirs_spatial(input_reservoir, world_position, world_normal, diffuse_brdf, + let merge_result = merge_reservoirs(input_reservoir, world_position, world_normal, diffuse_brdf, spatial.reservoir, spatial.world_position, spatial.world_normal, spatial.diffuse_brdf, &rng); let combined_reservoir = merge_result.merged_reservoir; @@ -79,6 +81,10 @@ fn spatial_and_shade(@builtin(global_invocation_id) global_id: vec3) { var pixel_color = textureLoad(view_output, global_id.xy); pixel_color += vec4(merge_result.selected_sample_radiance * combined_reservoir.unbiased_contribution_weight * view.exposure, 0.0); textureStore(view_output, global_id.xy, pixel_color); + +#ifdef VISUALIZE_WORLD_CACHE + textureStore(view_output, global_id.xy, vec4(query_world_cache(world_position, world_normal, view.world_position) * view.exposure, 1.0)); +#endif } fn generate_initial_reservoir(world_position: vec3, world_normal: vec3, rng: ptr) -> Reservoir { @@ -116,42 +122,52 @@ fn generate_initial_reservoir(world_position: vec3, world_normal: vec3 return reservoir; } -fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> Reservoir { +fn load_temporal_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3) -> NeighborInfo { let motion_vector = textureLoad(motion_vectors, pixel_id, 0).xy; let temporal_pixel_id_float = round(vec2(pixel_id) - (motion_vector * view.main_pass_viewport.zw)); - let temporal_pixel_id = vec2(temporal_pixel_id_float); // Check if the current pixel was off screen during the previous frame (current pixel is newly visible), // or if all temporal history should assumed to be invalid if any(temporal_pixel_id_float < vec2(0.0)) || any(temporal_pixel_id_float >= view.main_pass_viewport.zw) || bool(constants.reset) { - return empty_reservoir(); + return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0)); } - // Check if the pixel features have changed heavily between the current and previous frame - let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); - let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); - let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); - let temporal_world_normal = octahedral_decode(unpack_24bit_normal(temporal_gpixel.a)); - if pixel_dissimilar(depth, world_position, temporal_world_position, world_normal, temporal_world_normal) { - return empty_reservoir(); - } + let temporal_pixel_id_base = vec2(round(temporal_pixel_id_float)); + for (var i = 0u; i < 4u; i++) { + let temporal_pixel_id = permute_pixel(temporal_pixel_id_base, i); + + // Check if the pixel features have changed heavily between the current and previous frame + let temporal_depth = textureLoad(previous_depth_buffer, temporal_pixel_id, 0); + let temporal_gpixel = textureLoad(previous_gbuffer, temporal_pixel_id, 0); + let temporal_world_position = reconstruct_previous_world_position(temporal_pixel_id, temporal_depth); + let temporal_world_normal = octahedral_decode(unpack_24bit_normal(temporal_gpixel.a)); + let temporal_base_color = pow(unpack4x8unorm(temporal_gpixel.r).rgb, vec3(2.2)); + let temporal_diffuse_brdf = temporal_base_color / PI; + if pixel_dissimilar(depth, world_position, temporal_world_position, world_normal, temporal_world_normal) { + continue; + } - let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z); - var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index]; + let temporal_pixel_index = temporal_pixel_id.x + temporal_pixel_id.y * u32(view.main_pass_viewport.z); + var temporal_reservoir = gi_reservoirs_a[temporal_pixel_index]; - temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); + temporal_reservoir.confidence_weight = min(temporal_reservoir.confidence_weight, CONFIDENCE_WEIGHT_CAP); - return temporal_reservoir; + return NeighborInfo(temporal_reservoir, temporal_world_position, temporal_world_normal, temporal_diffuse_brdf); + } + + return NeighborInfo(empty_reservoir(), vec3(0.0), vec3(0.0), vec3(0.0)); } -struct SpatialInfo { - reservoir: Reservoir, - world_position: vec3, - world_normal: vec3, - diffuse_brdf: vec3, +fn permute_pixel(pixel_id: vec2, i: u32) -> vec2 { + let r = constants.frame_index + i; + let offset = vec2(r & 3u, (r >> 2u) & 3u); + var shifted_pixel_id = pixel_id + offset; + shifted_pixel_id ^= vec2(3u); + shifted_pixel_id -= offset; + return min(shifted_pixel_id, vec2(view.main_pass_viewport.zw - 1.0)); } -fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> SpatialInfo { +fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3, world_normal: vec3, rng: ptr) -> NeighborInfo { let spatial_pixel_id = get_neighbor_pixel_id(pixel_id, rng); let spatial_depth = textureLoad(depth_buffer, spatial_pixel_id, 0); @@ -161,13 +177,15 @@ fn load_spatial_reservoir(pixel_id: vec2, depth: f32, world_position: vec3< let spatial_base_color = pow(unpack4x8unorm(spatial_gpixel.r).rgb, vec3(2.2)); let spatial_diffuse_brdf = spatial_base_color / PI; if pixel_dissimilar(depth, world_position, spatial_world_position, world_normal, spatial_world_normal) { - return SpatialInfo(empty_reservoir(), spatial_world_position, spatial_world_normal, spatial_diffuse_brdf); + return NeighborInfo(empty_reservoir(), spatial_world_position, spatial_world_normal, spatial_diffuse_brdf); } let spatial_pixel_index = spatial_pixel_id.x + spatial_pixel_id.y * u32(view.main_pass_viewport.z); - let spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; + var spatial_reservoir = gi_reservoirs_b[spatial_pixel_index]; + + spatial_reservoir.radiance *= trace_point_visibility(world_position, spatial_reservoir.sample_point_world_position); - return SpatialInfo(spatial_reservoir, spatial_world_position, spatial_world_normal, spatial_diffuse_brdf); + return NeighborInfo(spatial_reservoir, spatial_world_position, spatial_world_normal, spatial_diffuse_brdf); } fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> vec2 { @@ -176,6 +194,13 @@ fn get_neighbor_pixel_id(center_pixel_id: vec2, rng: ptr) -> return vec2(spatial_id); } +struct NeighborInfo { + reservoir: Reservoir, + world_position: vec3, + world_normal: vec3, + diffuse_brdf: vec3, +} + fn jacobian( new_world_position: vec3, original_world_position: vec3, @@ -255,51 +280,12 @@ fn empty_reservoir() -> Reservoir { ); } -fn merge_reservoirs( - canonical_reservoir: Reservoir, - other_reservoir: Reservoir, - rng: ptr, -) -> Reservoir { - var combined_reservoir = empty_reservoir(); - combined_reservoir.confidence_weight = canonical_reservoir.confidence_weight + other_reservoir.confidence_weight; - - let mis_weight_denominator = select(0.0, 1.0 / combined_reservoir.confidence_weight, combined_reservoir.confidence_weight > 0.0); - - let canonical_mis_weight = canonical_reservoir.confidence_weight * mis_weight_denominator; - let canonical_target_function = luminance(canonical_reservoir.radiance); - let canonical_resampling_weight = canonical_mis_weight * (canonical_target_function * canonical_reservoir.unbiased_contribution_weight); - - let other_mis_weight = other_reservoir.confidence_weight * mis_weight_denominator; - let other_target_function = luminance(other_reservoir.radiance); - let other_resampling_weight = other_mis_weight * (other_target_function * other_reservoir.unbiased_contribution_weight); - - combined_reservoir.weight_sum = canonical_resampling_weight + other_resampling_weight; - - if rand_f(rng) < other_resampling_weight / combined_reservoir.weight_sum { - combined_reservoir.sample_point_world_position = other_reservoir.sample_point_world_position; - combined_reservoir.sample_point_world_normal = other_reservoir.sample_point_world_normal; - combined_reservoir.radiance = other_reservoir.radiance; - - let inverse_target_function = select(0.0, 1.0 / other_target_function, other_target_function > 0.0); - combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; - } else { - combined_reservoir.sample_point_world_position = canonical_reservoir.sample_point_world_position; - combined_reservoir.sample_point_world_normal = canonical_reservoir.sample_point_world_normal; - combined_reservoir.radiance = canonical_reservoir.radiance; - - let inverse_target_function = select(0.0, 1.0 / canonical_target_function, canonical_target_function > 0.0); - combined_reservoir.unbiased_contribution_weight = combined_reservoir.weight_sum * inverse_target_function; - } - - return combined_reservoir; -} - struct ReservoirMergeResult { merged_reservoir: Reservoir, selected_sample_radiance: vec3, } -fn merge_reservoirs_spatial( +fn merge_reservoirs( canonical_reservoir: Reservoir, canonical_world_position: vec3, canonical_world_normal: vec3, @@ -318,8 +304,7 @@ fn merge_reservoirs_spatial( let other_sample_radiance = other_reservoir.radiance * saturate(dot(normalize(other_reservoir.sample_point_world_position - canonical_world_position), canonical_world_normal)) * - canonical_diffuse_brdf * - trace_point_visibility(canonical_world_position, other_reservoir.sample_point_world_position); + canonical_diffuse_brdf; // Target functions for resampling and MIS let canonical_target_function_canonical_sample = luminance(canonical_sample_radiance); @@ -351,6 +336,11 @@ fn merge_reservoirs_spatial( canonical_reservoir.sample_point_world_normal ); + // Don't merge samples with huge jacobians, as it explodes the variance + if canonical_target_function_other_sample_jacobian > 2.0 { + return ReservoirMergeResult(canonical_reservoir, canonical_sample_radiance); + } + // Resampling weight for canonical sample let canonical_sample_mis_weight = balance_heuristic( canonical_reservoir.confidence_weight * canonical_target_function_canonical_sample, diff --git a/crates/bevy_solari/src/realtime/world_cache_compact.wgsl b/crates/bevy_solari/src/realtime/world_cache_compact.wgsl index a58abee4a9cc9..71585223a5e44 100644 --- a/crates/bevy_solari/src/realtime/world_cache_compact.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_compact.wgsl @@ -25,7 +25,7 @@ fn compact_world_cache_single_block( @builtin(local_invocation_index) t: u32, ) { if t == 0u { w1[0u] = 0u; } else { w1[t] = u32(world_cache_life[cell_id.x - 1u] != 0u); }; workgroupBarrier(); - if t < 1u { w2 [t] = w1[t]; } else { w2[t] = w1[t] + w1[t - 1u]; } workgroupBarrier(); + if t < 1u { w2[t] = w1[t]; } else { w2[t] = w1[t] + w1[t - 1u]; } workgroupBarrier(); if t < 2u { w1[t] = w2[t]; } else { w1[t] = w2[t] + w2[t - 2u]; } workgroupBarrier(); if t < 4u { w2[t] = w1[t]; } else { w2[t] = w1[t] + w1[t - 4u]; } workgroupBarrier(); if t < 8u { w1[t] = w2[t]; } else { w1[t] = w2[t] + w2[t - 8u]; } workgroupBarrier(); diff --git a/crates/bevy_solari/src/realtime/world_cache_query.wgsl b/crates/bevy_solari/src/realtime/world_cache_query.wgsl index b30900160c7d1..ecf9a0892906b 100644 --- a/crates/bevy_solari/src/realtime/world_cache_query.wgsl +++ b/crates/bevy_solari/src/realtime/world_cache_query.wgsl @@ -1,14 +1,16 @@ #define_import_path bevy_solari::world_cache -/// Controls how response the world cache is to changes in lighting -const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 30.0; +/// How responsive the world cache is to changes in lighting (higher is less responsive, lower is more responsive) +const WORLD_CACHE_MAX_TEMPORAL_SAMPLES: f32 = 10.0; /// Maximum amount of frames a cell can live for without being queried const WORLD_CACHE_CELL_LIFETIME: u32 = 30u; /// Maximum amount of attempts to find a cache entry after a hash collision const WORLD_CACHE_MAX_SEARCH_STEPS: u32 = 3u; -/// Controls the base size of each cache cell -const WORLD_CACHE_POSITION_BASE_CELL_SIZE: f32 = 0.4; +/// The size of a cache cell at the lowest LOD in meters +const WORLD_CACHE_POSITION_BASE_CELL_SIZE: f32 = 0.25; +/// How fast the world cache transitions between LODs as a function of distance to the camera +const WORLD_CACHE_POSITION_LOD_SCALE: f32 = 30.0; /// Marker value for an empty cell const WORLD_CACHE_EMPTY_CELL: u32 = 0u; @@ -36,9 +38,9 @@ struct WorldCacheGeometryData { #ifndef WORLD_CACHE_NON_ATOMIC_LIFE_BUFFER fn query_world_cache(world_position: vec3, world_normal: vec3, view_position: vec3) -> vec3 { - let world_position_quantized = bitcast>(quantize_position(world_position, view_position)); + let cell_size = get_cell_size(world_position, view_position); + let world_position_quantized = bitcast>(quantize_position(world_position, cell_size)); let world_normal_quantized = bitcast>(quantize_normal(world_normal)); - var key = compute_key(world_position_quantized, world_normal_quantized); let checksum = compute_checksum(world_position_quantized, world_normal_quantized); @@ -64,12 +66,13 @@ fn query_world_cache(world_position: vec3, world_normal: vec3, view_po } #endif -fn quantize_position(world_position: vec3, view_position: vec3) -> vec3 { - let base_size = WORLD_CACHE_POSITION_BASE_CELL_SIZE; - let d = distance(view_position, world_position); - let step = max((d * base_size) / 7.0, base_size); - let quantization_factor = exp2(floor(log2(step))); +fn get_cell_size(world_position: vec3, view_position: vec3) -> f32 { + let camera_distance = distance(view_position, world_position) / WORLD_CACHE_POSITION_LOD_SCALE; + let lod = exp2(floor(log2(1.0 + camera_distance))); + return WORLD_CACHE_POSITION_BASE_CELL_SIZE * lod; +} +fn quantize_position(world_position: vec3, quantization_factor: f32) -> vec3 { return floor(world_position / quantization_factor + 0.0001); } diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs index 124d5d05dc2fc..e2ba4d751e862 100644 --- a/crates/bevy_solari/src/scene/binder.rs +++ b/crates/bevy_solari/src/scene/binder.rs @@ -23,10 +23,6 @@ use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref}; const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap(); const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); -/// Average angular diameter of the sun as seen from earth. -/// -const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; - const TEXTURE_MAP_NONE: u32 = u32::MAX; const LIGHT_NOT_PRESENT_THIS_FRAME: u32 = u32::MAX; @@ -407,7 +403,7 @@ struct GpuDirectionalLight { impl GpuDirectionalLight { fn new(directional_light: &ExtractedDirectionalLight) -> Self { - let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0); + let cos_theta_max = cos(directional_light.sun_disk_angular_size / 2.0); let solid_angle = TAU * (1.0 - cos_theta_max); let luminance = (directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle; diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs index 7e574dd100bb2..58d6b7fd3abbb 100644 --- a/crates/bevy_solari/src/scene/blas.rs +++ b/crates/bevy_solari/src/scene/blas.rs @@ -105,20 +105,18 @@ pub fn compact_raytracing_blas( mut blas_manager: ResMut, render_queue: Res, ) { - let mut first_mesh_processed = None; - + let queue_size = blas_manager.compaction_queue.len(); + let mut meshes_processed = 0; let mut vertices_compacted = 0; - while vertices_compacted < MAX_COMPACTION_VERTICES_PER_FRAME - && let Some((mesh, vertex_count, compaction_started)) = - blas_manager.compaction_queue.pop_front() + + while !blas_manager.compaction_queue.is_empty() + && vertices_compacted < MAX_COMPACTION_VERTICES_PER_FRAME + && meshes_processed < queue_size { - // Stop iterating once we loop back around to the start of the list - if Some(mesh) == first_mesh_processed { - break; - } - if first_mesh_processed.is_none() { - first_mesh_processed = Some(mesh); - } + meshes_processed += 1; + + let (mesh, vertex_count, compaction_started) = + blas_manager.compaction_queue.pop_front().unwrap(); let Some(blas) = blas_manager.get(&mesh) else { continue; diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index b675bde5c05a5..0e832daac2b00 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -10,40 +10,32 @@ keywords = ["bevy"] [features] bevy_sprite_picking_backend = ["bevy_picking", "bevy_window"] -webgl = [] -webgpu = [] +bevy_text = ["dep:bevy_text", "bevy_window"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } -bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } -bevy_shader = { path = "../bevy_shader", version = "0.17.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } bevy_window = { path = "../bevy_window", version = "0.17.0-dev", optional = true } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } -bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ - "std", -] } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev", optional = true } # other -bytemuck = { version = "1", features = ["derive", "must_cast"] } -fixedbitset = "0.5" -derive_more = { version = "2", default-features = false, features = ["from"] } -bitflags = "2.3" radsort = "0.1" -nonmax = "0.5" tracing = { version = "0.1", default-features = false, features = ["std"] } +wgpu-types = { version = "26", default-features = false } + +[dev-dependencies] +approx = "0.5.1" [lints] workspace = true diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 434e5d4b5ce1d..cf68b9a1077a1 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -6,17 +6,16 @@ html_favicon_url = "https://bevy.org/assets/icon.png" )] -//! Provides 2D sprite rendering functionality. +//! Provides 2D sprite functionality. extern crate alloc; -mod mesh2d; #[cfg(feature = "bevy_sprite_picking_backend")] mod picking_backend; -mod render; mod sprite; +#[cfg(feature = "bevy_text")] +mod text2d; mod texture_slice; -mod tilemap_chunk; /// The sprite prelude. /// @@ -27,40 +26,36 @@ pub mod prelude { pub use crate::picking_backend::{ SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings, }; + #[cfg(feature = "bevy_text")] + #[doc(hidden)] + pub use crate::text2d::{Text2d, Text2dReader, Text2dWriter}; #[doc(hidden)] pub use crate::{ sprite::{Sprite, SpriteImageMode}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, MeshMaterial2d, ScalingMode, + ScalingMode, }; } +use bevy_asset::Assets; use bevy_camera::{ - primitives::{Aabb, MeshAabb as _}, - visibility::{NoFrustumCulling, VisibilitySystems}, + primitives::{Aabb, MeshAabb}, + visibility::NoFrustumCulling, + visibility::VisibilitySystems, }; -use bevy_shader::load_shader_library; -pub use mesh2d::*; +use bevy_mesh::{Mesh, Mesh2d}; #[cfg(feature = "bevy_sprite_picking_backend")] pub use picking_backend::*; -pub use render::*; pub use sprite::*; +#[cfg(feature = "bevy_text")] +pub use text2d::*; pub use texture_slice::*; -pub use tilemap_chunk::*; use bevy_app::prelude::*; -use bevy_asset::{embedded_asset, AssetEventSystems, Assets}; -use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; use bevy_ecs::prelude::*; -use bevy_image::{prelude::*, TextureAtlasPlugin}; -use bevy_mesh::{Mesh, Mesh2d}; -use bevy_render::{ - batching::sort_binned_render_phase, render_phase::AddRenderCommand, - render_resource::SpecializedRenderPipelines, ExtractSchedule, Render, RenderApp, RenderStartup, - RenderSystems, -}; +use bevy_image::{Image, TextureAtlasLayout, TextureAtlasPlugin}; -/// Adds support for 2D sprite rendering. +/// Adds support for 2D sprites. #[derive(Default)] pub struct SpritePlugin; @@ -77,66 +72,32 @@ pub type SpriteSystem = SpriteSystems; impl Plugin for SpritePlugin { fn build(&self, app: &mut App) { - load_shader_library!(app, "render/sprite_view_bindings.wgsl"); - - embedded_asset!(app, "render/sprite.wgsl"); - if !app.is_plugin_added::() { app.add_plugins(TextureAtlasPlugin); } + app.add_systems( + PostUpdate, + calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), + ); - app.add_plugins(( - Mesh2dRenderPlugin, - ColorMaterialPlugin, - TilemapChunkPlugin, - TilemapChunkMaterialPlugin, - )) - .add_systems( + #[cfg(feature = "bevy_text")] + app.add_systems( PostUpdate, ( - calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds), - ( - compute_slices_on_asset_event.before(AssetEventSystems), - compute_slices_on_sprite_change, - ) - .in_set(SpriteSystems::ComputeSlices), - ), + bevy_text::detect_text_needs_rerender::, + update_text2d_layout + .after(bevy_camera::CameraUpdateSystems) + .after(bevy_text::remove_dropped_font_atlas_sets) + .ambiguous_with(bevy_text::update_placeholder_layouts), + calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds), + ) + .chain() + .in_set(bevy_text::Text2dUpdateSystems) + .after(bevy_app::AnimationSystems), ); #[cfg(feature = "bevy_sprite_picking_backend")] app.add_plugins(SpritePickingPlugin); - - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .init_resource::>() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .init_resource::() - .add_render_command::() - .add_systems(RenderStartup, init_sprite_pipeline) - .add_systems( - ExtractSchedule, - ( - extract_sprites.in_set(SpriteSystems::ExtractSprites), - extract_sprite_events, - ), - ) - .add_systems( - Render, - ( - queue_sprites - .in_set(RenderSystems::Queue) - .ambiguous_with(queue_material2d_meshes::), - prepare_sprite_image_bind_groups.in_set(RenderSystems::PrepareBindGroups), - prepare_sprite_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), - sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), - sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), - ), - ); - }; } } @@ -191,11 +152,8 @@ pub fn calculate_bounds_2d( #[cfg(test)] mod test { - - use bevy_math::{Rect, Vec2, Vec3A}; - use bevy_utils::default; - use super::*; + use bevy_math::{Rect, Vec2, Vec3A}; #[test] fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() { @@ -258,7 +216,7 @@ mod test { .spawn(Sprite { custom_size: Some(Vec2::ZERO), image: image_handle, - ..default() + ..Sprite::default() }) .id(); @@ -322,7 +280,7 @@ mod test { Sprite { rect: Some(Rect::new(0., 0., 0.5, 1.)), image: image_handle, - ..default() + ..Sprite::default() }, Anchor::TOP_RIGHT, )) diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 47bbb18390cb9..4c8e423e67750 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -1,19 +1,21 @@ -use bevy_asset::{Assets, Handle}; -use bevy_camera::visibility::{self, Visibility, VisibilityClass}; +use bevy_asset::{AsAssetId, AssetId, Assets, Handle}; +use bevy_camera::{ + primitives::Aabb, + visibility::{self, Visibility, VisibilityClass}, +}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_image::{Image, TextureAtlas, TextureAtlasLayout}; -use bevy_math::{Rect, UVec2, Vec2}; +use bevy_math::{Rect, UVec2, Vec2, Vec3}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::sync_world::SyncToRenderWorld; use bevy_transform::components::Transform; use crate::TextureSlicer; /// Describes a sprite to be rendered to a 2D camera #[derive(Component, Debug, Default, Clone, Reflect)] -#[require(Transform, Visibility, SyncToRenderWorld, VisibilityClass, Anchor)] +#[require(Transform, Visibility, VisibilityClass, Anchor)] #[reflect(Component, Default, Debug, Clone)] #[component(on_add = visibility::add_visibility_class::)] pub struct Sprite { @@ -153,6 +155,14 @@ impl From> for Sprite { } } +impl AsAssetId for Sprite { + type Asset = Image; + + fn as_asset_id(&self) -> AssetId { + self.image.id() + } +} + /// Controls how the image is altered when scaled. #[derive(Default, Debug, Clone, Reflect, PartialEq)] #[reflect(Debug, Default, Clone)] @@ -261,6 +271,16 @@ impl Anchor { pub fn as_vec(&self) -> Vec2 { self.0 } + + /// Determine the bounds at the anchor + pub fn calculate_bounds(&self, size: Vec2) -> Aabb { + let x1 = (Anchor::TOP_LEFT.0.x - self.as_vec().x) * size.x; + let x2 = (Anchor::TOP_LEFT.0.x - self.as_vec().x + 1.) * size.x; + let y1 = (Anchor::TOP_LEFT.0.y - self.as_vec().y - 1.) * size.y; + let y2 = (Anchor::TOP_LEFT.0.y - self.as_vec().y) * size.y; + + Aabb::from_min_max(Vec3::new(x1, y1, 0.), Vec3::new(x2, y2, 0.)) + } } impl Default for Anchor { @@ -282,7 +302,7 @@ mod tests { use bevy_image::{Image, ToExtents}; use bevy_image::{TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, URect, UVec2, Vec2}; - use bevy_render::render_resource::{TextureDimension, TextureFormat}; + use wgpu_types::{TextureDimension, TextureFormat}; use crate::Anchor; diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs similarity index 64% rename from crates/bevy_text/src/text2d.rs rename to crates/bevy_sprite/src/text2d.rs index 69573e3eb5deb..3c0e5fa56564f 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -1,36 +1,31 @@ -use crate::pipeline::CosmicFontSystem; -use crate::{ - ComputedTextBlock, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, TextBounds, - TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, - TextSpanAccess, TextWriter, -}; +use crate::{Anchor, Sprite}; use bevy_asset::Assets; use bevy_camera::primitives::Aabb; use bevy_camera::visibility::{ - self, NoFrustumCulling, ViewVisibility, Visibility, VisibilityClass, + self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities, }; -use bevy_color::LinearRgba; +use bevy_camera::Camera; +use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHashSet; use bevy_ecs::{ change_detection::{DetectChanges, Ref}, component::Component, entity::Entity, - prelude::{ReflectComponent, With}, + prelude::ReflectComponent, query::{Changed, Without}, system::{Commands, Local, Query, Res, ResMut}, }; use bevy_image::prelude::*; -use bevy_math::{Vec2, Vec3}; +use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; -use bevy_render::sync_world::TemporaryRenderEntity; -use bevy_render::Extract; -use bevy_sprite::{ - Anchor, ExtractedSlice, ExtractedSlices, ExtractedSprite, ExtractedSprites, Sprite, +use bevy_text::{ + ComputedTextBlock, CosmicFontSystem, Font, FontAtlasSets, LineBreak, SwashCache, TextBounds, + TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextPipeline, TextReader, TextRoot, + TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; -use bevy_transform::prelude::GlobalTransform; -use bevy_window::{PrimaryWindow, Window}; +use core::any::TypeId; /// The top-level 2D text component. /// @@ -38,7 +33,7 @@ use bevy_window::{PrimaryWindow, Window}; /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) /// /// The string in this component is the first 'text span' in a hierarchy of text spans that are collected into -/// a [`ComputedTextBlock`]. See [`TextSpan`](crate::TextSpan) for the component used by children of entities with [`Text2d`]. +/// a [`ComputedTextBlock`]. See `TextSpan` for the component used by children of entities with [`Text2d`]. /// /// With `Text2d` the `justify` field of [`TextLayout`] only affects the internal alignment of a block of text and not its /// relative position, which is controlled by the [`Anchor`] component. @@ -50,7 +45,8 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_sprite::Text2d; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -132,117 +128,24 @@ pub type Text2dReader<'w, 's> = TextReader<'w, 's, Text2d>; /// 2d alias for [`TextWriter`]. pub type Text2dWriter<'w, 's> = TextWriter<'w, 's, Text2d>; -/// This system extracts the sprites from the 2D text components and adds them to the -/// "render world". -pub fn extract_text2d_sprite( - mut commands: Commands, - mut extracted_sprites: ResMut, - mut extracted_slices: ResMut, - texture_atlases: Extract>>, - windows: Extract>>, - text2d_query: Extract< - Query<( - Entity, - &ViewVisibility, - &ComputedTextBlock, - &TextLayoutInfo, - &TextBounds, - &Anchor, - &GlobalTransform, - )>, - >, - text_colors: Extract>, -) { - let mut start = extracted_slices.slices.len(); - let mut end = start + 1; - - // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 - let scale_factor = windows - .single() - .map(|window| window.resolution.scale_factor()) - .unwrap_or(1.0); - let scaling = GlobalTransform::from_scale(Vec2::splat(scale_factor.recip()).extend(1.)); - - for ( - main_entity, - view_visibility, - computed_block, - text_layout_info, - text_bounds, - anchor, - global_transform, - ) in text2d_query.iter() - { - if !view_visibility.get() { - continue; - } - - let size = Vec2::new( - text_bounds.width.unwrap_or(text_layout_info.size.x), - text_bounds.height.unwrap_or(text_layout_info.size.y), - ); - - let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; - let transform = - *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; - - let mut color = LinearRgba::WHITE; - let mut current_span = usize::MAX; - - for ( - i, - PositionedGlyph { - position, - atlas_info, - span_index, - .. - }, - ) in text_layout_info.glyphs.iter().enumerate() - { - if *span_index != current_span { - color = text_colors - .get( - computed_block - .entities() - .get(*span_index) - .map(|t| t.entity) - .unwrap_or(Entity::PLACEHOLDER), - ) - .map(|text_color| LinearRgba::from(text_color.0)) - .unwrap_or_default(); - current_span = *span_index; - } - let rect = texture_atlases - .get(atlas_info.texture_atlas) - .unwrap() - .textures[atlas_info.location.glyph_index] - .as_rect(); - extracted_slices.slices.push(ExtractedSlice { - offset: Vec2::new(position.x, -position.y), - rect, - size: rect.size(), - }); - - if text_layout_info.glyphs.get(i + 1).is_none_or(|info| { - info.span_index != current_span || info.atlas_info.texture != atlas_info.texture - }) { - let render_entity = commands.spawn(TemporaryRenderEntity).id(); - extracted_sprites.sprites.push(ExtractedSprite { - main_entity, - render_entity, - transform, - color, - image_handle_id: atlas_info.texture, - flip_x: false, - flip_y: false, - kind: bevy_sprite::ExtractedSpriteKind::Slices { - indices: start..end, - }, - }); - start = end; - } +/// Adds a shadow behind `Text2d` text +/// +/// Use `TextShadow` for text drawn with `bevy_ui` +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] +pub struct Text2dShadow { + /// Shadow displacement + /// With a value of zero the shadow will be hidden directly behind the text + pub offset: Vec2, + /// Color of the shadow + pub color: Color, +} - end += 1; +impl Default for Text2dShadow { + fn default() -> Self { + Self { + offset: Vec2::new(4., -4.), + color: Color::BLACK, } } } @@ -255,17 +158,18 @@ pub fn extract_text2d_sprite( /// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. pub fn update_text2d_layout( - mut last_scale_factor: Local>, + mut target_scale_factors: Local>, // Text items which should be reprocessed again, generally when the font hasn't loaded yet. mut queue: Local, mut textures: ResMut>, fonts: Res>, - windows: Query<&Window, With>, + camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut texture_atlases: ResMut>, mut font_atlas_sets: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( Entity, + Option<&RenderLayers>, Ref, Ref, &mut TextLayoutInfo, @@ -275,21 +179,46 @@ pub fn update_text2d_layout( mut font_system: ResMut, mut swash_cache: ResMut, ) { - // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 - let scale_factor = windows - .single() - .ok() - .map(|window| window.resolution.scale_factor()) - .or(*last_scale_factor) - .unwrap_or(1.); - - let inverse_scale_factor = scale_factor.recip(); + target_scale_factors.clear(); + target_scale_factors.extend( + camera_query + .iter() + .filter(|(_, visible_entities, _)| { + !visible_entities.get(TypeId::of::()).is_empty() + }) + .filter_map(|(camera, _, maybe_camera_mask)| { + camera.target_scaling_factor().map(|scale_factor| { + (scale_factor, maybe_camera_mask.cloned().unwrap_or_default()) + }) + }), + ); + + let mut previous_scale_factor = 0.; + let mut previous_mask = &RenderLayers::none(); + + for (entity, maybe_entity_mask, block, bounds, text_layout_info, mut computed) in + &mut text_query + { + let entity_mask = maybe_entity_mask.unwrap_or_default(); - let factor_changed = *last_scale_factor != Some(scale_factor); - *last_scale_factor = Some(scale_factor); + let scale_factor = if entity_mask == previous_mask && 0. < previous_scale_factor { + previous_scale_factor + } else { + // `Text2d` only supports generating a single text layout per Text2d entity. If a `Text2d` entity has multiple + // render targets with different scale factors, then we use the maximum of the scale factors. + let Some((scale_factor, mask)) = target_scale_factors + .iter() + .filter(|(_, camera_mask)| camera_mask.intersects(entity_mask)) + .max_by_key(|(scale_factor, _)| FloatOrd(*scale_factor)) + else { + continue; + }; + previous_scale_factor = *scale_factor; + previous_mask = mask; + *scale_factor + }; - for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query { - if factor_changed + if scale_factor != text_layout_info.scale_factor || computed.needs_rerender() || bounds.is_changed() || (!queue.is_empty() && queue.remove(&entity)) @@ -298,11 +227,9 @@ pub fn update_text2d_layout( width: if block.linebreak == LineBreak::NoWrap { None } else { - bounds.width.map(|width| scale_value(width, scale_factor)) + bounds.width.map(|width| width * scale_factor) }, - height: bounds - .height - .map(|height| scale_value(height, scale_factor)), + height: bounds.height.map(|height| height * scale_factor), }; let text_layout_info = text_layout_info.into_inner(); @@ -310,7 +237,7 @@ pub fn update_text2d_layout( text_layout_info, &fonts, text_reader.iter(entity), - scale_factor.into(), + scale_factor as f64, &block, text_bounds, &mut font_atlas_sets, @@ -329,21 +256,14 @@ pub fn update_text2d_layout( panic!("Fatal error when processing text: {e}."); } Ok(()) => { - text_layout_info.size.x = - scale_value(text_layout_info.size.x, inverse_scale_factor); - text_layout_info.size.y = - scale_value(text_layout_info.size.y, inverse_scale_factor); + text_layout_info.scale_factor = scale_factor; + text_layout_info.size *= scale_factor.recip(); } } } } } -/// Scales `value` by `factor`. -pub fn scale_value(value: f32, factor: f32) -> f32 { - value * factor -} - /// System calculating and inserting an [`Aabb`] component to entities with some /// [`TextLayoutInfo`] and [`Anchor`] components, and without a [`NoFrustumCulling`] component. /// @@ -386,9 +306,10 @@ mod tests { use bevy_app::{App, Update}; use bevy_asset::{load_internal_binary_asset, Handle}; + use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; use bevy_ecs::schedule::IntoScheduleConfigs; - - use crate::{detect_text_needs_rerender, TextIterScratch}; + use bevy_math::UVec2; + use bevy_text::{detect_text_needs_rerender, TextIterScratch}; use super::*; @@ -415,11 +336,28 @@ mod tests { .chain(), ); + let mut visible_entities = VisibleEntities::default(); + visible_entities.push(Entity::PLACEHOLDER, TypeId::of::()); + + app.world_mut().spawn(( + Camera { + computed: ComputedCameraValues { + target_info: Some(RenderTargetInfo { + physical_size: UVec2::splat(1000), + scale_factor: 1., + }), + ..Default::default() + }, + ..Default::default() + }, + visible_entities, + )); + // A font is needed to ensure the text is laid out with an actual size. load_internal_binary_asset!( app, Handle::default(), - "FiraMono-subset.ttf", + "../../bevy_text/src/FiraMono-subset.ttf", |bytes: &[u8], _path: String| { Font::try_from_bytes(bytes.to_vec()).unwrap() } ); diff --git a/crates/bevy_sprite/src/texture_slice/mod.rs b/crates/bevy_sprite/src/texture_slice/mod.rs index 7b1a1e33e2168..3fb1549f15fb9 100644 --- a/crates/bevy_sprite/src/texture_slice/mod.rs +++ b/crates/bevy_sprite/src/texture_slice/mod.rs @@ -1,15 +1,10 @@ mod border_rect; -mod computed_slices; mod slicer; use bevy_math::{Rect, Vec2}; pub use border_rect::BorderRect; pub use slicer::{SliceScaleMode, TextureSlicer}; -pub(crate) use computed_slices::{ - compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices, -}; - /// Single texture slice, representing a texture rect to draw in a given area #[derive(Debug, Clone, PartialEq)] pub struct TextureSlice { diff --git a/crates/bevy_sprite_render/Cargo.toml b/crates/bevy_sprite_render/Cargo.toml new file mode 100644 index 0000000000000..d0b20a3052750 --- /dev/null +++ b/crates/bevy_sprite_render/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "bevy_sprite_render" +version = "0.17.0-dev" +edition = "2024" +description = "Provides sprite rendering functionality for Bevy Engine" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] +webgl = [] +webgpu = [] +bevy_text = ["dep:bevy_text", "bevy_sprite/bevy_text"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.17.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } +bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } +bevy_shader = { path = "../bevy_shader", version = "0.17.0-dev" } +bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.17.0-dev", optional = true } +bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ + "std", +] } + +# other +bytemuck = { version = "1", features = ["derive", "must_cast"] } +fixedbitset = "0.5" +derive_more = { version = "2", default-features = false, features = ["from"] } +bitflags = "2.3" +nonmax = "0.5" +tracing = { version = "0.1", default-features = false, features = ["std"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_sprite_render/LICENSE-APACHE b/crates/bevy_sprite_render/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_sprite_render/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_sprite_render/LICENSE-MIT b/crates/bevy_sprite_render/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_sprite_render/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_sprite_render/README.md b/crates/bevy_sprite_render/README.md new file mode 100644 index 0000000000000..cd9d201616ffb --- /dev/null +++ b/crates/bevy_sprite_render/README.md @@ -0,0 +1,7 @@ +# Bevy Sprite + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_sprite_render.svg)](https://crates.io/crates/bevy_sprite_render) +[![Downloads](https://img.shields.io/crates/d/bevy_sprite_render.svg)](https://crates.io/crates/bevy_sprite_render) +[![Docs](https://docs.rs/bevy_sprite_render/badge.svg)](https://docs.rs/bevy_sprite_render/latest/bevy_sprite_render/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_sprite_render/src/lib.rs b/crates/bevy_sprite_render/src/lib.rs new file mode 100644 index 0000000000000..e902f13dee004 --- /dev/null +++ b/crates/bevy_sprite_render/src/lib.rs @@ -0,0 +1,126 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![forbid(unsafe_code)] +#![doc( + html_logo_url = "https://bevy.org/assets/icon.png", + html_favicon_url = "https://bevy.org/assets/icon.png" +)] + +//! Provides 2D sprite rendering functionality. + +extern crate alloc; + +mod mesh2d; +mod render; +#[cfg(feature = "bevy_text")] +mod text2d; +mod texture_slice; +mod tilemap_chunk; + +/// The sprite prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + #[doc(hidden)] + pub use crate::{ColorMaterial, MeshMaterial2d}; +} + +use bevy_shader::load_shader_library; +pub use mesh2d::*; +pub use render::*; +pub(crate) use texture_slice::*; +pub use tilemap_chunk::*; + +use bevy_app::prelude::*; +use bevy_asset::{embedded_asset, AssetEventSystems}; +use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d}; +use bevy_ecs::prelude::*; +use bevy_image::{prelude::*, TextureAtlasPlugin}; +use bevy_mesh::Mesh2d; +use bevy_render::{ + batching::sort_binned_render_phase, render_phase::AddRenderCommand, + render_resource::SpecializedRenderPipelines, sync_world::SyncToRenderWorld, ExtractSchedule, + Render, RenderApp, RenderStartup, RenderSystems, +}; +use bevy_sprite::Sprite; + +#[cfg(feature = "bevy_text")] +use crate::text2d::extract_text2d_sprite; + +/// Adds support for 2D sprite rendering. +#[derive(Default)] +pub struct SpriteRenderPlugin; + +/// System set for sprite rendering. +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub enum SpriteSystems { + ExtractSprites, + ComputeSlices, +} + +/// Deprecated alias for [`SpriteSystems`]. +#[deprecated(since = "0.17.0", note = "Renamed to `SpriteSystems`.")] +pub type SpriteSystem = SpriteSystems; + +impl Plugin for SpriteRenderPlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "render/sprite_view_bindings.wgsl"); + + embedded_asset!(app, "render/sprite.wgsl"); + + if !app.is_plugin_added::() { + app.add_plugins(TextureAtlasPlugin); + } + + app.add_plugins(( + Mesh2dRenderPlugin, + ColorMaterialPlugin, + TilemapChunkPlugin, + TilemapChunkMaterialPlugin, + )) + .add_systems( + PostUpdate, + ( + compute_slices_on_asset_event.before(AssetEventSystems), + compute_slices_on_sprite_change, + ) + .in_set(SpriteSystems::ComputeSlices), + ); + + app.register_required_components::(); + + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_systems(RenderStartup, init_sprite_pipeline) + .add_systems( + ExtractSchedule, + ( + extract_sprites.in_set(SpriteSystems::ExtractSprites), + extract_sprite_events, + #[cfg(feature = "bevy_text")] + extract_text2d_sprite.after(SpriteSystems::ExtractSprites), + ), + ) + .add_systems( + Render, + ( + queue_sprites + .in_set(RenderSystems::Queue) + .ambiguous_with(queue_material2d_meshes::), + prepare_sprite_image_bind_groups.in_set(RenderSystems::PrepareBindGroups), + prepare_sprite_view_bind_groups.in_set(RenderSystems::PrepareBindGroups), + sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), + sort_binned_render_phase::.in_set(RenderSystems::PhaseSort), + ), + ); + }; + } +} diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite_render/src/mesh2d/color_material.rs similarity index 98% rename from crates/bevy_sprite/src/mesh2d/color_material.rs rename to crates/bevy_sprite_render/src/mesh2d/color_material.rs index ee5ef79fa6094..052be3186012d 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite_render/src/mesh2d/color_material.rs @@ -87,7 +87,7 @@ impl From> for ColorMaterial { } } -// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/color_material.wgsl! +// NOTE: These must match the bit flags in bevy_sprite_render/src/mesh2d/color_material.wgsl! bitflags::bitflags! { #[repr(transparent)] pub struct ColorMaterialFlags: u32 { diff --git a/crates/bevy_sprite/src/mesh2d/color_material.wgsl b/crates/bevy_sprite_render/src/mesh2d/color_material.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/color_material.wgsl rename to crates/bevy_sprite_render/src/mesh2d/color_material.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite_render/src/mesh2d/material.rs similarity index 98% rename from crates/bevy_sprite/src/mesh2d/material.rs rename to crates/bevy_sprite_render/src/mesh2d/material.rs index 9a1954688f4d5..a96c1e01a37e1 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite_render/src/mesh2d/material.rs @@ -66,7 +66,7 @@ pub const MATERIAL_2D_BIND_GROUP_INDEX: usize = 2; /// check out the [`AsBindGroup`] documentation. /// /// ``` -/// # use bevy_sprite::{Material2d, MeshMaterial2d}; +/// # use bevy_sprite_render::{Material2d, MeshMaterial2d}; /// # use bevy_ecs::prelude::*; /// # use bevy_image::Image; /// # use bevy_reflect::TypePath; @@ -172,7 +172,7 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { /// # Example /// /// ``` -/// # use bevy_sprite::{ColorMaterial, MeshMaterial2d}; +/// # use bevy_sprite_render::{ColorMaterial, MeshMaterial2d}; /// # use bevy_ecs::prelude::*; /// # use bevy_mesh::{Mesh, Mesh2d}; /// # use bevy_color::palettes::basic::RED; @@ -409,7 +409,7 @@ where fn clone(&self) -> Self { Self { mesh_key: self.mesh_key, - bind_group_data: self.bind_group_data, + bind_group_data: self.bind_group_data.clone(), } } } @@ -737,7 +737,10 @@ pub fn specialize_material2d_meshes( let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { continue; }; - let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap(); + let Some(entity_tick) = entity_specialization_ticks.get(visible_entity) else { + error!("{visible_entity:?} is missing specialization tick. Spawning Meshes in PostUpdate or later is currently not fully supported."); + continue; + }; let last_specialized_tick = view_specialized_material_pipeline_cache .get(visible_entity) .map(|(tick, _)| *tick); @@ -763,7 +766,7 @@ pub fn specialize_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material_2d.key, + bind_group_data: material_2d.key.clone(), }, &mesh.layout, ); diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs similarity index 98% rename from crates/bevy_sprite/src/mesh2d/mesh.rs rename to crates/bevy_sprite_render/src/mesh2d/mesh.rs index 9f494fec614ec..1eff9be1c2904 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -221,7 +221,7 @@ impl Mesh2dUniform { } } -// NOTE: These must match the bit flags in bevy_sprite/src/mesh2d/mesh2d.wgsl! +// NOTE: These must match the bit flags in bevy_sprite_render/src/mesh2d/mesh2d.wgsl! bitflags::bitflags! { #[repr(transparent)] pub struct MeshFlags: u32 { @@ -328,17 +328,18 @@ pub fn init_mesh_2d_pipeline( } }; - let format_size = image.texture_descriptor.format.pixel_size(); - render_queue.write_texture( - texture.as_image_copy(), - image.data.as_ref().expect("Image has no data"), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(image.width() * format_size as u32), - rows_per_image: None, - }, - image.texture_descriptor.size, - ); + if let Ok(format_size) = image.texture_descriptor.format.pixel_size() { + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image has no data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + } let texture_view = texture.create_view(&TextureViewDescriptor::default()); GpuImage { diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_bindings.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_bindings.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_bindings.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_functions.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_types.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_vertex_output.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_output.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_vertex_output.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_output.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_view_bindings.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_view_bindings.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_view_bindings.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_view_bindings.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mesh2d_view_types.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_view_types.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mesh2d_view_types.wgsl rename to crates/bevy_sprite_render/src/mesh2d/mesh2d_view_types.wgsl diff --git a/crates/bevy_sprite/src/mesh2d/mod.rs b/crates/bevy_sprite_render/src/mesh2d/mod.rs similarity index 100% rename from crates/bevy_sprite/src/mesh2d/mod.rs rename to crates/bevy_sprite_render/src/mesh2d/mod.rs diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs similarity index 100% rename from crates/bevy_sprite/src/mesh2d/wireframe2d.rs rename to crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.wgsl similarity index 100% rename from crates/bevy_sprite/src/mesh2d/wireframe2d.wgsl rename to crates/bevy_sprite_render/src/mesh2d/wireframe2d.wgsl diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite_render/src/render/mod.rs similarity index 98% rename from crates/bevy_sprite/src/render/mod.rs rename to crates/bevy_sprite_render/src/render/mod.rs index e24b5fb0482a1..0ea54bf7f2ed2 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite_render/src/render/mod.rs @@ -1,6 +1,6 @@ use core::ops::Range; -use crate::{Anchor, ComputedTextureSlices, ScalingMode, Sprite}; +use crate::ComputedTextureSlices; use bevy_asset::{load_embedded_asset, AssetEvent, AssetId, AssetServer, Assets, Handle}; use bevy_camera::visibility::ViewVisibility; use bevy_color::{ColorToComponents, LinearRgba}; @@ -39,6 +39,7 @@ use bevy_render::{ Extract, }; use bevy_shader::{Shader, ShaderDefVal}; +use bevy_sprite::{Anchor, ScalingMode, Sprite}; use bevy_transform::components::GlobalTransform; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; @@ -92,17 +93,18 @@ pub fn init_sprite_pipeline( } }; - let format_size = image.texture_descriptor.format.pixel_size(); - render_queue.write_texture( - texture.as_image_copy(), - image.data.as_ref().expect("Image has no data"), - TexelCopyBufferLayout { - offset: 0, - bytes_per_row: Some(image.width() * format_size as u32), - rows_per_image: None, - }, - image.texture_descriptor.size, - ); + if let Ok(format_size) = image.texture_descriptor.format.pixel_size() { + render_queue.write_texture( + texture.as_image_copy(), + image.data.as_ref().expect("Image has no data"), + TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(image.width() * format_size as u32), + rows_per_image: None, + }, + image.texture_descriptor.size, + ); + } let texture_view = texture.create_view(&TextureViewDescriptor::default()); GpuImage { texture, @@ -856,7 +858,7 @@ pub fn prepare_sprite_image_bind_groups( // The sprite shader can then use the two least significant bits as the vertex index. // The rest of the properties to transform the vertex positions and UVs (which are // implicit) are baked into the instance transform, and UV offset and scale. - // See bevy_sprite/src/render/sprite.wgsl for the details. + // See bevy_sprite_render/src/render/sprite.wgsl for the details. sprite_meta.sprite_index_buffer.push(2); sprite_meta.sprite_index_buffer.push(0); sprite_meta.sprite_index_buffer.push(1); diff --git a/crates/bevy_sprite/src/render/sprite.wgsl b/crates/bevy_sprite_render/src/render/sprite.wgsl similarity index 100% rename from crates/bevy_sprite/src/render/sprite.wgsl rename to crates/bevy_sprite_render/src/render/sprite.wgsl diff --git a/crates/bevy_sprite/src/render/sprite_view_bindings.wgsl b/crates/bevy_sprite_render/src/render/sprite_view_bindings.wgsl similarity index 100% rename from crates/bevy_sprite/src/render/sprite_view_bindings.wgsl rename to crates/bevy_sprite_render/src/render/sprite_view_bindings.wgsl diff --git a/crates/bevy_sprite_render/src/text2d/mod.rs b/crates/bevy_sprite_render/src/text2d/mod.rs new file mode 100644 index 0000000000000..5dbd603ed21df --- /dev/null +++ b/crates/bevy_sprite_render/src/text2d/mod.rs @@ -0,0 +1,210 @@ +use crate::{ + ExtractedSlice, ExtractedSlices, ExtractedSprite, ExtractedSpriteKind, ExtractedSprites, +}; +use bevy_asset::{AssetId, Assets}; +use bevy_camera::visibility::ViewVisibility; +use bevy_color::LinearRgba; +use bevy_ecs::{ + entity::Entity, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_image::prelude::*; +use bevy_math::Vec2; +use bevy_render::sync_world::TemporaryRenderEntity; +use bevy_render::Extract; +use bevy_sprite::{Anchor, Text2dShadow}; +use bevy_text::{ + ComputedTextBlock, PositionedGlyph, TextBackgroundColor, TextBounds, TextColor, TextLayoutInfo, +}; +use bevy_transform::prelude::GlobalTransform; + +/// This system extracts the sprites from the 2D text components and adds them to the +/// "render world". +pub fn extract_text2d_sprite( + mut commands: Commands, + mut extracted_sprites: ResMut, + mut extracted_slices: ResMut, + texture_atlases: Extract>>, + text2d_query: Extract< + Query<( + Entity, + &ViewVisibility, + &ComputedTextBlock, + &TextLayoutInfo, + &TextBounds, + &Anchor, + Option<&Text2dShadow>, + &GlobalTransform, + )>, + >, + text_colors: Extract>, + text_background_colors_query: Extract>, +) { + let mut start = extracted_slices.slices.len(); + let mut end = start + 1; + + for ( + main_entity, + view_visibility, + computed_block, + text_layout_info, + text_bounds, + anchor, + maybe_shadow, + global_transform, + ) in text2d_query.iter() + { + let scaling = GlobalTransform::from_scale( + Vec2::splat(text_layout_info.scale_factor.recip()).extend(1.), + ); + if !view_visibility.get() { + continue; + } + + let size = Vec2::new( + text_bounds.width.unwrap_or(text_layout_info.size.x), + text_bounds.height.unwrap_or(text_layout_info.size.y), + ); + + let top_left = (Anchor::TOP_LEFT.0 - anchor.as_vec()) * size; + + for &(section_entity, rect) in text_layout_info.section_rects.iter() { + let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { + continue; + }; + let render_entity = commands.spawn(TemporaryRenderEntity).id(); + let offset = Vec2::new(rect.center().x, -rect.center().y); + let transform = *global_transform + * GlobalTransform::from_translation(top_left.extend(0.)) + * scaling + * GlobalTransform::from_translation(offset.extend(0.)); + extracted_sprites.sprites.push(ExtractedSprite { + main_entity, + render_entity, + transform, + color: text_background_color.0.into(), + image_handle_id: AssetId::default(), + flip_x: false, + flip_y: false, + kind: ExtractedSpriteKind::Single { + anchor: Vec2::ZERO, + rect: None, + scaling_mode: None, + custom_size: Some(rect.size()), + }, + }); + } + + if let Some(shadow) = maybe_shadow { + let shadow_transform = *global_transform + * GlobalTransform::from_translation((top_left + shadow.offset).extend(0.)) + * scaling; + let color = shadow.color.into(); + + for ( + i, + PositionedGlyph { + position, + atlas_info, + .. + }, + ) in text_layout_info.glyphs.iter().enumerate() + { + let rect = texture_atlases + .get(atlas_info.texture_atlas) + .unwrap() + .textures[atlas_info.location.glyph_index] + .as_rect(); + extracted_slices.slices.push(ExtractedSlice { + offset: Vec2::new(position.x, -position.y), + rect, + size: rect.size(), + }); + + if text_layout_info + .glyphs + .get(i + 1) + .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) + { + let render_entity = commands.spawn(TemporaryRenderEntity).id(); + extracted_sprites.sprites.push(ExtractedSprite { + main_entity, + render_entity, + transform: shadow_transform, + color, + image_handle_id: atlas_info.texture, + flip_x: false, + flip_y: false, + kind: ExtractedSpriteKind::Slices { + indices: start..end, + }, + }); + start = end; + } + + end += 1; + } + } + + let transform = + *global_transform * GlobalTransform::from_translation(top_left.extend(0.)) * scaling; + let mut color = LinearRgba::WHITE; + let mut current_span = usize::MAX; + + for ( + i, + PositionedGlyph { + position, + atlas_info, + span_index, + .. + }, + ) in text_layout_info.glyphs.iter().enumerate() + { + if *span_index != current_span { + color = text_colors + .get( + computed_block + .entities() + .get(*span_index) + .map(|t| t.entity) + .unwrap_or(Entity::PLACEHOLDER), + ) + .map(|text_color| LinearRgba::from(text_color.0)) + .unwrap_or_default(); + current_span = *span_index; + } + let rect = texture_atlases + .get(atlas_info.texture_atlas) + .unwrap() + .textures[atlas_info.location.glyph_index] + .as_rect(); + extracted_slices.slices.push(ExtractedSlice { + offset: Vec2::new(position.x, -position.y), + rect, + size: rect.size(), + }); + + if text_layout_info.glyphs.get(i + 1).is_none_or(|info| { + info.span_index != current_span || info.atlas_info.texture != atlas_info.texture + }) { + let render_entity = commands.spawn(TemporaryRenderEntity).id(); + extracted_sprites.sprites.push(ExtractedSprite { + main_entity, + render_entity, + transform, + color, + image_handle_id: atlas_info.texture, + flip_x: false, + flip_y: false, + kind: ExtractedSpriteKind::Slices { + indices: start..end, + }, + }); + start = end; + } + + end += 1; + } + } +} diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs similarity index 98% rename from crates/bevy_sprite/src/texture_slice/computed_slices.rs rename to crates/bevy_sprite_render/src/texture_slice/computed_slices.rs index d4972f03848fc..4011c2d34df65 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs @@ -1,10 +1,10 @@ -use super::TextureSlice; -use crate::{ExtractedSlice, Sprite, SpriteImageMode, TextureAtlasLayout}; +use crate::{ExtractedSlice, TextureAtlasLayout}; use bevy_asset::{AssetEvent, Assets}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_math::{Rect, Vec2}; use bevy_platform::collections::HashSet; +use bevy_sprite::{Sprite, SpriteImageMode, TextureSlice}; /// Component storing texture slices for tiled or sliced sprite entities /// diff --git a/crates/bevy_sprite_render/src/texture_slice/mod.rs b/crates/bevy_sprite_render/src/texture_slice/mod.rs new file mode 100644 index 0000000000000..650e09f6cf49b --- /dev/null +++ b/crates/bevy_sprite_render/src/texture_slice/mod.rs @@ -0,0 +1,5 @@ +mod computed_slices; + +pub(crate) use computed_slices::{ + compute_slices_on_asset_event, compute_slices_on_sprite_change, ComputedTextureSlices, +}; diff --git a/crates/bevy_sprite/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs similarity index 100% rename from crates/bevy_sprite/src/tilemap_chunk/mod.rs rename to crates/bevy_sprite_render/src/tilemap_chunk/mod.rs diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs similarity index 100% rename from crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs rename to crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.rs diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl b/crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wgsl similarity index 100% rename from crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl rename to crates/bevy_sprite_render/src/tilemap_chunk/tilemap_chunk_material.wgsl diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs index db40adeeb4172..ffe323c249efc 100644 --- a/crates/bevy_state/src/lib.rs +++ b/crates/bevy_state/src/lib.rs @@ -91,6 +91,61 @@ pub mod prelude { OnExit, OnTransition, State, StateSet, StateTransition, StateTransitionEvent, States, SubStates, TransitionSchedules, }, - state_scoped::{DespawnOnEnterState, DespawnOnExitState}, + state_scoped::{DespawnOnEnter, DespawnOnExit}, }; } + +#[cfg(test)] +mod tests { + use bevy_app::{App, PreStartup}; + use bevy_ecs::{ + resource::Resource, + system::{Commands, ResMut}, + }; + use bevy_state_macros::States; + + use crate::{ + app::{AppExtStates, StatesPlugin}, + state::OnEnter, + }; + + #[test] + fn state_transition_runs_before_pre_startup() { + // This test is not really a "requirement" of states (we could run state transitions after + // PreStartup), but this is the current policy and it is useful to ensure we are following + // it if we ever change how we initialize stuff. + + let mut app = App::new(); + app.add_plugins(StatesPlugin); + + #[derive(States, Default, PartialEq, Eq, Hash, Debug, Clone)] + enum TestState { + #[default] + A, + #[expect( + dead_code, + reason = "This struct is used as a compilation test to test the derive macros, and as such is intentionally never constructed." + )] + B, + } + + #[derive(Resource, Default, PartialEq, Eq, Debug)] + struct Thingy(usize); + + app.init_state::(); + + app.add_systems(OnEnter(TestState::A), move |mut commands: Commands| { + commands.init_resource::(); + }); + + app.add_systems(PreStartup, move |mut thingy: ResMut| { + // This system will fail if it runs before OnEnter. + thingy.0 += 1; + }); + + app.update(); + + // This assert only succeeds if first OnEnter(TestState::A) runs, followed by PreStartup. + assert_eq!(app.world().resource::(), &Thingy(1)); + } +} diff --git a/crates/bevy_state/src/state/computed_states.rs b/crates/bevy_state/src/state/computed_states.rs index dbe7a973d427e..680c1c3ea989c 100644 --- a/crates/bevy_state/src/state/computed_states.rs +++ b/crates/bevy_state/src/state/computed_states.rs @@ -13,7 +13,7 @@ use super::{state_set::StateSet, states::States}; /// ``` /// # use bevy_state::prelude::*; /// # use bevy_ecs::prelude::*; -/// +/// # /// /// Computed States require some state to derive from /// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] /// enum AppState { @@ -22,7 +22,6 @@ use super::{state_set::StateSet, states::States}; /// InGame { paused: bool } /// } /// -/// /// #[derive(Clone, PartialEq, Eq, Hash, Debug)] /// struct InGame; /// @@ -52,7 +51,7 @@ use super::{state_set::StateSet, states::States}; /// ``` /// # use bevy_state::prelude::*; /// # use bevy_ecs::prelude::*; -/// +/// # /// # struct App; /// # impl App { /// # fn new() -> Self { App } @@ -61,10 +60,10 @@ use super::{state_set::StateSet, states::States}; /// # } /// # struct AppState; /// # struct InGame; -/// -/// App::new() -/// .init_state::() -/// .add_computed_state::(); +/// # +/// App::new() +/// .init_state::() +/// .add_computed_state::(); /// ``` pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { /// The set of states from which the [`Self`] is derived. @@ -100,7 +99,7 @@ impl States for S { mod tests { use crate::{ app::{AppExtStates, StatesPlugin}, - prelude::DespawnOnEnterState, + prelude::DespawnOnEnter, state::{ComputedStates, StateTransition}, }; use bevy_app::App; @@ -133,7 +132,7 @@ mod tests { let world = app.world_mut(); - world.spawn((DespawnOnEnterState(TestComputedState), TestComponent)); + world.spawn((DespawnOnEnter(TestComputedState), TestComponent)); assert!(world.query::<&TestComponent>().single(world).is_ok()); world.run_schedule(StateTransition); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs index 2bbdd615baa43..7f2300e82d5be 100644 --- a/crates/bevy_state/src/state/states.rs +++ b/crates/bevy_state/src/state/states.rs @@ -46,11 +46,13 @@ use core::hash::Hash; /// /// # struct AppMock; /// # impl AppMock { +/// # fn init_state(&mut self) {} /// # fn add_systems(&mut self, schedule: S, systems: impl IntoScheduleConfigs) {} /// # } /// # struct Update; /// # let mut app = AppMock; /// +/// app.init_state::(); /// app.add_systems(Update, handle_escape_pressed.run_if(in_state(GameState::MainMenu))); /// app.add_systems(OnEnter(GameState::SettingsMenu), open_settings_menu); /// ``` @@ -67,8 +69,8 @@ pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug /// Should [state scoping](crate::state_scoped) be enabled for this state? /// If set to `true`, the - /// [`DespawnOnEnterState`](crate::state_scoped::DespawnOnEnterState) and - /// [`DespawnOnExitState`](crate::state_scoped::DespawnOnEnterState) + /// [`DespawnOnEnter`](crate::state_scoped::DespawnOnEnter) and + /// [`DespawnOnExit`](crate::state_scoped::DespawnOnExit) /// components are used to remove entities when entering or exiting the /// state. const SCOPED_ENTITIES_ENABLED: bool = false; diff --git a/crates/bevy_state/src/state_scoped.rs b/crates/bevy_state/src/state_scoped.rs index 1abf0975ea7f5..efeed8c765a9b 100644 --- a/crates/bevy_state/src/state_scoped.rs +++ b/crates/bevy_state/src/state_scoped.rs @@ -34,7 +34,7 @@ use crate::state::{StateTransitionEvent, States}; /// /// fn spawn_player(mut commands: Commands) { /// commands.spawn(( -/// DespawnOnExitState(GameState::InGame), +/// DespawnOnExit(GameState::InGame), /// Player /// )); /// } @@ -52,9 +52,9 @@ use crate::state::{StateTransitionEvent, States}; /// ``` #[derive(Component, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct DespawnOnExitState(pub S); +pub struct DespawnOnExit(pub S); -impl Default for DespawnOnExitState +impl Default for DespawnOnExit where S: States + Default, { @@ -63,12 +63,12 @@ where } } -/// Despawns entities marked with [`DespawnOnExitState`] when their state no +/// Despawns entities marked with [`DespawnOnExit`] when their state no /// longer matches the world state. pub fn despawn_entities_on_exit_state( mut commands: Commands, mut transitions: EventReader>, - query: Query<(Entity, &DespawnOnExitState)>, + query: Query<(Entity, &DespawnOnExit)>, ) { // We use the latest event, because state machine internals generate at most 1 // transition event (per type) each frame. No event means no change happened @@ -112,7 +112,7 @@ pub fn despawn_entities_on_exit_state( /// /// fn spawn_player(mut commands: Commands) { /// commands.spawn(( -/// DespawnOnEnterState(GameState::MainMenu), +/// DespawnOnEnter(GameState::MainMenu), /// Player /// )); /// } @@ -130,14 +130,14 @@ pub fn despawn_entities_on_exit_state( /// ``` #[derive(Component, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component))] -pub struct DespawnOnEnterState(pub S); +pub struct DespawnOnEnter(pub S); -/// Despawns entities marked with [`DespawnOnEnterState`] when their state +/// Despawns entities marked with [`DespawnOnEnter`] when their state /// matches the world state. pub fn despawn_entities_on_enter_state( mut commands: Commands, mut transitions: EventReader>, - query: Query<(Entity, &DespawnOnEnterState)>, + query: Query<(Entity, &DespawnOnEnter)>, ) { // We use the latest event, because state machine internals generate at most 1 // transition event (per type) each frame. No event means no change happened diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index 8321e8cb3265a..4e3ef2fa0feaa 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -64,7 +64,7 @@ impl Default for StateScopedEvents { } } -fn clear_events_on_exit_state( +fn clear_events_on_exit( mut c: Commands, mut transitions: EventReader>, ) { @@ -85,7 +85,7 @@ fn clear_events_on_exit_state( }); } -fn clear_events_on_enter_state( +fn clear_events_on_enter( mut c: Commands, mut transitions: EventReader>, ) { @@ -119,10 +119,8 @@ fn clear_events_on_state_transition( .resource_mut::>() .add_event::(state.clone(), transition_type); match transition_type { - TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit_state::), - TransitionType::OnEnter => { - app.add_systems(OnEnter(state), clear_events_on_enter_state::) - } + TransitionType::OnExit => app.add_systems(OnExit(state), clear_events_on_exit::), + TransitionType::OnEnter => app.add_systems(OnEnter(state), clear_events_on_enter::), }; } @@ -130,27 +128,27 @@ fn clear_events_on_state_transition( pub trait StateScopedEventsAppExt { /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// - /// Note that event cleanup is ambiguously ordered relative to - /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, + /// Note that event cleanup is ambiguously ordered relative to + /// [`DespawnOnExit`](crate::prelude::DespawnOnExit) entity cleanup, /// and the [`OnExit`] schedule for the target state. /// All of these (state scoped entities and events cleanup, and `OnExit`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_exit(&mut self, state: impl States) -> &mut Self; /// Clears an [`BufferedEvent`] when entering the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to - /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, + /// [`DespawnOnEnter`](crate::prelude::DespawnOnEnter) entity cleanup, /// and the [`OnEnter`] schedule for the target state. /// All of these (state scoped entities and events cleanup, and `OnEnter`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::EnterSchedules`. - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_enter(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -160,7 +158,7 @@ impl StateScopedEventsAppExt for App { self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -172,12 +170,12 @@ impl StateScopedEventsAppExt for App { } impl StateScopedEventsAppExt for SubApp { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } @@ -211,7 +209,7 @@ mod tests { app.add_event::(); app.add_event::() - .clear_events_on_exit_state::(TestState::A); + .clear_events_on_exit::(TestState::A); app.world_mut().write_event(StandardEvent).unwrap(); app.world_mut().write_event(StateScopedEvent).unwrap(); @@ -241,7 +239,7 @@ mod tests { app.add_event::(); app.add_event::() - .clear_events_on_enter_state::(TestState::B); + .clear_events_on_enter::(TestState::B); app.world_mut().write_event(StandardEvent).unwrap(); app.world_mut().write_event(StateScopedEvent).unwrap(); diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index a476ca78e2776..7974e0c82cb23 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -22,7 +22,7 @@ multi_threaded = [ # Uses `async-executor` as a task execution backend. # This backend is incompatible with `no_std` targets. -async_executor = ["bevy_platform/std", "dep:async-executor"] +async_executor = ["bevy_platform/std", "dep:async-executor", "futures-lite"] # Provide an implementation of `block_on` from `futures-lite`. futures-lite = ["bevy_platform/std", "futures-lite/std"] diff --git a/crates/bevy_tasks/src/futures.rs b/crates/bevy_tasks/src/futures.rs index 7bc6c59c00f5c..3f0c72c890ef5 100644 --- a/crates/bevy_tasks/src/futures.rs +++ b/crates/bevy_tasks/src/futures.rs @@ -1,24 +1,17 @@ -#![expect(unsafe_code, reason = "Futures require unsafe code.")] - //! Utilities for working with [`Future`]s. use core::{ future::Future, - pin::Pin, - task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, + pin::pin, + task::{Context, Poll, Waker}, }; /// Consumes a future, polls it once, and immediately returns the output /// or returns `None` if it wasn't ready yet. /// /// This will cancel the future if it's not ready. -pub fn now_or_never(mut future: F) -> Option { - let noop_waker = noop_waker(); - let mut cx = Context::from_waker(&noop_waker); - - // SAFETY: `future` is not moved and the original value is shadowed - let future = unsafe { Pin::new_unchecked(&mut future) }; - - match future.poll(&mut cx) { +pub fn now_or_never(future: F) -> Option { + let mut cx = Context::from_waker(Waker::noop()); + match pin!(future).poll(&mut cx) { Poll::Ready(x) => Some(x), _ => None, } @@ -27,30 +20,5 @@ pub fn now_or_never(mut future: F) -> Option { /// Polls a future once, and returns the output if ready /// or returns `None` if it wasn't ready yet. pub fn check_ready(future: &mut F) -> Option { - let noop_waker = noop_waker(); - let mut cx = Context::from_waker(&noop_waker); - - let future = Pin::new(future); - - match future.poll(&mut cx) { - Poll::Ready(x) => Some(x), - _ => None, - } -} - -fn noop_clone(_data: *const ()) -> RawWaker { - noop_raw_waker() -} -fn noop(_data: *const ()) {} - -const NOOP_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop); - -fn noop_raw_waker() -> RawWaker { - RawWaker::new(core::ptr::null(), &NOOP_WAKER_VTABLE) -} - -pub(crate) fn noop_waker() -> Waker { - // SAFETY: the `RawWakerVTable` is just a big noop and doesn't violate any of the rules in `RawWakerVTable`s documentation - // (which talks about retaining and releasing any "resources", of which there are none in this case) - unsafe { Waker::from_raw(noop_raw_waker()) } + now_or_never(future) } diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index ddb014bb9867b..7ec4f3748c692 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -132,8 +132,7 @@ cfg::switch! { let mut future = core::pin::pin!(future); // We don't care about the waker as we're just going to poll as fast as possible. - let waker = futures::noop_waker(); - let cx = &mut Context::from_waker(&waker); + let cx = &mut Context::from_waker(core::task::Waker::noop()); // Keep polling until the future is ready. loop { diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 25255a1e5d1d3..eb6f8502b3d80 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -384,12 +384,12 @@ impl TaskPool { unsafe { mem::transmute(external_executor) }; // SAFETY: As above, all futures must complete in this function so we can change the lifetime let scope_executor: &'env ThreadExecutor<'env> = unsafe { mem::transmute(scope_executor) }; - let spawned: ConcurrentQueue>>> = + let spawned: ConcurrentQueue>>> = ConcurrentQueue::unbounded(); // shadow the variable so that the owned value cannot be used for the rest of the function // SAFETY: As above, all futures must complete in this function so we can change the lifetime let spawned: &'env ConcurrentQueue< - FallibleTask>>, + FallibleTask>>, > = unsafe { mem::transmute(&spawned) }; let scope = Scope { @@ -628,7 +628,7 @@ pub struct Scope<'scope, 'env: 'scope, T> { executor: &'scope crate::executor::Executor<'scope>, external_executor: &'scope ThreadExecutor<'scope>, scope_executor: &'scope ThreadExecutor<'scope>, - spawned: &'scope ConcurrentQueue>>>, + spawned: &'scope ConcurrentQueue>>>, // make `Scope` invariant over 'scope and 'env scope: PhantomData<&'scope mut &'scope ()>, env: PhantomData<&'env mut &'env ()>, diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index a9e3a8939d2f4..cecbbf46fc2fa 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -22,12 +22,7 @@ bevy_image = { path = "../bevy_image", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" } -bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" } -bevy_render = { path = "../bevy_render", version = "0.17.0-dev" } -bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" } bevy_time = { path = "../bevy_time", version = "0.17.0-dev" } -bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } -bevy_window = { path = "../bevy_window", version = "0.17.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" } bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [ "std", @@ -35,17 +30,14 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea ] } # other +wgpu-types = { version = "26", default-features = false } cosmic-text = { version = "0.14", features = ["shape-run-cache"] } -cosmic_undo_2 = { version = "0.2.0" } thiserror = { version = "2", default-features = false } serde = { version = "1", features = ["derive"] } smallvec = { version = "1", default-features = false } sys-locale = "0.3.0" tracing = { version = "0.1", default-features = false, features = ["std"] } -[dev-dependencies] -approx = "0.5.1" - [lints] workspace = true diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 67a4703a59e2e..6cb03f4e66130 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -2,7 +2,7 @@ use bevy_asset::{Assets, Handle, RenderAssetUsages}; use bevy_image::{prelude::*, ImageSampler, ToExtents}; use bevy_math::{IVec2, UVec2}; use bevy_platform::collections::HashMap; -use bevy_render::render_resource::{TextureDimension, TextureFormat}; +use wgpu_types::{TextureDimension, TextureFormat}; use crate::{FontSmoothing, GlyphAtlasLocation, TextError}; diff --git a/crates/bevy_text/src/font_atlas_set.rs b/crates/bevy_text/src/font_atlas_set.rs index 7b3167cdc0abe..fd824041b4ac0 100644 --- a/crates/bevy_text/src/font_atlas_set.rs +++ b/crates/bevy_text/src/font_atlas_set.rs @@ -4,7 +4,7 @@ use bevy_image::prelude::*; use bevy_math::{IVec2, UVec2}; use bevy_platform::collections::HashMap; use bevy_reflect::TypePath; -use bevy_render::render_resource::{Extent3d, TextureDimension, TextureFormat}; +use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; use crate::{error::TextError, Font, FontAtlas, FontSmoothing, GlyphAtlasInfo}; diff --git a/crates/bevy_text/src/input.rs b/crates/bevy_text/src/input.rs deleted file mode 100644 index b16a200c1773f..0000000000000 --- a/crates/bevy_text/src/input.rs +++ /dev/null @@ -1,1382 +0,0 @@ -use std::time::Duration; - -use crate::buffer_dimensions; -use crate::load_font_to_fontdb; -use crate::CosmicFontSystem; -use crate::Font; -use crate::FontAtlasSets; -use crate::FontSmoothing; -use crate::Justify; -use crate::LineBreak; -use crate::LineHeight; -use crate::PositionedGlyph; -use crate::TextBounds; -use crate::TextError; -use crate::TextFont; -use crate::TextLayoutInfo; -use crate::TextPipeline; -use alloc::collections::VecDeque; -use bevy_asset::Assets; -use bevy_asset::Handle; -use bevy_derive::Deref; -use bevy_derive::DerefMut; -use bevy_ecs::change_detection::DetectChanges; -use bevy_ecs::change_detection::DetectChangesMut; -use bevy_ecs::component::Component; -use bevy_ecs::entity::Entity; -use bevy_ecs::event::EntityEvent; -use bevy_ecs::hierarchy::ChildOf; -use bevy_ecs::lifecycle::HookContext; -use bevy_ecs::prelude::ReflectComponent; -use bevy_ecs::query::Changed; -use bevy_ecs::query::Or; -use bevy_ecs::resource::Resource; -use bevy_ecs::schedule::SystemSet; -use bevy_ecs::system::Commands; -use bevy_ecs::system::Query; -use bevy_ecs::system::Res; -use bevy_ecs::system::ResMut; -use bevy_ecs::world::DeferredWorld; -use bevy_ecs::world::Ref; -use bevy_image::Image; -use bevy_image::TextureAtlasLayout; -use bevy_math::IVec2; -use bevy_math::Rect; -use bevy_math::UVec2; -use bevy_math::Vec2; -use bevy_reflect::prelude::ReflectDefault; -use bevy_reflect::Reflect; -use bevy_time::Time; -use cosmic_text::Action; -use cosmic_text::BorrowedWithFontSystem; -use cosmic_text::Buffer; -use cosmic_text::BufferLine; -use cosmic_text::Edit; -use cosmic_text::Editor; -use cosmic_text::Metrics; -pub use cosmic_text::Motion; -use cosmic_text::Selection; -/// Systems handling text input update and layout -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -pub struct TextInputSystems; - -/// Basic clipboard implementation that only works within the bevy app. -#[derive(Resource, Default)] -pub struct Clipboard(pub String); - -/// Get the text from a cosmic text buffer -fn get_cosmic_text_buffer_contents(buffer: &Buffer) -> String { - buffer - .lines - .iter() - .map(BufferLine::text) - .fold(String::new(), |mut out, line| { - if !out.is_empty() { - out.push('\n'); - } - out.push_str(line); - out - }) -} - -/// The text input buffer. -/// Primary component that contains the text layout. -/// -/// The `needs_redraw` method can be used to check if the buffer's contents have changed and need redrawing. -/// Component change detection is not reliable as the editor buffer needs to be borrowed mutably during updates. -#[derive(Component, Debug)] -#[require(TextInputAttributes, TextInputTarget, TextEdits, TextLayoutInfo)] -pub struct TextInputBuffer { - /// The cosmic text editor buffer. - pub editor: Editor<'static>, - /// Space advance width for the current font, used to determine the width of the cursor when it is at the end of a line - /// or when the buffer is empty. - pub space_advance: f32, - /// Controls cursor blinking. - /// If the value is none or greater than the `blink_interval` in `TextCursorStyle` then the cursor - /// is not displayed. - /// The timer is reset when a `TextEdit` is applied. - pub cursor_blink_timer: Option, -} - -impl Default for TextInputBuffer { - fn default() -> Self { - Self { - editor: Editor::new(Buffer::new_empty(Metrics::new(20.0, 20.0))), - space_advance: 20., - cursor_blink_timer: None, - } - } -} - -impl TextInputBuffer { - /// Use the cosmic text buffer mutably - pub fn with_buffer_mut(&mut self, f: F) -> T - where - F: FnOnce(&mut Buffer) -> T, - { - self.editor.with_buffer_mut(f) - } - - /// Use the cosmic text buffer - pub fn with_buffer(&self, f: F) -> T - where - F: FnOnce(&Buffer) -> T, - { - self.editor.with_buffer(f) - } - - /// True if the buffer is empty - pub fn is_empty(&self) -> bool { - self.with_buffer(|buffer| { - buffer.lines.is_empty() - || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) - }) - } - - /// Get the text contained in the text buffer - pub fn get_text(&self) -> String { - self.editor.with_buffer(get_cosmic_text_buffer_contents) - } - - /// Returns true if the buffer's contents have changed and need to be redrawn. - pub fn needs_redraw(&self) -> bool { - self.editor.redraw() - } -} - -/// Component containing the change history for a text input. -/// Text input entities without this component will ignore undo and redo actions. -#[derive(Component, Debug, Default)] -pub struct UndoHistory { - /// The commands to undo and undo - pub changes: cosmic_undo_2::Commands, -} - -impl UndoHistory { - /// Clear the history for the text input - pub fn clear(&mut self) { - self.changes.clear(); - } -} - -/// Details of the target the text input will be rendered to -#[derive(Component, PartialEq, Debug, Default)] -pub struct TextInputTarget { - /// Size of the target in physical pixels - pub size: Vec2, - /// Scale factor of the target - pub scale_factor: f32, -} - -impl TextInputTarget { - /// Returns true if the target has zero or negative size. - pub fn is_empty(&self) -> bool { - (self.scale_factor * self.size).cmple(Vec2::ZERO).all() - } -} - -/// Contains the current text in the text input buffer. -/// Automatically synchronized with the buffer by [`apply_text_edits`] after any edits are applied. -/// On insertion, replaces the current text in the text buffer. -#[derive(Component, PartialEq, Debug, Default, Deref)] -#[component( - on_insert = on_insert_text_input_value, -)] -pub struct TextInputValue(String); - -impl TextInputValue { - /// New text, when inserted replaces the current text in the text buffer - pub fn new(value: impl Into) -> Self { - Self(value.into()) - } - - /// Get the current text - pub fn get(&self) -> &str { - &self.0 - } -} - -/// Set the text input with the text from the `TextInputValue` when inserted. -fn on_insert_text_input_value(mut world: DeferredWorld, context: HookContext) { - if let Some(value) = world.get::(context.entity) { - let value = value.0.clone(); - if let Some(mut actions) = world.entity_mut(context.entity).get_mut::() { - actions.queue(TextEdit::SetText(value)); - } - } -} - -/// The time taken for the cursor to blink -#[derive(Resource)] -pub struct TextCursorBlinkInterval(pub Duration); - -impl Default for TextCursorBlinkInterval { - fn default() -> Self { - Self(Duration::from_secs_f32(0.5)) - } -} - -/// Common text input properties set by the user. -/// On changes, the text input systems will automatically update the buffer, layout and fonts as required. -#[derive(Component, Debug, PartialEq)] -pub struct TextInputAttributes { - /// The text input's font, also used for any [`Placeholder`] text or password mask. - /// A text input's glyphs must all be from the same font. - pub font: Handle, - /// The size of the font. - /// A text input's glyphs must all be the same size. - pub font_size: f32, - /// The height of each line. - /// A text input's lines must all be the same height. - pub line_height: LineHeight, - /// Determines how lines will be broken - pub line_break: LineBreak, - /// The horizontal alignment for all the text in the text input buffer. - pub justify: Justify, - /// Controls text antialiasing - pub font_smoothing: FontSmoothing, - /// Maximum number of glyphs the text input buffer can contain. - /// Any edits that extend the length above `max_chars` are ignored. - /// If set on a buffer longer than `max_chars` the buffer will be truncated. - pub max_chars: Option, - /// The maximum number of lines the buffer will display without scrolling. - /// * Clamped between zero and target height divided by line height. - /// * If None or equal or less than 0, will fill the target space. - /// * Only restricts the maximum number of visible lines, places no constraint on the text buffer's length. - /// * Supports fractional values, `visible_lines: Some(2.5)` will display two and a half lines of text. - pub visible_lines: Option, - /// Clear on submit (Triggered when [`apply_text_edits`] receives a [`TextEdit::Submit`] edit for an entity). - pub clear_on_submit: bool, -} - -impl Default for TextInputAttributes { - fn default() -> Self { - Self { - font: Default::default(), - font_size: 20., - line_height: LineHeight::RelativeToFont(1.2), - font_smoothing: Default::default(), - justify: Default::default(), - line_break: Default::default(), - max_chars: None, - visible_lines: None, - clear_on_submit: false, - } - } -} - -/// If a text input entity has a `TextInputFilter` component, after each [TextEdit] is applied, the [TextInputBuffer]’s text is checked -/// against the filter, and if it fails, the `TextEdit is rolled back. -#[derive(Component)] -pub enum TextInputFilter { - /// Positive integer input - /// accepts only digits - PositiveInteger, - /// Integer input - /// accepts only digits and a leading sign - Integer, - /// Decimal input - /// accepts only digits, a decimal point and a leading sign - Decimal, - /// Hexadecimal input - /// accepts only `0-9`, `a-f` and `A-F` - Hex, - /// Alphanumeric input - /// accepts only `0-9`, `a-z` and `A-Z` - Alphanumeric, - /// Custom filter - Custom(Box bool + Send + Sync>), -} - -impl TextInputFilter { - /// Returns `true` if the given `text` passes the filter - pub fn is_match(&self, text: &str) -> bool { - // Always passes if the input is empty unless using a custom filter - if text.is_empty() && !matches!(self, Self::Custom(_)) { - return true; - } - - match self { - TextInputFilter::PositiveInteger => text.chars().all(|c| c.is_ascii_digit()), - TextInputFilter::Integer => text - .strip_prefix('-') - .unwrap_or(text) - .chars() - .all(|c| c.is_ascii_digit()), - TextInputFilter::Decimal => text - .strip_prefix('-') - .unwrap_or(text) - .chars() - .try_fold(true, |is_int, c| match c { - '.' if is_int => Ok(false), - c if c.is_ascii_digit() => Ok(is_int), - _ => Err(()), - }) - .is_ok(), - TextInputFilter::Hex => text.chars().all(|c| c.is_ascii_hexdigit()), - TextInputFilter::Alphanumeric => text.chars().all(|c| c.is_ascii_alphanumeric()), - TextInputFilter::Custom(is_match) => is_match(text), - } - } - - /// Create a custom filter - pub fn custom(filter_fn: impl Fn(&str) -> bool + Send + Sync + 'static) -> Self { - Self::Custom(Box::new(filter_fn)) - } -} - -/// Add this component to hide the text input buffer contents -/// by replacing the characters with `mask_char`. -/// -/// It is strongly recommended to only use a `PasswordMask` with fixed-width fonts. -/// With variable width fonts mouse picking and horizontal scrolling -/// may not work correctly. -#[derive(Component)] -pub struct PasswordMask { - /// If true the password will not be hidden - pub show_password: bool, - /// Char that will replace the masked input characters, by default `*` - pub mask_char: char, - /// Buffer mirroring the actual text input buffer but only containing `mask_char`s - editor: Editor<'static>, -} - -impl Default for PasswordMask { - fn default() -> Self { - Self { - show_password: false, - mask_char: '*', - editor: Editor::new(Buffer::new_empty(Metrics::new(20.0, 20.0))), - } - } -} - -/// Text input commands queue -#[derive(Component, Default)] -pub struct TextEdits { - /// Commands to be applied before the text input is updated - pub queue: VecDeque, -} - -impl TextEdits { - /// queue an action - pub fn queue(&mut self, command: TextEdit) { - self.queue.push_back(command); - } -} - -/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. -#[derive(Debug, Clone)] -pub enum TextEdit { - /// Copy the selected text into the clipboard. Does nothing if no text is selected. - Copy, - /// Copy the selected text into the clipboard, then delete the selected text. Does nothing if no text is selected. - Cut, - /// Insert the contents of the clipboard at the current cursor position. Does nothing if the clipboard is empty. - Paste, - /// Move the cursor with some motion. - Motion { - /// The motion to perform. - motion: Motion, - /// Select the text from the initial cursor position to the end of the motion. - with_select: bool, - }, - /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. - Insert(char), - /// Set the character at the cursor, overwriting the previous character. Inserts if cursor is at the end of a line. - /// If there is a selection, replaces the selection with the character instead. - Overwrite(char), - /// Insert a string at the cursor. If there is a selection, replaces the selection with the string instead. - InsertString(String), - /// Start a new line. Ignored for single line text inputs. - NewLine, - /// Delete the character behind the cursor. - /// If there is a selection, deletes the selection instead. - Backspace, - /// Delete the character at the cursor. - /// If there is a selection, deletes the selection instead. - Delete, - /// Indent at the cursor. - Indent, - /// Unindent at the cursor. - Unindent, - /// Moves the cursor to the character at the given position. - Click(IVec2), - /// Selects the word at the given position. - DoubleClick(IVec2), - /// Selects the line at the given position. - TripleClick(IVec2), - /// Select the text up to the given position - Drag(IVec2), - /// Scroll vertically by the given number of lines. - /// Negative values scroll upwards towards the start of the text, positive downwards to the end of the text. - Scroll { - /// Number of lines to scroll. - /// Negative values scroll upwards towards the start of the text, positive downwards to the end of the text. - lines: i32, - }, - /// Undo the previous action. - Undo, - /// Redo an undone action. Must directly follow an Undo. - Redo, - /// Select the entire contents of the text input buffer. - SelectAll, - /// Select the line at the cursor. - SelectLine, - /// Clear any selection. - Escape, - /// Clear the text input buffer. - Clear, - /// Set the contents of the text input buffer. The existing contents are discarded. - SetText(String), - /// Submit the contents of the text input buffer - Submit, -} - -impl TextEdit { - /// An action that moves the cursor. - /// If `with_select` is true, it selects as it moves - pub fn motion(motion: Motion, with_select: bool) -> Self { - Self::Motion { - motion, - with_select, - } - } -} - -/// apply a motion action to the editor buffer -pub fn apply_motion<'a>( - editor: &mut BorrowedWithFontSystem>, - shift_pressed: bool, - motion: Motion, -) { - if shift_pressed { - if editor.selection() == Selection::None { - let cursor = editor.cursor(); - editor.set_selection(Selection::Normal(cursor)); - } - } else { - editor.action(Action::Escape); - } - editor.action(Action::Motion(motion)); -} - -/// Returns true if the cursor is at the end of a line -pub fn is_cursor_at_end_of_line(editor: &mut Editor<'_>) -> bool { - let cursor = editor.cursor(); - editor.with_buffer(|buffer| { - buffer - .lines - .get(cursor.line) - .map(|line| cursor.index == line.text().len()) - .unwrap_or(false) - }) -} - -/// Apply an action from the undo history to the text input buffer. -fn apply_action<'a>( - editor: &mut BorrowedWithFontSystem>, - action: cosmic_undo_2::Action<&cosmic_text::Change>, -) { - match action { - cosmic_undo_2::Action::Do(change) => { - editor.apply_change(change); - } - cosmic_undo_2::Action::Undo(change) => { - let mut reversed = change.clone(); - reversed.reverse(); - editor.apply_change(&reversed); - } - } - editor.set_redraw(true); -} - -/// Applies the [`TextEdit`]s queued for each [`TextInputBuffer`] and emits [`TextInputEvent`]s in response. -/// -/// After all edits are applied, if a text input entity has a [TextInputValue] component and its buffer was changed, -/// then the [TextInputValue]'s text is updated with the new contents of the [TextInputBuffer]. -pub fn apply_text_edits( - mut commands: Commands, - mut font_system: ResMut, - mut text_input_query: Query<( - Entity, - &mut TextInputBuffer, - &mut TextEdits, - &TextInputAttributes, - Option<&TextInputFilter>, - Option<&mut UndoHistory>, - Option<&mut TextInputValue>, - )>, - mut clipboard: ResMut, -) { - for ( - entity, - mut buffer, - mut text_input_actions, - attribs, - maybe_filter, - mut maybe_history, - maybe_value, - ) in text_input_query.iter_mut() - { - for edit in text_input_actions.queue.drain(..) { - match edit { - TextEdit::Submit => { - commands.trigger_targets( - TextInputEvent::Submission { - text: buffer.get_text(), - }, - entity, - ); - - if attribs.clear_on_submit { - let _ = apply_text_edit( - buffer.editor.borrow_with(&mut font_system), - maybe_history.as_mut().map(AsMut::as_mut), - maybe_filter, - attribs.max_chars, - &mut clipboard, - &TextEdit::Clear, - ); - - if let Some(history) = maybe_history.as_mut() { - history.clear(); - } - } - } - edit => { - if let Err(error) = apply_text_edit( - buffer.editor.borrow_with(&mut font_system), - maybe_history.as_mut().map(AsMut::as_mut), - maybe_filter, - attribs.max_chars, - &mut clipboard, - &edit, - ) { - commands.trigger_targets(TextInputEvent::InvalidEdit(error, edit), entity); - } - } - } - } - - if let Some(mut value) = maybe_value { - let contents = buffer.get_text(); - if value.0 != contents { - value.0 = contents; - commands.trigger_targets(TextInputEvent::TextChanged, entity); - } - } - } -} - -/// Updates the text input buffer in response to changes -/// that require regeneration of the the buffer's -/// metrics and attributes. -pub fn update_text_input_buffers( - mut text_input_query: Query<( - &mut TextInputBuffer, - Ref, - &TextEdits, - Ref, - )>, - time: Res