Merge branch 'main' into http-asset-sources

This commit is contained in:
François Mockers 2025-06-28 23:16:44 +02:00 committed by GitHub
commit de3e2b62dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
957 changed files with 37995 additions and 16458 deletions

2
.github/FUNDING.yml vendored
View File

@ -1 +1 @@
custom: https://bevyengine.org/donate/ custom: https://bevy.org/donate/

View File

@ -10,4 +10,4 @@ assignees: ''
Provide a link to the documentation and describe how it could be improved. In what ways is it incomplete, incorrect, or misleading? Provide a link to the documentation and describe how it could be improved. In what ways is it incomplete, incorrect, or misleading?
If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevyengine.org/learn/contribute/helping-out/writing-docs/) instead. If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevy.org/learn/contribute/helping-out/writing-docs/) instead.

View File

@ -13,9 +13,9 @@ env:
CARGO_PROFILE_TEST_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0
CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_DEV_DEBUG: 0
# If nightly is breaking CI, modify this variable to target a specific nightly version. # If nightly is breaking CI, modify this variable to target a specific nightly version.
NIGHTLY_TOOLCHAIN: nightly-2025-05-16 # pinned until a fix for https://github.com/rust-lang/miri/issues/4323 is released NIGHTLY_TOOLCHAIN: nightly
RUSTFLAGS: "-D warnings" RUSTFLAGS: "-D warnings"
BINSTALL_VERSION: "v1.12.3" BINSTALL_VERSION: "v1.12.5"
concurrency: concurrency:
group: ${{github.workflow}}-${{github.ref}} group: ${{github.workflow}}-${{github.ref}}
@ -95,7 +95,7 @@ jobs:
- name: CI job - name: CI job
# To run the tests one item at a time for troubleshooting, use # To run the tests one item at a time for troubleshooting, use
# cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact # cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact
run: cargo miri test -p bevy_ecs run: cargo miri test -p bevy_ecs --features bevy_utils/debug
env: env:
# -Zrandomize-layout makes sure we dont rely on the layout of anything that might change # -Zrandomize-layout makes sure we dont rely on the layout of anything that might change
RUSTFLAGS: -Zrandomize-layout RUSTFLAGS: -Zrandomize-layout
@ -247,7 +247,7 @@ jobs:
- name: Check wasm - name: Check wasm
run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort
env: env:
RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings" RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory"
markdownlint: markdownlint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -272,7 +272,7 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: cargo-bins/cargo-binstall@v1.12.3 - uses: cargo-bins/cargo-binstall@v1.12.5
- name: Install taplo - name: Install taplo
run: cargo binstall taplo-cli@0.9.3 --locked run: cargo binstall taplo-cli@0.9.3 --locked
- name: Run Taplo - name: Run Taplo
@ -293,7 +293,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check for typos - name: Check for typos
uses: crate-ci/typos@v1.32.0 uses: crate-ci/typos@v1.33.1
- name: Typos info - name: Typos info
if: failure() if: failure()
run: | run: |

View File

@ -82,7 +82,7 @@ jobs:
- name: Finalize documentation - name: Finalize documentation
run: | run: |
echo "<meta http-equiv=\"refresh\" content=\"0; url=bevy/index.html\">" > target/doc/index.html echo "<meta http-equiv=\"refresh\" content=\"0; url=bevy/index.html\">" > target/doc/index.html
echo "dev-docs.bevyengine.org" > target/doc/CNAME echo "dev-docs.bevy.org" > target/doc/CNAME
echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt
rm target/doc/.lock rm target/doc/.lock

View File

@ -49,7 +49,8 @@ jobs:
--exclude ci \ --exclude ci \
--exclude errors \ --exclude errors \
--exclude bevy_mobile_example \ --exclude bevy_mobile_example \
--exclude build-wasm-example --exclude build-wasm-example \
--exclude no_std_library
- name: Create PR - name: Create PR
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v7

View File

@ -43,5 +43,5 @@ jobs:
repo: context.repo.repo, repo: context.repo.repo,
body: `**Welcome**, new contributor! body: `**Welcome**, new contributor!
Please make sure you've read our [contributing guide](https://bevyengine.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` Please make sure you've read our [contributing guide](https://bevy.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨`
}) })

View File

@ -1,4 +1,4 @@
# Contributing to Bevy # Contributing to Bevy
If you'd like to help build Bevy, start by reading this If you'd like to help build Bevy, start by reading this
[introduction](https://bevyengine.org/learn/contribute/introduction). Thanks for contributing! [introduction](https://bevy.org/learn/contribute/introduction). Thanks for contributing!

View File

@ -1,16 +1,16 @@
[package] [package]
name = "bevy" name = "bevy"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
categories = ["game-engines", "graphics", "gui", "rendering"] categories = ["game-engines", "graphics", "gui", "rendering"]
description = "A refreshingly simple data-driven game engine and app framework" description = "A refreshingly simple data-driven game engine and app framework"
exclude = ["assets/", "tools/", ".github/", "crates/", "examples/wasm/assets/"] exclude = ["assets/", "tools/", ".github/", "crates/", "examples/wasm/assets/"]
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
keywords = ["game", "engine", "gamedev", "graphics", "bevy"] keywords = ["game", "engine", "gamedev", "graphics", "bevy"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
documentation = "https://docs.rs/bevy" documentation = "https://docs.rs/bevy"
rust-version = "1.85.0" rust-version = "1.86.0"
[workspace] [workspace]
resolver = "2" resolver = "2"
@ -133,6 +133,7 @@ default = [
"bevy_audio", "bevy_audio",
"bevy_color", "bevy_color",
"bevy_core_pipeline", "bevy_core_pipeline",
"bevy_core_widgets",
"bevy_anti_aliasing", "bevy_anti_aliasing",
"bevy_gilrs", "bevy_gilrs",
"bevy_gizmos", "bevy_gizmos",
@ -163,6 +164,8 @@ default = [
"vorbis", "vorbis",
"webgl2", "webgl2",
"x11", "x11",
"debug",
"zstd_rust",
] ]
# Recommended defaults for no_std applications # Recommended defaults for no_std applications
@ -245,6 +248,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"]
# Provides scene functionality # Provides scene functionality
bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"]
# Provides raytraced lighting (experimental)
bevy_solari = [
"bevy_internal/bevy_solari",
"bevy_asset",
"bevy_core_pipeline",
"bevy_pbr",
"bevy_render",
]
# Provides sprite functionality # Provides sprite functionality
bevy_sprite = [ bevy_sprite = [
"bevy_internal/bevy_sprite", "bevy_internal/bevy_sprite",
@ -291,6 +303,12 @@ bevy_log = ["bevy_internal/bevy_log"]
# Enable input focus subsystem # Enable input focus subsystem
bevy_input_focus = ["bevy_internal/bevy_input_focus"] bevy_input_focus = ["bevy_internal/bevy_input_focus"]
# Headless widget collection for Bevy UI.
bevy_core_widgets = ["bevy_internal/bevy_core_widgets"]
# Feathers widget collection.
experimental_bevy_feathers = ["bevy_internal/bevy_feathers"]
# Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation)
spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"]
@ -316,6 +334,9 @@ trace = ["bevy_internal/trace", "dep:tracing"]
# Basis Universal compressed texture support # Basis Universal compressed texture support
basis-universal = ["bevy_internal/basis-universal"] basis-universal = ["bevy_internal/basis-universal"]
# Enables compressed KTX2 UASTC texture output on the asset processor
compressed_image_saver = ["bevy_internal/compressed_image_saver"]
# BMP image format support # BMP image format support
bmp = ["bevy_internal/bmp"] bmp = ["bevy_internal/bmp"]
@ -364,8 +385,11 @@ webp = ["bevy_internal/webp"]
# For KTX2 supercompression # For KTX2 supercompression
zlib = ["bevy_internal/zlib"] zlib = ["bevy_internal/zlib"]
# For KTX2 supercompression # For KTX2 Zstandard decompression using pure rust [ruzstd](https://crates.io/crates/ruzstd). This is the safe default. For maximum performance, use "zstd_c".
zstd = ["bevy_internal/zstd"] zstd_rust = ["bevy_internal/zstd_rust"]
# For KTX2 Zstandard decompression using [zstd](https://crates.io/crates/zstd). This is a faster backend, but uses unsafe C bindings. For the safe option, stick to the default backend with "zstd_rust".
zstd_c = ["bevy_internal/zstd_c"]
# FLAC audio format support # FLAC audio format support
flac = ["bevy_internal/flac"] flac = ["bevy_internal/flac"]
@ -434,7 +458,7 @@ android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"]
detailed_trace = ["bevy_internal/detailed_trace"] 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`. # Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method for your `Camera2d` or `Camera3d`.
tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "zstd"] tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "bevy_image/zstd"]
# Include SMAA Look Up Tables KTX2 Files # Include SMAA Look Up Tables KTX2 Files
smaa_luts = ["bevy_internal/smaa_luts"] smaa_luts = ["bevy_internal/smaa_luts"]
@ -499,7 +523,10 @@ http_source = ["bevy_internal/http_source"]
http_source_cache = ["bevy_internal/http_source_cache"] http_source_cache = ["bevy_internal/http_source_cache"]
# Enable stepping-based debugging of Bevy systems # Enable stepping-based debugging of Bevy systems
bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] bevy_debug_stepping = [
"bevy_internal/bevy_debug_stepping",
"bevy_internal/debug",
]
# Enables the meshlet renderer for dense high-poly scenes (experimental) # Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = ["bevy_internal/meshlet"] meshlet = ["bevy_internal/meshlet"]
@ -540,30 +567,41 @@ libm = ["bevy_internal/libm"]
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures. # Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
web = ["bevy_internal/web"] web = ["bevy_internal/web"]
# Enable hotpatching of Bevy systems
hotpatching = ["bevy_internal/hotpatching"]
# Enable converting glTF coordinates to Bevy's coordinate system by default. This will be Bevy's default behavior starting in 0.18.
gltf_convert_coordinates_default = [
"bevy_internal/gltf_convert_coordinates_default",
]
# Enable collecting debug information about systems and components to help with diagnostics
debug = ["bevy_internal/debug"]
[dependencies] [dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } bevy_internal = { path = "crates/bevy_internal", version = "0.17.0-dev", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true } tracing = { version = "0.1", default-features = false, optional = true }
# Wasm does not support dynamic linking. # Wasm does not support dynamic linking.
[target.'cfg(not(target_family = "wasm"))'.dependencies] [target.'cfg(not(target_family = "wasm"))'.dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-features = false, optional = true } bevy_dylib = { path = "crates/bevy_dylib", version = "0.17.0-dev", default-features = false, optional = true }
[dev-dependencies] [dev-dependencies]
rand = "0.8.0" rand = "0.8.0"
rand_chacha = "0.3.1" rand_chacha = "0.3.1"
ron = "0.8.0" ron = "0.10"
flate2 = "1.0" flate2 = "1.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1.0.140"
bytemuck = "1.7" bytemuck = "1"
bevy_render = { path = "crates/bevy_render", version = "0.16.0-dev", default-features = false } bevy_render = { path = "crates/bevy_render", version = "0.17.0-dev", default-features = false }
# The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself.
bevy_ecs = { path = "crates/bevy_ecs", version = "0.16.0-dev", default-features = false } bevy_ecs = { path = "crates/bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_state = { path = "crates/bevy_state", version = "0.16.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.16.0-dev", default-features = false } bevy_asset = { path = "crates/bevy_asset", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "crates/bevy_reflect", version = "0.16.0-dev", default-features = false } bevy_reflect = { path = "crates/bevy_reflect", version = "0.17.0-dev", default-features = false }
bevy_image = { path = "crates/bevy_image", version = "0.16.0-dev", default-features = false } bevy_image = { path = "crates/bevy_image", version = "0.17.0-dev", default-features = false }
bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.16.0-dev", default-features = false } bevy_gizmos = { path = "crates/bevy_gizmos", version = "0.17.0-dev", default-features = false }
# Needed to poll Task examples # Needed to poll Task examples
futures-lite = "2.0.1" futures-lite = "2.0.1"
async-std = "1.13" async-std = "1.13"
@ -575,7 +613,7 @@ hyper = { version = "1", features = ["server", "http1"] }
http-body-util = "0.1" http-body-util = "0.1"
anyhow = "1" anyhow = "1"
macro_rules_attribute = "0.2" macro_rules_attribute = "0.2"
accesskit = "0.18" accesskit = "0.19"
nonmax = "0.5" nonmax = "0.5"
[target.'cfg(not(target_family = "wasm"))'.dev-dependencies] [target.'cfg(not(target_family = "wasm"))'.dev-dependencies]
@ -588,6 +626,17 @@ ureq = { version = "3", features = ["json"] }
wasm-bindgen = { version = "0.2" } wasm-bindgen = { version = "0.2" }
web-sys = { version = "0.3", features = ["Window"] } web-sys = { version = "0.3", features = ["Window"] }
[[example]]
name = "context_menu"
path = "examples/usage/context_menu.rs"
doc-scrape-examples = true
[package.metadata.example.context_menu]
name = "Context Menu"
description = "Example of a context menu"
category = "Usage"
wasm = true
[[example]] [[example]]
name = "hello_world" name = "hello_world"
path = "examples/hello_world.rs" path = "examples/hello_world.rs"
@ -817,6 +866,17 @@ description = "Generates a texture atlas (sprite sheet) from individual sprites"
category = "2D Rendering" category = "2D Rendering"
wasm = false wasm = false
[[example]]
name = "tilemap_chunk"
path = "examples/2d/tilemap_chunk.rs"
doc-scrape-examples = true
[package.metadata.example.tilemap_chunk]
name = "Tilemap Chunk"
description = "Renders a tilemap chunk"
category = "2D Rendering"
wasm = true
[[example]] [[example]]
name = "transparency_2d" name = "transparency_2d"
path = "examples/2d/transparency_2d.rs" path = "examples/2d/transparency_2d.rs"
@ -1005,6 +1065,17 @@ description = "Showcases different blend modes"
category = "3D Rendering" category = "3D Rendering"
wasm = true wasm = true
[[example]]
name = "manual_material"
path = "examples/3d/manual_material.rs"
doc-scrape-examples = true
[package.metadata.example.manual_material]
name = "Manual Material Implementation"
description = "Demonstrates how to implement a material manually using the mid-level render APIs"
category = "3D Rendering"
wasm = true
[[example]] [[example]]
name = "edit_material_on_gltf" name = "edit_material_on_gltf"
path = "examples/3d/edit_material_on_gltf.rs" path = "examples/3d/edit_material_on_gltf.rs"
@ -1248,6 +1319,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug
category = "3D Rendering" category = "3D Rendering"
wasm = false wasm = false
[[example]]
name = "solari"
path = "examples/3d/solari.rs"
doc-scrape-examples = true
required-features = ["bevy_solari"]
[package.metadata.example.solari]
name = "Solari"
description = "Demonstrates realtime dynamic raytraced lighting using Bevy Solari."
category = "3D Rendering"
wasm = false # Raytracing is not supported on the web
[[example]] [[example]]
name = "spherical_area_lights" name = "spherical_area_lights"
path = "examples/3d/spherical_area_lights.rs" path = "examples/3d/spherical_area_lights.rs"
@ -2076,6 +2159,7 @@ wasm = false
name = "dynamic" name = "dynamic"
path = "examples/ecs/dynamic.rs" path = "examples/ecs/dynamic.rs"
doc-scrape-examples = true doc-scrape-examples = true
required-features = ["debug"]
[package.metadata.example.dynamic] [package.metadata.example.dynamic]
name = "Dynamic ECS" name = "Dynamic ECS"
@ -3436,6 +3520,28 @@ description = "An example for CSS Grid layout"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true wasm = true
[[example]]
name = "gradients"
path = "examples/ui/gradients.rs"
doc-scrape-examples = true
[package.metadata.example.gradients]
name = "Gradients"
description = "An example demonstrating gradients"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "stacked_gradients"
path = "examples/ui/stacked_gradients.rs"
doc-scrape-examples = true
[package.metadata.example.stacked_gradients]
name = "Stacked Gradients"
description = "An example demonstrating stacked gradients"
category = "UI (User Interface)"
wasm = true
[[example]] [[example]]
name = "scroll" name = "scroll"
path = "examples/ui/scroll.rs" path = "examples/ui/scroll.rs"
@ -3524,6 +3630,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true wasm = true
[[example]]
name = "ui_transform"
path = "examples/ui/ui_transform.rs"
doc-scrape-examples = true
[package.metadata.example.ui_transform]
name = "UI Transform"
description = "An example demonstrating how to translate, rotate and scale UI elements."
category = "UI (User Interface)"
wasm = true
[[example]] [[example]]
name = "viewport_debug" name = "viewport_debug"
path = "examples/ui/viewport_debug.rs" path = "examples/ui/viewport_debug.rs"
@ -3907,6 +4024,16 @@ description = "A simple 2D screen shake effect"
category = "Camera" category = "Camera"
wasm = true wasm = true
[[example]]
name = "2d_on_ui"
path = "examples/camera/2d_on_ui.rs"
doc-scrape-examples = true
[package.metadata.example.2d_on_ui]
name = "2D on Bevy UI"
description = "Shows how to render 2D objects on top of Bevy UI"
category = "Camera"
wasm = true
[package.metadata.example.fps_overlay] [package.metadata.example.fps_overlay]
name = "FPS overlay" name = "FPS overlay"
@ -4375,3 +4502,61 @@ name = "Extended Bindless Material"
description = "Demonstrates bindless `ExtendedMaterial`" description = "Demonstrates bindless `ExtendedMaterial`"
category = "Shaders" category = "Shaders"
wasm = false wasm = false
[[example]]
name = "cooldown"
path = "examples/usage/cooldown.rs"
doc-scrape-examples = true
[package.metadata.example.cooldown]
name = "Cooldown"
description = "Example for cooldown on button clicks"
category = "Usage"
wasm = true
[[example]]
name = "hotpatching_systems"
path = "examples/ecs/hotpatching_systems.rs"
doc-scrape-examples = true
required-features = ["hotpatching"]
[package.metadata.example.hotpatching_systems]
name = "Hotpatching Systems"
description = "Demonstrates how to hotpatch systems"
category = "ECS (Entity Component System)"
wasm = false
[[example]]
name = "core_widgets"
path = "examples/ui/core_widgets.rs"
doc-scrape-examples = true
[package.metadata.example.core_widgets]
name = "Core Widgets"
description = "Demonstrates use of core (headless) widgets in Bevy UI"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "core_widgets_observers"
path = "examples/ui/core_widgets_observers.rs"
doc-scrape-examples = true
[package.metadata.example.core_widgets_observers]
name = "Core Widgets (w/Observers)"
description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "feathers"
path = "examples/ui/feathers.rs"
doc-scrape-examples = true
required-features = ["experimental_bevy_feathers"]
[package.metadata.example.feathers]
name = "Feathers Widgets"
description = "Gallery of Feathers Widgets"
category = "UI (User Interface)"
wasm = true
hidden = true

View File

@ -1,4 +1,4 @@
# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevyengine.org) # [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevy.org)
[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license)
[![Crates.io](https://img.shields.io/crates/v/bevy.svg)](https://crates.io/crates/bevy) [![Crates.io](https://img.shields.io/crates/v/bevy.svg)](https://crates.io/crates/bevy)
@ -13,7 +13,7 @@ Bevy is a refreshingly simple data-driven game engine built in Rust. It is free
## WARNING ## WARNING
Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevyengine.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevyengine.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevy.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevy.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment.
**MSRV:** Bevy relies heavily on improvements in the Rust language and compiler. **MSRV:** Bevy relies heavily on improvements in the Rust language and compiler.
As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust. As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust.
@ -29,15 +29,15 @@ As a result, the Minimum Supported Rust Version (MSRV) is generally close to "th
## About ## About
* **[Features](https://bevyengine.org):** A quick overview of Bevy's features. * **[Features](https://bevy.org):** A quick overview of Bevy's features.
* **[News](https://bevyengine.org/news/)**: A development blog that covers our progress, plans and shiny new features. * **[News](https://bevy.org/news/)**: A development blog that covers our progress, plans and shiny new features.
## Docs ## Docs
* **[Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. * **[Quick Start Guide](https://bevy.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy.
* **[Bevy Rust API Docs](https://docs.rs/bevy):** Bevy's Rust API docs, which are automatically generated from the doc comments in this repo. * **[Bevy Rust API Docs](https://docs.rs/bevy):** Bevy's Rust API docs, which are automatically generated from the doc comments in this repo.
* **[Official Examples](https://github.com/bevyengine/bevy/tree/latest/examples):** Bevy's dedicated, runnable examples, which are great for digging into specific concepts. * **[Official Examples](https://github.com/bevyengine/bevy/tree/latest/examples):** Bevy's dedicated, runnable examples, which are great for digging into specific concepts.
* **[Community-Made Learning Resources](https://bevyengine.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. * **[Community-Made Learning Resources](https://bevy.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community.
## Community ## Community
@ -46,11 +46,11 @@ Before contributing or participating in discussions with the community, you shou
* **[Discord](https://discord.gg/bevy):** Bevy's official discord server. * **[Discord](https://discord.gg/bevy):** Bevy's official discord server.
* **[Reddit](https://reddit.com/r/bevy):** Bevy's official subreddit. * **[Reddit](https://reddit.com/r/bevy):** Bevy's official subreddit.
* **[GitHub Discussions](https://github.com/bevyengine/bevy/discussions):** The best place for questions about Bevy, answered right here! * **[GitHub Discussions](https://github.com/bevyengine/bevy/discussions):** The best place for questions about Bevy, answered right here!
* **[Bevy Assets](https://bevyengine.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. * **[Bevy Assets](https://bevy.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials.
### Contributing ### Contributing
If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevyengine.org/learn/contribute/introduction)**. If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevy.org/learn/contribute/introduction)**.
For simple problems, feel free to [open an issue](https://github.com/bevyengine/bevy/issues) or For simple problems, feel free to [open an issue](https://github.com/bevyengine/bevy/issues) or
[PR](https://github.com/bevyengine/bevy/pulls) and tackle it yourself! [PR](https://github.com/bevyengine/bevy/pulls) and tackle it yourself!
@ -58,9 +58,9 @@ For more complex architecture decisions and experimental mad science, please ope
## Getting Started ## Getting Started
We recommend checking out the [Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction) for a brief introduction. We recommend checking out the [Quick Start Guide](https://bevy.org/learn/quick-start/introduction) for a brief introduction.
Follow the [Setup guide](https://bevyengine.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. Follow the [Setup guide](https://bevy.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly.
Once set up, you can quickly try out the [examples](https://github.com/bevyengine/bevy/tree/latest/examples) by cloning this repo and running the following commands: Once set up, you can quickly try out the [examples](https://github.com/bevyengine/bevy/tree/latest/examples) by cloning this repo and running the following commands:
```sh ```sh
@ -75,7 +75,7 @@ To draw a window with standard functionality enabled, use:
```rust ```rust
use bevy::prelude::*; use bevy::prelude::*;
fn main(){ fn main() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.run(); .run();
@ -84,7 +84,7 @@ fn main(){
### Fast Compiles ### Fast Compiles
Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevyengine.org/learn/quick-start/getting-started/setup). Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevy.org/learn/quick-start/getting-started/setup).
## [Bevy Cargo Features][cargo_features] ## [Bevy Cargo Features][cargo_features]
@ -96,7 +96,7 @@ This [list][cargo_features] outlines the different cargo features supported by B
Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on. Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on.
A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/donate/) if you like what we're building. A huge thanks to Bevy's [generous sponsors](https://bevy.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevy.org/donate/) if you like what we're building.
<!-- This next line need to stay exactly as is. It is required for BrowserStack sponsorship. --> <!-- This next line need to stay exactly as is. It is required for BrowserStack sponsorship. -->
This project is tested with BrowserStack. This project is tested with BrowserStack.

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
id="svg2321"
version="1.1"
viewBox="0 0 63.304429 63.304432"
height="63.304432mm"
width="63.304428mm"
sodipodi:docname="bevy_solari.svg"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.27643554"
inkscape:cx="54.262198"
inkscape:cy="-311.10327"
inkscape:window-width="1440"
inkscape:window-height="788"
inkscape:window-x="-6"
inkscape:window-y="-6"
inkscape:window-maximized="1"
inkscape:current-layer="svg2321" />
<defs
id="defs2315" />
<metadata
id="metadata2318">
<rdf:RDF>
<cc:Work
rdf:about="" />
</rdf:RDF>
</metadata>
<g
id="g11"
style="display:inline"
transform="translate(-27.298342,-111.49082)">
<path
style="fill:#ff8904;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1"
id="path10-7"
d="m 38.15031,109.44704 c 0.743942,3.07133 6.496307,7.00051 6.678099,10.15542 0.116231,2.01713 -5.225098,3.23914 -5.79502,5.17757 -0.891392,3.03182 2.125712,9.31077 0.705692,12.1339 -0.907907,1.80501 -6.144635,0.19263 -7.607418,1.5864 -2.287879,2.17994 -2.814466,9.12622 -5.455804,10.86111 -1.688772,1.10923 -5.417723,-2.9055 -7.381414,-2.42985 -3.071331,0.74394 -7.000511,6.49631 -10.1554234,6.6781 -2.0171309,0.11623 -3.2391354,-5.2251 -5.1775665,-5.79502 -3.03182154,-0.89139 -9.3107731,2.12571 -12.1339038,0.70569 -1.8050021,-0.9079 -0.1926248,-6.14463 -1.5863942,-7.60742 -2.1799381,-2.28787 -9.1262221,-2.81446 -10.8611151,-5.4558 -1.109224,-1.68877 2.9055,-5.41772 2.429851,-7.38141 -0.743942,-3.07133 -6.496306,-7.00051 -6.678099,-10.15543 -0.116231,-2.01713 5.225098,-3.23913 5.79502,-5.17756 0.891393,-3.03182 -2.125711,-9.31078 -0.705692,-12.13391 0.907907,-1.804999 6.144635,-0.19262 7.607418,-1.586391 2.2878791,-2.179939 2.8144662,-9.126222 5.4558046,-10.861115 1.6887711,-1.109225 5.4177222,2.905499 7.38141398,2.429851 3.07133072,-0.743942 7.00051032,-6.496307 10.15542342,-6.678099 2.01713,-0.116231 3.239135,5.225097 5.177566,5.79502 3.031822,0.891392 9.310773,-2.125712 12.133904,-0.705692 1.805002,0.907906 0.192625,6.144634 1.586394,7.607417 2.179938,2.28788 9.126222,2.814467 10.861115,5.455809 1.109224,1.68877 -2.905499,5.41772 -2.429851,7.38141 z"
transform="matrix(0.90823691,0,0,0.90823691,49.886257,35.27956)" />
<g
id="g3-6"
transform="translate(-517.96199,-278.01119)"
style="display:inline;opacity:1;fill:#ffd230;fill-opacity:1;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none">
<g
transform="matrix(-0.40426719,-0.17438247,-0.17438247,0.40426719,678.77611,389.84765)"
style="fill:#ffd230;fill-opacity:1"
id="g2-1">
<path
id="path1-4"
style="fill:#ffd230;fill-opacity:1;stroke:none;stroke-width:0.559814px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 2246.0312,2340.1914 v 0 c -0.016,3e-4 -0.031,0 -0.047,0 -0.9804,3.0675 -1.7386,6.3997 -1.8828,10.1953 -0.2712,7.1263 0.453,11.4639 -0.3672,16.0801 -0.8202,4.6163 -3.2453,9.161 -9.4141,16.2871 -7.3424,8.482 -18.9789,15.0453 -32.4199,17.2637 -2.5015,1.5971 -5.1421,3.0609 -7.9199,4.3633 10.4618,3.9385 21.4025,4.1531 30.0761,1.3066 15.2793,-5.0141 14.0962,-8.6155 20.9434,-19.1074 2.1569,-3.3051 4.6474,-5.8282 7.1484,-7.9004 7.1248,3.1068 14.1431,5.1015 18.5157,4.6074 2.351,-5.4505 -0.057,-11.7712 -4.0586,-17.7461 3.2821,-10.196 -1.6986,-20.4059 -12.7305,-24.0156 -2.8775,-0.9415 -5.4633,-1.3844 -7.8438,-1.3379 z m 8.2754,14.9707 a 4.1668789,4.2454995 48.679502 0 1 3.1973,1.3965 4.1668789,4.2454995 48.679502 0 1 -0.4375,5.9336 4.1668789,4.2454995 48.679502 0 1 -5.9394,-0.3262 4.1668789,4.2454995 48.679502 0 1 0.4375,-5.9336 4.1668789,4.2454995 48.679502 0 1 2.7421,-1.0703 z m -68.375,45.3789 c 0.1273,0.075 0.2572,0.1408 0.3848,0.2149 0.131,-0.049 0.2642,-0.1009 0.3945,-0.1504 -0.2598,-0.023 -0.5188,-0.039 -0.7793,-0.064 z"
transform="matrix(-0.55180403,-0.23802315,-0.23802315,0.55180403,1946.7322,-620.612)" />
</g>
</g>
<g
id="g5-2"
transform="matrix(-0.45399624,0.36689705,0.36689705,0.45399624,73.527335,10.816805)"
style="display:inline;opacity:1;fill:#fef3c6;fill-opacity:1;stroke:none;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
<g
id="g4-3"
transform="matrix(-0.35254083,0.28490586,0.28490586,0.35254083,477.11004,-1021.7666)"
style="opacity:1;fill:#fef3c6;fill-opacity:1">
<path
id="path2-2"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fef3c6;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.02362;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 2191.1465,2276.7832 c -5.9729,-0.035 -12.0979,2.348 -17.3613,7.459 -6.9129,6.7127 -9.0602,12.7555 -7.8477,20.2949 l 0.332,2.0684 -2.0664,-0.336 c -15.1877,-2.4609 -33.9847,-1.2178 -55.3711,7.4336 6.2868,2.6948 17.8259,7.1926 30.6309,13.3418 l 4.0605,1.9512 -4.414,0.8945 c -16.9087,3.4274 -36.9729,13.3275 -55.2989,34.9336 8.1981,-0.6372 24.9531,-2.6089 42.4278,-2.582 9.7138,0.015 19.2869,0.687 27.0859,2.709 7.7991,2.022 14.8874,6.6498 15.8861,10.0406 0.9987,3.3908 0.432,5.1761 -0.5519,7.8285 -0.9839,2.6524 -4.0098,6.6817 -8.1953,9.3418 -4.1855,2.6601 -9.4961,4.9849 -15.0137,6.9609 -11.0352,3.9521 -22.7798,6.4773 -27.9648,6.959 -1.1021,0.1024 -1.5421,0.4983 -1.9668,1.2696 -0.4247,0.7712 -0.659,1.9824 -0.6934,3.25 -0.046,1.6926 0.217,2.576 0.6949,3.246 0.4779,0.67 1.2243,0.9381 1.9934,0.9902 32.5822,2.2052 56.9441,-5.9907 74.6379,-13.0116 20.3508,-9.3311 33.2134,-27.7577 36.0058,-44.3477 1.7499,-10.395 1.3746,-15.4894 -0.3124,-19.8281 -1.6873,-4.3387 -4.9223,-8.1914 -9.0254,-15.5488 -2.6368,-4.7281 -4.1077,-9.367 -5.0196,-13.6875 l -0.1933,-0.9102 0.7265,-0.582 c 7.5403,-6.0446 13.6809,-12.6444 15.9102,-17.4492 -4.5742,-4.8648 -12.4787,-5.893 -21.3223,-4.9473 l -0.7265,0.076 -0.5118,-0.5215 c -4.7125,-4.8006 -10.5615,-7.2614 -16.5351,-7.2969 z m 2.6484,11.2324 c 2.593,-0.041 4.8808,1.7566 5.502,4.3223 0.7307,3.0216 -1.0812,6.0525 -4.0469,6.7695 -2.9656,0.7176 -5.9625,-1.1502 -6.6934,-4.1719 -0.7307,-3.0216 1.0812,-6.0525 4.0469,-6.7695 0.3902,-0.094 0.7897,-0.1445 1.1914,-0.1504 z"
transform="translate(5.0092774e-5,-757.87625)" />
</g>
</g>
<g
style="display:inline;opacity:1;fill:#fee685;fill-opacity:1;stroke-width:0.755906;stroke-miterlimit:4;stroke-dasharray:none"
id="g10-2"
transform="matrix(-0.50509374,0.06754889,0.06754889,0.50509374,156.75523,55.243465)">
<g
style="fill:#fee685;fill-opacity:1"
id="g9-1"
transform="translate(-20.244579,-6.1209206)">
<g
style="fill:#fee685;fill-opacity:1"
id="g8-6"
transform="translate(61.54776,-5.6726683)">
<g
id="g7-8"
style="fill:#fee685;fill-opacity:1">
<g
id="g6-5"
transform="matrix(-0.514626,0.06882369,0.06882369,0.514626,1184.3644,-811.9091)"
style="opacity:1;fill:#fee685;fill-opacity:1">
<path
id="path4-7"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#fee685;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.02362;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 2230.8945,2301.1738 c -1.9108,-0.039 -3.9117,0.162 -5.9785,0.6328 -0.1394,0.032 -0.2613,0.071 -0.3984,0.1036 -2.274,2.2481 -4.8127,4.5047 -7.5293,6.7168 0.8746,3.8597 2.1735,7.8829 4.4707,12.0019 3.9872,7.1495 7.2742,10.9657 9.2031,15.9258 1.9289,4.9601 2.2639,10.7945 0.4746,21.4238 -2.2183,13.178 -10.2404,27.1324 -22.959,37.4336 9.8717,-2.8792 18.2866,-8.1915 23.8575,-14.6269 6.0132,-6.9464 8.0191,-10.8762 8.7226,-14.836 0.7036,-3.9598 0.044,-8.2997 0.3242,-15.664 0.1805,-4.7447 1.1911,-8.8958 2.4766,-12.545 l 0.3086,-0.875 0.9219,-0.1211 c 8.2284,-1.0673 15.6654,-3.167 19.5097,-5.6484 -1.2349,-5.5522 -6.4807,-9.8603 -13.4277,-13.1348 l -0.6621,-0.3125 -0.166,-0.7129 c -2.2034,-9.4614 -9.5905,-15.5632 -19.1485,-15.7617 z m 4.7832,11.6856 a 4.8229105,4.9139092 17.729059 0 1 1.4473,0.2246 4.8229105,4.9139092 17.729059 0 1 3.0977,6.1484 4.8229105,4.9139092 17.729059 0 1 -6.0899,3.2129 4.8229105,4.9139092 17.729059 0 1 -3.0976,-6.1484 4.8229105,4.9139092 17.729059 0 1 4.6425,-3.4375 z"
transform="translate(1.2499985e-4,-757.87627)" />
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -7,8 +7,8 @@
} }
#import bevy_core_pipeline::tonemapping::tone_mapping #import bevy_core_pipeline::tonemapping::tone_mapping
@group(2) @binding(0) var my_array_texture: texture_2d_array<f32>; @group(3) @binding(0) var my_array_texture: texture_2d_array<f32>;
@group(2) @binding(1) var my_array_texture_sampler: sampler; @group(3) @binding(1) var my_array_texture_sampler: sampler;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -3,8 +3,8 @@
view_transformations::position_world_to_clip view_transformations::position_world_to_clip
} }
@group(2) @binding(0) var texture: texture_2d<f32>; @group(3) @binding(0) var texture: texture_2d<f32>;
@group(2) @binding(1) var texture_sampler: sampler; @group(3) @binding(1) var texture_sampler: sampler;
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32, @builtin(instance_index) instance_index: u32,

View File

@ -15,12 +15,12 @@ struct MaterialBindings {
} }
#ifdef BINDLESS #ifdef BINDLESS
@group(2) @binding(0) var<storage> materials: array<MaterialBindings>; @group(3) @binding(0) var<storage> materials: array<MaterialBindings>;
@group(2) @binding(10) var<storage> material_color: binding_array<Color>; @group(3) @binding(10) var<storage> material_color: binding_array<Color>;
#else // BINDLESS #else // BINDLESS
@group(2) @binding(0) var<uniform> material_color: Color; @group(3) @binding(0) var<uniform> material_color: Color;
@group(2) @binding(1) var material_color_texture: texture_2d<f32>; @group(3) @binding(1) var material_color_texture: texture_2d<f32>;
@group(2) @binding(2) var material_color_sampler: sampler; @group(3) @binding(2) var material_color_sampler: sampler;
#endif // BINDLESS #endif // BINDLESS
@fragment @fragment

View File

@ -1,12 +1,12 @@
#import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::forward_io::VertexOutput
#ifdef CUBEMAP_ARRAY #ifdef CUBEMAP_ARRAY
@group(2) @binding(0) var base_color_texture: texture_cube_array<f32>; @group(3) @binding(0) var base_color_texture: texture_cube_array<f32>;
#else #else
@group(2) @binding(0) var base_color_texture: texture_cube<f32>; @group(3) @binding(0) var base_color_texture: texture_cube<f32>;
#endif #endif
@group(2) @binding(1) var base_color_sampler: sampler; @group(3) @binding(1) var base_color_sampler: sampler;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -3,10 +3,10 @@ layout(location = 0) in vec2 v_Uv;
layout(location = 0) out vec4 o_Target; layout(location = 0) out vec4 o_Target;
layout(set = 2, binding = 0) uniform vec4 CustomMaterial_color; layout(set = 3, binding = 0) uniform vec4 CustomMaterial_color;
layout(set = 2, binding = 1) uniform texture2D CustomMaterial_texture; layout(set = 3, binding = 1) uniform texture2D CustomMaterial_texture;
layout(set = 2, binding = 2) uniform sampler CustomMaterial_sampler; layout(set = 3, binding = 2) uniform sampler CustomMaterial_sampler;
// wgsl modules can be imported and used in glsl // wgsl modules can be imported and used in glsl
// FIXME - this doesn't work any more ... // FIXME - this doesn't work any more ...

View File

@ -25,9 +25,9 @@ struct Mesh {
}; };
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE #ifdef PER_OBJECT_BUFFER_BATCH_SIZE
layout(set = 1, binding = 0) uniform Mesh Meshes[#{PER_OBJECT_BUFFER_BATCH_SIZE}]; layout(set = 2, binding = 0) uniform Mesh Meshes[#{PER_OBJECT_BUFFER_BATCH_SIZE}];
#else #else
layout(set = 1, binding = 0) readonly buffer _Meshes { layout(set = 2, binding = 0) readonly buffer _Meshes {
Mesh Meshes[]; Mesh Meshes[];
}; };
#endif // PER_OBJECT_BUFFER_BATCH_SIZE #endif // PER_OBJECT_BUFFER_BATCH_SIZE

View File

@ -10,7 +10,7 @@ struct CustomMaterial {
time: vec4<f32>, time: vec4<f32>,
} }
@group(2) @binding(0) var<uniform> material: CustomMaterial; @group(3) @binding(0) var<uniform> material: CustomMaterial;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -2,9 +2,9 @@
// we can import items from shader modules in the assets folder with a quoted path // we can import items from shader modules in the assets folder with a quoted path
#import "shaders/custom_material_import.wgsl"::COLOR_MULTIPLIER #import "shaders/custom_material_import.wgsl"::COLOR_MULTIPLIER
@group(2) @binding(0) var<uniform> material_color: vec4<f32>; @group(3) @binding(0) var<uniform> material_color: vec4<f32>;
@group(2) @binding(1) var material_color_texture: texture_2d<f32>; @group(3) @binding(1) var material_color_texture: texture_2d<f32>;
@group(2) @binding(2) var material_color_sampler: sampler; @group(3) @binding(2) var material_color_sampler: sampler;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -4,8 +4,8 @@
utils::coords_to_viewport_uv, utils::coords_to_viewport_uv,
} }
@group(2) @binding(0) var texture: texture_2d<f32>; @group(3) @binding(0) var texture: texture_2d<f32>;
@group(2) @binding(1) var texture_sampler: sampler; @group(3) @binding(1) var texture_sampler: sampler;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -3,7 +3,7 @@
struct CustomMaterial { struct CustomMaterial {
color: vec4<f32>, color: vec4<f32>,
}; };
@group(2) @binding(0) var<uniform> material: CustomMaterial; @group(3) @binding(0) var<uniform> material: CustomMaterial;
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32, @builtin(instance_index) instance_index: u32,

View File

@ -19,7 +19,7 @@ struct MyExtendedMaterial {
quantize_steps: u32, quantize_steps: u32,
} }
@group(2) @binding(100) @group(3) @binding(100)
var<uniform> my_extended_material: MyExtendedMaterial; var<uniform> my_extended_material: MyExtendedMaterial;
@fragment @fragment

View File

@ -42,19 +42,19 @@ struct ExampleBindlessExtendedMaterial {
// The indices of the bindless resources in the bindless resource arrays, for // The indices of the bindless resources in the bindless resource arrays, for
// the `ExampleBindlessExtension` fields. // the `ExampleBindlessExtension` fields.
@group(2) @binding(100) var<storage> example_extended_material_indices: @group(3) @binding(100) var<storage> example_extended_material_indices:
array<ExampleBindlessExtendedMaterialIndices>; array<ExampleBindlessExtendedMaterialIndices>;
// An array that holds the `ExampleBindlessExtendedMaterial` plain old data, // An array that holds the `ExampleBindlessExtendedMaterial` plain old data,
// indexed by `ExampleBindlessExtendedMaterialIndices.material`. // indexed by `ExampleBindlessExtendedMaterialIndices.material`.
@group(2) @binding(101) var<storage> example_extended_material: @group(3) @binding(101) var<storage> example_extended_material:
array<ExampleBindlessExtendedMaterial>; array<ExampleBindlessExtendedMaterial>;
#else // BINDLESS #else // BINDLESS
// In non-bindless mode, we simply use a uniform for the plain old data. // In non-bindless mode, we simply use a uniform for the plain old data.
@group(2) @binding(50) var<uniform> example_extended_material: ExampleBindlessExtendedMaterial; @group(3) @binding(50) var<uniform> example_extended_material: ExampleBindlessExtendedMaterial;
@group(2) @binding(51) var modulate_texture: texture_2d<f32>; @group(3) @binding(51) var modulate_texture: texture_2d<f32>;
@group(2) @binding(52) var modulate_sampler: sampler; @group(3) @binding(52) var modulate_sampler: sampler;
#endif // BINDLESS #endif // BINDLESS

View File

@ -1,22 +1,22 @@
#import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::forward_io::VertexOutput
@group(2) @binding(0) var test_texture_1d: texture_1d<f32>; @group(3) @binding(0) var test_texture_1d: texture_1d<f32>;
@group(2) @binding(1) var test_texture_1d_sampler: sampler; @group(3) @binding(1) var test_texture_1d_sampler: sampler;
@group(2) @binding(2) var test_texture_2d: texture_2d<f32>; @group(3) @binding(2) var test_texture_2d: texture_2d<f32>;
@group(2) @binding(3) var test_texture_2d_sampler: sampler; @group(3) @binding(3) var test_texture_2d_sampler: sampler;
@group(2) @binding(4) var test_texture_2d_array: texture_2d_array<f32>; @group(3) @binding(4) var test_texture_2d_array: texture_2d_array<f32>;
@group(2) @binding(5) var test_texture_2d_array_sampler: sampler; @group(3) @binding(5) var test_texture_2d_array_sampler: sampler;
@group(2) @binding(6) var test_texture_cube: texture_cube<f32>; @group(3) @binding(6) var test_texture_cube: texture_cube<f32>;
@group(2) @binding(7) var test_texture_cube_sampler: sampler; @group(3) @binding(7) var test_texture_cube_sampler: sampler;
@group(2) @binding(8) var test_texture_cube_array: texture_cube_array<f32>; @group(3) @binding(8) var test_texture_cube_array: texture_cube_array<f32>;
@group(2) @binding(9) var test_texture_cube_array_sampler: sampler; @group(3) @binding(9) var test_texture_cube_array_sampler: sampler;
@group(2) @binding(10) var test_texture_3d: texture_3d<f32>; @group(3) @binding(10) var test_texture_3d: texture_3d<f32>;
@group(2) @binding(11) var test_texture_3d_sampler: sampler; @group(3) @binding(11) var test_texture_3d_sampler: sampler;
@fragment @fragment
fn fragment(in: VertexOutput) {} fn fragment(in: VertexOutput) {}

View File

@ -12,7 +12,7 @@ struct VoxelVisualizationIrradianceVolumeInfo {
intensity: f32, intensity: f32,
} }
@group(2) @binding(100) @group(3) @binding(100)
var<uniform> irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo; var<uniform> irradiance_volume_info: VoxelVisualizationIrradianceVolumeInfo;
@fragment @fragment

View File

@ -4,7 +4,7 @@ struct LineMaterial {
color: vec4<f32>, color: vec4<f32>,
}; };
@group(2) @binding(0) var<uniform> material: LineMaterial; @group(3) @binding(0) var<uniform> material: LineMaterial;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -0,0 +1,11 @@
#import bevy_pbr::forward_io::VertexOutput
@group(3) @binding(0) var material_color_texture: texture_2d<f32>;
@group(3) @binding(1) var material_color_sampler: sampler;
@fragment
fn fragment(
mesh: VertexOutput,
) -> @location(0) vec4<f32> {
return textureSample(material_color_texture, material_color_sampler, mesh.uv);
}

View File

@ -4,7 +4,7 @@ struct CustomMaterial {
color: vec4<f32>, color: vec4<f32>,
}; };
@group(2) @binding(0) var<uniform> material: CustomMaterial; @group(3) @binding(0) var<uniform> material: CustomMaterial;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -11,7 +11,7 @@ struct ShowPrepassSettings {
padding_1: u32, padding_1: u32,
padding_2: u32, padding_2: u32,
} }
@group(2) @binding(0) var<uniform> settings: ShowPrepassSettings; @group(3) @binding(0) var<uniform> settings: ShowPrepassSettings;
@fragment @fragment
fn fragment( fn fragment(

View File

@ -3,7 +3,7 @@
view_transformations::position_world_to_clip view_transformations::position_world_to_clip
} }
@group(2) @binding(0) var<storage, read> colors: array<vec4<f32>, 5>; @group(3) @binding(0) var<storage, read> colors: array<vec4<f32>, 5>;
struct Vertex { struct Vertex {
@builtin(instance_index) instance_index: u32, @builtin(instance_index) instance_index: u32,

View File

@ -1,7 +1,7 @@
#import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::forward_io::VertexOutput
@group(2) @binding(0) var textures: binding_array<texture_2d<f32>>; @group(3) @binding(0) var textures: binding_array<texture_2d<f32>>;
@group(2) @binding(1) var nearest_sampler: sampler; @group(3) @binding(1) var nearest_sampler: sampler;
// We can also have array of samplers // We can also have array of samplers
// var samplers: binding_array<sampler>; // var samplers: binding_array<sampler>;

View File

@ -23,9 +23,9 @@ struct WaterSettings {
@group(0) @binding(1) var<uniform> globals: Globals; @group(0) @binding(1) var<uniform> globals: Globals;
@group(2) @binding(100) var water_normals_texture: texture_2d<f32>; @group(3) @binding(100) var water_normals_texture: texture_2d<f32>;
@group(2) @binding(101) var water_normals_sampler: sampler; @group(3) @binding(101) var water_normals_sampler: sampler;
@group(2) @binding(102) var<uniform> water_settings: WaterSettings; @group(3) @binding(102) var<uniform> water_settings: WaterSettings;
// Samples a single octave of noise and returns the resulting normal. // Samples a single octave of noise and returns the resulting normal.
fn sample_noise_octave(uv: vec2<f32>, strength: f32) -> vec3<f32> { fn sample_noise_octave(uv: vec2<f32>, strength: f32) -> vec3<f32> {

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -49,6 +49,7 @@ impl BenchModify for Table {
black_box(self.0) black_box(self.0)
} }
} }
impl BenchModify for Sparse { impl BenchModify for Sparse {
fn bench_modify(&mut self) -> f32 { fn bench_modify(&mut self) -> f32 {
self.0 += 1f32; self.0 += 1f32;

View File

@ -1,7 +1,7 @@
use core::hint::black_box; use core::hint::black_box;
use benches::bench; use benches::bench;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::{Bundle, InsertMode};
use bevy_ecs::component::ComponentCloneBehavior; use bevy_ecs::component::ComponentCloneBehavior;
use bevy_ecs::entity::EntityCloner; use bevy_ecs::entity::EntityCloner;
use bevy_ecs::hierarchy::ChildOf; use bevy_ecs::hierarchy::ChildOf;
@ -17,41 +17,15 @@ criterion_group!(
hierarchy_tall, hierarchy_tall,
hierarchy_wide, hierarchy_wide,
hierarchy_many, hierarchy_many,
filter
); );
#[derive(Component, Reflect, Default, Clone)] #[derive(Component, Reflect, Default, Clone)]
struct C1(Mat4); struct C<const N: usize>(Mat4);
#[derive(Component, Reflect, Default, Clone)] type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10>);
struct C2(Mat4);
#[derive(Component, Reflect, Default, Clone)] /// Sets the [`ComponentCloneBehavior`] for all explicit and required components in a bundle `B` to
struct C3(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C4(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C5(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C6(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C7(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C8(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C9(Mat4);
#[derive(Component, Reflect, Default, Clone)]
struct C10(Mat4);
type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10);
/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to
/// use the [`Reflect`] trait instead of [`Clone`]. /// use the [`Reflect`] trait instead of [`Clone`].
fn reflection_cloner<B: Bundle + GetTypeRegistration>( fn reflection_cloner<B: Bundle + GetTypeRegistration>(
world: &mut World, world: &mut World,
@ -71,7 +45,7 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
// this bundle are saved. // this bundle are saved.
let component_ids: Vec<_> = world.register_bundle::<B>().contributed_components().into(); let component_ids: Vec<_> = world.register_bundle::<B>().contributed_components().into();
let mut builder = EntityCloner::build(world); let mut builder = EntityCloner::build_opt_out(world);
// Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`. // Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`.
for component in component_ids { for component in component_ids {
@ -82,16 +56,15 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
builder.finish() builder.finish()
} }
/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a /// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`.
/// bundle `B`.
/// ///
/// The bundle must implement [`Default`], which is used to create the first entity that gets cloned /// The bundle must implement [`Default`], which is used to create the first entity that gets cloned
/// in the benchmark. /// in the benchmark.
/// ///
/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneHandler`] for all /// If `clone_via_reflect` is false, this will use the default [`ComponentCloneBehavior`] for all
/// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect` /// components (which is usually [`ComponentCloneBehavior::clone()`]). If `clone_via_reflect`
/// is true, it will overwrite the handler for all components in the bundle to be /// is true, it will overwrite the handler for all components in the bundle to be
/// [`ComponentCloneHandler::reflect_handler()`]. /// [`ComponentCloneBehavior::reflect()`].
fn bench_clone<B: Bundle + Default + GetTypeRegistration>( fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
b: &mut Bencher, b: &mut Bencher,
clone_via_reflect: bool, clone_via_reflect: bool,
@ -114,8 +87,7 @@ fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
}); });
} }
/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a /// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`.
/// bundle `B`.
/// ///
/// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several /// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several
/// children. It does so by setting up an entity tree with a given `height` where each entity has a /// children. It does so by setting up an entity tree with a given `height` where each entity has a
@ -135,7 +107,7 @@ fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(
let mut cloner = if clone_via_reflect { let mut cloner = if clone_via_reflect {
reflection_cloner::<B>(&mut world, true) reflection_cloner::<B>(&mut world, true)
} else { } else {
let mut builder = EntityCloner::build(&mut world); let mut builder = EntityCloner::build_opt_out(&mut world);
builder.linked_cloning(true); builder.linked_cloning(true);
builder.finish() builder.finish()
}; };
@ -169,7 +141,7 @@ fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(
// Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This // Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This
// constant represents this as an easy array that can be used in a `for` loop. // constant represents this as an easy array that can be used in a `for` loop.
const SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; const CLONE_SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)];
/// Benchmarks cloning a single entity with 10 components and no children. /// Benchmarks cloning a single entity with 10 components and no children.
fn single(c: &mut Criterion) { fn single(c: &mut Criterion) {
@ -178,7 +150,7 @@ fn single(c: &mut Criterion) {
// We're cloning 1 entity. // We're cloning 1 entity.
group.throughput(Throughput::Elements(1)); group.throughput(Throughput::Elements(1));
for (id, clone_via_reflect) in SCENARIOS { for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| { group.bench_function(id, |b| {
bench_clone::<ComplexBundle>(b, clone_via_reflect); bench_clone::<ComplexBundle>(b, clone_via_reflect);
}); });
@ -194,9 +166,9 @@ fn hierarchy_tall(c: &mut Criterion) {
// We're cloning both the root entity and its 50 descendents. // We're cloning both the root entity and its 50 descendents.
group.throughput(Throughput::Elements(51)); group.throughput(Throughput::Elements(51));
for (id, clone_via_reflect) in SCENARIOS { for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| { group.bench_function(id, |b| {
bench_clone_hierarchy::<C1>(b, 50, 1, clone_via_reflect); bench_clone_hierarchy::<C<1>>(b, 50, 1, clone_via_reflect);
}); });
} }
@ -210,9 +182,9 @@ fn hierarchy_wide(c: &mut Criterion) {
// We're cloning both the root entity and its 50 direct children. // We're cloning both the root entity and its 50 direct children.
group.throughput(Throughput::Elements(51)); group.throughput(Throughput::Elements(51));
for (id, clone_via_reflect) in SCENARIOS { for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| { group.bench_function(id, |b| {
bench_clone_hierarchy::<C1>(b, 1, 50, clone_via_reflect); bench_clone_hierarchy::<C<1>>(b, 1, 50, clone_via_reflect);
}); });
} }
@ -228,7 +200,7 @@ fn hierarchy_many(c: &mut Criterion) {
// of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :) // of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :)
group.throughput(Throughput::Elements(364)); group.throughput(Throughput::Elements(364));
for (id, clone_via_reflect) in SCENARIOS { for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| { group.bench_function(id, |b| {
bench_clone_hierarchy::<ComplexBundle>(b, 5, 3, clone_via_reflect); bench_clone_hierarchy::<ComplexBundle>(b, 5, 3, clone_via_reflect);
}); });
@ -236,3 +208,157 @@ fn hierarchy_many(c: &mut Criterion) {
group.finish(); group.finish();
} }
/// Filter scenario variant for bot opt-in and opt-out filters
#[derive(Clone, Copy)]
#[expect(
clippy::enum_variant_names,
reason = "'Opt' is not understood as an prefix but `OptOut'/'OptIn' are"
)]
enum FilterScenario {
OptOutNone,
OptOutNoneKeep(bool),
OptOutAll,
OptInNone,
OptInAll,
OptInAllWithoutRequired,
OptInAllKeep(bool),
OptInAllKeepWithoutRequired(bool),
}
impl From<FilterScenario> for String {
fn from(value: FilterScenario) -> Self {
match value {
FilterScenario::OptOutNone => "opt_out_none",
FilterScenario::OptOutNoneKeep(true) => "opt_out_none_keep_none",
FilterScenario::OptOutNoneKeep(false) => "opt_out_none_keep_all",
FilterScenario::OptOutAll => "opt_out_all",
FilterScenario::OptInNone => "opt_in_none",
FilterScenario::OptInAll => "opt_in_all",
FilterScenario::OptInAllWithoutRequired => "opt_in_all_without_required",
FilterScenario::OptInAllKeep(true) => "opt_in_all_keep_none",
FilterScenario::OptInAllKeep(false) => "opt_in_all_keep_all",
FilterScenario::OptInAllKeepWithoutRequired(true) => {
"opt_in_all_keep_none_without_required"
}
FilterScenario::OptInAllKeepWithoutRequired(false) => {
"opt_in_all_keep_all_without_required"
}
}
.into()
}
}
/// Common scenarios for different filter to be benchmarked.
const FILTER_SCENARIOS: [FilterScenario; 11] = [
FilterScenario::OptOutNone,
FilterScenario::OptOutNoneKeep(true),
FilterScenario::OptOutNoneKeep(false),
FilterScenario::OptOutAll,
FilterScenario::OptInNone,
FilterScenario::OptInAll,
FilterScenario::OptInAllWithoutRequired,
FilterScenario::OptInAllKeep(true),
FilterScenario::OptInAllKeep(false),
FilterScenario::OptInAllKeepWithoutRequired(true),
FilterScenario::OptInAllKeepWithoutRequired(false),
];
/// A helper function that benchmarks running [`EntityCloner::clone_entity`] with a bundle `B`.
///
/// The bundle must implement [`Default`], which is used to create the first entity that gets its components cloned
/// in the benchmark. It may also be used to populate the target entity depending on the scenario.
fn bench_filter<B: Bundle + Default>(b: &mut Bencher, scenario: FilterScenario) {
let mut world = World::default();
let mut spawn = |empty| match empty {
false => world.spawn(B::default()).id(),
true => world.spawn_empty().id(),
};
let source = spawn(false);
let (target, mut cloner);
match scenario {
FilterScenario::OptOutNone => {
target = spawn(true);
cloner = EntityCloner::default();
}
FilterScenario::OptOutNoneKeep(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_out(&mut world);
builder.insert_mode(InsertMode::Keep);
cloner = builder.finish();
}
FilterScenario::OptOutAll => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_out(&mut world);
builder.deny::<B>();
cloner = builder.finish();
}
FilterScenario::OptInNone => {
target = spawn(true);
let builder = EntityCloner::build_opt_in(&mut world);
cloner = builder.finish();
}
FilterScenario::OptInAll => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.allow::<B>();
cloner = builder.finish();
}
FilterScenario::OptInAllWithoutRequired => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.without_required_components(|builder| {
builder.allow::<B>();
});
cloner = builder.finish();
}
FilterScenario::OptInAllKeep(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.allow_if_new::<B>();
cloner = builder.finish();
}
FilterScenario::OptInAllKeepWithoutRequired(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.without_required_components(|builder| {
builder.allow_if_new::<B>();
});
cloner = builder.finish();
}
}
b.iter(|| {
// clones the given entity into the target
cloner.clone_entity(&mut world, black_box(source), black_box(target));
world.flush();
});
}
/// Benchmarks filtering of cloning a single entity with 5 unclonable components (each requiring 1 unclonable component) into a target.
fn filter(c: &mut Criterion) {
#[derive(Component, Default)]
#[component(clone_behavior = Ignore)]
struct C<const N: usize>;
#[derive(Component, Default)]
#[component(clone_behavior = Ignore)]
#[require(C::<N>)]
struct R<const N: usize>;
type RequiringBundle = (R<1>, R<2>, R<3>, R<4>, R<5>);
let mut group = c.benchmark_group(bench!("filter"));
// We're cloning 1 entity into a target.
group.throughput(Throughput::Elements(1));
for scenario in FILTER_SCENARIOS {
group.bench_function(scenario, |b| {
bench_filter::<RequiringBundle>(b, scenario);
});
}
group.finish();
}

View File

@ -1,6 +1,6 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
#[derive(Event)] #[derive(Event, BufferedEvent)]
struct BenchEvent<const SIZE: usize>([u8; SIZE]); struct BenchEvent<const SIZE: usize>([u8; SIZE]);
pub struct Benchmark<const SIZE: usize>(Events<BenchEvent<SIZE>>); pub struct Benchmark<const SIZE: usize>(Events<BenchEvent<SIZE>>);

View File

@ -10,19 +10,19 @@ fn send(c: &mut Criterion) {
group.warm_up_time(core::time::Duration::from_millis(500)); group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_4_events_{}", count), |b| { group.bench_function(format!("size_4_events_{count}"), |b| {
let mut bench = send::Benchmark::<4>::new(count); let mut bench = send::Benchmark::<4>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });
} }
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_16_events_{}", count), |b| { group.bench_function(format!("size_16_events_{count}"), |b| {
let mut bench = send::Benchmark::<16>::new(count); let mut bench = send::Benchmark::<16>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });
} }
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_512_events_{}", count), |b| { group.bench_function(format!("size_512_events_{count}"), |b| {
let mut bench = send::Benchmark::<512>::new(count); let mut bench = send::Benchmark::<512>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });
@ -35,19 +35,19 @@ fn iter(c: &mut Criterion) {
group.warm_up_time(core::time::Duration::from_millis(500)); group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_4_events_{}", count), |b| { group.bench_function(format!("size_4_events_{count}"), |b| {
let mut bench = iter::Benchmark::<4>::new(count); let mut bench = iter::Benchmark::<4>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });
} }
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_16_events_{}", count), |b| { group.bench_function(format!("size_16_events_{count}"), |b| {
let mut bench = iter::Benchmark::<4>::new(count); let mut bench = iter::Benchmark::<4>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });
} }
for count in [100, 1_000, 10_000] { for count in [100, 1_000, 10_000] {
group.bench_function(format!("size_512_events_{}", count), |b| { group.bench_function(format!("size_512_events_{count}"), |b| {
let mut bench = iter::Benchmark::<512>::new(count); let mut bench = iter::Benchmark::<512>::new(count);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });

View File

@ -1,6 +1,6 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
#[derive(Event)] #[derive(Event, BufferedEvent)]
struct BenchEvent<const SIZE: usize>([u8; SIZE]); struct BenchEvent<const SIZE: usize>([u8; SIZE]);
impl<const SIZE: usize> Default for BenchEvent<SIZE> { impl<const SIZE: usize> Default for BenchEvent<SIZE> {

View File

@ -45,7 +45,6 @@ pub fn heavy_compute(c: &mut Criterion) {
let mut system = IntoSystem::into_system(sys); let mut system = IntoSystem::into_system(sys);
system.initialize(&mut world); system.initialize(&mut world);
system.update_archetype_component_access(world.as_unsafe_world_cell());
b.iter(move || system.run((), &mut world)); b.iter(move || system.run((), &mut world));
}); });

View File

@ -37,7 +37,6 @@ impl Benchmark {
let mut system = IntoSystem::into_system(query_system); let mut system = IntoSystem::into_system(query_system);
system.initialize(&mut world); system.initialize(&mut world);
system.update_archetype_component_access(world.as_unsafe_world_cell());
Self(world, Box::new(system)) Self(world, Box::new(system))
} }

View File

@ -130,7 +130,7 @@ fn par_iter_simple(c: &mut Criterion) {
group.warm_up_time(core::time::Duration::from_millis(500)); group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for f in [0, 10, 100, 1000] { for f in [0, 10, 100, 1000] {
group.bench_function(format!("with_{}_fragment", f), |b| { group.bench_function(format!("with_{f}_fragment"), |b| {
let mut bench = par_iter_simple::Benchmark::new(f); let mut bench = par_iter_simple::Benchmark::new(f);
b.iter(move || bench.run()); b.iter(move || bench.run());
}); });

View File

@ -61,14 +61,10 @@ pub fn event_propagation(criterion: &mut Criterion) {
group.finish(); group.finish();
} }
#[derive(Clone, Component)] #[derive(Event, EntityEvent, Clone, Component)]
#[entity_event(traversal = &'static ChildOf, auto_propagate)]
struct TestEvent<const N: usize> {} struct TestEvent<const N: usize> {}
impl<const N: usize> Event for TestEvent<N> {
type Traversal = &'static ChildOf;
const AUTO_PROPAGATE: bool = true;
}
fn send_events<const N: usize, const N_EVENTS: usize>(world: &mut World, leaves: &[Entity]) { fn send_events<const N: usize, const N_EVENTS: usize>(world: &mut World, leaves: &[Entity]) {
let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap(); let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap();
@ -117,6 +113,6 @@ fn add_listeners_to_hierarchy<const DENSITY: usize, const N: usize>(
} }
} }
fn empty_listener<const N: usize>(trigger: Trigger<TestEvent<N>>) { fn empty_listener<const N: usize>(trigger: On<TestEvent<N>>) {
black_box(trigger); black_box(trigger);
} }

View File

@ -1,8 +1,8 @@
use core::hint::black_box; use core::hint::black_box;
use bevy_ecs::{ use bevy_ecs::{
event::Event, event::{EntityEvent, Event},
observer::{Trigger, TriggerTargets}, observer::{On, TriggerTargets},
world::World, world::World,
}; };
@ -13,7 +13,7 @@ fn deterministic_rand() -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(42) ChaCha8Rng::seed_from_u64(42)
} }
#[derive(Clone, Event)] #[derive(Clone, Event, EntityEvent)]
struct EventBase; struct EventBase;
pub fn observe_simple(criterion: &mut Criterion) { pub fn observe_simple(criterion: &mut Criterion) {
@ -46,7 +46,7 @@ pub fn observe_simple(criterion: &mut Criterion) {
group.finish(); group.finish();
} }
fn empty_listener_base(trigger: Trigger<EventBase>) { fn empty_listener_base(trigger: On<EventBase>) {
black_box(trigger); black_box(trigger);
} }

View File

@ -24,7 +24,7 @@ pub fn run_condition_yes(criterion: &mut Criterion) {
} }
// run once to initialize systems // run once to initialize systems
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });
@ -46,7 +46,7 @@ pub fn run_condition_no(criterion: &mut Criterion) {
} }
// run once to initialize systems // run once to initialize systems
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });
@ -77,7 +77,7 @@ pub fn run_condition_yes_with_query(criterion: &mut Criterion) {
} }
// run once to initialize systems // run once to initialize systems
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });
@ -105,7 +105,7 @@ pub fn run_condition_yes_with_resource(criterion: &mut Criterion) {
} }
// run once to initialize systems // run once to initialize systems
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });

View File

@ -26,7 +26,7 @@ pub fn empty_systems(criterion: &mut Criterion) {
schedule.add_systems(empty); schedule.add_systems(empty);
} }
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });
@ -38,7 +38,7 @@ pub fn empty_systems(criterion: &mut Criterion) {
schedule.add_systems((empty, empty, empty, empty, empty)); schedule.add_systems((empty, empty, empty, empty, empty));
} }
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function(format!("{}_systems", amount), |bencher| { group.bench_function(format!("{amount}_systems"), |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
}); });
@ -79,10 +79,7 @@ pub fn busy_systems(criterion: &mut Criterion) {
} }
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function( group.bench_function(
format!( format!("{entity_bunches:02}x_entities_{system_amount:02}_systems"),
"{:02}x_entities_{:02}_systems",
entity_bunches, system_amount
),
|bencher| { |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);
@ -128,10 +125,7 @@ pub fn contrived(criterion: &mut Criterion) {
} }
schedule.run(&mut world); schedule.run(&mut world);
group.bench_function( group.bench_function(
format!( format!("{entity_bunches:02}x_entities_{system_amount:02}_systems"),
"{:02}x_entities_{:02}_systems",
entity_bunches, system_amount
),
|bencher| { |bencher| {
bencher.iter(|| { bencher.iter(|| {
schedule.run(&mut world); schedule.run(&mut world);

View File

@ -37,7 +37,7 @@ pub fn spawn_commands(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [100, 1_000, 10_000] { for entity_count in [100, 1_000, 10_000] {
group.bench_function(format!("{}_entities", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut command_queue = CommandQueue::default(); let mut command_queue = CommandQueue::default();
@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) {
group.finish(); group.finish();
} }
pub fn nonempty_spawn_commands(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("nonempty_spawn_commands");
group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [100, 1_000, 10_000] {
group.bench_function(format!("{entity_count}_entities"), |bencher| {
let mut world = World::default();
let mut command_queue = CommandQueue::default();
bencher.iter(|| {
let mut commands = Commands::new(&mut command_queue, &world);
for i in 0..entity_count {
if black_box(i % 2 == 0) {
commands.spawn(A);
}
}
command_queue.apply(&mut world);
});
});
}
group.finish();
}
#[derive(Default, Component)] #[derive(Default, Component)]
struct Matrix([[f32; 4]; 4]); struct Matrix([[f32; 4]; 4]);
@ -137,7 +162,7 @@ pub fn fake_commands(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for command_count in [100, 1_000, 10_000] { for command_count in [100, 1_000, 10_000] {
group.bench_function(format!("{}_commands", command_count), |bencher| { group.bench_function(format!("{command_count}_commands"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut command_queue = CommandQueue::default(); let mut command_queue = CommandQueue::default();
@ -182,7 +207,7 @@ pub fn sized_commands_impl<T: Default + Command>(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for command_count in [100, 1_000, 10_000] { for command_count in [100, 1_000, 10_000] {
group.bench_function(format!("{}_commands", command_count), |bencher| { group.bench_function(format!("{command_count}_commands"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut command_queue = CommandQueue::default(); let mut command_queue = CommandQueue::default();

View File

@ -13,7 +13,7 @@ pub fn world_despawn(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [1, 100, 10_000] { for entity_count in [1, 100, 10_000] {
group.bench_function(format!("{}_entities", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
bencher.iter_batched_ref( bencher.iter_batched_ref(
|| { || {
let mut world = World::default(); let mut world = World::default();

View File

@ -13,7 +13,7 @@ pub fn world_despawn_recursive(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [1, 100, 10_000] { for entity_count in [1, 100, 10_000] {
group.bench_function(format!("{}_entities", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
bencher.iter_batched_ref( bencher.iter_batched_ref(
|| { || {
let mut world = World::default(); let mut world = World::default();

View File

@ -17,6 +17,7 @@ criterion_group!(
benches, benches,
empty_commands, empty_commands,
spawn_commands, spawn_commands,
nonempty_spawn_commands,
insert_commands, insert_commands,
fake_commands, fake_commands,
zero_sized_commands, zero_sized_commands,

View File

@ -13,7 +13,7 @@ pub fn world_spawn(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in [1, 100, 10_000] { for entity_count in [1, 100, 10_000] {
group.bench_function(format!("{}_entities", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
let mut world = World::default(); let mut world = World::default();
bencher.iter(|| { bencher.iter(|| {
for _ in 0..entity_count { for _ in 0..entity_count {

View File

@ -49,7 +49,7 @@ pub fn world_entity(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities"), |bencher| {
let world = setup::<Table>(entity_count); let world = setup::<Table>(entity_count);
bencher.iter(|| { bencher.iter(|| {
@ -72,7 +72,7 @@ pub fn world_get(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let world = setup::<Table>(entity_count); let world = setup::<Table>(entity_count);
bencher.iter(|| { bencher.iter(|| {
@ -84,7 +84,7 @@ pub fn world_get(criterion: &mut Criterion) {
} }
}); });
}); });
group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let world = setup::<Sparse>(entity_count); let world = setup::<Sparse>(entity_count);
bencher.iter(|| { bencher.iter(|| {
@ -107,7 +107,7 @@ pub fn world_query_get(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = setup::<Table>(entity_count); let mut world = setup::<Table>(entity_count);
let mut query = world.query::<&Table>(); let mut query = world.query::<&Table>();
@ -120,7 +120,7 @@ pub fn world_query_get(criterion: &mut Criterion) {
} }
}); });
}); });
group.bench_function(format!("{}_entities_table_wide", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table_wide"), |bencher| {
let mut world = setup_wide::<( let mut world = setup_wide::<(
WideTable<0>, WideTable<0>,
WideTable<1>, WideTable<1>,
@ -147,7 +147,7 @@ pub fn world_query_get(criterion: &mut Criterion) {
} }
}); });
}); });
group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = setup::<Sparse>(entity_count); let mut world = setup::<Sparse>(entity_count);
let mut query = world.query::<&Sparse>(); let mut query = world.query::<&Sparse>();
@ -160,37 +160,33 @@ pub fn world_query_get(criterion: &mut Criterion) {
} }
}); });
}); });
group.bench_function( group.bench_function(format!("{entity_count}_entities_sparse_wide"), |bencher| {
format!("{}_entities_sparse_wide", entity_count), let mut world = setup_wide::<(
|bencher| { WideSparse<0>,
let mut world = setup_wide::<( WideSparse<1>,
WideSparse<0>, WideSparse<2>,
WideSparse<1>, WideSparse<3>,
WideSparse<2>, WideSparse<4>,
WideSparse<3>, WideSparse<5>,
WideSparse<4>, )>(entity_count);
WideSparse<5>, let mut query = world.query::<(
)>(entity_count); &WideSparse<0>,
let mut query = world.query::<( &WideSparse<1>,
&WideSparse<0>, &WideSparse<2>,
&WideSparse<1>, &WideSparse<3>,
&WideSparse<2>, &WideSparse<4>,
&WideSparse<3>, &WideSparse<5>,
&WideSparse<4>, )>();
&WideSparse<5>,
)>();
bencher.iter(|| { bencher.iter(|| {
for i in 0..entity_count { for i in 0..entity_count {
// SAFETY: Range is exclusive. // SAFETY: Range is exclusive.
let entity = Entity::from_raw(EntityRow::new(unsafe { let entity =
NonMaxU32::new_unchecked(i) Entity::from_raw(EntityRow::new(unsafe { NonMaxU32::new_unchecked(i) }));
})); assert!(query.get(&world, entity).is_ok());
assert!(query.get(&world, entity).is_ok()); }
} });
}); });
},
);
} }
group.finish(); group.finish();
@ -202,7 +198,7 @@ pub fn world_query_iter(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = setup::<Table>(entity_count); let mut world = setup::<Table>(entity_count);
let mut query = world.query::<&Table>(); let mut query = world.query::<&Table>();
@ -216,7 +212,7 @@ pub fn world_query_iter(criterion: &mut Criterion) {
assert_eq!(black_box(count), entity_count); assert_eq!(black_box(count), entity_count);
}); });
}); });
group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = setup::<Sparse>(entity_count); let mut world = setup::<Sparse>(entity_count);
let mut query = world.query::<&Sparse>(); let mut query = world.query::<&Sparse>();
@ -241,7 +237,7 @@ pub fn world_query_for_each(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = setup::<Table>(entity_count); let mut world = setup::<Table>(entity_count);
let mut query = world.query::<&Table>(); let mut query = world.query::<&Table>();
@ -255,7 +251,7 @@ pub fn world_query_for_each(criterion: &mut Criterion) {
assert_eq!(black_box(count), entity_count); assert_eq!(black_box(count), entity_count);
}); });
}); });
group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = setup::<Sparse>(entity_count); let mut world = setup::<Sparse>(entity_count);
let mut query = world.query::<&Sparse>(); let mut query = world.query::<&Sparse>();
@ -280,7 +276,7 @@ pub fn query_get(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(4)); group.measurement_time(core::time::Duration::from_secs(4));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_entities_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_table"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut entities: Vec<_> = world let mut entities: Vec<_> = world
.spawn_batch((0..entity_count).map(|_| Table::default())) .spawn_batch((0..entity_count).map(|_| Table::default()))
@ -299,7 +295,7 @@ pub fn query_get(criterion: &mut Criterion) {
assert_eq!(black_box(count), entity_count); assert_eq!(black_box(count), entity_count);
}); });
}); });
group.bench_function(format!("{}_entities_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_entities_sparse"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut entities: Vec<_> = world let mut entities: Vec<_> = world
.spawn_batch((0..entity_count).map(|_| Sparse::default())) .spawn_batch((0..entity_count).map(|_| Sparse::default()))
@ -329,7 +325,7 @@ pub fn query_get_many<const N: usize>(criterion: &mut Criterion) {
group.measurement_time(core::time::Duration::from_secs(2 * N as u64)); group.measurement_time(core::time::Duration::from_secs(2 * N as u64));
for entity_count in RANGE.map(|i| i * 10_000) { for entity_count in RANGE.map(|i| i * 10_000) {
group.bench_function(format!("{}_calls_table", entity_count), |bencher| { group.bench_function(format!("{entity_count}_calls_table"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut entity_groups: Vec<_> = (0..entity_count) let mut entity_groups: Vec<_> = (0..entity_count)
.map(|_| [(); N].map(|_| world.spawn(Table::default()).id())) .map(|_| [(); N].map(|_| world.spawn(Table::default()).id()))
@ -352,7 +348,7 @@ pub fn query_get_many<const N: usize>(criterion: &mut Criterion) {
assert_eq!(black_box(count), entity_count); assert_eq!(black_box(count), entity_count);
}); });
}); });
group.bench_function(format!("{}_calls_sparse", entity_count), |bencher| { group.bench_function(format!("{entity_count}_calls_sparse"), |bencher| {
let mut world = World::default(); let mut world = World::default();
let mut entity_groups: Vec<_> = (0..entity_count) let mut entity_groups: Vec<_> = (0..entity_count)
.map(|_| [(); N].map(|_| world.spawn(Sparse::default()).id())) .map(|_| [(); N].map(|_| world.spawn(Sparse::default()).id()))

View File

@ -32,7 +32,7 @@ fn segment_ease(c: &mut Criterion) {
fn curve_position(c: &mut Criterion) { fn curve_position(c: &mut Criterion) {
/// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`]. /// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`].
fn bench_curve<M: Measurement, P: VectorSpace>( fn bench_curve<M: Measurement, P: VectorSpace<Scalar = f32>>(
group: &mut BenchmarkGroup<M>, group: &mut BenchmarkGroup<M>,
name: &str, name: &str,
curve: CubicCurve<P>, curve: CubicCurve<P>,

View File

@ -155,6 +155,7 @@ fn bench(c: &mut Criterion) {
&mesh.positions, &mesh.positions,
Some(&mesh.normals), Some(&mesh.normals),
Some(&mesh.indices), Some(&mesh.indices),
None,
backface_culling, backface_culling,
); );

View File

@ -142,7 +142,7 @@ fn concrete_map_apply(criterion: &mut Criterion) {
fn u64_to_n_byte_key(k: u64, n: usize) -> String { fn u64_to_n_byte_key(k: u64, n: usize) -> String {
let mut key = String::with_capacity(n); let mut key = String::with_capacity(n);
write!(&mut key, "{}", k).unwrap(); write!(&mut key, "{k}").unwrap();
// Pad key to n bytes. // Pad key to n bytes.
key.extend(iter::repeat_n('\0', n - key.len())); key.extend(iter::repeat_n('\0', n - key.len()));

View File

@ -55,7 +55,7 @@ fn concrete_struct_field(criterion: &mut Criterion) {
&s, &s,
|bencher, s| { |bencher, s| {
let field_names = (0..field_count) let field_names = (0..field_count)
.map(|i| format!("field_{}", i)) .map(|i| format!("field_{i}"))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
bencher.iter(|| { bencher.iter(|| {
@ -256,7 +256,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) {
let mut base = DynamicStruct::default(); let mut base = DynamicStruct::default();
for i in 0..field_count { for i in 0..field_count {
let field_name = format!("field_{}", i); let field_name = format!("field_{i}");
base.insert(&field_name, 1u32); base.insert(&field_name, 1u32);
} }
@ -283,7 +283,7 @@ fn dynamic_struct_apply(criterion: &mut Criterion) {
let mut base = DynamicStruct::default(); let mut base = DynamicStruct::default();
let mut patch = DynamicStruct::default(); let mut patch = DynamicStruct::default();
for i in 0..field_count { for i in 0..field_count {
let field_name = format!("field_{}", i); let field_name = format!("field_{i}");
base.insert(&field_name, 0u32); base.insert(&field_name, 0u32);
patch.insert(&field_name, 1u32); patch.insert(&field_name, 1u32);
} }
@ -309,11 +309,11 @@ fn dynamic_struct_insert(criterion: &mut Criterion) {
|bencher, field_count| { |bencher, field_count| {
let mut s = DynamicStruct::default(); let mut s = DynamicStruct::default();
for i in 0..*field_count { for i in 0..*field_count {
let field_name = format!("field_{}", i); let field_name = format!("field_{i}");
s.insert(&field_name, ()); s.insert(&field_name, ());
} }
let field = format!("field_{}", field_count); let field = format!("field_{field_count}");
bencher.iter_batched( bencher.iter_batched(
|| s.to_dynamic_struct(), || s.to_dynamic_struct(),
|mut s| { |mut s| {
@ -339,7 +339,7 @@ fn dynamic_struct_get_field(criterion: &mut Criterion) {
|bencher, field_count| { |bencher, field_count| {
let mut s = DynamicStruct::default(); let mut s = DynamicStruct::default();
for i in 0..*field_count { for i in 0..*field_count {
let field_name = format!("field_{}", i); let field_name = format!("field_{i}");
s.insert(&field_name, ()); s.insert(&field_name, ());
} }

View File

@ -1,9 +1,9 @@
[package] [package]
name = "bevy_a11y" name = "bevy_a11y"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides accessibility support for Bevy Engine" description = "Provides accessibility support for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy", "accessibility", "a11y"] keywords = ["bevy", "accessibility", "a11y"]
@ -40,13 +40,13 @@ critical-section = [
[dependencies] [dependencies]
# bevy # bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false } bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true }
# other # other
accesskit = { version = "0.18", default-features = false } accesskit = { version = "0.19", default-features = false }
serde = { version = "1", default-features = false, features = [ serde = { version = "1", default-features = false, features = [
"alloc", "alloc",
], optional = true } ], optional = true }

View File

@ -1,8 +1,8 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
#![no_std] #![no_std]
@ -26,7 +26,8 @@ use accesskit::Node;
use bevy_app::Plugin; use bevy_app::Plugin;
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
prelude::{Component, Event}, component::Component,
event::{BufferedEvent, Event},
resource::Resource, resource::Resource,
schedule::SystemSet, schedule::SystemSet,
}; };
@ -44,7 +45,7 @@ use serde::{Deserialize, Serialize};
use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
/// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`. /// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`.
#[derive(Event, Deref, DerefMut)] #[derive(Event, BufferedEvent, Deref, DerefMut)]
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
pub struct ActionRequest(pub accesskit::ActionRequest); pub struct ActionRequest(pub accesskit::ActionRequest);

View File

@ -1,43 +1,43 @@
[package] [package]
name = "bevy_animation" name = "bevy_animation"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides animation functionality for Bevy Engine" description = "Provides animation functionality for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
[dependencies] [dependencies]
# bevy # bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [
"petgraph", "petgraph",
] } ] }
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.16.0-dev" } bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std", "std",
"serialize", "serialize",
] } ] }
# other # other
petgraph = { version = "0.7", features = ["serde-1"] } petgraph = { version = "0.7", features = ["serde-1"] }
ron = "0.8" ron = "0.10"
serde = "1" serde = "1"
blake3 = { version = "1.0" } blake3 = { version = "1.0" }
downcast-rs = { version = "2", default-features = false, features = ["std"] } downcast-rs = { version = "2", default-features = false, features = ["std"] }
thiserror = { version = "2", default-features = false } thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] } derive_more = { version = "2", default-features = false, features = ["from"] }
either = "1.13" either = "1.13"
thread_local = "1" thread_local = "1"
uuid = { version = "1.13.1", features = ["v4"] } uuid = { version = "1.13.1", features = ["v4"] }

View File

@ -55,7 +55,7 @@ pub struct CubicKeyframeCurve<T> {
impl<V> Curve<V> for CubicKeyframeCurve<V> impl<V> Curve<V> for CubicKeyframeCurve<V>
where where
V: VectorSpace, V: VectorSpace<Scalar = f32>,
{ {
#[inline] #[inline]
fn domain(&self) -> Interval { fn domain(&self) -> Interval {
@ -179,7 +179,7 @@ pub struct WideLinearKeyframeCurve<T> {
impl<T> IterableCurve<T> for WideLinearKeyframeCurve<T> impl<T> IterableCurve<T> for WideLinearKeyframeCurve<T>
where where
T: VectorSpace, T: VectorSpace<Scalar = f32>,
{ {
#[inline] #[inline]
fn domain(&self) -> Interval { fn domain(&self) -> Interval {
@ -289,7 +289,7 @@ pub struct WideCubicKeyframeCurve<T> {
impl<T> IterableCurve<T> for WideCubicKeyframeCurve<T> impl<T> IterableCurve<T> for WideCubicKeyframeCurve<T>
where where
T: VectorSpace, T: VectorSpace<Scalar = f32>,
{ {
#[inline] #[inline]
fn domain(&self) -> Interval { fn domain(&self) -> Interval {
@ -406,7 +406,7 @@ fn cubic_spline_interpolation<T>(
step_duration: f32, step_duration: f32,
) -> T ) -> T
where where
T: VectorSpace, T: VectorSpace<Scalar = f32>,
{ {
let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp;
value_start * (coeffs.x * lerp + 1.0) value_start * (coeffs.x * lerp + 1.0)
@ -415,7 +415,7 @@ where
+ tangent_in_end * step_duration * lerp * coeffs.w + tangent_in_end * step_duration * lerp * coeffs.w
} }
fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( fn cubic_spline_interpolate_slices<'a, T: VectorSpace<Scalar = f32>>(
width: usize, width: usize,
first: &'a [T], first: &'a [T],
second: &'a [T], second: &'a [T],

View File

@ -1,10 +1,11 @@
//! The animation graph, which allows animations to be blended together. //! The animation graph, which allows animations to be blended together.
use core::{ use core::{
fmt::Write,
iter, iter,
ops::{Index, IndexMut, Range}, ops::{Index, IndexMut, Range},
}; };
use std::io::{self, Write}; use std::io;
use bevy_asset::{ use bevy_asset::{
io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext,

View File

@ -1,8 +1,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
//! Animation for the game engine Bevy //! Animation for the game engine Bevy
@ -324,13 +324,13 @@ impl AnimationClip {
.push(variable_curve); .push(variable_curve);
} }
/// Add a untargeted [`Event`] to this [`AnimationClip`]. /// Add an [`EntityEvent`] with no [`AnimationTarget`] to this [`AnimationClip`].
/// ///
/// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds) /// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// is reached in the animation. /// is reached in the animation.
/// ///
/// See also [`add_event_to_target`](Self::add_event_to_target). /// See also [`add_event_to_target`](Self::add_event_to_target).
pub fn add_event(&mut self, time: f32, event: impl Event + Clone) { pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) {
self.add_event_fn( self.add_event_fn(
time, time,
move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| {
@ -339,7 +339,7 @@ impl AnimationClip {
); );
} }
/// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
/// ///
/// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds) /// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds)
/// is reached in the animation. /// is reached in the animation.
@ -349,7 +349,7 @@ impl AnimationClip {
&mut self, &mut self,
target_id: AnimationTargetId, target_id: AnimationTargetId,
time: f32, time: f32,
event: impl Event + Clone, event: impl EntityEvent + Clone,
) { ) {
self.add_event_fn_to_target( self.add_event_fn_to_target(
target_id, target_id,
@ -360,19 +360,19 @@ impl AnimationClip {
); );
} }
/// Add a untargeted event function to this [`AnimationClip`]. /// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`].
/// ///
/// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) /// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds)
/// is reached in the animation. /// is reached in the animation.
/// ///
/// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`]. /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event`].
/// See also [`add_event_to_target`](Self::add_event_to_target). /// See also [`add_event_to_target`](Self::add_event_to_target).
/// ///
/// ``` /// ```
/// # use bevy_animation::AnimationClip; /// # use bevy_animation::AnimationClip;
/// # let mut clip = AnimationClip::default(); /// # let mut clip = AnimationClip::default();
/// clip.add_event_fn(1.0, |commands, entity, time, weight| { /// clip.add_event_fn(1.0, |commands, entity, time, weight| {
/// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}");
/// }) /// })
/// ``` /// ```
pub fn add_event_fn( pub fn add_event_fn(
@ -388,14 +388,14 @@ impl AnimationClip {
/// The `func` will trigger on the entity matching the target once the `time` (in seconds) /// The `func` will trigger on the entity matching the target once the `time` (in seconds)
/// is reached in the animation. /// is reached in the animation.
/// ///
/// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`]. /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event_to_target`].
/// Use [`add_event`](Self::add_event) instead if you don't have a specific target. /// Use [`add_event`](Self::add_event) instead if you don't have a specific target.
/// ///
/// ``` /// ```
/// # use bevy_animation::{AnimationClip, AnimationTargetId}; /// # use bevy_animation::{AnimationClip, AnimationTargetId};
/// # let mut clip = AnimationClip::default(); /// # let mut clip = AnimationClip::default();
/// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| { /// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| {
/// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}");
/// }) /// })
/// ``` /// ```
pub fn add_event_fn_to_target( pub fn add_event_fn_to_target(
@ -1534,7 +1534,7 @@ mod tests {
use super::*; use super::*;
#[derive(Event, Reflect, Clone)] #[derive(Event, EntityEvent, Reflect, Clone)]
struct A; struct A;
#[track_caller] #[track_caller]

View File

@ -1,9 +1,9 @@
[package] [package]
name = "bevy_anti_aliasing" name = "bevy_anti_aliasing"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides various anti aliasing implementations for Bevy Engine" description = "Provides various anti aliasing implementations for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
@ -16,17 +16,17 @@ smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"]
[dependencies] [dependencies]
# bevy # bevy
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.17.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.16.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
# other # other
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }

View File

@ -1,9 +1,9 @@
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
core_2d::graph::{Core2d, Node2d}, core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d}, core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state, FullscreenShader,
}; };
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::BevyDefault as _; use bevy_image::BevyDefault as _;
@ -95,20 +95,12 @@ impl ExtractComponent for ContrastAdaptiveSharpening {
} }
} }
const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle<Shader> =
weak_handle!("ef83f0a5-51df-4b51-9ab7-b5fd1ae5a397");
/// Adds Support for Contrast Adaptive Sharpening (CAS). /// Adds Support for Contrast Adaptive Sharpening (CAS).
pub struct CasPlugin; pub struct CasPlugin;
impl Plugin for CasPlugin { impl Plugin for CasPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
load_internal_asset!( embedded_asset!(app, "robust_contrast_adaptive_sharpening.wgsl");
app,
CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
"robust_contrast_adaptive_sharpening.wgsl",
Shader::from_wgsl
);
app.register_type::<ContrastAdaptiveSharpening>(); app.register_type::<ContrastAdaptiveSharpening>();
app.add_plugins(( app.add_plugins((
@ -171,6 +163,8 @@ impl Plugin for CasPlugin {
pub struct CasPipeline { pub struct CasPipeline {
texture_bind_group: BindGroupLayout, texture_bind_group: BindGroupLayout,
sampler: Sampler, sampler: Sampler,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
} }
impl FromWorld for CasPipeline { impl FromWorld for CasPipeline {
@ -194,6 +188,11 @@ impl FromWorld for CasPipeline {
CasPipeline { CasPipeline {
texture_bind_group, texture_bind_group,
sampler, sampler,
fullscreen_shader: render_world.resource::<FullscreenShader>().clone(),
fragment_shader: load_embedded_asset!(
render_world,
"robust_contrast_adaptive_sharpening.wgsl"
),
} }
} }
} }
@ -215,9 +214,9 @@ impl SpecializedRenderPipeline for CasPipeline {
RenderPipelineDescriptor { RenderPipelineDescriptor {
label: Some("contrast_adaptive_sharpening".into()), label: Some("contrast_adaptive_sharpening".into()),
layout: vec![self.texture_bind_group.clone()], layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(), vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE, shader: self.fragment_shader.clone(),
shader_defs, shader_defs,
entry_point: "fragment".into(), entry_point: "fragment".into(),
targets: vec![Some(ColorTargetState { targets: vec![Some(ColorTargetState {

View File

@ -1,9 +0,0 @@
//! Experimental rendering features.
//!
//! Experimental features are features with known problems, missing features,
//! compatibility issues, low performance, and/or future breaking changes, but
//! are included nonetheless for testing purposes.
pub mod taa {
pub use crate::taa::{TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing};
}

View File

@ -1,9 +1,9 @@
use bevy_app::prelude::*; use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
core_2d::graph::{Core2d, Node2d}, core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d}, core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state, FullscreenShader,
}; };
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_image::BevyDefault as _; use bevy_image::BevyDefault as _;
@ -80,13 +80,11 @@ impl Default for Fxaa {
} }
} }
const FXAA_SHADER_HANDLE: Handle<Shader> = weak_handle!("fc58c0a8-01c0-46e9-94cc-83a794bae7b0");
/// Adds support for Fast Approximate Anti-Aliasing (FXAA) /// Adds support for Fast Approximate Anti-Aliasing (FXAA)
pub struct FxaaPlugin; pub struct FxaaPlugin;
impl Plugin for FxaaPlugin { impl Plugin for FxaaPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
load_internal_asset!(app, FXAA_SHADER_HANDLE, "fxaa.wgsl", Shader::from_wgsl); embedded_asset!(app, "fxaa.wgsl");
app.register_type::<Fxaa>(); app.register_type::<Fxaa>();
app.add_plugins(ExtractComponentPlugin::<Fxaa>::default()); app.add_plugins(ExtractComponentPlugin::<Fxaa>::default());
@ -132,6 +130,8 @@ impl Plugin for FxaaPlugin {
pub struct FxaaPipeline { pub struct FxaaPipeline {
texture_bind_group: BindGroupLayout, texture_bind_group: BindGroupLayout,
sampler: Sampler, sampler: Sampler,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
} }
impl FromWorld for FxaaPipeline { impl FromWorld for FxaaPipeline {
@ -158,6 +158,8 @@ impl FromWorld for FxaaPipeline {
FxaaPipeline { FxaaPipeline {
texture_bind_group, texture_bind_group,
sampler, sampler,
fullscreen_shader: render_world.resource::<FullscreenShader>().clone(),
fragment_shader: load_embedded_asset!(render_world, "fxaa.wgsl"),
} }
} }
} }
@ -181,9 +183,9 @@ impl SpecializedRenderPipeline for FxaaPipeline {
RenderPipelineDescriptor { RenderPipelineDescriptor {
label: Some("fxaa".into()), label: Some("fxaa".into()),
layout: vec![self.texture_bind_group.clone()], layout: vec![self.texture_bind_group.clone()],
vertex: fullscreen_shader_vertex_state(), vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: FXAA_SHADER_HANDLE, shader: self.fragment_shader.clone(),
shader_defs: vec![ shader_defs: vec![
format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(), format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(),
format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(), format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(),

View File

@ -2,26 +2,25 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
use bevy_app::Plugin; use bevy_app::Plugin;
use contrast_adaptive_sharpening::CasPlugin; use contrast_adaptive_sharpening::CasPlugin;
use fxaa::FxaaPlugin; use fxaa::FxaaPlugin;
use smaa::SmaaPlugin; use smaa::SmaaPlugin;
use taa::TemporalAntiAliasPlugin;
pub mod contrast_adaptive_sharpening; pub mod contrast_adaptive_sharpening;
pub mod experimental;
pub mod fxaa; pub mod fxaa;
pub mod smaa; pub mod smaa;
pub mod taa;
mod taa;
#[derive(Default)] #[derive(Default)]
pub struct AntiAliasingPlugin; pub struct AntiAliasingPlugin;
impl Plugin for AntiAliasingPlugin { impl Plugin for AntiAliasingPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.add_plugins((FxaaPlugin, CasPlugin, SmaaPlugin)); app.add_plugins((FxaaPlugin, SmaaPlugin, TemporalAntiAliasPlugin, CasPlugin));
} }
} }

View File

@ -32,7 +32,7 @@
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
#[cfg(feature = "smaa_luts")] #[cfg(feature = "smaa_luts")]
use bevy_asset::load_internal_binary_asset; use bevy_asset::load_internal_binary_asset;
use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_asset::{embedded_asset, load_embedded_asset, weak_handle, Handle};
#[cfg(not(feature = "smaa_luts"))] #[cfg(not(feature = "smaa_luts"))]
use bevy_core_pipeline::tonemapping::lut_placeholder; use bevy_core_pipeline::tonemapping::lut_placeholder;
use bevy_core_pipeline::{ use bevy_core_pipeline::{
@ -50,7 +50,7 @@ use bevy_ecs::{
system::{lifetimeless::Read, Commands, Query, Res, ResMut}, system::{lifetimeless::Read, Commands, Query, Res, ResMut},
world::{FromWorld, World}, world::{FromWorld, World},
}; };
use bevy_image::{BevyDefault, Image}; use bevy_image::{BevyDefault, Image, ToExtents};
use bevy_math::{vec4, Vec4}; use bevy_math::{vec4, Vec4};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{ use bevy_render::{
@ -64,14 +64,13 @@ use bevy_render::{
binding_types::{sampler, texture_2d, uniform_buffer}, binding_types::{sampler, texture_2d, uniform_buffer},
AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, AddressMode, BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState, CachedRenderPipelineId, ColorTargetState, ColorWrites, CompareFunction, DepthStencilState,
DynamicUniformBuffer, Extent3d, FilterMode, FragmentState, LoadOp, MultisampleState, DynamicUniformBuffer, FilterMode, FragmentState, LoadOp, MultisampleState, Operations,
Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipeline, RenderPassDescriptor, RenderPipeline, RenderPipelineDescriptor, SamplerBindingType,
RenderPipelineDescriptor, SamplerBindingType, SamplerDescriptor, Shader, ShaderDefVal, SamplerDescriptor, Shader, ShaderDefVal, ShaderStages, ShaderType,
ShaderStages, ShaderType, SpecializedRenderPipeline, SpecializedRenderPipelines, SpecializedRenderPipeline, SpecializedRenderPipelines, StencilFaceState, StencilOperation,
StencilFaceState, StencilOperation, StencilState, StoreOp, TextureDescriptor, StencilState, StoreOp, TextureDescriptor, TextureDimension, TextureFormat,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureView, TextureSampleType, TextureUsages, TextureView, VertexState,
VertexState,
}, },
renderer::{RenderContext, RenderDevice, RenderQueue}, renderer::{RenderContext, RenderDevice, RenderQueue},
texture::{CachedTexture, GpuImage, TextureCache}, texture::{CachedTexture, GpuImage, TextureCache},
@ -80,8 +79,6 @@ use bevy_render::{
}; };
use bevy_utils::prelude::default; use bevy_utils::prelude::default;
/// The handle of the `smaa.wgsl` shader.
const SMAA_SHADER_HANDLE: Handle<Shader> = weak_handle!("fdd9839f-1ab4-4e0d-88a0-240b67da2ddf");
/// The handle of the area LUT, a KTX2 format texture that SMAA uses internally. /// The handle of the area LUT, a KTX2 format texture that SMAA uses internally.
const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle<Image> = const SMAA_AREA_LUT_TEXTURE_HANDLE: Handle<Image> =
weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0"); weak_handle!("569c4d67-c7fa-4958-b1af-0836023603c0");
@ -147,6 +144,8 @@ struct SmaaEdgeDetectionPipeline {
postprocess_bind_group_layout: BindGroupLayout, postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass. /// The bind group layout for data specific to this pass.
edge_detection_bind_group_layout: BindGroupLayout, edge_detection_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
} }
/// The pipeline data for phase 2 of SMAA: blending weight calculation. /// The pipeline data for phase 2 of SMAA: blending weight calculation.
@ -155,6 +154,8 @@ struct SmaaBlendingWeightCalculationPipeline {
postprocess_bind_group_layout: BindGroupLayout, postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass. /// The bind group layout for data specific to this pass.
blending_weight_calculation_bind_group_layout: BindGroupLayout, blending_weight_calculation_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
} }
/// The pipeline data for phase 3 of SMAA: neighborhood blending. /// The pipeline data for phase 3 of SMAA: neighborhood blending.
@ -163,6 +164,8 @@ struct SmaaNeighborhoodBlendingPipeline {
postprocess_bind_group_layout: BindGroupLayout, postprocess_bind_group_layout: BindGroupLayout,
/// The bind group layout for data specific to this pass. /// The bind group layout for data specific to this pass.
neighborhood_blending_bind_group_layout: BindGroupLayout, neighborhood_blending_bind_group_layout: BindGroupLayout,
/// The shader asset handle.
shader: Handle<Shader>,
} }
/// A unique identifier for a set of SMAA pipelines. /// A unique identifier for a set of SMAA pipelines.
@ -287,7 +290,7 @@ pub struct SmaaSpecializedRenderPipelines {
impl Plugin for SmaaPlugin { impl Plugin for SmaaPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// Load the shader. // Load the shader.
load_internal_asset!(app, SMAA_SHADER_HANDLE, "smaa.wgsl", Shader::from_wgsl); embedded_asset!(app, "smaa.wgsl");
// Load the two lookup textures. These are compressed textures in KTX2 // Load the two lookup textures. These are compressed textures in KTX2
// format. // format.
@ -431,18 +434,23 @@ impl FromWorld for SmaaPipelines {
), ),
); );
let shader = load_embedded_asset!(world, "smaa.wgsl");
SmaaPipelines { SmaaPipelines {
edge_detection: SmaaEdgeDetectionPipeline { edge_detection: SmaaEdgeDetectionPipeline {
postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
edge_detection_bind_group_layout, edge_detection_bind_group_layout,
shader: shader.clone(),
}, },
blending_weight_calculation: SmaaBlendingWeightCalculationPipeline { blending_weight_calculation: SmaaBlendingWeightCalculationPipeline {
postprocess_bind_group_layout: postprocess_bind_group_layout.clone(), postprocess_bind_group_layout: postprocess_bind_group_layout.clone(),
blending_weight_calculation_bind_group_layout, blending_weight_calculation_bind_group_layout,
shader: shader.clone(),
}, },
neighborhood_blending: SmaaNeighborhoodBlendingPipeline { neighborhood_blending: SmaaNeighborhoodBlendingPipeline {
postprocess_bind_group_layout, postprocess_bind_group_layout,
neighborhood_blending_bind_group_layout, neighborhood_blending_bind_group_layout,
shader,
}, },
} }
} }
@ -472,13 +480,13 @@ impl SpecializedRenderPipeline for SmaaEdgeDetectionPipeline {
self.edge_detection_bind_group_layout.clone(), self.edge_detection_bind_group_layout.clone(),
], ],
vertex: VertexState { vertex: VertexState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs: shader_defs.clone(), shader_defs: shader_defs.clone(),
entry_point: "edge_detection_vertex_main".into(), entry_point: "edge_detection_vertex_main".into(),
buffers: vec![], buffers: vec![],
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs, shader_defs,
entry_point: "luma_edge_detection_fragment_main".into(), entry_point: "luma_edge_detection_fragment_main".into(),
targets: vec![Some(ColorTargetState { targets: vec![Some(ColorTargetState {
@ -532,13 +540,13 @@ impl SpecializedRenderPipeline for SmaaBlendingWeightCalculationPipeline {
self.blending_weight_calculation_bind_group_layout.clone(), self.blending_weight_calculation_bind_group_layout.clone(),
], ],
vertex: VertexState { vertex: VertexState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs: shader_defs.clone(), shader_defs: shader_defs.clone(),
entry_point: "blending_weight_calculation_vertex_main".into(), entry_point: "blending_weight_calculation_vertex_main".into(),
buffers: vec![], buffers: vec![],
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs, shader_defs,
entry_point: "blending_weight_calculation_fragment_main".into(), entry_point: "blending_weight_calculation_fragment_main".into(),
targets: vec![Some(ColorTargetState { targets: vec![Some(ColorTargetState {
@ -580,13 +588,13 @@ impl SpecializedRenderPipeline for SmaaNeighborhoodBlendingPipeline {
self.neighborhood_blending_bind_group_layout.clone(), self.neighborhood_blending_bind_group_layout.clone(),
], ],
vertex: VertexState { vertex: VertexState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs: shader_defs.clone(), shader_defs: shader_defs.clone(),
entry_point: "neighborhood_blending_vertex_main".into(), entry_point: "neighborhood_blending_vertex_main".into(),
buffers: vec![], buffers: vec![],
}, },
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: SMAA_SHADER_HANDLE, shader: self.shader.clone(),
shader_defs, shader_defs,
entry_point: "neighborhood_blending_fragment_main".into(), entry_point: "neighborhood_blending_fragment_main".into(),
targets: vec![Some(ColorTargetState { targets: vec![Some(ColorTargetState {
@ -695,18 +703,12 @@ fn prepare_smaa_textures(
continue; continue;
}; };
let texture_size = Extent3d {
width: texture_size.x,
height: texture_size.y,
depth_or_array_layers: 1,
};
// Create the two-channel RG texture for phase 1 (edge detection). // Create the two-channel RG texture for phase 1 (edge detection).
let edge_detection_color_texture = texture_cache.get( let edge_detection_color_texture = texture_cache.get(
&render_device, &render_device,
TextureDescriptor { TextureDescriptor {
label: Some("SMAA edge detection color texture"), label: Some("SMAA edge detection color texture"),
size: texture_size, size: texture_size.to_extents(),
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
@ -721,7 +723,7 @@ fn prepare_smaa_textures(
&render_device, &render_device,
TextureDescriptor { TextureDescriptor {
label: Some("SMAA edge detection stencil texture"), label: Some("SMAA edge detection stencil texture"),
size: texture_size, size: texture_size.to_extents(),
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
@ -737,7 +739,7 @@ fn prepare_smaa_textures(
&render_device, &render_device,
TextureDescriptor { TextureDescriptor {
label: Some("SMAA blend texture"), label: Some("SMAA blend texture"),
size: texture_size, size: texture_size.to_extents(),
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
@ -838,7 +840,7 @@ impl ViewNode for SmaaNode {
view_smaa_uniform_offset, view_smaa_uniform_offset,
smaa_textures, smaa_textures,
view_smaa_bind_groups, view_smaa_bind_groups,
): QueryItem<'w, Self::ViewQuery>, ): QueryItem<'w, '_, Self::ViewQuery>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let pipeline_cache = world.resource::<PipelineCache>(); let pipeline_cache = world.resource::<PipelineCache>();

View File

@ -1,10 +1,10 @@
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
core_3d::graph::{Core3d, Node3d}, core_3d::graph::{Core3d, Node3d},
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
prelude::Camera3d, prelude::Camera3d,
prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures}, prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures},
FullscreenShader,
}; };
use bevy_diagnostic::FrameCount; use bevy_diagnostic::FrameCount;
use bevy_ecs::{ use bevy_ecs::{
@ -15,7 +15,7 @@ use bevy_ecs::{
system::{Commands, Query, Res, ResMut}, system::{Commands, Query, Res, ResMut},
world::{FromWorld, World}, world::{FromWorld, World},
}; };
use bevy_image::BevyDefault as _; use bevy_image::{BevyDefault as _, ToExtents};
use bevy_math::vec2; use bevy_math::vec2;
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{ use bevy_render::{
@ -25,8 +25,8 @@ use bevy_render::{
render_resource::{ render_resource::{
binding_types::{sampler, texture_2d, texture_depth_2d}, binding_types::{sampler, texture_2d, texture_depth_2d},
BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedRenderPipelineId,
ColorTargetState, ColorWrites, Extent3d, FilterMode, FragmentState, MultisampleState, ColorTargetState, ColorWrites, FilterMode, FragmentState, MultisampleState, Operations,
Operations, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor, PipelineCache, PrimitiveState, RenderPassColorAttachment, RenderPassDescriptor,
RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader, RenderPipelineDescriptor, Sampler, SamplerBindingType, SamplerDescriptor, Shader,
ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor, ShaderStages, SpecializedRenderPipeline, SpecializedRenderPipelines, TextureDescriptor,
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
@ -40,8 +40,6 @@ use bevy_render::{
}; };
use tracing::warn; use tracing::warn;
const TAA_SHADER_HANDLE: Handle<Shader> = weak_handle!("fea20d50-86b6-4069-aa32-374346aec00c");
/// Plugin for temporal anti-aliasing. /// Plugin for temporal anti-aliasing.
/// ///
/// See [`TemporalAntiAliasing`] for more details. /// See [`TemporalAntiAliasing`] for more details.
@ -49,7 +47,7 @@ pub struct TemporalAntiAliasPlugin;
impl Plugin for TemporalAntiAliasPlugin { impl Plugin for TemporalAntiAliasPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
load_internal_asset!(app, TAA_SHADER_HANDLE, "taa.wgsl", Shader::from_wgsl); embedded_asset!(app, "taa.wgsl");
app.register_type::<TemporalAntiAliasing>(); app.register_type::<TemporalAntiAliasing>();
@ -64,7 +62,7 @@ impl Plugin for TemporalAntiAliasPlugin {
.add_systems( .add_systems(
Render, Render,
( (
prepare_taa_jitter_and_mip_bias.in_set(RenderSystems::ManageViews), prepare_taa_jitter.in_set(RenderSystems::ManageViews),
prepare_taa_pipelines.in_set(RenderSystems::Prepare), prepare_taa_pipelines.in_set(RenderSystems::Prepare),
prepare_taa_history_textures.in_set(RenderSystems::PrepareResources), prepare_taa_history_textures.in_set(RenderSystems::PrepareResources),
), ),
@ -115,7 +113,6 @@ impl Plugin for TemporalAntiAliasPlugin {
/// ///
/// # Usage Notes /// # Usage Notes
/// ///
/// The [`TemporalAntiAliasPlugin`] must be added to your app.
/// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`]. /// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`].
/// ///
/// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`]. /// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`].
@ -128,11 +125,9 @@ impl Plugin for TemporalAntiAliasPlugin {
/// ///
/// 1. Write particle motion vectors to the motion vectors prepass texture /// 1. Write particle motion vectors to the motion vectors prepass texture
/// 2. Render particles after TAA /// 2. Render particles after TAA
///
/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component.
#[derive(Component, Reflect, Clone)] #[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)] #[reflect(Component, Default, Clone)]
#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)] #[require(TemporalJitter, MipBias, DepthPrepass, MotionVectorPrepass)]
#[doc(alias = "Taa")] #[doc(alias = "Taa")]
pub struct TemporalAntiAliasing { pub struct TemporalAntiAliasing {
/// Set to true to delete the saved temporal history (past frames). /// Set to true to delete the saved temporal history (past frames).
@ -243,6 +238,8 @@ struct TaaPipeline {
taa_bind_group_layout: BindGroupLayout, taa_bind_group_layout: BindGroupLayout,
nearest_sampler: Sampler, nearest_sampler: Sampler,
linear_sampler: Sampler, linear_sampler: Sampler,
fullscreen_shader: FullscreenShader,
fragment_shader: Handle<Shader>,
} }
impl FromWorld for TaaPipeline { impl FromWorld for TaaPipeline {
@ -287,6 +284,8 @@ impl FromWorld for TaaPipeline {
taa_bind_group_layout, taa_bind_group_layout,
nearest_sampler, nearest_sampler,
linear_sampler, linear_sampler,
fullscreen_shader: world.resource::<FullscreenShader>().clone(),
fragment_shader: load_embedded_asset!(world, "taa.wgsl"),
} }
} }
} }
@ -317,9 +316,9 @@ impl SpecializedRenderPipeline for TaaPipeline {
RenderPipelineDescriptor { RenderPipelineDescriptor {
label: Some("taa_pipeline".into()), label: Some("taa_pipeline".into()),
layout: vec![self.taa_bind_group_layout.clone()], layout: vec![self.taa_bind_group_layout.clone()],
vertex: fullscreen_shader_vertex_state(), vertex: self.fullscreen_shader.to_vertex_state(),
fragment: Some(FragmentState { fragment: Some(FragmentState {
shader: TAA_SHADER_HANDLE, shader: self.fragment_shader.clone(),
shader_defs, shader_defs,
entry_point: "taa".into(), entry_point: "taa".into(),
targets: vec![ targets: vec![
@ -345,16 +344,11 @@ impl SpecializedRenderPipeline for TaaPipeline {
} }
fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld>) { fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld>) {
let mut cameras_3d = main_world.query_filtered::<( let mut cameras_3d = main_world.query::<(
RenderEntity, RenderEntity,
&Camera, &Camera,
&Projection, &Projection,
&mut TemporalAntiAliasing, Option<&mut TemporalAntiAliasing>,
), (
With<Camera3d>,
With<TemporalJitter>,
With<DepthPrepass>,
With<MotionVectorPrepass>,
)>(); )>();
for (entity, camera, camera_projection, mut taa_settings) in for (entity, camera, camera_projection, mut taa_settings) in
@ -364,14 +358,12 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld
let mut entity_commands = commands let mut entity_commands = commands
.get_entity(entity) .get_entity(entity)
.expect("Camera entity wasn't synced."); .expect("Camera entity wasn't synced.");
if camera.is_active && has_perspective_projection { if taa_settings.is_some() && camera.is_active && has_perspective_projection {
entity_commands.insert(taa_settings.clone()); entity_commands.insert(taa_settings.as_deref().unwrap().clone());
taa_settings.reset = false; taa_settings.as_mut().unwrap().reset = false;
} else { } else {
// TODO: needs better strategy for cleaning up
entity_commands.remove::<( entity_commands.remove::<(
TemporalAntiAliasing, TemporalAntiAliasing,
// components added in prepare systems (because `TemporalAntiAliasNode` does not query extracted components)
TemporalAntiAliasHistoryTextures, TemporalAntiAliasHistoryTextures,
TemporalAntiAliasPipelineId, TemporalAntiAliasPipelineId,
)>(); )>();
@ -379,13 +371,22 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut<MainWorld
} }
} }
fn prepare_taa_jitter_and_mip_bias( fn prepare_taa_jitter(
frame_count: Res<FrameCount>, frame_count: Res<FrameCount>,
mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With<TemporalAntiAliasing>>, mut query: Query<
mut commands: Commands, &mut TemporalJitter,
(
With<TemporalAntiAliasing>,
With<Camera3d>,
With<TemporalJitter>,
With<DepthPrepass>,
With<MotionVectorPrepass>,
),
>,
) { ) {
// Halton sequence (2, 3) - 0.5, skipping i = 0 // Halton sequence (2, 3) - 0.5
let halton_sequence = [ let halton_sequence = [
vec2(0.0, 0.0),
vec2(0.0, -0.16666666), vec2(0.0, -0.16666666),
vec2(-0.25, 0.16666669), vec2(-0.25, 0.16666669),
vec2(0.25, -0.3888889), vec2(0.25, -0.3888889),
@ -393,17 +394,12 @@ fn prepare_taa_jitter_and_mip_bias(
vec2(0.125, 0.2777778), vec2(0.125, 0.2777778),
vec2(-0.125, -0.2777778), vec2(-0.125, -0.2777778),
vec2(0.375, 0.055555582), vec2(0.375, 0.055555582),
vec2(-0.4375, 0.3888889),
]; ];
let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()]; let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()];
for (entity, mut jitter, mip_bias) in &mut query { for mut jitter in &mut query {
jitter.offset = offset; jitter.offset = offset;
if mip_bias.is_none() {
commands.entity(entity).insert(MipBias(-1.0));
}
} }
} }
@ -424,11 +420,7 @@ fn prepare_taa_history_textures(
if let Some(physical_target_size) = camera.physical_target_size { if let Some(physical_target_size) = camera.physical_target_size {
let mut texture_descriptor = TextureDescriptor { let mut texture_descriptor = TextureDescriptor {
label: None, label: None,
size: Extent3d { size: physical_target_size.to_extents(),
depth_or_array_layers: 1,
width: physical_target_size.x,
height: physical_target_size.y,
},
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,

View File

@ -1,9 +1,9 @@
[package] [package]
name = "bevy_app" name = "bevy_app"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides core App functionality for Bevy Engine" description = "Provides core App functionality for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
@ -47,7 +47,6 @@ std = [
"bevy_ecs/std", "bevy_ecs/std",
"dep:ctrlc", "dep:ctrlc",
"downcast-rs/std", "downcast-rs/std",
"bevy_utils/std",
"bevy_tasks/std", "bevy_tasks/std",
"bevy_platform/std", "bevy_platform/std",
] ]
@ -72,16 +71,20 @@ web = [
"dep:console_error_panic_hook", "dep:console_error_panic_hook",
] ]
hotpatching = [
"bevy_ecs/hotpatching",
"dep:dioxus-devtools",
"dep:crossbeam-channel",
]
[dependencies] [dependencies]
# bevy # bevy
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false, features = [ bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false }
"alloc", bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false }
] } bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false }
bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false }
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false }
# other # other
downcast-rs = { version = "2", default-features = false } downcast-rs = { version = "2", default-features = false }
@ -90,8 +93,10 @@ variadics_please = "1.1"
tracing = { version = "0.1", default-features = false, optional = true } tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false } log = { version = "0.4", default-features = false }
cfg-if = "1.0.0" cfg-if = "1.0.0"
dioxus-devtools = { version = "0.7.0-alpha.1", optional = true }
crossbeam-channel = { version = "0.5.0", optional = true }
[target.'cfg(any(unix, windows))'.dependencies] [target.'cfg(any(all(unix, not(target_os = "horizon")), windows))'.dependencies]
ctrlc = { version = "3.4.4", optional = true } ctrlc = { version = "3.4.4", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@ -106,10 +106,13 @@ impl Default for App {
#[cfg(feature = "bevy_reflect")] #[cfg(feature = "bevy_reflect")]
{ {
use bevy_ecs::observer::ObservedBy;
app.init_resource::<AppTypeRegistry>(); app.init_resource::<AppTypeRegistry>();
app.register_type::<Name>(); app.register_type::<Name>();
app.register_type::<ChildOf>(); app.register_type::<ChildOf>();
app.register_type::<Children>(); app.register_type::<Children>();
app.register_type::<ObservedBy>();
} }
#[cfg(feature = "reflect_functions")] #[cfg(feature = "reflect_functions")]
@ -341,7 +344,7 @@ impl App {
self self
} }
/// Initializes `T` event handling by inserting an event queue resource ([`Events::<T>`]) /// Initializes [`BufferedEvent`] handling for `T` by inserting an event queue resource ([`Events::<T>`])
/// and scheduling an [`event_update_system`] in [`First`]. /// and scheduling an [`event_update_system`] in [`First`].
/// ///
/// See [`Events`] for information on how to define events. /// See [`Events`] for information on how to define events.
@ -352,7 +355,7 @@ impl App {
/// # use bevy_app::prelude::*; /// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*; /// # use bevy_ecs::prelude::*;
/// # /// #
/// # #[derive(Event)] /// # #[derive(Event, BufferedEvent)]
/// # struct MyEvent; /// # struct MyEvent;
/// # let mut app = App::new(); /// # let mut app = App::new();
/// # /// #
@ -360,7 +363,7 @@ impl App {
/// ``` /// ```
pub fn add_event<T>(&mut self) -> &mut Self pub fn add_event<T>(&mut self) -> &mut Self
where where
T: Event, T: BufferedEvent,
{ {
self.main_mut().add_event::<T>(); self.main_mut().add_event::<T>();
self self
@ -1306,6 +1309,8 @@ impl App {
/// Spawns an [`Observer`] entity, which will watch for and respond to the given event. /// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
/// ///
/// `observer` can be any system whose first parameter is [`On`].
///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
@ -1320,14 +1325,14 @@ impl App {
/// # friends_allowed: bool, /// # friends_allowed: bool,
/// # }; /// # };
/// # /// #
/// # #[derive(Event)] /// # #[derive(Event, EntityEvent)]
/// # struct Invite; /// # struct Invite;
/// # /// #
/// # #[derive(Component)] /// # #[derive(Component)]
/// # struct Friend; /// # struct Friend;
/// # /// #
/// // An observer system can be any system where the first parameter is a trigger ///
/// app.add_observer(|trigger: Trigger<Party>, friends: Query<Entity, With<Friend>>, mut commands: Commands| { /// app.add_observer(|trigger: On<Party>, friends: Query<Entity, With<Friend>>, mut commands: Commands| {
/// if trigger.event().friends_allowed { /// if trigger.event().friends_allowed {
/// for friend in friends.iter() { /// for friend in friends.iter() {
/// commands.trigger_targets(Invite, friend); /// commands.trigger_targets(Invite, friend);
@ -1402,7 +1407,7 @@ fn run_once(mut app: App) -> AppExit {
app.should_exit().unwrap_or(AppExit::Success) app.should_exit().unwrap_or(AppExit::Success)
} }
/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update, /// A [`BufferedEvent`] that indicates the [`App`] should exit. If one or more of these are present at the end of an update,
/// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller. /// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller.
/// ///
/// This event can be used to detect when an exit is requested. Make sure that systems listening /// This event can be used to detect when an exit is requested. Make sure that systems listening
@ -1412,7 +1417,7 @@ fn run_once(mut app: App) -> AppExit {
/// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns /// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns
/// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#)) /// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#))
/// we only allow error codes between 1 and [255](u8::MAX). /// we only allow error codes between 1 and [255](u8::MAX).
#[derive(Event, Debug, Clone, Default, PartialEq, Eq)] #[derive(Event, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)]
pub enum AppExit { pub enum AppExit {
/// [`App`] exited without any problems. /// [`App`] exited without any problems.
#[default] #[default]
@ -1480,9 +1485,9 @@ mod tests {
change_detection::{DetectChanges, ResMut}, change_detection::{DetectChanges, ResMut},
component::Component, component::Component,
entity::Entity, entity::Entity,
event::{Event, EventWriter, Events}, event::{BufferedEvent, Event, EventWriter, Events},
lifecycle::RemovedComponents,
query::With, query::With,
removal_detection::RemovedComponents,
resource::Resource, resource::Resource,
schedule::{IntoScheduleConfigs, ScheduleLabel}, schedule::{IntoScheduleConfigs, ScheduleLabel},
system::{Commands, Query}, system::{Commands, Query},
@ -1577,7 +1582,7 @@ mod tests {
app.add_systems(EnterMainMenu, (foo, bar)); app.add_systems(EnterMainMenu, (foo, bar));
app.world_mut().run_schedule(EnterMainMenu); app.world_mut().run_schedule(EnterMainMenu);
assert_eq!(app.world().entities().len(), 2); assert_eq!(app.world().entity_count(), 2);
} }
#[test] #[test]
@ -1846,7 +1851,7 @@ mod tests {
} }
#[test] #[test]
fn events_should_be_updated_once_per_update() { fn events_should_be_updated_once_per_update() {
#[derive(Event, Clone)] #[derive(Event, BufferedEvent, Clone)]
struct TestEvent; struct TestEvent;
let mut app = App::new(); let mut app = App::new();

View File

@ -0,0 +1,42 @@
//! Utilities for hotpatching code.
extern crate alloc;
use alloc::sync::Arc;
use bevy_ecs::{event::EventWriter, HotPatched};
#[cfg(not(target_family = "wasm"))]
use dioxus_devtools::connect_subsecond;
use dioxus_devtools::subsecond;
pub use dioxus_devtools::subsecond::{call, HotFunction};
use crate::{Last, Plugin};
/// Plugin connecting to Dioxus CLI to enable hot patching.
#[derive(Default)]
pub struct HotPatchPlugin;
impl Plugin for HotPatchPlugin {
fn build(&self, app: &mut crate::App) {
let (sender, receiver) = crossbeam_channel::bounded::<HotPatched>(1);
// Connects to the dioxus CLI that will handle rebuilds
// This will open a connection to the dioxus CLI to receive updated jump tables
// Sends a `HotPatched` message through the channel when the jump table is updated
#[cfg(not(target_family = "wasm"))]
connect_subsecond();
subsecond::register_handler(Arc::new(move || {
sender.send(HotPatched).unwrap();
}));
// Adds a system that will read the channel for new `HotPatched`, and forward them as event to the ECS
app.add_event::<HotPatched>().add_systems(
Last,
move |mut events: EventWriter<HotPatched>| {
if receiver.try_recv().is_ok() {
events.write_default();
}
},
);
}
}

View File

@ -8,8 +8,8 @@
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
#![no_std] #![no_std]
@ -28,21 +28,26 @@ mod main_schedule;
mod panic_handler; mod panic_handler;
mod plugin; mod plugin;
mod plugin_group; mod plugin_group;
mod propagate;
mod schedule_runner; mod schedule_runner;
mod sub_app; mod sub_app;
mod task_pool_plugin; mod task_pool_plugin;
#[cfg(all(any(unix, windows), feature = "std"))] #[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
mod terminal_ctrl_c_handler; mod terminal_ctrl_c_handler;
#[cfg(feature = "hotpatching")]
pub mod hotpatch;
pub use app::*; pub use app::*;
pub use main_schedule::*; pub use main_schedule::*;
pub use panic_handler::*; pub use panic_handler::*;
pub use plugin::*; pub use plugin::*;
pub use plugin_group::*; pub use plugin_group::*;
pub use propagate::*;
pub use schedule_runner::*; pub use schedule_runner::*;
pub use sub_app::*; pub use sub_app::*;
pub use task_pool_plugin::*; pub use task_pool_plugin::*;
#[cfg(all(any(unix, windows), feature = "std"))] #[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
pub use terminal_ctrl_c_handler::*; pub use terminal_ctrl_c_handler::*;
/// The app prelude. /// The app prelude.

View File

@ -1,4 +1,4 @@
//! This module provides panic handlers for [Bevy](https://bevyengine.org) //! This module provides panic handlers for [Bevy](https://bevy.org)
//! apps, and automatically configures platform specifics (i.e. Wasm or Android). //! apps, and automatically configures platform specifics (i.e. Wasm or Android).
//! //!
//! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`. //! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`.

View File

@ -0,0 +1,552 @@
use alloc::vec::Vec;
use core::marker::PhantomData;
use crate::{App, Plugin, Update};
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::ChildOf,
lifecycle::RemovedComponents,
query::{Changed, Or, QueryFilter, With, Without},
relationship::{Relationship, RelationshipTarget},
schedule::{IntoScheduleConfigs, SystemSet},
system::{Commands, Local, Query},
};
/// Plugin to automatically propagate a component value to all direct and transient relationship
/// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component.
///
/// The plugin Will maintain the target component over hierarchy changes, adding or removing
/// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a
/// relationship tree with a [`Propagate<C>`] source, or if the [`Propagate<C>`] component
/// is added, changed or removed.
///
/// Optionally you can include a query filter `F` to restrict the entities that are updated.
/// Note that the filter is not rechecked dynamically: changes to the filter state will not be
/// picked up until the [`Propagate`] component is touched, or the hierarchy is changed.
/// All members of the tree between source and target must match the filter for propagation
/// to reach a given target.
/// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`]
/// and [`PropagateStop`] components.
pub struct HierarchyPropagatePlugin<
C: Component + Clone + PartialEq,
F: QueryFilter = (),
R: Relationship = ChildOf,
>(PhantomData<fn() -> (C, F, R)>);
/// Causes the inner component to be added to this entity and all direct and transient relationship
/// targets. A target with a [`Propagate<C>`] component of its own will override propagation from
/// that point in the tree.
#[derive(Component, Clone, PartialEq)]
pub struct Propagate<C: Component + Clone + PartialEq>(pub C);
/// Stops the output component being added to this entity.
/// Relationship targets will still inherit the component from this entity or its parents.
#[derive(Component)]
pub struct PropagateOver<C>(PhantomData<fn() -> C>);
/// Stops the propagation at this entity. Children will not inherit the component.
#[derive(Component)]
pub struct PropagateStop<C>(PhantomData<fn() -> C>);
/// The set in which propagation systems are added. You can schedule your logic relative to this set.
#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)]
pub struct PropagateSet<C: Component + Clone + PartialEq> {
_p: PhantomData<fn() -> C>,
}
/// Internal struct for managing propagation
#[derive(Component, Clone, PartialEq)]
pub struct Inherited<C: Component + Clone + PartialEq>(pub C);
impl<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship> Default
for HierarchyPropagatePlugin<C, F, R>
{
fn default() -> Self {
Self(Default::default())
}
}
impl<C> Default for PropagateOver<C> {
fn default() -> Self {
Self(Default::default())
}
}
impl<C> Default for PropagateStop<C> {
fn default() -> Self {
Self(Default::default())
}
}
impl<C: Component + Clone + PartialEq> core::fmt::Debug for PropagateSet<C> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PropagateSet")
.field("_p", &self._p)
.finish()
}
}
impl<C: Component + Clone + PartialEq> Eq for PropagateSet<C> {}
impl<C: Component + Clone + PartialEq> core::hash::Hash for PropagateSet<C> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self._p.hash(state);
}
}
impl<C: Component + Clone + PartialEq> Default for PropagateSet<C> {
fn default() -> Self {
Self {
_p: Default::default(),
}
}
}
impl<C: Component + Clone + PartialEq, F: QueryFilter + 'static, R: Relationship> Plugin
for HierarchyPropagatePlugin<C, F, R>
{
fn build(&self, app: &mut App) {
app.add_systems(
Update,
(
update_source::<C, F>,
update_stopped::<C, F>,
update_reparented::<C, F, R>,
propagate_inherited::<C, F, R>,
propagate_output::<C, F>,
)
.chain()
.in_set(PropagateSet::<C>::default()),
);
}
}
/// add/remove `Inherited::<C>` and `C` for entities with a direct `Propagate::<C>`
pub fn update_source<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(Entity, &Propagate<C>),
(
Or<(Changed<Propagate<C>>, Without<Inherited<C>>)>,
Without<PropagateStop<C>>,
),
>,
mut removed: RemovedComponents<Propagate<C>>,
) {
for (entity, source) in &changed {
commands
.entity(entity)
.try_insert(Inherited(source.0.clone()));
}
for removed in removed.read() {
if let Ok(mut commands) = commands.get_entity(removed) {
commands.remove::<(Inherited<C>, C)>();
}
}
}
/// remove `Inherited::<C>` and `C` for entities with a `PropagateStop::<C>`
pub fn update_stopped<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
q: Query<Entity, (With<Inherited<C>>, With<PropagateStop<C>>, F)>,
) {
for entity in q.iter() {
let mut cmds = commands.entity(entity);
cmds.remove::<(Inherited<C>, C)>();
}
}
/// add/remove `Inherited::<C>` and `C` for entities which have changed relationship
pub fn update_reparented<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship>(
mut commands: Commands,
moved: Query<
(Entity, &R, Option<&Inherited<C>>),
(
Changed<R>,
Without<Propagate<C>>,
Without<PropagateStop<C>>,
F,
),
>,
relations: Query<&Inherited<C>>,
orphaned: Query<Entity, (With<Inherited<C>>, Without<Propagate<C>>, Without<R>, F)>,
) {
for (entity, relation, maybe_inherited) in &moved {
if let Ok(inherited) = relations.get(relation.get()) {
commands.entity(entity).try_insert(inherited.clone());
} else if maybe_inherited.is_some() {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
for orphan in &orphaned {
commands.entity(orphan).remove::<(Inherited<C>, C)>();
}
}
/// add/remove `Inherited::<C>` for targets of entities with modified `Inherited::<C>`
pub fn propagate_inherited<C: Component + Clone + PartialEq, F: QueryFilter, R: Relationship>(
mut commands: Commands,
changed: Query<
(&Inherited<C>, &R::RelationshipTarget),
(Changed<Inherited<C>>, Without<PropagateStop<C>>, F),
>,
recurse: Query<
(Option<&R::RelationshipTarget>, Option<&Inherited<C>>),
(Without<Propagate<C>>, Without<PropagateStop<C>>, F),
>,
mut removed: RemovedComponents<Inherited<C>>,
mut to_process: Local<Vec<(Entity, Option<Inherited<C>>)>>,
) {
// gather changed
for (inherited, targets) in &changed {
to_process.extend(
targets
.iter()
.map(|target| (target, Some(inherited.clone()))),
);
}
// and removed
for entity in removed.read() {
if let Ok((Some(targets), _)) = recurse.get(entity) {
to_process.extend(targets.iter().map(|target| (target, None)));
}
}
// propagate
while let Some((entity, maybe_inherited)) = (*to_process).pop() {
let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else {
continue;
};
if maybe_current == maybe_inherited.as_ref() {
continue;
}
if let Some(targets) = maybe_targets {
to_process.extend(
targets
.iter()
.map(|target| (target, maybe_inherited.clone())),
);
}
if let Some(inherited) = maybe_inherited {
commands.entity(entity).try_insert(inherited.clone());
} else {
commands.entity(entity).remove::<(Inherited<C>, C)>();
}
}
}
/// add `C` to entities with `Inherited::<C>`
pub fn propagate_output<C: Component + Clone + PartialEq, F: QueryFilter>(
mut commands: Commands,
changed: Query<
(Entity, &Inherited<C>, Option<&C>),
(Changed<Inherited<C>>, Without<PropagateOver<C>>, F),
>,
) {
for (entity, inherited, maybe_current) in &changed {
if maybe_current.is_some_and(|c| &inherited.0 == c) {
continue;
}
commands.entity(entity).try_insert(inherited.0.clone());
}
}
#[cfg(test)]
mod tests {
use bevy_ecs::schedule::Schedule;
use crate::{App, Update};
use super::*;
#[derive(Component, Clone, PartialEq, Debug)]
struct TestValue(u32);
#[test]
fn test_simple_propagate() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let intermediate = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(intermediate))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_ok());
}
#[test]
fn test_reparented() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_ok());
}
#[test]
fn test_reparented_with_prior() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator_a))
.id();
app.update();
assert_eq!(
app.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee),
Ok(&TestValue(1))
);
app.world_mut()
.commands()
.entity(propagatee)
.insert(ChildOf(propagator_b));
app.update();
assert_eq!(
app.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee),
Ok(&TestValue(2))
);
}
#[test]
fn test_remove_orphan() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_ok());
app.world_mut()
.commands()
.entity(propagatee)
.remove::<ChildOf>();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_err());
}
#[test]
fn test_remove_propagated() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_ok());
app.world_mut()
.commands()
.entity(propagator)
.remove::<Propagate<TestValue>>();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_err());
}
#[test]
fn test_propagate_over() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagate_over = app
.world_mut()
.spawn(TestValue(2))
.insert(ChildOf(propagator))
.id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagate_over))
.id();
app.update();
assert_eq!(
app.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee),
Ok(&TestValue(1))
);
}
#[test]
fn test_propagate_stop() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagate_stop = app
.world_mut()
.spawn(PropagateStop::<TestValue>::default())
.insert(ChildOf(propagator))
.id();
let no_propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagate_stop))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), no_propagatee)
.is_err());
}
#[test]
fn test_intermediate_override() {
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let intermediate = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(intermediate))
.id();
app.update();
assert_eq!(
app.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee),
Ok(&TestValue(1))
);
app.world_mut()
.entity_mut(intermediate)
.insert(Propagate(TestValue(2)));
app.update();
assert_eq!(
app.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee),
Ok(&TestValue(2))
);
}
#[test]
fn test_filter() {
#[derive(Component)]
struct Marker;
let mut app = App::new();
app.add_schedule(Schedule::new(Update));
app.add_plugins(HierarchyPropagatePlugin::<TestValue, With<Marker>>::default());
let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
let propagatee = app
.world_mut()
.spawn_empty()
.insert(ChildOf(propagator))
.id();
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_err());
// NOTE: changes to the filter condition are not rechecked
app.world_mut().entity_mut(propagator).insert(Marker);
app.world_mut().entity_mut(propagatee).insert(Marker);
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_err());
app.world_mut()
.entity_mut(propagator)
.insert(Propagate(TestValue(1)));
app.update();
assert!(app
.world_mut()
.query::<&TestValue>()
.get(app.world(), propagatee)
.is_ok());
}
}

View File

@ -338,7 +338,7 @@ impl SubApp {
/// See [`App::add_event`]. /// See [`App::add_event`].
pub fn add_event<T>(&mut self) -> &mut Self pub fn add_event<T>(&mut self) -> &mut Self
where where
T: Event, T: BufferedEvent,
{ {
if !self.world.contains_resource::<Events<T>>() { if !self.world.contains_resource::<Events<T>>() {
EventRegistry::register_event::<T>(self.world_mut()); EventRegistry::register_event::<T>(self.world_mut());

View File

@ -160,7 +160,7 @@ impl TaskPoolOptions {
pub fn create_default_pools(&self) { pub fn create_default_pools(&self) {
let total_threads = bevy_tasks::available_parallelism() let total_threads = bevy_tasks::available_parallelism()
.clamp(self.min_total_threads, self.max_total_threads); .clamp(self.min_total_threads, self.max_total_threads);
trace!("Assigning {} cores to default task pools", total_threads); trace!("Assigning {total_threads} cores to default task pools");
let mut remaining_threads = total_threads; let mut remaining_threads = total_threads;
@ -170,7 +170,7 @@ impl TaskPoolOptions {
.io .io
.get_number_of_threads(remaining_threads, total_threads); .get_number_of_threads(remaining_threads, total_threads);
trace!("IO Threads: {}", io_threads); trace!("IO Threads: {io_threads}");
remaining_threads = remaining_threads.saturating_sub(io_threads); remaining_threads = remaining_threads.saturating_sub(io_threads);
IoTaskPool::get_or_init(|| { IoTaskPool::get_or_init(|| {
@ -200,7 +200,7 @@ impl TaskPoolOptions {
.async_compute .async_compute
.get_number_of_threads(remaining_threads, total_threads); .get_number_of_threads(remaining_threads, total_threads);
trace!("Async Compute Threads: {}", async_compute_threads); trace!("Async Compute Threads: {async_compute_threads}");
remaining_threads = remaining_threads.saturating_sub(async_compute_threads); remaining_threads = remaining_threads.saturating_sub(async_compute_threads);
AsyncComputeTaskPool::get_or_init(|| { AsyncComputeTaskPool::get_or_init(|| {
@ -231,7 +231,7 @@ impl TaskPoolOptions {
.compute .compute
.get_number_of_threads(remaining_threads, total_threads); .get_number_of_threads(remaining_threads, total_threads);
trace!("Compute Threads: {}", compute_threads); trace!("Compute Threads: {compute_threads}");
ComputeTaskPool::get_or_init(|| { ComputeTaskPool::get_or_init(|| {
let builder = TaskPoolBuilder::default() let builder = TaskPoolBuilder::default()

View File

@ -1,9 +1,9 @@
[package] [package]
name = "bevy_asset" name = "bevy_asset"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides asset functionality for Bevy Engine" description = "Provides asset functionality for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
@ -21,19 +21,19 @@ watch = []
trace = [] trace = []
[dependencies] [dependencies]
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [
"bevy_reflect", "bevy_reflect",
] } ] }
bevy_asset_macros = { path = "macros", version = "0.16.0-dev" } bevy_asset_macros = { path = "macros", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [
"uuid", "uuid",
] } ] }
bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [
"async_executor", "async_executor",
] } ] }
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", default-features = false } bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false }
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std", "std",
] } ] }
@ -56,10 +56,10 @@ parking_lot = { version = "0.12", default-features = false, features = [
"arc_lock", "arc_lock",
"send_guard", "send_guard",
] } ] }
ron = { version = "0.8", default-features = false } ron = { version = "0.10", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] } serde = { version = "1", default-features = false, features = ["derive"] }
thiserror = { version = "2", default-features = false } thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] } derive_more = { version = "2", default-features = false, features = ["from"] }
uuid = { version = "1.13.1", default-features = false, features = [ uuid = { version = "1.13.1", default-features = false, features = [
"v4", "v4",
"serde", "serde",
@ -67,7 +67,7 @@ uuid = { version = "1.13.1", default-features = false, features = [
tracing = { version = "0.1", default-features = false } tracing = { version = "0.1", default-features = false }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
@ -80,13 +80,13 @@ web-sys = { version = "0.3", features = [
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
js-sys = "0.3" js-sys = "0.3"
uuid = { version = "1.13.1", default-features = false, features = ["js"] } uuid = { version = "1.13.1", default-features = false, features = ["js"] }
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [
"web", "web",
] } ] }
bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev", default-features = false, features = [ bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false, features = [
"web", "web",
] } ] }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [
"web", "web",
] } ] }

View File

@ -1,9 +1,9 @@
[package] [package]
name = "bevy_asset_macros" name = "bevy_asset_macros"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Derive implementations for bevy_asset" description = "Derive implementations for bevy_asset"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
@ -12,7 +12,7 @@ keywords = ["bevy"]
proc-macro = true proc-macro = true
[dependencies] [dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.16.0-dev" } bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" }
syn = "2.0" syn = "2.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"

View File

@ -1,6 +1,7 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! Macros for deriving asset traits.
use bevy_macro_utils::BevyManifest; use bevy_macro_utils::BevyManifest;
use proc_macro::{Span, TokenStream}; use proc_macro::{Span, TokenStream};
use quote::{format_ident, quote}; use quote::{format_ident, quote};
@ -12,6 +13,7 @@ pub(crate) fn bevy_asset_path() -> Path {
const DEPENDENCY_ATTRIBUTE: &str = "dependency"; const DEPENDENCY_ATTRIBUTE: &str = "dependency";
/// Implement the `Asset` trait.
#[proc_macro_derive(Asset, attributes(dependency))] #[proc_macro_derive(Asset, attributes(dependency))]
pub fn derive_asset(input: TokenStream) -> TokenStream { pub fn derive_asset(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);
@ -30,6 +32,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream {
}) })
} }
/// Implement the `VisitAssetDependencies` trait.
#[proc_macro_derive(VisitAssetDependencies, attributes(dependency))] #[proc_macro_derive(VisitAssetDependencies, attributes(dependency))]
pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream { pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);

View File

@ -106,7 +106,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> {
/// - Removed assets are not detected. /// - Removed assets are not detected.
/// ///
/// The list of changed assets only gets updated in the [`AssetEventSystems`] system set, /// The list of changed assets only gets updated in the [`AssetEventSystems`] system set,
/// which runs in `Last`. Therefore, `AssetChanged` will only pick up asset changes in schedules /// which runs in `PostUpdate`. Therefore, `AssetChanged` will only pick up asset changes in schedules
/// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule /// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule
/// after [`AssetEventSystems`] if you need to react without frame delay to asset changes. /// after [`AssetEventSystems`] if you need to react without frame delay to asset changes.
/// ///
@ -158,9 +158,9 @@ unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
fetch fetch
} }
unsafe fn init_fetch<'w>( unsafe fn init_fetch<'w, 's>(
world: UnsafeWorldCell<'w>, world: UnsafeWorldCell<'w>,
state: &Self::State, state: &'s Self::State,
last_run: Tick, last_run: Tick,
this_run: Tick, this_run: Tick,
) -> Self::Fetch<'w> { ) -> Self::Fetch<'w> {
@ -201,9 +201,9 @@ unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
const IS_DENSE: bool = <&A>::IS_DENSE; const IS_DENSE: bool = <&A>::IS_DENSE;
unsafe fn set_archetype<'w>( unsafe fn set_archetype<'w, 's>(
fetch: &mut Self::Fetch<'w>, fetch: &mut Self::Fetch<'w>,
state: &Self::State, state: &'s Self::State,
archetype: &'w Archetype, archetype: &'w Archetype,
table: &'w Table, table: &'w Table,
) { ) {
@ -215,7 +215,11 @@ unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
} }
} }
unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { unsafe fn set_table<'w, 's>(
fetch: &mut Self::Fetch<'w>,
state: &Self::State,
table: &'w Table,
) {
if let Some(inner) = &mut fetch.inner { if let Some(inner) = &mut fetch.inner {
// SAFETY: We delegate to the inner `set_table` for `A` // SAFETY: We delegate to the inner `set_table` for `A`
unsafe { unsafe {
@ -265,6 +269,7 @@ unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
#[inline] #[inline]
unsafe fn filter_fetch( unsafe fn filter_fetch(
state: &Self::State,
fetch: &mut Self::Fetch<'_>, fetch: &mut Self::Fetch<'_>,
entity: Entity, entity: Entity,
table_row: TableRow, table_row: TableRow,
@ -272,7 +277,7 @@ unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
fetch.inner.as_mut().is_some_and(|inner| { fetch.inner.as_mut().is_some_and(|inner| {
// SAFETY: We delegate to the inner `fetch` for `A` // SAFETY: We delegate to the inner `fetch` for `A`
unsafe { unsafe {
let handle = <&A>::fetch(inner, entity, table_row); let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
fetch.check.has_changed(handle) fetch.check.has_changed(handle)
} }
}) })

View File

@ -437,6 +437,18 @@ impl<A: Asset> Assets<A> {
result result
} }
/// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists.
///
/// This is the same as [`Assets::get_mut`] except it doesn't emit [`AssetEvent::Modified`].
#[inline]
pub fn get_mut_untracked(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A> {
let id: AssetId<A> = id.into();
match id {
AssetId::Index { index, .. } => self.dense_storage.get_mut(index),
AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid),
}
}
/// Removes (and returns) the [`Asset`] with the given `id`, if it exists. /// Removes (and returns) the [`Asset`] with the given `id`, if it exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`]. /// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
pub fn remove(&mut self, id: impl Into<AssetId<A>>) -> Option<A> { pub fn remove(&mut self, id: impl Into<AssetId<A>>) -> Option<A> {
@ -450,6 +462,8 @@ impl<A: Asset> Assets<A> {
/// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`]. /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`].
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`]. /// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
///
/// This is the same as [`Assets::remove`] except it doesn't emit [`AssetEvent::Removed`].
pub fn remove_untracked(&mut self, id: impl Into<AssetId<A>>) -> Option<A> { pub fn remove_untracked(&mut self, id: impl Into<AssetId<A>>) -> Option<A> {
let id: AssetId<A> = id.into(); let id: AssetId<A> = id.into();
self.duplicate_handles.remove(&id); self.duplicate_handles.remove(&id);

View File

@ -20,6 +20,7 @@ pub trait DirectAssetAccessExt {
settings: impl Fn(&mut S) + Send + Sync + 'static, settings: impl Fn(&mut S) + Send + Sync + 'static,
) -> Handle<A>; ) -> Handle<A>;
} }
impl DirectAssetAccessExt for World { impl DirectAssetAccessExt for World {
/// Insert an asset similarly to [`Assets::add`]. /// Insert an asset similarly to [`Assets::add`].
/// ///

View File

@ -1,12 +1,12 @@
use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId};
use bevy_ecs::event::Event; use bevy_ecs::event::{BufferedEvent, Event};
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
use core::fmt::Debug; use core::fmt::Debug;
/// An event emitted when a specific [`Asset`] fails to load. /// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load.
/// ///
/// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. /// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`].
#[derive(Event, Clone, Debug)] #[derive(Event, BufferedEvent, Clone, Debug)]
pub struct AssetLoadFailedEvent<A: Asset> { pub struct AssetLoadFailedEvent<A: Asset> {
/// The stable identifier of the asset that failed to load. /// The stable identifier of the asset that failed to load.
pub id: AssetId<A>, pub id: AssetId<A>,
@ -24,7 +24,7 @@ impl<A: Asset> AssetLoadFailedEvent<A> {
} }
/// An untyped version of [`AssetLoadFailedEvent`]. /// An untyped version of [`AssetLoadFailedEvent`].
#[derive(Event, Clone, Debug)] #[derive(Event, BufferedEvent, Clone, Debug)]
pub struct UntypedAssetLoadFailedEvent { pub struct UntypedAssetLoadFailedEvent {
/// The stable identifier of the asset that failed to load. /// The stable identifier of the asset that failed to load.
pub id: UntypedAssetId, pub id: UntypedAssetId,
@ -44,9 +44,9 @@ impl<A: Asset> From<&AssetLoadFailedEvent<A>> for UntypedAssetLoadFailedEvent {
} }
} }
/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. /// [`BufferedEvent`]s that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events.
#[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")] #[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")]
#[derive(Event, Reflect)] #[derive(Event, BufferedEvent, Reflect)]
pub enum AssetEvent<A: Asset> { pub enum AssetEvent<A: Asset> {
/// Emitted whenever an [`Asset`] is added. /// Emitted whenever an [`Asset`] is added.
Added { id: AssetId<A> }, Added { id: AssetId<A> },

View File

@ -56,6 +56,7 @@ pub(crate) struct EmbeddedEventHandler {
dir: Dir, dir: Dir,
last_event: Option<AssetSourceEvent>, last_event: Option<AssetSourceEvent>,
} }
impl FilesystemEventHandler for EmbeddedEventHandler { impl FilesystemEventHandler for EmbeddedEventHandler {
fn begin(&mut self) { fn begin(&mut self) {
self.last_event = None; self.last_event = None;

View File

@ -141,16 +141,19 @@ impl EmbeddedAssetRegistry {
pub trait GetAssetServer { pub trait GetAssetServer {
fn get_asset_server(&self) -> &AssetServer; fn get_asset_server(&self) -> &AssetServer;
} }
impl GetAssetServer for App { impl GetAssetServer for App {
fn get_asset_server(&self) -> &AssetServer { fn get_asset_server(&self) -> &AssetServer {
self.world().get_asset_server() self.world().get_asset_server()
} }
} }
impl GetAssetServer for World { impl GetAssetServer for World {
fn get_asset_server(&self) -> &AssetServer { fn get_asset_server(&self) -> &AssetServer {
self.resource() self.resource()
} }
} }
impl GetAssetServer for AssetServer { impl GetAssetServer for AssetServer {
fn get_asset_server(&self) -> &AssetServer { fn get_asset_server(&self) -> &AssetServer {
self self

View File

@ -82,7 +82,10 @@ impl HttpWasmAssetReader {
let reader = VecReader::new(bytes); let reader = VecReader::new(bytes);
Ok(reader) Ok(reader)
} }
404 => Err(AssetReaderError::NotFound(path)), // Some web servers, including itch.io's CDN, return 403 when a requested file isn't present.
// TODO: remove handling of 403 as not found when it's easier to configure
// see https://github.com/bevyengine/bevy/pull/19268#pullrequestreview-2882410105
403 | 404 => Err(AssetReaderError::NotFound(path)),
status => Err(AssetReaderError::HttpError(status)), status => Err(AssetReaderError::HttpError(status)),
} }
} }

View File

@ -141,8 +141,8 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
#![no_std] #![no_std]
@ -267,7 +267,7 @@ pub struct AssetPlugin {
/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid. /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
/// ///
/// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your /// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your
/// app will include scripts or modding support, as it could allow allow arbitrary file /// app will include scripts or modding support, as it could allow arbitrary file
/// access for malicious code. /// access for malicious code.
/// ///
/// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved) /// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved)
@ -275,10 +275,10 @@ pub struct AssetPlugin {
pub enum UnapprovedPathMode { pub enum UnapprovedPathMode {
/// Unapproved asset loading is allowed. This is strongly discouraged. /// Unapproved asset loading is allowed. This is strongly discouraged.
Allow, Allow,
/// Fails to load any asset that is is unapproved, unless an override method is used, like /// Fails to load any asset that is unapproved, unless an override method is used, like
/// [`AssetServer::load_override`]. /// [`AssetServer::load_override`].
Deny, Deny,
/// Fails to load any asset that is is unapproved. /// Fails to load any asset that is unapproved.
#[default] #[default]
Forbid, Forbid,
} }

View File

@ -12,7 +12,7 @@ use alloc::{
vec::Vec, vec::Vec,
}; };
use atomicow::CowArc; use atomicow::CowArc;
use bevy_ecs::world::World; use bevy_ecs::{error::BevyError, world::World};
use bevy_platform::collections::{HashMap, HashSet}; use bevy_platform::collections::{HashMap, HashSet};
use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
use core::any::{Any, TypeId}; use core::any::{Any, TypeId};
@ -34,7 +34,7 @@ pub trait AssetLoader: Send + Sync + 'static {
/// The settings type used by this [`AssetLoader`]. /// The settings type used by this [`AssetLoader`].
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
/// The type of [error](`std::error::Error`) which could be encountered by this loader. /// The type of [error](`std::error::Error`) which could be encountered by this loader.
type Error: Into<Box<dyn core::error::Error + Send + Sync + 'static>>; type Error: Into<BevyError>;
/// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`]. /// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
fn load( fn load(
&self, &self,
@ -58,10 +58,7 @@ pub trait ErasedAssetLoader: Send + Sync + 'static {
reader: &'a mut dyn Reader, reader: &'a mut dyn Reader,
meta: &'a dyn AssetMetaDyn, meta: &'a dyn AssetMetaDyn,
load_context: LoadContext<'a>, load_context: LoadContext<'a>,
) -> BoxedFuture< ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>>;
'a,
Result<ErasedLoadedAsset, Box<dyn core::error::Error + Send + Sync + 'static>>,
>;
/// Returns a list of extensions supported by this asset loader, without the preceding dot. /// Returns a list of extensions supported by this asset loader, without the preceding dot.
fn extensions(&self) -> &[&str]; fn extensions(&self) -> &[&str];
@ -89,10 +86,7 @@ where
reader: &'a mut dyn Reader, reader: &'a mut dyn Reader,
meta: &'a dyn AssetMetaDyn, meta: &'a dyn AssetMetaDyn,
mut load_context: LoadContext<'a>, mut load_context: LoadContext<'a>,
) -> BoxedFuture< ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>> {
'a,
Result<ErasedLoadedAsset, Box<dyn core::error::Error + Send + Sync + 'static>>,
> {
Box::pin(async move { Box::pin(async move {
let settings = meta let settings = meta
.loader_settings() .loader_settings()
@ -350,7 +344,7 @@ impl<'a> LoadContext<'a> {
/// Begins a new labeled asset load. Use the returned [`LoadContext`] to load /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
/// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load. /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
/// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent
/// context. /// context.
/// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
/// the labeled [`LoadContext`] back to the parent context. /// the labeled [`LoadContext`] back to the parent context.
@ -366,7 +360,7 @@ impl<'a> LoadContext<'a> {
/// # let load_context: LoadContext = panic!(); /// # let load_context: LoadContext = panic!();
/// let mut handles = Vec::new(); /// let mut handles = Vec::new();
/// for i in 0..2 { /// for i in 0..2 {
/// let mut labeled = load_context.begin_labeled_asset(); /// let labeled = load_context.begin_labeled_asset();
/// handles.push(std::thread::spawn(move || { /// handles.push(std::thread::spawn(move || {
/// (i.to_string(), labeled.finish(Image::default())) /// (i.to_string(), labeled.finish(Image::default()))
/// })); /// }));
@ -391,18 +385,18 @@ impl<'a> LoadContext<'a> {
/// [`LoadedAsset`], which is registered under the `label` label. /// [`LoadedAsset`], which is registered under the `label` label.
/// ///
/// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
/// result with [`LoadContext::add_labeled_asset`]. /// result with [`LoadContext::add_loaded_labeled_asset`].
/// ///
/// See [`AssetPath`] for more on labeled assets. /// See [`AssetPath`] for more on labeled assets.
pub fn labeled_asset_scope<A: Asset>( pub fn labeled_asset_scope<A: Asset, E>(
&mut self, &mut self,
label: String, label: String,
load: impl FnOnce(&mut LoadContext) -> A, load: impl FnOnce(&mut LoadContext) -> Result<A, E>,
) -> Handle<A> { ) -> Result<Handle<A>, E> {
let mut context = self.begin_labeled_asset(); let mut context = self.begin_labeled_asset();
let asset = load(&mut context); let asset = load(&mut context)?;
let loaded_asset = context.finish(asset); let loaded_asset = context.finish(asset);
self.add_loaded_labeled_asset(label, loaded_asset) Ok(self.add_loaded_labeled_asset(label, loaded_asset))
} }
/// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label. /// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
@ -416,7 +410,8 @@ impl<'a> LoadContext<'a> {
/// ///
/// See [`AssetPath`] for more on labeled assets. /// See [`AssetPath`] for more on labeled assets.
pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> { pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> {
self.labeled_asset_scope(label, |_| asset) self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset))
.expect("the closure returns Ok")
} }
/// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context. /// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.

View File

@ -490,7 +490,7 @@ impl<'a> AssetPath<'a> {
} }
/// Returns `true` if this [`AssetPath`] points to a file that is /// Returns `true` if this [`AssetPath`] points to a file that is
/// outside of it's [`AssetSource`](crate::io::AssetSource) folder. /// outside of its [`AssetSource`](crate::io::AssetSource) folder.
/// ///
/// ## Example /// ## Example
/// ``` /// ```

View File

@ -18,16 +18,16 @@ pub struct ReflectAsset {
handle_type_id: TypeId, handle_type_id: TypeId,
assets_resource_type_id: TypeId, assets_resource_type_id: TypeId,
get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>, get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>,
// SAFETY: // SAFETY:
// - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets<T>` resource mutably // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets<T>` resource mutably
// - may only be used to access **at most one** access at once // - may only be used to access **at most one** access at once
get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>, get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>,
add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle,
insert: fn(&mut World, UntypedHandle, &dyn PartialReflect), insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect),
len: fn(&World) -> usize, len: fn(&World) -> usize,
ids: for<'w> fn(&'w World) -> Box<dyn Iterator<Item = UntypedAssetId> + 'w>, ids: for<'w> fn(&'w World) -> Box<dyn Iterator<Item = UntypedAssetId> + 'w>,
remove: fn(&mut World, UntypedHandle) -> Option<Box<dyn Reflect>>, remove: fn(&mut World, UntypedAssetId) -> Option<Box<dyn Reflect>>,
} }
impl ReflectAsset { impl ReflectAsset {
@ -42,15 +42,19 @@ impl ReflectAsset {
} }
/// Equivalent of [`Assets::get`] /// Equivalent of [`Assets::get`]
pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> { pub fn get<'w>(
(self.get)(world, handle) &self,
world: &'w World,
asset_id: impl Into<UntypedAssetId>,
) -> Option<&'w dyn Reflect> {
(self.get)(world, asset_id.into())
} }
/// Equivalent of [`Assets::get_mut`] /// Equivalent of [`Assets::get_mut`]
pub fn get_mut<'w>( pub fn get_mut<'w>(
&self, &self,
world: &'w mut World, world: &'w mut World,
handle: UntypedHandle, asset_id: impl Into<UntypedAssetId>,
) -> Option<&'w mut dyn Reflect> { ) -> Option<&'w mut dyn Reflect> {
// SAFETY: unique world access // SAFETY: unique world access
#[expect( #[expect(
@ -58,7 +62,7 @@ impl ReflectAsset {
reason = "Use of unsafe `Self::get_unchecked_mut()` function." reason = "Use of unsafe `Self::get_unchecked_mut()` function."
)] )]
unsafe { unsafe {
(self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle) (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into())
} }
} }
@ -76,8 +80,8 @@ impl ReflectAsset {
/// # let handle_1: UntypedHandle = unimplemented!(); /// # let handle_1: UntypedHandle = unimplemented!();
/// # let handle_2: UntypedHandle = unimplemented!(); /// # let handle_2: UntypedHandle = unimplemented!();
/// let unsafe_world_cell = world.as_unsafe_world_cell(); /// let unsafe_world_cell = world.as_unsafe_world_cell();
/// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() }; /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_1).unwrap() };
/// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() }; /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_2).unwrap() };
/// // ^ not allowed, two mutable references through the same asset resource, even though the /// // ^ not allowed, two mutable references through the same asset resource, even though the
/// // handles are distinct /// // handles are distinct
/// ///
@ -96,10 +100,10 @@ impl ReflectAsset {
pub unsafe fn get_unchecked_mut<'w>( pub unsafe fn get_unchecked_mut<'w>(
&self, &self,
world: UnsafeWorldCell<'w>, world: UnsafeWorldCell<'w>,
handle: UntypedHandle, asset_id: impl Into<UntypedAssetId>,
) -> Option<&'w mut dyn Reflect> { ) -> Option<&'w mut dyn Reflect> {
// SAFETY: requirements are deferred to the caller // SAFETY: requirements are deferred to the caller
unsafe { (self.get_unchecked_mut)(world, handle) } unsafe { (self.get_unchecked_mut)(world, asset_id.into()) }
} }
/// Equivalent of [`Assets::add`] /// Equivalent of [`Assets::add`]
@ -107,13 +111,22 @@ impl ReflectAsset {
(self.add)(world, value) (self.add)(world, value)
} }
/// Equivalent of [`Assets::insert`] /// Equivalent of [`Assets::insert`]
pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) { pub fn insert(
(self.insert)(world, handle, value); &self,
world: &mut World,
asset_id: impl Into<UntypedAssetId>,
value: &dyn PartialReflect,
) {
(self.insert)(world, asset_id.into(), value);
} }
/// Equivalent of [`Assets::remove`] /// Equivalent of [`Assets::remove`]
pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option<Box<dyn Reflect>> { pub fn remove(
(self.remove)(world, handle) &self,
world: &mut World,
asset_id: impl Into<UntypedAssetId>,
) -> Option<Box<dyn Reflect>> {
(self.remove)(world, asset_id.into())
} }
/// Equivalent of [`Assets::len`] /// Equivalent of [`Assets::len`]
@ -137,17 +150,17 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
ReflectAsset { ReflectAsset {
handle_type_id: TypeId::of::<Handle<A>>(), handle_type_id: TypeId::of::<Handle<A>>(),
assets_resource_type_id: TypeId::of::<Assets<A>>(), assets_resource_type_id: TypeId::of::<Assets<A>>(),
get: |world, handle| { get: |world, asset_id| {
let assets = world.resource::<Assets<A>>(); let assets = world.resource::<Assets<A>>();
let asset = assets.get(&handle.typed_debug_checked()); let asset = assets.get(asset_id.typed_debug_checked());
asset.map(|asset| asset as &dyn Reflect) asset.map(|asset| asset as &dyn Reflect)
}, },
get_unchecked_mut: |world, handle| { get_unchecked_mut: |world, asset_id| {
// SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets<A>`, // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets<A>`,
// and must ensure to only have at most one reference to it live at all times. // and must ensure to only have at most one reference to it live at all times.
#[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")]
let assets = unsafe { world.get_resource_mut::<Assets<A>>().unwrap().into_inner() }; let assets = unsafe { world.get_resource_mut::<Assets<A>>().unwrap().into_inner() };
let asset = assets.get_mut(&handle.typed_debug_checked()); let asset = assets.get_mut(asset_id.typed_debug_checked());
asset.map(|asset| asset as &mut dyn Reflect) asset.map(|asset| asset as &mut dyn Reflect)
}, },
add: |world, value| { add: |world, value| {
@ -156,11 +169,11 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`"); .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`");
assets.add(value).untyped() assets.add(value).untyped()
}, },
insert: |world, handle, value| { insert: |world, asset_id, value| {
let mut assets = world.resource_mut::<Assets<A>>(); let mut assets = world.resource_mut::<Assets<A>>();
let value: A = FromReflect::from_reflect(value) let value: A = FromReflect::from_reflect(value)
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`");
assets.insert(&handle.typed_debug_checked(), value); assets.insert(asset_id.typed_debug_checked(), value);
}, },
len: |world| { len: |world| {
let assets = world.resource::<Assets<A>>(); let assets = world.resource::<Assets<A>>();
@ -170,9 +183,9 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
let assets = world.resource::<Assets<A>>(); let assets = world.resource::<Assets<A>>();
Box::new(assets.ids().map(AssetId::untyped)) Box::new(assets.ids().map(AssetId::untyped))
}, },
remove: |world, handle| { remove: |world, asset_id| {
let mut assets = world.resource_mut::<Assets<A>>(); let mut assets = world.resource_mut::<Assets<A>>();
let value = assets.remove(&handle.typed_debug_checked()); let value = assets.remove(asset_id.typed_debug_checked());
value.map(|value| Box::new(value) as Box<dyn Reflect>) value.map(|value| Box::new(value) as Box<dyn Reflect>)
}, },
} }
@ -200,7 +213,7 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
/// let reflect_asset = type_registry.get_type_data::<ReflectAsset>(reflect_handle.asset_type_id()).unwrap(); /// let reflect_asset = type_registry.get_type_data::<ReflectAsset>(reflect_handle.asset_type_id()).unwrap();
/// ///
/// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap(); /// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap();
/// let value = reflect_asset.get(world, handle).unwrap(); /// let value = reflect_asset.get(world, &handle).unwrap();
/// println!("{value:?}"); /// println!("{value:?}");
/// } /// }
/// ``` /// ```
@ -210,6 +223,7 @@ pub struct ReflectHandle {
downcast_handle_untyped: fn(&dyn Any) -> Option<UntypedHandle>, downcast_handle_untyped: fn(&dyn Any) -> Option<UntypedHandle>,
typed: fn(UntypedHandle) -> Box<dyn Reflect>, typed: fn(UntypedHandle) -> Box<dyn Reflect>,
} }
impl ReflectHandle { impl ReflectHandle {
/// The [`TypeId`] of the asset /// The [`TypeId`] of the asset
pub fn asset_type_id(&self) -> TypeId { pub fn asset_type_id(&self) -> TypeId {
@ -247,7 +261,7 @@ mod tests {
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
use core::any::TypeId; use core::any::TypeId;
use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle}; use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset};
use bevy_app::App; use bevy_app::App;
use bevy_ecs::reflect::AppTypeRegistry; use bevy_ecs::reflect::AppTypeRegistry;
use bevy_reflect::Reflect; use bevy_reflect::Reflect;
@ -281,7 +295,7 @@ mod tests {
let handle = reflect_asset.add(app.world_mut(), &value); let handle = reflect_asset.add(app.world_mut(), &value);
// struct is a reserved keyword, so we can't use it here // struct is a reserved keyword, so we can't use it here
let strukt = reflect_asset let strukt = reflect_asset
.get_mut(app.world_mut(), handle) .get_mut(app.world_mut(), &handle)
.unwrap() .unwrap()
.reflect_mut() .reflect_mut()
.as_struct() .as_struct()
@ -294,16 +308,12 @@ mod tests {
assert_eq!(reflect_asset.len(app.world()), 1); assert_eq!(reflect_asset.len(app.world()), 1);
let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); let ids: Vec<_> = reflect_asset.ids(app.world()).collect();
assert_eq!(ids.len(), 1); assert_eq!(ids.len(), 1);
let id = ids[0];
let fetched_handle = UntypedHandle::Weak(ids[0]); let asset = reflect_asset.get(app.world(), id).unwrap();
let asset = reflect_asset
.get(app.world(), fetched_handle.clone_weak())
.unwrap();
assert_eq!(asset.downcast_ref::<AssetType>().unwrap().field, "edited"); assert_eq!(asset.downcast_ref::<AssetType>().unwrap().field, "edited");
reflect_asset reflect_asset.remove(app.world_mut(), id).unwrap();
.remove(app.world_mut(), fetched_handle)
.unwrap();
assert_eq!(reflect_asset.len(app.world()), 0); assert_eq!(reflect_asset.len(app.world()), 0);
} }
} }

View File

@ -1931,7 +1931,7 @@ pub enum AssetLoadError {
base_path, base_path,
label, label,
all_labels.len(), all_labels.len(),
all_labels.iter().map(|l| format!("'{}'", l)).collect::<Vec<_>>().join(", "))] all_labels.iter().map(|l| format!("'{l}'")).collect::<Vec<_>>().join(", "))]
MissingLabel { MissingLabel {
base_path: AssetPath<'static>, base_path: AssetPath<'static>,
label: String, label: String,
@ -1945,7 +1945,7 @@ pub enum AssetLoadError {
pub struct AssetLoaderError { pub struct AssetLoaderError {
path: AssetPath<'static>, path: AssetPath<'static>,
loader_name: &'static str, loader_name: &'static str,
error: Arc<dyn core::error::Error + Send + Sync + 'static>, error: Arc<BevyError>,
} }
impl AssetLoaderError { impl AssetLoaderError {
@ -1953,6 +1953,14 @@ impl AssetLoaderError {
pub fn path(&self) -> &AssetPath<'static> { pub fn path(&self) -> &AssetPath<'static> {
&self.path &self.path
} }
/// The error the loader reported when attempting to load the asset.
///
/// If you know the type of the error the asset loader returned, you can use
/// [`BevyError::downcast_ref()`] to get it.
pub fn error(&self) -> &BevyError {
&self.error
}
} }
/// An error that occurs while resolving an asset added by `add_async`. /// An error that occurs while resolving an asset added by `add_async`.

View File

@ -1,39 +1,45 @@
[package] [package]
name = "bevy_audio" name = "bevy_audio"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Provides audio functionality for Bevy Engine" description = "Provides audio functionality for Bevy Engine"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy"] keywords = ["bevy"]
[dependencies] [dependencies]
# bevy # bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
# other # other
# TODO: Remove `coreaudio-sys` dep below when updating `cpal`.
rodio = { version = "0.20", default-features = false } rodio = { version = "0.20", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
cpal = { version = "0.15", optional = true } cpal = { version = "0.15", optional = true }
[target.'cfg(target_vendor = "apple")'.dependencies]
# NOTE: Explicitly depend on this patch version to fix:
# https://github.com/bevyengine/bevy/issues/18893
coreaudio-sys = { version = "0.2.17", default-features = false }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
rodio = { version = "0.20", default-features = false, features = [ rodio = { version = "0.20", default-features = false, features = [
"wasm-bindgen", "wasm-bindgen",
] } ] }
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false, features = [
"web", "web",
] } ] }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, features = [ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [
"web", "web",
] } ] }

View File

@ -57,6 +57,16 @@ pub struct PlaybackSettings {
/// Optional scale factor applied to the positions of this audio source and the listener, /// Optional scale factor applied to the positions of this audio source and the listener,
/// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale). /// overriding the default value configured on [`AudioPlugin::default_spatial_scale`](crate::AudioPlugin::default_spatial_scale).
pub spatial_scale: Option<SpatialScale>, pub spatial_scale: Option<SpatialScale>,
/// The point in time in the audio clip where playback should start. If set to `None`, it will
/// play from the beginning of the clip.
///
/// If the playback mode is set to `Loop`, each loop will start from this position.
pub start_position: Option<core::time::Duration>,
/// How long the audio should play before stopping. If set, the clip will play for at most
/// the specified duration. If set to `None`, it will play for as long as it can.
///
/// If the playback mode is set to `Loop`, each loop will last for this duration.
pub duration: Option<core::time::Duration>,
} }
impl Default for PlaybackSettings { impl Default for PlaybackSettings {
@ -81,6 +91,8 @@ impl PlaybackSettings {
muted: false, muted: false,
spatial: false, spatial: false,
spatial_scale: None, spatial_scale: None,
start_position: None,
duration: None,
}; };
/// Will play the associated audio source in a loop. /// Will play the associated audio source in a loop.
@ -136,6 +148,18 @@ impl PlaybackSettings {
self.spatial_scale = Some(spatial_scale); self.spatial_scale = Some(spatial_scale);
self self
} }
/// Helper to use a custom playback start position.
pub const fn with_start_position(mut self, start_position: core::time::Duration) -> Self {
self.start_position = Some(start_position);
self
}
/// Helper to use a custom playback duration.
pub const fn with_duration(mut self, duration: core::time::Duration) -> Self {
self.duration = Some(duration);
self
}
} }
/// Settings for the listener for spatial audio sources. /// Settings for the listener for spatial audio sources.

View File

@ -57,6 +57,7 @@ pub struct PlaybackRemoveMarker;
pub(crate) struct EarPositions<'w, 's> { pub(crate) struct EarPositions<'w, 's> {
pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>, pub(crate) query: Query<'w, 's, (Entity, &'static GlobalTransform, &'static SpatialListener)>,
} }
impl<'w, 's> EarPositions<'w, 's> { impl<'w, 's> EarPositions<'w, 's> {
/// Gets a set of transformed ear positions. /// Gets a set of transformed ear positions.
/// ///
@ -156,12 +157,49 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
} }
}; };
let decoder = audio_source.decoder();
match settings.mode { match settings.mode {
PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), PlaybackMode::Loop => match (settings.start_position, settings.duration) {
// custom start position and duration
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration)
.repeat_infinite(),
),
// custom start position
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position).repeat_infinite());
}
// custom duration
(None, Some(duration)) => {
sink.append(decoder.take_duration(duration).repeat_infinite());
}
// full clip
(None, None) => sink.append(decoder.repeat_infinite()),
},
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
sink.append(audio_source.decoder()); match (settings.start_position, settings.duration) {
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration),
),
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position));
}
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
(None, None) => sink.append(decoder),
}
} }
}; }
let mut sink = SpatialAudioSink::new(sink); let mut sink = SpatialAudioSink::new(sink);
@ -196,12 +234,49 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
} }
}; };
let decoder = audio_source.decoder();
match settings.mode { match settings.mode {
PlaybackMode::Loop => sink.append(audio_source.decoder().repeat_infinite()), PlaybackMode::Loop => match (settings.start_position, settings.duration) {
// custom start position and duration
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration)
.repeat_infinite(),
),
// custom start position
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position).repeat_infinite());
}
// custom duration
(None, Some(duration)) => {
sink.append(decoder.take_duration(duration).repeat_infinite());
}
// full clip
(None, None) => sink.append(decoder.repeat_infinite()),
},
PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => { PlaybackMode::Once | PlaybackMode::Despawn | PlaybackMode::Remove => {
sink.append(audio_source.decoder()); match (settings.start_position, settings.duration) {
(Some(start_position), Some(duration)) => sink.append(
decoder
.skip_duration(start_position)
.take_duration(duration),
),
(Some(start_position), None) => {
sink.append(decoder.skip_duration(start_position));
}
(None, Some(duration)) => sink.append(decoder.take_duration(duration)),
(None, None) => sink.append(decoder),
}
} }
}; }
let mut sink = AudioSink::new(sink); let mut sink = AudioSink::new(sink);

View File

@ -1,8 +1,8 @@
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
//! Audio support for the game engine Bevy //! Audio support for the game engine Bevy

View File

@ -42,6 +42,14 @@ pub trait AudioSinkPlayback {
/// No effect if not paused. /// No effect if not paused.
fn play(&self); fn play(&self);
/// Returns the position of the sound that's being played.
///
/// This takes into account any speedup or delay applied.
///
/// Example: if you [`set_speed(2.0)`](Self::set_speed) and [`position()`](Self::position) returns *5s*,
/// then the position in the recording is *10s* from its start.
fn position(&self) -> Duration;
/// Attempts to seek to a given position in the current source. /// Attempts to seek to a given position in the current source.
/// ///
/// This blocks between 0 and ~5 milliseconds. /// This blocks between 0 and ~5 milliseconds.
@ -181,6 +189,10 @@ impl AudioSinkPlayback for AudioSink {
self.sink.play(); self.sink.play();
} }
fn position(&self) -> Duration {
self.sink.get_pos()
}
fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { fn try_seek(&self, pos: Duration) -> Result<(), SeekError> {
self.sink.try_seek(pos) self.sink.try_seek(pos)
} }
@ -281,6 +293,10 @@ impl AudioSinkPlayback for SpatialAudioSink {
self.sink.play(); self.sink.play();
} }
fn position(&self) -> Duration {
self.sink.get_pos()
}
fn try_seek(&self, pos: Duration) -> Result<(), SeekError> { fn try_seek(&self, pos: Duration) -> Result<(), SeekError> {
self.sink.try_seek(pos) self.sink.try_seek(pos)
} }

View File

@ -34,7 +34,7 @@ impl GlobalVolume {
#[derive(Clone, Copy, Debug, Reflect)] #[derive(Clone, Copy, Debug, Reflect)]
#[reflect(Clone, Debug, PartialEq)] #[reflect(Clone, Debug, PartialEq)]
pub enum Volume { pub enum Volume {
/// Create a new [`Volume`] from the given volume in linear scale. /// Create a new [`Volume`] from the given volume in the linear scale.
/// ///
/// In a linear scale, the value `1.0` represents the "normal" volume, /// In a linear scale, the value `1.0` represents the "normal" volume,
/// meaning the audio is played at its original level. Values greater than /// meaning the audio is played at its original level. Values greater than
@ -144,7 +144,7 @@ impl Volume {
/// Returns the volume in decibels as a float. /// Returns the volume in decibels as a float.
/// ///
/// If the volume is silent / off / muted, i.e. it's underlying linear scale /// If the volume is silent / off / muted, i.e., its underlying linear scale
/// is `0.0`, this method returns negative infinity. /// is `0.0`, this method returns negative infinity.
pub fn to_decibels(&self) -> f32 { pub fn to_decibels(&self) -> f32 {
match self { match self {
@ -155,57 +155,95 @@ impl Volume {
/// The silent volume. Also known as "off" or "muted". /// The silent volume. Also known as "off" or "muted".
pub const SILENT: Self = Volume::Linear(0.0); pub const SILENT: Self = Volume::Linear(0.0);
}
impl core::ops::Add<Self> for Volume { /// Increases the volume by the specified percentage.
type Output = Self; ///
/// This method works in the linear domain, where a 100% increase
fn add(self, rhs: Self) -> Self { /// means doubling the volume (equivalent to +6.02dB).
use Volume::{Decibels, Linear}; ///
/// # Arguments
match (self, rhs) { /// * `percentage` - The percentage to increase (50.0 means 50% increase)
(Linear(a), Linear(b)) => Linear(a + b), ///
(Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( /// # Examples
decibels_to_linear(a) + decibels_to_linear(b), /// ```
)), /// use bevy_audio::Volume;
// {Linear, Decibels} favors the left hand side of the operation by ///
// first converting the right hand side to the same type as the left /// let volume = Volume::Linear(1.0);
// hand side and then performing the operation. /// let increased = volume.increase_by_percentage(100.0);
(Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)), /// assert_eq!(increased.to_linear(), 2.0);
(Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)), /// ```
} pub fn increase_by_percentage(&self, percentage: f32) -> Self {
let factor = 1.0 + (percentage / 100.0);
Volume::Linear(self.to_linear() * factor)
} }
}
impl core::ops::AddAssign<Self> for Volume { /// Decreases the volume by the specified percentage.
fn add_assign(&mut self, rhs: Self) { ///
*self = *self + rhs; /// This method works in the linear domain, where a 50% decrease
/// means halving the volume (equivalent to -6.02dB).
///
/// # Arguments
/// * `percentage` - The percentage to decrease (50.0 means 50% decrease)
///
/// # Examples
/// ```
/// use bevy_audio::Volume;
///
/// let volume = Volume::Linear(1.0);
/// let decreased = volume.decrease_by_percentage(50.0);
/// assert_eq!(decreased.to_linear(), 0.5);
/// ```
pub fn decrease_by_percentage(&self, percentage: f32) -> Self {
let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0);
Volume::Linear(self.to_linear() * factor)
} }
}
impl core::ops::Sub<Self> for Volume { /// Scales the volume to a specific linear factor relative to the current volume.
type Output = Self; ///
/// This is different from `adjust_by_linear` as it sets the volume to be
fn sub(self, rhs: Self) -> Self { /// exactly the factor times the original volume, rather than applying
use Volume::{Decibels, Linear}; /// the factor to the current volume.
///
match (self, rhs) { /// # Arguments
(Linear(a), Linear(b)) => Linear(a - b), /// * `factor` - The scaling factor (2.0 = twice as loud, 0.5 = half as loud)
(Decibels(a), Decibels(b)) => Decibels(linear_to_decibels( ///
decibels_to_linear(a) - decibels_to_linear(b), /// # Examples
)), /// ```
// {Linear, Decibels} favors the left hand side of the operation by /// use bevy_audio::Volume;
// first converting the right hand side to the same type as the left ///
// hand side and then performing the operation. /// let volume = Volume::Linear(0.8);
(Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)), /// let scaled = volume.scale_to_factor(1.25);
(Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)), /// assert_eq!(scaled.to_linear(), 1.0);
} /// ```
pub fn scale_to_factor(&self, factor: f32) -> Self {
Volume::Linear(self.to_linear() * factor)
} }
}
impl core::ops::SubAssign<Self> for Volume { /// Creates a fade effect by interpolating between current volume and target volume.
fn sub_assign(&mut self, rhs: Self) { ///
*self = *self - rhs; /// This method performs linear interpolation in the linear domain, which
/// provides a more natural-sounding fade effect.
///
/// # Arguments
/// * `target` - The target volume to fade towards
/// * `factor` - The interpolation factor (0.0 = current volume, 1.0 = target volume)
///
/// # Examples
/// ```
/// use bevy_audio::Volume;
///
/// let current = Volume::Linear(1.0);
/// let target = Volume::Linear(0.0);
/// let faded = current.fade_towards(target, 0.5);
/// assert_eq!(faded.to_linear(), 0.5);
/// ```
pub fn fade_towards(&self, target: Volume, factor: f32) -> Self {
let current_linear = self.to_linear();
let target_linear = target.to_linear();
let factor_clamped = factor.clamp(0.0, 1.0);
let interpolated = current_linear + (target_linear - current_linear) * factor_clamped;
Volume::Linear(interpolated)
} }
} }
@ -306,17 +344,11 @@ mod tests {
assert!( assert!(
db_delta.abs() < 1e-2, db_delta.abs() < 1e-2,
"Expected ~{}dB, got {}dB (delta {})", "Expected ~{db}dB, got {db_test}dB (delta {db_delta})",
db,
db_test,
db_delta
); );
assert!( assert!(
linear_relative_delta.abs() < 1e-3, linear_relative_delta.abs() < 1e-3,
"Expected ~{}, got {} (relative delta {})", "Expected ~{linear}, got {linear_test} (relative delta {linear_relative_delta})",
linear,
linear_test,
linear_relative_delta
); );
} }
} }
@ -337,8 +369,9 @@ mod tests {
Linear(f32::NEG_INFINITY).to_decibels().is_infinite(), Linear(f32::NEG_INFINITY).to_decibels().is_infinite(),
"Negative infinite linear scale is equivalent to infinite decibels" "Negative infinite linear scale is equivalent to infinite decibels"
); );
assert!( assert_eq!(
Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0, Decibels(f32::NEG_INFINITY).to_linear().abs(),
0.0,
"Negative infinity decibels is equivalent to zero linear scale" "Negative infinity decibels is equivalent to zero linear scale"
); );
@ -361,71 +394,89 @@ mod tests {
); );
} }
#[test]
fn test_increase_by_percentage() {
let volume = Linear(1.0);
// 100% increase should double the volume
let increased = volume.increase_by_percentage(100.0);
assert_eq!(increased.to_linear(), 2.0);
// 50% increase
let increased = volume.increase_by_percentage(50.0);
assert_eq!(increased.to_linear(), 1.5);
}
#[test]
fn test_decrease_by_percentage() {
let volume = Linear(1.0);
// 50% decrease should halve the volume
let decreased = volume.decrease_by_percentage(50.0);
assert_eq!(decreased.to_linear(), 0.5);
// 25% decrease
let decreased = volume.decrease_by_percentage(25.0);
assert_eq!(decreased.to_linear(), 0.75);
// 100% decrease should result in silence
let decreased = volume.decrease_by_percentage(100.0);
assert_eq!(decreased.to_linear(), 0.0);
}
#[test]
fn test_scale_to_factor() {
let volume = Linear(0.8);
let scaled = volume.scale_to_factor(1.25);
assert_eq!(scaled.to_linear(), 1.0);
}
#[test]
fn test_fade_towards() {
let current = Linear(1.0);
let target = Linear(0.0);
// 50% fade should result in 0.5 linear volume
let faded = current.fade_towards(target, 0.5);
assert_eq!(faded.to_linear(), 0.5);
// 0% fade should keep current volume
let faded = current.fade_towards(target, 0.0);
assert_eq!(faded.to_linear(), 1.0);
// 100% fade should reach target volume
let faded = current.fade_towards(target, 1.0);
assert_eq!(faded.to_linear(), 0.0);
}
#[test]
fn test_decibel_math_properties() {
let volume = Linear(1.0);
// Adding 20dB should multiply linear volume by 10
let adjusted = volume * Decibels(20.0);
assert_approx_eq(adjusted, Linear(10.0));
// Subtracting 20dB should divide linear volume by 10
let adjusted = volume / Decibels(20.0);
assert_approx_eq(adjusted, Linear(0.1));
}
fn assert_approx_eq(a: Volume, b: Volume) { fn assert_approx_eq(a: Volume, b: Volume) {
const EPSILON: f32 = 0.0001; const EPSILON: f32 = 0.0001;
match (a, b) { match (a, b) {
(Decibels(a), Decibels(b)) | (Linear(a), Linear(b)) => assert!( (Decibels(a), Decibels(b)) | (Linear(a), Linear(b)) => assert!(
(a - b).abs() < EPSILON, (a - b).abs() < EPSILON,
"Expected {:?} to be approximately equal to {:?}", "Expected {a:?} to be approximately equal to {b:?}",
a,
b
), ),
(a, b) => assert!( (a, b) => assert!(
(a.to_decibels() - b.to_decibels()).abs() < EPSILON, (a.to_decibels() - b.to_decibels()).abs() < EPSILON,
"Expected {:?} to be approximately equal to {:?}", "Expected {a:?} to be approximately equal to {b:?}",
a,
b
), ),
} }
} }
#[test]
fn volume_ops_add() {
// Linear to Linear.
assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0));
assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6));
assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0));
// Decibels to Decibels.
assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003));
assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599));
assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423));
// {Linear, Decibels} favors the left hand side of the operation.
assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5));
assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825));
}
#[test]
fn volume_ops_add_assign() {
// Linear to Linear.
let mut volume = Linear(0.5);
volume += Linear(0.5);
assert_approx_eq(volume, Linear(1.0));
}
#[test]
fn volume_ops_sub() {
// Linear to Linear.
assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0));
assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4));
assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0));
// Decibels to Decibels.
assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY));
assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506));
assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY));
}
#[test]
fn volume_ops_sub_assign() {
// Linear to Linear.
let mut volume = Linear(0.5);
volume -= Linear(0.5);
assert_approx_eq(volume, Linear(0.0));
}
#[test] #[test]
fn volume_ops_mul() { fn volume_ops_mul() {
// Linear to Linear. // Linear to Linear.

View File

@ -1,26 +1,26 @@
[package] [package]
name = "bevy_color" name = "bevy_color"
version = "0.16.0-dev" version = "0.17.0-dev"
edition = "2024" edition = "2024"
description = "Types for representing and manipulating color values" description = "Types for representing and manipulating color values"
homepage = "https://bevyengine.org" homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy" repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["bevy", "color"] keywords = ["bevy", "color"]
rust-version = "1.85.0" rust-version = "1.85.0"
[dependencies] [dependencies]
bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false, features = [ bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false, features = [
"curve", "curve",
] } ] }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", default-features = false, optional = true } bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, optional = true }
bytemuck = { version = "1", features = ["derive"] } bytemuck = { version = "1", features = ["derive"] }
serde = { version = "1.0", features = [ serde = { version = "1.0", features = [
"derive", "derive",
], default-features = false, optional = true } ], default-features = false, optional = true }
thiserror = { version = "2", default-features = false } thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] } derive_more = { version = "2", default-features = false, features = ["from"] }
wgpu-types = { version = "24", default-features = false, optional = true } wgpu-types = { version = "25", default-features = false, optional = true }
encase = { version = "0.10", default-features = false, optional = true } encase = { version = "0.10", default-features = false, optional = true }
[features] [features]

View File

@ -92,7 +92,7 @@ impl Hsla {
/// // Palette with 5 distinct hues /// // Palette with 5 distinct hues
/// let palette = (0..5).map(Hsla::sequential_dispersed).collect::<Vec<_>>(); /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::<Vec<_>>();
/// ``` /// ```
pub fn sequential_dispersed(index: u32) -> Self { pub const fn sequential_dispersed(index: u32) -> Self {
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32; const RATIO_360: f32 = 360.0 / u32::MAX as f32;

View File

@ -96,7 +96,7 @@ impl Lcha {
/// // Palette with 5 distinct hues /// // Palette with 5 distinct hues
/// let palette = (0..5).map(Lcha::sequential_dispersed).collect::<Vec<_>>(); /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::<Vec<_>>();
/// ``` /// ```
pub fn sequential_dispersed(index: u32) -> Self { pub const fn sequential_dispersed(index: u32) -> Self {
const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up
const RATIO_360: f32 = 360.0 / u32::MAX as f32; const RATIO_360: f32 = 360.0 / u32::MAX as f32;

View File

@ -1,8 +1,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)] #![forbid(unsafe_code)]
#![doc( #![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png", html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png" html_favicon_url = "https://bevy.org/assets/icon.png"
)] )]
#![no_std] #![no_std]
@ -262,6 +262,7 @@ macro_rules! impl_componentwise_vector_space {
} }
impl bevy_math::VectorSpace for $ty { impl bevy_math::VectorSpace for $ty {
type Scalar = f32;
const ZERO: Self = Self { const ZERO: Self = Self {
$($element: 0.0,)+ $($element: 0.0,)+
}; };

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