From 76b88290f8b10eb2f409be1c438050c0f8d62e72 Mon Sep 17 00:00:00 2001 From: Dusty DeWeese Date: Sat, 4 Mar 2023 11:05:07 -0800 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit a85b740f242cb0a239082fcfb8c1eceb23a266df Author: James Liu Date: Sun Jan 22 00:21:55 2023 +0000 Support recording multiple CommandBuffers in RenderContext (#7248) # Objective `RenderContext`, the core abstraction for running the render graph, currently only supports recording one `CommandBuffer` across the entire render graph. This means the entire buffer must be recorded sequentially, usually via the render graph itself. This prevents parallelization and forces users to only encode their commands in the render graph. ## Solution Allow `RenderContext` to store a `Vec` that it progressively appends to. By default, the context will not have a command encoder, but will create one as soon as either `begin_tracked_render_pass` or the `command_encoder` accesor is first called. `RenderContext::add_command_buffer` allows users to interrupt the current command encoder, flush it to the vec, append a user-provided `CommandBuffer` and reset the command encoder to start a new buffer. Users or the render graph will call `RenderContext::finish` to retrieve the series of buffers for submitting to the queue. This allows users to encode their own `CommandBuffer`s outside of the render graph, potentially in different threads, and store them in components or resources. Ideally, in the future, the core pipeline passes can run in `RenderStage::Render` systems and end up saving the completed command buffers to either `Commands` or a field in `RenderPhase`. ## Alternatives The alternative is to use to use wgpu's `RenderBundle`s, which can achieve similar results; however it's not universally available (no OpenGL, WebGL, and DX11). --- ## Changelog Added: `RenderContext::new` Added: `RenderContext::add_command_buffer` Added: `RenderContext::finish` Changed: `RenderContext::render_device` is now private. Use the accessor `RenderContext::render_device()` instead. Changed: `RenderContext::command_encoder` is now private. Use the accessor `RenderContext::command_encoder()` instead. Changed: `RenderContext` now supports adding external `CommandBuffer`s for inclusion into the render graphs. These buffers can be encoded outside of the render graph (i.e. in a system). ## Migration Guide `RenderContext`'s fields are now private. Use the accessors on `RenderContext` instead, and construct it with `RenderContext::new`. commit 603cb439d9ec9eba62de3493eb0a2553d25a7c55 Author: Marco Buono Date: Sat Jan 21 21:46:53 2023 +0000 Standard Material Blend Modes (#6644) # Objective - This PR adds support for blend modes to the PBR `StandardMaterial`. Screenshot 2022-11-18 at 20 00 56 Screenshot 2022-11-18 at 20 01 01 ## Solution - The existing `AlphaMode` enum is extended, adding three more modes: `AlphaMode::Premultiplied`, `AlphaMode::Add` and `AlphaMode::Multiply`; - All new modes are rendered in the existing `Transparent3d` phase; - The existing mesh flags for alpha mode are reorganized for a more compact/efficient representation, and new values are added; - `MeshPipelineKey::TRANSPARENT_MAIN_PASS` is refactored into `MeshPipelineKey::BLEND_BITS`. - `AlphaMode::Opaque` and `AlphaMode::Mask(f32)` share a single opaque pipeline key: `MeshPipelineKey::BLEND_OPAQUE`; - `Blend`, `Premultiplied` and `Add` share a single premultiplied alpha pipeline key, `MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA`. In the shader, color values are premultiplied accordingly (or not) depending on the blend mode to produce the three different results after PBR/tone mapping/dithering; - `Multiply` uses its own independent pipeline key, `MeshPipelineKey::BLEND_MULTIPLY`; - Example and documentation are provided. --- ## Changelog ### Added - Added support for additive and multiplicative blend modes in the PBR `StandardMaterial`, via `AlphaMode::Add` and `AlphaMode::Multiply`; - Added support for premultiplied alpha in the PBR `StandardMaterial`, via `AlphaMode::Premultiplied`; commit ff5e4fd1ec27ce4b6d4ad9f02562d889a4f4a7fe Author: targrub Date: Sat Jan 21 17:55:39 2023 +0000 Use `Time` `resource` instead of `Extract`ing `Time` (#7316) # Objective - "Fixes #7308". ## Solution - Use the `Time` `Resource` instead of `Extract>` commit cb4e8c832c0ee33ea0b28a6f79971955643055fb Author: Boxy Date: Sat Jan 21 00:55:23 2023 +0000 Update milestone section in `contributing.md` (#7213) Current info is not up to date as we are now using a train release model and frequently end up with PRs and issues in the milestone that are not resolved before release. As the release milestone is now mostly used for prioritizing what work gets done I updated this section to be about prioritizing PRs/issues instead of preparing releases. commit cef56a0d4782f50daef32d6c21d140510afe26d3 Author: Molot2032 <117271367+Molot2032@users.noreply.github.com> Date: Sat Jan 21 00:17:11 2023 +0000 Allow users of Text/TextBundle to choose from glyph_brush_layout's BuiltInLineBreaker options. (#7283) # Objective Currently, Text always uses the default linebreaking behaviour in glyph_brush_layout `BuiltInLineBreaker::Unicode` which breaks lines at word boundaries. However, glyph_brush_layout also supports breaking lines at any character by setting the linebreaker to `BuiltInLineBreaker::AnyChar`. Having text wrap character-by-character instead of at word boundaries is desirable in some cases - consider that consoles/terminals usually wrap this way. As a side note, the default Unicode linebreaker does not seem to handle emergency cases, where there is no word boundary on a line to break at. In that case, the text runs out of bounds. Issue #1867 shows an example of this. ## Solution Basically just copies how TextAlignment is exposed, but for a new enum TextLineBreakBehaviour. This PR exposes glyph_brush_layout's two simple linebreaking options (Unicode, AnyChar) to users of Text via the enum TextLineBreakBehaviour (which just translates those 2 aforementioned options), plus a method 'with_linebreak_behaviour' on Text and TextBundle. ## Changelog Added `Text::with_linebreak_behaviour` Added `TextBundle::with_linebreak_behaviour` `TextPipeline::queue_text` and `GlyphBrush::compute_glyphs` now need a TextLineBreakBehaviour argument, in order to pass through the new field. Modified the `text2d` example to show both linebreaking behaviours. ## Example Here's what the modified example looks like ![image](https://user-images.githubusercontent.com/117271367/213589184-b1a54bf3-116c-4721-8cb6-1cb69edb3070.png) commit a94830f0c9627a58fb1fff858b230444848ce677 Author: François Date: Sat Jan 21 00:01:28 2023 +0000 break feedback loop when moving cursor (#7298) # Objective - Fixes #7294 ## Solution - Do not trigger change detection when setting the cursor position from winit When moving the cursor continuously, Winit sends events: - CursorMoved(0) - CursorMoved(1) - => start of Bevy schedule execution - CursorMoved(2) - CursorMoved(3) - <= End of Bevy schedule execution if Bevy schedule runs after the event 1, events 2 and 3 would happen during the execution but would be read only on the next system run. During the execution, the system would detect a change on cursor position, and send back an order to winit to move it back to 1, so event 2 and 3 would be ignored. By bypassing change detection when setting the cursor from winit event, it doesn't trigger sending back that change to winit out of order. commit 1be3b6d59294e02cd8608451e565720417aa8637 Author: IceSentry Date: Fri Jan 20 23:10:37 2023 +0000 fix shader_instancing (#7305) # Objective - The changes to the MeshPipeline done for the prepass broke the shader_instancing example. The issue is that the view_layout changes based on if MSAA is enabled or not, but the example hardcoded the view_layout. ## Solution - Don't overwrite the bind_group_layout of the descriptor since the MeshPipeline already takes care of this in the specialize function. Closes https://github.com/bevyengine/bevy/issues/7285 commit eda3ffb0af66d9af3bebb7427841e75939b4d5b8 Author: Jonah Henriksson <33059163+JonahPlusPlus@users.noreply.github.com> Date: Fri Jan 20 19:08:04 2023 +0000 Added `resource_id` and changed `init_resource` and `init_non_send_resource` to return `ComponentId` (#7284) # Objective - `Components::resource_id` doesn't exist. Like `Components::component_id` but for resources. ## Solution - Created `Components::resource_id` and added some docs. --- ## Changelog - Added `Components::resource_id`. - Changed `World::init_resource` to return the generated `ComponentId`. - Changed `World::init_non_send_resource` to return the generated `ComponentId`. commit 02637b609e8c9371e8f8b54deae5ee8594fb76c1 Author: Jakob Hellermann Date: Fri Jan 20 14:25:25 2023 +0000 fix clippy (#7302) # Objective - `cargo clippy` should work (except for clippy::type_complexity) ## Solution - fix new clippy lints commit 0804136dcd10c8bfdf7a1099a3a0cb72f6ce2059 Author: François Date: Fri Jan 20 14:25:24 2023 +0000 expose cursor position with scale (#7297) # Objective - Fixes #7288 - Do not expose access directly to cursor position as it is the physical position, ignoring scale ## Solution - Make cursor position private - Expose getter/setter on the window to have access to the scale commit efa2c6edadb598b67015500ae933bc792962379b Author: François Date: Fri Jan 20 14:25:23 2023 +0000 revert stage changed for window closing (#7296) # Objective - Fix #7287 ## Solution - Revert stage changed in windows as entities PR for window closing systems how it was before: https://github.com/bevyengine/bevy/blob/f0c504947ce653068a424979faf226c1e990818d/crates/bevy_window/src/lib.rs#L92-L100 commit 06ada2e93decba7d1df1b12503655773ab6136f4 Author: Sjael Date: Fri Jan 20 14:25:21 2023 +0000 Changed Msaa to Enum (#7292) # Objective Fixes #6931 Continues #6954 by squashing `Msaa` to a flat enum Helps out #7215 # Solution ``` pub enum Msaa { Off = 1, #[default] Sample4 = 4, } ``` # Changelog - Modified - `Msaa` is now enum - Defaults to 4 samples - Uses `.samples()` method to get the sample number as `u32` # Migration Guide ``` let multi = Msaa { samples: 4 } // is now let multi = Msaa::Sample4 multi.samples // is now multi.samples() ``` Co-authored-by: Sjael commit 5d5a50468535ada3d504cc5f15c66c080a86cda7 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Fri Jan 20 13:39:23 2023 +0000 Revise `SystemParam` docs (#7274) # Objective Increase clarity in a few places for the `SystemParam` docs. commit f024bce2b8f59f212435cdfc3d8dd6a91743492b Author: Pascal Hertleif Date: Fri Jan 20 13:20:28 2023 +0000 Demand newer async-channel version (#7301) After #6503, bevy_render uses the `send_blocking` method introduced in async-channel 1.7, but depended only on ^1.4. I saw this after pulling main without running cargo update. # Objective - Fix minimum dependency version of async-channel ## Solution - Bump async-channel version constraint to ^1.8, which is currently the latest version. NOTE: Both bevy_ecs and bevy_tasks also depend on async-channel but they didn't use any newer features. commit dfea88c64d6ac96c73cf1d6e200ea507fb6b4477 Author: James Liu Date: Fri Jan 20 08:47:20 2023 +0000 Basic adaptive batching for parallel query iteration (#4777) # Objective Fixes #3184. Fixes #6640. Fixes #4798. Using `Query::par_for_each(_mut)` currently requires a `batch_size` parameter, which affects how it chunks up large archetypes and tables into smaller chunks to run in parallel. Tuning this value is difficult, as the performance characteristics entirely depends on the state of the `World` it's being run on. Typically, users will just use a flat constant and just tune it by hand until it performs well in some benchmarks. However, this is both error prone and risks overfitting the tuning on that benchmark. This PR proposes a naive automatic batch-size computation based on the current state of the `World`. ## Background `Query::par_for_each(_mut)` schedules a new Task for every archetype or table that it matches. Archetypes/tables larger than the batch size are chunked into smaller tasks. Assuming every entity matched by the query has an identical workload, this makes the worst case scenario involve using a batch size equal to the size of the largest matched archetype or table. Conversely, a batch size of `max {archetype, table} size / thread count * COUNT_PER_THREAD` is likely the sweetspot where the overhead of scheduling tasks is minimized, at least not without grouping small archetypes/tables together. There is also likely a strict minimum batch size below which the overhead of scheduling these tasks is heavier than running the entire thing single-threaded. ## Solution - [x] Remove the `batch_size` from `Query(State)::par_for_each` and friends. - [x] Add a check to compute `batch_size = max {archeytpe/table} size / thread count * COUNT_PER_THREAD` - [x] ~~Panic if thread count is 0.~~ Defer to `for_each` if the thread count is 1 or less. - [x] Early return if there is no matched table/archetype. - [x] Add override option for users have queries that strongly violate the initial assumption that all iterated entities have an equal workload. --- ## Changelog Changed: `Query::par_for_each(_mut)` has been changed to `Query::par_iter(_mut)` and will now automatically try to produce a batch size for callers based on the current `World` state. ## Migration Guide The `batch_size` parameter for `Query(State)::par_for_each(_mut)` has been removed. These calls will automatically compute a batch size for you. Remove these parameters from all calls to these functions. Before: ```rust fn parallel_system(query: Query<&MyComponent>) { query.par_for_each(32, |comp| { ... }); } ``` After: ```rust fn parallel_system(query: Query<&MyComponent>) { query.par_iter().for_each(|comp| { ... }); } ``` Co-authored-by: Arnav Choubey <56453634+x-52@users.noreply.github.com> Co-authored-by: Robert Swain Co-authored-by: François Co-authored-by: Corey Farwell Co-authored-by: Aevyrie commit cab065bad4302e4df427ffbafcdb112e755321ce Author: ickshonpe Date: Fri Jan 20 01:05:30 2023 +0000 remove the image loaded check for nodes without images in extract_uinodes (#7280) ## Problem `extract_uinodes` checks if an image is loaded for nodes without images ## Solution Move the image loading skip check so that it is only performed for nodes with a `UiImage` component. commit 2027af4c54082007fed2091f112a11cb0bc5fc08 Author: Mike Date: Thu Jan 19 23:45:46 2023 +0000 Pipelined Rendering (#6503) # Objective - Implement pipelined rendering - Fixes #5082 - Fixes #4718 ## User Facing Description Bevy now implements piplelined rendering! Pipelined rendering allows the app logic and rendering logic to run on different threads leading to large gains in performance. ![image](https://user-images.githubusercontent.com/2180432/202049871-3c00b801-58ab-448f-93fd-471e30aba55f.png) *tracy capture of many_foxes example* To use pipelined rendering, you just need to add the `PipelinedRenderingPlugin`. If you're using `DefaultPlugins` then it will automatically be added for you on all platforms except wasm. Bevy does not currently support multithreading on wasm which is needed for this feature to work. If you aren't using `DefaultPlugins` you can add the plugin manually. ```rust use bevy::prelude::*; use bevy::render::pipelined_rendering::PipelinedRenderingPlugin; fn main() { App::new() // whatever other plugins you need .add_plugin(RenderPlugin) // needs to be added after RenderPlugin .add_plugin(PipelinedRenderingPlugin) .run(); } ``` If for some reason pipelined rendering needs to be removed. You can also disable the plugin the normal way. ```rust use bevy::prelude::*; use bevy::render::pipelined_rendering::PipelinedRenderingPlugin; fn main() { App::new.add_plugins(DefaultPlugins.build().disable::()); } ``` ### A setup function was added to plugins A optional plugin lifecycle function was added to the `Plugin trait`. This function is called after all plugins have been built, but before the app runner is called. This allows for some final setup to be done. In the case of pipelined rendering, the function removes the sub app from the main app and sends it to the render thread. ```rust struct MyPlugin; impl Plugin for MyPlugin { fn build(&self, app: &mut App) { } // optional function fn setup(&self, app: &mut App) { // do some final setup before runner is called } } ``` ### A Stage for Frame Pacing In the `RenderExtractApp` there is a stage labelled `BeforeIoAfterRenderStart` that systems can be added to. The specific use case for this stage is for a frame pacing system that can delay the start of main app processing in render bound apps to reduce input latency i.e. "frame pacing". This is not currently built into bevy, but exists as `bevy` ```text |-------------------------------------------------------------------| | | BeforeIoAfterRenderStart | winit events | main schedule | | extract |---------------------------------------------------------| | | extract commands | rendering schedule | |-------------------------------------------------------------------| ``` ### Small API additions * `Schedule::remove_stage` * `App::insert_sub_app` * `App::remove_sub_app` * `TaskPool::scope_with_executor` ## Problems and Solutions ### Moving render app to another thread Most of the hard bits for this were done with the render redo. This PR just sends the render app back and forth through channels which seems to work ok. I originally experimented with using a scope to run the render task. It was cuter, but that approach didn't allow render to start before i/o processing. So I switched to using channels. There is much complexity in the coordination that needs to be done, but it's worth it. By moving rendering during i/o processing the frame times should be much more consistent in render bound apps. See https://github.com/bevyengine/bevy/issues/4691. ### Unsoundness with Sending World with NonSend resources Dropping !Send things on threads other than the thread they were spawned on is considered unsound. The render world doesn't have any nonsend resources. So if we tell the users to "pretty please don't spawn nonsend resource on the render world", we can avoid this problem. More seriously there is this https://github.com/bevyengine/bevy/pull/6534 pr, which patches the unsoundness by aborting the app if a nonsend resource is dropped on the wrong thread. ~~That PR should probably be merged before this one.~~ For a longer term solution we have this discussion going https://github.com/bevyengine/bevy/discussions/6552. ### NonSend Systems in render world The render world doesn't have any !Send resources, but it does have a non send system. While Window is Send, winit does have some API's that can only be accessed on the main thread. `prepare_windows` in the render schedule thus needs to be scheduled on the main thread. Currently we run nonsend systems by running them on the thread the TaskPool::scope runs on. When we move render to another thread this no longer works. To fix this, a new `scope_with_executor` method was added that takes a optional `TheadExecutor` that can only be ticked on the thread it was initialized on. The render world then holds a `MainThreadExecutor` resource which can be passed to the scope in the parallel executor that it uses to spawn it's non send systems on. ### Scopes executors between render and main should not share tasks Since the render world and the app world share the `ComputeTaskPool`. Because `scope` has executors for the ComputeTaskPool a system from the main world could run on the render thread or a render system could run on the main thread. This can cause performance problems because it can delay a stage from finishing. See https://github.com/bevyengine/bevy/pull/6503#issuecomment-1309791442 for more details. To avoid this problem, `TaskPool::scope` has been changed to not tick the ComputeTaskPool when it's used by the parallel executor. In the future when we move closer to the 1 thread to 1 logical core model we may want to overprovide threads, because the render and main app threads don't do much when executing the schedule. ## Performance My machine is Windows 11, AMD Ryzen 5600x, RX 6600 ### Examples #### This PR with pipelining vs Main > Note that these were run on an older version of main and the performance profile has probably changed due to optimizations Seeing a perf gain from 29% on many lights to 7% on many sprites.   | percent |   |   | Diff |   |   | Main |   |   | PR |   |   -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- tracy frame time | mean | median | sigma | mean | median | sigma | mean | median | sigma | mean | median | sigma many foxes | 27.01% | 27.34% | -47.09% | 1.58 | 1.55 | -1.78 | 5.85 | 5.67 | 3.78 | 4.27 | 4.12 | 5.56 many lights | 29.35% | 29.94% | -10.84% | 3.02 | 3.03 | -0.57 | 10.29 | 10.12 | 5.26 | 7.27 | 7.09 | 5.83 many animated sprites | 13.97% | 15.69% | 14.20% | 3.79 | 4.17 | 1.41 | 27.12 | 26.57 | 9.93 | 23.33 | 22.4 | 8.52 3d scene | 25.79% | 26.78% | 7.46% | 0.49 | 0.49 | 0.15 | 1.9 | 1.83 | 2.01 | 1.41 | 1.34 | 1.86 many cubes | 11.97% | 11.28% | 14.51% | 1.93 | 1.78 | 1.31 | 16.13 | 15.78 | 9.03 | 14.2 | 14 | 7.72 many sprites | 7.14% | 9.42% | -85.42% | 1.72 | 2.23 | -6.15 | 24.09 | 23.68 | 7.2 | 22.37 | 21.45 | 13.35 #### This PR with pipelining disabled vs Main Mostly regressions here. I don't think this should be a problem as users that are disabling pipelined rendering are probably running single threaded and not using the parallel executor. The regression is probably mostly due to the switch to use `async_executor::run` instead of `try_tick` and also having one less thread to run systems on. I'll do a writeup on why switching to `run` causes regressions, so we can try to eventually fix it. Using try_tick causes issues when pipeline rendering is enable as seen [here](https://github.com/bevyengine/bevy/pull/6503#issuecomment-1380803518)   | percent |   |   | Diff |   |   | Main |   |   | PR no pipelining |   |   -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- tracy frame time | mean | median | sigma | mean | median | sigma | mean | median | sigma | mean | median | sigma many foxes | -3.72% | -4.42% | -1.07% | -0.21 | -0.24 | -0.04 | 5.64 | 5.43 | 3.74 | 5.85 | 5.67 | 3.78 many lights | 0.29% | -0.30% | 4.75% | 0.03 | -0.03 | 0.25 | 10.29 | 10.12 | 5.26 | 10.26 | 10.15 | 5.01 many animated sprites | 0.22% | 1.81% | -2.72% | 0.06 | 0.48 | -0.27 | 27.12 | 26.57 | 9.93 | 27.06 | 26.09 | 10.2 3d scene | -15.79% | -14.75% | -31.34% | -0.3 | -0.27 | -0.63 | 1.9 | 1.83 | 2.01 | 2.2 | 2.1 | 2.64 many cubes | -2.85% | -3.30% | 0.00% | -0.46 | -0.52 | 0 | 16.13 | 15.78 | 9.03 | 16.59 | 16.3 | 9.03 many sprites | 2.49% | 2.41% | 0.69% | 0.6 | 0.57 | 0.05 | 24.09 | 23.68 | 7.2 | 23.49 | 23.11 | 7.15 ### Benchmarks Mostly the same except empty_systems has got a touch slower. The maybe_pipelining+1 column has the compute task pool with an extra thread over default added. This is because pipelining loses one thread over main to execute systems on, since the main thread no longer runs normal systems.
Click Me ```text group main maybe-pipelining+1 ----- ------------------------- ------------------ busy_systems/01x_entities_03_systems 1.07 30.7±1.32µs ? ?/sec 1.00 28.6±1.35µs ? ?/sec busy_systems/01x_entities_06_systems 1.10 52.1±1.10µs ? ?/sec 1.00 47.2±1.08µs ? ?/sec busy_systems/01x_entities_09_systems 1.00 74.6±1.36µs ? ?/sec 1.00 75.0±1.93µs ? ?/sec busy_systems/01x_entities_12_systems 1.03 100.6±6.68µs ? ?/sec 1.00 98.0±1.46µs ? ?/sec busy_systems/01x_entities_15_systems 1.11 128.5±3.53µs ? ?/sec 1.00 115.5±1.02µs ? ?/sec busy_systems/02x_entities_03_systems 1.16 50.4±2.56µs ? ?/sec 1.00 43.5±3.00µs ? ?/sec busy_systems/02x_entities_06_systems 1.00 87.1±1.27µs ? ?/sec 1.05 91.5±7.15µs ? ?/sec busy_systems/02x_entities_09_systems 1.04 139.9±6.37µs ? ?/sec 1.00 134.0±1.06µs ? ?/sec busy_systems/02x_entities_12_systems 1.05 179.2±3.47µs ? ?/sec 1.00 170.1±3.17µs ? ?/sec busy_systems/02x_entities_15_systems 1.01 219.6±3.75µs ? ?/sec 1.00 218.1±2.55µs ? ?/sec busy_systems/03x_entities_03_systems 1.10 70.6±2.33µs ? ?/sec 1.00 64.3±0.69µs ? ?/sec busy_systems/03x_entities_06_systems 1.02 130.2±3.11µs ? ?/sec 1.00 128.0±1.34µs ? ?/sec busy_systems/03x_entities_09_systems 1.00 195.0±10.11µs ? ?/sec 1.00 194.8±1.41µs ? ?/sec busy_systems/03x_entities_12_systems 1.01 261.7±4.05µs ? ?/sec 1.00 259.8±4.11µs ? ?/sec busy_systems/03x_entities_15_systems 1.00 318.0±3.04µs ? ?/sec 1.06 338.3±20.25µs ? ?/sec busy_systems/04x_entities_03_systems 1.00 82.9±0.63µs ? ?/sec 1.02 84.3±0.63µs ? ?/sec busy_systems/04x_entities_06_systems 1.01 181.7±3.65µs ? ?/sec 1.00 179.8±1.76µs ? ?/sec busy_systems/04x_entities_09_systems 1.04 265.0±4.68µs ? ?/sec 1.00 255.3±1.98µs ? ?/sec busy_systems/04x_entities_12_systems 1.00 335.9±3.00µs ? ?/sec 1.05 352.6±15.84µs ? ?/sec busy_systems/04x_entities_15_systems 1.00 418.6±10.26µs ? ?/sec 1.08 450.2±39.58µs ? ?/sec busy_systems/05x_entities_03_systems 1.07 114.3±0.95µs ? ?/sec 1.00 106.9±1.52µs ? ?/sec busy_systems/05x_entities_06_systems 1.08 229.8±2.90µs ? ?/sec 1.00 212.3±4.18µs ? ?/sec busy_systems/05x_entities_09_systems 1.03 329.3±1.99µs ? ?/sec 1.00 319.2±2.43µs ? ?/sec busy_systems/05x_entities_12_systems 1.06 454.7±6.77µs ? ?/sec 1.00 430.1±3.58µs ? ?/sec busy_systems/05x_entities_15_systems 1.03 554.6±6.15µs ? ?/sec 1.00 538.4±23.87µs ? ?/sec contrived/01x_entities_03_systems 1.00 14.0±0.15µs ? ?/sec 1.08 15.1±0.21µs ? ?/sec contrived/01x_entities_06_systems 1.04 28.5±0.37µs ? ?/sec 1.00 27.4±0.44µs ? ?/sec contrived/01x_entities_09_systems 1.00 41.5±4.38µs ? ?/sec 1.02 42.2±2.24µs ? ?/sec contrived/01x_entities_12_systems 1.06 55.9±1.49µs ? ?/sec 1.00 52.6±1.36µs ? ?/sec contrived/01x_entities_15_systems 1.02 68.0±2.00µs ? ?/sec 1.00 66.5±0.78µs ? ?/sec contrived/02x_entities_03_systems 1.03 25.2±0.38µs ? ?/sec 1.00 24.6±0.52µs ? ?/sec contrived/02x_entities_06_systems 1.00 46.3±0.49µs ? ?/sec 1.04 48.1±4.13µs ? ?/sec contrived/02x_entities_09_systems 1.02 70.4±0.99µs ? ?/sec 1.00 68.8±1.04µs ? ?/sec contrived/02x_entities_12_systems 1.06 96.8±1.49µs ? ?/sec 1.00 91.5±0.93µs ? ?/sec contrived/02x_entities_15_systems 1.02 116.2±0.95µs ? ?/sec 1.00 114.2±1.42µs ? ?/sec contrived/03x_entities_03_systems 1.00 33.2±0.38µs ? ?/sec 1.01 33.6±0.45µs ? ?/sec contrived/03x_entities_06_systems 1.00 62.4±0.73µs ? ?/sec 1.01 63.3±1.05µs ? ?/sec contrived/03x_entities_09_systems 1.02 96.4±0.85µs ? ?/sec 1.00 94.8±3.02µs ? ?/sec contrived/03x_entities_12_systems 1.01 126.3±4.67µs ? ?/sec 1.00 125.6±2.27µs ? ?/sec contrived/03x_entities_15_systems 1.03 160.2±9.37µs ? ?/sec 1.00 156.0±1.53µs ? ?/sec contrived/04x_entities_03_systems 1.02 41.4±3.39µs ? ?/sec 1.00 40.5±0.52µs ? ?/sec contrived/04x_entities_06_systems 1.00 78.9±1.61µs ? ?/sec 1.02 80.3±1.06µs ? ?/sec contrived/04x_entities_09_systems 1.02 121.8±3.97µs ? ?/sec 1.00 119.2±1.46µs ? ?/sec contrived/04x_entities_12_systems 1.00 157.8±1.48µs ? ?/sec 1.01 160.1±1.72µs ? ?/sec contrived/04x_entities_15_systems 1.00 197.9±1.47µs ? ?/sec 1.08 214.2±34.61µs ? ?/sec contrived/05x_entities_03_systems 1.00 49.1±0.33µs ? ?/sec 1.01 49.7±0.75µs ? ?/sec contrived/05x_entities_06_systems 1.00 95.0±0.93µs ? ?/sec 1.00 94.6±0.94µs ? ?/sec contrived/05x_entities_09_systems 1.01 143.2±1.68µs ? ?/sec 1.00 142.2±2.00µs ? ?/sec contrived/05x_entities_12_systems 1.00 191.8±2.03µs ? ?/sec 1.01 192.7±7.88µs ? ?/sec contrived/05x_entities_15_systems 1.02 239.7±3.71µs ? ?/sec 1.00 235.8±4.11µs ? ?/sec empty_systems/000_systems 1.01 47.8±0.67ns ? ?/sec 1.00 47.5±2.02ns ? ?/sec empty_systems/001_systems 1.00 1743.2±126.14ns ? ?/sec 1.01 1761.1±70.10ns ? ?/sec empty_systems/002_systems 1.01 2.2±0.04µs ? ?/sec 1.00 2.2±0.02µs ? ?/sec empty_systems/003_systems 1.02 2.7±0.09µs ? ?/sec 1.00 2.7±0.16µs ? ?/sec empty_systems/004_systems 1.00 3.1±0.11µs ? ?/sec 1.00 3.1±0.24µs ? ?/sec empty_systems/005_systems 1.00 3.5±0.05µs ? ?/sec 1.11 3.9±0.70µs ? ?/sec empty_systems/010_systems 1.00 5.5±0.12µs ? ?/sec 1.03 5.7±0.17µs ? ?/sec empty_systems/015_systems 1.00 7.9±0.19µs ? ?/sec 1.06 8.4±0.16µs ? ?/sec empty_systems/020_systems 1.00 10.4±1.25µs ? ?/sec 1.02 10.6±0.18µs ? ?/sec empty_systems/025_systems 1.00 12.4±0.39µs ? ?/sec 1.14 14.1±1.07µs ? ?/sec empty_systems/030_systems 1.00 15.1±0.39µs ? ?/sec 1.05 15.8±0.62µs ? ?/sec empty_systems/035_systems 1.00 16.9±0.47µs ? ?/sec 1.07 18.0±0.37µs ? ?/sec empty_systems/040_systems 1.00 19.3±0.41µs ? ?/sec 1.05 20.3±0.39µs ? ?/sec empty_systems/045_systems 1.00 22.4±1.67µs ? ?/sec 1.02 22.9±0.51µs ? ?/sec empty_systems/050_systems 1.00 24.4±1.67µs ? ?/sec 1.01 24.7±0.40µs ? ?/sec empty_systems/055_systems 1.05 28.6±5.27µs ? ?/sec 1.00 27.2±0.70µs ? ?/sec empty_systems/060_systems 1.02 29.9±1.64µs ? ?/sec 1.00 29.3±0.66µs ? ?/sec empty_systems/065_systems 1.02 32.7±3.15µs ? ?/sec 1.00 32.1±0.98µs ? ?/sec empty_systems/070_systems 1.00 33.0±1.42µs ? ?/sec 1.03 34.1±1.44µs ? ?/sec empty_systems/075_systems 1.00 34.8±0.89µs ? ?/sec 1.04 36.2±0.70µs ? ?/sec empty_systems/080_systems 1.00 37.0±1.82µs ? ?/sec 1.05 38.7±1.37µs ? ?/sec empty_systems/085_systems 1.00 38.7±0.76µs ? ?/sec 1.05 40.8±0.83µs ? ?/sec empty_systems/090_systems 1.00 41.5±1.09µs ? ?/sec 1.04 43.2±0.82µs ? ?/sec empty_systems/095_systems 1.00 43.6±1.10µs ? ?/sec 1.04 45.2±0.99µs ? ?/sec empty_systems/100_systems 1.00 46.7±2.27µs ? ?/sec 1.03 48.1±1.25µs ? ?/sec ```
## Migration Guide ### App `runner` and SubApp `extract` functions are now required to be Send This was changed to enable pipelined rendering. If this breaks your use case please report it as these new bounds might be able to be relaxed. ## ToDo * [x] redo benchmarking * [x] reinvestigate the perf of the try_tick -> run change for task pool scope commit b3224e135bd82b445fa506e6b68c71cb13a4e7be Author: IceSentry Date: Thu Jan 19 22:11:13 2023 +0000 Add depth and normal prepass (#6284) # Objective - Add a configurable prepass - A depth prepass is useful for various shader effects and to reduce overdraw. It can be expansive depending on the scene so it's important to be able to disable it if you don't need any effects that uses it or don't suffer from excessive overdraw. - The goal is to eventually use it for things like TAA, Ambient Occlusion, SSR and various other techniques that can benefit from having a prepass. ## Solution The prepass node is inserted before the main pass. It runs for each `Camera3d` with a prepass component (`DepthPrepass`, `NormalPrepass`). The presence of one of those components is used to determine which textures are generated in the prepass. When any prepass is enabled, the depth buffer generated will be used by the main pass to reduce overdraw. The prepass runs for each `Material` created with the `MaterialPlugin::prepass_enabled` option set to `true`. You can overload the shader used by the prepass by using `Material::prepass_vertex_shader()` and/or `Material::prepass_fragment_shader()`. It will also use the `Material::specialize()` for more advanced use cases. It is enabled by default on all materials. The prepass works on opaque materials and materials using an alpha mask. Transparent materials are ignored. The `StandardMaterial` overloads the prepass fragment shader to support alpha mask and normal maps. --- ## Changelog - Add a new `PrepassNode` that runs before the main pass - Add a `PrepassPlugin` to extract/prepare/queue the necessary data - Add a `DepthPrepass` and `NormalPrepass` component to control which textures will be created by the prepass and available in later passes. - Add a new `prepass_enabled` flag to the `MaterialPlugin` that will control if a material uses the prepass or not. - Add a new `prepass_enabled` flag to the `PbrPlugin` to control if the StandardMaterial uses the prepass. Currently defaults to false. - Add `Material::prepass_vertex_shader()` and `Material::prepass_fragment_shader()` to control the prepass from the `Material` ## Notes In bevy's sample 3d scene, the performance is actually worse when enabling the prepass, but on more complex scenes the performance is generally better. I would like more testing on this, but @DGriffin91 has reported a very noticeable improvements in some scenes. The prepass is also used by @JMS55 for TAA and GTAO discord thread: This PR was built on top of the work of multiple people Co-Authored-By: @superdump Co-Authored-By: @robtfm Co-Authored-By: @JMS55 Co-authored-by: Charles Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> commit 519f6f45de0fc16592c7adcf40748f174569f807 Author: Aceeri Date: Thu Jan 19 06:05:39 2023 +0000 Remove unnecessary windows.rs file (#7277) # Objective Accidentally re-added this old file at some point during the Windows as Entities PR apparently ## Solution Removed the file, its unused commit 884ebbf4b7a61d8748b2b309ab0bcdf02b51abbf Author: Mike Date: Thu Jan 19 05:08:55 2023 +0000 min version of fixedbitset was changed (#7275) # Objective - schedule v3 is using is_clear which was added in 0.4.2, so bump the version commit fe382acfd09870992c0516173360fd7da8c108a8 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Thu Jan 19 04:35:46 2023 +0000 Fix a typo on `Window::set_minimized` (#7276) # Objective There is a typo on the method `Window::set_minimized`. ## Solution fix it commit 629cfab135e5f4087ad2a481a4a9be8921d13b83 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Thu Jan 19 03:04:39 2023 +0000 Improve safety for `CommandQueue` internals (#7039) # Objective - Safety comments for the `CommandQueue` type are quite sparse and very imprecise. Sometimes, they are right for the wrong reasons or use circular reasoning. ## Solution - Document previously-implicit safety invariants. - Rewrite safety comments to actually reflect the specific invariants of each operation. - Use `OwningPtr` instead of raw pointers, to encode an invariant in the type system instead of via comments. - Use typed pointer methods when possible to increase reliability. --- ## Changelog + Added the function `OwningPtr::read_unaligned`. commit ddfafab971e335ce5a47d4e4b3fcf51f124d999f Author: Aceeri Date: Thu Jan 19 00:38:28 2023 +0000 Windows as Entities (#5589) # Objective Fix https://github.com/bevyengine/bevy/issues/4530 - Make it easier to open/close/modify windows by setting them up as `Entity`s with a `Window` component. - Make multiple windows very simple to set up. (just add a `Window` component to an entity and it should open) ## Solution - Move all properties of window descriptor to ~components~ a component. - Replace `WindowId` with `Entity`. - ~Use change detection for components to update backend rather than events/commands. (The `CursorMoved`/`WindowResized`/... events are kept for user convenience.~ Check each field individually to see what we need to update, events are still kept for user convenience. --- ## Changelog - `WindowDescriptor` renamed to `Window`. - Width/height consolidated into a `WindowResolution` component. - Requesting maximization/minimization is done on the [`Window::state`] field. - `WindowId` is now `Entity`. ## Migration Guide - Replace `WindowDescriptor` with `Window`. - Change `width` and `height` fields in a `WindowResolution`, either by doing ```rust WindowResolution::new(width, height) // Explicitly // or using From<_> for tuples for convenience (1920., 1080.).into() ``` - Replace any `WindowCommand` code to just modify the `Window`'s fields directly and creating/closing windows is now by spawning/despawning an entity with a `Window` component like so: ```rust let window = commands.spawn(Window { ... }).id(); // open window commands.entity(window).despawn(); // close window ``` ## Unresolved - ~How do we tell when a window is minimized by a user?~ ~Currently using the `Resize(0, 0)` as an indicator of minimization.~ No longer attempting to tell given how finnicky this was across platforms, now the user can only request that a window be maximized/minimized. ## Future work - Move `exit_on_close` functionality out from windowing and into app(?) - https://github.com/bevyengine/bevy/issues/5621 - https://github.com/bevyengine/bevy/issues/7099 - https://github.com/bevyengine/bevy/issues/7098 Co-authored-by: Carter Anderson commit f0c504947ce653068a424979faf226c1e990818d Author: Stephen Martindale Date: Wed Jan 18 23:02:38 2023 +0000 Docs: App::run() might never return; effect of WinitSettings::return_from_run. (#7228) # Objective See: - https://github.com/bevyengine/bevy/issues/7067#issuecomment-1381982285 - (This does not fully close that issue in my opinion.) - https://discord.com/channels/691052431525675048/1063454009769340989 ## Solution This merge request adds documentation: 1. Alert users to the fact that `App::run()` might never return and code placed after it might never be executed. 2. Makes `winit::WinitSettings::return_from_run` discoverable. 3. Better explains why `winit::WinitSettings::return_from_run` is discouraged and better links to up-stream docs. on that topic. 4. Adds notes to the `app/return_after_run.rs` example which otherwise promotes a feature that carries caveats. Furthermore, w.r.t `winit::WinitSettings::return_from_run`: - Broken links to `winit` docs are fixed. - Links now point to BOTH `EventLoop::run()` and `EventLoopExtRunReturn::run_return()` which are the salient up-stream pages and make more sense, taken together. - Collateral damage: "Supported platforms" heading; disambiguation of "run" → `App::run()`; links. ## Future Work I deliberately structured the "`run()` might not return" section under `App::run()` to allow for alternative patterns (e.g. `AppExit` event, `WindowClosed` event) to be listed or mentioned, beneath it, in the future. commit f8feec6ef1a47a6c8a562399b883d92198f02222 Author: targrub Date: Wed Jan 18 17:20:27 2023 +0000 Fix tiny clippy issue for upcoming Rust version (#7266) Co-authored-by: targrub <62773321+targrub@users.noreply.github.com> commit e0b921fbd99dffb612860ac9684f800b472a66ec Author: harudagondi Date: Wed Jan 18 17:20:26 2023 +0000 AudioOutput is actually a normal resource now, not a non-send resource (#7262) # Objective - Fixes #7260 ## Solution - #6649 used `init_non_send_resource` for `AudioOutput`, but this is before #6436 was merged. - Use `init_resource` instead. commit 46293ce1e4c61421e353dc0b0431da67af6b7568 Author: Rob Parrett Date: Wed Jan 18 17:06:08 2023 +0000 Fix init_non_send_resource overwriting previous values (#7261) # Objective Repeated calls to `init_non_send_resource` currently overwrite the old value because the wrong storage is being checked. ## Solution Use the correct storage. Add some tests. ## Notes Without the fix, the new test fails with ``` thread 'world::tests::init_non_send_resource_does_not_overwrite' panicked at 'assertion failed: `(left == right)` left: `1`, right: `0`', crates/bevy_ecs/src/world/mod.rs:2267:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace test world::tests::init_non_send_resource_does_not_overwrite ... FAILED ``` This was introduced by #7174 and it seems like a fairly straightforward oopsie. commit d6bfd44f8f48af92c727a42ca7fe43f32a2ab747 Author: Charles Bournhonesque Date: Wed Jan 18 14:26:07 2023 +0000 update doc comment for new_archetype in query-state (#7241) # Objective I was reading through the bevy_ecs code, trying to understand how everything works. I was getting a bit confused when reading the doc comment for the `new_archetype` function; it looks like it doesn't create a new archetype but instead updates some internal state in the SystemParam to facility QueryIteration. (I still couldn't find where a new archetype was actually created) ## Solution - Adding a doc comment with a more correct explanation. If it's deemed correct, I can also update the doc-comment for the other `new_archetype` calls commit 88b353c4b1665625d5aabe6149e184ec5ba5984c Author: James Liu Date: Wed Jan 18 02:19:19 2023 +0000 Reduce the use of atomics in the render phase (#7084) # Objective Speed up the render phase of rendering. An extension of #6885. `SystemState::get` increments the `World`'s change tick atomically every time it's called. This is notably more expensive than a unsynchronized increment, even without contention. It also updates the archetypes, even when there has been nothing to update when it's called repeatedly. ## Solution Piggyback off of #6885. Split `SystemState::validate_world_and_update_archetypes` into `SystemState::validate_world` and `SystemState::update_archetypes`, and make the later `pub`. Then create safe variants of `SystemState::get_unchecked_manual` that still validate the `World` but do not update archetypes and do not increment the change tick using `World::read_change_tick` and `World::change_tick`. Update `RenderCommandState` to call `SystemState::update_archetypes` in `Draw::prepare` and `SystemState::get_manual` in `Draw::draw`. ## Performance There's a slight perf benefit (~2%) for `main_opaque_pass_3d` on `many_foxes` (340.39 us -> 333.32 us) ![image](https://user-images.githubusercontent.com/3137680/210643746-25320b98-3e2b-4a95-8084-892c23bb8b4e.png) ## Alternatives We can change `SystemState::get` to not increment the `World`'s change tick. Though this would still put updating the archetypes and an atomic read on the hot-path. --- ## Changelog Added: `SystemState::get_manual` Added: `SystemState::get_manual_mut` Added: `SystemState::update_archetypes` commit 9eefd7c022efe572ffd2840cfc7a8b9eee982428 Author: ickshonpe Date: Wed Jan 18 02:19:17 2023 +0000 Remove VerticalAlign from TextAlignment (#6807) # Objective Remove the `VerticalAlign` enum. Text's alignment field should only affect the text's internal text alignment, not its position. The only way to control a `TextBundle`'s position and bounds should be through the manipulation of the constraints in the `Style` components of the nodes in the Bevy UI's layout tree. `Text2dBundle` should have a separate `Anchor` component that sets its position relative to its transform. Related issues: #676, #1490, #5502, #5513, #5834, #6717, #6724, #6741, #6748 ## Changelog * Changed `TextAlignment` into an enum with `Left`, `Center`, and `Right` variants. * Removed the `HorizontalAlign` and `VerticalAlign` types. * Added an `Anchor` component to `Text2dBundle` * Added `Component` derive to `Anchor` * Use `f32::INFINITY` instead of `f32::MAX` to represent unbounded text in Text2dBounds ## Migration Guide The `alignment` field of `Text` now only affects the text's internal alignment. ### Change `TextAlignment` to TextAlignment` which is now an enum. Replace: * `TextAlignment::TOP_LEFT`, `TextAlignment::CENTER_LEFT`, `TextAlignment::BOTTOM_LEFT` with `TextAlignment::Left` * `TextAlignment::TOP_CENTER`, `TextAlignment::CENTER_LEFT`, `TextAlignment::BOTTOM_CENTER` with `TextAlignment::Center` * `TextAlignment::TOP_RIGHT`, `TextAlignment::CENTER_RIGHT`, `TextAlignment::BOTTOM_RIGHT` with `TextAlignment::Right` ### Changes for `Text2dBundle` `Text2dBundle` has a new field 'text_anchor' that takes an `Anchor` component that controls its position relative to its transform. commit 4ff50f6b5062145592575c98d9cc85f04a23ec82 Author: IceSentry Date: Wed Jan 18 02:07:26 2023 +0000 fix load_internal_binary_asset with debug_asset_server (#7246) # Objective - Enabling the `debug_asset_server` feature doesn't compile when using it with `load_internal_binary_asset!()`. The issue is because it assumes the loader takes an `&'static str` as a parameter, but binary assets loader expect `&'static [u8]`. ## Solution - Add a generic type for the loader and use a different type in `load_internal_asset` and `load_internal_binary_asset` commit 0df67cdaae30becd35447c6767d5e30afeee17f1 Author: dis-da-moe Date: Tue Jan 17 22:42:00 2023 +0000 Add `AddAudioSource` trait and improve `Decodable` docs (#6649) # Objective - Fixes #6361 - Fixes #6362 - Fixes #6364 ## Solution - Added an example for creating a custom `Decodable` type - Clarified the documentation on `Decodable` - Added an `AddAudioSource` trait and implemented it for `App` Co-authored-by: dis-da-moe <84386186+dis-da-moe@users.noreply.github.com> commit 7d0edbc4d65c483d3e73c574b93a4a36560c0d07 Author: James Liu Date: Tue Jan 17 22:26:51 2023 +0000 Improve change detection behavior for transform propagation (#6870) # Objective Fix #4647. If any child is changed, or even reordered, `Changed` is true, which causes transform propagation to propagate changes to all siblings of a changed child, even if they don't need to be. ## Solution As `Parent` and `Children` are updated in tandem in hierarchy commands after #4800. `Changed` is true on the child when `Changed` is true on the parent. However, unlike checking children, checking `Changed` is only localized to the current entity and will not force propagation to the siblings. Also took the opportunity to change propagation to use `Query::iter_many` instead of repeated `Query::get` calls. Should cut a bit of the overhead out of propagation. This means we won't panic when there isn't a `Parent` on the child, just skip over it. The tests from #4608 still pass, so the change detection here still works just fine under this approach. commit 0ca9c618e1dedecfe737d9a1a23748192e348441 Author: Boxy Date: Tue Jan 17 21:11:26 2023 +0000 Update "Classifying PRs" section to talk about `D-Complex` (#7216) The current section does not talk about `D-Complex` and lists things like "adds unsafe code" as a reason to mark a PR `S-Controversial`. This is not how `D-Complex` and `S-Controversial` are being used at the moment. This PR lists what classifies a PR as `D-Complex` and what classifies a PR as `S-Controversial`. It also links to some PRs with each combination of labels to help give an idea for what this means in practice. cc #7211 which is doing a similar thing commit 63a291c6a800a12e8beb3dbad8f64927380493ca Author: Mike Date: Tue Jan 17 17:54:53 2023 +0000 add tests for change detection and conditions for stageless (#7249) # Objective - add some tests for how change detection and run criteria interact in stageless commit 45dfa71e032fef2827f154f60928da84e11cc92d Author: robtfm <50659922+robtfm@users.noreply.github.com> Date: Tue Jan 17 17:39:28 2023 +0000 fix bloom viewport (#6802) # Objective fix bloom when used on a camera with a viewport specified ## Solution - pass viewport into the prefilter shader, and use it to read from the correct section of the original rendered screen - don't apply viewport for the intermediate bloom passes, only for the final blend output commit 1cc663f2909e8d8a989668383bb0c3c5f034a816 Author: wyhaya Date: Tue Jan 17 13:26:43 2023 +0000 Improve `Color::hex` performance (#6940) # Objective Improve `Color::hex` performance #### Bench ```bash running 2 tests test bench_color_hex_after ... bench: 4 ns/iter (+/- 0) test bench_color_hex_before ... bench: 14 ns/iter (+/- 0) ``` ## Solution Use `const fn` decode hex value. --- ## Changelog Rename ```rust HexColorError::Hex(FromHexError) -> HexColorError::Char(char) ``` commit 16ff05acdf2f04c37059c76dc87457d36f78c2f8 Author: 2ne1ugly Date: Tue Jan 17 04:20:42 2023 +0000 Add `World::clear_resources` & `World::clear_all` (#3212) # Objective - Fixes #3158 ## Solution - clear columns My implementation of `clear_resources` do not remove the components itself but it clears the columns that keeps the resource data. I'm not sure if the issue meant to clear all resources, even the components and component ids (which I'm not sure if it's possible) Co-authored-by: 2ne1ugly <47616772+2ne1ugly@users.noreply.github.com> commit b5893e570d2a68471d2f3d147751dcc33bac32e0 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Tue Jan 17 03:29:08 2023 +0000 Add a missing impl of `ReadOnlySystemParam` for `Option>` (#7245) # Objective The trait `ReadOnlySystemParam` is not implemented for `Option>`, even though it should be. Follow-up to #7243. This fixes another mistake made in #6919. ## Solution Add the missing impl. commit 0efe66b081e0e8ea5bf089b7d2394598a9774993 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Tue Jan 17 01:39:19 2023 +0000 Remove an incorrect impl of `ReadOnlySystemParam` for `NonSendMut` (#7243) # Objective The trait `ReadOnlySystemParam` is implemented for `NonSendMut`, when it should not be. This mistake was made in #6919. ## Solution Remove the incorrect impl. commit 684f07595f2f440556cd26a3d8fb5af0d809f872 Author: Cameron <51241057+maniwani@users.noreply.github.com> Date: Tue Jan 17 01:39:17 2023 +0000 Add `bevy_ecs::schedule_v3` module (#6587) # Objective Complete the first part of the migration detailed in bevyengine/rfcs#45. ## Solution Add all the new stuff. ### TODO - [x] Impl tuple methods. - [x] Impl chaining. - [x] Port ambiguity detection. - [x] Write docs. - [x] ~~Write more tests.~~(will do later) - [ ] Write changelog and examples here? - [x] ~~Replace `petgraph`.~~ (will do later) Co-authored-by: james7132 Co-authored-by: Michael Hsu Co-authored-by: Mike Hsu commit 6b4795c428f694a332f0a4710df1ba9b5499bc26 Author: ira Date: Mon Jan 16 23:13:11 2023 +0000 Add `Camera::viewport_to_world_2d` (#6557) # Objective Add a simpler and less expensive 2D variant of `viewport_to_world`. Co-authored-by: devil-ira commit 39e14a4a40014fe5f14bef9cf39916ed3e799b2e Author: Alice Cecile Date: Mon Jan 16 22:10:51 2023 +0000 Make `EntityRef::new` unsafe (#7222) # Objective - We rely on the construction of `EntityRef` to be valid elsewhere in unsafe code. This construction is not checked (for performance reasons), and thus this private method must be unsafe. - Fixes #7218. ## Solution - Make the method unsafe. - Add safety docs. - Improve safety docs slightly for the sibling `EntityMut::new`. - Add debug asserts to start to verify these assumptions in debug mode. ## Context for reviewers I attempted to verify the `EntityLocation` more thoroughly, but this turned out to be more work than expected. I've spun that off into #7221 as a result. commit e44990a48d97fe73aef8f53d2016bd05b260e1ba Author: ld000 Date: Mon Jan 16 21:24:15 2023 +0000 Add ReplaceChildren and ClearChildren EntityCommands (#6035) # Objective Fixes #5859 ## Solution - Add `ClearChildren` and `ReplaceChildren` commands in the `crates/bevy_hierarchy/src/child_builder.rs` --- ## Changelog - Added `ClearChildren` and `ReplaceChildren` struct - Added `clear_children(&mut self) -> &mut Self` and `replace_children(&mut self, children: &[Entity]) -> &mut Self` function in `BuildChildren` trait - Changed `PushChildren` `write` function body to a `push_children ` function to reused in `ReplaceChildren` - Added `clear_children` function - Added `push_and_replace_children_commands` and `push_and_clear_children_commands` test Co-authored-by: ld000 Co-authored-by: lidong63 commit d4e3fcdfbf306c10cd28cb914e228cc0a24c2336 Author: Elbert Ronnie Date: Mon Jan 16 21:09:24 2023 +0000 Fix incorrect behavior of `just_pressed` and `just_released` in `Input` (#7238) # Objective - Fixes a bug where `just_pressed` and `just_released` in `Input` might behave incorrectly due calling `clear` 3 times in a single frame through these three different systems: `gamepad_button_event_system`, `gamepad_axis_event_system` and `gamepad_connection_system` in any order ## Solution - Call `clear` only once and before all the above three systems, i.e. in `gamepad_event_system` ## Additional Info - Discussion in Discord: https://discord.com/channels/691052431525675048/768253008416342076/1064621963693273279 commit addc36fe297bce3325cf1fa110a67692ac09d232 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Mon Jan 16 20:35:15 2023 +0000 Add safety comments to usages of `byte_add` (`Ptr`, `PtrMut`, `OwningPtr`) (#7214) # Objective The usages of the unsafe function `byte_add` are not properly documented. Follow-up to #7151. ## Solution Add safety comments to each call-site. commit 5fd628ebd32aea9a38882dfc38cc3160cf4c82f7 Author: ira Date: Mon Jan 16 20:20:37 2023 +0000 Fix Alien Cake Addict example despawn warnings (#7236) # Problem The example's `teardown` system despawns all entities besides the camera using `despawn_recursive` causing it to despawn child entities multiple times which logs a warning. ![image](https://user-images.githubusercontent.com/29694403/212756554-06b3fa42-ddcb-4a05-b841-f587488a10fc.png) # Solution Use `despawn` instead. Co-authored-by: Devil Ira commit 2f4cf768661839079f49fe5ef9527248a9131b95 Author: Nicola Papale Date: Mon Jan 16 18:13:04 2023 +0000 Fix axis settings constructor (#7233) # Objective Currently, the `AxisSettings::new` function is unusable due to an implementation quirk. It only allows `AxisSettings` where the bounds that are supposed to be positive are negative! ## Solution - We fix the bound check - We add a test to make sure the method is usable Seems like the error slipped through because of the relatively verbose code style. With all those `if/else`, very long names, range syntax, the bound check is actually hard to spot. I first refactored a lot of code, but I left out the refactor because the fix should be integrated independently. --- ## Changelog - Fix `AxisSettings::new` only accepting invalid bounds commit 83028994d17842e975b17bd849ab76c3ef6e5fbb Author: Thierry Berger Date: Mon Jan 16 17:36:09 2023 +0000 Optional BEVY_ASSET_ROOT to find assets directory (#5346) # Objective Fixes #5345 ## Changelog - Support optional env variable `BEVY_ASSET_ROOT` to explicitly specify root assets directory. commit a792f37040f5edb62a6a13166c72978e1cbc9c9c Author: Dawid Piotrowski Date: Mon Jan 16 17:17:45 2023 +0000 Relative cursor position (#7199) # Objective Add useful information about cursor position relative to a UI node. Fixes #7079. ## Solution - Added a new `RelativeCursorPosition` component --- ## Changelog - Added - `RelativeCursorPosition` - an example showcasing the new component Co-authored-by: Dawid Piotrowski <41804418+Pietrek14@users.noreply.github.com> commit 517deda215f58da2b6f27d373c42b97a93a95d58 Author: Daniel Chia Date: Mon Jan 16 15:41:14 2023 +0000 Make PipelineCache internally mutable. (#7205) # Objective - Allow rendering queue systems to use a `Res` even for queueing up new rendering pipelines. This is part of unblocking parallel execution queue systems. ## Solution - Make `PipelineCache` internally mutable w.r.t to queueing new pipelines. Pipelines are no longer immediately updated into the cache state, but rather queued into a Vec. The Vec of pending new pipelines is then later processed at the same time we actually create the queued pipelines on the GPU device. --- ## Changelog `PipelineCache` no longer requires mutable access in order to queue render / compute pipelines. ## Migration Guide * Most usages of `resource_mut::` and `ResMut` can be changed to `resource::` and `Res` as long as they don't use any methods requiring mutability - the only public method requiring it is `process_queue`. commit 4b326fb4caed0bcad85954083f87846adface8ad Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Mon Jan 16 15:41:12 2023 +0000 Improve safety for `BlobVec::replace_unchecked` (#7181) # Objective - The function `BlobVec::replace_unchecked` has informal use of safety comments. - This function does strange things with `OwningPtr` in order to get around the borrow checker. ## Solution - Put safety comments in front of each unsafe operation. Describe the specific invariants of each operation and how they apply here. - Added a guard type `OnDrop`, which is used to simplify ownership transfer in case of a panic. --- ## Changelog + Added the guard type `bevy_utils::OnDrop`. + Added conversions from `Ptr`, `PtrMut`, and `OwningPtr` to `NonNull`. commit 38005b07021691af5d36f07a78a69671e250a5f1 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Mon Jan 16 15:22:38 2023 +0000 Support piping exclusive systems (#7023) # Objective Fix #5248. ## Solution Support `In` parameters and allow returning arbitrary types in exclusive systems. --- ## Changelog - Exclusive systems may now be used with system piping. ## Migration Guide Exclusive systems (systems that access `&mut World`) now support system piping, so the `ExclusiveSystemParamFunction` trait now has generics for the `In`put and `Out`put types. ```rust // Before fn my_generic_system(system_function: T) where T: ExclusiveSystemParamFunction { ... } // After fn my_generic_system(system_function: T) where T: ExclusiveSystemParamFunction { ... } ``` commit c56bbcb3b037916ffa64d4b753fb8636756ea9be Author: Carter Anderson Date: Sun Jan 15 06:13:56 2023 +0000 Use Bevy People links in The Bevy Organization Doc (#7200) Bevy People should be considered the source of truth for Bevy Organization roles. This replaces inline lists of maintainers and SMEs with links to Bevy People. commit e42c0988eba616902d2fa615adde6d5598b3712a Author: 2ne1ugly <47616772+2ne1ugly@users.noreply.github.com> Date: Sun Jan 15 05:56:14 2023 +0000 Add missing discord link in "the_bevy_organization.md" (#7203) # Objective - Add change that was suggested in https://github.com/bevyengine/bevy/pull/7185#pullrequestreview-1248746032 but missed ## Solution - Add the change commit 82b0e712cea379f8134b93d154efec616314f7a9 Author: Carter Anderson Date: Sat Jan 14 20:36:56 2023 +0000 Subject Matter Experts and new Bevy Org docs (#7185) We are in the process of rolling out a new Bevy Organization role! (Subject Matter Expert) This adds a new "The Bevy Organization" document and links to it from CONTRIBUTING.md. This doc describes how the Bevy Organization will work going forward. It outlines the functionality of each role, as well as the expectations we have for them. The previously existing roles (Project Lead, Maintainer) still work the same way, but their definition and scope have been made much clearer. Tomorrow we will be announcing this publicly in a blog post. This will describe the motivation and announce the first round of SMEs . But before that it makes sense to do a quick review round first. Given the quick turnaround on this PR, this isn't the best platform to discuss changes to the SME system (or its validity). After you have read the announcement tomorrow, feel free to start discussions wherever is preferable to you (this repo, discord, etc). So for now, please just review for clarity / typos / phrasing / missed info / etc. [Rendered](https://github.com/bevyengine/bevy/blob/08ceae43dbfacf630447e5f5f1cfa53397242687/docs/the_bevy_organization.md) commit 908c40dd88f88219ab8fc11650531bd194bab2e7 Author: Sludge <96552222+SludgePhD@users.noreply.github.com> Date: Sat Jan 14 18:33:38 2023 +0000 Implement `Clone` for all pipeline types (#6653) # Objective Pipelines can be customized by wrapping an existing pipeline in a newtype and adding custom logic to its implementation of `SpecializedMeshPipeline::specialize`. To make that easier, the wrapped pipeline type needs to implement `Clone`. For example, the current non-cloneable pipelines require wrapper pipelines to pull apart the wrapped pipeline like this: ```rust impl FromWorld for Wireframe2dPipeline { fn from_world(world: &mut World) -> Self { let p = &world.resource::>(); Self { mesh2d_pipeline: p.mesh2d_pipeline.clone(), material2d_layout: p.material2d_layout.clone(), vertex_shader: p.vertex_shader.clone(), fragment_shader: p.fragment_shader.clone(), } } } ``` ## Solution Derive or implement `Clone` on all built-in pipeline types. This is easy to do since they mostly just contain cheaply clonable reference-counted types. --- ## Changelog Implement `Clone` for all pipeline types. commit d9265db3447dfe8b74b40de297a3ad7d696f4ece Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Fri Jan 13 22:35:43 2023 +0000 Implement `ReadOnlySystemParam` for `Extract<>` (#7182) # Objective - `Extract` does not implement `ReadOnlySystemParam` even though it should. - Noticed by @hymm on discord: https://discord.com/channels/691052431525675048/749335865876021248/1063535818267963543 ## Solution Implement the trait. commit 0af8e1c2117fa5b36e084848edf0a173207d6f4e Author: robtfm <50659922+robtfm@users.noreply.github.com> Date: Fri Jan 13 17:06:24 2023 +0000 fix spot dir nan again (#7176) # Objective fix error with shadow shader's spotlight direction calculation when direction.y ~= 0 fixes #7152 ## Solution same as #6167: in shadows.wgsl, clamp 1-x^2-z^2 to >= 0 so that we can safely sqrt it commit 008c156991a52532fbab7061a1bee668c78fac91 Author: Jakob Hellermann Date: Fri Jan 13 16:50:26 2023 +0000 refactor: move internals from `entity_ref` to `World`, add `SAFETY` comments (#6402) # Objective There are some utility functions for actually working with `Storages` inside `entity_ref.rs` that are used both for `EntityRef/EntityMut` and `World`, with a `// TODO: move to Storages`. This PR moves them to private methods on `World`, because that's the safest API boundary. On `Storages` you would need to ensure that you pass `Components` from the same world. ## Solution - move get_component[_with_type], get_ticks[_with_type], get_component_and_ticks[_with_type] to `World` (still pub(crate)) - replace `pub use entity_ref::*;` with `pub use entity_ref::{EntityRef, EntityMut}` and qualified `entity_ref::get_mut[_by_id]` in `world.rs` - add safety comments to a bunch of methods commit feac2c206c820934942e5228a1e77c723385e717 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Thu Jan 12 23:25:11 2023 +0000 Remove duplicate lookups from `Resource` initialization (#7174) # Objective * `World::init_resource` and `World::get_resource_or_insert_with` are implemented naively, and as such they perform duplicate `TypeId -> ComponentId` lookups. * `World::get_resource_or_insert_with` contains an additional duplicate `ComponentId -> ResourceData` lookup. * This function also contains an unnecessary panic branch, which we rely on the optimizer to be able to remove. ## Solution Implement the functions using engine-internal code, instead of combining high-level functions. This allows computed variables to persist across different branches, instead of being recomputed. commit b47c466880dd94b36e085e4e7fc7d271cf94353b Author: James Liu Date: Thu Jan 12 22:39:59 2023 +0000 Use Ref instead of &T and Changed (#7175) # Objective Follow up #7097. Use `Ref` instead of `&T` and the change detection query params. ## Solution Replace them. commit 689eab6fb7028e3da2d0e01b8e2d8b26f59c92cb Author: Nicola Papale Date: Thu Jan 12 18:46:11 2023 +0000 Add an extension trait to `EntityCommands` to update hierarchy while preserving `GlobalTransform` (#7024) # Objective It is often necessary to update an entity's parent while keeping its GlobalTransform static. Currently it is cumbersome and error-prone (two questions in the discord `#help` channel in the past week) - Part 2, resolves #5475 - Builds on: #7020. ## Solution - Added the `BuildChildrenTransformExt` trait, it is part of `bevy::prelude` and adds the following methods to `EntityCommands`: - `set_parent_in_place`: Change the parent of an entity and update its `Transform` in order to preserve its `GlobalTransform` after the parent change - `remove_parent_in_place`: Remove an entity from a hierarchy, while preserving its `GlobalTransform`. --- ## Changelog - Added the `BuildChildrenTransformExt` trait, it is part of `bevy::prelude` and adds the following methods to `EntityCommands`: - `set_parent_in_place`: Change the parent of an entity and update its `Transform` in order to preserve its `GlobalTransform` after the parent change - `remove_parent_in_place`: Remove an entity from a hierarchy, while preserving its `GlobalTransform`. Co-authored-by: Nicola Papale commit ba3069f008c7384cbc3179991699ceb38a6f3a8f Author: François Date: Thu Jan 12 17:15:20 2023 +0000 Change default FocusPolicy to Pass (#7161) # Objective - While building UI, it makes more sense for most nodes to have a `FocusPolicy` of `Pass`, so that user interaction can correctly bubble - Only `ButtonBundle` blocks by default This change means that for someone adding children to a button, it's not needed to change the focus policy of those children to `Pass` for the button to continue to work. --- ## Changelog - `FocusPolicy` default has changed from `FocusPolicy::Block` to `FocusPolicy::Pass` ## Migration Guide - `FocusPolicy` default has changed from `FocusPolicy::Block` to `FocusPolicy::Pass` commit 76de9f940786698ec7a33ecbfe916acbc9e2f33b Author: Kurt Kühnert Date: Thu Jan 12 15:11:58 2023 +0000 Improve render phase documentation (#7016) # Objective The documentation of the bevy_render crate is still pretty incomplete. This PR follows up on #6885 and improves the documentation of the `render_phase` module. This module contains one of our most important rendering abstractions and the current documentation is pretty confusing. This PR tries to clarify what all of these pieces are for and how they work together to form bevy`s modular rendering logic. ## Solution ### Code Reformating - I have moved the `rangefinder` into the `render_phase` module since it is only used there. - I have moved the `PhaseItem` (and the `BatchedPhaseItem`) from `render_phase::draw` over to `render_phase::mod`. This does not change the public-facing API since they are reexported anyway, but this change makes the relation between `RenderPhase` and `PhaseItem` clear and easier to discover. ### Documentation - revised all documentation in the `render_phase` module - added a module-level explanation of how `RenderPhase`s, `RenderPass`es, `PhaseItem`s, `Draw` functions, and `RenderCommands` relate to each other and how they are used --- ## Changelog - The `rangefinder` module has been moved into the `render_phase` module. ## Migration Guide - The `rangefinder` module has been moved into the `render_phase` module. ```rust //old use bevy::render::rangefinder::*; // new use bevy::render::render_phase::rangefinder::*; ``` commit f4920bbd6d2d31c1fb25a359c2aff71eefe2620a Author: James Liu Date: Wed Jan 11 23:31:22 2023 +0000 Mark TableRow and TableId as repr(transparent) (#7166) # Objective Following #6681, both `TableRow` and `TableId` are now part of `EntityLocation`. However, the safety invariant on `EntityLocation` requires that all of the constituent fields are `repr(transprent)` or `repr(C)` and the bit pattern of all 1s must be valid. This is not true for `TableRow` and `TableId` currently. ## Solution Mark `TableRow` and `TableId` to satisfy the safety requirement. Add safety comments on `ArchetypeId`, `ArchetypeRow`, `TableId` and `TableRow`. commit dfc4f05c87bba6c54ae8bc8d70c32f4dec4fefba Author: James Liu Date: Wed Jan 11 23:12:20 2023 +0000 Ensure Ptr/PtrMut/OwningPtr are aligned when casting in debug builds (#7117) # Objective Improve safety testing when using `bevy_ptr` types. This is a follow-up to #7113. ## Solution Add a debug-only assertion that pointers are aligned when casting to a concrete type. This should very quickly catch any unsoundness from unaligned pointers, even without miri. However, this can have a large negative perf impact on debug builds. --- ## Changelog Added: `Ptr::deref` will now panic in debug builds if the pointer is not aligned. Added: `PtrMut::deref_mut` will now panic in debug builds if the pointer is not aligned. Added: `OwningPtr::read` will now panic in debug builds if the pointer is not aligned. Added: `OwningPtr::drop_as` will now panic in debug builds if the pointer is not aligned. commit 60be8759e37e00131d01b649e288c599652aff0d Author: François Date: Wed Jan 11 21:12:02 2023 +0000 add helper for macro to get either bevy::x or bevy_x depending on how it was imported (#7164) # Objective - It can be useful for third party crates to work independently on how bevy is imported ## Solution - Expose an helper to get a subcrate path for macros commit 7783393c56f183bcd3b5506906b84d3014594b55 Author: 张林伟 Date: Wed Jan 11 21:12:01 2023 +0000 Expose transform propagate systems (#7145) # Objective - I tried to create a fork of bevy_rapier to track latest bevy main branch. But bevy_rapier depends on bevy internal `propagate_transforms` system (see https://github.com/dimforge/bevy_rapier/blob/master/src/plugin/plugin.rs#L64). - `propagate_transforms` system was changed to private in https://github.com/bevyengine/bevy/pull/4775. I don't know if it's reasonable that making `propagate_transforms` public. I also created an issue to bevy_rapier https://github.com/dimforge/bevy_rapier/issues/307 to see how offical team will solve this issue. ## Solution - make `propagate_transforms` system public. commit aa3dd14badcb0e1f170614bce35040ef22a234d1 Author: François Date: Wed Jan 11 20:52:04 2023 +0000 gate an import used only for a debug assert (#7165) # Objective - There is a warning when building in release: ``` warning: unused import: `bevy_ecs::system::Local` --> crates/bevy_render/src/extract_resource.rs:5:5 | 5 | use bevy_ecs::system::Local; | ^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default ``` - It's used https://github.com/bevyengine/bevy/blob/59751d6e33d94eff6e1fc20c9ae155974b3860b1/crates/bevy_render/src/extract_resource.rs#L47 - Fix it ## Solution - Gate the import - repeat of #5320 commit 59751d6e33d94eff6e1fc20c9ae155974b3860b1 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Wed Jan 11 17:47:54 2023 +0000 Add a method for converting `MutUntyped` -> `Mut` (#7113) # Objective `MutUntyped` is a struct that stores a `PtrMut` alongside change tick metadata. Working with this type is cumbersome, and has few benefits over storing the pointer and change ticks separately. Related: #6430 (title is out of date) ## Solution Add a convenience method for transforming an untyped change detection pointer into its typed counterpart. --- ## Changelog - Added the method `MutUntyped::with_type`. commit 15ee98db8d1c6705111e0f11a8fc240ceaf9f2db Author: Guillaume Gomez Date: Wed Jan 11 17:01:11 2023 +0000 Add "transparent" doc alias for Color::NONE (#7160) As mentioned in https://github.com/bevyengine/bevy/pull/6530. It allows to not create a new constant and simply having it to show up in the documentation when someone is looking for "transparent" (case insensitive) in rustdoc search. cc @alice-i-cecile commit 6cc01c144947b54392a71a261651c6fe11916eea Author: Gino Valente Date: Wed Jan 11 16:46:27 2023 +0000 bevy_reflect: Add simple enum support to reflection paths (#6560) # Objective Enums are now reflectable, but are not accessible via reflection paths. This would allow us to do things like: ```rust #[derive(Reflect)] struct MyStruct { data: MyEnum } #[derive(Reflect)] struct MyEnum { Foo(u32, u32), Bar(bool) } let x = MyStruct { data: MyEnum::Foo(123), }; assert_eq!(*x.get_path::("data.1").unwrap(), 123); ``` ## Solution Added support for enums in reflection paths. ##### Note This uses a simple approach of just getting the field with the given accessor. It does not do matching or anything else to ensure the enum is the intended variant. This means that the variant must be known ahead of time or matched outside the reflection path (i.e. path to variant, perform manual match, and continue pathing). --- ## Changelog - Added support for enums in reflection paths commit 229d6c686fae19ff45b7bddfe9f853096b56f211 Author: Gino Valente Date: Wed Jan 11 16:25:37 2023 +0000 bevy_reflect: Simplify `take`-or-else-`from_reflect` operation (#6566) # Objective There are times where we want to simply take an owned `dyn Reflect` and cast it to a type `T`. Currently, this involves doing: ```rust let value = value.take::().unwrap_or_else(|value| { T::from_reflect(&*value).unwrap_or_else(|| { panic!( "expected value of type {} to convert to type {}.", value.type_name(), std::any::type_name::() ) }) }); ``` This is a common operation that could be easily be simplified. ## Solution Add the `FromReflect::take_from_reflect` method. This first tries to `take` the value, calling `from_reflect` iff that fails. ```rust let value = T::take_from_reflect(value).unwrap_or_else(|value| { panic!( "expected value of type {} to convert to type {}.", value.type_name(), std::any::type_name::() ) }); ``` Based on suggestion from @soqb on [Discord](https://discord.com/channels/691052431525675048/1002362493634629796/1041046880316043374). --- ## Changelog - Add `FromReflect::take_from_reflect` method commit 9dd8fbc570fb08eca9301ac4cd47844624eed22a Author: Joshua Chapman Date: Wed Jan 11 15:41:54 2023 +0000 Added Ref to allow immutable access with change detection (#7097) # Objective - Fixes #7066 ## Solution - Split the ChangeDetection trait into ChangeDetection and ChangeDetectionMut - Added Ref as equivalent to &T with change detection --- ## Changelog - Support for Ref which allow inspecting change detection flags in an immutable way ## Migration Guide - While bevy prelude includes both ChangeDetection and ChangeDetectionMut any code explicitly referencing ChangeDetection might need to be updated to ChangeDetectionMut or both. Specifically any reading logic requires ChangeDetection while writes requires ChangeDetectionMut. use bevy_ecs::change_detection::DetectChanges -> use bevy_ecs::change_detection::{DetectChanges, DetectChangesMut} - Previously Res had methods to access change detection `is_changed` and `is_added` those methods have been moved to the `DetectChanges` trait. If you are including bevy prelude you will have access to these types otherwise you will need to `use bevy_ecs::change_detection::DetectChanges` to continue using them. commit 0d2cdb450d49bc8abea5e0b46275288efc453f3c Author: 张林伟 Date: Wed Jan 11 09:51:22 2023 +0000 Fix beta clippy lints (#7154) # Objective - When I run `cargo run -p ci` for my pr locally using latest beta toolchain, the ci failed due to [uninlined_format_args](https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args) and [needless_lifetimes](https://rust-lang.github.io/rust-clippy/master/index.html#needless_lifetimes) lints ## Solution - Fix lints according to clippy suggestions. commit bb79903938691220641a9afcdb73b9bce7ca522b Author: 张林伟 Date: Wed Jan 11 09:32:07 2023 +0000 Fix clippy issue for benches crate (#6806) # Objective - https://github.com/bevyengine/bevy/pull/3505 marked `S-Adopt-Me` , this pr is to continue his work. ## Solution - run `cargo clippy --workspace --all-targets --all-features -- -Aclippy::type_complexity -Wclippy::doc_markdown -Wclippy::redundant_else -Wclippy::match_same_arms -Wclippy::semicolon_if_nothing_returned -Wclippy::explicit_iter_loop -Wclippy::map_flatten -Dwarnings` under benches dir. - fix issue according to suggestion. commit 512f376fc1f88a696849cd06586bccc85441bb2e Author: Boxy Date: Tue Jan 10 23:12:52 2023 +0000 Document alignment requirements of `Ptr`, `PtrMut` and `OwningPtr` (#7151) # Objective The types in the `bevy_ptr` accidentally did not document anything relating to alignment. This is unsound as many methods rely on the pointer being correctly aligned. ## Solution This PR introduces new safety invariants on the `$ptr::new`, `$ptr::byte_offset` and `$ptr::byte_add` methods requiring them to keep the pointer aligned. This is consistent with the documentation of these pointer types which document them as being "type erased borrows". As it was pointed out (by @JoJoJet in #7117) that working with unaligned pointers can be useful (for example our commands abstraction which does not try to align anything properly, see #7039) this PR also introduces a default type parameter to all the pointer types that specifies whether it has alignment requirements or not. I could not find any code in `bevy_ecs` that would need unaligned pointers right now so this is going unused. --- ## Changelog - Correctly document alignment requirements on `bevy_ptr` types. - Support variants of `bevy_ptr` types that do not require being correctly aligned for the pointee type. ## Migration Guide - Safety invariants on `bevy_ptr` types' `new` `byte_add` and `byte_offset` methods have been changed. All callers should re-audit for soundness. commit a13b6f8a054c0d6d21de5cb6dd2ed7705c9bea92 Author: Mike Date: Tue Jan 10 22:32:42 2023 +0000 Thread executor for running tasks on specific threads. (#7087) # Objective - Spawn tasks from other threads onto an async executor, but limit those tasks to run on a specific thread. - This is a continuation of trying to break up some of the changes in pipelined rendering. - Eventually this will be used to allow `NonSend` systems to run on the main thread in pipelined rendering #6503 and also to solve #6552. - For this specific PR this allows for us to store a thread executor in a thread local, rather than recreating a scope executor for every scope which should save on a little work. ## Solution - We create a Executor that does a runtime check for what thread it's on before creating a !Send ticker. The ticker is the only way for the executor to make progress. --- ## Changelog - create a ThreadExecutor that can only be ticked on one thread. commit d4babafe81afb20f1d8d3665c1f04440dba90628 Author: Boxy Date: Tue Jan 10 18:55:23 2023 +0000 Make `Query` fields private (#7149) `Query`'s fields being `pub(crate)` means that the struct can be constructed via safe code from anywhere in `bevy_ecs` . This is Not Good since it is intended that all construction of this type goes through `Query::new` which is an `unsafe fn` letting various `Query` methods rely on those invariants holding even though they can be trivially bypassed. This has no user facing impact commit 3600c5a340bb4d3a7fae0c463e01d6c0028ccf97 Author: Nicola Papale Date: Tue Jan 10 18:55:22 2023 +0000 Remove the `GlobalTransform::translation_mut` method (#7134) # Objective It is possible to manually update `GlobalTransform`. The engine actually assumes this is not possible. For example, `propagate_transform` does not update children of an `Entity` which **`GlobalTransform`** changed, leading to unexpected behaviors. A `GlobalTransform` set by the user may also be blindly overwritten by the propagation system. ## Solution - Remove `translation_mut` - Explain to users that they shouldn't manually update the `GlobalTransform` - Remove `global_vs_local.rs` example, since it misleads users in believing that it is a valid use-case to manually update the `GlobalTransform` --- ## Changelog - Remove `GlobalTransform::translation_mut` ## Migration Guide `GlobalTransform::translation_mut` has been removed without alternative, if you were relying on this, update the `Transform` instead. If the given entity had children or parent, you may need to remove its parent to make its transform independent (in which case the new `Commands::set_parent_in_place` and `Commands::remove_parent_in_place` may be of interest) Bevy may add in the future a way to toggle transform propagation on an entity basis. commit fa40e2badb1dbd0ffa8a78ef9068e68abaf70493 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Tue Jan 10 18:41:50 2023 +0000 Fix a miscompilation with `#[derive(SystemParam)]` (#7105) # Objective - Fix #7103. - The issue is caused because I forgot to add a where clause to a generated struct in #7056. ## Solution - Add the where clause. commit a2071783446e85db716db4bb0068b03444d3825a Author: Tirth Patel Date: Tue Jan 10 17:48:34 2023 +0000 Add wrapping_add to change_tick (#7146) # Objective Fixes #7140 ## Solution As discussed in the issue, added wrapping_add --- commit d03c1a06874f95bba1127b9aa2fd7a4d0b3eb723 Author: Boxy Date: Tue Jan 10 17:25:45 2023 +0000 Ensure `Query` does not use the wrong `World` (#7150) `Query` relies on the `World` it stores being the same as the world used for creating the `QueryState` it stores. If they are not the same then everything is very unsound. This was not actually being checked anywhere, `Query::new` did not have a safety invariant or even an assertion that the `WorldId`'s are the same. This shouldn't have any user facing impact unless we have really messed up in bevy and have unsoundness elsewhere (in which case we would now get a panic instead of being unsound). commit aaaf357dbb352232810a02c7bbd0090109eaea53 Author: zeroacez Date: Tue Jan 10 17:25:44 2023 +0000 Added docs for ``.apply()``in basic usage of ``systemState`` (#7138) # Objective Fixes #5940 ## Solution Added the suggested comment. Co-authored-by: zeroacez <43633834+zeroacez@users.noreply.github.com> commit e4d54739e7539a45e9c47157ef3a6f27f0f32a5c Author: Mike Date: Tue Jan 10 17:07:27 2023 +0000 add link to tracy compatibility table (#7144) # Objective - Fixes https://github.com/bevyengine/bevy/issues/5200 commit 9adc8cdaf6d3ae9249654d80f44fc6b327f721ba Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Mon Jan 9 22:20:10 2023 +0000 Add `Mut::reborrow` (#7114) # Objective - In some cases, you need a `Mut` pointer, but you only have a mutable reference to one. There is no easy way of converting `&'a mut Mut<'_, T>` -> `Mut<'a, T>` outside of the engine. ### Example (Before) ```rust fn do_with_mut(val: Mut) { ... } for x: Mut in &mut query { // The function expects a `Mut`, so `x` gets moved here. do_with_mut(x); // Error: use of moved value. do_a_thing(&x); } ``` ## Solution - Add the function `reborrow`, which performs the mapping. This is analogous to `PtrMut::reborrow`. ### Example (After) ```rust fn do_with_mut(val: Mut) { ... } for x: Mut in &mut query { // We reborrow `x`, so the original does not get moved. do_with_mut(x.reborrow()); // Works fine. do_a_thing(&x); } ``` --- ## Changelog - Added the method `reborrow` to `Mut`, `ResMut`, `NonSendMut`, and `MutUntyped`. commit 871c80c103f977425b28e3ae31e565785cffa4be Author: Giacomo Stevanato Date: Mon Jan 9 21:57:14 2023 +0000 Add `TypeRegistrationDeserializer` and remove `BorrowedStr` (#7094) # Objective This a follow-up to #6894, see https://github.com/bevyengine/bevy/pull/6894#discussion_r1045203113 The goal is to avoid cloning any string when getting a `&TypeRegistration` corresponding to a string which is being deserialized. As a bonus code duplication is also reduced. ## Solution The manual deserialization of a string and lookup into the type registry has been moved into a separate `TypeRegistrationDeserializer` type, which implements `DeserializeSeed` with a `Visitor` that accepts any string with `visit_str`, even ones that may not live longer than that function call. `BorrowedStr` has been removed since it's no longer used. --- ## Changelog - The type `TypeRegistrationDeserializer` has been added, which simplifies getting a `&TypeRegistration` while deserializing a string. commit 9be47e3328b729e31b80ab9d23e5331fc7079bf8 Author: François Date: Mon Jan 9 21:43:30 2023 +0000 Fix overflow scaling for images (#7142) # Objective - Fixes #4057 - Do not multiply position by scale factor commit 76a4695f334aed1504777686950452579363ebbb Author: 2ne1ugly <47616772+2ne1ugly@users.noreply.github.com> Date: Mon Jan 9 21:43:29 2023 +0000 Fix doc in `App::add_sub_app` (#7139) # Objective - Fix the name of function parameter name in docs ## Solution - Change `f` to `sub_app_runner` --- It confused me a bit when I was reading the docs in the autocomplete hint. Hesitated about filing a PR since it's just a one single word change in the comment. Is this the right process to change these docs? commit 0e9f80e00b513fd7cb17bf971a970d23f902bde4 Author: 2ne1ugly <47616772+2ne1ugly@users.noreply.github.com> Date: Mon Jan 9 21:43:27 2023 +0000 Implement `SparseSetIndex` for `WorldId` (#7125) # Objective - Fixes #7124 ## Solution - Add Hash Derive on `WorldId` - Add `SparseSetIndex` impl commit 7df680bb0a2e3da35b27160b0e52b0e817c289a4 Author: François Date: Mon Jan 9 21:19:48 2023 +0000 add rust-version for MSRV and CI job to check (#6852) # Objective - Fixes #6777, fixes #2998, replaces #5518 - Help avoid confusing error message when using an older version of Rust ## Solution - Add the `rust-version` field to `Cargo.toml` - Add a CI job checking the MSRV - Add the job to bors commit afe0a0650bdf907cf1826a9de20d4a728b9ef7f4 Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Mon Jan 9 20:56:06 2023 +0000 Relax `Sync` bound on `Local as ExclusiveSystemParam` (#7040) # Objective The type `Local` unnecessarily has the bound `T: Sync` when the local is used in an exclusive system. ## Solution Lift the bound. --- ## Changelog Removed the bound `T: Sync` from `Local` when used as an `ExclusiveSystemParam`. commit aaf384ae589a88ee2b842fd2050f68bf7e77c6df Author: James Liu Date: Mon Jan 9 20:40:34 2023 +0000 Panic on dropping NonSend in non-origin thread. (#6534) # Objective Fixes #3310. Fixes #6282. Fixes #6278. Fixes #3666. ## Solution Split out `!Send` resources into `NonSendResources`. Add a `origin_thread_id` to all `!Send` Resources, check it on dropping `NonSendResourceData`, if there's a mismatch, panic. Moved all of the checks that `MainThreadValidator` would do into `NonSendResources` instead. All `!Send` resources now individually track which thread they were inserted from. This is validated against for every access, mutation, and drop that could be done against the value. A regression test using an altered version of the example from #3310 has been added. This is a stopgap solution for the current status quo. A full solution may involve fully removing `!Send` resources/components from `World`, which will likely require a much more thorough design on how to handle the existing in-engine and ecosystem use cases. This PR also introduces another breaking change: ```rust use bevy_ecs::prelude::*; #[derive(Resource)] struct Resource(u32); fn main() { let mut world = World::new(); world.insert_resource(Resource(1)); world.insert_non_send_resource(Resource(2)); let res = world.get_resource_mut::().unwrap(); assert_eq!(res.0, 2); } ``` This code will run correctly on 0.9.1 but not with this PR, since NonSend resources and normal resources have become actual distinct concepts storage wise. ## Changelog Changed: Fix soundness bug with `World: Send`. Dropping a `World` that contains a `!Send` resource on the wrong thread will now panic. ## Migration Guide Normal resources and `NonSend` resources no longer share the same backing storage. If `R: Resource`, then `NonSend` and `Res` will return different instances from each other. If you are using both `Res` and `NonSend` (or their mutable variants), to fetch the same resources, it's strongly advised to use `Res`. commit 1b9c156479c60c2b791081f4b5d7db5edf38726b Author: radiish Date: Mon Jan 9 19:47:07 2023 +0000 reflect: add `insert` and `remove` methods to `List` (#7063) # Objective - Fixes #7061 ## Solution - Add and implement `insert` and `remove` methods for `List`. --- ## Changelog - Added `insert` and `remove` methods to `List`. - Changed the `push` and `pop` methods on `List` to have default implementations. ## Migration Guide - Manual implementors of `List` need to implement the new methods `insert` and `remove` and consider whether to use the new default implementation of `push` and `pop`. Co-authored-by: radiish commit bef9bc18449936a112b74d60395b46109cfefed9 Author: James Liu Date: Mon Jan 9 19:24:56 2023 +0000 Reduce branching in TrackedRenderPass (#7053) # Objective Speed up the render phase for rendering. ## Solution - Follow up #6988 and make the internals of atomic IDs `NonZeroU32`. This niches the `Option`s of the IDs in draw state, which reduces the size and branching behavior when evaluating for equality. - Require `&RenderDevice` to get the device's `Limits` when initializing a `TrackedRenderPass` to preallocate the bind groups and vertex buffer state in `DrawState`, this removes the branch on needing to resize those `Vec`s. ## Performance This produces a similar speed up akin to that of #6885. This shows an approximate 6% speed up in `main_opaque_pass_3d` on `many_foxes` (408.79 us -> 388us). This should be orthogonal to the gains seen there. ![image](https://user-images.githubusercontent.com/3137680/209906239-e430f026-63c2-4b95-957e-a2045b810d79.png) --- ## Changelog Added: `RenderContext::begin_tracked_render_pass`. Changed: `TrackedRenderPass` now requires a `&RenderDevice` on construction. Removed: `bevy_render::render_phase::DrawState`. It was not usable in any form outside of `bevy_render`. ## Migration Guide TODO commit d76b53bf4db552d505c4d319f5cf565579635002 Author: Mike Date: Mon Jan 9 19:24:54 2023 +0000 Separate Extract from Sub App Schedule (#7046) # Objective - This pulls out some of the changes to Plugin setup and sub apps from #6503 to make that PR easier to review. - Separate the extract stage from running the sub app's schedule to allow for them to be run on separate threads in the future - Fixes #6990 ## Solution - add a run method to `SubApp` that runs the schedule - change the name of `sub_app_runner` to extract to make it clear that this function is only for extracting data between the main app and the sub app - remove the extract stage from the sub app schedule so it can be run separately. This is done by adding a `setup` method to the `Plugin` trait that runs after all plugin build methods run. This is required to allow the extract stage to be removed from the schedule after all the plugins have added their systems to the stage. We will also need the setup method for pipelined rendering to setup the render thread. See https://github.com/bevyengine/bevy/blob/e3267965e15f14be18eec942dcaf16807144eb05/crates/bevy_render/src/pipelined_rendering.rs#L57-L98 ## Changelog - Separate SubApp Extract stage from running the sub app schedule. ## Migration Guide ### SubApp `runner` has conceptually been changed to an `extract` function. The `runner` no longer is in charge of running the sub app schedule. It's only concern is now moving data between the main world and the sub app. The `sub_app.app.schedule` is now run for you after the provided function is called. ```rust // before fn main() { let sub_app = App::empty(); sub_app.add_stage(MyStage, SystemStage::parallel()); App::new().add_sub_app(MySubApp, sub_app, move |main_world, sub_app| { extract(app_world, render_app); render_app.app.schedule.run(); }); } // after fn main() { let sub_app = App::empty(); sub_app.add_stage(MyStage, SystemStage::parallel()); App::new().add_sub_app(MySubApp, sub_app, move |main_world, sub_app| { extract(app_world, render_app); // schedule is automatically called for you after extract is run }); } ``` commit e94215c4c6d8d7fbd4632bf957d1abe1c2424999 Author: DevinLeamy Date: Mon Jan 9 19:24:52 2023 +0000 Gamepad events refactor (#6965) # Objective - Remove redundant gamepad events - Simplify consuming gamepad events. - Refactor: Separate handling of gamepad events into multiple systems. ## Solution - Removed `GamepadEventRaw`, and `GamepadEventType`. - Added bespoke `GamepadConnectionEvent`, `GamepadAxisChangedEvent`, and `GamepadButtonChangedEvent`. - Refactored `gamepad_event_system`. - Added `gamepad_button_event_system`, `gamepad_axis_event_system`, and `gamepad_connection_system`, which update the `Input` and `Axis` resources using their corresponding event type. Gamepad events are now handled in their own systems and have their own types. This allows for querying for gamepad events without having to match on `GamepadEventType` and makes creating handlers for specific gamepad event types, like a `GamepadConnectionEvent` or `GamepadButtonChangedEvent` possible. We remove `GamepadEventRaw` by filtering the gamepad events, using `GamepadSettings`, _at the source_, in `bevy_gilrs`. This way we can create `GamepadEvent`s directly and avoid creating `GamepadEventRaw` which do not pass the user defined filters. We expose ordered `GamepadEvent`s and we can respond to individual gamepad event types. ## Migration Guide - Replace `GamepadEvent` and `GamepadEventRaw` types with their specific gamepad event type. commit fa15b319309c0e7fc66a8cf9394f34e7ae3f6cf9 Author: Sebastian Meßmer Date: Mon Jan 9 19:24:51 2023 +0000 Smooth Transition between Animations (#6922) # Objective - Fixes https://github.com/bevyengine/bevy/discussions/6338 This PR allows for smooth transitions between different animations. ## Solution - This PR uses very simple linear blending of animations. - When starting a new animation, you can give it a duration, and throughout that duration, the previous and the new animation are being linearly blended, until only the new animation is running. - I'm aware of https://github.com/bevyengine/rfcs/pull/49 and https://github.com/bevyengine/rfcs/pull/51, which are more complete solutions to this problem, but they seem still far from being implemented. Until they're ready, this PR allows for the most basic use case of blending, i.e. smoothly transitioning between different animations. ## Migration Guide - no bc breaking changes commit a41e869aa9e410b37d88d1c01a651b78c4ed3250 Author: Yyee Date: Mon Jan 9 19:05:30 2023 +0000 Expose symphonia features from rodio in bevy_audio and bevy (#6388) # Objective Fix #6301 ## Solution Add new features in `bevy_audio` to use `symphonia` sound format from `rodio` Also add in `bevy` commit ee4e98f8a98e1f528065ddaa4a87394715a4c339 Author: IceSentry Date: Mon Jan 9 18:50:55 2023 +0000 Support storage buffers in derive `AsBindGroup` (#6129) # Objective - Storage buffers are useful and not currently supported by the `AsBindGroup` derive which means you need to expand the macro if you need a storage buffer ## Solution - Add a new `#[storage]` attribute to the derive `AsBindGroup` macro. - Support and optional `read_only` parameter that defaults to false when not present. - Support visibility parameters like the texture and sampler attributes. --- ## Changelog - Add a new `#[storage(index)]` attribute to the derive `AsBindGroup` macro. Co-authored-by: IceSentry commit 16748b838793487b4251f4ab177ffda71ab128b5 Author: Robert Swain Date: Mon Jan 9 13:41:59 2023 +0000 bevy_render: Run calculate_bounds in the end-of-update exclusive systems (#7127) # Objective - Avoid slower than necessary first frame after spawning many entities due to them not having `Aabb`s and so being marked visible - Avoids unnecessarily large system and VRAM allocations as a consequence ## Solution - I noticed when debugging the `many_cubes` stress test in Xcode that the `MeshUniform` binding was much larger than it needed to be. I realised that this was because initially, all mesh entities are marked as being visible because they don't have `Aabb`s because `calculate_bounds` is being run in `PostUpdate` and there are no system commands applications before executing the visibility check systems that need the `Aabb`s. The solution then is to run the `calculate_bounds` system just before the previous system commands are applied which is at the end of the `Update` stage. commit 1efdbb7e3ea2c7226385eb457123322430891b1d Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Sat Jan 7 23:20:32 2023 +0000 Remove the `SystemParamState` trait and remove types like `ResState` (#6919) Spiritual successor to #5205. Actual successor to #6865. # Objective Currently, system params are defined using three traits: `SystemParam`, `ReadOnlySystemParam`, `SystemParamState`. The behavior for each param is specified by the `SystemParamState` trait, while `SystemParam` simply defers to the state. Splitting the traits in this way makes it easier to implement within macros, but it increases the cognitive load. Worst of all, this approach requires each `MySystemParam` to have a public `MySystemParamState` type associated with it. ## Solution * Merge the trait `SystemParamState` into `SystemParam`. * Remove all trivial `SystemParam` state types. * `OptionNonSendMutState`: you will not be missed. --- - [x] Fix/resolve the remaining test failure. ## Changelog * Removed the trait `SystemParamState`, merging its functionality into `SystemParam`. ## Migration Guide **Note**: this should replace the migration guide for #6865. This is relative to Bevy 0.9, not main. The traits `SystemParamState` and `SystemParamFetch` have been removed, and their functionality has been transferred to `SystemParam`. ```rust // Before (0.9) impl SystemParam for MyParam<'_, '_> { type State = MyParamState; } unsafe impl SystemParamState for MyParamState { fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { ... } } unsafe impl<'w, 's> SystemParamFetch<'w, 's> for MyParamState { type Item = MyParam<'w, 's>; fn get_param(&mut self, ...) -> Self::Item; } unsafe impl ReadOnlySystemParamFetch for MyParamState { } // After (0.10) unsafe impl SystemParam for MyParam<'_, '_> { type State = MyParamState; type Item<'w, 's> = MyParam<'w, 's>; fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { ... } fn get_param<'w, 's>(state: &mut Self::State, ...) -> Self::Item<'w, 's>; } unsafe impl ReadOnlySystemParam for MyParam<'_, '_> { } ``` The trait `ReadOnlySystemParamFetch` has been replaced with `ReadOnlySystemParam`. ```rust // Before unsafe impl ReadOnlySystemParamFetch for MyParamState {} // After unsafe impl ReadOnlySystemParam for MyParam<'_, '_> {} ``` commit 076e6f780cdc523d7b2e5bea03aa093227bceb1c Author: JoJoJet <21144246+JoJoJet@users.noreply.github.com> Date: Fri Jan 6 23:24:25 2023 +0000 Update an outdated example for `Mut::map_unchanged` (#7115) # Objective - The doctest for `Mut::map_unchanged` uses a fake function `set_if_not_equal` to demonstrate usage. - Now that #6853 has been merged, we can use `Mut::set_if_neq` directly instead of mocking it. commit 41a5c30fb713a16c9b3f1a8eca10b0ff869a52ff Author: 1e1001 Date: Fri Jan 6 18:00:22 2023 +0000 add `Axis::devices` to get all the input devices (#5400) (github made me type out a message for the commit which looked like it was for the pr, sorry) # Objective - Add a way to get all of the input devices of an `Axis`, primarily useful for looping through them ## Solution - Adds `Axis::devices()` which returns a `FixedSizeIterator` - Adds a (probably unneeded) `test_axis_devices` test because tests are cool. --- ## Changelog - Added `Axis::devices()` method ## Migration Guide Not a breaking change. commit ebc5cb352d781fd51f14e588ee8045c03f2d2f04 Author: A-Walrus Date: Fri Jan 6 17:46:44 2023 +0000 Fix doc comment "Turbo" -> "Extreme" (#7091) # Objective Doc comment mentions turbo which is a sensitivity that doesn't exist. ## Solution Change the comment to "Extreme" which does exist commit 653c062ba325535c652f8e5bbe912052e5688ab4 Author: iiYese Date: Fri Jan 6 15:40:10 2023 +0000 Added missing details to SystemParam Local documentation. (#7106) # Objective `SystemParam` `Local`s documentation currently leaves out information that should be documented. - What happens when multiple `SystemParam`s within the same system have the same `Local` type. - What lifetime parameter is expected by `Local`. ## Solution - Added sentences to documentation to communicate this information. - Renamed `Local` lifetimes in code to `'s` where they previously were not. Users can get complicated incorrect suggested fixes if they pass the wrong lifetime. Some instance of the code had `'w` indicating the expected lifetime might not have been known to those that wrote the code either. Co-authored-by: iiYese <83026177+iiYese@users.noreply.github.com> commit 3dd8b42f7287340913055db34db5606c1720b9d5 Author: Rob Parrett Date: Fri Jan 6 00:43:30 2023 +0000 Fix various typos (#7096) I stumbled across a typo in some docs. Fixed some more while I was in there. --- .github/bors.toml | 1 + .github/workflows/ci.yml | 26 + CONTRIBUTING.md | 128 +- Cargo.toml | 60 +- assets/shaders/show_prepass.wgsl | 26 + .../bevy_ecs/components/archetype_updates.rs | 2 +- .../bevy_ecs/iteration/heavy_compute.rs | 2 +- .../benches/bevy_ecs/scheduling/schedule.rs | 8 +- benches/benches/bevy_ecs/scheduling/stages.rs | 6 +- benches/benches/bevy_ecs/world/commands.rs | 9 +- benches/benches/bevy_ecs/world/world_get.rs | 2 +- benches/benches/bevy_reflect/list.rs | 4 +- benches/benches/bevy_reflect/map.rs | 10 +- benches/benches/bevy_tasks/iter.rs | 12 +- crates/bevy_animation/src/lib.rs | 248 ++- crates/bevy_app/src/app.rs | 133 +- crates/bevy_app/src/plugin.rs | 11 +- crates/bevy_asset/src/asset_server.rs | 4 +- crates/bevy_asset/src/assets.rs | 4 +- crates/bevy_asset/src/debug_asset_server.rs | 4 +- crates/bevy_asset/src/io/file_asset_io.rs | 8 +- crates/bevy_asset/src/reflect.rs | 6 +- crates/bevy_audio/Cargo.toml | 7 + crates/bevy_audio/src/audio_source.rs | 36 +- crates/bevy_audio/src/lib.rs | 15 +- .../bevy_core_pipeline/src/bloom/bloom.wgsl | 6 +- crates/bevy_core_pipeline/src/bloom/mod.rs | 114 +- .../src/core_2d/main_pass_2d_node.rs | 13 +- .../src/core_3d/main_pass_3d_node.rs | 70 +- crates/bevy_core_pipeline/src/core_3d/mod.rs | 89 +- crates/bevy_core_pipeline/src/fxaa/mod.rs | 7 +- crates/bevy_core_pipeline/src/fxaa/node.rs | 6 +- crates/bevy_core_pipeline/src/lib.rs | 4 + crates/bevy_core_pipeline/src/prepass/mod.rs | 147 ++ crates/bevy_core_pipeline/src/prepass/node.rs | 134 ++ .../bevy_core_pipeline/src/tonemapping/mod.rs | 4 +- .../src/tonemapping/node.rs | 6 +- .../bevy_core_pipeline/src/upscaling/mod.rs | 4 +- .../bevy_core_pipeline/src/upscaling/node.rs | 6 +- crates/bevy_diagnostic/src/diagnostic.rs | 2 +- crates/bevy_ecs/Cargo.toml | 2 +- crates/bevy_ecs/macros/src/component.rs | 3 +- crates/bevy_ecs/macros/src/fetch.rs | 6 +- crates/bevy_ecs/macros/src/lib.rs | 127 +- crates/bevy_ecs/src/archetype.rs | 6 +- crates/bevy_ecs/src/bundle.rs | 3 +- crates/bevy_ecs/src/change_detection.rs | 349 +++- crates/bevy_ecs/src/component.rs | 27 + crates/bevy_ecs/src/entity/mod.rs | 4 +- crates/bevy_ecs/src/lib.rs | 52 +- crates/bevy_ecs/src/query/fetch.rs | 177 +- crates/bevy_ecs/src/query/mod.rs | 4 +- crates/bevy_ecs/src/query/par_iter.rs | 202 +++ crates/bevy_ecs/src/query/state.rs | 95 +- .../src/schedule/ambiguity_detection.rs | 4 + .../src/schedule/executor_parallel.rs | 84 +- crates/bevy_ecs/src/schedule/mod.rs | 18 +- crates/bevy_ecs/src/schedule/stage.rs | 13 +- crates/bevy_ecs/src/schedule_v3/condition.rs | 97 ++ crates/bevy_ecs/src/schedule_v3/config.rs | 649 ++++++++ .../bevy_ecs/src/schedule_v3/executor/mod.rs | 90 ++ .../schedule_v3/executor/multi_threaded.rs | 575 +++++++ .../src/schedule_v3/executor/simple.rs | 111 ++ .../schedule_v3/executor/single_threaded.rs | 137 ++ .../bevy_ecs/src/schedule_v3/graph_utils.rs | 233 +++ crates/bevy_ecs/src/schedule_v3/migration.rs | 38 + crates/bevy_ecs/src/schedule_v3/mod.rs | 622 ++++++++ crates/bevy_ecs/src/schedule_v3/schedule.rs | 1099 +++++++++++++ crates/bevy_ecs/src/schedule_v3/set.rs | 149 ++ crates/bevy_ecs/src/schedule_v3/state.rs | 64 + crates/bevy_ecs/src/storage/blob_vec.rs | 110 +- crates/bevy_ecs/src/storage/mod.rs | 3 +- crates/bevy_ecs/src/storage/resource.rs | 173 +- crates/bevy_ecs/src/storage/sparse_set.rs | 6 + crates/bevy_ecs/src/storage/table.rs | 8 +- .../src/system/commands/command_queue.rs | 94 +- crates/bevy_ecs/src/system/commands/mod.rs | 3 +- .../src/system/commands/parallel_scope.rs | 25 +- .../src/system/exclusive_function_system.rs | 112 +- .../src/system/exclusive_system_param.rs | 70 +- crates/bevy_ecs/src/system/function_system.rs | 124 +- crates/bevy_ecs/src/system/mod.rs | 16 +- crates/bevy_ecs/src/system/query.rs | 133 +- crates/bevy_ecs/src/system/system.rs | 8 + crates/bevy_ecs/src/system/system_param.rs | 731 +++------ crates/bevy_ecs/src/system/system_piping.rs | 25 +- crates/bevy_ecs/src/world/entity_ref.rs | 481 ++---- crates/bevy_ecs/src/world/identifier.rs | 14 +- crates/bevy_ecs/src/world/mod.rs | 705 ++++++-- crates/bevy_ecs/src/world/world_cell.rs | 4 +- crates/bevy_encase_derive/src/lib.rs | 13 +- crates/bevy_gilrs/src/gilrs_system.rs | 81 +- crates/bevy_hierarchy/src/child_builder.rs | 136 +- crates/bevy_hierarchy/src/hierarchy.rs | 2 +- crates/bevy_input/src/axis.rs | 34 + crates/bevy_input/src/gamepad.rs | 640 ++++---- crates/bevy_input/src/input.rs | 4 +- crates/bevy_input/src/keyboard.rs | 2 +- crates/bevy_input/src/lib.rs | 34 +- crates/bevy_input/src/mouse.rs | 2 +- crates/bevy_internal/Cargo.toml | 7 + crates/bevy_internal/src/default_plugins.rs | 6 + crates/bevy_macro_utils/src/attrs.rs | 10 +- crates/bevy_macro_utils/src/lib.rs | 82 +- crates/bevy_pbr/src/alpha.rs | 24 + crates/bevy_pbr/src/lib.rs | 34 +- crates/bevy_pbr/src/light.rs | 6 +- crates/bevy_pbr/src/material.rs | 72 +- crates/bevy_pbr/src/pbr_material.rs | 42 +- crates/bevy_pbr/src/prepass/mod.rs | 611 +++++++ crates/bevy_pbr/src/prepass/prepass.wgsl | 81 + .../src/prepass/prepass_bindings.wgsl | 18 + .../src/render/clustered_forward.wgsl | 2 +- crates/bevy_pbr/src/render/light.rs | 36 +- crates/bevy_pbr/src/render/mesh.rs | 261 ++- .../src/render/mesh_view_bindings.wgsl | 12 + crates/bevy_pbr/src/render/pbr.wgsl | 3 + crates/bevy_pbr/src/render/pbr_functions.wgsl | 93 +- crates/bevy_pbr/src/render/pbr_prepass.wgsl | 77 + crates/bevy_pbr/src/render/pbr_types.wgsl | 16 +- crates/bevy_pbr/src/render/shadows.wgsl | 2 +- crates/bevy_pbr/src/render/utils.wgsl | 24 +- crates/bevy_pbr/src/wireframe.rs | 8 +- crates/bevy_ptr/src/lib.rs | 194 ++- crates/bevy_reflect/Cargo.toml | 2 +- .../src/trait_reflection.rs | 13 +- crates/bevy_reflect/src/from_reflect.rs | 20 + crates/bevy_reflect/src/impls/smallvec.rs | 16 + crates/bevy_reflect/src/impls/std.rs | 110 +- crates/bevy_reflect/src/list.rs | 41 +- crates/bevy_reflect/src/path.rs | 78 +- crates/bevy_reflect/src/serde/de.rs | 78 +- crates/bevy_reflect/src/serde/ser.rs | 18 +- crates/bevy_render/Cargo.toml | 4 +- .../bevy_render/macros/src/as_bind_group.rs | 82 +- crates/bevy_render/macros/src/lib.rs | 5 +- crates/bevy_render/src/camera/camera.rs | 161 +- .../src/camera/camera_driver_node.rs | 8 +- crates/bevy_render/src/camera/projection.rs | 2 +- crates/bevy_render/src/color/mod.rs | 146 +- crates/bevy_render/src/extract_param.rs | 45 +- crates/bevy_render/src/extract_resource.rs | 1 + crates/bevy_render/src/lib.rs | 173 +- crates/bevy_render/src/pipelined_rendering.rs | 155 ++ .../bevy_render/src/render_graph/node_slot.rs | 2 +- crates/bevy_render/src/render_phase/draw.rs | 193 +-- .../src/render_phase/draw_state.rs | 129 +- crates/bevy_render/src/render_phase/mod.rs | 197 ++- .../src/{ => render_phase}/rangefinder.rs | 5 +- .../src/render_resource/bind_group.rs | 13 + .../src/render_resource/pipeline_cache.rs | 32 +- .../render_resource/pipeline_specializer.rs | 4 +- .../src/render_resource/resource_macros.rs | 18 +- .../bevy_render/src/renderer/graph_runner.rs | 12 +- crates/bevy_render/src/renderer/mod.rs | 72 +- crates/bevy_render/src/texture/basis.rs | 20 +- crates/bevy_render/src/texture/dds.rs | 9 +- .../bevy_render/src/texture/fallback_image.rs | 139 +- crates/bevy_render/src/texture/ktx2.rs | 48 +- crates/bevy_render/src/texture/mod.rs | 2 + crates/bevy_render/src/view/mod.rs | 44 +- crates/bevy_render/src/view/visibility/mod.rs | 13 +- crates/bevy_render/src/view/window.rs | 85 +- .../bevy_scene/src/dynamic_scene_builder.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 2 +- crates/bevy_scene/src/serde.rs | 32 +- crates/bevy_sprite/src/mesh2d/material.rs | 18 +- crates/bevy_sprite/src/render/mod.rs | 10 +- crates/bevy_sprite/src/sprite.rs | 2 +- crates/bevy_tasks/src/lib.rs | 7 +- .../src/single_threaded_task_pool.rs | 32 + crates/bevy_tasks/src/task_pool.rs | 162 +- crates/bevy_tasks/src/thread_executor.rs | 128 ++ crates/bevy_text/src/glyph_brush.rs | 15 +- crates/bevy_text/src/lib.rs | 8 +- crates/bevy_text/src/pipeline.rs | 11 +- crates/bevy_text/src/text.rs | 151 +- crates/bevy_text/src/text2d.rs | 111 +- crates/bevy_time/src/fixed_timestep.rs | 4 + crates/bevy_time/src/stopwatch.rs | 2 +- crates/bevy_transform/src/commands.rs | 101 ++ .../src/components/global_transform.rs | 15 +- crates/bevy_transform/src/lib.rs | 12 +- crates/bevy_transform/src/systems.rs | 124 +- crates/bevy_ui/src/flex/mod.rs | 44 +- crates/bevy_ui/src/focus.rs | 103 +- crates/bevy_ui/src/node_bundles.rs | 40 +- crates/bevy_ui/src/render/mod.rs | 52 +- crates/bevy_ui/src/render/render_pass.rs | 9 +- crates/bevy_ui/src/ui_node.rs | 4 +- crates/bevy_ui/src/widget/text.rs | 17 +- crates/bevy_utils/Cargo.toml | 2 + crates/bevy_utils/src/label.rs | 41 + crates/bevy_utils/src/lib.rs | 61 +- crates/bevy_utils/src/syncunsafecell.rs | 122 ++ crates/bevy_window/src/cursor.rs | 15 +- crates/bevy_window/src/event.rs | 105 +- crates/bevy_window/src/lib.rs | 98 +- crates/bevy_window/src/raw_handle.rs | 3 +- crates/bevy_window/src/system.rs | 45 +- crates/bevy_window/src/window.rs | 1419 +++++++---------- crates/bevy_window/src/windows.rs | 88 - crates/bevy_winit/src/converters.rs | 4 +- crates/bevy_winit/src/lib.rs | 715 ++++----- crates/bevy_winit/src/system.rs | 284 ++++ crates/bevy_winit/src/web_resize.rs | 9 +- crates/bevy_winit/src/winit_config.rs | 29 +- crates/bevy_winit/src/winit_windows.rs | 283 ++-- docs/cargo_features.md | 7 + docs/profiling.md | 2 +- docs/the_bevy_organization.md | 73 + examples/2d/mesh2d_manual.rs | 6 +- examples/2d/text2d.rs | 100 +- examples/3d/blend_modes.rs | 378 +++++ examples/3d/fxaa.rs | 13 +- examples/3d/msaa.rs | 17 +- examples/3d/split_screen.rs | 42 +- examples/3d/transparency_3d.rs | 2 +- examples/README.md | 5 +- examples/animation/animated_fox.rs | 6 +- examples/app/return_after_run.rs | 19 +- .../external_source_external_thread.rs | 2 +- examples/audio/decodable.rs | 100 ++ examples/ecs/parallel_query.rs | 24 +- examples/games/alien_cake_addict.rs | 2 +- examples/games/contributors.rs | 10 +- examples/input/gamepad_input_events.rs | 57 +- examples/input/mouse_grab.rs | 17 +- examples/ios/src/lib.rs | 11 +- .../shader/compute_shader_game_of_life.rs | 9 +- examples/shader/post_processing.rs | 10 +- examples/shader/shader_instancing.rs | 11 +- examples/shader/shader_material_glsl.rs | 2 +- examples/shader/shader_prepass.rs | 244 +++ examples/stress_tests/bevymark.rs | 36 +- examples/stress_tests/many_buttons.rs | 6 +- examples/stress_tests/many_cubes.rs | 6 +- examples/stress_tests/many_foxes.rs | 14 +- examples/stress_tests/many_lights.rs | 26 +- examples/stress_tests/many_sprites.rs | 6 +- examples/tools/gamepad_viewer.rs | 59 +- .../scene_viewer/camera_controller_plugin.rs | 22 +- examples/tools/scene_viewer/main.rs | 5 +- .../transforms/global_vs_local_translation.rs | 198 --- examples/ui/relative_cursor_position.rs | 80 + examples/ui/text.rs | 2 +- examples/ui/text_debug.rs | 11 +- examples/ui/window_fallthrough.rs | 23 +- examples/window/low_power.rs | 6 +- examples/window/multiple_windows.rs | 38 +- examples/window/scale_factor_override.rs | 43 +- examples/window/transparent_window.rs | 9 +- examples/window/window_resizing.rs | 10 +- examples/window/window_settings.rs | 68 +- tests/window/minimising.rs | 11 +- tests/window/resizing.rs | 31 +- tools/spancmp/src/main.rs | 2 +- 257 files changed, 14756 insertions(+), 5799 deletions(-) create mode 100644 assets/shaders/show_prepass.wgsl create mode 100644 crates/bevy_core_pipeline/src/prepass/mod.rs create mode 100644 crates/bevy_core_pipeline/src/prepass/node.rs create mode 100644 crates/bevy_ecs/src/query/par_iter.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/condition.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/config.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/executor/mod.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/executor/simple.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/executor/single_threaded.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/graph_utils.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/migration.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/mod.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/schedule.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/set.rs create mode 100644 crates/bevy_ecs/src/schedule_v3/state.rs create mode 100644 crates/bevy_pbr/src/prepass/mod.rs create mode 100644 crates/bevy_pbr/src/prepass/prepass.wgsl create mode 100644 crates/bevy_pbr/src/prepass/prepass_bindings.wgsl create mode 100644 crates/bevy_pbr/src/render/pbr_prepass.wgsl create mode 100644 crates/bevy_render/src/pipelined_rendering.rs rename crates/bevy_render/src/{ => render_phase}/rangefinder.rs (89%) create mode 100644 crates/bevy_tasks/src/thread_executor.rs create mode 100644 crates/bevy_transform/src/commands.rs create mode 100644 crates/bevy_utils/src/syncunsafecell.rs delete mode 100644 crates/bevy_window/src/windows.rs create mode 100644 crates/bevy_winit/src/system.rs create mode 100644 docs/the_bevy_organization.md create mode 100644 examples/3d/blend_modes.rs create mode 100644 examples/audio/decodable.rs create mode 100644 examples/shader/shader_prepass.rs delete mode 100644 examples/transforms/global_vs_local_translation.rs create mode 100644 examples/ui/relative_cursor_position.rs diff --git a/.github/bors.toml b/.github/bors.toml index 7c237230f15ca..087a395e95150 100644 --- a/.github/bors.toml +++ b/.github/bors.toml @@ -17,6 +17,7 @@ status = [ "build-without-default-features (bevy)", "build-without-default-features (bevy_ecs)", "build-without-default-features (bevy_reflect)", + "msrv", ] use_squash_merge = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5b282b74a27c..eafdcfebbde87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -275,3 +275,29 @@ jobs: run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev - name: Run cargo udeps run: cargo udeps + + msrv: + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v3 + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-msrv-${{ hashFiles('**/Cargo.toml') }} + - name: get MSRV + run: | + msrv=`cargo metadata --no-deps --format-version 1 | jq --raw-output '.packages[] | select(.name=="bevy") | .rust_version'` + echo "MSRV=$msrv" >> $GITHUB_ENV + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.MSRV }} + - name: Install alsa and udev + run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev + - name: Run cargo check + run: cargo check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad7f58a9b1ef8..d824700af79c7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,7 +50,7 @@ Bevy is a completely free and open source game engine built in Rust. It currentl Bevy also currently has the following "development process" goals: * **Rapid experimentation over API stability**: We need the freedom to experiment and iterate in order to build the best engine we can. This will change over time as APIs prove their staying power. -* **Consistent vision**: The engine needs to feel consistent and cohesive. This takes precedent over democratic and/or decentralized processes. See [*How we're organized*](#how-were-organized) for more details. +* **Consistent vision**: The engine needs to feel consistent and cohesive. This takes precedent over democratic and/or decentralized processes. See our [*Bevy Organization doc*](/docs/the_bevy_organization.md) for more details. * **Flexibility over bureaucracy**: Developers should feel productive and unencumbered by development processes. * **Focus**: The Bevy Org should focus on building a small number of features excellently over merging every new community-contributed feature quickly. Sometimes this means pull requests will sit unmerged for a long time. This is the price of focus and we are willing to pay it. Fortunately Bevy is modular to its core. 3rd party plugins are a great way to work around this policy. * **User-facing API ergonomics come first**: Solid user experience should receive significant focus and investment. It should rarely be compromised in the interest of internal implementation details. @@ -66,72 +66,87 @@ Bevy also currently has the following "development process" goals: * Most new features should have at least one minimal [example](https://github.com/bevyengine/bevy/tree/main/examples). These also serve as simple integration tests, as they are run as part of our CI process. * The more complex or "core" a feature is, the more strict we are about unit tests. Use your best judgement here. We will let you know if your pull request needs more tests. We use [Rust's built in testing framework](https://doc.rust-lang.org/book/ch11-01-writing-tests.html). -## How we're organized +## The Bevy Organization -@cart is, for now, our singular Benevolent Dictator and project lead. -He makes the final decision on both design and code changes within Bevy in order to ensure a coherent vision and consistent quality of code. +The Bevy Organization is the group of people responsible for stewarding the Bevy project. It handles things like merging pull requests, choosing project direction, managing bugs / issues / feature requests, running the Bevy website, controlling access to secrets, defining and enforcing best practices, etc. -In practice, @cart serves as a shockingly accountable dictator: open to new ideas and to changing his mind in the face of compelling arguments or community consensus. -Check out the next section for details on how this plays out. +Note that you *do not* need to be a member of the Bevy Organization to contribute to Bevy. Community contributors (this means you) can freely open issues, submit pull requests, and review pull requests. -[Bevy Org members](https://github.com/orgs/bevyengine/people) are contributors who: +Check out our dedicated [Bevy Organization document](/docs/the_bevy_organization.md) to learn more about how we're organized. -1. Have actively engaged with Bevy development. -2. Have demonstrated themselves to be polite and welcoming representatives of the project with an understanding of our goals and direction. -3. Have asked to join the Bevy Org. Reach out to @cart on [Discord] or email us at bevyengine@gmail.com if you are interested. Everyone is welcome to do this. We generally accept membership requests, so don't hesitate if you are interested! - -All Bevy Org members are also Triage Team members. These people can label and close issues and PRs but do not have merge rights or any special authority within the community. +### Classifying PRs -Merge rights within the org are relatively centralized: this requires a large amount of trust when it comes to ethics, technical ability, and ability to enforce consistent project direction. +Our merge strategy relies on the classification of PRs on two axes: -The current structure is as follows: +* How controversial are the design decisions +* How complex is the implementation -* @cart is our project lead, and has final say on controversial decisions -* There is a small group of other maintainers (@alice-i-cecile, @mockersf and @superdump), who have merge rights but abide by the following rules: - * Trivial PRs can be merged without approvals. - * Relatively uncontroversial PRs can be merged following approval from at least two other community members with appropriate expertise. - * Controversial PRs are added to a backlog for @cart to address once two maintainers agree that they are ready. - * If 45 days elapse without action on a controversial PR (approval, feedback or an explicit request to defer), they can be merged without project lead approval. -* The Bevy org is made up of trusted community contributors: this is a relatively low bar, and org members help triage and maintain the project. -* Community contributors (this means you!) can freely open issues, submit PRs and review PRs to improve Bevy. - * As discussed above, community reviews on PRs are incredibly helpful to enable maintainers to merge in uncontroversial PRs in a timely fashion. +PRs with non-trivial design decisions are given the [`S-Controversial`] label. This indicates that +the PR needs more thorough design review or an [RFC](https://github.com/bevyengine/rfcs), if complex enough. -### Classifying PRs +PRs that are non-trivial to review are given the [`D-Complex`] label. This indicates that the PR +should be reviewed more thoroughly and by people with experience in the area that the PR touches. -This strategy relies on a classification of PRs into three categories: **trivial**, **uncontroversial** and **controversial**. When making PRs, try to split out more controversial changes from less controversial ones, in order to make your work easier to review and merge. -PRs that are deemed controversial will receive the `S-Controversial` label, and will have to go through the more thorough review process. +It is also a good idea to try and split out simple changes from more complex changes if it is not helpful for then to be reviewed together. + +Some things that are reason to apply the [`S-Controversial`] label to a PR: -PRs are trivial if there is no reasonable argument against them. This might include: +1. Changes to a project-wide workflow or style. +2. New architecture for a large feature. +3. Serious tradeoffs were made. +4. Heavy user impact. +5. New ways for users to make mistakes (footguns). +6. Adding a dependency +7. Touching licensing information (due to level of precision required). +8. Adding root-level files (due to the high level of visibility) + +Some things that are reason to apply the [`D-Complex`] label to a PR: + +1. Introduction or modification of soundness relevent code (for example `unsafe` code) +2. High levels of technical complexity. +3. Large-scale code reorganization + +Examples of PRs that are not [`S-Controversial`] or [`D-Complex`]: * Fixing dead links. -* Removing dead code or dependencies. +* Removing dead code or unused dependencies. * Typo and grammar fixes. +* [Add `Mut::reborrow`](https://github.com/bevyengine/bevy/pull/7114) +* [Add `Res::clone`](https://github.com/bevyengine/bevy/pull/4109) -PRs are controversial if there is serious design discussion required, or a large impact to contributors or users. Factors that increase controversy include: +Examples of PRs that are [`S-Controversial`] but not [`D-Complex`]: -1. Changes to project-wide workflow or style. -2. New architecture for a large feature. -3. PRs where a serious tradeoff must be made. -4. Heavy user impact. -5. New ways for users to make mistakes (footguns). -6. Introductions of `unsafe` code. -7. Large-scale code reorganization. -8. High levels of technical complexity. -9. Adding a dependency. -10. Touching licensing information (due to the level of precision required). -11. Adding root-level files (due to the high level of visibility). +* [Implement and require `#[derive(Component)]` on all component structs](https://github.com/bevyengine/bevy/pull/2254) +* [Use default serde impls for Entity](https://github.com/bevyengine/bevy/pull/6194) -Finally, changes are "relatively uncontroversial" if they are neither trivial or controversial. -Most PRs should fall into this category. +Examples of PRs that are not [`S-Controversial`] but are [`D-Complex`]: -## How we work together +* [Ensure `Ptr`/`PtrMut`/`OwningPtr` are aligned in debug builds](https://github.com/bevyengine/bevy/pull/7117) +* [Replace `BlobVec`'s `swap_scratch` with a `swap_nonoverlapping`](https://github.com/bevyengine/bevy/pull/4853) -Making a game engine is a huge project and facilitating collaboration is a lot of work. -At the moment @cart is our only paid contributor, so [go sponsor him!](https://github.com/sponsors/cart) -We track issues and pull requests that must be included in releases using [Milestones](https://github.com/bevyengine/bevy/milestones). +Examples of PRs that are both [`S-Controversial`] and [`D-Complex`]: -### Making changes to Bevy +* [bevy_reflect: Binary formats](https://github.com/bevyengine/bevy/pull/6140) + +Some useful pull request queries: + +* [PRs which need reviews and are not `D-Complex`](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+-label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked++) +* [`D-Complex` PRs which need reviews](https://github.com/bevyengine/bevy/pulls?q=is%3Apr+label%3AD-Complex+-label%3AS-Ready-For-Final-Review+-label%3AS-Blocked) + +[`S-Controversial`]: https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AS-Controversial +[`D-Complex`]: https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AD-Complex + +### Prioritizing PRs and issues + +We use [Milestones](https://github.com/bevyengine/bevy/milestones) to track issues and PRs that: + +* Need to be merged/fixed before the next release. This is generally for extremely bad bugs i.e. UB or important functionality being broken. +* Would have higher user impact and are almost ready to be merged/fixed. + +There are also two priority labels: [`P-Critical`](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AP-Critical) and [`P-High`](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AP-High) that can be used to find issues and PRs that need to be resolved urgently. + +## Making changes to Bevy Most changes don't require much "process". If your change is relatively straightforward, just do the following: @@ -139,10 +154,10 @@ Most changes don't require much "process". If your change is relatively straight * [GitHub Discussions]: An informal discussion with the community. This is the place to start if you want to propose a feature or specific implementation. * [Issue](https://github.com/bevyengine/bevy/issues): A formal way for us to track a bug or feature. Please look for duplicates before opening a new issue and consider starting with a Discussion. * [Pull Request](https://github.com/bevyengine/bevy/pulls) (or PR for short): A request to merge code changes. This starts our "review process". You are welcome to start with a pull request, but consider starting with an Issue or Discussion for larger changes (or if you aren't certain about a design). We don't want anyone to waste their time on code that didn't have a chance to be merged! But conversely, sometimes PRs are the most efficient way to propose a change. Just use your own judgement here. -2. Other community members review and comment in an ad-hoc fashion. Active subject matter experts may be pulled into a thread using `@mentions`. If your PR has been quiet for a while and is ready for review, feel free to leave a message to "bump" the thread, or bring it up on [Discord] in an appropriate engine development channel. +2. Other community members review and comment in an ad-hoc fashion. Active subject matter experts may be pulled into a thread using `@mentions`. If your PR has been quiet for a while and is ready for review, feel free to leave a message to "bump" the thread, or bring it up on [Discord](https://discord.gg/bevy) in an appropriate engine development channel. 3. Once they're content with the pull request (design, code quality, documentation, tests), individual reviewers leave "Approved" reviews. 4. After consensus has been reached (typically two approvals from the community or one for extremely simple changes) and CI passes, the [S-Ready-For-Final-Review](https://github.com/bevyengine/bevy/issues?q=is%3Aopen+is%3Aissue+label%3AS-Ready-For-Final-Review) label is added. -5. When they find time, [someone with merge rights](#how-were-organized) performs a final code review and merges the PR using [Bors](https://bors.tech/) by typing `bors r+`. +5. When they find time, someone with merge rights performs a final code review and merges the PR using [Bors](https://bors.tech/) by typing `bors r+`. ### Complex changes @@ -180,7 +195,7 @@ If you release a game on [itch.io](https://itch.io/games/tag-bevy) we'd be thril ### Teaching others -Bevy is still very young, and light on documentation, tutorials and accumulated expertise. +Bevy is still very young, and light on documentation, tutorials, and accumulated expertise. By helping others with their issues, and teaching them about Bevy, you will naturally learn the engine and codebase in greater depth while also making our community better! Some of the best ways to do this are: @@ -273,7 +288,7 @@ Not even our project lead is exempt from reviews and RFCs! By giving feedback on this work (and related supporting work), you can help us make sure our releases are both high-quality and timely. Finally, if nothing brings you more satisfaction than seeing every last issue labeled and all resolved issues closed, feel free to message @cart for a Bevy org role to help us keep things tidy. -As discussed in [*How we're organized*](#how-were-organized), this role only requires good faith and a basic understanding of our development process. +As discussed in our [*Bevy Organization doc*](/docs/the_bevy_organization.md), this role only requires good faith and a basic understanding of our development process. ### How to adopt pull requests @@ -295,15 +310,6 @@ Then notify org members to close the original. `Adopted #number-original-pull-request` -### Maintaining code - -Maintainers can merge uncontroversial pull requests that have at least two approvals (or at least one for trivial changes). - -These search filters show the requests that can be merged by maintainers, and those which need a final approval from @cart. - -1. Pulls requests which are ready for maintainers to merge without consultation: [requests to pull](https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AS-Ready-For-Final-Review+-label%3AS-Controversial+-label%3AS-Blocked+-label%3AS-Adopt-Me+) -2. Pull requests which need final input from @cart: [requests to verify](https://github.com/bevyengine/bevy/pulls?q=is%3Aopen+is%3Apr+label%3AS-Ready-For-Final-Review+label%3AS-Controversial+) - ### Contributing code Bevy is actively open to code contributions from community members. @@ -336,7 +342,7 @@ If you end up adding a new official Bevy crate to the `bevy` repo: When contributing, please: -* Try to loosely follow the workflow in [*How we work together*](#how-we-work-together). +* Try to loosely follow the workflow in [*Making changes to Bevy*](#making-changes-to-bevy). * Consult the [style guide](.github/contributing/engine_style_guide.md) to help keep our code base tidy. * Explain what you're doing and why. * Document new code with doc comments. diff --git a/Cargo.toml b/Cargo.toml index 11e7cc5fb06d9..547846e5fc45d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" readme = "README.md" repository = "https://github.com/bevyengine/bevy" +rust-version = "1.66.0" [workspace] exclude = ["benches", "crates/bevy_ecs_compile_fail_tests", "crates/bevy_reflect_compile_fail_tests"] @@ -89,6 +90,13 @@ flac = ["bevy_internal/flac"] mp3 = ["bevy_internal/mp3"] vorbis = ["bevy_internal/vorbis"] wav = ["bevy_internal/wav"] +symphonia-aac = ["bevy_internal/symphonia-aac"] +symphonia-all = ["bevy_internal/symphonia-all"] +symphonia-flac = ["bevy_internal/symphonia-flac"] +symphonia-isomp4 = ["bevy_internal/symphonia-isomp4"] +symphonia-mp3 = ["bevy_internal/symphonia-mp3"] +symphonia-vorbis = ["bevy_internal/symphonia-vorbis"] +symphonia-wav = ["bevy_internal/symphonia-wav"] # Enable watching file system for asset hot reload filesystem_watcher = ["bevy_internal/filesystem_watcher"] @@ -289,6 +297,16 @@ description = "A scene showcasing the built-in 3D shapes" category = "3D Rendering" wasm = true +[[example]] +name = "blend_modes" +path = "examples/3d/blend_modes.rs" + +[package.metadata.example.blend_modes] +name = "Blend Modes" +description = "Showcases different blend modes" +category = "3D Rendering" +wasm = true + [[example]] name = "lighting" path = "examples/3d/lighting.rs" @@ -755,6 +773,16 @@ description = "Shows how to load and play an audio file, and control how it's pl category = "Audio" wasm = true +[[example]] +name = "decodable" +path = "examples/audio/decodable.rs" + +[package.metadata.example.decodable] +name = "Decodable" +description = "Shows how to create and register a custom audio source by implementing the `Decodable` type." +category = "Audio" +wasm = true + # Diagnostics [[example]] name = "log_diagnostics" @@ -1202,6 +1230,17 @@ description = "A shader and a material that uses it" category = "Shaders" wasm = true +[[example]] +name = "shader_prepass" +path = "examples/shader/shader_prepass.rs" + +[package.metadata.example.shader_prepass] +name = "Material Prepass" +description = "A shader that uses the depth texture generated in a prepass" +category = "Shaders" +wasm = false + + [[example]] name = "shader_material_screenspace_texture" path = "examples/shader/shader_material_screenspace_texture.rs" @@ -1376,17 +1415,6 @@ description = "Shows a visualization of gamepad buttons, sticks, and triggers" category = "Tools" wasm = false -# Transforms -[[example]] -name = "global_vs_local_translation" -path = "examples/transforms/global_vs_local_translation.rs" - -[package.metadata.example.global_vs_local_translation] -name = "Global / Local Translation" -description = "Illustrates the difference between direction of a translation in respect to local object or global object Transform" -category = "Transforms" -wasm = true - [[example]] name = "3d_rotation" path = "examples/transforms/3d_rotation.rs" @@ -1458,6 +1486,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text category = "UI (User Interface)" wasm = true +[[example]] +name = "relative_cursor_position" +path = "examples/ui/relative_cursor_position.rs" + +[package.metadata.example.relative_cursor_position] +name = "Relative Cursor Position" +description = "Showcases the RelativeCursorPosition component" +category = "UI (User Interface)" +wasm = true + [[example]] name = "text" path = "examples/ui/text.rs" diff --git a/assets/shaders/show_prepass.wgsl b/assets/shaders/show_prepass.wgsl new file mode 100644 index 0000000000000..592143aa6ee19 --- /dev/null +++ b/assets/shaders/show_prepass.wgsl @@ -0,0 +1,26 @@ +#import bevy_pbr::mesh_types +#import bevy_pbr::mesh_view_bindings +#import bevy_pbr::utils + +@group(1) @binding(0) +var show_depth: f32; +@group(1) @binding(1) +var show_normal: f32; + +@fragment +fn fragment( + @builtin(position) frag_coord: vec4, + @builtin(sample_index) sample_index: u32, + #import bevy_pbr::mesh_vertex_output +) -> @location(0) vec4 { + if show_depth == 1.0 { + let depth = prepass_depth(frag_coord, sample_index); + return vec4(depth, depth, depth, 1.0); + } else if show_normal == 1.0 { + let normal = prepass_normal(frag_coord, sample_index); + return vec4(normal, 1.0); + } else { + // transparent + return vec4(0.0); + } +} diff --git a/benches/benches/bevy_ecs/components/archetype_updates.rs b/benches/benches/bevy_ecs/components/archetype_updates.rs index 7574104c38be9..8cb4c5552db0c 100644 --- a/benches/benches/bevy_ecs/components/archetype_updates.rs +++ b/benches/benches/bevy_ecs/components/archetype_updates.rs @@ -109,7 +109,7 @@ pub fn added_archetypes(criterion: &mut Criterion) { stage.run(&mut world); }, criterion::BatchSize::LargeInput, - ) + ); }, ); } diff --git a/benches/benches/bevy_ecs/iteration/heavy_compute.rs b/benches/benches/bevy_ecs/iteration/heavy_compute.rs index 440d1bcb22f6f..99b3fdb7649da 100644 --- a/benches/benches/bevy_ecs/iteration/heavy_compute.rs +++ b/benches/benches/bevy_ecs/iteration/heavy_compute.rs @@ -34,7 +34,7 @@ pub fn heavy_compute(c: &mut Criterion) { })); fn sys(mut query: Query<(&mut Position, &mut Transform)>) { - query.par_for_each_mut(128, |(mut pos, mut mat)| { + query.par_iter_mut().for_each_mut(|(mut pos, mut mat)| { for _ in 0..100 { mat.0 = mat.0.inverse(); } diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index 49de37079b73b..55887fb643c53 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -110,11 +110,11 @@ pub fn build_schedule(criterion: &mut Criterion) { // Not particularly realistic but this can be refined later. for i in 0..graph_size { let mut sys = empty_system.label(labels[i]).before(DummyLabel); - for a in 0..i { - sys = sys.after(labels[a]); + for label in labels.iter().take(i) { + sys = sys.after(*label); } - for b in i + 1..graph_size { - sys = sys.before(labels[b]); + for label in &labels[i + 1..graph_size] { + sys = sys.before(*label); } app.add_system(sys); } diff --git a/benches/benches/bevy_ecs/scheduling/stages.rs b/benches/benches/bevy_ecs/scheduling/stages.rs index 0f258f4f99f38..fec9ba5eab305 100644 --- a/benches/benches/bevy_ecs/scheduling/stages.rs +++ b/benches/benches/bevy_ecs/scheduling/stages.rs @@ -58,7 +58,7 @@ pub fn empty_systems(criterion: &mut Criterion) { }); }); } - group.finish() + group.finish(); } pub fn busy_systems(criterion: &mut Criterion) { @@ -107,7 +107,7 @@ pub fn busy_systems(criterion: &mut Criterion) { ); } } - group.finish() + group.finish(); } pub fn contrived(criterion: &mut Criterion) { @@ -158,5 +158,5 @@ pub fn contrived(criterion: &mut Criterion) { ); } } - group.finish() + group.finish(); } diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 41f9bbe29e12a..6a55cb5ea82fe 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -61,7 +61,6 @@ pub fn spawn_commands(criterion: &mut Criterion) { entity.despawn(); } } - drop(commands); command_queue.apply(&mut world); }); }); @@ -82,7 +81,7 @@ pub fn insert_commands(criterion: &mut Criterion) { group.measurement_time(std::time::Duration::from_secs(4)); let entity_count = 10_000; - group.bench_function(format!("insert"), |bencher| { + group.bench_function("insert", |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); let mut entities = Vec::new(); @@ -97,11 +96,10 @@ pub fn insert_commands(criterion: &mut Criterion) { .entity(*entity) .insert((Matrix::default(), Vec3::default())); } - drop(commands); command_queue.apply(&mut world); }); }); - group.bench_function(format!("insert_batch"), |bencher| { + group.bench_function("insert_batch", |bencher| { let mut world = World::default(); let mut command_queue = CommandQueue::default(); let mut entities = Vec::new(); @@ -116,7 +114,6 @@ pub fn insert_commands(criterion: &mut Criterion) { values.push((*entity, (Matrix::default(), Vec3::default()))); } commands.insert_or_spawn_batch(values); - drop(commands); command_queue.apply(&mut world); }); }); @@ -160,7 +157,6 @@ pub fn fake_commands(criterion: &mut Criterion) { commands.add(FakeCommandB(0)); } } - drop(commands); command_queue.apply(&mut world); }); }); @@ -203,7 +199,6 @@ pub fn sized_commands_impl(criterion: &mut Criterion) { for _ in 0..command_count { commands.add(T::default()); } - drop(commands); command_queue.apply(&mut world); }); }); diff --git a/benches/benches/bevy_ecs/world/world_get.rs b/benches/benches/bevy_ecs/world/world_get.rs index 536b5a7b8b4a9..4fc7ed51046b2 100644 --- a/benches/benches/bevy_ecs/world/world_get.rs +++ b/benches/benches/bevy_ecs/world/world_get.rs @@ -273,7 +273,7 @@ pub fn query_get_component_simple(criterion: &mut Criterion) { bencher.iter(|| { for _x in 0..100000 { - let mut a = unsafe { query.get_unchecked(&mut world, entity).unwrap() }; + let mut a = unsafe { query.get_unchecked(&world, entity).unwrap() }; a.0 += 1.0; } }); diff --git a/benches/benches/bevy_reflect/list.rs b/benches/benches/bevy_reflect/list.rs index 3c48dd22e4556..0c366e0540557 100644 --- a/benches/benches/bevy_reflect/list.rs +++ b/benches/benches/bevy_reflect/list.rs @@ -44,7 +44,7 @@ fn list_apply( let f_base = f_base(size); let patch = f_patch(size); bencher.iter_batched( - || f_base(), + f_base, |mut base| base.apply(black_box(&patch)), BatchSize::SmallInput, ); @@ -58,7 +58,7 @@ fn concrete_list_apply(criterion: &mut Criterion) { group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); - let empty_base = |_: usize| || Vec::::new(); + let empty_base = |_: usize| Vec::::new; let full_base = |size: usize| move || iter::repeat(0).take(size).collect::>(); let patch = |size: usize| iter::repeat(1).take(size).collect::>(); diff --git a/benches/benches/bevy_reflect/map.rs b/benches/benches/bevy_reflect/map.rs index b5c8a95eeec3c..b27a096d3d072 100644 --- a/benches/benches/bevy_reflect/map.rs +++ b/benches/benches/bevy_reflect/map.rs @@ -60,7 +60,7 @@ fn concrete_map_apply(criterion: &mut Criterion) { group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); - let empty_base = |_: usize| || HashMap::::default(); + let empty_base = |_: usize| HashMap::::default; let key_range_base = |size: usize| { move || { @@ -136,7 +136,7 @@ fn dynamic_map_apply(criterion: &mut Criterion) { group.warm_up_time(WARM_UP_TIME); group.measurement_time(MEASUREMENT_TIME); - let empty_base = |_: usize| || DynamicMap::default(); + let empty_base = |_: usize| DynamicMap::default; let key_range_base = |size: usize| { move || { @@ -240,8 +240,8 @@ fn dynamic_map_get(criterion: &mut Criterion) { } bencher.iter(|| { - for i in 0..size { - let key = black_box(&keys[i]); + for key in keys.iter().take(size) { + let key = black_box(key); assert!(map.get(key).is_some()); } }); @@ -262,7 +262,7 @@ fn dynamic_map_insert(criterion: &mut Criterion) { &size, |bencher, &size| { bencher.iter_batched( - || DynamicMap::default(), + DynamicMap::default, |mut map| { for i in 0..size as u64 { let key = black_box(i); diff --git a/benches/benches/bevy_tasks/iter.rs b/benches/benches/bevy_tasks/iter.rs index 74b043f9a234e..ee1babf4e2f46 100644 --- a/benches/benches/bevy_tasks/iter.rs +++ b/benches/benches/bevy_tasks/iter.rs @@ -28,7 +28,7 @@ fn bench_overhead(c: &mut Criterion) { c.bench_function("overhead_iter", |b| { b.iter(|| { v.iter_mut().for_each(noop); - }) + }); }); let mut v = (0..10000).collect::>(); @@ -41,7 +41,7 @@ fn bench_overhead(c: &mut Criterion) { |b, _| { b.iter(|| { ParChunksMut(v.chunks_mut(100)).for_each(&pool, noop); - }) + }); }, ); } @@ -63,7 +63,7 @@ fn bench_for_each(c: &mut Criterion) { busy_work(10000); *x *= *x; }); - }) + }); }); let mut v = (0..10000).collect::>(); @@ -79,7 +79,7 @@ fn bench_for_each(c: &mut Criterion) { busy_work(10000); *x *= *x; }); - }) + }); }, ); } @@ -109,7 +109,7 @@ fn bench_many_maps(c: &mut Criterion) { .map(|x| busy_doubles(x, 1000)) .map(|x| busy_doubles(x, 1000)) .for_each(drop); - }) + }); }); let v = (0..10000).collect::>(); @@ -133,7 +133,7 @@ fn bench_many_maps(c: &mut Criterion) { .map(|x| busy_doubles(x, 1000)) .map(|x| busy_doubles(x, 1000)) .for_each(&pool, drop); - }) + }); }, ); } diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 9b9e8de991af9..da8527b8b2d5e 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -3,12 +3,13 @@ #![warn(missing_docs)] use std::ops::Deref; +use std::time::Duration; use bevy_app::{App, CoreStage, Plugin}; use bevy_asset::{AddAsset, Assets, Handle}; use bevy_core::Name; use bevy_ecs::{ - change_detection::DetectChanges, + change_detection::{DetectChanges, Mut}, entity::Entity, prelude::Component, query::With, @@ -114,11 +115,8 @@ impl AnimationClip { } } -/// Animation controls -#[derive(Component, Reflect)] -#[reflect(Component)] -pub struct AnimationPlayer { - paused: bool, +#[derive(Reflect)] +struct PlayingAnimation { repeat: bool, speed: f32, elapsed: f32, @@ -126,10 +124,9 @@ pub struct AnimationPlayer { path_cache: Vec>>, } -impl Default for AnimationPlayer { +impl Default for PlayingAnimation { fn default() -> Self { Self { - paused: false, repeat: false, speed: 1.0, elapsed: 0.0, @@ -139,33 +136,106 @@ impl Default for AnimationPlayer { } } +/// An animation that is being faded out as part of a transition +struct AnimationTransition { + /// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out. + current_weight: f32, + /// How much to decrease `current_weight` per second + weight_decline_per_sec: f32, + /// The animation that is being faded out + animation: PlayingAnimation, +} + +/// Animation controls +#[derive(Component, Default, Reflect)] +#[reflect(Component)] +pub struct AnimationPlayer { + paused: bool, + + animation: PlayingAnimation, + + // List of previous animations we're currently transitioning away from. + // Usually this is empty, when transitioning between animations, there is + // one entry. When another animation transition happens while a transition + // is still ongoing, then there can be more than one entry. + // Once a transition is finished, it will be automatically removed from the list + #[reflect(ignore)] + transitions: Vec, +} + impl AnimationPlayer { /// Start playing an animation, resetting state of the player + /// This will use a linear blending between the previous and the new animation to make a smooth transition pub fn start(&mut self, handle: Handle) -> &mut Self { - *self = Self { + self.animation = PlayingAnimation { animation_clip: handle, ..Default::default() }; + + // We want a hard transition. + // In case any previous transitions are still playing, stop them + self.transitions.clear(); + + self + } + + /// Start playing an animation, resetting state of the player + /// This will use a linear blending between the previous and the new animation to make a smooth transition + pub fn start_with_transition( + &mut self, + handle: Handle, + transition_duration: Duration, + ) -> &mut Self { + let mut animation = PlayingAnimation { + animation_clip: handle, + ..Default::default() + }; + std::mem::swap(&mut animation, &mut self.animation); + + // Add the current transition. If other transitions are still ongoing, + // this will keep those transitions running and cause a transition between + // the output of that previous transition to the new animation. + self.transitions.push(AnimationTransition { + current_weight: 1.0, + weight_decline_per_sec: 1.0 / transition_duration.as_secs_f32(), + animation, + }); + self } /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. + /// If `transition_duration` is set, this will use a linear blending + /// between the previous and the new animation to make a smooth transition pub fn play(&mut self, handle: Handle) -> &mut Self { - if self.animation_clip != handle || self.is_paused() { + if self.animation.animation_clip != handle || self.is_paused() { self.start(handle); } self } + /// Start playing an animation, resetting state of the player, unless the requested animation is already playing. + /// This will use a linear blending between the previous and the new animation to make a smooth transition + pub fn play_with_transition( + &mut self, + handle: Handle, + transition_duration: Duration, + ) -> &mut Self { + if self.animation.animation_clip != handle || self.is_paused() { + self.start_with_transition(handle, transition_duration); + } + self + } + /// Set the animation to repeat pub fn repeat(&mut self) -> &mut Self { - self.repeat = true; + self.animation.repeat = true; self } /// Stop the animation from repeating pub fn stop_repeating(&mut self) -> &mut Self { - self.repeat = false; + self.animation.repeat = false; self } @@ -186,23 +256,23 @@ impl AnimationPlayer { /// Speed of the animation playback pub fn speed(&self) -> f32 { - self.speed + self.animation.speed } /// Set the speed of the animation playback pub fn set_speed(&mut self, speed: f32) -> &mut Self { - self.speed = speed; + self.animation.speed = speed; self } /// Time elapsed playing the animation pub fn elapsed(&self) -> f32 { - self.elapsed + self.animation.elapsed } /// Seek to a specific time in the animation pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self { - self.elapsed = elapsed; + self.animation.elapsed = elapsed; self } } @@ -282,38 +352,122 @@ pub fn animation_player( parents: Query<(Option>, Option<&Parent>)>, mut animation_players: Query<(Entity, Option<&Parent>, &mut AnimationPlayer)>, ) { - animation_players.par_for_each_mut(10, |(root, maybe_parent, mut player)| { - let Some(animation_clip) = animations.get(&player.animation_clip) else { return }; - // Continue if paused unless the `AnimationPlayer` was changed - // This allow the animation to still be updated if the player.elapsed field was manually updated in pause - if player.paused && !player.is_changed() { - return; - } - if !player.paused { - player.elapsed += time.delta_seconds() * player.speed; + animation_players + .par_iter_mut() + .for_each_mut(|(root, maybe_parent, mut player)| { + update_transitions(&mut player, &time); + run_animation_player( + root, + player, + &time, + &animations, + &names, + &transforms, + maybe_parent, + &parents, + &children, + ); + }); +} + +#[allow(clippy::too_many_arguments)] +fn run_animation_player( + root: Entity, + mut player: Mut, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &Query<&mut Transform>, + maybe_parent: Option<&Parent>, + parents: &Query<(Option>, Option<&Parent>)>, + children: &Query<&Children>, +) { + let paused = player.paused; + // Continue if paused unless the `AnimationPlayer` was changed + // This allow the animation to still be updated if the player.elapsed field was manually updated in pause + if paused && !player.is_changed() { + return; + } + + // Apply the main animation + apply_animation( + 1.0, + &mut player.animation, + paused, + root, + time, + animations, + names, + transforms, + maybe_parent, + parents, + children, + ); + + // Apply any potential fade-out transitions from previous animations + for AnimationTransition { + current_weight, + animation, + .. + } in &mut player.transitions + { + apply_animation( + *current_weight, + animation, + paused, + root, + time, + animations, + names, + transforms, + maybe_parent, + parents, + children, + ); + } +} + +#[allow(clippy::too_many_arguments)] +fn apply_animation( + weight: f32, + animation: &mut PlayingAnimation, + paused: bool, + root: Entity, + time: &Time, + animations: &Assets, + names: &Query<&Name>, + transforms: &Query<&mut Transform>, + maybe_parent: Option<&Parent>, + parents: &Query<(Option>, Option<&Parent>)>, + children: &Query<&Children>, +) { + if let Some(animation_clip) = animations.get(&animation.animation_clip) { + if !paused { + animation.elapsed += time.delta_seconds() * animation.speed; } - let mut elapsed = player.elapsed; - if player.repeat { + let mut elapsed = animation.elapsed; + if animation.repeat { elapsed %= animation_clip.duration; } if elapsed < 0.0 { elapsed += animation_clip.duration; } - if player.path_cache.len() != animation_clip.paths.len() { - player.path_cache = vec![Vec::new(); animation_clip.paths.len()]; + if animation.path_cache.len() != animation_clip.paths.len() { + animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; } - if !verify_no_ancestor_player(maybe_parent, &parents) { + if !verify_no_ancestor_player(maybe_parent, parents) { warn!("Animation player on {:?} has a conflicting animation player on an ancestor. Cannot safely animate.", root); return; } + for (path, bone_id) in &animation_clip.paths { - let cached_path = &mut player.path_cache[*bone_id]; + let cached_path = &mut animation.path_cache[*bone_id]; let curves = animation_clip.get_curves(*bone_id).unwrap(); - let Some(target) = find_bone(root, path, &children, &names, cached_path) else { continue }; + let Some(target) = find_bone(root, path, children, names, cached_path) else { continue }; // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias // any of their descendant Transforms. - // - // The system scheduler prevents any other system from mutating Transforms at the same time, + // + // The system scheduler prevents any other system from mutating Transforms at the same time, // so the only way this fetch can alias is if two AnimationPlayers are targetting the same bone. // This can only happen if there are two or more AnimationPlayers are ancestors to the same // entities. By verifying that there is no other AnimationPlayer in the ancestors of a @@ -327,11 +481,16 @@ pub fn animation_player( // Some curves have only one keyframe used to set a transform if curve.keyframe_timestamps.len() == 1 { match &curve.keyframes { - Keyframes::Rotation(keyframes) => transform.rotation = keyframes[0], + Keyframes::Rotation(keyframes) => { + transform.rotation = transform.rotation.slerp(keyframes[0], weight); + } Keyframes::Translation(keyframes) => { - transform.translation = keyframes[0]; + transform.translation = + transform.translation.lerp(keyframes[0], weight); + } + Keyframes::Scale(keyframes) => { + transform.scale = transform.scale.lerp(keyframes[0], weight); } - Keyframes::Scale(keyframes) => transform.scale = keyframes[0], } continue; } @@ -362,24 +521,31 @@ pub fn animation_player( rot_end = -rot_end; } // Rotations are using a spherical linear interpolation - transform.rotation = - rot_start.normalize().slerp(rot_end.normalize(), lerp); + let rot = rot_start.normalize().slerp(rot_end.normalize(), lerp); + transform.rotation = transform.rotation.slerp(rot, weight); } Keyframes::Translation(keyframes) => { let translation_start = keyframes[step_start]; let translation_end = keyframes[step_start + 1]; let result = translation_start.lerp(translation_end, lerp); - transform.translation = result; + transform.translation = transform.translation.lerp(result, weight); } Keyframes::Scale(keyframes) => { let scale_start = keyframes[step_start]; let scale_end = keyframes[step_start + 1]; let result = scale_start.lerp(scale_end, lerp); - transform.scale = result; + transform.scale = transform.scale.lerp(result, weight); } } } } + } +} + +fn update_transitions(player: &mut AnimationPlayer, time: &Time) { + player.transitions.retain_mut(|animation| { + animation.current_weight -= animation.weight_decline_per_sec * time.delta_seconds(); + animation.current_weight > 0.0 }); } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 16caea1be6c47..66f38cbd44ee2 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -67,7 +67,7 @@ pub struct App { /// the application's event loop and advancing the [`Schedule`]. /// Typically, it is not configured manually, but set by one of Bevy's built-in plugins. /// See `bevy::winit::WinitPlugin` and [`ScheduleRunnerPlugin`](crate::schedule_runner::ScheduleRunnerPlugin). - pub runner: Box, + pub runner: Box, // Send bound is required to make App Send /// A container of [`Stage`]s set to be run in a linear order. pub schedule: Schedule, sub_apps: HashMap, @@ -87,10 +87,68 @@ impl Debug for App { } } -/// Each `SubApp` has its own [`Schedule`] and [`World`], enabling a separation of concerns. -struct SubApp { - app: App, - runner: Box, +/// A [`SubApp`] contains its own [`Schedule`] and [`World`] separate from the main [`App`]. +/// This is useful for situations where data and data processing should be kept completely separate +/// from the main application. The primary use of this feature in bevy is to enable pipelined rendering. +/// +/// # Example +/// +/// ```rust +/// # use bevy_app::{App, AppLabel}; +/// # use bevy_ecs::prelude::*; +/// +/// #[derive(Resource, Default)] +/// struct Val(pub i32); +/// +/// #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] +/// struct ExampleApp; +/// +/// #[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] +/// struct ExampleStage; +/// +/// let mut app = App::empty(); +/// // initialize the main app with a value of 0; +/// app.insert_resource(Val(10)); +/// +/// // create a app with a resource and a single stage +/// let mut sub_app = App::empty(); +/// sub_app.insert_resource(Val(100)); +/// let mut example_stage = SystemStage::single_threaded(); +/// example_stage.add_system(|counter: Res| { +/// // since we assigned the value from the main world in extract +/// // we see that value instead of 100 +/// assert_eq!(counter.0, 10); +/// }); +/// sub_app.add_stage(ExampleStage, example_stage); +/// +/// // add the sub_app to the app +/// app.add_sub_app(ExampleApp, sub_app, |main_world, sub_app| { +/// sub_app.world.resource_mut::().0 = main_world.resource::().0; +/// }); +/// +/// // This will run the schedules once, since we're using the default runner +/// app.run(); +/// ``` +pub struct SubApp { + /// The [`SubApp`]'s instance of [`App`] + pub app: App, + + /// A function that allows access to both the [`SubApp`] [`World`] and the main [`App`]. This is + /// useful for moving data between the sub app and the main app. + pub extract: Box, +} + +impl SubApp { + /// Runs the `SubApp`'s schedule. + pub fn run(&mut self) { + self.app.schedule.run(&mut self.app.world); + self.app.world.clear_trackers(); + } + + /// Extracts data from main world to this sub-app. + pub fn extract(&mut self, main_world: &mut World) { + (self.extract)(main_world, &mut self.app); + } } impl Debug for SubApp { @@ -148,13 +206,16 @@ impl App { /// /// See [`add_sub_app`](Self::add_sub_app) and [`run_once`](Schedule::run_once) for more details. pub fn update(&mut self) { - #[cfg(feature = "trace")] - let _bevy_frame_update_span = info_span!("frame").entered(); - self.schedule.run(&mut self.world); - - for sub_app in self.sub_apps.values_mut() { - (sub_app.runner)(&mut self.world, &mut sub_app.app); - sub_app.app.world.clear_trackers(); + { + #[cfg(feature = "trace")] + let _bevy_frame_update_span = info_span!("main app").entered(); + self.schedule.run(&mut self.world); + } + for (_label, sub_app) in self.sub_apps.iter_mut() { + #[cfg(feature = "trace")] + let _sub_app_span = info_span!("sub app", name = ?_label).entered(); + sub_app.extract(&mut self.world); + sub_app.run(); } self.world.clear_trackers(); @@ -165,6 +226,24 @@ impl App { /// Finalizes the [`App`] configuration. For general usage, see the example on the item /// level documentation. /// + /// # `run()` might not return + /// + /// Calls to [`App::run()`] might never return. + /// + /// In simple and *headless* applications, one can expect that execution will + /// proceed, normally, after calling [`run()`](App::run()) but this is not the case for + /// windowed applications. + /// + /// Windowed apps are typically driven by an *event loop* or *message loop* and + /// some window-manager APIs expect programs to terminate when their primary + /// window is closed and that event loop terminates – behaviour of processes that + /// do not is often platform dependent or undocumented. + /// + /// By default, *Bevy* uses the `winit` crate for window creation. See + /// [`WinitSettings::return_from_run`](https://docs.rs/bevy/latest/bevy/winit/struct.WinitSettings.html#structfield.return_from_run) + /// for further discussion of this topic and for a mechanism to require that [`App::run()`] + /// *does* return – albeit one that carries its own caveats and disclaimers. + /// /// # Panics /// /// Panics if called from `Plugin::build()`, because it would prevent other plugins to properly build. @@ -176,6 +255,14 @@ impl App { if app.is_building_plugin { panic!("App::run() was called from within Plugin::Build(), which is not allowed."); } + + // temporarily remove the plugin registry to run each plugin's setup function on app. + let mut plugin_registry = std::mem::take(&mut app.plugin_registry); + for plugin in &plugin_registry { + plugin.setup(&mut app); + } + std::mem::swap(&mut app.plugin_registry, &mut plugin_registry); + let runner = std::mem::replace(&mut app.runner, Box::new(run_once)); (runner)(app); } @@ -811,7 +898,7 @@ impl App { /// App::new() /// .set_runner(my_runner); /// ``` - pub fn set_runner(&mut self, run_fn: impl Fn(App) + 'static) -> &mut Self { + pub fn set_runner(&mut self, run_fn: impl Fn(App) + 'static + Send) -> &mut Self { self.runner = Box::new(run_fn); self } @@ -851,8 +938,7 @@ impl App { match self.add_boxed_plugin(Box::new(plugin)) { Ok(app) => app, Err(AppError::DuplicatePlugin { plugin_name }) => panic!( - "Error adding plugin {}: : plugin was already added in application", - plugin_name + "Error adding plugin {plugin_name}: : plugin was already added in application" ), } } @@ -997,20 +1083,21 @@ impl App { /// Adds an [`App`] as a child of the current one. /// - /// The provided function `f` is called by the [`update`](Self::update) method. The [`World`] + /// The provided function `extract` is normally called by the [`update`](Self::update) method. + /// After extract is called, the [`Schedule`] of the sub app is run. The [`World`] /// parameter represents the main app world, while the [`App`] parameter is just a mutable /// reference to the `SubApp` itself. pub fn add_sub_app( &mut self, label: impl AppLabel, app: App, - sub_app_runner: impl Fn(&mut World, &mut App) + 'static, + extract: impl Fn(&mut World, &mut App) + 'static + Send, ) -> &mut Self { self.sub_apps.insert( label.as_label(), SubApp { app, - runner: Box::new(sub_app_runner), + extract: Box::new(extract), }, ); self @@ -1050,6 +1137,16 @@ impl App { } } + /// Inserts an existing sub app into the app + pub fn insert_sub_app(&mut self, label: impl AppLabel, sub_app: SubApp) { + self.sub_apps.insert(label.as_label(), sub_app); + } + + /// Removes a sub app from the app. Returns [`None`] if the label doesn't exist. + pub fn remove_sub_app(&mut self, label: impl AppLabel) -> Option { + self.sub_apps.remove(&label.as_label()) + } + /// Retrieves a `SubApp` inside this [`App`] with the given label, if it exists. Otherwise returns /// an [`Err`] containing the given label. pub fn get_sub_app(&self, label: impl AppLabel) -> Result<&App, impl AppLabel> { diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index 7e6e0c575a4d7..22bc37e00eb2a 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -10,16 +10,25 @@ use std::any::Any; /// can only be added once to an [`App`]. /// /// If the plugin may need to be added twice or more, the function [`is_unique()`](Self::is_unique) -/// should be overriden to return `false`. Plugins are considered duplicate if they have the same +/// should be overridden to return `false`. Plugins are considered duplicate if they have the same /// [`name()`](Self::name). The default `name()` implementation returns the type name, which means /// generic plugins with different type parameters will not be considered duplicates. pub trait Plugin: Downcast + Any + Send + Sync { /// Configures the [`App`] to which this plugin is added. fn build(&self, app: &mut App); + + /// Runs after all plugins are built, but before the app runner is called. + /// This can be useful if you have some resource that other plugins need during their build step, + /// but after build you want to remove it and send it to another thread. + fn setup(&self, _app: &mut App) { + // do nothing + } + /// Configures a name for the [`Plugin`] which is primarily used for debugging. fn name(&self) -> &str { std::any::type_name::() } + /// If the plugin can be meaningfully instantiated several times in an [`App`](crate::App), /// override this method to return `false`. fn is_unique(&self) -> bool { diff --git a/crates/bevy_asset/src/asset_server.rs b/crates/bevy_asset/src/asset_server.rs index 97c3ede26e623..41c91fe7c7f4f 100644 --- a/crates/bevy_asset/src/asset_server.rs +++ b/crates/bevy_asset/src/asset_server.rs @@ -284,7 +284,7 @@ impl AssetServer { /// to look for loaders of `bar.baz` and `baz` assets. /// /// By default the `ROOT` is the directory of the Application, but this can be overridden by - /// setting the `"CARGO_MANIFEST_DIR"` environment variable + /// setting the `"BEVY_ASSET_ROOT"` or `"CARGO_MANIFEST_DIR"` environment variable /// (see ) /// to another directory. When the application is run through Cargo, then /// `"CARGO_MANIFEST_DIR"` is automatically set to the root folder of your crate (workspace). @@ -387,7 +387,7 @@ impl AssetServer { return Err(err); } - // if version has changed since we loaded and grabbed a lock, return. theres is a newer + // if version has changed since we loaded and grabbed a lock, return. there is a newer // version being loaded let mut asset_sources = self.server.asset_sources.write(); let source_info = asset_sources diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index dd3d4727e4261..1a3a300553990 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -417,7 +417,7 @@ macro_rules! load_internal_asset { let mut debug_app = $app .world .non_send_resource_mut::<$crate::debug_asset_server::DebugAssetApp>(); - $crate::debug_asset_server::register_handle_with_loader( + $crate::debug_asset_server::register_handle_with_loader::<_, &'static str>( $loader, &mut debug_app, $handle, @@ -455,7 +455,7 @@ macro_rules! load_internal_binary_asset { let mut debug_app = $app .world .non_send_resource_mut::<$crate::debug_asset_server::DebugAssetApp>(); - $crate::debug_asset_server::register_handle_with_loader( + $crate::debug_asset_server::register_handle_with_loader::<_, &'static [u8]>( $loader, &mut debug_app, $handle, diff --git a/crates/bevy_asset/src/debug_asset_server.rs b/crates/bevy_asset/src/debug_asset_server.rs index 690dd6769dc7c..e8809d5269c58 100644 --- a/crates/bevy_asset/src/debug_asset_server.rs +++ b/crates/bevy_asset/src/debug_asset_server.rs @@ -116,8 +116,8 @@ pub(crate) fn sync_debug_assets( /// /// If this feels a bit odd ... that's because it is. This was built to improve the UX of the /// `load_internal_asset` macro. -pub fn register_handle_with_loader( - _loader: fn(&'static str) -> A, +pub fn register_handle_with_loader( + _loader: fn(T) -> A, app: &mut DebugAssetApp, handle: HandleUntyped, file_path: &str, diff --git a/crates/bevy_asset/src/io/file_asset_io.rs b/crates/bevy_asset/src/io/file_asset_io.rs index 59447dfac5031..e4f9a57bba0cd 100644 --- a/crates/bevy_asset/src/io/file_asset_io.rs +++ b/crates/bevy_asset/src/io/file_asset_io.rs @@ -60,10 +60,14 @@ impl FileAssetIo { /// Returns the base path of the assets directory, which is normally the executable's parent /// directory. /// - /// If the `CARGO_MANIFEST_DIR` environment variable is set, then its value will be used + /// If a `BEVY_ASSET_ROOT` environment variable is set, then its value will be used. + /// + /// Else if the `CARGO_MANIFEST_DIR` environment variable is set, then its value will be used /// instead. It's set by cargo when running with `cargo run`. pub fn get_base_path() -> PathBuf { - if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { + if let Ok(env_bevy_asset_root) = env::var("BEVY_ASSET_ROOT") { + PathBuf::from(env_bevy_asset_root) + } else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") { PathBuf::from(manifest_dir) } else { env::current_exe() diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 81b22b5603999..3d01ac148d635 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -94,13 +94,13 @@ impl ReflectAsset { } /// Equivalent of [`Assets::add`] - pub fn add<'w>(&self, world: &'w mut World, value: &dyn Reflect) -> HandleUntyped { + pub fn add(&self, world: &mut World, value: &dyn Reflect) -> HandleUntyped { (self.add)(world, value) } /// Equivalent of [`Assets::set`] - pub fn set<'w>( + pub fn set( &self, - world: &'w mut World, + world: &mut World, handle: HandleUntyped, value: &dyn Reflect, ) -> HandleUntyped { diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index 3d5f9d89d04e7..eca14a2c34ba1 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -30,3 +30,10 @@ mp3 = ["rodio/mp3"] flac = ["rodio/flac"] wav = ["rodio/wav"] vorbis = ["rodio/vorbis"] +symphonia-aac = ["rodio/symphonia-aac"] +symphonia-all = ["rodio/symphonia-all"] +symphonia-flac = ["rodio/symphonia-flac"] +symphonia-isomp4 = ["rodio/symphonia-isomp4"] +symphonia-mp3 = ["rodio/symphonia-mp3"] +symphonia-vorbis = ["rodio/symphonia-vorbis"] +symphonia-wav = ["rodio/symphonia-wav"] diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 470eccd04f59e..dda555f13a4de 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; +use bevy_asset::{Asset, AssetLoader, LoadContext, LoadedAsset}; use bevy_reflect::TypeUuid; use bevy_utils::BoxedFuture; use std::{io::Cursor, sync::Arc}; @@ -63,14 +63,24 @@ impl AssetLoader for AudioLoader { } } -/// A type implementing this trait can be decoded as a rodio source +/// A type implementing this trait can be converted to a [`rodio::Source`] type. +/// It must be [`Send`] and [`Sync`], and usually implements [`Asset`] so needs to be [`TypeUuid`], +/// in order to be registered. +/// Types that implement this trait usually contain raw sound data that can be converted into an iterator of samples. +/// This trait is implemented for [`AudioSource`]. +/// Check the example `audio/decodable` for how to implement this trait on a custom type. pub trait Decodable: Send + Sync + 'static { - /// The decoder that can decode the implementing type - type Decoder: rodio::Source + Send + Iterator; - /// A single value given by the decoder + /// The type of the audio samples. + /// Usually a [`u16`], [`i16`] or [`f32`], as those implement [`rodio::Sample`]. + /// Other types can implement the [`rodio::Sample`] trait as well. type DecoderItem: rodio::Sample + Send + Sync; - /// Build and return a [`Self::Decoder`] for the implementing type + /// The type of the iterator of the audio samples, + /// which iterates over samples of type [`Self::DecoderItem`]. + /// Must be a [`rodio::Source`] so that it can provide information on the audio it is iterating over. + type Decoder: rodio::Source + Send + Iterator; + + /// Build and return a [`Self::Decoder`] of the implementing type fn decoder(&self) -> Self::Decoder; } @@ -82,3 +92,17 @@ impl Decodable for AudioSource { rodio::Decoder::new(Cursor::new(self.clone())).unwrap() } } + +/// A trait that allows adding a custom audio source to the object. +/// This is implemented for [`App`][bevy_app::App] to allow registering custom [`Decodable`] types. +pub trait AddAudioSource { + /// Registers an audio source. + /// The type must implement [`Decodable`], + /// so that it can be converted to a [`rodio::Source`] type, + /// and [`Asset`], so that it can be registered as an asset. + /// To use this method on [`App`][bevy_app::App], + /// the [audio][super::AudioPlugin] and [asset][bevy_asset::AssetPlugin] plugins must be added first. + fn add_audio_source(&mut self) -> &mut Self + where + T: Decodable + Asset; +} diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 9fc4a5484b88c..eb7e3b3222901 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -35,12 +35,13 @@ pub mod prelude { pub use audio::*; pub use audio_output::*; pub use audio_source::*; + pub use rodio::cpal::Sample as CpalSample; pub use rodio::source::Source; pub use rodio::Sample; use bevy_app::prelude::*; -use bevy_asset::AddAsset; +use bevy_asset::{AddAsset, Asset}; /// Adds support for audio playback to a Bevy Application /// @@ -63,3 +64,15 @@ impl Plugin for AudioPlugin { app.init_asset_loader::(); } } + +impl AddAudioSource for App { + fn add_audio_source(&mut self) -> &mut Self + where + T: Decodable + Asset, + { + self.add_asset::() + .init_resource::>() + .init_resource::>() + .add_system_to_stage(CoreStage::PostUpdate, play_queued_audio_system::) + } +} diff --git a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl index 0e6addeef3ee3..a6bab621db520 100644 --- a/crates/bevy_core_pipeline/src/bloom/bloom.wgsl +++ b/crates/bevy_core_pipeline/src/bloom/bloom.wgsl @@ -5,6 +5,7 @@ struct BloomUniforms { knee: f32, scale: f32, intensity: f32, + viewport: vec4, }; @group(0) @binding(0) @@ -87,7 +88,8 @@ fn sample_original_3x3_tent(uv: vec2, scale: vec2) -> vec4 { } @fragment -fn downsample_prefilter(@location(0) uv: vec2) -> @location(0) vec4 { +fn downsample_prefilter(@location(0) output_uv: vec2) -> @location(0) vec4 { + let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw; let texel_size = 1.0 / vec2(textureDimensions(original)); let scale = texel_size; @@ -98,7 +100,7 @@ fn downsample_prefilter(@location(0) uv: vec2) -> @location(0) vec4 { 0.25 / uniforms.knee, ); - var o: vec4 = sample_13_tap(uv, scale); + var o: vec4 = sample_13_tap(sample_uv, scale); o = quadratic_threshold(o, uniforms.threshold, curve); o = max(o, vec4(0.00001)); diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 0700c3b16b3ae..ad438b2a25dcf 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, world::{FromWorld, World}, }; -use bevy_math::UVec2; +use bevy_math::{UVec2, UVec4, Vec4}; use bevy_reflect::{Reflect, TypeUuid}; use bevy_render::{ camera::ExtractedCamera, @@ -17,7 +17,6 @@ use bevy_render::{ }, prelude::Camera, render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, - render_phase::TrackedRenderPass, render_resource::*, renderer::{RenderContext, RenderDevice}, texture::{CachedTexture, TextureCache}, @@ -153,18 +152,27 @@ impl ExtractComponent for BloomSettings { return None; } - camera.physical_viewport_size().map(|size| { + if let (Some((origin, _)), Some(size), Some(target_size)) = ( + camera.physical_viewport_rect(), + camera.physical_viewport_size(), + camera.physical_target_size(), + ) { let min_view = size.x.min(size.y) / 2; let mip_count = calculate_mip_count(min_view); let scale = (min_view / 2u32.pow(mip_count)) as f32 / 8.0; - BloomUniform { + Some(BloomUniform { threshold: settings.threshold, knee: settings.knee, scale: settings.scale * scale, intensity: settings.intensity, - } - }) + viewport: UVec4::new(origin.x, origin.y, size.x, size.y).as_vec4() + / UVec4::new(target_size.x, target_size.y, target_size.x, target_size.y) + .as_vec4(), + }) + } else { + None + } } } @@ -232,95 +240,78 @@ impl Node for BloomNode { { let view = &BloomTextures::texture_view(&textures.texture_a, 0); let mut prefilter_pass = - TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( - &RenderPassDescriptor { - label: Some("bloom_prefilter_pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view, - resolve_target: None, - ops: Operations::default(), - })], - depth_stencil_attachment: None, - }, - )); + render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("bloom_prefilter_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }); prefilter_pass.set_render_pipeline(downsampling_prefilter_pipeline); prefilter_pass.set_bind_group( 0, &bind_groups.prefilter_bind_group, &[uniform_index.index()], ); - if let Some(viewport) = camera.viewport.as_ref() { - prefilter_pass.set_camera_viewport(viewport); - } prefilter_pass.draw(0..3, 0..1); } for mip in 1..textures.mip_count { let view = &BloomTextures::texture_view(&textures.texture_a, mip); let mut downsampling_pass = - TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( - &RenderPassDescriptor { - label: Some("bloom_downsampling_pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view, - resolve_target: None, - ops: Operations::default(), - })], - depth_stencil_attachment: None, - }, - )); + render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("bloom_downsampling_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }); downsampling_pass.set_render_pipeline(downsampling_pipeline); downsampling_pass.set_bind_group( 0, &bind_groups.downsampling_bind_groups[mip as usize - 1], &[uniform_index.index()], ); - if let Some(viewport) = camera.viewport.as_ref() { - downsampling_pass.set_camera_viewport(viewport); - } downsampling_pass.draw(0..3, 0..1); } for mip in (1..textures.mip_count).rev() { let view = &BloomTextures::texture_view(&textures.texture_b, mip - 1); let mut upsampling_pass = - TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( - &RenderPassDescriptor { - label: Some("bloom_upsampling_pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view, - resolve_target: None, - ops: Operations::default(), - })], - depth_stencil_attachment: None, - }, - )); + render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("bloom_upsampling_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view, + resolve_target: None, + ops: Operations::default(), + })], + depth_stencil_attachment: None, + }); upsampling_pass.set_render_pipeline(upsampling_pipeline); upsampling_pass.set_bind_group( 0, &bind_groups.upsampling_bind_groups[mip as usize - 1], &[uniform_index.index()], ); - if let Some(viewport) = camera.viewport.as_ref() { - upsampling_pass.set_camera_viewport(viewport); - } upsampling_pass.draw(0..3, 0..1); } { let mut upsampling_final_pass = - TrackedRenderPass::new(render_context.command_encoder.begin_render_pass( - &RenderPassDescriptor { - label: Some("bloom_upsampling_final_pass"), - color_attachments: &[Some(view_target.get_unsampled_color_attachment( - Operations { - load: LoadOp::Load, - store: true, - }, - ))], - depth_stencil_attachment: None, - }, - )); + render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("bloom_upsampling_final_pass"), + color_attachments: &[Some(view_target.get_unsampled_color_attachment( + Operations { + load: LoadOp::Load, + store: true, + }, + ))], + depth_stencil_attachment: None, + }); upsampling_final_pass.set_render_pipeline(upsampling_final_pipeline); upsampling_final_pass.set_bind_group( 0, @@ -443,7 +434,7 @@ impl FromWorld for BloomPipelines { ], }); - let mut pipeline_cache = world.resource_mut::(); + let pipeline_cache = world.resource::(); let downsampling_prefilter_pipeline = pipeline_cache.queue_render_pipeline(RenderPipelineDescriptor { @@ -621,6 +612,7 @@ pub struct BloomUniform { knee: f32, scale: f32, intensity: f32, + viewport: Vec4, } #[derive(Component)] diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index 2a262b426195b..b5660c4c0aa58 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -3,7 +3,6 @@ use crate::{ core_2d::{camera_2d::Camera2d, Transparent2d}, }; use bevy_ecs::prelude::*; -use bevy_render::render_phase::TrackedRenderPass; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, @@ -63,7 +62,8 @@ impl Node for MainPass2dNode { { #[cfg(feature = "trace")] let _main_pass_2d = info_span!("main_pass_2d").entered(); - let pass_descriptor = RenderPassDescriptor { + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_pass_2d"), color_attachments: &[Some(target.get_color_attachment(Operations { load: match camera_2d.clear_color { @@ -76,12 +76,7 @@ impl Node for MainPass2dNode { store: true, }))], depth_stencil_attachment: None, - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); @@ -106,7 +101,7 @@ impl Node for MainPass2dNode { }; render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); } diff --git a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs index 1067643e03b12..5003fbfd538f1 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs @@ -1,9 +1,9 @@ use crate::{ clear_color::{ClearColor, ClearColorConfig}, core_3d::{AlphaMask3d, Camera3d, Opaque3d, Transparent3d}, + prepass::{DepthPrepass, NormalPrepass}, }; use bevy_ecs::prelude::*; -use bevy_render::render_phase::TrackedRenderPass; use bevy_render::{ camera::ExtractedCamera, render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, @@ -15,6 +15,8 @@ use bevy_render::{ #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; +use super::Camera3dDepthLoadOp; + pub struct MainPass3dNode { query: QueryState< ( @@ -25,6 +27,8 @@ pub struct MainPass3dNode { &'static Camera3d, &'static ViewTarget, &'static ViewDepthTexture, + Option<&'static DepthPrepass>, + Option<&'static NormalPrepass>, ), With, >, @@ -56,13 +60,20 @@ impl Node for MainPass3dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (camera, opaque_phase, alpha_mask_phase, transparent_phase, camera_3d, target, depth) = - match self.query.get_manual(world, view_entity) { - Ok(query) => query, - Err(_) => { - return Ok(()); - } // No window - }; + let Ok(( + camera, + opaque_phase, + alpha_mask_phase, + transparent_phase, + camera_3d, + target, + depth, + depth_prepass, + normal_prepass, + )) = self.query.get_manual(world, view_entity) else { + // No window + return Ok(()); + }; // Always run opaque pass to ensure screen is cleared { @@ -70,7 +81,8 @@ impl Node for MainPass3dNode { // NOTE: Scoped to drop the mutable borrow of render_context #[cfg(feature = "trace")] let _main_opaque_pass_3d_span = info_span!("main_opaque_pass_3d").entered(); - let pass_descriptor = RenderPassDescriptor { + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_opaque_pass_3d"), // NOTE: The opaque pass loads the color // buffer as well as writing to it. @@ -88,18 +100,20 @@ impl Node for MainPass3dNode { view: &depth.view, // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it depth_ops: Some(Operations { - // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. - load: camera_3d.depth_load_op.clone().into(), + load: if depth_prepass.is_some() || normal_prepass.is_some() { + // if any prepass runs, it will generate a depth buffer so we should use it, + // even if only the normal_prepass is used. + Camera3dDepthLoadOp::Load + } else { + // NOTE: 0.0 is the far plane due to bevy's use of reverse-z projections. + camera_3d.depth_load_op.clone() + } + .into(), store: true, }), stencil_ops: None, }), - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); @@ -113,7 +127,8 @@ impl Node for MainPass3dNode { // NOTE: Scoped to drop the mutable borrow of render_context #[cfg(feature = "trace")] let _main_alpha_mask_pass_3d_span = info_span!("main_alpha_mask_pass_3d").entered(); - let pass_descriptor = RenderPassDescriptor { + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_alpha_mask_pass_3d"), // NOTE: The alpha_mask pass loads the color buffer as well as overwriting it where appropriate. color_attachments: &[Some(target.get_color_attachment(Operations { @@ -129,12 +144,7 @@ impl Node for MainPass3dNode { }), stencil_ops: None, }), - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); @@ -148,7 +158,8 @@ impl Node for MainPass3dNode { // NOTE: Scoped to drop the mutable borrow of render_context #[cfg(feature = "trace")] let _main_transparent_pass_3d_span = info_span!("main_transparent_pass_3d").entered(); - let pass_descriptor = RenderPassDescriptor { + + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("main_transparent_pass_3d"), // NOTE: The transparent pass loads the color buffer as well as overwriting it where appropriate. color_attachments: &[Some(target.get_color_attachment(Operations { @@ -169,12 +180,7 @@ impl Node for MainPass3dNode { }), stencil_ops: None, }), - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); @@ -199,7 +205,7 @@ impl Node for MainPass3dNode { }; render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 34a430a345398..1da08fc83193c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -7,6 +7,7 @@ pub mod graph { pub const VIEW_ENTITY: &str = "view_entity"; } pub mod node { + pub const PREPASS: &str = "prepass"; pub const MAIN_PASS: &str = "main_pass"; pub const BLOOM: &str = "bloom"; pub const TONEMAPPING: &str = "tonemapping"; @@ -43,7 +44,11 @@ use bevy_render::{ }; use bevy_utils::{FloatOrd, HashMap}; -use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode}; +use crate::{ + prepass::{node::PrepassNode, DepthPrepass}, + tonemapping::TonemappingNode, + upscaling::UpscalingNode, +}; pub struct Core3dPlugin; @@ -68,20 +73,29 @@ impl Plugin for Core3dPlugin { .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::); + let prepass_node = PrepassNode::new(&mut render_app.world); let pass_node_3d = MainPass3dNode::new(&mut render_app.world); let tonemapping = TonemappingNode::new(&mut render_app.world); let upscaling = UpscalingNode::new(&mut render_app.world); let mut graph = render_app.world.resource_mut::(); let mut draw_3d_graph = RenderGraph::default(); + draw_3d_graph.add_node(graph::node::PREPASS, prepass_node); draw_3d_graph.add_node(graph::node::MAIN_PASS, pass_node_3d); draw_3d_graph.add_node(graph::node::TONEMAPPING, tonemapping); draw_3d_graph.add_node(graph::node::END_MAIN_PASS_POST_PROCESSING, EmptyNode); draw_3d_graph.add_node(graph::node::UPSCALING, upscaling); + let input_node_id = draw_3d_graph.set_input(vec![SlotInfo::new( graph::input::VIEW_ENTITY, SlotType::Entity, )]); + draw_3d_graph.add_slot_edge( + input_node_id, + graph::input::VIEW_ENTITY, + graph::node::PREPASS, + PrepassNode::IN_VIEW, + ); draw_3d_graph.add_slot_edge( input_node_id, graph::input::VIEW_ENTITY, @@ -100,6 +114,7 @@ impl Plugin for Core3dPlugin { graph::node::UPSCALING, UpscalingNode::IN_VIEW, ); + draw_3d_graph.add_node_edge(graph::node::PREPASS, graph::node::MAIN_PASS); draw_3d_graph.add_node_edge(graph::node::MAIN_PASS, graph::node::TONEMAPPING); draw_3d_graph.add_node_edge( graph::node::TONEMAPPING, @@ -253,7 +268,7 @@ pub fn prepare_core_3d_depth_textures( msaa: Res, render_device: Res, views_3d: Query< - (Entity, &ExtractedCamera), + (Entity, &ExtractedCamera, Option<&DepthPrepass>), ( With>, With>, @@ -262,34 +277,46 @@ pub fn prepare_core_3d_depth_textures( >, ) { let mut textures = HashMap::default(); - for (entity, camera) in &views_3d { - if let Some(physical_target_size) = camera.physical_target_size { - let cached_texture = textures - .entry(camera.target.clone()) - .or_insert_with(|| { - texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("view_depth_texture"), - size: Extent3d { - depth_or_array_layers: 1, - width: physical_target_size.x, - height: physical_target_size.y, - }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24 - * bit depth for better performance */ - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ) - }) - .clone(); - commands.entity(entity).insert(ViewDepthTexture { - texture: cached_texture.texture, - view: cached_texture.default_view, - }); - } + for (entity, camera, depth_prepass) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let cached_texture = textures + .entry(camera.target.clone()) + .or_insert_with(|| { + // Default usage required to write to the depth texture + let mut usage = TextureUsages::RENDER_ATTACHMENT; + if depth_prepass.is_some() { + // Required to read the output of the prepass + usage |= TextureUsages::COPY_SRC; + } + + // The size of the depth texture + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let descriptor = TextureDescriptor { + label: Some("view_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + // PERF: vulkan docs recommend using 24 bit depth for better performance + format: TextureFormat::Depth32Float, + usage, + }; + + texture_cache.get(&render_device, descriptor) + }) + .clone(); + + commands.entity(entity).insert(ViewDepthTexture { + texture: cached_texture.texture, + view: cached_texture.default_view, + }); } } diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs index f49425e7250c4..4b2a7a2ea9401 100644 --- a/crates/bevy_core_pipeline/src/fxaa/mod.rs +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -47,7 +47,8 @@ pub struct Fxaa { /// Use lower sensitivity for a sharper, faster, result. /// Use higher sensitivity for a slower, smoother, result. - /// Ultra and Turbo settings can result in significant smearing and loss of detail. + /// [Ultra](`Sensitivity::Ultra`) and [Extreme](`Sensitivity::Extreme`) + /// settings can result in significant smearing and loss of detail. /// The minimum amount of local contrast required to apply algorithm. pub edge_threshold: Sensitivity, @@ -222,7 +223,7 @@ impl SpecializedRenderPipeline for FxaaPipeline { pub fn prepare_fxaa_pipelines( mut commands: Commands, - mut pipeline_cache: ResMut, + pipeline_cache: Res, mut pipelines: ResMut>, fxaa_pipeline: Res, views: Query<(Entity, &ExtractedView, &Fxaa)>, @@ -232,7 +233,7 @@ pub fn prepare_fxaa_pipelines( continue; } let pipeline_id = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &fxaa_pipeline, FxaaPipelineKey { edge_threshold: fxaa.edge_threshold, diff --git a/crates/bevy_core_pipeline/src/fxaa/node.rs b/crates/bevy_core_pipeline/src/fxaa/node.rs index 6e12151c2fe63..5050e3c4b3920 100644 --- a/crates/bevy_core_pipeline/src/fxaa/node.rs +++ b/crates/bevy_core_pipeline/src/fxaa/node.rs @@ -78,7 +78,7 @@ impl Node for FxaaNode { Some((id, bind_group)) if source.id() == *id => bind_group, cached_bind_group => { let sampler = render_context - .render_device + .render_device() .create_sampler(&SamplerDescriptor { mipmap_filter: FilterMode::Linear, mag_filter: FilterMode::Linear, @@ -88,7 +88,7 @@ impl Node for FxaaNode { let bind_group = render_context - .render_device + .render_device() .create_bind_group(&BindGroupDescriptor { label: None, layout: &fxaa_pipeline.texture_bind_group, @@ -120,7 +120,7 @@ impl Node for FxaaNode { }; let mut render_pass = render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); render_pass.set_pipeline(pipeline); diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index adfe9d500f038..5b0fe9eaea21c 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -4,6 +4,7 @@ pub mod core_2d; pub mod core_3d; pub mod fullscreen_vertex_shader; pub mod fxaa; +pub mod prepass; pub mod tonemapping; pub mod upscaling; @@ -23,6 +24,7 @@ use crate::{ core_3d::Core3dPlugin, fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, fxaa::FxaaPlugin, + prepass::{DepthPrepass, NormalPrepass}, tonemapping::TonemappingPlugin, upscaling::UpscalingPlugin, }; @@ -44,6 +46,8 @@ impl Plugin for CorePipelinePlugin { app.register_type::() .register_type::() + .register_type::() + .register_type::() .init_resource::() .add_plugin(ExtractResourcePlugin::::default()) .add_plugin(Core2dPlugin) diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs new file mode 100644 index 0000000000000..a3d05259d59cd --- /dev/null +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -0,0 +1,147 @@ +//! Run a prepass before the main pass to generate depth and/or normals texture, sometimes called a thin g-buffer. +//! These textures are useful for various screen-space effects and reducing overdraw in the main pass. +//! +//! The prepass only runs for opaque meshes or meshes with an alpha mask. Transparent meshes are ignored. +//! +//! To enable the prepass, you need to add a prepass component to a [`crate::prelude::Camera3d`]. +//! +//! [`DepthPrepass`] +//! [`NormalPrepass`] +//! +//! The textures are automatically added to the default mesh view bindings. You can also get the raw textures +//! by querying the [`ViewPrepassTextures`] component on any camera with a prepass component. +//! +//! The depth prepass will always run and generate the depth buffer as a side effect, but it won't copy it +//! to a separate texture unless the [`DepthPrepass`] is activated. This means that if any prepass component is present +//! it will always create a depth buffer that will be used by the main pass. +//! +//! When using the default mesh view bindings you should be able to use `prepass_depth()` +//! and `prepass_normal()` to load the related textures. These functions are defined in `bevy_pbr::utils`. +//! See the `shader_prepass` example that shows how to use it. +//! +//! The prepass runs for each `Material`. You can control if the prepass should run per-material by setting the `prepass_enabled` +//! flag on the `MaterialPlugin`. +//! +//! Currently only works for 3D. + +pub mod node; + +use bevy_ecs::prelude::*; +use bevy_reflect::Reflect; +use bevy_render::{ + render_phase::{CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem}, + render_resource::{CachedRenderPipelineId, Extent3d, TextureFormat}, + texture::CachedTexture, +}; +use bevy_utils::FloatOrd; + +pub const DEPTH_PREPASS_FORMAT: TextureFormat = TextureFormat::Depth32Float; +pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm; + +/// If added to a [`crate::prelude::Camera3d`] then depth values will be copied to a separate texture available to the main pass. +#[derive(Component, Default, Reflect)] +pub struct DepthPrepass; + +/// If added to a [`crate::prelude::Camera3d`] then vertex world normals will be copied to a separate texture available to the main pass. +/// Normals will have normal map textures already applied. +#[derive(Component, Default, Reflect)] +pub struct NormalPrepass; + +/// Textures that are written to by the prepass. +/// +/// This component will only be present if any of the relevant prepass components are also present. +#[derive(Component)] +pub struct ViewPrepassTextures { + /// The depth texture generated by the prepass. + /// Exists only if [`DepthPrepass`] is added to the `ViewTarget` + pub depth: Option, + /// The normals texture generated by the prepass. + /// Exists only if [`NormalPrepass`] is added to the `ViewTarget` + pub normal: Option, + /// The size of the textures. + pub size: Extent3d, +} + +/// Opaque phase of the 3D prepass. +/// +/// Sorted front-to-back by the z-distance in front of the camera. +/// +/// Used to render all 3D meshes with materials that have no transparency. +pub struct Opaque3dPrepass { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for Opaque3dPrepass { + type SortKey = FloatOrd; + + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + +impl CachedRenderPipelinePhaseItem for Opaque3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} + +/// Alpha mask phase of the 3D prepass. +/// +/// Sorted front-to-back by the z-distance in front of the camera. +/// +/// Used to render all meshes with a material with an alpha mask. +pub struct AlphaMask3dPrepass { + pub distance: f32, + pub entity: Entity, + pub pipeline_id: CachedRenderPipelineId, + pub draw_function: DrawFunctionId, +} + +impl PhaseItem for AlphaMask3dPrepass { + type SortKey = FloatOrd; + + fn entity(&self) -> Entity { + self.entity + } + + #[inline] + fn sort_key(&self) -> Self::SortKey { + FloatOrd(self.distance) + } + + #[inline] + fn draw_function(&self) -> DrawFunctionId { + self.draw_function + } + + #[inline] + fn sort(items: &mut [Self]) { + radsort::sort_by_key(items, |item| item.distance); + } +} + +impl CachedRenderPipelinePhaseItem for AlphaMask3dPrepass { + #[inline] + fn cached_pipeline(&self) -> CachedRenderPipelineId { + self.pipeline_id + } +} diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs new file mode 100644 index 0000000000000..81724ee4ba3b1 --- /dev/null +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -0,0 +1,134 @@ +use bevy_ecs::prelude::*; +use bevy_ecs::query::QueryState; +use bevy_render::{ + camera::ExtractedCamera, + prelude::Color, + render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_phase::RenderPhase, + render_resource::{ + LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, + RenderPassDescriptor, + }, + renderer::RenderContext, + view::{ExtractedView, ViewDepthTexture}, +}; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; + +use super::{AlphaMask3dPrepass, Opaque3dPrepass, ViewPrepassTextures}; + +/// Render node used by the prepass. +/// +/// By default, inserted before the main pass in the render graph. +pub struct PrepassNode { + main_view_query: QueryState< + ( + &'static ExtractedCamera, + &'static RenderPhase, + &'static RenderPhase, + &'static ViewDepthTexture, + &'static ViewPrepassTextures, + ), + With, + >, +} + +impl PrepassNode { + pub const IN_VIEW: &'static str = "view"; + + pub fn new(world: &mut World) -> Self { + Self { + main_view_query: QueryState::new(world), + } + } +} + +impl Node for PrepassNode { + fn input(&self) -> Vec { + vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] + } + + fn update(&mut self, world: &mut World) { + self.main_view_query.update_archetypes(world); + } + + fn run( + &self, + graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let Ok(( + camera, + opaque_prepass_phase, + alpha_mask_prepass_phase, + view_depth_texture, + view_prepass_textures, + )) = self.main_view_query.get_manual(world, view_entity) else { + return Ok(()); + }; + + if opaque_prepass_phase.items.is_empty() && alpha_mask_prepass_phase.items.is_empty() { + return Ok(()); + } + + let mut color_attachments = vec![]; + if let Some(view_normals_texture) = &view_prepass_textures.normal { + color_attachments.push(Some(RenderPassColorAttachment { + view: &view_normals_texture.default_view, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(Color::BLACK.into()), + store: true, + }, + })); + } + + { + // Set up the pass descriptor with the depth attachment and optional color attachments + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some("prepass"), + color_attachments: &color_attachments, + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &view_depth_texture.view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: true, + }), + stencil_ops: None, + }), + }); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + // Always run opaque pass to ensure screen is cleared + { + // Run the prepass, sorted front-to-back + #[cfg(feature = "trace")] + let _opaque_prepass_span = info_span!("opaque_prepass").entered(); + opaque_prepass_phase.render(&mut render_pass, world, view_entity); + } + + if !alpha_mask_prepass_phase.items.is_empty() { + // Run the prepass, sorted front-to-back + #[cfg(feature = "trace")] + let _alpha_mask_prepass_span = info_span!("alpha_mask_prepass").entered(); + alpha_mask_prepass_phase.render(&mut render_pass, world, view_entity); + } + } + + if let Some(prepass_depth_texture) = &view_prepass_textures.depth { + // Copy depth buffer to texture + render_context.command_encoder().copy_texture_to_texture( + view_depth_texture.texture.as_image_copy(), + prepass_depth_texture.texture.as_image_copy(), + view_prepass_textures.size, + ); + } + + Ok(()) + } +} diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 785c231a43fb9..f7233fd575102 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -126,7 +126,7 @@ pub struct ViewTonemappingPipeline(CachedRenderPipelineId); pub fn queue_view_tonemapping_pipelines( mut commands: Commands, - mut pipeline_cache: ResMut, + pipeline_cache: Res, mut pipelines: ResMut>, upscaling_pipeline: Res, view_targets: Query<(Entity, &Tonemapping)>, @@ -136,7 +136,7 @@ pub fn queue_view_tonemapping_pipelines( let key = TonemappingPipelineKey { deband_dither: *deband_dither, }; - let pipeline = pipelines.specialize(&mut pipeline_cache, &upscaling_pipeline, key); + let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); commands .entity(entity) diff --git a/crates/bevy_core_pipeline/src/tonemapping/node.rs b/crates/bevy_core_pipeline/src/tonemapping/node.rs index f9edf882c73fa..c814de5c00ed7 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/node.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/node.rs @@ -72,12 +72,12 @@ impl Node for TonemappingNode { Some((id, bind_group)) if source.id() == *id => bind_group, cached_bind_group => { let sampler = render_context - .render_device + .render_device() .create_sampler(&SamplerDescriptor::default()); let bind_group = render_context - .render_device + .render_device() .create_bind_group(&BindGroupDescriptor { label: None, layout: &tonemapping_pipeline.texture_bind_group, @@ -112,7 +112,7 @@ impl Node for TonemappingNode { }; let mut render_pass = render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); render_pass.set_pipeline(pipeline); diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index b8fa9718d000f..a25e10632cd90 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -112,7 +112,7 @@ pub struct ViewUpscalingPipeline(CachedRenderPipelineId); fn queue_view_upscaling_pipelines( mut commands: Commands, - mut pipeline_cache: ResMut, + pipeline_cache: Res, mut pipelines: ResMut>, upscaling_pipeline: Res, view_targets: Query<(Entity, &ViewTarget)>, @@ -122,7 +122,7 @@ fn queue_view_upscaling_pipelines( upscaling_mode: UpscalingMode::Filtering, texture_format: view_target.out_texture_format(), }; - let pipeline = pipelines.specialize(&mut pipeline_cache, &upscaling_pipeline, key); + let pipeline = pipelines.specialize(&pipeline_cache, &upscaling_pipeline, key); commands .entity(entity) diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 895c3e5e1b0a2..44cf195f724ca 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -63,12 +63,12 @@ impl Node for UpscalingNode { Some((id, bind_group)) if upscaled_texture.id() == *id => bind_group, cached_bind_group => { let sampler = render_context - .render_device + .render_device() .create_sampler(&SamplerDescriptor::default()); let bind_group = render_context - .render_device + .render_device() .create_bind_group(&BindGroupDescriptor { label: None, layout: &upscaling_pipeline.texture_bind_group, @@ -108,7 +108,7 @@ impl Node for UpscalingNode { }; let mut render_pass = render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); render_pass.set_pipeline(pipeline); diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 7d947359db3c3..aa2c8682588fc 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -111,7 +111,7 @@ impl Diagnostic { /// The smoothing factor used for the exponential smoothing used for /// [`smoothed`](Self::smoothed). /// - /// If measurements come in less fequently than `smoothing_factor` seconds + /// If measurements come in less frequently than `smoothing_factor` seconds /// apart, no smoothing will be applied. As measurements come in more /// frequently, the smoothing takes a greater effect such that it takes /// approximately `smoothing_factor` seconds for 83% of an instantaneous diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 000c2121a7d1c..cff59621cf887 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -23,7 +23,7 @@ bevy_ecs_macros = { path = "macros", version = "0.9.0" } async-channel = "1.4" event-listener = "2.5" thread_local = "1.1.4" -fixedbitset = "0.4" +fixedbitset = "0.4.2" fxhash = "0.2" downcast-rs = "1.2" serde = { version = "1", features = ["derive"] } diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index a75c4a92b3fe9..b051e630ebc44 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -86,8 +86,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { return Err(Error::new_spanned( m.lit, format!( - "Invalid storage type `{}`, expected '{}' or '{}'.", - s, TABLE, SPARSE_SET + "Invalid storage type `{s}`, expected '{TABLE}' or '{SPARSE_SET}'.", ), )) } diff --git a/crates/bevy_ecs/macros/src/fetch.rs b/crates/bevy_ecs/macros/src/fetch.rs index f4bd82b61a7f4..418c5741e75e2 100644 --- a/crates/bevy_ecs/macros/src/fetch.rs +++ b/crates/bevy_ecs/macros/src/fetch.rs @@ -52,8 +52,7 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { fetch_struct_attributes.is_mutable = true; } else { panic!( - "The `{}` attribute is expected to have no value or arguments", - MUTABLE_ATTRIBUTE_NAME + "The `{MUTABLE_ATTRIBUTE_NAME}` attribute is expected to have no value or arguments", ); } } else if ident == DERIVE_ATTRIBUTE_NAME { @@ -63,8 +62,7 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { .extend(meta_list.nested.iter().cloned()); } else { panic!( - "Expected a structured list within the `{}` attribute", - DERIVE_ATTRIBUTE_NAME + "Expected a structured list within the `{DERIVE_ATTRIBUTE_NAME}` attribute", ); } } else { diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index b697c9d0ba497..07d01a8c870ab 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -4,7 +4,9 @@ mod component; mod fetch; use crate::fetch::derive_world_query_impl; -use bevy_macro_utils::{derive_label, get_named_struct_fields, BevyManifest}; +use bevy_macro_utils::{ + derive_boxed_label, derive_label, derive_set, get_named_struct_fields, BevyManifest, +}; use proc_macro::TokenStream; use proc_macro2::Span; use quote::{format_ident, quote}; @@ -216,7 +218,6 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { let mut tokens = TokenStream::new(); let max_params = 8; let params = get_idents(|i| format!("P{i}"), max_params); - let params_state = get_idents(|i| format!("PF{i}"), max_params); let metas = get_idents(|i| format!("m{i}"), max_params); let mut param_fn_muts = Vec::new(); for (i, param) in params.iter().enumerate() { @@ -238,7 +239,7 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { // Conflicting params in ParamSet are not accessible at the same time // ParamSets are guaranteed to not conflict with other SystemParams unsafe { - <#param::State as SystemParamState>::get_param(&mut self.param_states.#index, &self.system_meta, self.world, self.change_tick) + #param::get_param(&mut self.param_states.#index, &self.system_meta, self.world, self.change_tick) } } }); @@ -246,36 +247,29 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { for param_count in 1..=max_params { let param = ¶ms[0..param_count]; - let param_state = ¶ms_state[0..param_count]; let meta = &metas[0..param_count]; let param_fn_mut = ¶m_fn_muts[0..param_count]; tokens.extend(TokenStream::from(quote! { - impl<'w, 's, #(#param: SystemParam,)*> SystemParam for ParamSet<'w, 's, (#(#param,)*)> - { - type State = ParamSetState<(#(#param::State,)*)>; - } - - // SAFETY: All parameters are constrained to ReadOnlyState, so World is only read - + // SAFETY: All parameters are constrained to ReadOnlySystemParam, so World is only read unsafe impl<'w, 's, #(#param,)*> ReadOnlySystemParam for ParamSet<'w, 's, (#(#param,)*)> where #(#param: ReadOnlySystemParam,)* { } // SAFETY: Relevant parameter ComponentId and ArchetypeComponentId access is applied to SystemMeta. If any ParamState conflicts // with any prior access, a panic will occur. - - unsafe impl<#(#param_state: SystemParamState,)*> SystemParamState for ParamSetState<(#(#param_state,)*)> + unsafe impl<'_w, '_s, #(#param: SystemParam,)*> SystemParam for ParamSet<'_w, '_s, (#(#param,)*)> { - type Item<'w, 's> = ParamSet<'w, 's, (#(<#param_state as SystemParamState>::Item::<'w, 's>,)*)>; + type State = (#(#param::State,)*); + type Item<'w, 's> = ParamSet<'w, 's, (#(#param,)*)>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { #( // Pretend to add each param to the system alone, see if it conflicts let mut #meta = system_meta.clone(); #meta.component_access_set.clear(); #meta.archetype_component_access.clear(); - #param_state::init(world, &mut #meta); - let #param = #param_state::init(world, &mut system_meta.clone()); + #param::init_state(world, &mut #meta); + let #param = #param::init_state(world, &mut system_meta.clone()); )* #( system_meta @@ -285,29 +279,26 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { .archetype_component_access .extend(&#meta.archetype_component_access); )* - ParamSetState((#(#param,)*)) + (#(#param,)*) } - fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { - let (#(#param,)*) = &mut self.0; - #( - #param.new_archetype(archetype, system_meta); - )* + fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { + <(#(#param,)*) as SystemParam>::new_archetype(state, archetype, system_meta); } - fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) { - self.0.apply(system_meta, world) + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + <(#(#param,)*) as SystemParam>::apply(state, system_meta, world); } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { ParamSet { - param_states: &mut state.0, + param_states: state, system_meta: system_meta.clone(), world, change_tick, @@ -317,7 +308,6 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { impl<'w, 's, #(#param: SystemParam,)*> ParamSet<'w, 's, (#(#param,)*)> { - #(#param_fn_mut)* } })); @@ -411,7 +401,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { } } - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let (_impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let lifetimeless_generics: Vec<_> = generics .params @@ -419,6 +409,12 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { .filter(|g| !matches!(g, GenericParam::Lifetime(_))) .collect(); + let mut shadowed_lifetimes: Vec<_> = generics.lifetimes().map(|x| x.lifetime.clone()).collect(); + for lifetime in &mut shadowed_lifetimes { + let shadowed_ident = format_ident!("_{}", lifetime.ident); + lifetime.ident = shadowed_ident; + } + let mut punctuated_generics = Punctuated::<_, Token![,]>::new(); punctuated_generics.extend(lifetimeless_generics.iter().map(|g| match g { GenericParam::Type(g) => GenericParam::Type(TypeParam { @@ -432,15 +428,6 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { _ => unreachable!(), })); - let mut punctuated_generics_no_bounds = punctuated_generics.clone(); - for g in &mut punctuated_generics_no_bounds { - match g { - GenericParam::Type(g) => g.bounds.clear(), - GenericParam::Lifetime(g) => g.bounds.clear(), - GenericParam::Const(_) => {} - } - } - let mut punctuated_generic_idents = Punctuated::<_, Token![,]>::new(); punctuated_generic_idents.extend(lifetimeless_generics.iter().map(|g| match g { GenericParam::Type(g) => &g.ident, @@ -479,12 +466,9 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { // The struct can still be accessed via SystemParam::State, e.g. EventReaderState can be accessed via // as SystemParam>::State const _: () = { - impl #impl_generics #path::system::SystemParam for #struct_name #ty_generics #where_clause { - type State = FetchState<'static, 'static, #punctuated_generic_idents>; - } - #[doc(hidden)] - #state_struct_visibility struct FetchState <'w, 's, #(#lifetimeless_generics,)*> { + #state_struct_visibility struct FetchState <'w, 's, #(#lifetimeless_generics,)*> + #where_clause { state: (#(<#tuple_types as #path::system::SystemParam>::State,)*), marker: std::marker::PhantomData<( <#path::prelude::Query<'w, 's, ()> as #path::system::SystemParam>::State, @@ -492,34 +476,33 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { )>, } - unsafe impl<#punctuated_generics> #path::system::SystemParamState for - FetchState<'static, 'static, #punctuated_generic_idents> - #where_clause { - type Item<'w, 's> = #struct_name #ty_generics; + unsafe impl<'w, 's, #punctuated_generics> #path::system::SystemParam for #struct_name #ty_generics #where_clause { + type State = FetchState<'static, 'static, #punctuated_generic_idents>; + type Item<'_w, '_s> = #struct_name <#(#shadowed_lifetimes,)* #punctuated_generic_idents>; - fn init(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self { - Self { - state: #path::system::SystemParamState::init(world, system_meta), + fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State { + FetchState { + state: <(#(#tuple_types,)*) as #path::system::SystemParam>::init_state(world, system_meta), marker: std::marker::PhantomData, } } - fn new_archetype(&mut self, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) { - self.state.new_archetype(archetype, system_meta) + fn new_archetype(state: &mut Self::State, archetype: &#path::archetype::Archetype, system_meta: &mut #path::system::SystemMeta) { + <(#(#tuple_types,)*) as #path::system::SystemParam>::new_archetype(&mut state.state, archetype, system_meta) } - fn apply(&mut self, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { - self.state.apply(system_meta, world) + fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { + <(#(#tuple_types,)*) as #path::system::SystemParam>::apply(&mut state.state, system_meta, world); } - unsafe fn get_param<'w, 's>( - state: &'s mut Self, + unsafe fn get_param<'w2, 's2>( + state: &'s2 mut Self::State, system_meta: &#path::system::SystemMeta, - world: &'w #path::world::World, + world: &'w2 #path::world::World, change_tick: u32, - ) -> Self::Item<'w, 's> { + ) -> Self::Item<'w2, 's2> { let (#(#tuple_patterns,)*) = < - <(#(#tuple_types,)*) as #path::system::SystemParam>::State as #path::system::SystemParamState + (#(#tuple_types,)*) as #path::system::SystemParam >::get_param(&mut state.state, system_meta, world, change_tick); #struct_name { #(#fields: #field_locals,)* @@ -584,6 +567,32 @@ pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream { derive_label(input, &trait_path, "run_criteria_label") } +/// Derive macro generating an impl of the trait `ScheduleLabel`. +#[proc_macro_derive(ScheduleLabel)] +pub fn derive_schedule_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_ecs_path(); + trait_path + .segments + .push(format_ident!("schedule_v3").into()); + trait_path + .segments + .push(format_ident!("ScheduleLabel").into()); + derive_boxed_label(input, &trait_path) +} + +/// Derive macro generating an impl of the trait `SystemSet`. +#[proc_macro_derive(SystemSet)] +pub fn derive_system_set(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_ecs_path(); + trait_path + .segments + .push(format_ident!("schedule_v3").into()); + trait_path.segments.push(format_ident!("SystemSet").into()); + derive_set(input, &trait_path) +} + pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::default().get_path("bevy_ecs") } diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index ffd9fdc518783..a25551f013da3 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -40,6 +40,7 @@ use std::{ /// [`World`]: crate::world::World /// [`Entities::get`]: crate::entity::Entities #[derive(Debug, Copy, Clone, Eq, PartialEq)] +// SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation #[repr(transparent)] pub struct ArchetypeRow(u32); @@ -68,6 +69,7 @@ impl ArchetypeRow { /// [`World`]: crate::world::World /// [`EMPTY`]: crate::archetype::ArchetypeId::EMPTY #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +// SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation #[repr(transparent)] pub struct ArchetypeId(u32); @@ -625,7 +627,7 @@ impl Archetypes { self.archetypes.len() } - /// Fetches an immutable reference to the archetype without any compoennts. + /// Fetches an immutable reference to the archetype without any components. /// /// Shorthand for `archetypes.get(ArchetypeId::EMPTY).unwrap()` #[inline] @@ -634,7 +636,7 @@ impl Archetypes { unsafe { self.archetypes.get_unchecked(ArchetypeId::EMPTY.index()) } } - /// Fetches an mutable reference to the archetype without any compoennts. + /// Fetches an mutable reference to the archetype without any components. #[inline] pub(crate) fn empty_mut(&mut self) -> &mut Archetype { // SAFETY: empty archetype always exists diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 259de1abed446..042138a3c0db8 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -731,8 +731,7 @@ unsafe fn initialize_bundle( deduped.dedup(); assert!( deduped.len() == component_ids.len(), - "Bundle {} has duplicate components", - bundle_type_name + "Bundle {bundle_type_name} has duplicate components", ); BundleInfo { id, component_ids } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index d029d9dad9bf9..3802af55dd349 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -23,19 +23,55 @@ pub const CHECK_TICK_THRESHOLD: u32 = 518_400_000; /// Changes stop being detected once they become this old. pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1); +/// Types that can read change detection information. +/// This change detection is controlled by [`DetectChangesMut`] types such as [`ResMut`]. +/// +/// ## Example +/// Using types that implement [`DetectChanges`], such as [`Res`], provide +/// a way to query if a value has been mutated in another system. +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Resource)] +/// struct MyResource(u32); +/// +/// fn my_system(mut resource: Res) { +/// if resource.is_changed() { +/// println!("My component was mutated!"); +/// } +/// } +/// ``` +pub trait DetectChanges { + /// Returns `true` if this value was added after the system last ran. + fn is_added(&self) -> bool; + + /// Returns `true` if this value was added or mutably dereferenced after the system last ran. + fn is_changed(&self) -> bool; + + /// Returns the change tick recording the previous time this data was changed. + /// + /// Note that components and resources are also marked as changed upon insertion. + /// + /// For comparison, the previous change tick of a system can be read using the + /// [`SystemChangeTick`](crate::system::SystemChangeTick) + /// [`SystemParam`](crate::system::SystemParam). + fn last_changed(&self) -> u32; +} + /// Types that implement reliable change detection. /// /// ## Example -/// Using types that implement [`DetectChanges`], such as [`ResMut`], provide +/// Using types that implement [`DetectChangesMut`], such as [`ResMut`], provide /// a way to query if a value has been mutated in another system. -/// Normally change detecting is triggered by either [`DerefMut`] or [`AsMut`], however -/// it can be manually triggered via [`DetectChanges::set_changed`]. +/// Normally change detection is triggered by either [`DerefMut`] or [`AsMut`], however +/// it can be manually triggered via [`set_if_neq`](`DetectChangesMut::set_changed`). /// /// To ensure that changes are only triggered when the value actually differs, /// check if the value would change before assignment, such as by checking that `new != old`. -/// You must be *sure* that you are not mutably derefencing in this process. +/// You must be *sure* that you are not mutably dereferencing in this process. /// -/// [`set_if_neq`](DetectChanges::set_if_neq) is a helper +/// [`set_if_neq`](DetectChangesMut::set_if_neq) is a helper /// method for this common functionality. /// /// ``` @@ -53,18 +89,12 @@ pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1); /// } /// ``` /// -pub trait DetectChanges { +pub trait DetectChangesMut: DetectChanges { /// The type contained within this smart pointer /// - /// For example, for `Res` this would be `T`. + /// For example, for `ResMut` this would be `T`. type Inner: ?Sized; - /// Returns `true` if this value was added after the system last ran. - fn is_added(&self) -> bool; - - /// Returns `true` if this value was added or mutably dereferenced after the system last ran. - fn is_changed(&self) -> bool; - /// Flags this value as having been changed. /// /// Mutably accessing this smart pointer will automatically flag this value as having been changed. @@ -73,21 +103,12 @@ pub trait DetectChanges { /// **Note**: This operation cannot be undone. fn set_changed(&mut self); - /// Returns the change tick recording the previous time this data was changed. - /// - /// Note that components and resources are also marked as changed upon insertion. - /// - /// For comparison, the previous change tick of a system can be read using the - /// [`SystemChangeTick`](crate::system::SystemChangeTick) - /// [`SystemParam`](crate::system::SystemParam). - fn last_changed(&self) -> u32; - /// Manually sets the change tick recording the previous time this data was mutated. /// /// # Warning /// This is a complex and error-prone operation, primarily intended for use with rollback networking strategies. - /// If you merely want to flag this data as changed, use [`set_changed`](DetectChanges::set_changed) instead. - /// If you want to avoid triggering change detection, use [`bypass_change_detection`](DetectChanges::bypass_change_detection) instead. + /// If you merely want to flag this data as changed, use [`set_changed`](DetectChangesMut::set_changed) instead. + /// If you want to avoid triggering change detection, use [`bypass_change_detection`](DetectChangesMut::bypass_change_detection) instead. fn set_last_changed(&mut self, last_change_tick: u32); /// Manually bypasses change detection, allowing you to mutate the underlying value without updating the change tick. @@ -113,8 +134,6 @@ pub trait DetectChanges { macro_rules! change_detection_impl { ($name:ident < $( $generics:tt ),+ >, $target:ty, $($traits:ident)?) => { impl<$($generics),* : ?Sized $(+ $traits)?> DetectChanges for $name<$($generics),*> { - type Inner = $target; - #[inline] fn is_added(&self) -> bool { self.ticks @@ -129,6 +148,35 @@ macro_rules! change_detection_impl { .is_older_than(self.ticks.last_change_tick, self.ticks.change_tick) } + #[inline] + fn last_changed(&self) -> u32 { + self.ticks.last_change_tick + } + } + + impl<$($generics),*: ?Sized $(+ $traits)?> Deref for $name<$($generics),*> { + type Target = $target; + + #[inline] + fn deref(&self) -> &Self::Target { + self.value + } + } + + impl<$($generics),* $(: $traits)?> AsRef<$target> for $name<$($generics),*> { + #[inline] + fn as_ref(&self) -> &$target { + self.deref() + } + } + } +} + +macro_rules! change_detection_mut_impl { + ($name:ident < $( $generics:tt ),+ >, $target:ty, $($traits:ident)?) => { + impl<$($generics),* : ?Sized $(+ $traits)?> DetectChangesMut for $name<$($generics),*> { + type Inner = $target; + #[inline] fn set_changed(&mut self) { self.ticks @@ -136,11 +184,6 @@ macro_rules! change_detection_impl { .set_changed(self.ticks.change_tick); } - #[inline] - fn last_changed(&self) -> u32 { - self.ticks.last_change_tick - } - #[inline] fn set_last_changed(&mut self, last_change_tick: u32) { self.ticks.last_change_tick = last_change_tick @@ -165,15 +208,6 @@ macro_rules! change_detection_impl { } } - impl<$($generics),*: ?Sized $(+ $traits)?> Deref for $name<$($generics),*> { - type Target = $target; - - #[inline] - fn deref(&self) -> &Self::Target { - self.value - } - } - impl<$($generics),* : ?Sized $(+ $traits)?> DerefMut for $name<$($generics),*> { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { @@ -182,13 +216,6 @@ macro_rules! change_detection_impl { } } - impl<$($generics),* $(: $traits)?> AsRef<$target> for $name<$($generics),*> { - #[inline] - fn as_ref(&self) -> &$target { - self.deref() - } - } - impl<$($generics),* $(: $traits)?> AsMut<$target> for $name<$($generics),*> { #[inline] fn as_mut(&mut self) -> &mut $target { @@ -209,27 +236,43 @@ macro_rules! impl_methods { self.value } + /// Returns a `Mut<>` with a smaller lifetime. + /// This is useful if you have `&mut + #[doc = stringify!($name)] + /// `, but you need a `Mut`. + /// + /// Note that calling [`DetectChangesMut::set_last_changed`] on the returned value + /// will not affect the original. + pub fn reborrow(&mut self) -> Mut<'_, $target> { + Mut { + value: self.value, + ticks: TicksMut { + added: self.ticks.added, + changed: self.ticks.changed, + last_change_tick: self.ticks.last_change_tick, + change_tick: self.ticks.change_tick, + } + } + } + /// Maps to an inner value by applying a function to the contained reference, without flagging a change. /// /// You should never modify the argument passed to the closure -- if you want to modify the data - /// without flagging a change, consider using [`DetectChanges::bypass_change_detection`] to make your intent explicit. + /// without flagging a change, consider using [`DetectChangesMut::bypass_change_detection`] to make your intent explicit. /// /// ```rust /// # use bevy_ecs::prelude::*; - /// # pub struct Vec2; + /// # #[derive(PartialEq)] pub struct Vec2; /// # impl Vec2 { pub const ZERO: Self = Self; } /// # #[derive(Component)] pub struct Transform { translation: Vec2 } - /// # mod my_utils { - /// # pub fn set_if_not_equal(x: bevy_ecs::prelude::Mut, val: T) { unimplemented!() } - /// # } /// // When run, zeroes the translation of every entity. /// fn reset_positions(mut transforms: Query<&mut Transform>) { /// for transform in &mut transforms { /// // We pinky promise not to modify `t` within the closure. /// // Breaking this promise will result in logic errors, but will never cause undefined behavior. - /// let translation = transform.map_unchanged(|t| &mut t.translation); + /// let mut translation = transform.map_unchanged(|t| &mut t.translation); /// // Only reset the translation if it isn't already zero; - /// my_utils::set_if_not_equal(translation, Vec2::ZERO); + /// translation.set_if_neq(Vec2::ZERO); /// } /// } /// # bevy_ecs::system::assert_is_system(reset_positions); @@ -259,14 +302,40 @@ macro_rules! impl_debug { }; } +#[derive(Clone)] pub(crate) struct Ticks<'a> { + pub(crate) added: &'a Tick, + pub(crate) changed: &'a Tick, + pub(crate) last_change_tick: u32, + pub(crate) change_tick: u32, +} + +impl<'a> Ticks<'a> { + /// # Safety + /// This should never alias the underlying ticks with a mutable one such as `TicksMut`. + #[inline] + pub(crate) unsafe fn from_tick_cells( + cells: TickCells<'a>, + last_change_tick: u32, + change_tick: u32, + ) -> Self { + Self { + added: cells.added.deref(), + changed: cells.changed.deref(), + last_change_tick, + change_tick, + } + } +} + +pub(crate) struct TicksMut<'a> { pub(crate) added: &'a mut Tick, pub(crate) changed: &'a mut Tick, pub(crate) last_change_tick: u32, pub(crate) change_tick: u32, } -impl<'a> Ticks<'a> { +impl<'a> TicksMut<'a> { /// # Safety /// This should never alias the underlying ticks. All access must be unique. #[inline] @@ -284,6 +353,71 @@ impl<'a> Ticks<'a> { } } +impl<'a> From> for Ticks<'a> { + fn from(ticks: TicksMut<'a>) -> Self { + Ticks { + added: ticks.added, + changed: ticks.changed, + last_change_tick: ticks.last_change_tick, + change_tick: ticks.change_tick, + } + } +} + +/// Shared borrow of a [`Resource`]. +/// +/// See the [`Resource`] documentation for usage. +/// +/// If you need a unique mutable borrow, use [`ResMut`] instead. +/// +/// # Panics +/// +/// Panics when used as a [`SystemParameter`](crate::system::SystemParam) if the resource does not exist. +/// +/// Use `Option>` instead if the resource might not always exist. +pub struct Res<'w, T: ?Sized + Resource> { + pub(crate) value: &'w T, + pub(crate) ticks: Ticks<'w>, +} + +impl<'w, T: Resource> Res<'w, T> { + // no it shouldn't clippy + #[allow(clippy::should_implement_trait)] + pub fn clone(this: &Self) -> Self { + Self { + value: this.value, + ticks: this.ticks.clone(), + } + } + + pub fn into_inner(self) -> &'w T { + self.value + } +} + +impl<'w, T: Resource> From> for Res<'w, T> { + fn from(res: ResMut<'w, T>) -> Self { + Self { + value: res.value, + ticks: res.ticks.into(), + } + } +} + +impl<'w, 'a, T: Resource> IntoIterator for &'a Res<'w, T> +where + &'a T: IntoIterator, +{ + type Item = <&'a T as IntoIterator>::Item; + type IntoIter = <&'a T as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.value.into_iter() + } +} +change_detection_impl!(Res<'w, T>, T, Resource); +impl_debug!(Res<'w, T>, Resource); + /// Unique mutable borrow of a [`Resource`]. /// /// See the [`Resource`] documentation for usage. @@ -297,7 +431,7 @@ impl<'a> Ticks<'a> { /// Use `Option>` instead if the resource might not always exist. pub struct ResMut<'a, T: ?Sized + Resource> { pub(crate) value: &'a mut T, - pub(crate) ticks: Ticks<'a>, + pub(crate) ticks: TicksMut<'a>, } impl<'w, 'a, T: Resource> IntoIterator for &'a ResMut<'w, T> @@ -326,6 +460,7 @@ where } change_detection_impl!(ResMut<'a, T>, T, Resource); +change_detection_mut_impl!(ResMut<'a, T>, T, Resource); impl_methods!(ResMut<'a, T>, T, Resource); impl_debug!(ResMut<'a, T>, Resource); @@ -354,10 +489,11 @@ impl<'a, T: Resource> From> for Mut<'a, T> { /// Use `Option>` instead if the resource might not always exist. pub struct NonSendMut<'a, T: ?Sized + 'static> { pub(crate) value: &'a mut T, - pub(crate) ticks: Ticks<'a>, + pub(crate) ticks: TicksMut<'a>, } change_detection_impl!(NonSendMut<'a, T>, T,); +change_detection_mut_impl!(NonSendMut<'a, T>, T,); impl_methods!(NonSendMut<'a, T>, T,); impl_debug!(NonSendMut<'a, T>,); @@ -372,10 +508,46 @@ impl<'a, T: 'static> From> for Mut<'a, T> { } } +/// Shared borrow of an entity's component with access to change detection. +/// Similar to [`Mut`] but is immutable and so doesn't require unique access. +pub struct Ref<'a, T: ?Sized> { + pub(crate) value: &'a T, + pub(crate) ticks: Ticks<'a>, +} + +impl<'a, T: ?Sized> Ref<'a, T> { + pub fn into_inner(self) -> &'a T { + self.value + } +} + +impl<'w, 'a, T> IntoIterator for &'a Ref<'w, T> +where + &'a T: IntoIterator, +{ + type Item = <&'a T as IntoIterator>::Item; + type IntoIter = <&'a T as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.value.into_iter() + } +} +change_detection_impl!(Ref<'a, T>, T,); +impl_debug!(Ref<'a, T>,); + /// Unique mutable borrow of an entity's component pub struct Mut<'a, T: ?Sized> { pub(crate) value: &'a mut T, - pub(crate) ticks: Ticks<'a>, + pub(crate) ticks: TicksMut<'a>, +} + +impl<'a, T: ?Sized> From> for Ref<'a, T> { + fn from(mut_ref: Mut<'a, T>) -> Self { + Self { + value: mut_ref.value, + ticks: mut_ref.ticks.into(), + } + } } impl<'w, 'a, T> IntoIterator for &'a Mut<'w, T> @@ -404,6 +576,7 @@ where } change_detection_impl!(Mut<'a, T>, T,); +change_detection_mut_impl!(Mut<'a, T>, T,); impl_methods!(Mut<'a, T>, T,); impl_debug!(Mut<'a, T>,); @@ -417,22 +590,40 @@ impl_debug!(Mut<'a, T>,); /// or are defined outside of rust this can be used. pub struct MutUntyped<'a> { pub(crate) value: PtrMut<'a>, - pub(crate) ticks: Ticks<'a>, + pub(crate) ticks: TicksMut<'a>, } impl<'a> MutUntyped<'a> { /// Returns the pointer to the value, marking it as changed. /// - /// In order to avoid marking the value as changed, you need to call [`bypass_change_detection`](DetectChanges::bypass_change_detection). + /// In order to avoid marking the value as changed, you need to call [`bypass_change_detection`](DetectChangesMut::bypass_change_detection). #[inline] pub fn into_inner(mut self) -> PtrMut<'a> { self.set_changed(); self.value } + /// Returns a [`MutUntyped`] with a smaller lifetime. + /// This is useful if you have `&mut MutUntyped`, but you need a `MutUntyped`. + /// + /// Note that calling [`DetectChangesMut::set_last_changed`] on the returned value + /// will not affect the original. + #[inline] + pub fn reborrow(&mut self) -> MutUntyped { + MutUntyped { + value: self.value.reborrow(), + ticks: TicksMut { + added: self.ticks.added, + changed: self.ticks.changed, + last_change_tick: self.ticks.last_change_tick, + change_tick: self.ticks.change_tick, + }, + } + } + /// Returns a pointer to the value without taking ownership of this smart pointer, marking it as changed. /// - /// In order to avoid marking the value as changed, you need to call [`bypass_change_detection`](DetectChanges::bypass_change_detection). + /// In order to avoid marking the value as changed, you need to call [`bypass_change_detection`](DetectChangesMut::bypass_change_detection). #[inline] pub fn as_mut(&mut self) -> PtrMut<'_> { self.set_changed(); @@ -444,11 +635,20 @@ impl<'a> MutUntyped<'a> { pub fn as_ref(&self) -> Ptr<'_> { self.value.as_ref() } + + /// Transforms this [`MutUntyped`] into a [`Mut`] with the same lifetime. + /// + /// # Safety + /// - `T` must be the erased pointee type for this [`MutUntyped`]. + pub unsafe fn with_type(self) -> Mut<'a, T> { + Mut { + value: self.value.deref_mut(), + ticks: self.ticks, + } + } } impl<'a> DetectChanges for MutUntyped<'a> { - type Inner = PtrMut<'a>; - #[inline] fn is_added(&self) -> bool { self.ticks @@ -464,13 +664,17 @@ impl<'a> DetectChanges for MutUntyped<'a> { } #[inline] - fn set_changed(&mut self) { - self.ticks.changed.set_changed(self.ticks.change_tick); + fn last_changed(&self) -> u32 { + self.ticks.last_change_tick } +} + +impl<'a> DetectChangesMut for MutUntyped<'a> { + type Inner = PtrMut<'a>; #[inline] - fn last_changed(&self) -> u32 { - self.ticks.last_change_tick + fn set_changed(&mut self) { + self.ticks.changed.set_changed(self.ticks.change_tick); } #[inline] @@ -511,7 +715,9 @@ mod tests { use crate::{ self as bevy_ecs, - change_detection::{Mut, NonSendMut, ResMut, Ticks, CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE}, + change_detection::{ + Mut, NonSendMut, ResMut, TicksMut, CHECK_TICK_THRESHOLD, MAX_CHANGE_AGE, + }, component::{Component, ComponentTicks, Tick}, query::ChangeTrackers, system::{IntoSystem, Query, System}, @@ -519,6 +725,7 @@ mod tests { }; use super::DetectChanges; + use super::DetectChangesMut; #[derive(Component, PartialEq)] struct C; @@ -622,7 +829,7 @@ mod tests { added: Tick::new(1), changed: Tick::new(2), }; - let ticks = Ticks { + let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, last_change_tick: 3, @@ -647,7 +854,7 @@ mod tests { added: Tick::new(1), changed: Tick::new(2), }; - let ticks = Ticks { + let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, last_change_tick: 3, @@ -676,7 +883,7 @@ mod tests { added: Tick::new(1), changed: Tick::new(2), }; - let ticks = Ticks { + let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, last_change_tick, diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 39f89b95ffaaf..6e8097584317a 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -498,6 +498,7 @@ impl Components { self.get_id(TypeId::of::()) } + /// Type-erased equivalent of [`Components::resource_id`]. #[inline] pub fn get_resource_id(&self, type_id: TypeId) -> Option { self.resource_indices @@ -505,6 +506,32 @@ impl Components { .map(|index| ComponentId(*index)) } + /// Returns the [`ComponentId`] of the given [`Resource`] type `T`. + /// + /// The returned `ComponentId` is specific to the `Components` instance + /// it was retrieved from and should not be used with another `Components` + /// instance. + /// + /// Returns [`None`] if the `Resource` type has not + /// yet been initialized using [`Components::init_resource`]. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// let mut world = World::new(); + /// + /// #[derive(Resource, Default)] + /// struct ResourceA; + /// + /// let resource_a_id = world.init_resource::(); + /// + /// assert_eq!(resource_a_id, world.components().resource_id::().unwrap()) + /// ``` + #[inline] + pub fn resource_id(&self) -> Option { + self.get_resource_id(TypeId::of::()) + } + #[inline] pub fn init_resource(&mut self) -> ComponentId { // SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`] diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index c7f74c5f8d052..afcba27bb68c1 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -586,7 +586,7 @@ impl Entities { /// - `location` must be valid for the entity at `index` or immediately made valid afterwards /// before handing control to unknown code. pub(crate) unsafe fn set(&mut self, index: u32, location: EntityLocation) { - // SAFETY: Caller guarentees that `index` a valid entity index + // SAFETY: Caller guarantees that `index` a valid entity index self.meta.get_unchecked_mut(index as usize).location = location; } @@ -747,7 +747,7 @@ impl EntityMeta { // SAFETY: // This type must not contain any pointers at any level, and be safe to fully fill with u8::MAX. /// A location of an entity in an archetype. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] #[repr(C)] pub struct EntityLocation { /// The ID of the [`Archetype`] the [`Entity`] belongs to. diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 46f1a8dc4281c..103fd5106f5b8 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -14,6 +14,7 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod schedule; +pub mod schedule_v3; pub mod storage; pub mod system; pub mod world; @@ -28,7 +29,7 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ bundle::Bundle, - change_detection::DetectChanges, + change_detection::{DetectChanges, DetectChangesMut}, component::Component, entity::Entity, event::{EventReader, EventWriter, Events}, @@ -399,7 +400,8 @@ mod tests { let results = Arc::new(Mutex::new(Vec::new())); world .query::<(Entity, &A)>() - .par_for_each(&world, 2, |(e, &A(i))| { + .par_iter(&world) + .for_each(|(e, &A(i))| { results.lock().unwrap().push((e, i)); }); results.lock().unwrap().sort(); @@ -419,11 +421,10 @@ mod tests { let e4 = world.spawn((SparseStored(4), A(1))).id(); let e5 = world.spawn((SparseStored(5), A(1))).id(); let results = Arc::new(Mutex::new(Vec::new())); - world.query::<(Entity, &SparseStored)>().par_for_each( - &world, - 2, - |(e, &SparseStored(i))| results.lock().unwrap().push((e, i)), - ); + world + .query::<(Entity, &SparseStored)>() + .par_iter(&world) + .for_each(|(e, &SparseStored(i))| results.lock().unwrap().push((e, i))); results.lock().unwrap().sort(); assert_eq!( &*results.lock().unwrap(), @@ -1267,6 +1268,15 @@ mod tests { assert_eq!(*world.non_send_resource_mut::(), 456); } + #[test] + fn non_send_resource_points_to_distinct_data() { + let mut world = World::default(); + world.insert_resource(A(123)); + world.insert_non_send_resource(A(456)); + assert_eq!(*world.resource::(), A(123)); + assert_eq!(*world.non_send_resource::(), A(456)); + } + #[test] #[should_panic] fn non_send_resource_panic() { @@ -1406,31 +1416,18 @@ mod tests { assert_eq!(world.resource::().0, 1); } - #[test] - fn non_send_resource_scope() { - let mut world = World::default(); - world.insert_non_send_resource(NonSendA::default()); - world.resource_scope(|world: &mut World, mut value: Mut| { - value.0 += 1; - assert!(!world.contains_resource::()); - }); - assert_eq!(world.non_send_resource::().0, 1); - } - #[test] #[should_panic( - expected = "attempted to access NonSend resource bevy_ecs::tests::NonSendA off of the main thread" + expected = "Attempted to access or drop non-send resource bevy_ecs::tests::NonSendA from thread" )] - fn non_send_resource_scope_from_different_thread() { + fn non_send_resource_drop_from_different_thread() { let mut world = World::default(); world.insert_non_send_resource(NonSendA::default()); let thread = std::thread::spawn(move || { - // Accessing the non-send resource on a different thread + // Dropping the non-send resource on a different thread // Should result in a panic - world.resource_scope(|_: &mut World, mut value: Mut| { - value.0 += 1; - }); + drop(world); }); if let Err(err) = thread.join() { @@ -1438,6 +1435,13 @@ mod tests { } } + #[test] + fn non_send_resource_drop_from_same_thread() { + let mut world = World::default(); + world.insert_non_send_resource(NonSendA::default()); + drop(world); + } + #[test] fn insert_overwrite_drop() { let (dropck1, dropped1) = DropCk::new_pair(); diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 4a3f49f302d95..5733ffd262829 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,11 +1,11 @@ use crate::{ archetype::{Archetype, ArchetypeComponentId}, - change_detection::Ticks, + change_detection::{Ticks, TicksMut}, component::{Component, ComponentId, ComponentStorage, ComponentTicks, StorageType, Tick}, entity::Entity, query::{Access, DebugCheckedUnwrap, FilteredAccess}, storage::{ComponentSparseSet, Table, TableRow}, - world::{Mut, World}, + world::{Mut, Ref, World}, }; use bevy_ecs_macros::all_tuples; pub use bevy_ecs_macros::WorldQuery; @@ -415,7 +415,7 @@ pub unsafe trait WorldQuery { // This does not have a default body of `{}` because 99% of cases need to add accesses // and forgetting to do so would be unsound. fn update_component_access(state: &Self::State, access: &mut FilteredAccess); - // This does not have a default body of `{}` becaues 99% of cases need to add accesses + // This does not have a default body of `{}` because 99% of cases need to add accesses // and forgetting to do so would be unsound. fn update_archetype_component_access( state: &Self::State, @@ -653,6 +653,167 @@ unsafe impl WorldQuery for &T { /// SAFETY: access is read only unsafe impl ReadOnlyWorldQuery for &T {} +#[doc(hidden)] +pub struct RefFetch<'w, T> { + // T::Storage = TableStorage + table_data: Option<( + ThinSlicePtr<'w, UnsafeCell>, + ThinSlicePtr<'w, UnsafeCell>, + ThinSlicePtr<'w, UnsafeCell>, + )>, + // T::Storage = SparseStorage + sparse_set: Option<&'w ComponentSparseSet>, + + last_change_tick: u32, + change_tick: u32, +} + +/// SAFETY: `Self` is the same as `Self::ReadOnly` +unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { + type Fetch<'w> = RefFetch<'w, T>; + type Item<'w> = Ref<'w, T>; + type ReadOnly = Self; + type State = ComponentId; + + fn shrink<'wlong: 'wshort, 'wshort>(item: Ref<'wlong, T>) -> Ref<'wshort, T> { + item + } + + const IS_DENSE: bool = { + match T::Storage::STORAGE_TYPE { + StorageType::Table => true, + StorageType::SparseSet => false, + } + }; + + const IS_ARCHETYPAL: bool = true; + + unsafe fn init_fetch<'w>( + world: &'w World, + &component_id: &ComponentId, + last_change_tick: u32, + change_tick: u32, + ) -> RefFetch<'w, T> { + RefFetch { + table_data: None, + sparse_set: (T::Storage::STORAGE_TYPE == StorageType::SparseSet).then(|| { + world + .storages() + .sparse_sets + .get(component_id) + .debug_checked_unwrap() + }), + last_change_tick, + change_tick, + } + } + + unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> { + RefFetch { + table_data: fetch.table_data, + sparse_set: fetch.sparse_set, + last_change_tick: fetch.last_change_tick, + change_tick: fetch.change_tick, + } + } + + #[inline] + unsafe fn set_archetype<'w>( + fetch: &mut RefFetch<'w, T>, + component_id: &ComponentId, + _archetype: &'w Archetype, + table: &'w Table, + ) { + if Self::IS_DENSE { + Self::set_table(fetch, component_id, table); + } + } + + #[inline] + unsafe fn set_table<'w>( + fetch: &mut RefFetch<'w, T>, + &component_id: &ComponentId, + table: &'w Table, + ) { + let column = table.get_column(component_id).debug_checked_unwrap(); + fetch.table_data = Some(( + column.get_data_slice().into(), + column.get_added_ticks_slice().into(), + column.get_changed_ticks_slice().into(), + )); + } + + #[inline(always)] + unsafe fn fetch<'w>( + fetch: &mut Self::Fetch<'w>, + entity: Entity, + table_row: TableRow, + ) -> Self::Item<'w> { + match T::Storage::STORAGE_TYPE { + StorageType::Table => { + let (table_components, added_ticks, changed_ticks) = + fetch.table_data.debug_checked_unwrap(); + Ref { + value: table_components.get(table_row.index()).deref(), + ticks: Ticks { + added: added_ticks.get(table_row.index()).deref(), + changed: changed_ticks.get(table_row.index()).deref(), + change_tick: fetch.change_tick, + last_change_tick: fetch.last_change_tick, + }, + } + } + StorageType::SparseSet => { + let (component, ticks) = fetch + .sparse_set + .debug_checked_unwrap() + .get_with_ticks(entity) + .debug_checked_unwrap(); + Ref { + value: component.deref(), + ticks: Ticks::from_tick_cells(ticks, fetch.last_change_tick, fetch.change_tick), + } + } + } + } + + fn update_component_access( + &component_id: &ComponentId, + access: &mut FilteredAccess, + ) { + assert!( + !access.access().has_write(component_id), + "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", + std::any::type_name::(), + ); + access.add_read(component_id); + } + + fn update_archetype_component_access( + &component_id: &ComponentId, + archetype: &Archetype, + access: &mut Access, + ) { + if let Some(archetype_component_id) = archetype.get_archetype_component_id(component_id) { + access.add_read(archetype_component_id); + } + } + + fn init_state(world: &mut World) -> ComponentId { + world.init_component::() + } + + fn matches_component_set( + &state: &ComponentId, + set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + set_contains_id(state) + } +} + +/// SAFETY: access is read only +unsafe impl<'__w, T: Component> ReadOnlyWorldQuery for Ref<'__w, T> {} + #[doc(hidden)] pub struct WriteFetch<'w, T> { // T::Storage = TableStorage @@ -755,7 +916,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { fetch.table_data.debug_checked_unwrap(); Mut { value: table_components.get(table_row.index()).deref_mut(), - ticks: Ticks { + ticks: TicksMut { added: added_ticks.get(table_row.index()).deref_mut(), changed: changed_ticks.get(table_row.index()).deref_mut(), change_tick: fetch.change_tick, @@ -771,7 +932,11 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { .debug_checked_unwrap(); Mut { value: component.assert_unique().deref_mut(), - ticks: Ticks::from_tick_cells(ticks, fetch.last_change_tick, fetch.change_tick), + ticks: TicksMut::from_tick_cells( + ticks, + fetch.last_change_tick, + fetch.change_tick, + ), } } } @@ -886,7 +1051,7 @@ unsafe impl WorldQuery for Option { fn update_component_access(state: &T::State, access: &mut FilteredAccess) { // We don't want to add the `with`/`without` of `T` as `Option` will match things regardless of // `T`'s filters. for example `Query<(Option<&U>, &mut V)>` will match every entity with a `V` component - // regardless of whether it has a `U` component. If we dont do this the query will not conflict with + // regardless of whether it has a `U` component. If we don't do this the query will not conflict with // `Query<&mut V, Without>` which would be unsound. let mut intermediate = access.clone(); T::update_component_access(state, &mut intermediate); diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 220754b5e0ff0..1b3e1f4d08bca 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -2,12 +2,14 @@ mod access; mod fetch; mod filter; mod iter; +mod par_iter; mod state; pub use access::*; pub use fetch::*; pub use filter::*; pub use iter::*; +pub use par_iter::*; pub use state::*; /// A debug checked version of [`Option::unwrap_unchecked`]. Will panic in @@ -25,7 +27,7 @@ pub(crate) trait DebugCheckedUnwrap { unsafe fn debug_checked_unwrap(self) -> Self::Item; } -// Thes two impls are explicitly split to ensure that the unreachable! macro +// These two impls are explicitly split to ensure that the unreachable! macro // does not cause inlining to fail when compiling in release mode. #[cfg(debug_assertions)] impl DebugCheckedUnwrap for Option { diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs new file mode 100644 index 0000000000000..fd53fb71a2b9d --- /dev/null +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -0,0 +1,202 @@ +use crate::world::World; +use bevy_tasks::ComputeTaskPool; +use std::ops::Range; + +use super::{QueryItem, QueryState, ROQueryItem, ReadOnlyWorldQuery, WorldQuery}; + +/// Dictates how a parallel query chunks up large tables/archetypes +/// during iteration. +/// +/// A parallel query will chunk up large tables and archetypes into +/// chunks of at most a certain batch size. +/// +/// By default, this batch size is automatically determined by dividing +/// the size of the largest matched archetype by the number +/// of threads. This attempts to minimize the overhead of scheduling +/// tasks onto multiple threads, but assumes each entity has roughly the +/// same amount of work to be done, which may not hold true in every +/// workload. +/// +/// See [`Query::par_iter`] for more information. +/// +/// [`Query::par_iter`]: crate::system::Query::par_iter +#[derive(Clone)] +pub struct BatchingStrategy { + /// The upper and lower limits for how large a batch of entities. + /// + /// Setting the bounds to the same value will result in a fixed + /// batch size. + /// + /// Defaults to `[1, usize::MAX]`. + pub batch_size_limits: Range, + /// The number of batches per thread in the [`ComputeTaskPool`]. + /// Increasing this value will decrease the batch size, which may + /// increase the scheduling overhead for the iteration. + /// + /// Defaults to 1. + pub batches_per_thread: usize, +} + +impl BatchingStrategy { + /// Creates a new unconstrained default batching strategy. + pub const fn new() -> Self { + Self { + batch_size_limits: 1..usize::MAX, + batches_per_thread: 1, + } + } + + /// Declares a batching strategy with a fixed batch size. + pub const fn fixed(batch_size: usize) -> Self { + Self { + batch_size_limits: batch_size..batch_size, + batches_per_thread: 1, + } + } + + pub const fn min_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size_limits.start = batch_size; + self + } + + pub const fn max_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size_limits.end = batch_size; + self + } + + pub fn batches_per_thread(mut self, batches_per_thread: usize) -> Self { + assert!( + batches_per_thread > 0, + "The number of batches per thread must be non-zero." + ); + self.batches_per_thread = batches_per_thread; + self + } +} + +/// A parallel iterator over query results of a [`Query`](crate::system::Query). +/// +/// This struct is created by the [`Query::par_iter`](crate::system::Query::iter) and +/// [`Query::par_iter_mut`](crate::system::Query::iter_mut) methods. +pub struct QueryParIter<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> { + pub(crate) world: &'w World, + pub(crate) state: &'s QueryState, + pub(crate) batching_strategy: BatchingStrategy, +} + +impl<'w, 's, Q: ReadOnlyWorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> { + /// Runs `func` on each query result in parallel. + /// + /// This can only be called for read-only queries, see [`Self::for_each_mut`] for + /// write-queries. + /// + /// # Panics + /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being + /// initialized and run from the ECS scheduler, this should never panic. + /// + /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool + #[inline] + pub fn for_each) + Send + Sync + Clone>(&self, func: FN) { + // SAFETY: query is read only + unsafe { + self.for_each_unchecked(func); + } + } +} + +impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> { + /// Changes the batching strategy used when iterating. + /// + /// For more information on how this affects the resultant iteration, see + /// [`BatchingStrategy`]. + pub fn batching_strategy(mut self, strategy: BatchingStrategy) -> Self { + self.batching_strategy = strategy; + self + } + + /// Runs `func` on each query result in parallel. + /// + /// # Panics + /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being + /// initialized and run from the ECS scheduler, this should never panic. + /// + /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool + #[inline] + pub fn for_each_mut) + Send + Sync + Clone>(&mut self, func: FN) { + // SAFETY: query has unique world access + unsafe { + self.for_each_unchecked(func); + } + } + + /// Runs `func` on each query result in parallel. + /// + /// # Panics + /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being + /// initialized and run from the ECS scheduler, this should never panic. + /// + /// # Safety + /// + /// This does not check for mutable query correctness. To be safe, make sure mutable queries + /// have unique access to the components they query. + /// + /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool + #[inline] + pub unsafe fn for_each_unchecked) + Send + Sync + Clone>( + &self, + func: FN, + ) { + let thread_count = ComputeTaskPool::get().thread_num(); + if thread_count <= 1 { + self.state.for_each_unchecked_manual( + self.world, + func, + self.world.last_change_tick(), + self.world.read_change_tick(), + ); + } else { + // Need a batch size of at least 1. + let batch_size = self.get_batch_size(thread_count).max(1); + self.state.par_for_each_unchecked_manual( + self.world, + batch_size, + func, + self.world.last_change_tick(), + self.world.read_change_tick(), + ); + } + } + + fn get_batch_size(&self, thread_count: usize) -> usize { + if self.batching_strategy.batch_size_limits.is_empty() { + return self.batching_strategy.batch_size_limits.start; + } + + assert!( + thread_count > 0, + "Attempted to run parallel iteration over a query with an empty TaskPool" + ); + let max_size = if Q::IS_DENSE && F::IS_DENSE { + let tables = &self.world.storages().tables; + self.state + .matched_table_ids + .iter() + .map(|id| tables[*id].entity_count()) + .max() + .unwrap_or(0) + } else { + let archetypes = &self.world.archetypes(); + self.state + .matched_archetype_ids + .iter() + .map(|id| archetypes[*id].len()) + .max() + .unwrap_or(0) + }; + let batch_size = max_size / (thread_count * self.batching_strategy.batches_per_thread); + batch_size.clamp( + self.batching_strategy.batch_size_limits.start, + self.batching_strategy.batch_size_limits.end, + ) + } +} diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index c96dbca3938f8..b7b163a2f5340 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -4,7 +4,8 @@ use crate::{ entity::Entity, prelude::FromWorld, query::{ - Access, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, QueryIter, WorldQuery, + Access, BatchingStrategy, DebugCheckedUnwrap, FilteredAccess, QueryCombinationIter, + QueryIter, QueryParIter, WorldQuery, }, storage::{TableId, TableRow}, world::{World, WorldId}, @@ -140,7 +141,7 @@ impl QueryState { } /// Takes a query for the given [`World`], checks if the given world is the same as the query, and - /// generates new archetypes for the given world. + /// updates the [`QueryState`]'s view of the [`World`] with any newly-added archetypes. /// /// # Panics /// @@ -166,7 +167,8 @@ impl QueryState { ); } - /// Creates a new [`Archetype`]. + /// Update the current [`QueryState`] with information from the provided [`Archetype`] + /// (if applicable, i.e. if the archetype has any intersecting [`ComponentId`] with the current [`QueryState`]). pub fn new_archetype(&mut self, archetype: &Archetype) { if Q::matches_component_set(&self.fetch_state, &|id| archetype.contains(id)) && F::matches_component_set(&self.filter_state, &|id| archetype.contains(id)) @@ -812,87 +814,34 @@ impl QueryState { ); } - /// Runs `func` on each query result in parallel. + /// Returns a parallel iterator over the query results for the given [`World`]. /// - /// This can only be called for read-only queries, see [`Self::par_for_each_mut`] for - /// write-queries. - /// - /// # Panics - /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being - /// initialized and run from the ECS scheduler, this should never panic. - #[inline] - pub fn par_for_each<'w, FN: Fn(ROQueryItem<'w, Q>) + Send + Sync + Clone>( - &mut self, - world: &'w World, - batch_size: usize, - func: FN, - ) { - // SAFETY: query is read only - unsafe { - self.update_archetypes(world); - self.as_readonly().par_for_each_unchecked_manual( - world, - batch_size, - func, - world.last_change_tick(), - world.read_change_tick(), - ); - } - } - - /// Runs `func` on each query result in parallel. + /// This can only be called for read-only queries, see [`par_iter_mut`] for write-queries. /// - /// # Panics - /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being - /// initialized and run from the ECS scheduler, this should never panic. + /// [`par_iter_mut`]: Self::par_iter_mut #[inline] - pub fn par_for_each_mut<'w, FN: Fn(Q::Item<'w>) + Send + Sync + Clone>( - &mut self, - world: &'w mut World, - batch_size: usize, - func: FN, - ) { - let change_tick = world.change_tick(); - // SAFETY: query has unique world access - unsafe { - self.update_archetypes(world); - self.par_for_each_unchecked_manual( - world, - batch_size, - func, - world.last_change_tick(), - change_tick, - ); + pub fn par_iter<'w, 's>(&'s mut self, world: &'w World) -> QueryParIter<'w, 's, Q, F> { + self.update_archetypes(world); + QueryParIter { + world, + state: self, + batching_strategy: BatchingStrategy::new(), } } - /// Runs `func` on each query result in parallel. - /// - /// This can only be called for read-only queries. - /// - /// # Panics - /// The [`ComputeTaskPool`] is not initialized. If using this from a query that is being - /// initialized and run from the ECS scheduler, this should never panic. + /// Returns a parallel iterator over the query results for the given [`World`]. /// - /// # Safety + /// This can only be called for mutable queries, see [`par_iter`] for read-only-queries. /// - /// This does not check for mutable query correctness. To be safe, make sure mutable queries - /// have unique access to the components they query. + /// [`par_iter`]: Self::par_iter #[inline] - pub unsafe fn par_for_each_unchecked<'w, FN: Fn(Q::Item<'w>) + Send + Sync + Clone>( - &mut self, - world: &'w World, - batch_size: usize, - func: FN, - ) { + pub fn par_iter_mut<'w, 's>(&'s mut self, world: &'w mut World) -> QueryParIter<'w, 's, Q, F> { self.update_archetypes(world); - self.par_for_each_unchecked_manual( + QueryParIter { world, - batch_size, - func, - world.last_change_tick(), - world.read_change_tick(), - ); + state: self, + batching_strategy: BatchingStrategy::new(), + } } /// Runs `func` on each query result for the given [`World`], where the last change and diff --git a/crates/bevy_ecs/src/schedule/ambiguity_detection.rs b/crates/bevy_ecs/src/schedule/ambiguity_detection.rs index 558754c9dfcd5..5e1269309154d 100644 --- a/crates/bevy_ecs/src/schedule/ambiguity_detection.rs +++ b/crates/bevy_ecs/src/schedule/ambiguity_detection.rs @@ -335,6 +335,7 @@ mod tests { fn read_only() { let mut world = World::new(); world.insert_resource(R); + world.insert_non_send_resource(R); world.spawn(A); world.init_resource::>(); @@ -394,6 +395,7 @@ mod tests { fn nonsend() { let mut world = World::new(); world.insert_resource(R); + world.insert_non_send_resource(R); let mut test_stage = SystemStage::parallel(); test_stage @@ -497,6 +499,7 @@ mod tests { fn ignore_all_ambiguities() { let mut world = World::new(); world.insert_resource(R); + world.insert_non_send_resource(R); let mut test_stage = SystemStage::parallel(); test_stage @@ -513,6 +516,7 @@ mod tests { fn ambiguous_with_label() { let mut world = World::new(); world.insert_resource(R); + world.insert_non_send_resource(R); #[derive(SystemLabel)] struct IgnoreMe; diff --git a/crates/bevy_ecs/src/schedule/executor_parallel.rs b/crates/bevy_ecs/src/schedule/executor_parallel.rs index 68dd1f1ea798d..0db9627633ba1 100644 --- a/crates/bevy_ecs/src/schedule/executor_parallel.rs +++ b/crates/bevy_ecs/src/schedule/executor_parallel.rs @@ -1,11 +1,15 @@ +use std::sync::Arc; + +use crate as bevy_ecs; use crate::{ archetype::ArchetypeComponentId, query::Access, schedule::{ParallelSystemExecutor, SystemContainer}, + system::Resource, world::World, }; use async_channel::{Receiver, Sender}; -use bevy_tasks::{ComputeTaskPool, Scope, TaskPool}; +use bevy_tasks::{ComputeTaskPool, Scope, TaskPool, ThreadExecutor}; #[cfg(feature = "trace")] use bevy_utils::tracing::Instrument; use event_listener::Event; @@ -14,6 +18,16 @@ use fixedbitset::FixedBitSet; #[cfg(test)] use scheduling_event::*; +/// New-typed [`ThreadExecutor`] [`Resource`] that is used to run systems on the main thread +#[derive(Resource, Default, Clone)] +pub struct MainThreadExecutor(pub Arc>); + +impl MainThreadExecutor { + pub fn new() -> Self { + MainThreadExecutor(Arc::new(ThreadExecutor::new())) + } +} + struct SystemSchedulingMetadata { /// Used to signal the system's task to start the system. start: Event, @@ -124,40 +138,46 @@ impl ParallelSystemExecutor for ParallelExecutor { } } - ComputeTaskPool::init(TaskPool::default).scope(|scope| { - self.prepare_systems(scope, systems, world); - if self.should_run.count_ones(..) == 0 { - return; - } - let parallel_executor = async { - // All systems have been ran if there are no queued or running systems. - while 0 != self.queued.count_ones(..) + self.running.count_ones(..) { - self.process_queued_systems(); - // Avoid deadlocking if no systems were actually started. - if self.running.count_ones(..) != 0 { - // Wait until at least one system has finished. - let index = self - .finish_receiver - .recv() - .await - .unwrap_or_else(|error| unreachable!("{}", error)); - self.process_finished_system(index); - // Gather other systems than may have finished. - while let Ok(index) = self.finish_receiver.try_recv() { + let thread_executor = world.get_resource::().map(|e| &*e.0); + + ComputeTaskPool::init(TaskPool::default).scope_with_executor( + false, + thread_executor, + |scope| { + self.prepare_systems(scope, systems, world); + if self.should_run.count_ones(..) == 0 { + return; + } + let parallel_executor = async { + // All systems have been ran if there are no queued or running systems. + while 0 != self.queued.count_ones(..) + self.running.count_ones(..) { + self.process_queued_systems(); + // Avoid deadlocking if no systems were actually started. + if self.running.count_ones(..) != 0 { + // Wait until at least one system has finished. + let index = self + .finish_receiver + .recv() + .await + .unwrap_or_else(|error| unreachable!("{}", error)); self.process_finished_system(index); + // Gather other systems than may have finished. + while let Ok(index) = self.finish_receiver.try_recv() { + self.process_finished_system(index); + } + // At least one system has finished, so active access is outdated. + self.rebuild_active_access(); } - // At least one system has finished, so active access is outdated. - self.rebuild_active_access(); + self.update_counters_and_queue_systems(); } - self.update_counters_and_queue_systems(); - } - }; - #[cfg(feature = "trace")] - let span = bevy_utils::tracing::info_span!("parallel executor"); - #[cfg(feature = "trace")] - let parallel_executor = parallel_executor.instrument(span); - scope.spawn(parallel_executor); - }); + }; + #[cfg(feature = "trace")] + let span = bevy_utils::tracing::info_span!("parallel executor"); + #[cfg(feature = "trace")] + let parallel_executor = parallel_executor.instrument(span); + scope.spawn(parallel_executor); + }, + ); } } diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 222339606fec3..bc78e8f0798ac 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -220,14 +220,11 @@ impl Schedule { stage_label: impl StageLabel, system: impl IntoSystemDescriptor, ) -> &mut Self { - // Use a function instead of a closure to ensure that it is codegend inside bevy_ecs instead + // Use a function instead of a closure to ensure that it is codegened inside bevy_ecs instead // of the game. Closures inherit generic parameters from their enclosing function. #[cold] fn stage_not_found(stage_label: &dyn Debug) -> ! { - panic!( - "Stage '{:?}' does not exist or is not a SystemStage", - stage_label - ) + panic!("Stage '{stage_label:?}' does not exist or is not a SystemStage",) } let label = stage_label.as_label(); @@ -361,6 +358,17 @@ impl Schedule { .and_then(|stage| stage.downcast_mut::()) } + /// Removes a [`Stage`] from the schedule. + pub fn remove_stage(&mut self, stage_label: impl StageLabel) -> Option> { + let label = stage_label.as_label(); + + let Some(index) = self.stage_order.iter().position(|x| *x == label) else { + return None; + }; + self.stage_order.remove(index); + self.stages.remove(&label) + } + /// Executes each [`Stage`] contained in the schedule, one at a time. pub fn run_once(&mut self, world: &mut World) { for label in &self.stage_order { diff --git a/crates/bevy_ecs/src/schedule/stage.rs b/crates/bevy_ecs/src/schedule/stage.rs index d7386d2e5990d..05846c630bb7c 100644 --- a/crates/bevy_ecs/src/schedule/stage.rs +++ b/crates/bevy_ecs/src/schedule/stage.rs @@ -287,10 +287,9 @@ impl SystemStage { .then(|| descriptor.system.name()) { panic!( - "The system {} has a run criteria, but its `SystemSet` also has a run \ + "The system {name} has a run criteria, but its `SystemSet` also has a run \ criteria. This is not supported. Consider moving the system into a \ - different `SystemSet` or calling `add_system()` instead.", - name + different `SystemSet` or calling `add_system()` instead." ) } } @@ -459,8 +458,7 @@ impl SystemStage { writeln!(message, " - {}", nodes[*index].name()).unwrap(); writeln!( message, - " wants to be after (because of labels: {:?})", - labels, + " wants to be after (because of labels: {labels:?})", ) .unwrap(); } @@ -565,10 +563,7 @@ impl SystemStage { if let RunCriteriaInner::Piped { input: parent, .. } = &mut criteria.inner { let label = &criteria.after[0]; *parent = *labels.get(label).unwrap_or_else(|| { - panic!( - "Couldn't find run criteria labelled {:?} to pipe from.", - label - ) + panic!("Couldn't find run criteria labelled {label:?} to pipe from.",) }); } } diff --git a/crates/bevy_ecs/src/schedule_v3/condition.rs b/crates/bevy_ecs/src/schedule_v3/condition.rs new file mode 100644 index 0000000000000..e617375f763a0 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/condition.rs @@ -0,0 +1,97 @@ +pub use common_conditions::*; + +use crate::system::BoxedSystem; + +pub type BoxedCondition = BoxedSystem<(), bool>; + +/// A system that determines if one or more scheduled systems should run. +/// +/// Implemented for functions and closures that convert into [`System`](crate::system::System) +/// with [read-only](crate::system::ReadOnlySystemParam) parameters. +pub trait Condition: sealed::Condition {} + +impl Condition for F where F: sealed::Condition {} + +mod sealed { + use crate::system::{IntoSystem, IsFunctionSystem, ReadOnlySystemParam, SystemParamFunction}; + + pub trait Condition: IntoSystem<(), bool, Params> {} + + impl Condition<(IsFunctionSystem, Params, Marker)> for F + where + F: SystemParamFunction<(), bool, Params, Marker> + Send + Sync + 'static, + Params: ReadOnlySystemParam + 'static, + Marker: 'static, + { + } +} + +mod common_conditions { + use crate::schedule_v3::{State, States}; + use crate::system::{Res, Resource}; + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the resource exists. + pub fn resource_exists() -> impl FnMut(Option>) -> bool + where + T: Resource, + { + move |res: Option>| res.is_some() + } + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the resource is equal to `value`. + /// + /// # Panics + /// + /// The condition will panic if the resource does not exist. + pub fn resource_equals(value: T) -> impl FnMut(Res) -> bool + where + T: Resource + PartialEq, + { + move |res: Res| *res == value + } + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the resource exists and is equal to `value`. + /// + /// The condition will return `false` if the resource does not exist. + pub fn resource_exists_and_equals(value: T) -> impl FnMut(Option>) -> bool + where + T: Resource + PartialEq, + { + move |res: Option>| match res { + Some(res) => *res == value, + None => false, + } + } + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the state machine exists. + pub fn state_exists() -> impl FnMut(Option>>) -> bool { + move |current_state: Option>>| current_state.is_some() + } + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the state machine is currently in `state`. + /// + /// # Panics + /// + /// The condition will panic if the resource does not exist. + pub fn state_equals(state: S) -> impl FnMut(Res>) -> bool { + move |current_state: Res>| current_state.0 == state + } + + /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` + /// if the state machine exists and is currently in `state`. + /// + /// The condition will return `false` if the state does not exist. + pub fn state_exists_and_equals( + state: S, + ) -> impl FnMut(Option>>) -> bool { + move |current_state: Option>>| match current_state { + Some(current_state) => current_state.0 == state, + None => false, + } + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/config.rs b/crates/bevy_ecs/src/schedule_v3/config.rs new file mode 100644 index 0000000000000..d7a87c374c047 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/config.rs @@ -0,0 +1,649 @@ +use bevy_ecs_macros::all_tuples; +use bevy_utils::default; + +use crate::{ + schedule_v3::{ + condition::{BoxedCondition, Condition}, + graph_utils::{Ambiguity, Dependency, DependencyKind, GraphInfo}, + set::{BoxedSystemSet, IntoSystemSet, SystemSet}, + }, + system::{BoxedSystem, IntoSystem, System}, +}; + +/// A [`SystemSet`] with scheduling metadata. +pub struct SystemSetConfig { + pub(super) set: BoxedSystemSet, + pub(super) graph_info: GraphInfo, + pub(super) conditions: Vec, +} + +impl SystemSetConfig { + fn new(set: BoxedSystemSet) -> Self { + // system type sets are automatically populated + // to avoid unintentionally broad changes, they cannot be configured + assert!( + !set.is_system_type(), + "configuring system type sets is not allowed" + ); + + Self { + set, + graph_info: GraphInfo { + sets: Vec::new(), + dependencies: Vec::new(), + ambiguous_with: default(), + }, + conditions: Vec::new(), + } + } +} + +/// A [`System`] with scheduling metadata. +pub struct SystemConfig { + pub(super) system: BoxedSystem, + pub(super) graph_info: GraphInfo, + pub(super) conditions: Vec, +} + +impl SystemConfig { + fn new(system: BoxedSystem) -> Self { + // include system in its default sets + let sets = system.default_system_sets().into_iter().collect(); + Self { + system, + graph_info: GraphInfo { + sets, + dependencies: Vec::new(), + ambiguous_with: default(), + }, + conditions: Vec::new(), + } + } +} + +fn new_condition

(condition: impl Condition

) -> BoxedCondition { + let condition_system = IntoSystem::into_system(condition); + assert!( + condition_system.is_send(), + "Condition `{}` accesses thread-local resources. This is not currently supported.", + condition_system.name() + ); + + Box::new(condition_system) +} + +fn ambiguous_with(graph_info: &mut GraphInfo, set: BoxedSystemSet) { + match &mut graph_info.ambiguous_with { + detection @ Ambiguity::Check => { + *detection = Ambiguity::IgnoreWithSet(vec![set]); + } + Ambiguity::IgnoreWithSet(ambiguous_with) => { + ambiguous_with.push(set); + } + Ambiguity::IgnoreAll => (), + } +} + +/// Types that can be converted into a [`SystemSetConfig`]. +/// +/// This has been implemented for all types that implement [`SystemSet`] and boxed trait objects. +pub trait IntoSystemSetConfig: sealed::IntoSystemSetConfig { + /// Convert into a [`SystemSetConfig`]. + #[doc(hidden)] + fn into_config(self) -> SystemSetConfig; + /// Add to the provided `set`. + fn in_set(self, set: impl SystemSet) -> SystemSetConfig; + /// Run before all systems in `set`. + fn before(self, set: impl IntoSystemSet) -> SystemSetConfig; + /// Run after all systems in `set`. + fn after(self, set: impl IntoSystemSet) -> SystemSetConfig; + /// Run the systems in this set only if the [`Condition`] is `true`. + /// + /// The `Condition` will be evaluated at most once (per schedule run), + /// the first time a system in this set prepares to run. + fn run_if

(self, condition: impl Condition

) -> SystemSetConfig; + /// Suppress warnings and errors that would result from systems in this set having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemSetConfig; + /// Suppress warnings and errors that would result from systems in this set having ambiguities + /// (conflicting access but indeterminate order) with any other system. + fn ambiguous_with_all(self) -> SystemSetConfig; +} + +impl IntoSystemSetConfig for S +where + S: SystemSet + sealed::IntoSystemSetConfig, +{ + fn into_config(self) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)) + } + + fn in_set(self, set: impl SystemSet) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).in_set(set) + } + + fn before(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).before(set) + } + + fn after(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).after(set) + } + + fn run_if

(self, condition: impl Condition

) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).run_if(condition) + } + + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).ambiguous_with(set) + } + + fn ambiguous_with_all(self) -> SystemSetConfig { + SystemSetConfig::new(Box::new(self)).ambiguous_with_all() + } +} + +impl IntoSystemSetConfig for BoxedSystemSet { + fn into_config(self) -> SystemSetConfig { + SystemSetConfig::new(self) + } + + fn in_set(self, set: impl SystemSet) -> SystemSetConfig { + SystemSetConfig::new(self).in_set(set) + } + + fn before(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(self).before(set) + } + + fn after(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(self).after(set) + } + + fn run_if

(self, condition: impl Condition

) -> SystemSetConfig { + SystemSetConfig::new(self).run_if(condition) + } + + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemSetConfig { + SystemSetConfig::new(self).ambiguous_with(set) + } + + fn ambiguous_with_all(self) -> SystemSetConfig { + SystemSetConfig::new(self).ambiguous_with_all() + } +} + +impl IntoSystemSetConfig for SystemSetConfig { + fn into_config(self) -> Self { + self + } + + fn in_set(mut self, set: impl SystemSet) -> Self { + assert!( + !set.is_system_type(), + "adding arbitrary systems to a system type set is not allowed" + ); + self.graph_info.sets.push(Box::new(set)); + self + } + + fn before(mut self, set: impl IntoSystemSet) -> Self { + self.graph_info.dependencies.push(Dependency::new( + DependencyKind::Before, + Box::new(set.into_system_set()), + )); + self + } + + fn after(mut self, set: impl IntoSystemSet) -> Self { + self.graph_info.dependencies.push(Dependency::new( + DependencyKind::After, + Box::new(set.into_system_set()), + )); + self + } + + fn run_if

(mut self, condition: impl Condition

) -> Self { + self.conditions.push(new_condition(condition)); + self + } + + fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { + ambiguous_with(&mut self.graph_info, Box::new(set.into_system_set())); + self + } + + fn ambiguous_with_all(mut self) -> Self { + self.graph_info.ambiguous_with = Ambiguity::IgnoreAll; + self + } +} + +/// Types that can be converted into a [`SystemConfig`]. +/// +/// This has been implemented for boxed [`System`](crate::system::System) +/// trait objects and all functions that turn into such. +pub trait IntoSystemConfig: sealed::IntoSystemConfig { + /// Convert into a [`SystemConfig`]. + #[doc(hidden)] + fn into_config(self) -> SystemConfig; + /// Add to `set` membership. + fn in_set(self, set: impl SystemSet) -> SystemConfig; + /// Run before all systems in `set`. + fn before(self, set: impl IntoSystemSet) -> SystemConfig; + /// Run after all systems in `set`. + fn after(self, set: impl IntoSystemSet) -> SystemConfig; + /// Run only if the [`Condition`] is `true`. + /// + /// The `Condition` will be evaluated at most once (per schedule run), + /// when the system prepares to run. + fn run_if

(self, condition: impl Condition

) -> SystemConfig; + /// Suppress warnings and errors that would result from this system having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfig; + /// Suppress warnings and errors that would result from this system having ambiguities + /// (conflicting access but indeterminate order) with any other system. + fn ambiguous_with_all(self) -> SystemConfig; +} + +impl IntoSystemConfig for F +where + F: IntoSystem<(), (), Params> + sealed::IntoSystemConfig, +{ + fn into_config(self) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))) + } + + fn in_set(self, set: impl SystemSet) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).in_set(set) + } + + fn before(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).before(set) + } + + fn after(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).after(set) + } + + fn run_if

(self, condition: impl Condition

) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).run_if(condition) + } + + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).ambiguous_with(set) + } + + fn ambiguous_with_all(self) -> SystemConfig { + SystemConfig::new(Box::new(IntoSystem::into_system(self))).ambiguous_with_all() + } +} + +impl IntoSystemConfig<()> for BoxedSystem<(), ()> { + fn into_config(self) -> SystemConfig { + SystemConfig::new(self) + } + + fn in_set(self, set: impl SystemSet) -> SystemConfig { + SystemConfig::new(self).in_set(set) + } + + fn before(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(self).before(set) + } + + fn after(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(self).after(set) + } + + fn run_if

(self, condition: impl Condition

) -> SystemConfig { + SystemConfig::new(self).run_if(condition) + } + + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfig { + SystemConfig::new(self).ambiguous_with(set) + } + + fn ambiguous_with_all(self) -> SystemConfig { + SystemConfig::new(self).ambiguous_with_all() + } +} + +impl IntoSystemConfig<()> for SystemConfig { + fn into_config(self) -> Self { + self + } + + fn in_set(mut self, set: impl SystemSet) -> Self { + assert!( + !set.is_system_type(), + "adding arbitrary systems to a system type set is not allowed" + ); + self.graph_info.sets.push(Box::new(set)); + self + } + + fn before(mut self, set: impl IntoSystemSet) -> Self { + self.graph_info.dependencies.push(Dependency::new( + DependencyKind::Before, + Box::new(set.into_system_set()), + )); + self + } + + fn after(mut self, set: impl IntoSystemSet) -> Self { + self.graph_info.dependencies.push(Dependency::new( + DependencyKind::After, + Box::new(set.into_system_set()), + )); + self + } + + fn run_if

(mut self, condition: impl Condition

) -> Self { + self.conditions.push(new_condition(condition)); + self + } + + fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { + ambiguous_with(&mut self.graph_info, Box::new(set.into_system_set())); + self + } + + fn ambiguous_with_all(mut self) -> Self { + self.graph_info.ambiguous_with = Ambiguity::IgnoreAll; + self + } +} + +// only `System` system objects can be scheduled +mod sealed { + use crate::{ + schedule_v3::{BoxedSystemSet, SystemSet}, + system::{BoxedSystem, IntoSystem}, + }; + + use super::{SystemConfig, SystemSetConfig}; + + pub trait IntoSystemConfig {} + + impl> IntoSystemConfig for F {} + + impl IntoSystemConfig<()> for BoxedSystem<(), ()> {} + + impl IntoSystemConfig<()> for SystemConfig {} + + pub trait IntoSystemSetConfig {} + + impl IntoSystemSetConfig for S {} + + impl IntoSystemSetConfig for BoxedSystemSet {} + + impl IntoSystemSetConfig for SystemSetConfig {} +} + +/// A collection of [`SystemConfig`]. +pub struct SystemConfigs { + pub(super) systems: Vec, + /// If `true`, adds `before -> after` ordering constraints between the successive elements. + pub(super) chained: bool, +} + +/// Types that can convert into a [`SystemConfigs`]. +pub trait IntoSystemConfigs +where + Self: Sized, +{ + /// Convert into a [`SystemConfigs`]. + #[doc(hidden)] + fn into_configs(self) -> SystemConfigs; + + /// Add these systems to the provided `set`. + fn in_set(self, set: impl SystemSet) -> SystemConfigs { + self.into_configs().in_set(set) + } + + /// Run before all systems in `set`. + fn before(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().before(set) + } + + /// Run after all systems in `set`. + fn after(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().after(set) + } + + /// Suppress warnings and errors that would result from these systems having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().ambiguous_with(set) + } + + /// Suppress warnings and errors that would result from these systems having ambiguities + /// (conflicting access but indeterminate order) with any other system. + fn ambiguous_with_all(self) -> SystemConfigs { + self.into_configs().ambiguous_with_all() + } + + /// Treat this collection as a sequence of systems. + /// + /// Ordering constraints will be applied between the successive elements. + fn chain(self) -> SystemConfigs { + self.into_configs().chain() + } +} + +impl IntoSystemConfigs<()> for SystemConfigs { + fn into_configs(self) -> Self { + self + } + + fn in_set(mut self, set: impl SystemSet) -> Self { + assert!( + !set.is_system_type(), + "adding arbitrary systems to a system type set is not allowed" + ); + for config in &mut self.systems { + config.graph_info.sets.push(set.dyn_clone()); + } + + self + } + + fn before(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.systems { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::Before, set.dyn_clone())); + } + + self + } + + fn after(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.systems { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::After, set.dyn_clone())); + } + + self + } + + fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.systems { + ambiguous_with(&mut config.graph_info, set.dyn_clone()); + } + + self + } + + fn ambiguous_with_all(mut self) -> Self { + for config in &mut self.systems { + config.graph_info.ambiguous_with = Ambiguity::IgnoreAll; + } + + self + } + + fn chain(mut self) -> Self { + self.chained = true; + self + } +} + +/// A collection of [`SystemSetConfig`]. +pub struct SystemSetConfigs { + pub(super) sets: Vec, + /// If `true`, adds `before -> after` ordering constraints between the successive elements. + pub(super) chained: bool, +} + +/// Types that can convert into a [`SystemSetConfigs`]. +pub trait IntoSystemSetConfigs +where + Self: Sized, +{ + /// Convert into a [`SystemSetConfigs`]. + #[doc(hidden)] + fn into_configs(self) -> SystemSetConfigs; + + /// Add these system sets to the provided `set`. + fn in_set(self, set: impl SystemSet) -> SystemSetConfigs { + self.into_configs().in_set(set) + } + + /// Run before all systems in `set`. + fn before(self, set: impl IntoSystemSet) -> SystemSetConfigs { + self.into_configs().before(set) + } + + /// Run after all systems in `set`. + fn after(self, set: impl IntoSystemSet) -> SystemSetConfigs { + self.into_configs().after(set) + } + + /// Suppress warnings and errors that would result from systems in these sets having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemSetConfigs { + self.into_configs().ambiguous_with(set) + } + + /// Suppress warnings and errors that would result from systems in these sets having ambiguities + /// (conflicting access but indeterminate order) with any other system. + fn ambiguous_with_all(self) -> SystemSetConfigs { + self.into_configs().ambiguous_with_all() + } + + /// Treat this collection as a sequence of system sets. + /// + /// Ordering constraints will be applied between the successive elements. + fn chain(self) -> SystemSetConfigs { + self.into_configs().chain() + } +} + +impl IntoSystemSetConfigs for SystemSetConfigs { + fn into_configs(self) -> Self { + self + } + + fn in_set(mut self, set: impl SystemSet) -> Self { + assert!( + !set.is_system_type(), + "adding arbitrary systems to a system type set is not allowed" + ); + for config in &mut self.sets { + config.graph_info.sets.push(set.dyn_clone()); + } + + self + } + + fn before(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.sets { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::Before, set.dyn_clone())); + } + + self + } + + fn after(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.sets { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::After, set.dyn_clone())); + } + + self + } + + fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + for config in &mut self.sets { + ambiguous_with(&mut config.graph_info, set.dyn_clone()); + } + + self + } + + fn ambiguous_with_all(mut self) -> Self { + for config in &mut self.sets { + config.graph_info.ambiguous_with = Ambiguity::IgnoreAll; + } + + self + } + + fn chain(mut self) -> Self { + self.chained = true; + self + } +} + +macro_rules! impl_system_collection { + ($(($param: ident, $sys: ident)),*) => { + impl<$($param, $sys),*> IntoSystemConfigs<($($param,)*)> for ($($sys,)*) + where + $($sys: IntoSystemConfig<$param>),* + { + #[allow(non_snake_case)] + fn into_configs(self) -> SystemConfigs { + let ($($sys,)*) = self; + SystemConfigs { + systems: vec![$($sys.into_config(),)*], + chained: false, + } + } + } + } +} + +macro_rules! impl_system_set_collection { + ($($set: ident),*) => { + impl<$($set: IntoSystemSetConfig),*> IntoSystemSetConfigs for ($($set,)*) + { + #[allow(non_snake_case)] + fn into_configs(self) -> SystemSetConfigs { + let ($($set,)*) = self; + SystemSetConfigs { + sets: vec![$($set.into_config(),)*], + chained: false, + } + } + } + } +} + +all_tuples!(impl_system_collection, 0, 15, P, S); +all_tuples!(impl_system_set_collection, 0, 15, S); diff --git a/crates/bevy_ecs/src/schedule_v3/executor/mod.rs b/crates/bevy_ecs/src/schedule_v3/executor/mod.rs new file mode 100644 index 0000000000000..bfc1eef14d609 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/executor/mod.rs @@ -0,0 +1,90 @@ +mod multi_threaded; +mod simple; +mod single_threaded; + +pub use self::multi_threaded::MultiThreadedExecutor; +pub use self::simple::SimpleExecutor; +pub use self::single_threaded::SingleThreadedExecutor; + +use fixedbitset::FixedBitSet; + +use crate::{ + schedule_v3::{BoxedCondition, NodeId}, + system::BoxedSystem, + world::World, +}; + +/// Types that can run a [`SystemSchedule`] on a [`World`]. +pub(super) trait SystemExecutor: Send + Sync { + fn kind(&self) -> ExecutorKind; + fn init(&mut self, schedule: &SystemSchedule); + fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World); +} + +/// Specifies how a [`Schedule`](super::Schedule) will be run. +/// +/// [`MultiThreaded`](ExecutorKind::MultiThreaded) is the default. +#[derive(PartialEq, Eq, Default)] +pub enum ExecutorKind { + /// Runs the schedule using a single thread. + /// + /// Useful if you're dealing with a single-threaded environment, saving your threads for + /// other things, or just trying minimize overhead. + SingleThreaded, + /// Like [`SingleThreaded`](ExecutorKind::SingleThreaded) but calls [`apply_buffers`](crate::system::System::apply_buffers) + /// immediately after running each system. + Simple, + /// Runs the schedule using a thread pool. Non-conflicting systems can run in parallel. + #[default] + MultiThreaded, +} + +/// Holds systems and conditions of a [`Schedule`](super::Schedule) sorted in topological order +/// (along with dependency information for multi-threaded execution). +/// +/// Since the arrays are sorted in the same order, elements are referenced by their index. +/// `FixedBitSet` is used as a smaller, more efficient substitute of `HashSet`. +#[derive(Default)] +pub(super) struct SystemSchedule { + pub(super) systems: Vec, + pub(super) system_conditions: Vec>, + pub(super) set_conditions: Vec>, + pub(super) system_ids: Vec, + pub(super) set_ids: Vec, + pub(super) system_dependencies: Vec, + pub(super) system_dependents: Vec>, + pub(super) sets_of_systems: Vec, + pub(super) systems_in_sets: Vec, +} + +impl SystemSchedule { + pub const fn new() -> Self { + Self { + systems: Vec::new(), + system_conditions: Vec::new(), + set_conditions: Vec::new(), + system_ids: Vec::new(), + set_ids: Vec::new(), + system_dependencies: Vec::new(), + system_dependents: Vec::new(), + sets_of_systems: Vec::new(), + systems_in_sets: Vec::new(), + } + } +} + +/// Instructs the executor to call [`apply_buffers`](crate::system::System::apply_buffers) +/// on the systems that have run but not applied their buffers. +/// +/// **Notes** +/// - This function (currently) does nothing if it's called manually or wrapped inside a [`PipeSystem`](crate::system::PipeSystem). +/// - Modifying a [`Schedule`](super::Schedule) may change the order buffers are applied. +#[allow(unused_variables)] +pub fn apply_system_buffers(world: &mut World) {} + +/// Returns `true` if the [`System`](crate::system::System) is an instance of [`apply_system_buffers`]. +pub(super) fn is_apply_system_buffers(system: &BoxedSystem) -> bool { + use std::any::Any; + // deref to use `System::type_id` instead of `Any::type_id` + system.as_ref().type_id() == apply_system_buffers.type_id() +} diff --git a/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs new file mode 100644 index 0000000000000..3debba3ac59b4 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/executor/multi_threaded.rs @@ -0,0 +1,575 @@ +use bevy_tasks::{ComputeTaskPool, Scope, TaskPool}; +use bevy_utils::default; +use bevy_utils::syncunsafecell::SyncUnsafeCell; +#[cfg(feature = "trace")] +use bevy_utils::tracing::{info_span, Instrument}; + +use async_channel::{Receiver, Sender}; +use fixedbitset::FixedBitSet; + +use crate::{ + archetype::ArchetypeComponentId, + query::Access, + schedule_v3::{ + is_apply_system_buffers, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + }, + system::BoxedSystem, + world::World, +}; + +/// A funky borrow split of [`SystemSchedule`] required by the [`MultiThreadedExecutor`]. +struct SyncUnsafeSchedule<'a> { + systems: &'a [SyncUnsafeCell], + conditions: Conditions<'a>, +} + +struct Conditions<'a> { + system_conditions: &'a mut [Vec], + set_conditions: &'a mut [Vec], + sets_of_systems: &'a [FixedBitSet], + systems_in_sets: &'a [FixedBitSet], +} + +impl SyncUnsafeSchedule<'_> { + fn new(schedule: &mut SystemSchedule) -> SyncUnsafeSchedule<'_> { + SyncUnsafeSchedule { + systems: SyncUnsafeCell::from_mut(schedule.systems.as_mut_slice()).as_slice_of_cells(), + conditions: Conditions { + system_conditions: &mut schedule.system_conditions, + set_conditions: &mut schedule.set_conditions, + sets_of_systems: &schedule.sets_of_systems, + systems_in_sets: &schedule.systems_in_sets, + }, + } + } +} + +/// Per-system data used by the [`MultiThreadedExecutor`]. +// Copied here because it can't be read from the system when it's running. +struct SystemTaskMetadata { + /// The `ArchetypeComponentId` access of the system. + archetype_component_access: Access, + /// Indices of the systems that directly depend on the system. + dependents: Vec, + /// Is `true` if the system does not access `!Send` data. + is_send: bool, + /// Is `true` if the system is exclusive. + is_exclusive: bool, +} + +/// Runs the schedule using a thread pool. Non-conflicting systems can run in parallel. +pub struct MultiThreadedExecutor { + /// Sends system completion events. + sender: Sender, + /// Receives system completion events. + receiver: Receiver, + /// Metadata for scheduling and running system tasks. + system_task_metadata: Vec, + /// Union of the accesses of all currently running systems. + active_access: Access, + /// Returns `true` if a system with non-`Send` access is running. + local_thread_running: bool, + /// Returns `true` if an exclusive system is running. + exclusive_running: bool, + /// The number of systems that are running. + num_running_systems: usize, + /// The number of systems that have completed. + num_completed_systems: usize, + /// The number of dependencies each system has that have not completed. + num_dependencies_remaining: Vec, + /// System sets whose conditions have been evaluated. + evaluated_sets: FixedBitSet, + /// Systems that have no remaining dependencies and are waiting to run. + ready_systems: FixedBitSet, + /// copy of `ready_systems` + ready_systems_copy: FixedBitSet, + /// Systems that are running. + running_systems: FixedBitSet, + /// Systems that got skipped. + skipped_systems: FixedBitSet, + /// Systems whose conditions have been evaluated and were run or skipped. + completed_systems: FixedBitSet, + /// Systems that have run but have not had their buffers applied. + unapplied_systems: FixedBitSet, +} + +impl Default for MultiThreadedExecutor { + fn default() -> Self { + Self::new() + } +} + +impl SystemExecutor for MultiThreadedExecutor { + fn kind(&self) -> ExecutorKind { + ExecutorKind::MultiThreaded + } + + fn init(&mut self, schedule: &SystemSchedule) { + // pre-allocate space + let sys_count = schedule.system_ids.len(); + let set_count = schedule.set_ids.len(); + + self.evaluated_sets = FixedBitSet::with_capacity(set_count); + self.ready_systems = FixedBitSet::with_capacity(sys_count); + self.ready_systems_copy = FixedBitSet::with_capacity(sys_count); + self.running_systems = FixedBitSet::with_capacity(sys_count); + self.completed_systems = FixedBitSet::with_capacity(sys_count); + self.skipped_systems = FixedBitSet::with_capacity(sys_count); + self.unapplied_systems = FixedBitSet::with_capacity(sys_count); + + self.system_task_metadata = Vec::with_capacity(sys_count); + for index in 0..sys_count { + self.system_task_metadata.push(SystemTaskMetadata { + archetype_component_access: default(), + dependents: schedule.system_dependents[index].clone(), + is_send: schedule.systems[index].is_send(), + is_exclusive: schedule.systems[index].is_exclusive(), + }); + } + + self.num_dependencies_remaining = Vec::with_capacity(sys_count); + } + + fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + // reset counts + let num_systems = schedule.systems.len(); + self.num_running_systems = 0; + self.num_completed_systems = 0; + self.num_dependencies_remaining.clear(); + self.num_dependencies_remaining + .extend_from_slice(&schedule.system_dependencies); + + for (system_index, dependencies) in self.num_dependencies_remaining.iter_mut().enumerate() { + if *dependencies == 0 { + self.ready_systems.insert(system_index); + } + } + + let world = SyncUnsafeCell::from_mut(world); + let SyncUnsafeSchedule { + systems, + mut conditions, + } = SyncUnsafeSchedule::new(schedule); + + ComputeTaskPool::init(TaskPool::default).scope(|scope| { + // the executor itself is a `Send` future so that it can run + // alongside systems that claim the local thread + let executor = async { + while self.num_completed_systems < num_systems { + // SAFETY: self.ready_systems does not contain running systems + unsafe { + self.spawn_system_tasks(scope, systems, &mut conditions, world); + } + + if self.num_running_systems > 0 { + // wait for systems to complete + let index = self + .receiver + .recv() + .await + .unwrap_or_else(|error| unreachable!("{}", error)); + + self.finish_system_and_signal_dependents(index); + + while let Ok(index) = self.receiver.try_recv() { + self.finish_system_and_signal_dependents(index); + } + + self.rebuild_active_access(); + } + } + + // SAFETY: all systems have completed + let world = unsafe { &mut *world.get() }; + apply_system_buffers(&mut self.unapplied_systems, systems, world); + + debug_assert!(self.ready_systems.is_clear()); + debug_assert!(self.running_systems.is_clear()); + debug_assert!(self.unapplied_systems.is_clear()); + self.active_access.clear(); + self.evaluated_sets.clear(); + self.skipped_systems.clear(); + self.completed_systems.clear(); + }; + + #[cfg(feature = "trace")] + let executor_span = info_span!("schedule_task"); + #[cfg(feature = "trace")] + let executor = executor.instrument(executor_span); + scope.spawn(executor); + }); + } +} + +impl MultiThreadedExecutor { + pub fn new() -> Self { + let (sender, receiver) = async_channel::unbounded(); + Self { + sender, + receiver, + system_task_metadata: Vec::new(), + num_running_systems: 0, + num_completed_systems: 0, + num_dependencies_remaining: Vec::new(), + active_access: default(), + local_thread_running: false, + exclusive_running: false, + evaluated_sets: FixedBitSet::new(), + ready_systems: FixedBitSet::new(), + ready_systems_copy: FixedBitSet::new(), + running_systems: FixedBitSet::new(), + skipped_systems: FixedBitSet::new(), + completed_systems: FixedBitSet::new(), + unapplied_systems: FixedBitSet::new(), + } + } + + /// # Safety + /// Caller must ensure that `self.ready_systems` does not contain any systems that + /// have been mutably borrowed (such as the systems currently running). + unsafe fn spawn_system_tasks<'scope>( + &mut self, + scope: &Scope<'_, 'scope, ()>, + systems: &'scope [SyncUnsafeCell], + conditions: &mut Conditions, + cell: &'scope SyncUnsafeCell, + ) { + if self.exclusive_running { + return; + } + + // can't borrow since loop mutably borrows `self` + let mut ready_systems = std::mem::take(&mut self.ready_systems_copy); + ready_systems.clear(); + ready_systems.union_with(&self.ready_systems); + + for system_index in ready_systems.ones() { + assert!(!self.running_systems.contains(system_index)); + // SAFETY: Caller assured that these systems are not running. + // Therefore, no other reference to this system exists and there is no aliasing. + let system = unsafe { &mut *systems[system_index].get() }; + + // SAFETY: No exclusive system is running. + // Therefore, there is no existing mutable reference to the world. + let world = unsafe { &*cell.get() }; + if !self.can_run(system_index, system, conditions, world) { + // NOTE: exclusive systems with ambiguities are susceptible to + // being significantly displaced here (compared to single-threaded order) + // if systems after them in topological order can run + // if that becomes an issue, `break;` if exclusive system + continue; + } + + self.ready_systems.set(system_index, false); + + if !self.should_run(system_index, system, conditions, world) { + self.skip_system_and_signal_dependents(system_index); + continue; + } + + self.running_systems.insert(system_index); + self.num_running_systems += 1; + + if self.system_task_metadata[system_index].is_exclusive { + // SAFETY: `can_run` confirmed that no systems are running. + // Therefore, there is no existing reference to the world. + unsafe { + let world = &mut *cell.get(); + self.spawn_exclusive_system_task(scope, system_index, systems, world); + } + break; + } + + // SAFETY: No other reference to this system exists. + unsafe { + self.spawn_system_task(scope, system_index, systems, world); + } + } + + // give back + self.ready_systems_copy = ready_systems; + } + + fn can_run( + &mut self, + system_index: usize, + system: &mut BoxedSystem, + conditions: &mut Conditions, + world: &World, + ) -> bool { + #[cfg(feature = "trace")] + let _span = info_span!("check_access", name = &*system.name()).entered(); + + let system_meta = &self.system_task_metadata[system_index]; + if system_meta.is_exclusive && self.num_running_systems > 0 { + return false; + } + + if !system_meta.is_send && self.local_thread_running { + return false; + } + + // TODO: an earlier out if world's archetypes did not change + for set_idx in conditions.sets_of_systems[system_index].difference(&self.evaluated_sets) { + for condition in &mut conditions.set_conditions[set_idx] { + condition.update_archetype_component_access(world); + if !condition + .archetype_component_access() + .is_compatible(&self.active_access) + { + return false; + } + } + } + + for condition in &mut conditions.system_conditions[system_index] { + condition.update_archetype_component_access(world); + if !condition + .archetype_component_access() + .is_compatible(&self.active_access) + { + return false; + } + } + + if !self.skipped_systems.contains(system_index) { + system.update_archetype_component_access(world); + if !system + .archetype_component_access() + .is_compatible(&self.active_access) + { + return false; + } + + // PERF: use an optimized clear() + extend() operation + let meta_access = + &mut self.system_task_metadata[system_index].archetype_component_access; + meta_access.clear(); + meta_access.extend(system.archetype_component_access()); + } + + true + } + + fn should_run( + &mut self, + system_index: usize, + _system: &BoxedSystem, + conditions: &mut Conditions, + world: &World, + ) -> bool { + #[cfg(feature = "trace")] + let _span = info_span!("check_conditions", name = &*_system.name()).entered(); + + let mut should_run = !self.skipped_systems.contains(system_index); + for set_idx in conditions.sets_of_systems[system_index].ones() { + if self.evaluated_sets.contains(set_idx) { + continue; + } + + // evaluate system set's conditions + let set_conditions_met = + evaluate_and_fold_conditions(&mut conditions.set_conditions[set_idx], world); + + if !set_conditions_met { + self.skipped_systems + .union_with(&conditions.systems_in_sets[set_idx]); + } + + should_run &= set_conditions_met; + self.evaluated_sets.insert(set_idx); + } + + // evaluate system's conditions + let system_conditions_met = + evaluate_and_fold_conditions(&mut conditions.system_conditions[system_index], world); + + if !system_conditions_met { + self.skipped_systems.insert(system_index); + } + + should_run &= system_conditions_met; + + should_run + } + + /// # Safety + /// Caller must not alias systems that are running. + unsafe fn spawn_system_task<'scope>( + &mut self, + scope: &Scope<'_, 'scope, ()>, + system_index: usize, + systems: &'scope [SyncUnsafeCell], + world: &'scope World, + ) { + // SAFETY: this system is not running, no other reference exists + let system = unsafe { &mut *systems[system_index].get() }; + + #[cfg(feature = "trace")] + let task_span = info_span!("system_task", name = &*system.name()); + #[cfg(feature = "trace")] + let system_span = info_span!("system", name = &*system.name()); + + let sender = self.sender.clone(); + let task = async move { + #[cfg(feature = "trace")] + let system_guard = system_span.enter(); + // SAFETY: access is compatible + unsafe { system.run_unsafe((), world) }; + #[cfg(feature = "trace")] + drop(system_guard); + sender + .send(system_index) + .await + .unwrap_or_else(|error| unreachable!("{}", error)); + }; + + #[cfg(feature = "trace")] + let task = task.instrument(task_span); + + let system_meta = &self.system_task_metadata[system_index]; + self.active_access + .extend(&system_meta.archetype_component_access); + + if system_meta.is_send { + scope.spawn(task); + } else { + self.local_thread_running = true; + scope.spawn_on_scope(task); + } + } + + /// # Safety + /// Caller must ensure no systems are currently borrowed. + unsafe fn spawn_exclusive_system_task<'scope>( + &mut self, + scope: &Scope<'_, 'scope, ()>, + system_index: usize, + systems: &'scope [SyncUnsafeCell], + world: &'scope mut World, + ) { + // SAFETY: this system is not running, no other reference exists + let system = unsafe { &mut *systems[system_index].get() }; + + #[cfg(feature = "trace")] + let task_span = info_span!("system_task", name = &*system.name()); + #[cfg(feature = "trace")] + let system_span = info_span!("system", name = &*system.name()); + + let sender = self.sender.clone(); + if is_apply_system_buffers(system) { + // TODO: avoid allocation + let mut unapplied_systems = self.unapplied_systems.clone(); + let task = async move { + #[cfg(feature = "trace")] + let system_guard = system_span.enter(); + apply_system_buffers(&mut unapplied_systems, systems, world); + #[cfg(feature = "trace")] + drop(system_guard); + sender + .send(system_index) + .await + .unwrap_or_else(|error| unreachable!("{}", error)); + }; + + #[cfg(feature = "trace")] + let task = task.instrument(task_span); + scope.spawn_on_scope(task); + } else { + let task = async move { + #[cfg(feature = "trace")] + let system_guard = system_span.enter(); + system.run((), world); + #[cfg(feature = "trace")] + drop(system_guard); + sender + .send(system_index) + .await + .unwrap_or_else(|error| unreachable!("{}", error)); + }; + + #[cfg(feature = "trace")] + let task = task.instrument(task_span); + scope.spawn_on_scope(task); + } + + self.exclusive_running = true; + self.local_thread_running = true; + } + + fn finish_system_and_signal_dependents(&mut self, system_index: usize) { + if self.system_task_metadata[system_index].is_exclusive { + self.exclusive_running = false; + } + + if !self.system_task_metadata[system_index].is_send { + self.local_thread_running = false; + } + + debug_assert!(self.num_running_systems >= 1); + self.num_running_systems -= 1; + self.num_completed_systems += 1; + self.running_systems.set(system_index, false); + self.completed_systems.insert(system_index); + self.unapplied_systems.insert(system_index); + self.signal_dependents(system_index); + } + + fn skip_system_and_signal_dependents(&mut self, system_index: usize) { + self.num_completed_systems += 1; + self.completed_systems.insert(system_index); + self.signal_dependents(system_index); + } + + fn signal_dependents(&mut self, system_index: usize) { + #[cfg(feature = "trace")] + let _span = info_span!("signal_dependents").entered(); + for &dep_idx in &self.system_task_metadata[system_index].dependents { + let remaining = &mut self.num_dependencies_remaining[dep_idx]; + debug_assert!(*remaining >= 1); + *remaining -= 1; + if *remaining == 0 && !self.completed_systems.contains(dep_idx) { + self.ready_systems.insert(dep_idx); + } + } + } + + fn rebuild_active_access(&mut self) { + self.active_access.clear(); + for index in self.running_systems.ones() { + let system_meta = &self.system_task_metadata[index]; + self.active_access + .extend(&system_meta.archetype_component_access); + } + } +} + +fn apply_system_buffers( + unapplied_systems: &mut FixedBitSet, + systems: &[SyncUnsafeCell], + world: &mut World, +) { + for system_index in unapplied_systems.ones() { + // SAFETY: none of these systems are running, no other references exist + let system = unsafe { &mut *systems[system_index].get() }; + #[cfg(feature = "trace")] + let _apply_buffers_span = info_span!("apply_buffers", name = &*system.name()).entered(); + system.apply_buffers(world); + } + + unapplied_systems.clear(); +} + +fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &World) -> bool { + // not short-circuiting is intentional + #[allow(clippy::unnecessary_fold)] + conditions + .iter_mut() + .map(|condition| { + #[cfg(feature = "trace")] + let _condition_span = info_span!("condition", name = &*condition.name()).entered(); + // SAFETY: caller ensures system access is compatible + unsafe { condition.run_unsafe((), world) } + }) + .fold(true, |acc, res| acc && res) +} diff --git a/crates/bevy_ecs/src/schedule_v3/executor/simple.rs b/crates/bevy_ecs/src/schedule_v3/executor/simple.rs new file mode 100644 index 0000000000000..1d45aa29129b3 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/executor/simple.rs @@ -0,0 +1,111 @@ +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use fixedbitset::FixedBitSet; + +use crate::{ + schedule_v3::{BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + world::World, +}; + +/// A variant of [`SingleThreadedExecutor`](crate::schedule_v3::SingleThreadedExecutor) that calls +/// [`apply_buffers`](crate::system::System::apply_buffers) immediately after running each system. +#[derive(Default)] +pub struct SimpleExecutor { + /// Systems sets whose conditions have been evaluated. + evaluated_sets: FixedBitSet, + /// Systems that have run or been skipped. + completed_systems: FixedBitSet, +} + +impl SystemExecutor for SimpleExecutor { + fn kind(&self) -> ExecutorKind { + ExecutorKind::Simple + } + + fn init(&mut self, schedule: &SystemSchedule) { + let sys_count = schedule.system_ids.len(); + let set_count = schedule.set_ids.len(); + self.evaluated_sets = FixedBitSet::with_capacity(set_count); + self.completed_systems = FixedBitSet::with_capacity(sys_count); + } + + fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + for system_index in 0..schedule.systems.len() { + #[cfg(feature = "trace")] + let name = schedule.systems[system_index].name(); + #[cfg(feature = "trace")] + let should_run_span = info_span!("check_conditions", name = &*name).entered(); + + let mut should_run = !self.completed_systems.contains(system_index); + for set_idx in schedule.sets_of_systems[system_index].ones() { + if self.evaluated_sets.contains(set_idx) { + continue; + } + + // evaluate system set's conditions + let set_conditions_met = + evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + + if !set_conditions_met { + self.completed_systems + .union_with(&schedule.systems_in_sets[set_idx]); + } + + should_run &= set_conditions_met; + self.evaluated_sets.insert(set_idx); + } + + // evaluate system's conditions + let system_conditions_met = + evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + + should_run &= system_conditions_met; + + #[cfg(feature = "trace")] + should_run_span.exit(); + + // system has either been skipped or will run + self.completed_systems.insert(system_index); + + if !should_run { + continue; + } + + let system = &mut schedule.systems[system_index]; + #[cfg(feature = "trace")] + let system_span = info_span!("system", name = &*name).entered(); + system.run((), world); + #[cfg(feature = "trace")] + system_span.exit(); + + #[cfg(feature = "trace")] + let _apply_buffers_span = info_span!("apply_buffers", name = &*name).entered(); + system.apply_buffers(world); + } + + self.evaluated_sets.clear(); + self.completed_systems.clear(); + } +} + +impl SimpleExecutor { + pub const fn new() -> Self { + Self { + evaluated_sets: FixedBitSet::new(), + completed_systems: FixedBitSet::new(), + } + } +} + +fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { + // not short-circuiting is intentional + #[allow(clippy::unnecessary_fold)] + conditions + .iter_mut() + .map(|condition| { + #[cfg(feature = "trace")] + let _condition_span = info_span!("condition", name = &*condition.name()).entered(); + condition.run((), world) + }) + .fold(true, |acc, res| acc && res) +} diff --git a/crates/bevy_ecs/src/schedule_v3/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule_v3/executor/single_threaded.rs new file mode 100644 index 0000000000000..289b05b8c1e78 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/executor/single_threaded.rs @@ -0,0 +1,137 @@ +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use fixedbitset::FixedBitSet; + +use crate::{ + schedule_v3::{ + is_apply_system_buffers, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + }, + world::World, +}; + +/// Runs the schedule using a single thread. +/// +/// Useful if you're dealing with a single-threaded environment, saving your threads for +/// other things, or just trying minimize overhead. +#[derive(Default)] +pub struct SingleThreadedExecutor { + /// System sets whose conditions have been evaluated. + evaluated_sets: FixedBitSet, + /// Systems that have run or been skipped. + completed_systems: FixedBitSet, + /// Systems that have run but have not had their buffers applied. + unapplied_systems: FixedBitSet, +} + +impl SystemExecutor for SingleThreadedExecutor { + fn kind(&self) -> ExecutorKind { + ExecutorKind::SingleThreaded + } + + fn init(&mut self, schedule: &SystemSchedule) { + // pre-allocate space + let sys_count = schedule.system_ids.len(); + let set_count = schedule.set_ids.len(); + self.evaluated_sets = FixedBitSet::with_capacity(set_count); + self.completed_systems = FixedBitSet::with_capacity(sys_count); + self.unapplied_systems = FixedBitSet::with_capacity(sys_count); + } + + fn run(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + for system_index in 0..schedule.systems.len() { + #[cfg(feature = "trace")] + let name = schedule.systems[system_index].name(); + #[cfg(feature = "trace")] + let should_run_span = info_span!("check_conditions", name = &*name).entered(); + + let mut should_run = !self.completed_systems.contains(system_index); + for set_idx in schedule.sets_of_systems[system_index].ones() { + if self.evaluated_sets.contains(set_idx) { + continue; + } + + // evaluate system set's conditions + let set_conditions_met = + evaluate_and_fold_conditions(&mut schedule.set_conditions[set_idx], world); + + if !set_conditions_met { + self.completed_systems + .union_with(&schedule.systems_in_sets[set_idx]); + } + + should_run &= set_conditions_met; + self.evaluated_sets.insert(set_idx); + } + + // evaluate system's conditions + let system_conditions_met = + evaluate_and_fold_conditions(&mut schedule.system_conditions[system_index], world); + + should_run &= system_conditions_met; + + #[cfg(feature = "trace")] + should_run_span.exit(); + + // system has either been skipped or will run + self.completed_systems.insert(system_index); + + if !should_run { + continue; + } + + let system = &mut schedule.systems[system_index]; + if is_apply_system_buffers(system) { + #[cfg(feature = "trace")] + let system_span = info_span!("system", name = &*name).entered(); + self.apply_system_buffers(schedule, world); + #[cfg(feature = "trace")] + system_span.exit(); + } else { + #[cfg(feature = "trace")] + let system_span = info_span!("system", name = &*name).entered(); + system.run((), world); + #[cfg(feature = "trace")] + system_span.exit(); + self.unapplied_systems.insert(system_index); + } + } + + self.apply_system_buffers(schedule, world); + self.evaluated_sets.clear(); + self.completed_systems.clear(); + } +} + +impl SingleThreadedExecutor { + pub const fn new() -> Self { + Self { + evaluated_sets: FixedBitSet::new(), + completed_systems: FixedBitSet::new(), + unapplied_systems: FixedBitSet::new(), + } + } + + fn apply_system_buffers(&mut self, schedule: &mut SystemSchedule, world: &mut World) { + for system_index in self.unapplied_systems.ones() { + let system = &mut schedule.systems[system_index]; + #[cfg(feature = "trace")] + let _apply_buffers_span = info_span!("apply_buffers", name = &*system.name()).entered(); + system.apply_buffers(world); + } + + self.unapplied_systems.clear(); + } +} + +fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut World) -> bool { + // not short-circuiting is intentional + #[allow(clippy::unnecessary_fold)] + conditions + .iter_mut() + .map(|condition| { + #[cfg(feature = "trace")] + let _condition_span = info_span!("condition", name = &*condition.name()).entered(); + condition.run((), world) + }) + .fold(true, |acc, res| acc && res) +} diff --git a/crates/bevy_ecs/src/schedule_v3/graph_utils.rs b/crates/bevy_ecs/src/schedule_v3/graph_utils.rs new file mode 100644 index 0000000000000..b58bad317a959 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/graph_utils.rs @@ -0,0 +1,233 @@ +use std::fmt::Debug; + +use bevy_utils::{ + petgraph::{graphmap::NodeTrait, prelude::*}, + HashMap, HashSet, +}; +use fixedbitset::FixedBitSet; + +use crate::schedule_v3::set::*; + +/// Unique identifier for a system or system set. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum NodeId { + System(usize), + Set(usize), +} + +impl NodeId { + /// Returns the internal integer value. + pub fn index(&self) -> usize { + match self { + NodeId::System(index) | NodeId::Set(index) => *index, + } + } + + /// Returns `true` if the identified node is a system. + pub const fn is_system(&self) -> bool { + matches!(self, NodeId::System(_)) + } + + /// Returns `true` if the identified node is a system set. + pub const fn is_set(&self) -> bool { + matches!(self, NodeId::Set(_)) + } +} + +/// Specifies what kind of edge should be added to the dependency graph. +#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub(crate) enum DependencyKind { + /// A node that should be preceded. + Before, + /// A node that should be succeeded. + After, +} + +/// An edge to be added to the dependency graph. +#[derive(Clone)] +pub(crate) struct Dependency { + pub(crate) kind: DependencyKind, + pub(crate) set: BoxedSystemSet, +} + +impl Dependency { + pub fn new(kind: DependencyKind, set: BoxedSystemSet) -> Self { + Self { kind, set } + } +} + +/// Configures ambiguity detection for a single system. +#[derive(Clone, Debug, Default)] +pub(crate) enum Ambiguity { + #[default] + Check, + /// Ignore warnings with systems in any of these system sets. May contain duplicates. + IgnoreWithSet(Vec), + /// Ignore all warnings. + IgnoreAll, +} + +#[derive(Clone)] +pub(crate) struct GraphInfo { + pub(crate) sets: Vec, + pub(crate) dependencies: Vec, + pub(crate) ambiguous_with: Ambiguity, +} + +/// Converts 2D row-major pair of indices into a 1D array index. +pub(crate) fn index(row: usize, col: usize, num_cols: usize) -> usize { + debug_assert!(col < num_cols); + (row * num_cols) + col +} + +/// Converts a 1D array index into a 2D row-major pair of indices. +pub(crate) fn row_col(index: usize, num_cols: usize) -> (usize, usize) { + (index / num_cols, index % num_cols) +} + +/// Stores the results of the graph analysis. +pub(crate) struct CheckGraphResults { + /// Boolean reachability matrix for the graph. + pub(crate) reachable: FixedBitSet, + /// Pairs of nodes that have a path connecting them. + pub(crate) connected: HashSet<(V, V)>, + /// Pairs of nodes that don't have a path connecting them. + pub(crate) disconnected: HashSet<(V, V)>, + /// Edges that are redundant because a longer path exists. + pub(crate) transitive_edges: Vec<(V, V)>, + /// Variant of the graph with no transitive edges. + pub(crate) transitive_reduction: DiGraphMap, + /// Variant of the graph with all possible transitive edges. + // TODO: this will very likely be used by "if-needed" ordering + #[allow(dead_code)] + pub(crate) transitive_closure: DiGraphMap, +} + +impl Default for CheckGraphResults { + fn default() -> Self { + Self { + reachable: FixedBitSet::new(), + connected: HashSet::new(), + disconnected: HashSet::new(), + transitive_edges: Vec::new(), + transitive_reduction: DiGraphMap::new(), + transitive_closure: DiGraphMap::new(), + } + } +} + +/// Processes a DAG and computes its: +/// - transitive reduction (along with the set of removed edges) +/// - transitive closure +/// - reachability matrix (as a bitset) +/// - pairs of nodes connected by a path +/// - pairs of nodes not connected by a path +/// +/// The algorithm implemented comes from +/// ["On the calculation of transitive reduction-closure of orders"][1] by Habib, Morvan and Rampon. +/// +/// [1]: https://doi.org/10.1016/0012-365X(93)90164-O +pub(crate) fn check_graph( + graph: &DiGraphMap, + topological_order: &[V], +) -> CheckGraphResults +where + V: NodeTrait + Debug, +{ + if graph.node_count() == 0 { + return CheckGraphResults::default(); + } + + let n = graph.node_count(); + + // build a copy of the graph where the nodes and edges appear in topsorted order + let mut map = HashMap::with_capacity(n); + let mut topsorted = DiGraphMap::::new(); + // iterate nodes in topological order + for (i, &node) in topological_order.iter().enumerate() { + map.insert(node, i); + topsorted.add_node(node); + // insert nodes as successors to their predecessors + for pred in graph.neighbors_directed(node, Direction::Incoming) { + topsorted.add_edge(pred, node, ()); + } + } + + let mut reachable = FixedBitSet::with_capacity(n * n); + let mut connected = HashSet::new(); + let mut disconnected = HashSet::new(); + + let mut transitive_edges = Vec::new(); + let mut transitive_reduction = DiGraphMap::::new(); + let mut transitive_closure = DiGraphMap::::new(); + + let mut visited = FixedBitSet::with_capacity(n); + + // iterate nodes in topological order + for node in topsorted.nodes() { + transitive_reduction.add_node(node); + transitive_closure.add_node(node); + } + + // iterate nodes in reverse topological order + for a in topsorted.nodes().rev() { + let index_a = *map.get(&a).unwrap(); + // iterate their successors in topological order + for b in topsorted.neighbors_directed(a, Direction::Outgoing) { + let index_b = *map.get(&b).unwrap(); + debug_assert!(index_a < index_b); + if !visited[index_b] { + // edge is not redundant + transitive_reduction.add_edge(a, b, ()); + transitive_closure.add_edge(a, b, ()); + reachable.insert(index(index_a, index_b, n)); + + let successors = transitive_closure + .neighbors_directed(b, Direction::Outgoing) + .collect::>(); + for c in successors { + let index_c = *map.get(&c).unwrap(); + debug_assert!(index_b < index_c); + if !visited[index_c] { + visited.insert(index_c); + transitive_closure.add_edge(a, c, ()); + reachable.insert(index(index_a, index_c, n)); + } + } + } else { + // edge is redundant + transitive_edges.push((a, b)); + } + } + + visited.clear(); + } + + // partition pairs of nodes into "connected by path" and "not connected by path" + for i in 0..(n - 1) { + // reachable is upper triangular because the nodes were topsorted + for index in index(i, i + 1, n)..=index(i, n - 1, n) { + let (a, b) = row_col(index, n); + let pair = (topological_order[a], topological_order[b]); + if reachable[index] { + connected.insert(pair); + } else { + disconnected.insert(pair); + } + } + } + + // fill diagonal (nodes reach themselves) + // for i in 0..n { + // reachable.set(index(i, i, n), true); + // } + + CheckGraphResults { + reachable, + connected, + disconnected, + transitive_edges, + transitive_reduction, + transitive_closure, + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/migration.rs b/crates/bevy_ecs/src/schedule_v3/migration.rs new file mode 100644 index 0000000000000..c4932c2fbca7c --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/migration.rs @@ -0,0 +1,38 @@ +use crate::schedule_v3::*; +use crate::world::World; + +/// Temporary "stageless" `App` methods. +pub trait AppExt { + /// Sets the [`Schedule`] that will be modified by default when you call `App::add_system` + /// and similar methods. + /// + /// **Note:** This will create the schedule if it does not already exist. + fn set_default_schedule(&mut self, label: impl ScheduleLabel) -> &mut Self; + /// Applies the function to the [`Schedule`] associated with `label`. + /// + /// **Note:** This will create the schedule if it does not already exist. + fn edit_schedule( + &mut self, + label: impl ScheduleLabel, + f: impl FnMut(&mut Schedule), + ) -> &mut Self; + /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules + /// for each state variant, and an instance of [`apply_state_transition::`] in + /// \ so that transitions happen before `Update`. + fn add_state(&mut self) -> &mut Self; +} + +/// Temporary "stageless" [`World`] methods. +pub trait WorldExt { + /// Runs the [`Schedule`] associated with `label`. + fn run_schedule(&mut self, label: impl ScheduleLabel); +} + +impl WorldExt for World { + fn run_schedule(&mut self, label: impl ScheduleLabel) { + if let Some(mut schedule) = self.resource_mut::().remove(&label) { + schedule.run(self); + self.resource_mut::().insert(label, schedule); + } + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/mod.rs b/crates/bevy_ecs/src/schedule_v3/mod.rs new file mode 100644 index 0000000000000..11828cbbe5fc0 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/mod.rs @@ -0,0 +1,622 @@ +mod condition; +mod config; +mod executor; +mod graph_utils; +mod migration; +mod schedule; +mod set; +mod state; + +pub use self::condition::*; +pub use self::config::*; +pub use self::executor::*; +use self::graph_utils::*; +pub use self::migration::*; +pub use self::schedule::*; +pub use self::set::*; +pub use self::state::*; + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU32, Ordering}; + + pub use crate as bevy_ecs; + pub use crate::schedule_v3::{IntoSystemConfig, IntoSystemSetConfig, Schedule, SystemSet}; + pub use crate::system::{Res, ResMut}; + pub use crate::{prelude::World, system::Resource}; + + #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] + enum TestSet { + A, + B, + C, + D, + X, + } + + #[derive(Resource, Default)] + struct SystemOrder(Vec); + + #[derive(Resource, Default)] + struct RunConditionBool(pub bool); + + #[derive(Resource, Default)] + struct Counter(pub AtomicU32); + + fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { + move |world| world.resource_mut::().0.push(tag) + } + + fn make_function_system(tag: u32) -> impl FnMut(ResMut) { + move |mut resource: ResMut| resource.0.push(tag) + } + + fn named_system(mut resource: ResMut) { + resource.0.push(u32::MAX); + } + + fn named_exclusive_system(world: &mut World) { + world.resource_mut::().0.push(u32::MAX); + } + + fn counting_system(counter: Res) { + counter.0.fetch_add(1, Ordering::Relaxed); + } + + mod system_execution { + use super::*; + + #[test] + fn run_system() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_system(make_function_system(0)); + schedule.run(&mut world); + + assert_eq!(world.resource::().0, vec![0]); + } + + #[test] + fn run_exclusive_system() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_system(make_exclusive_system(0)); + schedule.run(&mut world); + + assert_eq!(world.resource::().0, vec![0]); + } + + #[test] + #[cfg(not(miri))] + fn parallel_execution() { + use bevy_tasks::{ComputeTaskPool, TaskPool}; + use std::sync::{Arc, Barrier}; + + let mut world = World::default(); + let mut schedule = Schedule::default(); + let thread_count = ComputeTaskPool::init(TaskPool::default).thread_num(); + + let barrier = Arc::new(Barrier::new(thread_count)); + + for _ in 0..thread_count { + let inner = barrier.clone(); + schedule.add_system(move || { + inner.wait(); + }); + } + + schedule.run(&mut world); + } + } + + mod system_ordering { + use super::*; + + #[test] + fn order_systems() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_system(named_system); + schedule.add_system(make_function_system(1).before(named_system)); + schedule.add_system( + make_function_system(0) + .after(named_system) + .in_set(TestSet::A), + ); + schedule.run(&mut world); + + assert_eq!(world.resource::().0, vec![1, u32::MAX, 0]); + + world.insert_resource(SystemOrder::default()); + + assert_eq!(world.resource::().0, vec![]); + + // modify the schedule after it's been initialized and test ordering with sets + schedule.configure_set(TestSet::A.after(named_system)); + schedule.add_system( + make_function_system(3) + .before(TestSet::A) + .after(named_system), + ); + schedule.add_system(make_function_system(4).after(TestSet::A)); + schedule.run(&mut world); + + assert_eq!( + world.resource::().0, + vec![1, u32::MAX, 3, 0, 4] + ); + } + + #[test] + fn order_exclusive_systems() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_system(named_exclusive_system); + schedule.add_system(make_exclusive_system(1).before(named_exclusive_system)); + schedule.add_system(make_exclusive_system(0).after(named_exclusive_system)); + schedule.run(&mut world); + + assert_eq!(world.resource::().0, vec![1, u32::MAX, 0]); + } + + #[test] + fn add_systems_correct_order() { + #[derive(Resource)] + struct X(Vec); + + let mut world = World::new(); + world.init_resource::(); + + let mut schedule = Schedule::new(); + schedule.add_systems( + ( + make_function_system(0), + make_function_system(1), + make_exclusive_system(2), + make_function_system(3), + ) + .chain(), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![0, 1, 2, 3]); + } + } + + mod conditions { + use crate::change_detection::DetectChanges; + + use super::*; + + #[test] + fn system_with_condition() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + world.init_resource::(); + + schedule.add_system( + make_function_system(0).run_if(|condition: Res| condition.0), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + world.resource_mut::().0 = true; + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![0]); + } + + #[test] + fn run_exclusive_system_with_condition() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + world.init_resource::(); + + schedule.add_system( + make_exclusive_system(0).run_if(|condition: Res| condition.0), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + world.resource_mut::().0 = true; + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![0]); + } + + #[test] + fn multiple_conditions_on_system() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_system(counting_system.run_if(|| false).run_if(|| false)); + schedule.add_system(counting_system.run_if(|| true).run_if(|| false)); + schedule.add_system(counting_system.run_if(|| false).run_if(|| true)); + schedule.add_system(counting_system.run_if(|| true).run_if(|| true)); + + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + } + + #[test] + fn multiple_conditions_on_system_sets() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.configure_set(TestSet::A.run_if(|| false).run_if(|| false)); + schedule.add_system(counting_system.in_set(TestSet::A)); + schedule.configure_set(TestSet::B.run_if(|| true).run_if(|| false)); + schedule.add_system(counting_system.in_set(TestSet::B)); + schedule.configure_set(TestSet::C.run_if(|| false).run_if(|| true)); + schedule.add_system(counting_system.in_set(TestSet::C)); + schedule.configure_set(TestSet::D.run_if(|| true).run_if(|| true)); + schedule.add_system(counting_system.in_set(TestSet::D)); + + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + } + + #[test] + fn systems_nested_in_system_sets() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.configure_set(TestSet::A.run_if(|| false)); + schedule.add_system(counting_system.in_set(TestSet::A).run_if(|| false)); + schedule.configure_set(TestSet::B.run_if(|| true)); + schedule.add_system(counting_system.in_set(TestSet::B).run_if(|| false)); + schedule.configure_set(TestSet::C.run_if(|| false)); + schedule.add_system(counting_system.in_set(TestSet::C).run_if(|| true)); + schedule.configure_set(TestSet::D.run_if(|| true)); + schedule.add_system(counting_system.in_set(TestSet::D).run_if(|| true)); + + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + } + + #[test] + fn system_conditions_and_change_detection() { + #[derive(Resource, Default)] + struct Bool2(pub bool); + + let mut world = World::default(); + world.init_resource::(); + world.init_resource::(); + world.init_resource::(); + let mut schedule = Schedule::default(); + + schedule.add_system( + counting_system + .run_if(|res1: Res| res1.is_changed()) + .run_if(|res2: Res| res2.is_changed()), + ); + + // both resource were just added. + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // nothing has changed + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // RunConditionBool has changed, but counting_system did not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // internal state for the bool2 run criteria was updated in the + // previous run, so system still does not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // internal state for bool2 was updated, so system still does not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // now check that it works correctly changing Bool2 first and then RunConditionBool + world.get_resource_mut::().unwrap().0 = false; + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 2); + } + + #[test] + fn system_set_conditions_and_change_detection() { + #[derive(Resource, Default)] + struct Bool2(pub bool); + + let mut world = World::default(); + world.init_resource::(); + world.init_resource::(); + world.init_resource::(); + let mut schedule = Schedule::default(); + + schedule.configure_set( + TestSet::A + .run_if(|res1: Res| res1.is_changed()) + .run_if(|res2: Res| res2.is_changed()), + ); + + schedule.add_system(counting_system.in_set(TestSet::A)); + + // both resource were just added. + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // nothing has changed + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // RunConditionBool has changed, but counting_system did not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // internal state for the bool2 run criteria was updated in the + // previous run, so system still does not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // internal state for bool2 was updated, so system still does not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // the system only runs when both are changed on the same run + world.get_resource_mut::().unwrap().0 = false; + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 2); + } + + #[test] + fn mixed_conditions_and_change_detection() { + #[derive(Resource, Default)] + struct Bool2(pub bool); + + let mut world = World::default(); + world.init_resource::(); + world.init_resource::(); + world.init_resource::(); + let mut schedule = Schedule::default(); + + schedule + .configure_set(TestSet::A.run_if(|res1: Res| res1.is_changed())); + + schedule.add_system( + counting_system + .run_if(|res2: Res| res2.is_changed()) + .in_set(TestSet::A), + ); + + // both resource were just added. + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // nothing has changed + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // RunConditionBool has changed, but counting_system did not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // we now only change bool2 and the system also should not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // internal state for the bool2 run criteria was updated in the + // previous run, so system still does not run + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); + + // the system only runs when both are changed on the same run + world.get_resource_mut::().unwrap().0 = false; + world.get_resource_mut::().unwrap().0 = false; + schedule.run(&mut world); + assert_eq!(world.resource::().0.load(Ordering::Relaxed), 2); + } + } + + mod schedule_build_errors { + use super::*; + + #[test] + #[should_panic] + fn dependency_loop() { + let mut schedule = Schedule::new(); + schedule.configure_set(TestSet::X.after(TestSet::X)); + } + + #[test] + fn dependency_cycle() { + let mut world = World::new(); + let mut schedule = Schedule::new(); + + schedule.configure_set(TestSet::A.after(TestSet::B)); + schedule.configure_set(TestSet::B.after(TestSet::A)); + + let result = schedule.initialize(&mut world); + assert!(matches!(result, Err(ScheduleBuildError::DependencyCycle))); + + fn foo() {} + fn bar() {} + + let mut world = World::new(); + let mut schedule = Schedule::new(); + + schedule.add_systems((foo.after(bar), bar.after(foo))); + let result = schedule.initialize(&mut world); + assert!(matches!(result, Err(ScheduleBuildError::DependencyCycle))); + } + + #[test] + #[should_panic] + fn hierarchy_loop() { + let mut schedule = Schedule::new(); + schedule.configure_set(TestSet::X.in_set(TestSet::X)); + } + + #[test] + fn hierarchy_cycle() { + let mut world = World::new(); + let mut schedule = Schedule::new(); + + schedule.configure_set(TestSet::A.in_set(TestSet::B)); + schedule.configure_set(TestSet::B.in_set(TestSet::A)); + + let result = schedule.initialize(&mut world); + assert!(matches!(result, Err(ScheduleBuildError::HierarchyCycle))); + } + + #[test] + fn system_type_set_ambiguity() { + // Define some systems. + fn foo() {} + fn bar() {} + + let mut world = World::new(); + let mut schedule = Schedule::new(); + + // Schedule `bar` to run after `foo`. + schedule.add_system(foo); + schedule.add_system(bar.after(foo)); + + // There's only one `foo`, so it's fine. + let result = schedule.initialize(&mut world); + assert!(result.is_ok()); + + // Schedule another `foo`. + schedule.add_system(foo); + + // When there are multiple instances of `foo`, dependencies on + // `foo` are no longer allowed. Too much ambiguity. + let result = schedule.initialize(&mut world); + assert!(matches!( + result, + Err(ScheduleBuildError::SystemTypeSetAmbiguity(_)) + )); + + // same goes for `ambiguous_with` + let mut schedule = Schedule::new(); + schedule.add_system(foo); + schedule.add_system(bar.ambiguous_with(foo)); + let result = schedule.initialize(&mut world); + assert!(result.is_ok()); + schedule.add_system(foo); + let result = schedule.initialize(&mut world); + assert!(matches!( + result, + Err(ScheduleBuildError::SystemTypeSetAmbiguity(_)) + )); + } + + #[test] + #[should_panic] + fn in_system_type_set() { + fn foo() {} + fn bar() {} + + let mut schedule = Schedule::new(); + schedule.add_system(foo.in_set(bar.into_system_set())); + } + + #[test] + #[should_panic] + fn configure_system_type_set() { + fn foo() {} + let mut schedule = Schedule::new(); + schedule.configure_set(foo.into_system_set()); + } + + #[test] + fn hierarchy_redundancy() { + let mut world = World::new(); + let mut schedule = Schedule::new(); + + schedule.set_build_settings( + ScheduleBuildSettings::new().with_hierarchy_detection(LogLevel::Error), + ); + + // Add `A`. + schedule.configure_set(TestSet::A); + + // Add `B` as child of `A`. + schedule.configure_set(TestSet::B.in_set(TestSet::A)); + + // Add `X` as child of both `A` and `B`. + schedule.configure_set(TestSet::X.in_set(TestSet::A).in_set(TestSet::B)); + + // `X` cannot be the `A`'s child and grandchild at the same time. + let result = schedule.initialize(&mut world); + assert!(matches!( + result, + Err(ScheduleBuildError::HierarchyRedundancy) + )); + } + + #[test] + fn cross_dependency() { + let mut world = World::new(); + let mut schedule = Schedule::new(); + + // Add `B` and give it both kinds of relationships with `A`. + schedule.configure_set(TestSet::B.in_set(TestSet::A)); + schedule.configure_set(TestSet::B.after(TestSet::A)); + let result = schedule.initialize(&mut world); + assert!(matches!( + result, + Err(ScheduleBuildError::CrossDependency(_, _)) + )); + } + + #[test] + fn ambiguity() { + #[derive(Resource)] + struct X; + + fn res_ref(_x: Res) {} + fn res_mut(_x: ResMut) {} + + let mut world = World::new(); + let mut schedule = Schedule::new(); + + schedule.set_build_settings( + ScheduleBuildSettings::new().with_ambiguity_detection(LogLevel::Error), + ); + + schedule.add_systems((res_ref, res_mut)); + let result = schedule.initialize(&mut world); + assert!(matches!(result, Err(ScheduleBuildError::Ambiguity))); + } + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/schedule.rs b/crates/bevy_ecs/src/schedule_v3/schedule.rs new file mode 100644 index 0000000000000..8aeb01d995336 --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/schedule.rs @@ -0,0 +1,1099 @@ +use std::{ + fmt::{Debug, Write}, + result::Result, +}; + +use bevy_utils::default; +#[cfg(feature = "trace")] +use bevy_utils::tracing::info_span; +use bevy_utils::{ + petgraph::{algo::tarjan_scc, prelude::*}, + thiserror::Error, + tracing::{error, info, warn}, + HashMap, HashSet, +}; + +use fixedbitset::FixedBitSet; + +use crate::{ + self as bevy_ecs, + component::ComponentId, + schedule_v3::*, + system::{BoxedSystem, Resource}, + world::World, +}; + +/// Resource that stores [`Schedule`]s mapped to [`ScheduleLabel`]s. +#[derive(Default, Resource)] +pub struct Schedules { + inner: HashMap, +} + +impl Schedules { + /// Constructs an empty `Schedules` with zero initial capacity. + pub fn new() -> Self { + Self { + inner: HashMap::new(), + } + } + + /// Inserts a labeled schedule into the map. + /// + /// If the map already had an entry for `label`, `schedule` is inserted, + /// and the old schedule is returned. Otherwise, `None` is returned. + pub fn insert(&mut self, label: impl ScheduleLabel, schedule: Schedule) -> Option { + let label: Box = Box::new(label); + if self.inner.contains_key(&label) { + warn!("schedule with label {:?} already exists", label); + } + self.inner.insert(label, schedule) + } + + /// Removes the schedule corresponding to the `label` from the map, returning it if it existed. + pub fn remove(&mut self, label: &dyn ScheduleLabel) -> Option { + if !self.inner.contains_key(label) { + warn!("schedule with label {:?} not found", label); + } + self.inner.remove(label) + } + + /// Returns a reference to the schedule associated with `label`, if it exists. + pub fn get(&self, label: &dyn ScheduleLabel) -> Option<&Schedule> { + self.inner.get(label) + } + + /// Returns a mutable reference to the schedule associated with `label`, if it exists. + pub fn get_mut(&mut self, label: &dyn ScheduleLabel) -> Option<&mut Schedule> { + self.inner.get_mut(label) + } + + /// Iterates the change ticks of all systems in all stored schedules and clamps any older than + /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). + /// This prevents overflow and thus prevents false positives. + pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + #[cfg(feature = "trace")] + let _all_span = info_span!("check stored schedule ticks").entered(); + // label used when trace feature is enabled + #[allow(unused_variables)] + for (label, schedule) in self.inner.iter_mut() { + #[cfg(feature = "trace")] + let name = format!("{label:?}"); + #[cfg(feature = "trace")] + let _one_span = info_span!("check schedule ticks", name = &name).entered(); + schedule.check_change_ticks(change_tick); + } + } +} + +/// A collection of systems, and the metadata and executor needed to run them +/// in a certain order under certain conditions. +pub struct Schedule { + graph: ScheduleGraph, + executable: SystemSchedule, + executor: Box, + executor_initialized: bool, +} + +impl Default for Schedule { + fn default() -> Self { + Self::new() + } +} + +impl Schedule { + /// Constructs an empty `Schedule`. + pub fn new() -> Self { + Self { + graph: ScheduleGraph::new(), + executable: SystemSchedule::new(), + executor: Box::new(MultiThreadedExecutor::new()), + executor_initialized: false, + } + } + + /// Add a system to the schedule. + pub fn add_system

(&mut self, system: impl IntoSystemConfig

) -> &mut Self { + self.graph.add_system(system); + self + } + + /// Add a collection of systems to the schedule. + pub fn add_systems

(&mut self, systems: impl IntoSystemConfigs

) -> &mut Self { + self.graph.add_systems(systems); + self + } + + /// Configure a system set in this schedule. + pub fn configure_set(&mut self, set: impl IntoSystemSetConfig) -> &mut Self { + self.graph.configure_set(set); + self + } + + /// Configure a collection of system sets in this schedule. + pub fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) -> &mut Self { + self.graph.configure_sets(sets); + self + } + + /// Changes the system set that new systems and system sets will join by default + /// if they aren't already part of one. + pub fn set_default_set(&mut self, set: impl SystemSet) -> &mut Self { + self.graph.set_default_set(set); + self + } + + /// Changes miscellaneous build settings. + pub fn set_build_settings(&mut self, settings: ScheduleBuildSettings) -> &mut Self { + self.graph.settings = settings; + self + } + + /// Returns the schedule's current execution strategy. + pub fn get_executor_kind(&self) -> ExecutorKind { + self.executor.kind() + } + + /// Sets the schedule's execution strategy. + pub fn set_executor_kind(&mut self, executor: ExecutorKind) -> &mut Self { + if executor != self.executor.kind() { + self.executor = match executor { + ExecutorKind::Simple => Box::new(SimpleExecutor::new()), + ExecutorKind::SingleThreaded => Box::new(SingleThreadedExecutor::new()), + ExecutorKind::MultiThreaded => Box::new(MultiThreadedExecutor::new()), + }; + self.executor_initialized = false; + } + self + } + + /// Runs all systems in this schedule on the `world`, using its current execution strategy. + pub fn run(&mut self, world: &mut World) { + world.check_change_ticks(); + self.initialize(world).unwrap(); + // TODO: label + #[cfg(feature = "trace")] + let _span = info_span!("schedule").entered(); + self.executor.run(&mut self.executable, world); + } + + /// Initializes any newly-added systems and conditions, rebuilds the executable schedule, + /// and re-initializes the executor. + pub fn initialize(&mut self, world: &mut World) -> Result<(), ScheduleBuildError> { + if self.graph.changed { + self.graph.initialize(world); + self.graph.update_schedule(&mut self.executable)?; + self.graph.changed = false; + self.executor_initialized = false; + } + + if !self.executor_initialized { + self.executor.init(&self.executable); + self.executor_initialized = true; + } + + Ok(()) + } + + /// Iterates the change ticks of all systems in the schedule and clamps any older than + /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). + /// This prevents overflow and thus prevents false positives. + pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + for system in &mut self.executable.systems { + system.check_change_tick(change_tick); + } + + for conditions in &mut self.executable.system_conditions { + for system in conditions.iter_mut() { + system.check_change_tick(change_tick); + } + } + + for conditions in &mut self.executable.set_conditions { + for system in conditions.iter_mut() { + system.check_change_tick(change_tick); + } + } + } +} + +/// A directed acylic graph structure. +#[derive(Default)] +struct Dag { + /// A directed graph. + graph: DiGraphMap, + /// A cached topological ordering of the graph. + topsort: Vec, +} + +impl Dag { + fn new() -> Self { + Self { + graph: DiGraphMap::new(), + topsort: Vec::new(), + } + } +} + +/// A [`SystemSet`] with metadata, stored in a [`ScheduleGraph`]. +struct SystemSetNode { + inner: BoxedSystemSet, + /// `true` if this system set was modified with `configure_set` + configured: bool, +} + +impl SystemSetNode { + pub fn new(set: BoxedSystemSet) -> Self { + Self { + inner: set, + configured: false, + } + } + + pub fn name(&self) -> String { + format!("{:?}", &self.inner) + } + + pub fn is_system_type(&self) -> bool { + self.inner.is_system_type() + } +} + +/// Metadata for a [`Schedule`]. +#[derive(Default)] +struct ScheduleGraph { + systems: Vec>, + system_conditions: Vec>>, + system_sets: Vec, + system_set_conditions: Vec>>, + system_set_ids: HashMap, + uninit: Vec<(NodeId, usize)>, + hierarchy: Dag, + dependency: Dag, + dependency_flattened: Dag, + ambiguous_with: UnGraphMap, + ambiguous_with_flattened: UnGraphMap, + ambiguous_with_all: HashSet, + default_set: Option, + changed: bool, + settings: ScheduleBuildSettings, +} + +impl ScheduleGraph { + pub fn new() -> Self { + Self { + systems: Vec::new(), + system_conditions: Vec::new(), + system_sets: Vec::new(), + system_set_conditions: Vec::new(), + system_set_ids: HashMap::new(), + uninit: Vec::new(), + hierarchy: Dag::new(), + dependency: Dag::new(), + dependency_flattened: Dag::new(), + ambiguous_with: UnGraphMap::new(), + ambiguous_with_flattened: UnGraphMap::new(), + ambiguous_with_all: HashSet::new(), + default_set: None, + changed: false, + settings: default(), + } + } + + fn set_default_set(&mut self, set: impl SystemSet) { + assert!( + !set.is_system_type(), + "adding arbitrary systems to a system type set is not allowed" + ); + self.default_set = Some(Box::new(set)); + } + + fn add_systems

(&mut self, systems: impl IntoSystemConfigs

) { + let SystemConfigs { systems, chained } = systems.into_configs(); + let mut system_iter = systems.into_iter(); + if chained { + let Some(prev) = system_iter.next() else { return }; + let mut prev_id = self.add_system_inner(prev).unwrap(); + for next in system_iter { + let next_id = self.add_system_inner(next).unwrap(); + self.dependency.graph.add_edge(prev_id, next_id, ()); + prev_id = next_id; + } + } else { + for system in system_iter { + self.add_system_inner(system).unwrap(); + } + } + } + + fn add_system

(&mut self, system: impl IntoSystemConfig

) { + self.add_system_inner(system).unwrap(); + } + + fn add_system_inner

( + &mut self, + system: impl IntoSystemConfig

, + ) -> Result { + let SystemConfig { + system, + mut graph_info, + conditions, + } = system.into_config(); + + let id = NodeId::System(self.systems.len()); + + if graph_info.sets.is_empty() { + if let Some(default) = self.default_set.as_ref() { + graph_info.sets.push(default.dyn_clone()); + } + } + + // graph updates are immediate + self.update_graphs(id, graph_info)?; + + // system init has to be deferred (need `&mut World`) + self.uninit.push((id, 0)); + self.systems.push(Some(system)); + self.system_conditions.push(Some(conditions)); + + Ok(id) + } + + fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) { + let SystemSetConfigs { sets, chained } = sets.into_configs(); + let mut set_iter = sets.into_iter(); + if chained { + let Some(prev) = set_iter.next() else { return }; + let mut prev_id = self.configure_set_inner(prev).unwrap(); + for next in set_iter { + let next_id = self.configure_set_inner(next).unwrap(); + self.dependency.graph.add_edge(prev_id, next_id, ()); + prev_id = next_id; + } + } else { + for set in set_iter { + self.configure_set_inner(set).unwrap(); + } + } + } + + fn configure_set(&mut self, set: impl IntoSystemSetConfig) { + self.configure_set_inner(set).unwrap(); + } + + fn configure_set_inner( + &mut self, + set: impl IntoSystemSetConfig, + ) -> Result { + let SystemSetConfig { + set, + mut graph_info, + mut conditions, + } = set.into_config(); + + let id = match self.system_set_ids.get(&set) { + Some(&id) => id, + None => self.add_set(set.dyn_clone()), + }; + + let meta = &mut self.system_sets[id.index()]; + let already_configured = std::mem::replace(&mut meta.configured, true); + + // a system set can be configured multiple times, so this "default check" + // should only happen the first time `configure_set` is called on it + if !already_configured && graph_info.sets.is_empty() { + if let Some(default) = self.default_set.as_ref() { + info!("adding system set `{:?}` to default: `{:?}`", set, default); + graph_info.sets.push(default.dyn_clone()); + } + } + + // graph updates are immediate + self.update_graphs(id, graph_info)?; + + // system init has to be deferred (need `&mut World`) + let system_set_conditions = + self.system_set_conditions[id.index()].get_or_insert_with(Vec::new); + self.uninit.push((id, system_set_conditions.len())); + system_set_conditions.append(&mut conditions); + + Ok(id) + } + + fn add_set(&mut self, set: BoxedSystemSet) -> NodeId { + let id = NodeId::Set(self.system_sets.len()); + self.system_sets.push(SystemSetNode::new(set.dyn_clone())); + self.system_set_conditions.push(None); + self.system_set_ids.insert(set, id); + id + } + + fn check_sets( + &mut self, + id: &NodeId, + graph_info: &GraphInfo, + ) -> Result<(), ScheduleBuildError> { + for set in &graph_info.sets { + match self.system_set_ids.get(set) { + Some(set_id) => { + if id == set_id { + return Err(ScheduleBuildError::HierarchyLoop(set.dyn_clone())); + } + } + None => { + self.add_set(set.dyn_clone()); + } + } + } + + Ok(()) + } + + fn check_edges( + &mut self, + id: &NodeId, + graph_info: &GraphInfo, + ) -> Result<(), ScheduleBuildError> { + for Dependency { kind: _, set } in &graph_info.dependencies { + match self.system_set_ids.get(set) { + Some(set_id) => { + if id == set_id { + return Err(ScheduleBuildError::DependencyLoop(set.dyn_clone())); + } + } + None => { + self.add_set(set.dyn_clone()); + } + } + } + + if let Ambiguity::IgnoreWithSet(ambiguous_with) = &graph_info.ambiguous_with { + for set in ambiguous_with { + if !self.system_set_ids.contains_key(set) { + self.add_set(set.dyn_clone()); + } + } + } + + Ok(()) + } + + fn update_graphs( + &mut self, + id: NodeId, + graph_info: GraphInfo, + ) -> Result<(), ScheduleBuildError> { + self.check_sets(&id, &graph_info)?; + self.check_edges(&id, &graph_info)?; + self.changed = true; + + let GraphInfo { + sets, + dependencies, + ambiguous_with, + } = graph_info; + + if !self.hierarchy.graph.contains_node(id) { + self.hierarchy.graph.add_node(id); + } + + for set in sets.into_iter().map(|set| self.system_set_ids[&set]) { + self.hierarchy.graph.add_edge(set, id, ()); + } + + if !self.dependency.graph.contains_node(id) { + self.dependency.graph.add_node(id); + } + + for (kind, set) in dependencies + .into_iter() + .map(|Dependency { kind, set }| (kind, self.system_set_ids[&set])) + { + let (lhs, rhs) = match kind { + DependencyKind::Before => (id, set), + DependencyKind::After => (set, id), + }; + self.dependency.graph.add_edge(lhs, rhs, ()); + } + + match ambiguous_with { + Ambiguity::Check => (), + Ambiguity::IgnoreWithSet(ambigous_with) => { + for set in ambigous_with + .into_iter() + .map(|set| self.system_set_ids[&set]) + { + self.ambiguous_with.add_edge(id, set, ()); + } + } + Ambiguity::IgnoreAll => { + self.ambiguous_with_all.insert(id); + } + } + + Ok(()) + } + + fn initialize(&mut self, world: &mut World) { + for (id, i) in self.uninit.drain(..) { + match id { + NodeId::System(index) => { + self.systems[index].as_mut().unwrap().initialize(world); + if let Some(v) = self.system_conditions[index].as_mut() { + for condition in v.iter_mut() { + condition.initialize(world); + } + } + } + NodeId::Set(index) => { + if let Some(v) = self.system_set_conditions[index].as_mut() { + for condition in v.iter_mut().skip(i) { + condition.initialize(world); + } + } + } + } + } + } + + fn build_schedule(&mut self) -> Result { + // check hierarchy for cycles + let hier_scc = tarjan_scc(&self.hierarchy.graph); + if self.contains_cycles(&hier_scc) { + self.report_cycles(&hier_scc); + return Err(ScheduleBuildError::HierarchyCycle); + } + + self.hierarchy.topsort = hier_scc.into_iter().flatten().rev().collect::>(); + + let hier_results = check_graph(&self.hierarchy.graph, &self.hierarchy.topsort); + if self.contains_hierarchy_conflicts(&hier_results.transitive_edges) { + self.report_hierarchy_conflicts(&hier_results.transitive_edges); + if matches!(self.settings.hierarchy_detection, LogLevel::Error) { + return Err(ScheduleBuildError::HierarchyRedundancy); + } + } + + // remove redundant edges + self.hierarchy.graph = hier_results.transitive_reduction; + + // check dependencies for cycles + let dep_scc = tarjan_scc(&self.dependency.graph); + if self.contains_cycles(&dep_scc) { + self.report_cycles(&dep_scc); + return Err(ScheduleBuildError::DependencyCycle); + } + + self.dependency.topsort = dep_scc.into_iter().flatten().rev().collect::>(); + + // nodes can have dependent XOR hierarchical relationship + let dep_results = check_graph(&self.dependency.graph, &self.dependency.topsort); + for &(a, b) in dep_results.connected.iter() { + if hier_results.connected.contains(&(a, b)) || hier_results.connected.contains(&(b, a)) + { + let name_a = self.get_node_name(&a); + let name_b = self.get_node_name(&b); + return Err(ScheduleBuildError::CrossDependency(name_a, name_b)); + } + } + + // map system sets to all their member systems + let mut systems_in_sets = HashMap::with_capacity(self.system_sets.len()); + // iterate in reverse topological order (bottom-up) + for &id in self.hierarchy.topsort.iter().rev() { + if id.is_system() { + continue; + } + + let set = id; + systems_in_sets.insert(set, Vec::new()); + + for child in self + .hierarchy + .graph + .neighbors_directed(set, Direction::Outgoing) + { + match child { + NodeId::System(_) => { + systems_in_sets.get_mut(&set).unwrap().push(child); + } + NodeId::Set(_) => { + let [sys, child_sys] = + systems_in_sets.get_many_mut([&set, &child]).unwrap(); + sys.extend_from_slice(child_sys); + } + } + } + } + + // can't depend on or be ambiguous with system type sets that have many instances + for (&set, systems) in systems_in_sets.iter() { + let node = &self.system_sets[set.index()]; + if node.is_system_type() { + let ambiguities = self.ambiguous_with.edges(set).count(); + let mut dependencies = 0; + dependencies += self + .dependency + .graph + .edges_directed(set, Direction::Incoming) + .count(); + dependencies += self + .dependency + .graph + .edges_directed(set, Direction::Outgoing) + .count(); + if systems.len() > 1 && (ambiguities > 0 || dependencies > 0) { + return Err(ScheduleBuildError::SystemTypeSetAmbiguity( + node.inner.dyn_clone(), + )); + } + } + } + + // flatten dependency graph + let mut dependency_flattened = DiGraphMap::new(); + for id in self.dependency.graph.nodes() { + if id.is_system() { + dependency_flattened.add_node(id); + } + } + + for (lhs, rhs, _) in self.dependency.graph.all_edges() { + match (lhs, rhs) { + (NodeId::System(_), NodeId::System(_)) => { + dependency_flattened.add_edge(lhs, rhs, ()); + } + (NodeId::Set(_), NodeId::System(_)) => { + for &lhs_ in &systems_in_sets[&lhs] { + dependency_flattened.add_edge(lhs_, rhs, ()); + } + } + (NodeId::System(_), NodeId::Set(_)) => { + for &rhs_ in &systems_in_sets[&rhs] { + dependency_flattened.add_edge(lhs, rhs_, ()); + } + } + (NodeId::Set(_), NodeId::Set(_)) => { + for &lhs_ in &systems_in_sets[&lhs] { + for &rhs_ in &systems_in_sets[&rhs] { + dependency_flattened.add_edge(lhs_, rhs_, ()); + } + } + } + } + } + + // check flattened dependencies for cycles + let flat_scc = tarjan_scc(&dependency_flattened); + if self.contains_cycles(&flat_scc) { + self.report_cycles(&flat_scc); + return Err(ScheduleBuildError::DependencyCycle); + } + + self.dependency_flattened.graph = dependency_flattened; + self.dependency_flattened.topsort = + flat_scc.into_iter().flatten().rev().collect::>(); + + let flat_results = check_graph( + &self.dependency_flattened.graph, + &self.dependency_flattened.topsort, + ); + + // remove redundant edges + self.dependency_flattened.graph = flat_results.transitive_reduction; + + // flatten allowed ambiguities + let mut ambiguous_with_flattened = UnGraphMap::new(); + for (lhs, rhs, _) in self.ambiguous_with.all_edges() { + match (lhs, rhs) { + (NodeId::System(_), NodeId::System(_)) => { + ambiguous_with_flattened.add_edge(lhs, rhs, ()); + } + (NodeId::Set(_), NodeId::System(_)) => { + for &lhs_ in &systems_in_sets[&lhs] { + ambiguous_with_flattened.add_edge(lhs_, rhs, ()); + } + } + (NodeId::System(_), NodeId::Set(_)) => { + for &rhs_ in &systems_in_sets[&rhs] { + ambiguous_with_flattened.add_edge(lhs, rhs_, ()); + } + } + (NodeId::Set(_), NodeId::Set(_)) => { + for &lhs_ in &systems_in_sets[&lhs] { + for &rhs_ in &systems_in_sets[&rhs] { + ambiguous_with_flattened.add_edge(lhs_, rhs_, ()); + } + } + } + } + } + + self.ambiguous_with_flattened = ambiguous_with_flattened; + + // check for conflicts + let mut conflicting_systems = Vec::new(); + for &(a, b) in flat_results.disconnected.iter() { + if self.ambiguous_with_flattened.contains_edge(a, b) + || self.ambiguous_with_all.contains(&a) + || self.ambiguous_with_all.contains(&b) + { + continue; + } + + let system_a = self.systems[a.index()].as_ref().unwrap(); + let system_b = self.systems[b.index()].as_ref().unwrap(); + if system_a.is_exclusive() || system_b.is_exclusive() { + conflicting_systems.push((a, b, Vec::new())); + } else { + let access_a = system_a.component_access(); + let access_b = system_b.component_access(); + if !access_a.is_compatible(access_b) { + let conflicts = access_a.get_conflicts(access_b); + conflicting_systems.push((a, b, conflicts)); + } + } + } + + if self.contains_conflicts(&conflicting_systems) { + self.report_conflicts(&conflicting_systems); + if matches!(self.settings.ambiguity_detection, LogLevel::Error) { + return Err(ScheduleBuildError::Ambiguity); + } + } + + // build the schedule + let dg_system_ids = self.dependency_flattened.topsort.clone(); + let dg_system_idx_map = dg_system_ids + .iter() + .cloned() + .enumerate() + .map(|(i, id)| (id, i)) + .collect::>(); + + let hg_systems = self + .hierarchy + .topsort + .iter() + .cloned() + .enumerate() + .filter(|&(_i, id)| id.is_system()) + .collect::>(); + + let (hg_set_idxs, hg_set_ids): (Vec<_>, Vec<_>) = self + .hierarchy + .topsort + .iter() + .cloned() + .enumerate() + .filter(|&(_i, id)| { + // ignore system sets that have no conditions + // ignore system type sets (already covered, they don't have conditions) + id.is_set() + && self.system_set_conditions[id.index()] + .as_ref() + .filter(|v| !v.is_empty()) + .is_some() + }) + .unzip(); + + let sys_count = self.systems.len(); + let set_count = hg_set_ids.len(); + let node_count = self.systems.len() + self.system_sets.len(); + + // get the number of dependencies and the immediate dependents of each system + // (needed by multi-threaded executor to run systems in the correct order) + let mut system_dependencies = Vec::with_capacity(sys_count); + let mut system_dependents = Vec::with_capacity(sys_count); + for &sys_id in &dg_system_ids { + let num_dependencies = self + .dependency_flattened + .graph + .neighbors_directed(sys_id, Direction::Incoming) + .count(); + + let dependents = self + .dependency_flattened + .graph + .neighbors_directed(sys_id, Direction::Outgoing) + .map(|dep_id| dg_system_idx_map[&dep_id]) + .collect::>(); + + system_dependencies.push(num_dependencies); + system_dependents.push(dependents); + } + + // get the rows and columns of the hierarchy graph's reachability matrix + // (needed to we can evaluate conditions in the correct order) + let mut systems_in_sets = vec![FixedBitSet::with_capacity(sys_count); set_count]; + for (i, &row) in hg_set_idxs.iter().enumerate() { + let bitset = &mut systems_in_sets[i]; + for &(col, sys_id) in &hg_systems { + let idx = dg_system_idx_map[&sys_id]; + let is_descendant = hier_results.reachable[index(row, col, node_count)]; + bitset.set(idx, is_descendant); + } + } + + let mut sets_of_systems = vec![FixedBitSet::with_capacity(set_count); sys_count]; + for &(col, sys_id) in &hg_systems { + let i = dg_system_idx_map[&sys_id]; + let bitset = &mut sets_of_systems[i]; + for (idx, &row) in hg_set_idxs + .iter() + .enumerate() + .take_while(|&(_idx, &row)| row < col) + { + let is_ancestor = hier_results.reachable[index(row, col, node_count)]; + bitset.set(idx, is_ancestor); + } + } + + Ok(SystemSchedule { + systems: Vec::with_capacity(sys_count), + system_conditions: Vec::with_capacity(sys_count), + set_conditions: Vec::with_capacity(set_count), + system_ids: dg_system_ids, + set_ids: hg_set_ids, + system_dependencies, + system_dependents, + sets_of_systems, + systems_in_sets, + }) + } + + fn update_schedule(&mut self, schedule: &mut SystemSchedule) -> Result<(), ScheduleBuildError> { + if !self.uninit.is_empty() { + return Err(ScheduleBuildError::Uninitialized); + } + + // move systems out of old schedule + for ((id, system), conditions) in schedule + .system_ids + .drain(..) + .zip(schedule.systems.drain(..)) + .zip(schedule.system_conditions.drain(..)) + { + self.systems[id.index()] = Some(system); + self.system_conditions[id.index()] = Some(conditions); + } + + for (id, conditions) in schedule + .set_ids + .drain(..) + .zip(schedule.set_conditions.drain(..)) + { + self.system_set_conditions[id.index()] = Some(conditions); + } + + *schedule = self.build_schedule()?; + + // move systems into new schedule + for &id in &schedule.system_ids { + let system = self.systems[id.index()].take().unwrap(); + let conditions = self.system_conditions[id.index()].take().unwrap(); + schedule.systems.push(system); + schedule.system_conditions.push(conditions); + } + + for &id in &schedule.set_ids { + let conditions = self.system_set_conditions[id.index()].take().unwrap(); + schedule.set_conditions.push(conditions); + } + + Ok(()) + } +} + +// methods for reporting errors +impl ScheduleGraph { + fn get_node_name(&self, id: &NodeId) -> String { + match id { + NodeId::System(_) => self.systems[id.index()] + .as_ref() + .unwrap() + .name() + .to_string(), + NodeId::Set(_) => self.system_sets[id.index()].name(), + } + } + + fn get_node_kind(id: &NodeId) -> &'static str { + match id { + NodeId::System(_) => "system", + NodeId::Set(_) => "system set", + } + } + + fn contains_hierarchy_conflicts(&self, transitive_edges: &[(NodeId, NodeId)]) -> bool { + if transitive_edges.is_empty() { + return false; + } + + true + } + + fn report_hierarchy_conflicts(&self, transitive_edges: &[(NodeId, NodeId)]) { + let mut message = String::from("hierarchy contains redundant edge(s)"); + for (parent, child) in transitive_edges { + writeln!( + message, + " -- {:?} '{:?}' cannot be child of set '{:?}', longer path exists", + Self::get_node_kind(child), + self.get_node_name(child), + self.get_node_name(parent), + ) + .unwrap(); + } + + error!("{}", message); + } + + fn contains_cycles(&self, strongly_connected_components: &[Vec]) -> bool { + if strongly_connected_components + .iter() + .all(|scc| scc.len() == 1) + { + return false; + } + + true + } + + fn report_cycles(&self, strongly_connected_components: &[Vec]) { + let components_with_cycles = strongly_connected_components + .iter() + .filter(|scc| scc.len() > 1) + .cloned() + .collect::>(); + + let mut message = format!( + "schedule contains at least {} cycle(s)", + components_with_cycles.len() + ); + + writeln!(message, " -- cycle(s) found within:").unwrap(); + for (i, scc) in components_with_cycles.into_iter().enumerate() { + let names = scc + .iter() + .map(|id| self.get_node_name(id)) + .collect::>(); + writeln!(message, " ---- {i}: {names:?}").unwrap(); + } + + error!("{}", message); + } + + fn contains_conflicts(&self, conflicts: &[(NodeId, NodeId, Vec)]) -> bool { + if conflicts.is_empty() { + return false; + } + + true + } + + fn report_conflicts(&self, ambiguities: &[(NodeId, NodeId, Vec)]) { + let mut string = String::from( + "Some systems with conflicting access have indeterminate execution order. \ + Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n", + ); + + for (system_a, system_b, conflicts) in ambiguities { + debug_assert!(system_a.is_system()); + debug_assert!(system_b.is_system()); + let name_a = self.get_node_name(system_a); + let name_b = self.get_node_name(system_b); + + writeln!(string, " -- {name_a} and {name_b}").unwrap(); + if !conflicts.is_empty() { + writeln!(string, " conflict on: {conflicts:?}").unwrap(); + } else { + // one or both systems must be exclusive + let world = std::any::type_name::(); + writeln!(string, " conflict on: {world}").unwrap(); + } + } + + warn!("{}", string); + } +} + +/// Category of errors encountered during schedule construction. +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum ScheduleBuildError { + /// A system set contains itself. + #[error("`{0:?}` contains itself.")] + HierarchyLoop(BoxedSystemSet), + /// The hierarchy of system sets contains a cycle. + #[error("System set hierarchy contains cycle(s).")] + HierarchyCycle, + /// The hierarchy of system sets contains redundant edges. + /// + /// This error is disabled by default, but can be opted-in using [`ScheduleBuildSettings`]. + #[error("System set hierarchy contains redundant edges.")] + HierarchyRedundancy, + /// A system (set) has been told to run before itself. + #[error("`{0:?}` depends on itself.")] + DependencyLoop(BoxedSystemSet), + /// The dependency graph contains a cycle. + #[error("System dependencies contain cycle(s).")] + DependencyCycle, + /// Tried to order a system (set) relative to a system set it belongs to. + #[error("`{0:?}` and `{1:?}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")] + CrossDependency(String, String), + /// Tried to order a system (set) relative to all instances of some system function. + #[error("Tried to order against `fn {0:?}` in a schedule that has more than one `{0:?}` instance. `fn {0:?}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")] + SystemTypeSetAmbiguity(BoxedSystemSet), + /// Systems with conflicting access have indeterminate run order. + /// + /// This error is disabled by default, but can be opted-in using [`ScheduleBuildSettings`]. + #[error("Systems with conflicting access have indeterminate run order.")] + Ambiguity, + /// Tried to run a schedule before all of its systems have been initialized. + #[error("Systems in schedule have not been initialized.")] + Uninitialized, +} + +/// Specifies how schedule construction should respond to detecting a certain kind of issue. +pub enum LogLevel { + /// Occurrences are logged only. + Warn, + /// Occurrences are logged and result in errors. + Error, +} + +/// Specifies miscellaneous settings for schedule construction. +pub struct ScheduleBuildSettings { + ambiguity_detection: LogLevel, + hierarchy_detection: LogLevel, +} + +impl Default for ScheduleBuildSettings { + fn default() -> Self { + Self::new() + } +} + +impl ScheduleBuildSettings { + pub const fn new() -> Self { + Self { + ambiguity_detection: LogLevel::Warn, + hierarchy_detection: LogLevel::Warn, + } + } + + /// Determines whether the presence of ambiguities (systems with conflicting access but indeterminate order) + /// is only logged or also results in an [`Ambiguity`](ScheduleBuildError::Ambiguity) error. + pub fn with_ambiguity_detection(mut self, level: LogLevel) -> Self { + self.ambiguity_detection = level; + self + } + + /// Determines whether the presence of redundant edges in the hierarchy of system sets is only + /// logged or also results in a [`HierarchyRedundancy`](ScheduleBuildError::HierarchyRedundancy) + /// error. + pub fn with_hierarchy_detection(mut self, level: LogLevel) -> Self { + self.hierarchy_detection = level; + self + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/set.rs b/crates/bevy_ecs/src/schedule_v3/set.rs new file mode 100644 index 0000000000000..5b29f31e6a3ae --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/set.rs @@ -0,0 +1,149 @@ +use std::fmt::Debug; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; + +pub use bevy_ecs_macros::{ScheduleLabel, SystemSet}; +use bevy_utils::define_boxed_label; +use bevy_utils::label::DynHash; + +use crate::system::{ + ExclusiveSystemParam, ExclusiveSystemParamFunction, IsExclusiveFunctionSystem, + IsFunctionSystem, SystemParam, SystemParamFunction, +}; + +define_boxed_label!(ScheduleLabel); + +pub type BoxedSystemSet = Box; +pub type BoxedScheduleLabel = Box; + +/// Types that identify logical groups of systems. +pub trait SystemSet: DynHash + Debug + Send + Sync + 'static { + /// Returns `true` if this system set is a [`SystemTypeSet`]. + fn is_system_type(&self) -> bool { + false + } + + #[doc(hidden)] + fn dyn_clone(&self) -> Box; +} + +impl PartialEq for dyn SystemSet { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other.as_dyn_eq()) + } +} + +impl Eq for dyn SystemSet {} + +impl Hash for dyn SystemSet { + fn hash(&self, state: &mut H) { + self.dyn_hash(state); + } +} + +impl Clone for Box { + fn clone(&self) -> Self { + self.dyn_clone() + } +} + +/// A [`SystemSet`] grouping instances of the same function. +/// +/// This kind of set is automatically populated and thus has some special rules: +/// - You cannot manually add members. +/// - You cannot configure them. +/// - You cannot order something relative to one if it has more than one member. +pub struct SystemTypeSet(PhantomData T>); + +impl SystemTypeSet { + pub(crate) fn new() -> Self { + Self(PhantomData) + } +} + +impl Debug for SystemTypeSet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SystemTypeSet") + .field(&std::any::type_name::()) + .finish() + } +} + +impl Hash for SystemTypeSet { + fn hash(&self, _state: &mut H) { + // all systems of a given type are the same + } +} +impl Clone for SystemTypeSet { + fn clone(&self) -> Self { + Self(PhantomData) + } +} + +impl Copy for SystemTypeSet {} + +impl PartialEq for SystemTypeSet { + #[inline] + fn eq(&self, _other: &Self) -> bool { + // all systems of a given type are the same + true + } +} + +impl Eq for SystemTypeSet {} + +impl SystemSet for SystemTypeSet { + fn is_system_type(&self) -> bool { + true + } + + fn dyn_clone(&self) -> Box { + Box::new(*self) + } +} + +/// Types that can be converted into a [`SystemSet`]. +pub trait IntoSystemSet: Sized { + type Set: SystemSet; + + fn into_system_set(self) -> Self::Set; +} + +// systems sets +impl IntoSystemSet<()> for S { + type Set = Self; + + #[inline] + fn into_system_set(self) -> Self::Set { + self + } +} + +// systems +impl IntoSystemSet<(IsFunctionSystem, In, Out, Param, Marker)> for F +where + Param: SystemParam, + F: SystemParamFunction, +{ + type Set = SystemTypeSet; + + #[inline] + fn into_system_set(self) -> Self::Set { + SystemTypeSet::new() + } +} + +// exclusive systems +impl IntoSystemSet<(IsExclusiveFunctionSystem, In, Out, Param, Marker)> + for F +where + Param: ExclusiveSystemParam, + F: ExclusiveSystemParamFunction, +{ + type Set = SystemTypeSet; + + #[inline] + fn into_system_set(self) -> Self::Set { + SystemTypeSet::new() + } +} diff --git a/crates/bevy_ecs/src/schedule_v3/state.rs b/crates/bevy_ecs/src/schedule_v3/state.rs new file mode 100644 index 0000000000000..85f357c670f7f --- /dev/null +++ b/crates/bevy_ecs/src/schedule_v3/state.rs @@ -0,0 +1,64 @@ +use std::fmt::Debug; +use std::hash::Hash; +use std::mem; + +use crate as bevy_ecs; +use crate::schedule_v3::{ScheduleLabel, SystemSet, WorldExt}; +use crate::system::Resource; +use crate::world::World; + +/// Types that can define states in a finite-state machine. +pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + type Iter: Iterator; + + /// Returns an iterator over all the state variants. + fn states() -> Self::Iter; +} + +/// The label of a [`Schedule`](super::Schedule) that runs whenever [`State`] +/// enters this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnEnter(pub S); + +/// The label of a [`Schedule`](super::Schedule) that runs whenever [`State`] +/// exits this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnExit(pub S); + +/// A [`SystemSet`] that will run within \ when this state is active. +/// +/// This is provided for convenience. A more general [`state_equals`](super::state_equals) +/// [condition](super::Condition) also exists for systems that need to run elsewhere. +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnUpdate(pub S); + +/// A finite-state machine whose transitions have associated schedules +/// ([`OnEnter(state)`] and [`OnExit(state)`]). +/// +/// The current state value can be accessed through this resource. To *change* the state, +/// queue a transition in the [`NextState`] resource, and it will be applied by the next +/// [`apply_state_transition::`] system. +#[derive(Resource)] +pub struct State(pub S); + +/// The next state of [`State`]. +/// +/// To queue a transition, just set the contained value to `Some(next_state)`. +#[derive(Resource)] +pub struct NextState(pub Option); + +/// If a new state is queued in [`NextState`], this system: +/// - Takes the new state value from [`NextState`] and updates [`State`]. +/// - Runs the [`OnExit(exited_state)`] schedule. +/// - Runs the [`OnEnter(entered_state)`] schedule. +pub fn apply_state_transition(world: &mut World) { + if world.resource::>().0.is_some() { + let entered_state = world.resource_mut::>().0.take().unwrap(); + let exited_state = mem::replace( + &mut world.resource_mut::>().0, + entered_state.clone(), + ); + world.run_schedule(OnExit(exited_state)); + world.run_schedule(OnEnter(entered_state)); + } +} diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index c0bfa3302572c..49b370b7b8e3a 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -6,6 +6,7 @@ use std::{ }; use bevy_ptr::{OwningPtr, Ptr, PtrMut}; +use bevy_utils::OnDrop; /// A flat, type-erased data storage type /// @@ -46,10 +47,11 @@ impl BlobVec { drop: Option)>, capacity: usize, ) -> BlobVec { + let align = NonZeroUsize::new(item_layout.align()).expect("alignment must be > 0"); + let data = bevy_ptr::dangling_with_align(align); if item_layout.size() == 0 { - let align = NonZeroUsize::new(item_layout.align()).expect("alignment must be > 0"); BlobVec { - data: bevy_ptr::dangling_with_align(align), + data, capacity: usize::MAX, len: 0, item_layout, @@ -57,7 +59,7 @@ impl BlobVec { } } else { let mut blob_vec = BlobVec { - data: NonNull::dangling(), + data, capacity: 0, len: 0, item_layout, @@ -152,31 +154,51 @@ impl BlobVec { /// [`BlobVec`]'s `item_layout` pub unsafe fn replace_unchecked(&mut self, index: usize, value: OwningPtr<'_>) { debug_assert!(index < self.len()); - // If `drop` panics, then when the collection is dropped during stack unwinding, the - // collection's `Drop` impl will call `drop` again for the old value (which is still stored - // in the collection), so we get a double drop. To prevent that, we set len to 0 until we're - // done. - let old_len = self.len; - let ptr = self.get_unchecked_mut(index).promote().as_ptr(); - self.len = 0; - // Drop the old value, then write back, justifying the promotion - // If the drop impl for the old value panics then we run the drop impl for `value` too. + + // Pointer to the value in the vector that will get replaced. + // SAFETY: The caller ensures that `index` fits in this vector. + let destination = NonNull::from(self.get_unchecked_mut(index)); + let source = value.as_ptr(); + if let Some(drop) = self.drop { - struct OnDrop(F); - impl Drop for OnDrop { - fn drop(&mut self) { - (self.0)(); - } - } - let value = value.as_ptr(); - let on_unwind = OnDrop(|| (drop)(OwningPtr::new(NonNull::new_unchecked(value)))); + // Temporarily set the length to zero, so that if `drop` panics the caller + // will not be left with a `BlobVec` containing a dropped element within + // its initialized range. + let old_len = self.len; + self.len = 0; + + // Transfer ownership of the old value out of the vector, so it can be dropped. + // SAFETY: + // - `destination` was obtained from a `PtrMut` in this vector, which ensures it is non-null, + // well-aligned for the underlying type, and has proper provenance. + // - The storage location will get overwritten with `value` later, which ensures + // that the element will not get observed or double dropped later. + // - If a panic occurs, `self.len` will remain `0`, which ensures a double-drop + // does not occur. Instead, all elements will be forgotten. + let old_value = OwningPtr::new(destination); - (drop)(OwningPtr::new(NonNull::new_unchecked(ptr))); + // This closure will run in case `drop()` panics, + // which ensures that `value` does not get forgotten. + let on_unwind = OnDrop::new(|| drop(value)); + drop(old_value); + + // If the above code does not panic, make sure that `value` doesn't get dropped. core::mem::forget(on_unwind); + + // Make the vector's contents observable again, since panics are no longer possible. + self.len = old_len; } - std::ptr::copy_nonoverlapping::(value.as_ptr(), ptr, self.item_layout.size()); - self.len = old_len; + + // Copy the new value into the vector, overwriting the previous value. + // SAFETY: + // - `source` and `destination` were obtained from `OwningPtr`s, which ensures they are + // valid for both reads and writes. + // - The value behind `source` will only be dropped if the above branch panics, + // so it must still be initialized and it is safe to transfer ownership into the vector. + // - `source` and `destination` were obtained from different memory locations, + // both of which we have exclusive access to, so they are guaranteed not to overlap. + std::ptr::copy_nonoverlapping::(source, destination.as_ptr(), self.item_layout.size()); } /// Pushes a value to the [`BlobVec`]. @@ -211,6 +233,8 @@ impl BlobVec { #[must_use = "The returned pointer should be used to dropped the removed element"] pub unsafe fn swap_remove_and_forget_unchecked(&mut self, index: usize) -> OwningPtr<'_> { debug_assert!(index < self.len()); + // Since `index` must be strictly less than `self.len` and `index` is at least zero, + // `self.len` must be at least one. Thus, this cannot underflow. let new_len = self.len - 1; let size = self.item_layout.size(); if index != new_len { @@ -222,6 +246,10 @@ impl BlobVec { } self.len = new_len; // Cannot use get_unchecked here as this is technically out of bounds after changing len. + // SAFETY: + // - `new_len` is less than the old len, so it must fit in this vector's allocation. + // - `size` is a multiple of the erased type's alignment, + // so adding a multiple of `size` will preserve alignment. self.get_ptr_mut().byte_add(new_len * size).promote() } @@ -263,7 +291,13 @@ impl BlobVec { #[inline] pub unsafe fn get_unchecked(&self, index: usize) -> Ptr<'_> { debug_assert!(index < self.len()); - self.get_ptr().byte_add(index * self.item_layout.size()) + let size = self.item_layout.size(); + // SAFETY: + // - The caller ensures that `index` fits in this vector, + // so this operation will not overflow the original allocation. + // - `size` is a multiple of the erased type's alignment, + // so adding a multiple of `size` will preserve alignment. + self.get_ptr().byte_add(index * size) } /// # Safety @@ -271,8 +305,13 @@ impl BlobVec { #[inline] pub unsafe fn get_unchecked_mut(&mut self, index: usize) -> PtrMut<'_> { debug_assert!(index < self.len()); - let layout_size = self.item_layout.size(); - self.get_ptr_mut().byte_add(index * layout_size) + let size = self.item_layout.size(); + // SAFETY: + // - The caller ensures that `index` fits in this vector, + // so this operation will not overflow the original allocation. + // - `size` is a multiple of the erased type's alignment, + // so adding a multiple of `size` will preserve alignment. + self.get_ptr_mut().byte_add(index * size) } /// Gets a [`Ptr`] to the start of the vec @@ -304,15 +343,18 @@ impl BlobVec { // accidentally drop elements twice in the event of a drop impl panicking. self.len = 0; if let Some(drop) = self.drop { - let layout_size = self.item_layout.size(); + let size = self.item_layout.size(); for i in 0..len { - // SAFETY: `i * layout_size` is inbounds for the allocation, and the item is left unreachable so it can be safely promoted to an `OwningPtr` - unsafe { - // NOTE: this doesn't use self.get_unchecked(i) because the debug_assert on index - // will panic here due to self.len being set to 0 - let ptr = self.get_ptr_mut().byte_add(i * layout_size).promote(); - (drop)(ptr); - } + // SAFETY: + // * 0 <= `i` < `len`, so `i * size` must be in bounds for the allocation. + // * `size` is a multiple of the erased type's alignment, + // so adding a multiple of `size` will preserve alignment. + // * The item is left unreachable so it can be safely promoted to an `OwningPtr`. + // NOTE: `self.get_unchecked_mut(i)` cannot be used here, since the `debug_assert` + // would panic due to `self.len` being set to 0. + let item = unsafe { self.get_ptr_mut().byte_add(i * size).promote() }; + // SAFETY: `item` was obtained from this `BlobVec`, so its underlying type must match `drop`. + unsafe { drop(item) }; } } } diff --git a/crates/bevy_ecs/src/storage/mod.rs b/crates/bevy_ecs/src/storage/mod.rs index 6e848a042b492..b2fab3fdcb590 100644 --- a/crates/bevy_ecs/src/storage/mod.rs +++ b/crates/bevy_ecs/src/storage/mod.rs @@ -14,5 +14,6 @@ pub use table::*; pub struct Storages { pub sparse_sets: SparseSets, pub tables: Tables, - pub resources: Resources, + pub resources: Resources, + pub non_send_resources: Resources, } diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index 01fa742e06362..f1a0e9fb53552 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,20 +1,63 @@ use crate::archetype::ArchetypeComponentId; +use crate::change_detection::{MutUntyped, TicksMut}; use crate::component::{ComponentId, ComponentTicks, Components, TickCells}; use crate::storage::{Column, SparseSet, TableRow}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +use std::{mem::ManuallyDrop, thread::ThreadId}; /// The type-erased backing storage and metadata for a single resource within a [`World`]. /// +/// If `SEND` is false, values of this type will panic if dropped from a different thread. +/// /// [`World`]: crate::world::World -pub struct ResourceData { - column: Column, +pub struct ResourceData { + column: ManuallyDrop, + type_name: String, id: ArchetypeComponentId, + origin_thread_id: Option, +} + +impl Drop for ResourceData { + fn drop(&mut self) { + if self.is_present() { + // If this thread is already panicking, panicking again will cause + // the entire process to abort. In this case we choose to avoid + // dropping or checking this altogether and just leak the column. + if std::thread::panicking() { + return; + } + self.validate_access(); + } + // SAFETY: Drop is only called once upon dropping the ResourceData + // and is inaccessible after this as the parent ResourceData has + // been dropped. The validate_access call above will check that the + // data is dropped on the thread it was inserted from. + unsafe { + ManuallyDrop::drop(&mut self.column); + } + } } -impl ResourceData { +impl ResourceData { /// The only row in the underlying column. const ROW: TableRow = TableRow::new(0); + #[inline] + fn validate_access(&self) { + if SEND { + return; + } + if self.origin_thread_id != Some(std::thread::current().id()) { + // Panic in tests, as testing for aborting is nearly impossible + panic!( + "Attempted to access or drop non-send resource {} from thread {:?} on a thread {:?}. This is not allowed. Aborting.", + self.type_name, + self.origin_thread_id, + std::thread::current().id() + ); + } + } + /// Returns true if the resource is populated. #[inline] pub fn is_present(&self) -> bool { @@ -28,9 +71,16 @@ impl ResourceData { } /// Gets a read-only pointer to the underlying resource, if available. + /// + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not accessed from the + /// original thread it was inserted from. #[inline] pub fn get_data(&self) -> Option> { - self.column.get_data(Self::ROW) + self.column.get_data(Self::ROW).map(|res| { + self.validate_access(); + res + }) } /// Gets a read-only reference to the change ticks of the underlying resource, if available. @@ -39,26 +89,49 @@ impl ResourceData { self.column.get_ticks(Self::ROW) } + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not accessed from the + /// original thread it was inserted in. #[inline] pub(crate) fn get_with_ticks(&self) -> Option<(Ptr<'_>, TickCells<'_>)> { - self.column.get(Self::ROW) + self.column.get(Self::ROW).map(|res| { + self.validate_access(); + res + }) + } + + pub(crate) fn get_mut( + &mut self, + last_change_tick: u32, + change_tick: u32, + ) -> Option> { + let (ptr, ticks) = self.get_with_ticks()?; + Some(MutUntyped { + // SAFETY: We have exclusive access to the underlying storage. + value: unsafe { ptr.assert_unique() }, + // SAFETY: We have exclusive access to the underlying storage. + ticks: unsafe { TicksMut::from_tick_cells(ticks, last_change_tick, change_tick) }, + }) } /// Inserts a value into the resource. If a value is already present /// it will be replaced. /// - /// # Safety - /// `value` must be valid for the underlying type for the resource. - /// - /// The underlying type must be [`Send`] or be inserted from the main thread. - /// This can be validated with [`World::validate_non_send_access_untyped`]. + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not replaced from + /// the original thread it was inserted in. /// - /// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped + /// # Safety + /// - `value` must be valid for the underlying type for the resource. #[inline] pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: u32) { if self.is_present() { + self.validate_access(); self.column.replace(Self::ROW, value, change_tick); } else { + if !SEND { + self.origin_thread_id = Some(std::thread::current().id()); + } self.column.push(value, ComponentTicks::new(change_tick)); } } @@ -66,13 +139,12 @@ impl ResourceData { /// Inserts a value into the resource with a pre-existing change tick. If a /// value is already present it will be replaced. /// - /// # Safety - /// `value` must be valid for the underlying type for the resource. - /// - /// The underlying type must be [`Send`] or be inserted from the main thread. - /// This can be validated with [`World::validate_non_send_access_untyped`]. + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not replaced from + /// the original thread it was inserted in. /// - /// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped + /// # Safety + /// - `value` must be valid for the underlying type for the resource. #[inline] pub(crate) unsafe fn insert_with_ticks( &mut self, @@ -80,6 +152,7 @@ impl ResourceData { change_ticks: ComponentTicks, ) { if self.is_present() { + self.validate_access(); self.column.replace_untracked(Self::ROW, value); *self.column.get_added_ticks_unchecked(Self::ROW).deref_mut() = change_ticks.added; *self @@ -87,35 +160,41 @@ impl ResourceData { .get_changed_ticks_unchecked(Self::ROW) .deref_mut() = change_ticks.changed; } else { + if !SEND { + self.origin_thread_id = Some(std::thread::current().id()); + } self.column.push(value, change_ticks); } } /// Removes a value from the resource, if present. /// - /// # Safety - /// The underlying type must be [`Send`] or be removed from the main thread. - /// This can be validated with [`World::validate_non_send_access_untyped`]. - /// - /// The removed value must be used or dropped. - /// - /// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not removed from the + /// original thread it was inserted from. #[inline] #[must_use = "The returned pointer to the removed component should be used or dropped"] - pub(crate) unsafe fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> { - self.column.swap_remove_and_forget(Self::ROW) + pub(crate) fn remove(&mut self) -> Option<(OwningPtr<'_>, ComponentTicks)> { + if SEND { + self.column.swap_remove_and_forget(Self::ROW) + } else { + self.is_present() + .then(|| self.validate_access()) + .and_then(|_| self.column.swap_remove_and_forget(Self::ROW)) + } } /// Removes a value from the resource, if present, and drops it. /// - /// # Safety - /// The underlying type must be [`Send`] or be removed from the main thread. - /// This can be validated with [`World::validate_non_send_access_untyped`]. - /// - /// [`World::validate_non_send_access_untyped`]: crate::world::World::validate_non_send_access_untyped + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not + /// accessed from the original thread it was inserted in. #[inline] - pub(crate) unsafe fn remove_and_drop(&mut self) { - self.column.clear(); + pub(crate) fn remove_and_drop(&mut self) { + if self.is_present() { + self.validate_access(); + self.column.clear(); + } } } @@ -124,11 +203,11 @@ impl ResourceData { /// [`Resource`]: crate::system::Resource /// [`World`]: crate::world::World #[derive(Default)] -pub struct Resources { - resources: SparseSet, +pub struct Resources { + resources: SparseSet>, } -impl Resources { +impl Resources { /// The total number of resources stored in the [`World`] /// /// [`World`]: crate::world::World @@ -138,7 +217,7 @@ impl Resources { } /// Iterate over all resources that have been initialized, i.e. given a [`ComponentId`] - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator)> { self.resources.iter().map(|(id, data)| (*id, data)) } @@ -153,13 +232,19 @@ impl Resources { /// Gets read-only access to a resource, if it exists. #[inline] - pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData> { + pub fn get(&self, component_id: ComponentId) -> Option<&ResourceData> { self.resources.get(component_id) } + /// Clears all resources. + #[inline] + pub fn clear(&mut self) { + self.resources.clear(); + } + /// Gets mutable access to a resource, if it exists. #[inline] - pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData> { + pub(crate) fn get_mut(&mut self, component_id: ComponentId) -> Option<&mut ResourceData> { self.resources.get_mut(component_id) } @@ -167,17 +252,23 @@ impl Resources { /// /// # Panics /// Will panic if `component_id` is not valid for the provided `components` + /// If `SEND` is false, this will panic if `component_id`'s `ComponentInfo` is not registered as being `Send` + `Sync`. pub(crate) fn initialize_with( &mut self, component_id: ComponentId, components: &Components, f: impl FnOnce() -> ArchetypeComponentId, - ) -> &mut ResourceData { + ) -> &mut ResourceData { self.resources.get_or_insert_with(component_id, || { let component_info = components.get_info(component_id).unwrap(); + if SEND { + assert!(component_info.is_send_and_sync()); + } ResourceData { - column: Column::with_capacity(component_info, 1), + column: ManuallyDrop::new(Column::with_capacity(component_info, 1)), + type_name: String::from(component_info.name()), id: f(), + origin_thread_id: None, } }) } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index cc3da14682cb9..d145122c33456 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -447,6 +447,12 @@ impl SparseSet { }) } + pub fn clear(&mut self) { + self.dense.clear(); + self.indices.clear(); + self.sparse.clear(); + } + pub(crate) fn into_immutable(self) -> ImmutableSparseSet { ImmutableSparseSet { dense: self.dense.into_boxed_slice(), diff --git a/crates/bevy_ecs/src/storage/table.rs b/crates/bevy_ecs/src/storage/table.rs index e5dc58dc26d5a..fe2fd6cd783ba 100644 --- a/crates/bevy_ecs/src/storage/table.rs +++ b/crates/bevy_ecs/src/storage/table.rs @@ -26,6 +26,8 @@ use std::{ /// [`Archetype`]: crate::archetype::Archetype /// [`Archetype::table_id`]: crate::archetype::Archetype::table_id #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation +#[repr(transparent)] pub struct TableId(u32); impl TableId { @@ -49,14 +51,14 @@ impl TableId { /// A opaque newtype for rows in [`Table`]s. Specifies a single row in a specific table. /// -/// Values of this type are retreivable from [`Archetype::entity_table_row`] and can be +/// Values of this type are retrievable from [`Archetype::entity_table_row`] and can be /// used alongside [`Archetype::table_id`] to fetch the exact table and row where an /// [`Entity`]'s /// /// Values of this type are only valid so long as entities have not moved around. /// Adding and removing components from an entity, or despawning it will invalidate /// potentially any table row in the table the entity was previously stored in. Users -/// should *always* fetch the approripate row from the entity's [`Archetype`] before +/// should *always* fetch the appropriate row from the entity's [`Archetype`] before /// fetching the entity's components. /// /// [`Archetype`]: crate::archetype::Archetype @@ -64,6 +66,8 @@ impl TableId { /// [`Archetype::table_id`]: crate::archetype::Archetype::table_id /// [`Entity`]: crate::entity::Entity #[derive(Debug, Clone, Copy, PartialEq, Eq)] +// SAFETY: Must be repr(transparent) due to the safety requirements on EntityLocation +#[repr(transparent)] pub struct TableRow(u32); impl TableRow { diff --git a/crates/bevy_ecs/src/system/commands/command_queue.rs b/crates/bevy_ecs/src/system/commands/command_queue.rs index 4ec89226e4ef0..1a40b5ab67c0d 100644 --- a/crates/bevy_ecs/src/system/commands/command_queue.rs +++ b/crates/bevy_ecs/src/system/commands/command_queue.rs @@ -1,23 +1,32 @@ -use std::mem::{ManuallyDrop, MaybeUninit}; +use std::{mem::MaybeUninit, ptr::NonNull}; + +use bevy_ptr::{OwningPtr, Unaligned}; use super::Command; use crate::world::World; struct CommandMeta { + /// Offset from the start of `CommandQueue.bytes` at which the corresponding command is stored. offset: usize, - func: unsafe fn(value: *mut MaybeUninit, world: &mut World), + /// SAFETY: The `value` must point to a value of type `T: Command`, + /// where `T` is some specific type that was used to produce this metadata. + apply_command: unsafe fn(value: OwningPtr, world: &mut World), } /// A queue of [`Command`]s // -// NOTE: [`CommandQueue`] is implemented via a `Vec>` over a `Vec>` +// NOTE: [`CommandQueue`] is implemented via a `Vec>` instead of a `Vec>` // as an optimization. Since commands are used frequently in systems as a way to spawn // entities/components/resources, and it's not currently possible to parallelize these // due to mutable [`World`] access, maximizing performance for [`CommandQueue`] is // preferred to simplicity of implementation. #[derive(Default)] pub struct CommandQueue { + /// Densely stores the data for all commands in the queue. bytes: Vec>, + /// Metadata for each command stored in the queue. + /// SAFETY: Each entry must have a corresponding value stored in `bytes`, + /// stored at offset `CommandMeta.offset` and with an underlying type matching `CommandMeta.apply_command`. metas: Vec, } @@ -34,45 +43,45 @@ impl CommandQueue { where C: Command, { - /// SAFETY: This function is only every called when the `command` bytes is the associated - /// [`Commands`] `T` type. Also this only reads the data via `read_unaligned` so unaligned - /// accesses are safe. - unsafe fn write_command(command: *mut MaybeUninit, world: &mut World) { - let command = command.cast::().read_unaligned(); - command.write(world); - } - - let size = std::mem::size_of::(); let old_len = self.bytes.len(); + // SAFETY: After adding the metadata, we correctly write the corresponding `command` + // of type `C` into `self.bytes`. Zero-sized commands do not get written into the buffer, + // so we'll just use a dangling pointer, which is valid for zero-sized types. self.metas.push(CommandMeta { offset: old_len, - func: write_command::, + apply_command: |command, world| { + // SAFETY: According to the invariants of `CommandMeta.apply_command`, + // `command` must point to a value of type `C`. + let command: C = unsafe { command.read_unaligned() }; + command.write(world); + }, }); - // Use `ManuallyDrop` to forget `command` right away, avoiding - // any use of it after the `ptr::copy_nonoverlapping`. - let command = ManuallyDrop::new(command); - + let size = std::mem::size_of::(); if size > 0 { + // Ensure that the buffer has enough space at the end to fit a value of type `C`. + // Since `C` is non-zero sized, this also guarantees that the buffer is non-null. self.bytes.reserve(size); - // SAFETY: The internal `bytes` vector has enough storage for the - // command (see the call the `reserve` above), the vector has - // its length set appropriately and can contain any kind of bytes. - // In case we're writing a ZST and the `Vec` hasn't allocated yet - // then `as_mut_ptr` will be a dangling (non null) pointer, and - // thus valid for ZST writes. - // Also `command` is forgotten so that when `apply` is called - // later, a double `drop` does not occur. - unsafe { - std::ptr::copy_nonoverlapping( - &*command as *const C as *const MaybeUninit, - self.bytes.as_mut_ptr().add(old_len), - size, - ); - self.bytes.set_len(old_len + size); - } + // SAFETY: The buffer must be at least as long as `old_len`, so this operation + // will not overflow the pointer's original allocation. + let ptr: *mut C = unsafe { self.bytes.as_mut_ptr().add(old_len).cast() }; + + // Transfer ownership of the command into the buffer. + // SAFETY: `ptr` must be non-null, since it is within a non-null buffer. + // The call to `reserve()` ensures that the buffer has enough space to fit a value of type `C`, + // and it is valid to write any bit pattern since the underlying buffer is of type `MaybeUninit`. + unsafe { ptr.write_unaligned(command) }; + + // Grow the vector to include the command we just wrote. + // SAFETY: Due to the call to `.reserve(size)` above, + // this is guaranteed to fit in the vector's capacity. + unsafe { self.bytes.set_len(old_len + size) }; + } else { + // Instead of writing zero-sized types into the buffer, we'll just use a dangling pointer. + // We must forget the command so it doesn't get double-dropped when the queue gets applied. + std::mem::forget(command); } } @@ -83,17 +92,22 @@ impl CommandQueue { // flush the previously queued entities world.flush(); - // SAFETY: In the iteration below, `meta.func` will safely consume and drop each pushed command. - // This operation is so that we can reuse the bytes `Vec`'s internal storage and prevent - // unnecessary allocations. + // Reset the buffer, so it can be reused after this function ends. + // In the loop below, ownership of each command will be transferred into user code. + // SAFETY: `set_len(0)` is always valid. unsafe { self.bytes.set_len(0) }; for meta in self.metas.drain(..) { - // SAFETY: The implementation of `write_command` is safe for the according Command type. - // It's ok to read from `bytes.as_mut_ptr()` because we just wrote to it in `push`. - // The bytes are safely cast to their original type, safely read, and then dropped. + // SAFETY: `CommandQueue` guarantees that each metadata must have a corresponding value stored in `self.bytes`, + // so this addition will not overflow its original allocation. + let cmd = unsafe { self.bytes.as_mut_ptr().add(meta.offset) }; + // SAFETY: It is safe to transfer ownership out of `self.bytes`, since the call to `set_len(0)` above + // gaurantees that nothing stored in the buffer will get observed after this function ends. + // `cmd` points to a valid address of a stored command, so it must be non-null. + let cmd = unsafe { OwningPtr::new(NonNull::new_unchecked(cmd.cast())) }; + // SAFETY: The underlying type of `cmd` matches the type expected by `meta.apply_command`. unsafe { - (meta.func)(self.bytes.as_mut_ptr().add(meta.offset), world); + (meta.apply_command)(cmd, world); } } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 9f1c46a2e7b2b..ed2d469b9b2f5 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -282,8 +282,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn entity<'a>(&'a mut self, entity: Entity) -> EntityCommands<'w, 's, 'a> { self.get_entity(entity).unwrap_or_else(|| { panic!( - "Attempting to create an EntityCommands for entity {:?}, which doesn't exist.", - entity + "Attempting to create an EntityCommands for entity {entity:?}, which doesn't exist.", ) }) } diff --git a/crates/bevy_ecs/src/system/commands/parallel_scope.rs b/crates/bevy_ecs/src/system/commands/parallel_scope.rs index 72c8118aa5152..c249ba3e967c0 100644 --- a/crates/bevy_ecs/src/system/commands/parallel_scope.rs +++ b/crates/bevy_ecs/src/system/commands/parallel_scope.rs @@ -5,19 +5,19 @@ use thread_local::ThreadLocal; use crate::{ entity::Entities, prelude::World, - system::{SystemMeta, SystemParam, SystemParamState}, + system::{SystemMeta, SystemParam}, }; use super::{CommandQueue, Commands}; +/// The internal [`SystemParam`] state of the [`ParallelCommands`] type #[doc(hidden)] #[derive(Default)] -/// The internal [`SystemParamState`] of the [`ParallelCommands`] type pub struct ParallelCommandsState { thread_local_storage: ThreadLocal>, } -/// An alternative to [`Commands`] that can be used in parallel contexts, such as those in [`Query::par_for_each`](crate::system::Query::par_for_each) +/// An alternative to [`Commands`] that can be used in parallel contexts, such as those in [`Query::par_iter`](crate::system::Query::par_iter) /// /// Note: Because command application order will depend on how many threads are ran, non-commutative commands may result in non-deterministic results. /// @@ -33,7 +33,7 @@ pub struct ParallelCommandsState { /// mut query: Query<(Entity, &Velocity)>, /// par_commands: ParallelCommands /// ) { -/// query.par_for_each(32, |(entity, velocity)| { +/// query.par_iter().for_each(|(entity, velocity)| { /// if velocity.magnitude() > 10.0 { /// par_commands.command_scope(|mut commands| { /// commands.entity(entity).despawn(); @@ -48,30 +48,27 @@ pub struct ParallelCommands<'w, 's> { entities: &'w Entities, } -impl SystemParam for ParallelCommands<'_, '_> { - type State = ParallelCommandsState; -} - // SAFETY: no component or resource access to report -unsafe impl SystemParamState for ParallelCommandsState { +unsafe impl SystemParam for ParallelCommands<'_, '_> { + type State = ParallelCommandsState; type Item<'w, 's> = ParallelCommands<'w, 's>; - fn init(_: &mut World, _: &mut crate::system::SystemMeta) -> Self { - Self::default() + fn init_state(_: &mut World, _: &mut crate::system::SystemMeta) -> Self::State { + ParallelCommandsState::default() } - fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { #[cfg(feature = "trace")] let _system_span = bevy_utils::tracing::info_span!("system_commands", name = _system_meta.name()) .entered(); - for cq in &mut self.thread_local_storage { + for cq in &mut state.thread_local_storage { cq.get_mut().apply(world); } } unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, _: &crate::system::SystemMeta, world: &'w World, _: u32, diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 8b1607c75898e..1d5c7fca36781 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -6,12 +6,12 @@ use crate::{ schedule::{SystemLabel, SystemLabelId}, system::{ check_system_change_tick, AsSystemLabel, ExclusiveSystemParam, ExclusiveSystemParamItem, - ExclusiveSystemParamState, IntoSystem, System, SystemMeta, SystemTypeIdLabel, + In, InputMarker, IntoSystem, System, SystemMeta, SystemTypeIdLabel, }, world::{World, WorldId}, }; use bevy_ecs_macros::all_tuples; -use std::{borrow::Cow, marker::PhantomData}; +use std::{any::TypeId, borrow::Cow, marker::PhantomData}; /// A function system that runs with exclusive [`World`] access. /// @@ -19,7 +19,7 @@ use std::{borrow::Cow, marker::PhantomData}; /// [`ExclusiveSystemParam`]s. /// /// [`ExclusiveFunctionSystem`] must be `.initialized` before they can be run. -pub struct ExclusiveFunctionSystem +pub struct ExclusiveFunctionSystem where Param: ExclusiveSystemParam, { @@ -28,18 +28,21 @@ where system_meta: SystemMeta, world_id: Option, // NOTE: PhantomData T> gives this safe Send/Sync impls - marker: PhantomData Marker>, + marker: PhantomData (Out, Marker)>, } pub struct IsExclusiveFunctionSystem; -impl IntoSystem<(), (), (IsExclusiveFunctionSystem, Param, Marker)> for F +impl IntoSystem + for F where + In: 'static, + Out: 'static, Param: ExclusiveSystemParam + 'static, Marker: 'static, - F: ExclusiveSystemParamFunction + Send + Sync + 'static, + F: ExclusiveSystemParamFunction + Send + Sync + 'static, { - type System = ExclusiveFunctionSystem; + type System = ExclusiveFunctionSystem; fn into_system(func: Self) -> Self::System { ExclusiveFunctionSystem { func, @@ -53,20 +56,27 @@ where const PARAM_MESSAGE: &str = "System's param_state was not found. Did you forget to initialize this system before running it?"; -impl System for ExclusiveFunctionSystem +impl System for ExclusiveFunctionSystem where + In: 'static, + Out: 'static, Param: ExclusiveSystemParam + 'static, Marker: 'static, - F: ExclusiveSystemParamFunction + Send + Sync + 'static, + F: ExclusiveSystemParamFunction + Send + Sync + 'static, { - type In = (); - type Out = (); + type In = In; + type Out = Out; #[inline] fn name(&self) -> Cow<'static, str> { self.system_meta.name.clone() } + #[inline] + fn type_id(&self) -> TypeId { + TypeId::of::() + } + #[inline] fn component_access(&self) -> &Access { self.system_meta.component_access_set.combined_access() @@ -90,20 +100,22 @@ where panic!("Cannot run exclusive systems with a shared World reference"); } - fn run(&mut self, _input: Self::In, world: &mut World) -> Self::Out { + fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out { let saved_last_tick = world.last_change_tick; world.last_change_tick = self.system_meta.last_change_tick; - let params = ::State::get_param( + let params = Param::get_param( self.param_state.as_mut().expect(PARAM_MESSAGE), &self.system_meta, ); - self.func.run(world, params); + let out = self.func.run(world, input, params); let change_tick = world.change_tick.get_mut(); self.system_meta.last_change_tick = *change_tick; - *change_tick += 1; + *change_tick = change_tick.wrapping_add(1); world.last_change_tick = saved_last_tick; + + out } #[inline] @@ -122,17 +134,14 @@ where #[inline] fn apply_buffers(&mut self, world: &mut World) { let param_state = self.param_state.as_mut().expect(PARAM_MESSAGE); - param_state.apply(world); + Param::apply(param_state, world); } #[inline] fn initialize(&mut self, world: &mut World) { self.world_id = Some(world.id()); self.system_meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); - self.param_state = Some(::init( - world, - &mut self.system_meta, - )); + self.param_state = Some(Param::init(world, &mut self.system_meta)); } fn update_archetype_component_access(&mut self, _world: &World) {} @@ -145,13 +154,22 @@ where self.system_meta.name.as_ref(), ); } + fn default_labels(&self) -> Vec { vec![self.func.as_system_label().as_label()] } + + fn default_system_sets(&self) -> Vec> { + let set = crate::schedule_v3::SystemTypeSet::::new(); + vec![Box::new(set)] + } } -impl> - AsSystemLabel<(Param, Marker, IsExclusiveFunctionSystem)> for T +impl AsSystemLabel<(In, Out, Param, Marker, IsExclusiveFunctionSystem)> + for T +where + Param: ExclusiveSystemParam, + T: ExclusiveSystemParamFunction, { #[inline] fn as_system_label(&self) -> SystemLabelId { @@ -163,38 +181,70 @@ impl: +pub trait ExclusiveSystemParamFunction: Send + Sync + 'static { - fn run(&mut self, world: &mut World, param_value: ExclusiveSystemParamItem); + fn run( + &mut self, + world: &mut World, + input: In, + param_value: ExclusiveSystemParamItem, + ) -> Out; } macro_rules! impl_exclusive_system_function { ($($param: ident),*) => { #[allow(non_snake_case)] - impl ExclusiveSystemParamFunction<($($param,)*), ()> for Func + impl ExclusiveSystemParamFunction<(), Out, ($($param,)*), ()> for Func where for <'a> &'a mut Func: - FnMut(&mut World, $($param),*) + - FnMut(&mut World, $(ExclusiveSystemParamItem<$param>),*) + FnMut(&mut World, $($param),*) -> Out + + FnMut(&mut World, $(ExclusiveSystemParamItem<$param>),*) -> Out, + Out: 'static, { #[inline] - fn run(&mut self, world: &mut World, param_value: ExclusiveSystemParamItem< ($($param,)*)>) { + fn run(&mut self, world: &mut World, _in: (), param_value: ExclusiveSystemParamItem< ($($param,)*)>) -> Out { // Yes, this is strange, but `rustc` fails to compile this impl // without using this function. It fails to recognise that `func` // is a function, potentially because of the multiple impls of `FnMut` #[allow(clippy::too_many_arguments)] - fn call_inner<$($param,)*>( - mut f: impl FnMut(&mut World, $($param,)*), + fn call_inner( + mut f: impl FnMut(&mut World, $($param,)*) -> Out, world: &mut World, $($param: $param,)* - ) { + ) -> Out { f(world, $($param,)*) } let ($($param,)*) = param_value; call_inner(self, world, $($param),*) } } + #[allow(non_snake_case)] + impl ExclusiveSystemParamFunction for Func + where + for <'a> &'a mut Func: + FnMut(In, &mut World, $($param),*) -> Out + + FnMut(In, &mut World, $(ExclusiveSystemParamItem<$param>),*) -> Out, + Out: 'static, + { + #[inline] + fn run(&mut self, world: &mut World, input: Input, param_value: ExclusiveSystemParamItem< ($($param,)*)>) -> Out { + // Yes, this is strange, but `rustc` fails to compile this impl + // without using this function. It fails to recognise that `func` + // is a function, potentially because of the multiple impls of `FnMut` + #[allow(clippy::too_many_arguments)] + fn call_inner( + mut f: impl FnMut(In, &mut World, $($param,)*) -> Out, + input: Input, + world: &mut World, + $($param: $param,)* + ) -> Out { + f(In(input), world, $($param,)*) + } + let ($($param,)*) = param_value; + call_inner(self, input, world, $($param),*) + } + } }; } // Note that we rely on the highest impl to be <= the highest order of the tuple impls diff --git a/crates/bevy_ecs/src/system/exclusive_system_param.rs b/crates/bevy_ecs/src/system/exclusive_system_param.rs index 6c52738901254..7f5b25f77fba3 100644 --- a/crates/bevy_ecs/src/system/exclusive_system_param.rs +++ b/crates/bevy_ecs/src/system/exclusive_system_param.rs @@ -1,108 +1,89 @@ use crate::{ prelude::{FromWorld, QueryState}, query::{ReadOnlyWorldQuery, WorldQuery}, - system::{Local, LocalState, SystemMeta, SystemParam, SystemState}, + system::{Local, SystemMeta, SystemParam, SystemState}, world::World, }; use bevy_ecs_macros::all_tuples; use bevy_utils::synccell::SyncCell; pub trait ExclusiveSystemParam: Sized { - type State: ExclusiveSystemParamState; -} - -pub type ExclusiveSystemParamItem<'s, P> = - <

::State as ExclusiveSystemParamState>::Item<'s>; - -/// The state of a [`SystemParam`]. -pub trait ExclusiveSystemParamState: Send + Sync + 'static { - type Item<'s>: ExclusiveSystemParam; + type State: Send + Sync + 'static; + type Item<'s>: ExclusiveSystemParam; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self; + fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; #[inline] - fn apply(&mut self, _world: &mut World) {} + fn apply(_state: &mut Self::State, _world: &mut World) {} - fn get_param<'s>(state: &'s mut Self, system_meta: &SystemMeta) -> Self::Item<'s>; + fn get_param<'s>(state: &'s mut Self::State, system_meta: &SystemMeta) -> Self::Item<'s>; } +pub type ExclusiveSystemParamItem<'s, P> =

::Item<'s>; + impl<'a, Q: WorldQuery + 'static, F: ReadOnlyWorldQuery + 'static> ExclusiveSystemParam for &'a mut QueryState { type State = QueryState; -} - -impl ExclusiveSystemParamState - for QueryState -{ type Item<'s> = &'s mut QueryState; - fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self { + fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { QueryState::new(world) } - fn get_param<'s>(state: &'s mut Self, _system_meta: &SystemMeta) -> Self::Item<'s> { + fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { state } } impl<'a, P: SystemParam + 'static> ExclusiveSystemParam for &'a mut SystemState

{ type State = SystemState

; -} - -impl ExclusiveSystemParamState for SystemState

{ type Item<'s> = &'s mut SystemState

; - fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self { + fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { SystemState::new(world) } - fn get_param<'s>(state: &'s mut Self, _system_meta: &SystemMeta) -> Self::Item<'s> { + fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { state } } -impl<'s, T: FromWorld + Send + Sync + 'static> ExclusiveSystemParam for Local<'s, T> { - type State = LocalState; -} - -impl ExclusiveSystemParamState for LocalState { +impl<'_s, T: FromWorld + Send + 'static> ExclusiveSystemParam for Local<'_s, T> { + type State = SyncCell; type Item<'s> = Local<'s, T>; - fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self(SyncCell::new(T::from_world(world))) + fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + SyncCell::new(T::from_world(world)) } - fn get_param<'s>(state: &'s mut Self, _system_meta: &SystemMeta) -> Self::Item<'s> { - Local(state.0.get()) + fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { + Local(state.get()) } } macro_rules! impl_exclusive_system_param_tuple { ($($param: ident),*) => { - impl<$($param: ExclusiveSystemParam),*> ExclusiveSystemParam for ($($param,)*) { - type State = ($($param::State,)*); - } - #[allow(unused_variables)] #[allow(non_snake_case)] - impl<$($param: ExclusiveSystemParamState),*> ExclusiveSystemParamState for ($($param,)*) { + impl<$($param: ExclusiveSystemParam),*> ExclusiveSystemParam for ($($param,)*) { + type State = ($($param::State,)*); type Item<'s> = ($($param::Item<'s>,)*); #[inline] - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { + fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { (($($param::init(_world, _system_meta),)*)) } #[inline] - fn apply(&mut self, _world: &mut World) { - let ($($param,)*) = self; - $($param.apply(_world);)* + fn apply(state: &mut Self::State, _world: &mut World) { + let ($($param,)*) = state; + $($param::apply($param, _world);)* } #[inline] #[allow(clippy::unused_unit)] fn get_param<'s>( - state: &'s mut Self, + state: &'s mut Self::State, system_meta: &SystemMeta, ) -> Self::Item<'s> { @@ -110,7 +91,6 @@ macro_rules! impl_exclusive_system_param_tuple { ($($param::get_param($param, system_meta),)*) } } - }; } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 2cc88f9e30b77..7493f6d3a1584 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -5,14 +5,11 @@ use crate::{ prelude::FromWorld, query::{Access, FilteredAccessSet}, schedule::{SystemLabel, SystemLabelId}, - system::{ - check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem, - SystemParamState, - }, + system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem}, world::{World, WorldId}, }; use bevy_ecs_macros::all_tuples; -use std::{borrow::Cow, fmt::Debug, marker::PhantomData}; +use std::{any::TypeId, borrow::Cow, fmt::Debug, marker::PhantomData}; /// The metadata of a [`System`]. #[derive(Clone)] @@ -109,6 +106,9 @@ impl SystemMeta { /// // Use system_state.get_mut(&mut world) and unpack your system parameters into variables! /// // system_state.get(&world) provides read-only versions of your system parameters instead. /// let (event_writer, maybe_resource, query) = system_state.get_mut(&mut world); +/// +/// // If you are using [`Commands`], you can choose when you want to apply them to the world. +/// // You need to manually call `.apply(world)` on the [`SystemState`] to apply them. /// ``` /// Caching: /// ```rust @@ -141,7 +141,7 @@ impl SystemMeta { /// ``` pub struct SystemState { meta: SystemMeta, - param_state: ::State, + param_state: Param::State, world_id: WorldId, archetype_generation: ArchetypeGeneration, } @@ -150,7 +150,7 @@ impl SystemState { pub fn new(world: &mut World) -> Self { let mut meta = SystemMeta::new::(); meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); - let param_state = ::init(world, &mut meta); + let param_state = Param::init_state(world, &mut meta); Self { meta, param_state, @@ -170,7 +170,8 @@ impl SystemState { where Param: ReadOnlySystemParam, { - self.validate_world_and_update_archetypes(world); + self.validate_world(world); + self.update_archetypes(world); // SAFETY: Param is read-only and doesn't allow mutable access to World. It also matches the World this SystemState was created with. unsafe { self.get_unchecked_manual(world) } } @@ -178,7 +179,8 @@ impl SystemState { /// Retrieve the mutable [`SystemParam`] values. #[inline] pub fn get_mut<'w, 's>(&'s mut self, world: &'w mut World) -> SystemParamItem<'w, 's, Param> { - self.validate_world_and_update_archetypes(world); + self.validate_world(world); + self.update_archetypes(world); // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. unsafe { self.get_unchecked_manual(world) } } @@ -188,7 +190,7 @@ impl SystemState { /// This function should be called manually after the values returned by [`SystemState::get`] and [`SystemState::get_mut`] /// are finished being used. pub fn apply(&mut self, world: &mut World) { - self.param_state.apply(&self.meta, world); + Param::apply(&mut self.param_state, &self.meta, world); } #[inline] @@ -196,21 +198,71 @@ impl SystemState { self.world_id == world.id() } - fn validate_world_and_update_archetypes(&mut self, world: &World) { + /// Asserts that the [`SystemState`] matches the provided [`World`]. + #[inline] + fn validate_world(&self, world: &World) { assert!(self.matches_world(world), "Encountered a mismatched World. A SystemState cannot be used with Worlds other than the one it was created with."); + } + + /// Updates the state's internal view of the `world`'s archetypes. If this is not called before fetching the parameters, + /// the results may not accurately reflect what is in the `world`. + /// + /// This is only required if [`SystemState::get_manual`] or [`SystemState::get_manual_mut`] is being called, and it only needs to + /// be called if the `world` has been structurally mutated (i.e. added/removed a component or resource). Users using + /// [`SystemState::get`] or [`SystemState::get_mut`] do not need to call this as it will be automatically called for them. + #[inline] + pub fn update_archetypes(&mut self, world: &World) { let archetypes = world.archetypes(); let new_generation = archetypes.generation(); let old_generation = std::mem::replace(&mut self.archetype_generation, new_generation); let archetype_index_range = old_generation.value()..new_generation.value(); for archetype_index in archetype_index_range { - self.param_state.new_archetype( + Param::new_archetype( + &mut self.param_state, &archetypes[ArchetypeId::new(archetype_index)], &mut self.meta, ); } } + /// Retrieve the [`SystemParam`] values. This can only be called when all parameters are read-only. + /// This will not update the state's view of the world's archetypes automatically nor increment the + /// world's change tick. + /// + /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this + /// function. + /// + /// Users should strongly prefer to use [`SystemState::get`] over this function. + #[inline] + pub fn get_manual<'w, 's>(&'s mut self, world: &'w World) -> SystemParamItem<'w, 's, Param> + where + Param: ReadOnlySystemParam, + { + self.validate_world(world); + let change_tick = world.read_change_tick(); + // SAFETY: Param is read-only and doesn't allow mutable access to World. It also matches the World this SystemState was created with. + unsafe { self.fetch(world, change_tick) } + } + + /// Retrieve the mutable [`SystemParam`] values. This will not update the state's view of the world's archetypes + /// automatically nor increment the world's change tick. + /// + /// For this to return accurate results, ensure [`SystemState::update_archetypes`] is called before this + /// function. + /// + /// Users should strongly prefer to use [`SystemState::get_mut`] over this function. + #[inline] + pub fn get_manual_mut<'w, 's>( + &'s mut self, + world: &'w mut World, + ) -> SystemParamItem<'w, 's, Param> { + self.validate_world(world); + let change_tick = world.change_tick(); + // SAFETY: World is uniquely borrowed and matches the World this SystemState was created with. + unsafe { self.fetch(world, change_tick) } + } + /// Retrieve the [`SystemParam`] values. This will not update archetypes automatically. /// /// # Safety @@ -223,12 +275,20 @@ impl SystemState { world: &'w World, ) -> SystemParamItem<'w, 's, Param> { let change_tick = world.increment_change_tick(); - let param = ::get_param( - &mut self.param_state, - &self.meta, - world, - change_tick, - ); + self.fetch(world, change_tick) + } + + /// # Safety + /// This call might access any of the input parameters in a way that violates Rust's mutability rules. Make sure the data + /// access is safe in the context of global [`World`] access. The passed-in [`World`] _must_ be the [`World`] the [`SystemState`] was + /// created with. + #[inline] + unsafe fn fetch<'w, 's>( + &'s mut self, + world: &'w World, + change_tick: u32, + ) -> SystemParamItem<'w, 's, Param> { + let param = Param::get_param(&mut self.param_state, &self.meta, world, change_tick); self.meta.last_change_tick = change_tick; param } @@ -320,7 +380,7 @@ where world_id: Option, archetype_generation: ArchetypeGeneration, // NOTE: PhantomData T> gives this safe Send/Sync impls - marker: PhantomData (In, Out, Marker)>, + marker: PhantomData (Out, Marker)>, } pub struct IsFunctionSystem; @@ -372,6 +432,11 @@ where self.system_meta.name.clone() } + #[inline] + fn type_id(&self) -> TypeId { + TypeId::of::() + } + #[inline] fn component_access(&self) -> &Access { self.system_meta.component_access_set.combined_access() @@ -400,7 +465,7 @@ where // We update the archetype component access correctly based on `Param`'s requirements // in `update_archetype_component_access`. // Our caller upholds the requirements. - let params = ::State::get_param( + let params = Param::get_param( self.param_state.as_mut().expect(Self::PARAM_MESSAGE), &self.system_meta, world, @@ -422,17 +487,14 @@ where #[inline] fn apply_buffers(&mut self, world: &mut World) { let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE); - param_state.apply(&self.system_meta, world); + Param::apply(param_state, &self.system_meta, world); } #[inline] fn initialize(&mut self, world: &mut World) { self.world_id = Some(world.id()); self.system_meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); - self.param_state = Some(::init( - world, - &mut self.system_meta, - )); + self.param_state = Some(Param::init_state(world, &mut self.system_meta)); } fn update_archetype_component_access(&mut self, world: &World) { @@ -443,7 +505,9 @@ where let archetype_index_range = old_generation.value()..new_generation.value(); for archetype_index in archetype_index_range { - self.param_state.as_mut().unwrap().new_archetype( + let param_state = self.param_state.as_mut().unwrap(); + Param::new_archetype( + param_state, &archetypes[ArchetypeId::new(archetype_index)], &mut self.system_meta, ); @@ -458,9 +522,15 @@ where self.system_meta.name.as_ref(), ); } + fn default_labels(&self) -> Vec { vec![self.func.as_system_label().as_label()] } + + fn default_system_sets(&self) -> Vec> { + let set = crate::schedule_v3::SystemTypeSet::::new(); + vec![Box::new(set)] + } } /// A [`SystemLabel`] that was automatically generated for a system on the basis of its `TypeId`. @@ -514,7 +584,7 @@ impl Copy for SystemTypeIdLabel {} /// pub fn pipe( /// mut a: A, /// mut b: B, -/// ) -> impl FnMut(In, ParamSet<(SystemParamItem, SystemParamItem)>) -> BOut +/// ) -> impl FnMut(In, ParamSet<(AParam, BParam)>) -> BOut /// where /// // We need A and B to be systems, add those bounds /// A: SystemParamFunction, diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index be66144b4a6a8..5fd37e11534b4 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -130,15 +130,14 @@ pub fn assert_is_system>(sys: S) mod tests { use std::any::TypeId; - use crate::prelude::StageLabel; - use crate::{ self as bevy_ecs, archetype::{ArchetypeComponentId, Archetypes}, bundle::Bundles, + change_detection::DetectChanges, component::{Component, Components}, entity::{Entities, Entity}, - prelude::AnyOf, + prelude::{AnyOf, StageLabel}, query::{Added, Changed, Or, With, Without}, schedule::{Schedule, Stage, SystemStage}, system::{ @@ -1202,4 +1201,15 @@ mod tests { ); }); } + + #[test] + #[should_panic = "Attempted to use bevy_ecs::query::state::QueryState<()> with a mismatched World."] + fn query_validates_world_id() { + let mut world1 = World::new(); + let world2 = World::new(); + let qstate = world1.query::<()>(); + // SAFETY: doesnt access anything + let query = unsafe { Query::new(&world2, &qstate, 0, 0, false) }; + query.iter(); + } } diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 8f7e4f70bc303..a86d40e62b038 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -2,8 +2,8 @@ use crate::{ component::Component, entity::Entity, query::{ - QueryCombinationIter, QueryEntityError, QueryIter, QueryManyIter, QuerySingleError, - QueryState, ROQueryItem, ReadOnlyWorldQuery, WorldQuery, + BatchingStrategy, QueryCombinationIter, QueryEntityError, QueryIter, QueryManyIter, + QueryParIter, QuerySingleError, QueryState, ROQueryItem, ReadOnlyWorldQuery, WorldQuery, }, world::{Mut, World}, }; @@ -188,7 +188,7 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug}; /// |Query methods|Effect| /// |:---:|---| /// |[`iter`]\([`_mut`][`iter_mut`])|Returns an iterator over all query items.| -/// |[`for_each`]\([`_mut`][`for_each_mut`]),
[`par_for_each`]\([`_mut`][`par_for_each_mut`])|Runs a specified function for each query item.| +/// |[`for_each`]\([`_mut`][`for_each_mut`]),
[`par_iter`]\([`_mut`][`par_iter_mut`])|Runs a specified function for each query item.| /// |[`iter_many`]\([`_mut`][`iter_many_mut`])|Iterates or runs a specified function over query items generated by a list of entities.| /// |[`iter_combinations`]\([`_mut`][`iter_combinations_mut`])|Returns an iterator over all combinations of a specified number of query items.| /// |[`get`]\([`_mut`][`get_mut`])|Returns the query item for the specified entity.| @@ -224,7 +224,7 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug}; /// |Query operation|Computational complexity| /// |:---:|:---:| /// |[`iter`]\([`_mut`][`iter_mut`])|O(n)| -/// |[`for_each`]\([`_mut`][`for_each_mut`]),
[`par_for_each`]\([`_mut`][`par_for_each_mut`])|O(n)| +/// |[`for_each`]\([`_mut`][`for_each_mut`]),
[`par_iter`]\([`_mut`][`par_iter_mut`])|O(n)| /// |[`iter_many`]\([`_mut`][`iter_many_mut`])|O(k)| /// |[`iter_combinations`]\([`_mut`][`iter_combinations_mut`])|O(nCr)| /// |[`get`]\([`_mut`][`get_mut`])|O(1)| @@ -263,8 +263,8 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug}; /// [`many`]: Self::many /// [`many_mut`]: Self::many_mut /// [`Or`]: crate::query::Or -/// [`par_for_each`]: Self::par_for_each -/// [`par_for_each_mut`]: Self::par_for_each_mut +/// [`par_iter`]: Self::par_iter +/// [`par_iter_mut`]: Self::par_iter_mut /// [performance]: #performance /// [`single`]: Self::single /// [`single_mut`]: Self::single_mut @@ -274,16 +274,16 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug}; /// [`With`]: crate::query::With /// [`Without`]: crate::query::Without pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> { - pub(crate) world: &'world World, - pub(crate) state: &'state QueryState, - pub(crate) last_change_tick: u32, - pub(crate) change_tick: u32, + world: &'world World, + state: &'state QueryState, + last_change_tick: u32, + change_tick: u32, // SAFETY: This is used to ensure that `get_component_mut::` properly fails when a Query writes C // and gets converted to a read-only query using `to_readonly`. Without checking this, `get_component_mut` relies on // QueryState's archetype_component_access, which will continue allowing write access to C after being cast to // the read-only variant. This whole situation is confusing and error prone. Ideally this is a temporary hack // until we sort out a cleaner alternative. - pub(crate) force_read_only_component_access: bool, + force_read_only_component_access: bool, } impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> std::fmt::Debug for Query<'w, 's, Q, F> { @@ -295,6 +295,10 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> std::fmt::Debug for Query<'w, impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { /// Creates a new query. /// + /// # Panics + /// + /// This will panic if the world used to create `state` is not `world`. + /// /// # Safety /// /// This will create a query that could violate memory safety rules. Make sure that this is only @@ -305,9 +309,12 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { state: &'s QueryState, last_change_tick: u32, change_tick: u32, + force_read_only_component_access: bool, ) -> Self { + state.validate_world(world); + Self { - force_read_only_component_access: false, + force_read_only_component_access, world, state, last_change_tick, @@ -323,14 +330,16 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { pub fn to_readonly(&self) -> Query<'_, 's, Q::ReadOnly, F::ReadOnly> { let new_state = self.state.as_readonly(); // SAFETY: This is memory safe because it turns the query immutable. - Query { - // SAFETY: this must be set to true or `get_component_mut` will be unsound. See the comments - // on this field for more details - force_read_only_component_access: true, - world: self.world, - state: new_state, - last_change_tick: self.last_change_tick, - change_tick: self.change_tick, + unsafe { + Query::new( + self.world, + new_state, + self.last_change_tick, + self.change_tick, + // SAFETY: this must be set to true or `get_component_mut` will be unsound. See the comments + // on this field for more details + true, + ) } } @@ -725,80 +734,32 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { }; } - /// Runs `f` on each read-only query item in parallel. - /// - /// Parallelization is achieved by using the [`World`]'s [`ComputeTaskPool`]. - /// - /// # Tasks and batch size + /// Returns a parallel iterator over the query results for the given [`World`]. /// - /// The items in the query get sorted into batches. - /// Internally, this function spawns a group of futures that each take on a `batch_size` sized section of the items (or less if the division is not perfect). - /// Then, the tasks in the [`ComputeTaskPool`] work through these futures. + /// This can only be called for read-only queries, see [`par_iter_mut`] for write-queries. /// - /// You can use this value to tune between maximum multithreading ability (many small batches) and minimum parallelization overhead (few big batches). - /// Rule of thumb: If the function body is (mostly) computationally expensive but there are not many items, a small batch size (=more batches) may help to even out the load. - /// If the body is computationally cheap and you have many items, a large batch size (=fewer batches) avoids spawning additional futures that don't help to even out the load. - /// - /// [`ComputeTaskPool`]: bevy_tasks::prelude::ComputeTaskPool - /// - /// # Panics - /// - /// This method panics if the [`ComputeTaskPool`] resource is added to the `World` before using this method. - /// If using this from a query that is being initialized and run from the [`Schedule`](crate::schedule::Schedule), this never panics. - /// - /// # See also - /// - /// - [`par_for_each_mut`](Self::par_for_each_mut) for operating on mutable query items. + /// [`par_iter_mut`]: Self::par_iter_mut #[inline] - pub fn par_for_each<'this>( - &'this self, - batch_size: usize, - f: impl Fn(ROQueryItem<'this, Q>) + Send + Sync + Clone, - ) { - // SAFETY: system runs without conflicts with other systems. same-system queries have runtime - // borrow checks when they conflict - unsafe { - self.state.as_readonly().par_for_each_unchecked_manual( - self.world, - batch_size, - f, - self.last_change_tick, - self.change_tick, - ); - }; + pub fn par_iter(&mut self) -> QueryParIter<'_, '_, Q::ReadOnly, F::ReadOnly> { + QueryParIter { + world: self.world, + state: self.state.as_readonly(), + batching_strategy: BatchingStrategy::new(), + } } - /// Runs `f` on each read-only query item in parallel. + /// Returns a parallel iterator over the query results for the given [`World`]. /// - /// Parallelization is achieved by using the [`World`]'s [`ComputeTaskPool`]. - /// - /// # Panics - /// - /// This method panics if the [`ComputeTaskPool`] resource is added to the `World` before using this method. - /// If using this from a query that is being initialized and run from the [`Schedule`](crate::schedule::Schedule), this never panics. - /// - /// [`ComputeTaskPool`]: bevy_tasks::prelude::ComputeTaskPool - /// - /// # See also + /// This can only be called for mutable queries, see [`par_iter`] for read-only-queries. /// - /// - [`par_for_each`](Self::par_for_each) for more usage details. + /// [`par_iter`]: Self::par_iter #[inline] - pub fn par_for_each_mut<'a>( - &'a mut self, - batch_size: usize, - f: impl Fn(Q::Item<'a>) + Send + Sync + Clone, - ) { - // SAFETY: system runs without conflicts with other systems. same-system queries have runtime - // borrow checks when they conflict - unsafe { - self.state.par_for_each_unchecked_manual( - self.world, - batch_size, - f, - self.last_change_tick, - self.change_tick, - ); - }; + pub fn par_iter_mut(&mut self) -> QueryParIter<'_, '_, Q, F> { + QueryParIter { + world: self.world, + state: self.state, + batching_strategy: BatchingStrategy::new(), + } } /// Returns the read-only query item for the given [`Entity`]. diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 43fcdb41dd3cb..a6034f0d53829 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -5,6 +5,8 @@ use crate::{ archetype::ArchetypeComponentId, change_detection::MAX_CHANGE_AGE, component::ComponentId, query::Access, schedule::SystemLabelId, world::World, }; + +use std::any::TypeId; use std::borrow::Cow; /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) @@ -26,6 +28,8 @@ pub trait System: Send + Sync + 'static { type Out; /// Returns the system's name. fn name(&self) -> Cow<'static, str>; + /// Returns the [`TypeId`] of the underlying system type. + fn type_id(&self) -> TypeId; /// Returns the system's component [`Access`]. fn component_access(&self) -> &Access; /// Returns the system's archetype component [`Access`]. @@ -64,6 +68,10 @@ pub trait System: Send + Sync + 'static { fn default_labels(&self) -> Vec { Vec::new() } + /// Returns the system's default [system sets](crate::schedule_v3::SystemSet). + fn default_system_sets(&self) -> Vec> { + Vec::new() + } /// Gets the system's last change tick fn get_last_change_tick(&self) -> u32; /// Sets the system's last change tick diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 7b09919698a33..4c1288ceb92de 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -1,9 +1,9 @@ -pub use crate::change_detection::{NonSendMut, ResMut}; +pub use crate::change_detection::{NonSendMut, Res, ResMut}; use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, - change_detection::Ticks, - component::{Component, ComponentId, ComponentTicks, Components, Tick}, + change_detection::{Ticks, TicksMut}, + component::{Component, ComponentId, ComponentTicks, Components}, entity::{Entities, Entity}, query::{ Access, FilteredAccess, FilteredAccessSet, QueryState, ReadOnlyWorldQuery, WorldQuery, @@ -72,7 +72,7 @@ use std::{ /// /// ```text /// expected ... [ParamType] -/// found associated type `<<[ParamType] as SystemParam>::State as SystemParamState>::Item<'_, '_>` +/// found associated type `<[ParamType] as SystemParam>::Item<'_, '_>` /// ``` /// where `[ParamType]` is the type of one of your fields. /// To solve this error, you can wrap the field of type `[ParamType]` with [`StaticSystemParam`] @@ -81,7 +81,7 @@ use std::{ /// ## Details /// /// The derive macro requires that the [`SystemParam`] implementation of -/// each field `F`'s [`State`](`SystemParam::State`)'s [`Item`](`SystemParamState::Item`) is itself `F` +/// each field `F`'s [`Item`](`SystemParam::Item`)'s is itself `F` /// (ignoring lifetimes for simplicity). /// This assumption is due to type inference reasons, so that the derived [`SystemParam`] can be /// used as an argument to a function system. @@ -121,35 +121,51 @@ use std::{ /// /// [`SyncCell`]: bevy_utils::synccell::SyncCell /// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html -pub trait SystemParam: Sized { - type State: SystemParamState; -} - -pub type SystemParamItem<'w, 's, P> = <

::State as SystemParamState>::Item<'w, 's>; - -/// The state of a [`SystemParam`]. /// /// # Safety /// -/// It is the implementor's responsibility to ensure `system_meta` is populated with the _exact_ -/// [`World`] access used by the [`SystemParamState`]. -/// Additionally, it is the implementor's responsibility to ensure there is no -/// conflicting access across all [`SystemParam`]'s. -pub unsafe trait SystemParamState: Send + Sync + 'static { - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self; +/// The implementor must ensure the following is true. +/// - [`SystemParam::init_state`] correctly registers all [`World`] accesses used +/// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). +/// - None of the world accesses may conflict with any prior accesses registered +/// on `system_meta`. +pub unsafe trait SystemParam: Sized { + /// Used to store data which persists across invocations of a system. + type State: Send + Sync + 'static; + + /// The item type returned when constructing this system param. + /// The value of this associated type should be `Self`, instantiated with new lifetimes. + /// + /// You could think of `SystemParam::Item<'w, 's>` as being an *operation* that changes the lifetimes bound to `Self`. + type Item<'world, 'state>: SystemParam; + + /// Registers any [`World`] access used by this [`SystemParam`] + /// and creates a new instance of this param's [`State`](Self::State). + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; + + /// For the specified [`Archetype`], registers the components accessed by this [`SystemParam`] (if applicable). #[inline] - fn new_archetype(&mut self, _archetype: &Archetype, _system_meta: &mut SystemMeta) {} + fn new_archetype( + _state: &mut Self::State, + _archetype: &Archetype, + _system_meta: &mut SystemMeta, + ) { + } + + /// Applies any deferred mutations stored in this [`SystemParam`]'s state. + /// This is used to apply [`Commands`] at the end of a stage. #[inline] #[allow(unused_variables)] - fn apply(&mut self, system_meta: &SystemMeta, _world: &mut World) {} + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {} - type Item<'world, 'state>: SystemParam; /// # Safety /// - /// This call might access any of the input parameters in an unsafe way. Make sure the data - /// access is safe in the context of the system scheduler. + /// This call might use any of the [`World`] accesses that were registered in [`Self::init_state`]. + /// - None of those accesses may conflict with any other [`SystemParam`]s + /// that exist at the same time, including those on other threads. + /// - `world` must be the same `World` that was used to initialize [`state`](SystemParam::init_state). unsafe fn get_param<'world, 'state>( - state: &'state mut Self, + state: &'state mut Self::State, system_meta: &SystemMeta, world: &'world World, change_tick: u32, @@ -159,14 +175,11 @@ pub unsafe trait SystemParamState: Send + Sync + 'static { /// A [`SystemParam`] that only reads a given [`World`]. /// /// # Safety -/// This must only be implemented for [`SystemParam`] impls that exclusively read the World passed in to [`SystemParamState::get_param`] +/// This must only be implemented for [`SystemParam`] impls that exclusively read the World passed in to [`SystemParam::get_param`] pub unsafe trait ReadOnlySystemParam: SystemParam {} -impl<'w, 's, Q: WorldQuery + 'static, F: ReadOnlyWorldQuery + 'static> SystemParam - for Query<'w, 's, Q, F> -{ - type State = QueryState; -} +/// Shorthand way of accessing the associated type [`SystemParam::Item`] for a given [`SystemParam`]. +pub type SystemParamItem<'w, 's, P> =

for SetPrepassViewBindGroup { + type Param = SRes; + type ViewWorldQuery = Read; + type ItemWorldQuery = (); + + #[inline] + fn render<'w>( + _item: &P, + view_uniform_offset: &'_ ViewUniformOffset, + _entity: (), + prepass_view_bind_group: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let prepass_view_bind_group = prepass_view_bind_group.into_inner(); + pass.set_bind_group( + I, + prepass_view_bind_group.bind_group.as_ref().unwrap(), + &[view_uniform_offset.offset], + ); + RenderCommandResult::Success + } +} + +pub type DrawPrepass = ( + SetItemPipeline, + SetPrepassViewBindGroup<0>, + SetMaterialBindGroup, + SetMeshBindGroup<2>, + DrawMesh, +); diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl new file mode 100644 index 0000000000000..d2050675f891a --- /dev/null +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -0,0 +1,81 @@ +#import bevy_pbr::prepass_bindings +#import bevy_pbr::mesh_functions + +// Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can +// pass them to custom prepass shaders like pbr_prepass.wgsl. +struct Vertex { + @location(0) position: vec3, + +#ifdef VERTEX_UVS + @location(1) uv: vec2, +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS + @location(2) normal: vec3, +#ifdef VERTEX_TANGENTS + @location(3) tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS + +#ifdef SKINNED + @location(4) joint_indices: vec4, + @location(5) joint_weights: vec4, +#endif // SKINNED +} + +struct VertexOutput { + @builtin(position) clip_position: vec4, + +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS +} + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + +#ifdef SKINNED + var model = skin_model(vertex.joint_indices, vertex.joint_weights); +#else // SKINNED + var model = mesh.model; +#endif // SKINNED + + out.clip_position = mesh_position_local_to_clip(model, vec4(vertex.position, 1.0)); + +#ifdef VERTEX_UVS + out.uv = vertex.uv; +#endif // VERTEX_UVS + +#ifdef NORMAL_PREPASS +#ifdef SKINNED + out.world_normal = skin_normals(model, vertex.normal); +#else // SKINNED + out.world_normal = mesh_normal_local_to_world(vertex.normal); +#endif // SKINNED + +#ifdef VERTEX_TANGENTS + out.world_tangent = mesh_tangent_local_to_world(model, vertex.tangent); +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS + + return out; +} + +#ifdef NORMAL_PREPASS +struct FragmentInput { + @location(1) world_normal: vec3, +} + +@fragment +fn fragment(in: FragmentInput) -> @location(0) vec4 { + return vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); +} +#endif // NORMAL_PREPASS diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl new file mode 100644 index 0000000000000..cd338af0ed7da --- /dev/null +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -0,0 +1,18 @@ +#define_import_path bevy_pbr::prepass_bindings + +#import bevy_pbr::mesh_view_types +#import bevy_pbr::mesh_types + +@group(0) @binding(0) +var view: View; + +// Material bindings will be in @group(1) + +@group(2) @binding(0) +var mesh: Mesh; + +#ifdef SKINNED +@group(2) @binding(1) +var joint_matrices: SkinnedMesh; +#import bevy_pbr::skinning +#endif diff --git a/crates/bevy_pbr/src/render/clustered_forward.wgsl b/crates/bevy_pbr/src/render/clustered_forward.wgsl index c291aee2ff33d..5939b7d31b2e7 100644 --- a/crates/bevy_pbr/src/render/clustered_forward.wgsl +++ b/crates/bevy_pbr/src/render/clustered_forward.wgsl @@ -10,7 +10,7 @@ fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { // NOTE: had to use -view_z to make it positive else log(negative) is nan z_slice = u32(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w + 1.0); } - // NOTE: We use min as we may limit the far z plane used for clustering to be closeer than + // NOTE: We use min as we may limit the far z plane used for clustering to be closer than // the furthest thing being drawn. This means that we need to limit to the maximum cluster. return min(z_slice, lights.cluster_dimensions.z - 1u); } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 9cd2bf0bbd83c..657534c30c220 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -213,7 +213,7 @@ pub const MAX_UNIFORM_BUFFER_POINT_LIGHTS: usize = 256; pub const MAX_DIRECTIONAL_LIGHTS: usize = 10; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; -#[derive(Resource)] +#[derive(Resource, Clone)] pub struct ShadowPipeline { pub view_layout: BindGroupLayout, pub mesh_layout: BindGroupLayout, @@ -1190,7 +1190,7 @@ pub fn prepare_lights( .spawn(( ShadowView { depth_texture_view, - pass_name: format!("shadow pass directional light {}", light_index), + pass_name: format!("shadow pass directional light {light_index}"), }, ExtractedView { viewport: UVec4::new( @@ -1626,7 +1626,7 @@ pub fn queue_shadows( casting_meshes: Query<&Handle, Without>, render_meshes: Res>, mut pipelines: ResMut>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, view_lights: Query<&ViewLightEntities>, mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase)>, point_light_entities: Query<&CubemapVisibleEntities, With>, @@ -1661,7 +1661,7 @@ pub fn queue_shadows( let key = ShadowPipelineKey::from_primitive_topology(mesh.primitive_topology); let pipeline_id = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &shadow_pipeline, key, &mesh.layout, @@ -1770,23 +1770,19 @@ impl Node for ShadowPassNode { continue; } - let pass_descriptor = RenderPassDescriptor { - label: Some(&view_light.pass_name), - color_attachments: &[], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &view_light.depth_texture_view, - depth_ops: Some(Operations { - load: LoadOp::Clear(0.0), - store: true, + let mut render_pass = + render_context.begin_tracked_render_pass(RenderPassDescriptor { + label: Some(&view_light.pass_name), + color_attachments: &[], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &view_light.depth_texture_view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: true, + }), + stencil_ops: None, }), - stencil_ops: None, - }), - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); shadow_phase.render(&mut render_pass, world, view_light_entity); } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 92b65621922ac..d1fb3a3c62162 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -5,6 +5,7 @@ use crate::{ }; use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_core_pipeline::prepass::ViewPrepassTextures; use bevy_ecs::{ prelude::*, query::ROQueryItem, @@ -19,12 +20,14 @@ use bevy_render::{ skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, GpuBufferInfo, Mesh, MeshVertexBufferLayout, }, + prelude::Msaa, render_asset::RenderAssets, render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, DefaultImageSampler, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, + BevyDefault, DefaultImageSampler, FallbackImagesDepth, FallbackImagesMsaa, GpuImage, Image, + ImageSampler, TextureFormatPixelInfo, }, view::{ComputedVisibility, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, Extract, RenderApp, RenderStage, @@ -254,6 +257,7 @@ pub fn extract_skinned_meshes( #[derive(Resource, Clone)] pub struct MeshPipeline { pub view_layout: BindGroupLayout, + pub view_layout_multisampled: BindGroupLayout, pub mesh_layout: BindGroupLayout, pub skinned_mesh_layout: BindGroupLayout, // This dummy white texture is to be used in place of optional StandardMaterial textures @@ -272,8 +276,12 @@ impl FromWorld for MeshPipeline { let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { - entries: &[ + /// Returns the appropriate bind group layout vec based on the parameters + fn layout_entries( + clustered_forward_buffer_binding_type: BufferBindingType, + multisampled: bool, + ) -> Vec { + let mut entries = vec![ // View BindGroupLayoutEntry { binding: 0, @@ -381,6 +389,7 @@ impl FromWorld for MeshPipeline { }, count: None, }, + // Globals BindGroupLayoutEntry { binding: 9, visibility: ShaderStages::VERTEX_FRAGMENT, @@ -391,10 +400,45 @@ impl FromWorld for MeshPipeline { }, count: None, }, - ], + ]; + if cfg!(not(feature = "webgl")) { + // Depth texture + entries.push(BindGroupLayoutEntry { + binding: 10, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled, + sample_type: TextureSampleType::Depth, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }); + // Normal texture + entries.push(BindGroupLayoutEntry { + binding: 11, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Texture { + multisampled, + sample_type: TextureSampleType::Float { filterable: true }, + view_dimension: TextureViewDimension::D2, + }, + count: None, + }); + } + entries + } + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { label: Some("mesh_view_layout"), + entries: &layout_entries(clustered_forward_buffer_binding_type, false), }); + let view_layout_multisampled = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + label: Some("mesh_view_layout_multisampled"), + entries: &layout_entries(clustered_forward_buffer_binding_type, true), + }); + let mesh_binding = BindGroupLayoutEntry { binding: 0, visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, @@ -480,6 +524,7 @@ impl FromWorld for MeshPipeline { MeshPipeline { view_layout, + view_layout_multisampled, mesh_layout, skinned_mesh_layout, clustered_forward_buffer_binding_type, @@ -512,10 +557,16 @@ bitflags::bitflags! { /// MSAA uses the highest 3 bits for the MSAA log2(sample count) to support up to 128x MSAA. pub struct MeshPipelineKey: u32 { const NONE = 0; - const TRANSPARENT_MAIN_PASS = (1 << 0); - const HDR = (1 << 1); - const TONEMAP_IN_SHADER = (1 << 2); - const DEBAND_DITHER = (1 << 3); + const HDR = (1 << 0); + const TONEMAP_IN_SHADER = (1 << 1); + const DEBAND_DITHER = (1 << 2); + const DEPTH_PREPASS = (1 << 3); + const NORMAL_PREPASS = (1 << 4); + const ALPHA_MASK = (1 << 5); + const BLEND_RESERVED_BITS = Self::BLEND_MASK_BITS << Self::BLEND_SHIFT_BITS; // ← Bitmask reserving bits for the blend state + const BLEND_OPAQUE = (0 << Self::BLEND_SHIFT_BITS); // ← Values are just sequential within the mask, and can range from 0 to 3 + const BLEND_PREMULTIPLIED_ALPHA = (1 << Self::BLEND_SHIFT_BITS); // + const BLEND_MULTIPLY = (2 << Self::BLEND_SHIFT_BITS); // ← We still have room for one more value without adding more bits const MSAA_RESERVED_BITS = Self::MSAA_MASK_BITS << Self::MSAA_SHIFT_BITS; const PRIMITIVE_TOPOLOGY_RESERVED_BITS = Self::PRIMITIVE_TOPOLOGY_MASK_BITS << Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS; } @@ -525,7 +576,11 @@ impl MeshPipelineKey { const MSAA_MASK_BITS: u32 = 0b111; const MSAA_SHIFT_BITS: u32 = 32 - Self::MSAA_MASK_BITS.count_ones(); const PRIMITIVE_TOPOLOGY_MASK_BITS: u32 = 0b111; - const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = Self::MSAA_SHIFT_BITS - 3; + const PRIMITIVE_TOPOLOGY_SHIFT_BITS: u32 = + Self::MSAA_SHIFT_BITS - Self::PRIMITIVE_TOPOLOGY_MASK_BITS.count_ones(); + const BLEND_MASK_BITS: u32 = 0b11; + const BLEND_SHIFT_BITS: u32 = + Self::PRIMITIVE_TOPOLOGY_SHIFT_BITS - Self::BLEND_MASK_BITS.count_ones(); pub fn from_msaa_samples(msaa_samples: u32) -> Self { let msaa_bits = @@ -607,7 +662,14 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); } - let mut bind_group_layout = vec![self.view_layout.clone()]; + let mut bind_group_layout = match key.msaa_samples() { + 1 => vec![self.view_layout.clone()], + _ => { + shader_defs.push("MULTISAMPLED".into()); + vec![self.view_layout_multisampled.clone()] + } + }; + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) { @@ -622,12 +684,30 @@ impl SpecializedMeshPipeline for MeshPipeline { let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; let (label, blend, depth_write_enabled); - if key.contains(MeshPipelineKey::TRANSPARENT_MAIN_PASS) { - label = "transparent_mesh_pipeline".into(); - blend = Some(BlendState::ALPHA_BLENDING); + let pass = key.intersection(MeshPipelineKey::BLEND_RESERVED_BITS); + if pass == MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA { + label = "premultiplied_alpha_mesh_pipeline".into(); + blend = Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_PREMULTIPLIED_ALPHA".into()); // For the transparent pass, fragments that are closer will be alpha blended // but their depth is not written to the depth buffer depth_write_enabled = false; + } else if pass == MeshPipelineKey::BLEND_MULTIPLY { + label = "multiply_mesh_pipeline".into(); + blend = Some(BlendState { + color: BlendComponent { + src_factor: BlendFactor::Dst, + dst_factor: BlendFactor::OneMinusSrcAlpha, + operation: BlendOperation::Add, + }, + alpha: BlendComponent::OVER, + }); + shader_defs.push("PREMULTIPLY_ALPHA".into()); + shader_defs.push("BLEND_MULTIPLY".into()); + // For the multiply pass, fragments that are closer will be alpha blended + // but their depth is not written to the depth buffer + depth_write_enabled = false; } else { label = "opaque_mesh_pipeline".into(); blend = Some(BlendState::REPLACE); @@ -646,9 +726,10 @@ impl SpecializedMeshPipeline for MeshPipeline { } } - let format = match key.contains(MeshPipelineKey::HDR) { - true => ViewTarget::TEXTURE_FORMAT_HDR, - false => TextureFormat::bevy_default(), + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() }; Ok(RenderPipelineDescriptor { @@ -681,7 +762,7 @@ impl SpecializedMeshPipeline for MeshPipeline { depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth32Float, depth_write_enabled, - depth_compare: CompareFunction::Greater, + depth_compare: CompareFunction::GreaterEqual, stencil: StencilState { front: StencilFaceState::IGNORE, back: StencilFaceState::IGNORE, @@ -803,7 +884,15 @@ pub fn queue_mesh_view_bind_groups( light_meta: Res, global_light_meta: Res, view_uniforms: Res, - views: Query<(Entity, &ViewShadowBindings, &ViewClusterBindings)>, + views: Query<( + Entity, + &ViewShadowBindings, + &ViewClusterBindings, + Option<&ViewPrepassTextures>, + )>, + mut fallback_images: FallbackImagesMsaa, + mut fallback_depths: FallbackImagesDepth, + msaa: Res, globals_buffer: Res, ) { if let (Some(view_binding), Some(light_binding), Some(point_light_binding), Some(globals)) = ( @@ -812,58 +901,94 @@ pub fn queue_mesh_view_bind_groups( global_light_meta.gpu_point_lights.binding(), globals_buffer.buffer.binding(), ) { - for (entity, view_shadow_bindings, view_cluster_bindings) in &views { + for (entity, view_shadow_bindings, view_cluster_bindings, prepass_textures) in &views { + let layout = if msaa.samples() > 1 { + &mesh_pipeline.view_layout_multisampled + } else { + &mesh_pipeline.view_layout + }; + + let mut entries = vec![ + BindGroupEntry { + binding: 0, + resource: view_binding.clone(), + }, + BindGroupEntry { + binding: 1, + resource: light_binding.clone(), + }, + BindGroupEntry { + binding: 2, + resource: BindingResource::TextureView( + &view_shadow_bindings.point_light_depth_texture_view, + ), + }, + BindGroupEntry { + binding: 3, + resource: BindingResource::Sampler(&shadow_pipeline.point_light_sampler), + }, + BindGroupEntry { + binding: 4, + resource: BindingResource::TextureView( + &view_shadow_bindings.directional_light_depth_texture_view, + ), + }, + BindGroupEntry { + binding: 5, + resource: BindingResource::Sampler(&shadow_pipeline.directional_light_sampler), + }, + BindGroupEntry { + binding: 6, + resource: point_light_binding.clone(), + }, + BindGroupEntry { + binding: 7, + resource: view_cluster_bindings.light_index_lists_binding().unwrap(), + }, + BindGroupEntry { + binding: 8, + resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(), + }, + BindGroupEntry { + binding: 9, + resource: globals.clone(), + }, + ]; + + // When using WebGL with MSAA, we can't create the fallback textures required by the prepass + // When using WebGL, and MSAA is disabled, we can't bind the textures either + if cfg!(not(feature = "webgl")) { + let depth_view = match prepass_textures.and_then(|x| x.depth.as_ref()) { + Some(texture) => &texture.default_view, + None => { + &fallback_depths + .image_for_samplecount(msaa.samples()) + .texture_view + } + }; + entries.push(BindGroupEntry { + binding: 10, + resource: BindingResource::TextureView(depth_view), + }); + + let normal_view = match prepass_textures.and_then(|x| x.normal.as_ref()) { + Some(texture) => &texture.default_view, + None => { + &fallback_images + .image_for_samplecount(msaa.samples()) + .texture_view + } + }; + entries.push(BindGroupEntry { + binding: 11, + resource: BindingResource::TextureView(normal_view), + }); + } + let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: view_binding.clone(), - }, - BindGroupEntry { - binding: 1, - resource: light_binding.clone(), - }, - BindGroupEntry { - binding: 2, - resource: BindingResource::TextureView( - &view_shadow_bindings.point_light_depth_texture_view, - ), - }, - BindGroupEntry { - binding: 3, - resource: BindingResource::Sampler(&shadow_pipeline.point_light_sampler), - }, - BindGroupEntry { - binding: 4, - resource: BindingResource::TextureView( - &view_shadow_bindings.directional_light_depth_texture_view, - ), - }, - BindGroupEntry { - binding: 5, - resource: BindingResource::Sampler( - &shadow_pipeline.directional_light_sampler, - ), - }, - BindGroupEntry { - binding: 6, - resource: point_light_binding.clone(), - }, - BindGroupEntry { - binding: 7, - resource: view_cluster_bindings.light_index_lists_binding().unwrap(), - }, - BindGroupEntry { - binding: 8, - resource: view_cluster_bindings.offsets_and_counts_binding().unwrap(), - }, - BindGroupEntry { - binding: 9, - resource: globals.clone(), - }, - ], + entries: &entries, label: Some("mesh_view_bind_group"), - layout: &mesh_pipeline.view_layout, + layout, }); commands.entity(entity).insert(MeshViewBindGroup { diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index cfe9ae87ef6a3..999d78152c2c1 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -43,3 +43,15 @@ var cluster_offsets_and_counts: ClusterOffsetsAndCounts; @group(0) @binding(9) var globals: Globals; + +#ifdef MULTISAMPLED +@group(0) @binding(10) +var depth_prepass_texture: texture_depth_multisampled_2d; +@group(0) @binding(11) +var normal_prepass_texture: texture_multisampled_2d; +#else +@group(0) @binding(10) +var depth_prepass_texture: texture_depth_2d; +@group(0) @binding(11) +var normal_prepass_texture: texture_2d; +#endif \ No newline at end of file diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index cfe825190d2a5..3f5774551535a 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -106,6 +106,9 @@ fn fragment(in: FragmentInput) -> @location(0) vec4 { // SRGB; the GPU will assume our output is linear and will apply an SRGB conversion. output_rgb = pow(output_rgb, vec3(2.2)); output_color = vec4(output_rgb, output_color.a); +#endif +#ifdef PREMULTIPLY_ALPHA + output_color = premultiply_alpha(material.flags, output_color); #endif return output_color; } diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index f851862f250c6..bb638e6ea2979 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -5,13 +5,14 @@ #endif -fn alpha_discard(material: StandardMaterial, output_color: vec4) -> vec4{ +fn alpha_discard(material: StandardMaterial, output_color: vec4) -> vec4 { var color = output_color; - if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE) != 0u) { + let alpha_mode = material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { // NOTE: If rendering as opaque, alpha should be ignored so set to 1.0 color.a = 1.0; - } else if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) { - if (color.a >= material.alpha_cutoff) { + } else if alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK { + if color.a >= material.alpha_cutoff { // NOTE: If rendering as masked alpha and >= the cutoff, render as fully opaque color.a = 1.0; } else { @@ -75,7 +76,7 @@ fn apply_normal_mapping( #ifdef STANDARDMATERIAL_NORMAL_MAP // Nt is the tangent-space normal. var Nt = textureSample(normal_map_texture, normal_map_sampler, uv).rgb; - if ((standard_material_flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u) { + if (standard_material_flags & STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP) != 0u { // Only use the xy components and derive z for 2-component normal maps. Nt = vec3(Nt.rg * 2.0 - 1.0, 0.0); Nt.z = sqrt(1.0 - Nt.x * Nt.x - Nt.y * Nt.y); @@ -83,7 +84,7 @@ fn apply_normal_mapping( Nt = Nt * 2.0 - 1.0; } // Normal maps authored for DirectX require flipping the y component - if ((standard_material_flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u) { + if (standard_material_flags & STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y) != 0u { Nt.y = -Nt.y; } // NOTE: The mikktspace method of normal mapping applies maps the tangent-space normal from @@ -106,7 +107,7 @@ fn calculate_view( is_orthographic: bool, ) -> vec3 { var V: vec3; - if (is_orthographic) { + if is_orthographic { // Orthographic view vector V = normalize(vec3(view.view_proj[0].z, view.view_proj[1].z, view.view_proj[2].z)); } else { @@ -151,6 +152,7 @@ fn pbr_input_new() -> PbrInput { return pbr_input; } +#ifndef NORMAL_PREPASS fn pbr( in: PbrInput, ) -> vec4 { @@ -232,10 +234,11 @@ fn pbr( let specular_ambient = EnvBRDFApprox(F0, perceptual_roughness, NdotV); output_color = vec4( - light_accum + - (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + - emissive.rgb * output_color.a, - output_color.a); + light_accum + + (diffuse_ambient + specular_ambient) * lights.ambient_color.rgb * occlusion + + emissive.rgb * output_color.a, + output_color.a + ); output_color = cluster_debug_visualization( output_color, @@ -247,6 +250,7 @@ fn pbr( return output_color; } +#endif // NORMAL_PREPASS #ifdef TONEMAP_IN_SHADER fn tone_mapping(in: vec4) -> vec4 { @@ -257,11 +261,74 @@ fn tone_mapping(in: vec4) -> vec4 { // Not needed with sRGB buffer // output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2)); } -#endif +#endif // TONEMAP_IN_SHADER #ifdef DEBAND_DITHER fn dither(color: vec4, pos: vec2) -> vec4 { return vec4(color.rgb + screen_space_dither(pos.xy), color.a); } +#endif // DEBAND_DITHER + +#ifdef PREMULTIPLY_ALPHA +fn premultiply_alpha(standard_material_flags: u32, color: vec4) -> vec4 { +// `Blend`, `Premultiplied` and `Alpha` all share the same `BlendState`. Depending +// on the alpha mode, we premultiply the color channels by the alpha channel value, +// (and also optionally replace the alpha value with 0.0) so that the result produces +// the desired blend mode when sent to the blending operation. +#ifdef BLEND_PREMULTIPLIED_ALPHA + // For `BlendState::PREMULTIPLIED_ALPHA_BLENDING` the blend function is: + // + // result = 1 * src_color + (1 - src_alpha) * dst_color + let alpha_mode = standard_material_flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if (alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND) { + // Here, we premultiply `src_color` by `src_alpha` (ahead of time, here in the shader) + // + // src_color *= src_alpha + // + // We end up with: + // + // result = 1 * (src_alpha * src_color) + (1 - src_alpha) * dst_color + // result = src_alpha * src_color + (1 - src_alpha) * dst_color + // + // Which is the blend operation for regular alpha blending `BlendState::ALPHA_BLENDING` + return vec4(color.rgb * color.a, color.a); + } else if (alpha_mode == STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD) { + // Here, we premultiply `src_color` by `src_alpha`, and replace `src_alpha` with 0.0: + // + // src_color *= src_alpha + // src_alpha = 0.0 + // + // We end up with: + // + // result = 1 * (src_alpha * src_color) + (1 - 0) * dst_color + // result = src_alpha * src_color + 1 * dst_color + // + // Which is the blend operation for additive blending + return vec4(color.rgb * color.a, 0.0); + } else { + // Here, we don't do anything, so that we get premultiplied alpha blending. (As expected) + return color.rgba; + } +#endif +// `Multiply` uses its own `BlendState`, but we still need to premultiply here in the +// shader so that we get correct results as we tweak the alpha channel +#ifdef BLEND_MULTIPLY + // The blend function is: + // + // result = dst_color * src_color + (1 - src_alpha) * dst_color + // + // We premultiply `src_color` by `src_alpha`: + // + // src_color *= src_alpha + // + // We end up with: + // + // result = dst_color * (src_color * src_alpha) + (1 - src_alpha) * dst_color + // result = src_alpha * (src_color * dst_color) + (1 - src_alpha) * dst_color + // + // Which is the blend operation for multiplicative blending with arbitrary mixing + // controlled by the source alpha channel + return vec4(color.rgb * color.a, color.a); +#endif +} #endif - diff --git a/crates/bevy_pbr/src/render/pbr_prepass.wgsl b/crates/bevy_pbr/src/render/pbr_prepass.wgsl new file mode 100644 index 0000000000000..54584ef682666 --- /dev/null +++ b/crates/bevy_pbr/src/render/pbr_prepass.wgsl @@ -0,0 +1,77 @@ +#import bevy_pbr::prepass_bindings +#import bevy_pbr::pbr_bindings +#import bevy_pbr::pbr_functions + +struct FragmentInput { + @builtin(front_facing) is_front: bool, + @builtin(position) frag_coord: vec4, +#ifdef VERTEX_UVS + @location(0) uv: vec2, +#endif // VERTEX_UVS +#ifdef NORMAL_PREPASS + @location(1) world_normal: vec3, +#ifdef VERTEX_TANGENTS + @location(2) world_tangent: vec4, +#endif // VERTEX_TANGENTS +#endif // NORMAL_PREPASS +}; + +// We can use a simplified version of alpha_discard() here since we only need to handle the alpha_cutoff +fn prepass_alpha_discard(in: FragmentInput) { +#ifdef ALPHA_MASK + var output_color: vec4 = material.base_color; + +#ifdef VERTEX_UVS + if (material.flags & STANDARD_MATERIAL_FLAGS_BASE_COLOR_TEXTURE_BIT) != 0u { + output_color = output_color * textureSample(base_color_texture, base_color_sampler, in.uv); + } +#endif // VERTEX_UVS + + if ((material.flags & STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK) != 0u) && output_color.a < material.alpha_cutoff { + discard; + } +#endif // ALPHA_MASK +} + +#ifdef NORMAL_PREPASS + +@fragment +fn fragment(in: FragmentInput) -> @location(0) vec4 { + prepass_alpha_discard(in); + + // NOTE: Unlit bit not set means == 0 is true, so the true case is if lit + if (material.flags & STANDARD_MATERIAL_FLAGS_UNLIT_BIT) == 0u { + let world_normal = prepare_world_normal( + in.world_normal, + (material.flags & STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT) != 0u, + in.is_front, + ); + + let normal = apply_normal_mapping( + material.flags, + world_normal, +#ifdef VERTEX_TANGENTS +#ifdef STANDARDMATERIAL_NORMAL_MAP + in.world_tangent, +#endif // STANDARDMATERIAL_NORMAL_MAP +#endif // VERTEX_TANGENTS +#ifdef VERTEX_UVS + in.uv, +#endif // VERTEX_UVS + ); + + return vec4(normal * 0.5 + vec3(0.5), 1.0); + } else { + return vec4(in.world_normal * 0.5 + vec3(0.5), 1.0); + } +} + +#else // NORMAL_PREPASS + +@fragment +fn fragment(in: FragmentInput) { + prepass_alpha_discard(in); +} + +#endif // NORMAL_PREPASS + diff --git a/crates/bevy_pbr/src/render/pbr_types.wgsl b/crates/bevy_pbr/src/render/pbr_types.wgsl index 879cc64e2d744..8b9eb3297b292 100644 --- a/crates/bevy_pbr/src/render/pbr_types.wgsl +++ b/crates/bevy_pbr/src/render/pbr_types.wgsl @@ -17,11 +17,17 @@ let STANDARD_MATERIAL_FLAGS_METALLIC_ROUGHNESS_TEXTURE_BIT: u32 = 4u; let STANDARD_MATERIAL_FLAGS_OCCLUSION_TEXTURE_BIT: u32 = 8u; let STANDARD_MATERIAL_FLAGS_DOUBLE_SIDED_BIT: u32 = 16u; let STANDARD_MATERIAL_FLAGS_UNLIT_BIT: u32 = 32u; -let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 64u; -let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 128u; -let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 256u; -let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 512u; -let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 1024u; +let STANDARD_MATERIAL_FLAGS_TWO_COMPONENT_NORMAL_MAP: u32 = 64u; +let STANDARD_MATERIAL_FLAGS_FLIP_NORMAL_MAP_Y: u32 = 128u; +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS: u32 = 3758096384u; // (0b111u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE: u32 = 0u; // (0u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MASK: u32 = 536870912u; // (1u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND: u32 = 1073741824u; // (2u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED: u32 = 1610612736u; // (3u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD: u32 = 2147483648u; // (4u32 << 29) +let STANDARD_MATERIAL_FLAGS_ALPHA_MODE_MULTIPLY: u32 = 2684354560u; // (5u32 << 29) +// ↑ To calculate/verify the values above, use the following playground: +// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=7792f8dd6fc6a8d4d0b6b1776898a7f4 // Creates a StandardMaterial with default values fn standard_material_new() -> StandardMaterial { diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index 70c2ed3383a2e..34f4c3b627f48 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -49,7 +49,7 @@ fn fetch_spot_shadow(light_id: u32, frag_position: vec4, surface_normal: ve // construct the light view matrix var spot_dir = vec3((*light).light_custom_data.x, 0.0, (*light).light_custom_data.y); // reconstruct spot dir from x/z and y-direction flag - spot_dir.y = sqrt(1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z); + spot_dir.y = sqrt(max(0.0, 1.0 - spot_dir.x * spot_dir.x - spot_dir.z * spot_dir.z)); if (((*light).flags & POINT_LIGHT_FLAGS_SPOT_LIGHT_Y_NEGATIVE) != 0u) { spot_dir.y = -spot_dir.y; } diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 20b71ee83457e..641010868f086 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -11,7 +11,7 @@ fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3 { vec3(1.0) ); - return value * mix( vec3(1.0), rgb, vec3(saturation)); + return value * mix(vec3(1.0), rgb, vec3(saturation)); } fn random1D(s: f32) -> f32 { @@ -25,3 +25,25 @@ fn random1D(s: f32) -> f32 { fn coords_to_viewport_uv(position: vec2, viewport: vec4) -> vec2 { return (position - viewport.xy) / viewport.zw; } + +#ifndef NORMAL_PREPASS +fn prepass_normal(frag_coord: vec4, sample_index: u32) -> vec3 { +#ifdef MULTISAMPLED + let normal_sample = textureLoad(normal_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let normal_sample = textureLoad(normal_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return normal_sample.xyz * 2.0 - vec3(1.0); +} +#endif // NORMAL_PREPASS + +#ifndef DEPTH_PREPASS +fn prepass_depth(frag_coord: vec4, sample_index: u32) -> f32 { +#ifdef MULTISAMPLED + let depth_sample = textureLoad(depth_prepass_texture, vec2(frag_coord.xy), i32(sample_index)); +#else + let depth_sample = textureLoad(depth_prepass_texture, vec2(frag_coord.xy), 0); +#endif + return depth_sample; +} +#endif // DEPTH_PREPASS \ No newline at end of file diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 2b31a051317d2..a680a079058ad 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -70,7 +70,7 @@ pub struct WireframeConfig { pub global: bool, } -#[derive(Resource)] +#[derive(Resource, Clone)] pub struct WireframePipeline { mesh_pipeline: MeshPipeline, shader: Handle, @@ -108,7 +108,7 @@ fn queue_wireframes( wireframe_config: Res, wireframe_pipeline: Res, mut pipelines: ResMut>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, msaa: Res, mut material_meshes: ParamSet<( Query<(Entity, &Handle, &MeshUniform)>, @@ -117,7 +117,7 @@ fn queue_wireframes( mut views: Query<(&ExtractedView, &VisibleEntities, &mut RenderPhase)>, ) { let draw_custom = opaque_3d_draw_functions.read().id::(); - let msaa_key = MeshPipelineKey::from_msaa_samples(msaa.samples); + let msaa_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); for (view, visible_entities, mut opaque_phase) in &mut views { let rangefinder = view.rangefinder3d(); @@ -128,7 +128,7 @@ fn queue_wireframes( let key = view_key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); let pipeline_id = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &wireframe_pipeline, key, &mesh.layout, diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index bf38fd5ff1d9f..f80cf2f51958b 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -7,17 +7,37 @@ use core::{ cell::UnsafeCell, marker::PhantomData, mem::ManuallyDrop, num::NonZeroUsize, ptr::NonNull, }; +#[derive(Copy, Clone)] +/// Used as a type argument to [`Ptr`], [`PtrMut`] and [`OwningPtr`] to specify that the pointer is aligned. +pub struct Aligned; +#[derive(Copy, Clone)] +/// Used as a type argument to [`Ptr`], [`PtrMut`] and [`OwningPtr`] to specify that the pointer is not aligned. +pub struct Unaligned; + +/// Trait that is only implemented for [`Aligned`] and [`Unaligned`] to work around the lack of ability +/// to have const generics of an enum. +pub trait IsAligned: sealed::Sealed {} +impl IsAligned for Aligned {} +impl IsAligned for Unaligned {} + +mod sealed { + pub trait Sealed {} + impl Sealed for super::Aligned {} + impl Sealed for super::Unaligned {} +} + /// Type-erased borrow of some unknown type chosen when constructing this type. /// /// This type tries to act "borrow-like" which means that: /// - It should be considered immutable: its target must not be changed while this pointer is alive. /// - It must always points to a valid value of whatever the pointee type is. /// - The lifetime `'a` accurately represents how long the pointer is valid for. +/// - Must be sufficiently aligned for the unknown pointee type. /// /// It may be helpful to think of this type as similar to `&'a dyn Any` but without /// the metadata and able to point to data that does not correspond to a Rust type. #[derive(Copy, Clone)] -pub struct Ptr<'a>(NonNull, PhantomData<&'a u8>); +pub struct Ptr<'a, A: IsAligned = Aligned>(NonNull, PhantomData<(&'a u8, A)>); /// Type-erased mutable borrow of some unknown type chosen when constructing this type. /// @@ -26,10 +46,11 @@ pub struct Ptr<'a>(NonNull, PhantomData<&'a u8>); /// aliased mutability. /// - It must always points to a valid value of whatever the pointee type is. /// - The lifetime `'a` accurately represents how long the pointer is valid for. +/// - Must be sufficiently aligned for the unknown pointee type. /// /// It may be helpful to think of this type as similar to `&'a mut dyn Any` but without /// the metadata and able to point to data that does not correspond to a Rust type. -pub struct PtrMut<'a>(NonNull, PhantomData<&'a mut u8>); +pub struct PtrMut<'a, A: IsAligned = Aligned>(NonNull, PhantomData<(&'a mut u8, A)>); /// Type-erased Box-like pointer to some unknown type chosen when constructing this type. /// Conceptually represents ownership of whatever data is being pointed to and so is @@ -42,14 +63,28 @@ pub struct PtrMut<'a>(NonNull, PhantomData<&'a mut u8>); /// to aliased mutability and potentially use after free bugs. /// - It must always points to a valid value of whatever the pointee type is. /// - The lifetime `'a` accurately represents how long the pointer is valid for. +/// - Must be sufficiently aligned for the unknown pointee type. /// /// It may be helpful to think of this type as similar to `&'a mut ManuallyDrop` but /// without the metadata and able to point to data that does not correspond to a Rust type. -pub struct OwningPtr<'a>(NonNull, PhantomData<&'a mut u8>); +pub struct OwningPtr<'a, A: IsAligned = Aligned>(NonNull, PhantomData<(&'a mut u8, A)>); macro_rules! impl_ptr { ($ptr:ident) => { - impl $ptr<'_> { + impl<'a> $ptr<'a, Aligned> { + /// Removes the alignment requirement of this pointer + pub fn to_unaligned(self) -> $ptr<'a, Unaligned> { + $ptr(self.0, PhantomData) + } + } + + impl<'a, A: IsAligned> From<$ptr<'a, A>> for NonNull { + fn from(ptr: $ptr<'a, A>) -> Self { + ptr.0 + } + } + + impl $ptr<'_, A> { /// Calculates the offset from a pointer. /// As the pointer is type-erased, there is no size information available. The provided /// `count` parameter is in raw bytes. @@ -57,7 +92,9 @@ macro_rules! impl_ptr { /// *See also: [`ptr::offset`][ptr_offset]* /// /// # Safety - /// the offset cannot make the existing ptr null, or take it out of bounds for its allocation. + /// - The offset cannot make the existing ptr null, or take it out of bounds for its allocation. + /// - If the `A` type parameter is [`Aligned`] then the offset must not make the resulting pointer + /// be unaligned for the pointee type. /// /// [ptr_offset]: https://doc.rust-lang.org/std/primitive.pointer.html#method.offset #[inline] @@ -75,7 +112,9 @@ macro_rules! impl_ptr { /// *See also: [`ptr::add`][ptr_add]* /// /// # Safety - /// the offset cannot make the existing ptr null, or take it out of bounds for its allocation. + /// - The offset cannot make the existing ptr null, or take it out of bounds for its allocation. + /// - If the `A` type parameter is [`Aligned`] then the offset must not make the resulting pointer + /// be unaligned for the pointee type. /// /// [ptr_add]: https://doc.rust-lang.org/std/primitive.pointer.html#method.add #[inline] @@ -85,18 +124,9 @@ macro_rules! impl_ptr { PhantomData, ) } - - /// Creates a new instance from a raw pointer. - /// - /// # Safety - /// The lifetime for the returned item must not exceed the lifetime `inner` is valid for - #[inline] - pub unsafe fn new(inner: NonNull) -> Self { - Self(inner, PhantomData) - } } - impl Pointer for $ptr<'_> { + impl Pointer for $ptr<'_, A> { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Pointer::fmt(&self.0, f) @@ -109,23 +139,38 @@ impl_ptr!(Ptr); impl_ptr!(PtrMut); impl_ptr!(OwningPtr); -impl<'a> Ptr<'a> { +impl<'a, A: IsAligned> Ptr<'a, A> { + /// Creates a new instance from a raw pointer. + /// + /// # Safety + /// - `inner` must point to valid value of whatever the pointee type is. + /// - If the `A` type parameter is [`Aligned`] then `inner` must be sufficiently aligned for the pointee type. + /// - `inner` must have correct provenance to allow reads of the pointee type. + /// - The lifetime `'a` must be constrained such that this [`Ptr`] will stay valid and nothing + /// can mutate the pointee while this [`Ptr`] is live except through an `UnsafeCell`. + #[inline] + pub unsafe fn new(inner: NonNull) -> Self { + Self(inner, PhantomData) + } + /// Transforms this [`Ptr`] into an [`PtrMut`] /// /// # Safety /// Another [`PtrMut`] for the same [`Ptr`] must not be created until the first is dropped. #[inline] - pub unsafe fn assert_unique(self) -> PtrMut<'a> { + pub unsafe fn assert_unique(self) -> PtrMut<'a, A> { PtrMut(self.0, PhantomData) } /// Transforms this [`Ptr`] into a `&T` with the same lifetime /// /// # Safety - /// Must point to a valid `T` + /// - `T` must be the erased pointee type for this [`Ptr`]. + /// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned + /// for the pointee type `T`. #[inline] pub unsafe fn deref(self) -> &'a T { - &*self.as_ptr().cast() + &*self.as_ptr().cast::().debug_ensure_aligned() } /// Gets the underlying pointer, erasing the associated lifetime. @@ -148,23 +193,38 @@ impl<'a, T> From<&'a T> for Ptr<'a> { } } -impl<'a> PtrMut<'a> { +impl<'a, A: IsAligned> PtrMut<'a, A> { + /// Creates a new instance from a raw pointer. + /// + /// # Safety + /// - `inner` must point to valid value of whatever the pointee type is. + /// - If the `A` type parameter is [`Aligned`] then `inner` must be sufficiently aligned for the pointee type. + /// - `inner` must have correct provenance to allow read and writes of the pointee type. + /// - The lifetime `'a` must be constrained such that this [`PtrMut`] will stay valid and nothing + /// else can read or mutate the pointee while this [`PtrMut`] is live. + #[inline] + pub unsafe fn new(inner: NonNull) -> Self { + Self(inner, PhantomData) + } + /// Transforms this [`PtrMut`] into an [`OwningPtr`] /// /// # Safety /// Must have right to drop or move out of [`PtrMut`]. #[inline] - pub unsafe fn promote(self) -> OwningPtr<'a> { + pub unsafe fn promote(self) -> OwningPtr<'a, A> { OwningPtr(self.0, PhantomData) } /// Transforms this [`PtrMut`] into a `&mut T` with the same lifetime /// /// # Safety - /// Must point to a valid `T` + /// - `T` must be the erased pointee type for this [`PtrMut`]. + /// - If the type parameter `A` is [`Unaligned`] then this pointer must be sufficiently aligned + /// for the pointee type `T`. #[inline] pub unsafe fn deref_mut(self) -> &'a mut T { - &mut *self.as_ptr().cast() + &mut *self.as_ptr().cast::().debug_ensure_aligned() } /// Gets the underlying pointer, erasing the associated lifetime. @@ -177,16 +237,16 @@ impl<'a> PtrMut<'a> { self.0.as_ptr() } - /// Gets a `PtrMut` from this with a smaller lifetime. + /// Gets a [`PtrMut`] from this with a smaller lifetime. #[inline] - pub fn reborrow(&mut self) -> PtrMut<'_> { + pub fn reborrow(&mut self) -> PtrMut<'_, A> { // SAFE: the ptrmut we're borrowing from is assumed to be valid unsafe { PtrMut::new(self.0) } } /// Gets an immutable reference from this mutable reference #[inline] - pub fn as_ref(&self) -> Ptr<'_> { + pub fn as_ref(&self) -> Ptr<'_, A> { // SAFE: The `PtrMut` type's guarantees about the validity of this pointer are a superset of `Ptr` s guarantees unsafe { Ptr::new(self.0) } } @@ -210,23 +270,44 @@ impl<'a> OwningPtr<'a> { // so it's safe to promote it to an owning pointer. f(unsafe { PtrMut::from(&mut *temp).promote() }) } +} +impl<'a, A: IsAligned> OwningPtr<'a, A> { + /// Creates a new instance from a raw pointer. + /// + /// # Safety + /// - `inner` must point to valid value of whatever the pointee type is. + /// - If the `A` type parameter is [`Aligned`] then `inner` must be sufficiently aligned for the pointee type. + /// - `inner` must have correct provenance to allow read and writes of the pointee type. + /// - The lifetime `'a` must be constrained such that this [`OwningPtr`] will stay valid and nothing + /// else can read or mutate the pointee while this [`OwningPtr`] is live. + #[inline] + pub unsafe fn new(inner: NonNull) -> Self { + Self(inner, PhantomData) + } /// Consumes the [`OwningPtr`] to obtain ownership of the underlying data of type `T`. /// /// # Safety - /// Must point to a valid `T`. + /// - `T` must be the erased pointee type for this [`OwningPtr`]. + /// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned + /// for the pointee type `T`. #[inline] pub unsafe fn read(self) -> T { - self.as_ptr().cast::().read() + self.as_ptr().cast::().debug_ensure_aligned().read() } /// Consumes the [`OwningPtr`] to drop the underlying data of type `T`. /// /// # Safety - /// Must point to a valid `T`. + /// - `T` must be the erased pointee type for this [`OwningPtr`]. + /// - If the type parameter `A` is `Unaligned` then this pointer must be sufficiently aligned + /// for the pointee type `T`. #[inline] pub unsafe fn drop_as(self) { - self.as_ptr().cast::().drop_in_place(); + self.as_ptr() + .cast::() + .debug_ensure_aligned() + .drop_in_place(); } /// Gets the underlying pointer, erasing the associated lifetime. @@ -241,18 +322,27 @@ impl<'a> OwningPtr<'a> { /// Gets an immutable pointer from this owned pointer. #[inline] - pub fn as_ref(&self) -> Ptr<'_> { + pub fn as_ref(&self) -> Ptr<'_, A> { // SAFE: The `Owning` type's guarantees about the validity of this pointer are a superset of `Ptr` s guarantees unsafe { Ptr::new(self.0) } } /// Gets a mutable pointer from this owned pointer. #[inline] - pub fn as_mut(&mut self) -> PtrMut<'_> { + pub fn as_mut(&mut self) -> PtrMut<'_, A> { // SAFE: The `Owning` type's guarantees about the validity of this pointer are a superset of `Ptr` s guarantees unsafe { PtrMut::new(self.0) } } } +impl<'a> OwningPtr<'a, Unaligned> { + /// Consumes the [`OwningPtr`] to obtain ownership of the underlying data of type `T`. + /// + /// # Safety + /// - `T` must be the erased pointee type for this [`OwningPtr`]. + pub unsafe fn read_unaligned(self) -> T { + self.as_ptr().cast::().read_unaligned() + } +} /// Conceptually equivalent to `&'a [T]` but with length information cut out for performance reasons pub struct ThinSlicePtr<'a, T> { @@ -292,9 +382,10 @@ impl<'a, T> Copy for ThinSlicePtr<'a, T> {} impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> { #[inline] fn from(slice: &'a [T]) -> Self { + let ptr = slice.as_ptr() as *mut T; Self { // SAFETY: a reference can never be null - ptr: unsafe { NonNull::new_unchecked(slice.as_ptr() as *mut T) }, + ptr: unsafe { NonNull::new_unchecked(ptr.debug_ensure_aligned()) }, #[cfg(debug_assertions)] len: slice.len(), _marker: PhantomData, @@ -305,6 +396,7 @@ impl<'a, T> From<&'a [T]> for ThinSlicePtr<'a, T> { /// Creates a dangling pointer with specified alignment. /// See [`NonNull::dangling`]. pub fn dangling_with_align(align: NonZeroUsize) -> NonNull { + debug_assert!(align.is_power_of_two(), "Alignment must be power of two."); // SAFETY: The pointer will not be null, since it was created // from the address of a `NonZeroUsize`. unsafe { NonNull::new_unchecked(align.get() as *mut u8) } @@ -357,3 +449,37 @@ impl<'a, T> UnsafeCellDeref<'a, T> for &'a UnsafeCell { self.get().read() } } + +trait DebugEnsureAligned { + fn debug_ensure_aligned(self) -> Self; +} + +// Disable this for miri runs as it already checks if pointer to reference +// casts are properly aligned. +#[cfg(all(debug_assertions, not(miri)))] +impl DebugEnsureAligned for *mut T { + #[track_caller] + fn debug_ensure_aligned(self) -> Self { + let align = core::mem::align_of::(); + // Implemenation shamelessly borrowed from the currently unstable + // ptr.is_aligned_to. + // + // Replace once https://github.com/rust-lang/rust/issues/96284 is stable. + assert!( + self as usize & (align - 1) == 0, + "pointer is not aligned. Address {:p} does not have alignemnt {} for type {}", + self, + align, + core::any::type_name::(), + ); + self + } +} + +#[cfg(any(not(debug_assertions), miri))] +impl DebugEnsureAligned for *mut T { + #[inline(always)] + fn debug_ensure_aligned(self) -> Self { + self + } +} diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index c7d54e9964837..f74889c5ab06b 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -29,7 +29,7 @@ downcast-rs = "1.2" parking_lot = "0.12.1" thiserror = "1.0" once_cell = "1.11" -serde = { version = "1", features = ["derive"] } +serde = "1" smallvec = { version = "1.6", features = ["serde", "union", "const_generics"], optional = true } glam = { version = "0.22", features = ["serde"], optional = true } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs b/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs index 5441fa3c1f8da..14bd9d6d40674 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs @@ -35,21 +35,16 @@ pub(crate) fn reflect_trait(_args: &TokenStream, input: TokenStream) -> TokenStr let bevy_reflect_path = BevyManifest::default().get_path("bevy_reflect"); let struct_doc = format!( - " A type generated by the #[reflect_trait] macro for the `{}` trait.\n\n This allows casting from `dyn Reflect` to `dyn {}`.", - trait_ident, - trait_ident + " A type generated by the #[reflect_trait] macro for the `{trait_ident}` trait.\n\n This allows casting from `dyn Reflect` to `dyn {trait_ident}`.", ); let get_doc = format!( - " Downcast a `&dyn Reflect` type to `&dyn {}`.\n\n If the type cannot be downcast, `None` is returned.", - trait_ident, + " Downcast a `&dyn Reflect` type to `&dyn {trait_ident}`.\n\n If the type cannot be downcast, `None` is returned.", ); let get_mut_doc = format!( - " Downcast a `&mut dyn Reflect` type to `&mut dyn {}`.\n\n If the type cannot be downcast, `None` is returned.", - trait_ident, + " Downcast a `&mut dyn Reflect` type to `&mut dyn {trait_ident}`.\n\n If the type cannot be downcast, `None` is returned.", ); let get_box_doc = format!( - " Downcast a `Box` type to `Box`.\n\n If the type cannot be downcast, this will return `Err(Box)`.", - trait_ident, + " Downcast a `Box` type to `Box`.\n\n If the type cannot be downcast, this will return `Err(Box)`.", ); TokenStream::from(quote! { diff --git a/crates/bevy_reflect/src/from_reflect.rs b/crates/bevy_reflect/src/from_reflect.rs index e9df08aefc459..41c4c3582bd3a 100644 --- a/crates/bevy_reflect/src/from_reflect.rs +++ b/crates/bevy_reflect/src/from_reflect.rs @@ -13,6 +13,26 @@ use crate::{FromType, Reflect}; pub trait FromReflect: Reflect + Sized { /// Constructs a concrete instance of `Self` from a reflected value. fn from_reflect(reflect: &dyn Reflect) -> Option; + + /// Attempts to downcast the given value to `Self` using, + /// constructing the value using [`from_reflect`] if that fails. + /// + /// This method is more efficient than using [`from_reflect`] for cases where + /// the given value is likely a boxed instance of `Self` (i.e. `Box`) + /// rather than a boxed dynamic type (e.g. [`DynamicStruct`], [`DynamicList`], etc.). + /// + /// [`from_reflect`]: Self::from_reflect + /// [`DynamicStruct`]: crate::DynamicStruct + /// [`DynamicList`]: crate::DynamicList + fn take_from_reflect(reflect: Box) -> Result> { + match reflect.take::() { + Ok(value) => Ok(value), + Err(value) => match Self::from_reflect(value.as_ref()) { + None => Err(value), + Some(value) => Ok(value), + }, + } + } } /// Type data that represents the [`FromReflect`] trait and allows it to be used dynamically. diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index f44957175f384..4a4c64e4ff9dd 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -49,6 +49,22 @@ impl List for SmallVec where T::Item: FromReflect, { + fn insert(&mut self, index: usize, value: Box) { + let value = value.take::().unwrap_or_else(|value| { + ::Item::from_reflect(&*value).unwrap_or_else(|| { + panic!( + "Attempted to insert invalid value of type {}.", + value.type_name() + ) + }) + }); + SmallVec::insert(self, index, value); + } + + fn remove(&mut self, index: usize) -> Box { + Box::new(self.remove(index)) + } + fn push(&mut self, value: Box) { let value = value.take::().unwrap_or_else(|value| { ::Item::from_reflect(&*value).unwrap_or_else(|| { diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index ca7ad9c8710e9..9b7bbf8ff77fa 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -179,7 +179,7 @@ impl_from_reflect_value!(NonZeroU8); impl_from_reflect_value!(NonZeroI8); macro_rules! impl_reflect_for_veclike { - ($ty:ty, $push:expr, $pop:expr, $sub:ty) => { + ($ty:ty, $insert:expr, $remove:expr, $push:expr, $pop:expr, $sub:ty) => { impl Array for $ty { #[inline] fn get(&self, index: usize) -> Option<&dyn Reflect> { @@ -213,15 +213,29 @@ macro_rules! impl_reflect_for_veclike { } impl List for $ty { - fn push(&mut self, value: Box) { + fn insert(&mut self, index: usize, value: Box) { let value = value.take::().unwrap_or_else(|value| { T::from_reflect(&*value).unwrap_or_else(|| { panic!( - "Attempted to push invalid value of type {}.", + "Attempted to insert invalid value of type {}.", value.type_name() ) }) }); + $insert(self, index, value); + } + + fn remove(&mut self, index: usize) -> Box { + Box::new($remove(self, index)) + } + + fn push(&mut self, value: Box) { + let value = T::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to push invalid value of type {}.", + value.type_name() + ) + }); $push(self, value); } @@ -328,9 +342,11 @@ macro_rules! impl_reflect_for_veclike { }; } -impl_reflect_for_veclike!(Vec, Vec::push, Vec::pop, [T]); +impl_reflect_for_veclike!(Vec, Vec::insert, Vec::remove, Vec::push, Vec::pop, [T]); impl_reflect_for_veclike!( VecDeque, + VecDeque::insert, + VecDeque::remove, VecDeque::push_back, VecDeque::pop_back, VecDeque:: @@ -391,21 +407,17 @@ impl Map for HashMap { key: Box, value: Box, ) -> Option> { - let key = key.take::().unwrap_or_else(|key| { - K::from_reflect(&*key).unwrap_or_else(|| { - panic!( - "Attempted to insert invalid key of type {}.", - key.type_name() - ) - }) + let key = K::take_from_reflect(key).unwrap_or_else(|key| { + panic!( + "Attempted to insert invalid key of type {}.", + key.type_name() + ) }); - let value = value.take::().unwrap_or_else(|value| { - V::from_reflect(&*value).unwrap_or_else(|| { - panic!( - "Attempted to insert invalid value of type {}.", - value.type_name() - ) - }) + let value = V::take_from_reflect(value).unwrap_or_else(|value| { + panic!( + "Attempted to insert invalid value of type {}.", + value.type_name() + ) }); self.insert(key, value) .map(|old_value| Box::new(old_value) as Box) @@ -811,25 +823,24 @@ impl Reflect for Option { // New variant -> perform a switch match value.variant_name() { "Some" => { - let field = value - .field_at(0) - .unwrap_or_else(|| { - panic!( - "Field in `Some` variant of {} should exist", - std::any::type_name::>() - ) - }) - .clone_value() - .take::() - .unwrap_or_else(|value| { - T::from_reflect(&*value).unwrap_or_else(|| { + let field = T::take_from_reflect( + value + .field_at(0) + .unwrap_or_else(|| { panic!( - "Field in `Some` variant of {} should be of type {}", - std::any::type_name::>(), - std::any::type_name::() + "Field in `Some` variant of {} should exist", + std::any::type_name::>() ) }) - }); + .clone_value(), + ) + .unwrap_or_else(|_| { + panic!( + "Field in `Some` variant of {} should be of type {}", + std::any::type_name::>(), + std::any::type_name::() + ) + }); *self = Some(field); } "None" => { @@ -878,25 +889,24 @@ impl FromReflect for Option { if let ReflectRef::Enum(dyn_enum) = reflect.reflect_ref() { match dyn_enum.variant_name() { "Some" => { - let field = dyn_enum - .field_at(0) - .unwrap_or_else(|| { - panic!( - "Field in `Some` variant of {} should exist", - std::any::type_name::>() - ) - }) - .clone_value() - .take::() - .unwrap_or_else(|value| { - T::from_reflect(&*value).unwrap_or_else(|| { + let field = T::take_from_reflect( + dyn_enum + .field_at(0) + .unwrap_or_else(|| { panic!( - "Field in `Some` variant of {} should be of type {}", - std::any::type_name::>(), - std::any::type_name::() + "Field in `Some` variant of {} should exist", + std::any::type_name::>() ) }) - }); + .clone_value(), + ) + .unwrap_or_else(|_| { + panic!( + "Field in `Some` variant of {} should be of type {}", + std::any::type_name::>(), + std::any::type_name::() + ) + }); Some(Some(field)) } "None" => Some(None), diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index 6a2c2a8767381..2d67a62f24140 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -9,18 +9,43 @@ use crate::{ /// An ordered, mutable list of [Reflect] items. This corresponds to types like [`std::vec::Vec`]. /// -/// This is a sub-trait of [`Array`] as it implements a [`push`](List::push) function, allowing -/// it's internal size to grow. +/// This is a sub-trait of [`Array`], however as it implements [insertion](List::insert) and [removal](List::remove), +/// it's internal size may change. /// /// This trait expects index 0 to contain the _front_ element. /// The _back_ element must refer to the element with the largest index. /// These two rules above should be upheld by manual implementors. +/// +/// [`push`](List::push) and [`pop`](List::pop) have default implementations, +/// however it may be faster to implement them manually. pub trait List: Reflect + Array { + /// Inserts an element at position `index` within the list, + /// shifting all elements after it towards the back of the list. + /// + /// # Panics + /// Panics if `index > len`. + fn insert(&mut self, index: usize, element: Box); + + /// Removes and returns the element at position `index` within the list, + /// shifting all elements before it towards the front of the list. + /// + /// # Panics + /// Panics if `index` is out of bounds. + fn remove(&mut self, index: usize) -> Box; + /// Appends an element to the _back_ of the list. - fn push(&mut self, value: Box); + fn push(&mut self, value: Box) { + self.insert(self.len(), value); + } /// Removes the _back_ element from the list and returns it, or [`None`] if it is empty. - fn pop(&mut self) -> Option>; + fn pop(&mut self) -> Option> { + if self.is_empty() { + None + } else { + Some(self.remove(self.len() - 1)) + } + } /// Clones the list, producing a [`DynamicList`]. fn clone_dynamic(&self) -> DynamicList { @@ -174,6 +199,14 @@ impl Array for DynamicList { } impl List for DynamicList { + fn insert(&mut self, index: usize, element: Box) { + self.values.insert(index, element); + } + + fn remove(&mut self, index: usize) -> Box { + self.values.remove(index) + } + fn push(&mut self, value: Box) { DynamicList::push_box(self, value); } diff --git a/crates/bevy_reflect/src/path.rs b/crates/bevy_reflect/src/path.rs index 0b508fa0f749e..17cbb36e33dc1 100644 --- a/crates/bevy_reflect/src/path.rs +++ b/crates/bevy_reflect/src/path.rs @@ -1,6 +1,6 @@ use std::num::ParseIntError; -use crate::{Array, Reflect, ReflectMut, ReflectRef}; +use crate::{Array, Reflect, ReflectMut, ReflectRef, VariantType}; use thiserror::Error; /// An error returned from a failed path string query. @@ -29,6 +29,8 @@ pub enum ReflectPathError<'a> { IndexParseError(#[from] ParseIntError), #[error("failed to downcast to the path result to the given type")] InvalidDowncast, + #[error("expected either a struct variant or tuple variant, but found a unit variant")] + InvalidVariantAccess { index: usize, accessor: &'a str }, } /// A trait which allows nested values to be retrieved with path strings. @@ -280,6 +282,29 @@ fn read_field<'r, 'p>( }, )?) } + ReflectRef::Enum(reflect_enum) => match reflect_enum.variant_type() { + VariantType::Struct => { + Ok(reflect_enum + .field(field) + .ok_or(ReflectPathError::InvalidField { + index: current_index, + field, + })?) + } + VariantType::Tuple => { + let tuple_index = field.parse::()?; + Ok(reflect_enum + .field_at(tuple_index) + .ok_or(ReflectPathError::InvalidField { + index: current_index, + field, + })?) + } + _ => Err(ReflectPathError::InvalidVariantAccess { + index: current_index, + accessor: field, + }), + }, _ => Err(ReflectPathError::ExpectedStruct { index: current_index, }), @@ -309,6 +334,29 @@ fn read_field_mut<'r, 'p>( }, )?) } + ReflectMut::Enum(reflect_enum) => match reflect_enum.variant_type() { + VariantType::Struct => { + Ok(reflect_enum + .field_mut(field) + .ok_or(ReflectPathError::InvalidField { + index: current_index, + field, + })?) + } + VariantType::Tuple => { + let tuple_index = field.parse::()?; + Ok(reflect_enum.field_at_mut(tuple_index).ok_or( + ReflectPathError::InvalidField { + index: current_index, + field, + }, + )?) + } + _ => Err(ReflectPathError::InvalidVariantAccess { + index: current_index, + accessor: field, + }), + }, _ => Err(ReflectPathError::ExpectedStruct { index: current_index, }), @@ -416,6 +464,9 @@ mod tests { x: B, y: Vec, z: D, + unit_variant: F, + tuple_variant: F, + struct_variant: F, } #[derive(Reflect)] @@ -435,6 +486,13 @@ mod tests { #[derive(Reflect)] struct E(f32, usize); + #[derive(Reflect, FromReflect, PartialEq, Debug)] + enum F { + Unit, + Tuple(u32, u32), + Struct { value: char }, + } + let mut a = A { w: 1, x: B { @@ -443,6 +501,9 @@ mod tests { }, y: vec![C { baz: 1.0 }, C { baz: 2.0 }], z: D(E(10.0, 42)), + unit_variant: F::Unit, + tuple_variant: F::Tuple(123, 321), + struct_variant: F::Struct { value: 'm' }, }; assert_eq!(*a.get_path::("w").unwrap(), 1); @@ -451,9 +512,16 @@ mod tests { assert_eq!(*a.get_path::("y[1].baz").unwrap(), 2.0); assert_eq!(*a.get_path::("z.0.1").unwrap(), 42); + assert_eq!(*a.get_path::("unit_variant").unwrap(), F::Unit); + assert_eq!(*a.get_path::("tuple_variant.1").unwrap(), 321); + assert_eq!(*a.get_path::("struct_variant.value").unwrap(), 'm'); + *a.get_path_mut::("y[1].baz").unwrap() = 3.0; assert_eq!(a.y[1].baz, 3.0); + *a.get_path_mut::("tuple_variant.0").unwrap() = 1337; + assert_eq!(a.tuple_variant, F::Tuple(1337, 321)); + assert_eq!( a.path("x.notreal").err().unwrap(), ReflectPathError::InvalidField { @@ -462,6 +530,14 @@ mod tests { } ); + assert_eq!( + a.path("unit_variant.0").err().unwrap(), + ReflectPathError::InvalidVariantAccess { + index: 13, + accessor: "0" + } + ); + assert_eq!( a.path("x..").err().unwrap(), ReflectPathError::ExpectedIdent { index: 2 } diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index 91021d33d68b8..60d3b0aa2047c 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -12,7 +12,6 @@ use serde::de::{ }; use serde::Deserialize; use std::any::TypeId; -use std::borrow::Cow; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::slice::Iter; @@ -211,13 +210,6 @@ impl<'de> Visitor<'de> for U32Visitor { } } -/// Helper struct for deserializing strings without allocating (when possible). -/// -/// Based on [this comment](https://github.com/bevyengine/bevy/pull/6894#discussion_r1045069010). -#[derive(Deserialize)] -#[serde(transparent)] -struct BorrowableCowStr<'a>(#[serde(borrow)] Cow<'a, str>); - /// A general purpose deserializer for reflected types. /// /// This will return a [`Box`] containing the deserialized data. @@ -265,6 +257,54 @@ impl<'a, 'de> DeserializeSeed<'de> for UntypedReflectDeserializer<'a> { } } +/// A deserializer for type registrations. +/// +/// This will return a [`&TypeRegistration`] corresponding to the given type. +/// This deserializer expects a string containing the _full_ [type name] of the +/// type to find the `TypeRegistration` of. +/// +/// [`&TypeRegistration`]: crate::TypeRegistration +/// [type name]: std::any::type_name +pub struct TypeRegistrationDeserializer<'a> { + registry: &'a TypeRegistry, +} + +impl<'a> TypeRegistrationDeserializer<'a> { + pub fn new(registry: &'a TypeRegistry) -> Self { + Self { registry } + } +} + +impl<'a, 'de> DeserializeSeed<'de> for TypeRegistrationDeserializer<'a> { + type Value = &'a TypeRegistration; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct TypeRegistrationVisitor<'a>(&'a TypeRegistry); + + impl<'de, 'a> Visitor<'de> for TypeRegistrationVisitor<'a> { + type Value = &'a TypeRegistration; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string containing `type` entry for the reflected value") + } + + fn visit_str(self, type_name: &str) -> Result + where + E: Error, + { + self.0.get_with_name(type_name).ok_or_else(|| { + Error::custom(format_args!("No registration found for `{type_name}`")) + }) + } + } + + deserializer.deserialize_str(TypeRegistrationVisitor(self.registry)) + } +} + struct UntypedReflectDeserializerVisitor<'a> { registry: &'a TypeRegistry, } @@ -280,14 +320,9 @@ impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { where A: MapAccess<'de>, { - let type_name = map - .next_key::()? - .ok_or_else(|| Error::invalid_length(0, &"at least one entry"))? - .0; - - let registration = self.registry.get_with_name(&type_name).ok_or_else(|| { - Error::custom(format_args!("No registration found for `{type_name}`")) - })?; + let registration = map + .next_key_seed(TypeRegistrationDeserializer::new(self.registry))? + .ok_or_else(|| Error::invalid_length(0, &"at least one entry"))?; let value = map.next_value_seed(TypedReflectDeserializer { registration, registry: self.registry, @@ -432,16 +467,14 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { TypeInfo::Value(_) => { // This case should already be handled Err(de::Error::custom(format_args!( - "the TypeRegistration for {} doesn't have ReflectDeserialize", - type_name + "the TypeRegistration for {type_name} doesn't have ReflectDeserialize", ))) } TypeInfo::Dynamic(_) => { // We could potentially allow this but we'd have no idea what the actual types of the // fields are and would rely on the deserializer to determine them (e.g. `i32` vs `i64`) Err(de::Error::custom(format_args!( - "cannot deserialize arbitrary dynamic type {}", - type_name + "cannot deserialize arbitrary dynamic type {type_name}", ))) } } @@ -1045,10 +1078,7 @@ fn get_registration<'a, E: Error>( registry: &'a TypeRegistry, ) -> Result<&'a TypeRegistration, E> { let registration = registry.get(type_id).ok_or_else(|| { - Error::custom(format_args!( - "no registration found for type `{}`", - type_name - )) + Error::custom(format_args!("no registration found for type `{type_name}`",)) })?; Ok(registration) } diff --git a/crates/bevy_reflect/src/serde/ser.rs b/crates/bevy_reflect/src/serde/ser.rs index 6403b5e99933f..e9e4abe1d852d 100644 --- a/crates/bevy_reflect/src/serde/ser.rs +++ b/crates/bevy_reflect/src/serde/ser.rs @@ -56,8 +56,7 @@ fn get_type_info( TypeInfo::Dynamic(..) => match registry.get_with_name(type_name) { Some(registration) => Ok(registration.type_info()), None => Err(Error::custom(format_args!( - "no registration found for dynamic type with name {}", - type_name + "no registration found for dynamic type with name {type_name}", ))), }, info => Ok(info), @@ -197,8 +196,7 @@ impl<'a> Serialize for StructSerializer<'a> { TypeInfo::Struct(struct_info) => struct_info, info => { return Err(Error::custom(format_args!( - "expected struct type but received {:?}", - info + "expected struct type but received {info:?}" ))); } }; @@ -247,8 +245,7 @@ impl<'a> Serialize for TupleStructSerializer<'a> { TypeInfo::TupleStruct(tuple_struct_info) => tuple_struct_info, info => { return Err(Error::custom(format_args!( - "expected tuple struct type but received {:?}", - info + "expected tuple struct type but received {info:?}" ))); } }; @@ -296,8 +293,7 @@ impl<'a> Serialize for EnumSerializer<'a> { TypeInfo::Enum(enum_info) => enum_info, info => { return Err(Error::custom(format_args!( - "expected enum type but received {:?}", - info + "expected enum type but received {info:?}" ))); } }; @@ -308,8 +304,7 @@ impl<'a> Serialize for EnumSerializer<'a> { .variant_at(variant_index as usize) .ok_or_else(|| { Error::custom(format_args!( - "variant at index `{}` does not exist", - variant_index + "variant at index `{variant_index}` does not exist", )) })?; let variant_name = variant_info.name(); @@ -333,8 +328,7 @@ impl<'a> Serialize for EnumSerializer<'a> { VariantInfo::Struct(struct_info) => struct_info, info => { return Err(Error::custom(format_args!( - "expected struct variant type but received {:?}", - info + "expected struct variant type but received {info:?}", ))); } }; diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 605871172c2ec..b47461e769364 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -44,12 +44,14 @@ bevy_time = { path = "../bevy_time", version = "0.9.0" } bevy_transform = { path = "../bevy_transform", version = "0.9.0" } bevy_window = { path = "../bevy_window", version = "0.9.0" } bevy_utils = { path = "../bevy_utils", version = "0.9.0" } +bevy_tasks = { path = "../bevy_tasks", version = "0.9.0" } # rendering image = { version = "0.24", default-features = false } # misc wgpu = { version = "0.14.0", features = ["spirv"] } +wgpu-hal = "0.14.1" codespan-reporting = "0.11.0" naga = { version = "0.10.0", features = ["glsl-in", "spv-in", "spv-out", "wgsl-in", "wgsl-out"] } serde = { version = "1", features = ["derive"] } @@ -61,7 +63,6 @@ thread_local = "1.1" thiserror = "1.0" futures-lite = "1.4.0" anyhow = "1.0" -hex = "0.4.2" hexasphere = "8.0" parking_lot = "0.12.1" regex = "1.5" @@ -75,3 +76,4 @@ basis-universal = { version = "0.2.0", optional = true } encase = { version = "0.4", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = ["profile-with-tracing"], optional = true } +async-channel = "1.8" diff --git a/crates/bevy_render/macros/src/as_bind_group.rs b/crates/bevy_render/macros/src/as_bind_group.rs index bbc3c3850cf0b..ee8a048758d12 100644 --- a/crates/bevy_render/macros/src/as_bind_group.rs +++ b/crates/bevy_render/macros/src/as_bind_group.rs @@ -11,6 +11,7 @@ use syn::{ const UNIFORM_ATTRIBUTE_NAME: Symbol = Symbol("uniform"); const TEXTURE_ATTRIBUTE_NAME: Symbol = Symbol("texture"); const SAMPLER_ATTRIBUTE_NAME: Symbol = Symbol("sampler"); +const STORAGE_ATTRIBUTE_NAME: Symbol = Symbol("storage"); const BIND_GROUP_DATA_ATTRIBUTE_NAME: Symbol = Symbol("bind_group_data"); #[derive(Copy, Clone, Debug)] @@ -18,6 +19,7 @@ enum BindingType { Uniform, Texture, Sampler, + Storage, } #[derive(Clone)] @@ -55,7 +57,6 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { } } else if attr_ident == UNIFORM_ATTRIBUTE_NAME { let (binding_index, converted_shader_type) = get_uniform_binding_attr(attr)?; - binding_impls.push(quote! {{ use #render_path::render_resource::AsBindGroupShaderType; let mut buffer = #render_path::render_resource::encase::UniformBuffer::new(Vec::new()); @@ -126,6 +127,8 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { BindingType::Texture } else if attr_ident == SAMPLER_ATTRIBUTE_NAME { BindingType::Sampler + } else if attr_ident == STORAGE_ATTRIBUTE_NAME { + BindingType::Storage } else { continue; }; @@ -190,7 +193,45 @@ pub fn derive_as_bind_group(ast: syn::DeriveInput) -> Result { } match binding_type { - BindingType::Uniform => { /* uniform codegen is deferred to account for combined uniform bindings */ + BindingType::Uniform => { + // uniform codegen is deferred to account for combined uniform bindings + } + BindingType::Storage => { + let StorageAttrs { + visibility, + read_only, + } = get_storage_binding_attr(nested_meta_items)?; + let visibility = + visibility.hygenic_quote("e! { #render_path::render_resource }); + + let field_name = field.ident.as_ref().unwrap(); + let field_ty = &field.ty; + + binding_impls.push(quote! {{ + use #render_path::render_resource::AsBindGroupShaderType; + let mut buffer = #render_path::render_resource::encase::StorageBuffer::new(Vec::new()); + buffer.write(&self.#field_name).unwrap(); + #render_path::render_resource::OwnedBindingResource::Buffer(render_device.create_buffer_with_data( + &#render_path::render_resource::BufferInitDescriptor { + label: None, + usage: #render_path::render_resource::BufferUsages::COPY_DST | #render_path::render_resource::BufferUsages::STORAGE, + contents: buffer.as_ref(), + }, + )) + }}); + + binding_layouts.push(quote! { + #render_path::render_resource::BindGroupLayoutEntry { + binding: #binding_index, + visibility: #visibility, + ty: #render_path::render_resource::BindingType::Buffer { + ty: #render_path::render_resource::BufferBindingType::Storage { read_only: #read_only }, + has_dynamic_offset: false, + min_binding_size: Some(<#field_ty as #render_path::render_resource::ShaderType>::min_size()), + }, + count: None, + } + }); } BindingType::Texture => { let TextureAttrs { @@ -861,3 +902,40 @@ fn get_sampler_binding_type_value(lit_str: &LitStr) -> Result) -> Result { + let mut visibility = ShaderStageVisibility::vertex_fragment(); + let mut read_only = false; + + for meta in metas { + use syn::{Meta::List, Meta::Path, NestedMeta::Meta}; + match meta { + // Parse #[storage(0, visibility(...))]. + Meta(List(m)) if m.path == VISIBILITY => { + visibility = get_visibility_flag_value(&m.nested)?; + } + Meta(Path(path)) if path == READ_ONLY => { + read_only = true; + } + _ => { + return Err(Error::new_spanned( + meta, + "Not a valid attribute. Available attributes: `read_only`, `visibility`", + )); + } + } + } + + Ok(StorageAttrs { + visibility, + read_only, + }) +} diff --git a/crates/bevy_render/macros/src/lib.rs b/crates/bevy_render/macros/src/lib.rs index 863b48baf5fa0..e8da33ad3c48b 100644 --- a/crates/bevy_render/macros/src/lib.rs +++ b/crates/bevy_render/macros/src/lib.rs @@ -17,7 +17,10 @@ pub fn derive_extract_resource(input: TokenStream) -> TokenStream { extract_resource::derive_extract_resource(input) } -#[proc_macro_derive(AsBindGroup, attributes(uniform, texture, sampler, bind_group_data))] +#[proc_macro_derive( + AsBindGroup, + attributes(uniform, texture, sampler, bind_group_data, storage) +)] pub fn derive_as_bind_group(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 72bcfa3556df0..53252ff453588 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -13,6 +13,7 @@ use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, + prelude::With, reflect::ReflectComponent, system::{Commands, Query, Res}, }; @@ -21,7 +22,10 @@ use bevy_reflect::prelude::*; use bevy_reflect::FromReflect; use bevy_transform::components::GlobalTransform; use bevy_utils::HashSet; -use bevy_window::{WindowCreated, WindowId, WindowResized, Windows}; +use bevy_window::{ + NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, +}; + use std::{borrow::Cow, ops::Range}; use wgpu::{Extent3d, TextureFormat}; @@ -248,6 +252,25 @@ impl Camera { }) } + /// Returns a 2D world position computed from a position on this [`Camera`]'s viewport. + /// + /// Useful for 2D cameras and other cameras with an orthographic projection pointing along the Z axis. + /// + /// To get the world space coordinates with Normalized Device Coordinates, you should use + /// [`ndc_to_world`](Self::ndc_to_world). + pub fn viewport_to_world_2d( + &self, + camera_transform: &GlobalTransform, + viewport_position: Vec2, + ) -> Option { + let target_size = self.logical_viewport_size()?; + let ndc = viewport_position * 2. / target_size - Vec2::ONE; + + let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?; + + Some(world_near_plane.truncate()) + } + /// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates. /// /// When the position is within the viewport the values returned will be between -1.0 and 1.0 on the X and Y axes, @@ -306,10 +329,21 @@ impl CameraRenderGraph { /// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`](bevy_window::Window) /// swapchain or an [`Image`]. -#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Debug, Clone, Reflect)] pub enum RenderTarget { /// Window to which the camera's view is rendered. - Window(WindowId), + Window(WindowRef), + /// Image to which the camera's view is rendered. + Image(Handle), +} + +/// Normalized version of the render target. +/// +/// Once we have this we shouldn't need to resolve it down anymore. +#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum NormalizedRenderTarget { + /// Window to which the camera's view is rendered. + Window(NormalizedWindowRef), /// Image to which the camera's view is rendered. Image(Handle), } @@ -321,16 +355,28 @@ impl Default for RenderTarget { } impl RenderTarget { + /// Normalize the render target down to a more concrete value, mostly used for equality comparisons. + pub fn normalize(&self, primary_window: Option) -> Option { + match self { + RenderTarget::Window(window_ref) => window_ref + .normalize(primary_window) + .map(NormalizedRenderTarget::Window), + RenderTarget::Image(handle) => Some(NormalizedRenderTarget::Image(handle.clone())), + } + } +} + +impl NormalizedRenderTarget { pub fn get_texture_view<'a>( &self, windows: &'a ExtractedWindows, images: &'a RenderAssets, ) -> Option<&'a TextureView> { match self { - RenderTarget::Window(window_id) => windows - .get(window_id) + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) .and_then(|window| window.swap_chain_texture.as_ref()), - RenderTarget::Image(image_handle) => { + NormalizedRenderTarget::Image(image_handle) => { images.get(image_handle).map(|image| &image.texture_view) } } @@ -343,47 +389,55 @@ impl RenderTarget { images: &'a RenderAssets, ) -> Option { match self { - RenderTarget::Window(window_id) => windows - .get(window_id) + NormalizedRenderTarget::Window(window_ref) => windows + .get(&window_ref.entity()) .and_then(|window| window.swap_chain_texture_format), - RenderTarget::Image(image_handle) => { + NormalizedRenderTarget::Image(image_handle) => { images.get(image_handle).map(|image| image.texture_format) } } } - pub fn get_render_target_info( + pub fn get_render_target_info<'a>( &self, - windows: &Windows, + resolutions: impl IntoIterator, images: &Assets, ) -> Option { - Some(match self { - RenderTarget::Window(window_id) => { - let window = windows.get(*window_id)?; - RenderTargetInfo { - physical_size: UVec2::new(window.physical_width(), window.physical_height()), - scale_factor: window.scale_factor(), - } - } - RenderTarget::Image(image_handle) => { + match self { + NormalizedRenderTarget::Window(window_ref) => resolutions + .into_iter() + .find(|(entity, _)| *entity == window_ref.entity()) + .map(|(_, window)| RenderTargetInfo { + physical_size: UVec2::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ), + scale_factor: window.resolution.scale_factor(), + }), + NormalizedRenderTarget::Image(image_handle) => { let image = images.get(image_handle)?; let Extent3d { width, height, .. } = image.texture_descriptor.size; - RenderTargetInfo { + Some(RenderTargetInfo { physical_size: UVec2::new(width, height), scale_factor: 1.0, - } + }) } - }) + } } + // Check if this render target is contained in the given changed windows or images. fn is_changed( &self, - changed_window_ids: &[WindowId], + changed_window_ids: &HashSet, changed_image_handles: &HashSet<&Handle>, ) -> bool { match self { - RenderTarget::Window(window_id) => changed_window_ids.contains(window_id), - RenderTarget::Image(image_handle) => changed_image_handles.contains(&image_handle), + NormalizedRenderTarget::Window(window_ref) => { + changed_window_ids.contains(&window_ref.entity()) + } + NormalizedRenderTarget::Image(image_handle) => { + changed_image_handles.contains(&image_handle) + } } } } @@ -412,29 +466,16 @@ pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, mut image_asset_events: EventReader>, - windows: Res, + primary_window: Query>, + windows: Query<(Entity, &Window)>, images: Res>, mut cameras: Query<(&mut Camera, &mut T)>, ) { - let mut changed_window_ids = Vec::new(); - - // Collect all unique window IDs of changed windows by inspecting created windows - for event in window_created_events.iter() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } + let primary_window = primary_window.iter().next(); - // Collect all unique window IDs of changed windows by inspecting resized windows - for event in window_resized_events.iter() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } + let mut changed_window_ids = HashSet::new(); + changed_window_ids.extend(window_created_events.iter().map(|event| event.window)); + changed_window_ids.extend(window_resized_events.iter().map(|event| event.window)); let changed_image_handles: HashSet<&Handle> = image_asset_events .iter() @@ -453,18 +494,18 @@ pub fn camera_system( .as_ref() .map(|viewport| viewport.physical_size); - if camera - .target - .is_changed(&changed_window_ids, &changed_image_handles) - || camera.is_added() - || camera_projection.is_changed() - || camera.computed.old_viewport_size != viewport_size - { - camera.computed.target_info = camera.target.get_render_target_info(&windows, &images); - camera.computed.old_viewport_size = viewport_size; - if let Some(size) = camera.logical_viewport_size() { - camera_projection.update(size.x, size.y); - camera.computed.projection_matrix = camera_projection.get_projection_matrix(); + if let Some(normalized_target) = camera.target.normalize(primary_window) { + if normalized_target.is_changed(&changed_window_ids, &changed_image_handles) + || camera.is_added() + || camera_projection.is_changed() + || camera.computed.old_viewport_size != viewport_size + { + camera.computed.target_info = + normalized_target.get_render_target_info(&windows, &images); + if let Some(size) = camera.logical_viewport_size() { + camera_projection.update(size.x, size.y); + camera.computed.projection_matrix = camera_projection.get_projection_matrix(); + } } } } @@ -472,7 +513,7 @@ pub fn camera_system( #[derive(Component, Debug)] pub struct ExtractedCamera { - pub target: RenderTarget, + pub target: Option, pub physical_viewport_size: Option, pub physical_target_size: Option, pub viewport: Option, @@ -491,7 +532,9 @@ pub fn extract_cameras( &VisibleEntities, )>, >, + primary_window: Extract>>, ) { + let primary_window = primary_window.iter().next(); for (entity, camera, camera_render_graph, transform, visible_entities) in query.iter() { if !camera.is_active { continue; @@ -506,7 +549,7 @@ pub fn extract_cameras( } commands.get_or_spawn(entity).insert(( ExtractedCamera { - target: camera.target.clone(), + target: camera.target.normalize(primary_window), viewport: camera.viewport.clone(), physical_viewport_size: Some(viewport_size), physical_target_size: Some(target_size), diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index f57929f30caeb..539383cd56e4a 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -1,5 +1,5 @@ use crate::{ - camera::{ExtractedCamera, RenderTarget}, + camera::{ExtractedCamera, NormalizedRenderTarget}, render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue}, renderer::RenderContext, view::ExtractedWindows, @@ -52,8 +52,8 @@ impl Node for CameraDriverNode { } previous_order_target = Some(new_order_target); if let Ok((_, camera)) = self.cameras.get_manual(world, entity) { - if let RenderTarget::Window(id) = camera.target { - camera_windows.insert(id); + if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target { + camera_windows.insert(window_ref.entity()); } graph .run_sub_graph(camera.render_graph.clone(), vec![SlotValue::Entity(entity)])?; @@ -98,7 +98,7 @@ impl Node for CameraDriverNode { }; render_context - .command_encoder + .command_encoder() .begin_render_pass(&pass_descriptor); } diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 1e30953ee109b..c3d6591e17b2d 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -19,7 +19,7 @@ impl Default for CameraProjectionPlugin { } } -/// Label for [`camera_system`], shared accross all `T`. +/// Label for [`camera_system`], shared across all `T`. /// /// [`camera_system`]: crate::camera::camera_system #[derive(SystemLabel, Clone, Eq, PartialEq, Hash, Debug)] diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index 4b9574dc22a75..18817aec03f3f 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -91,6 +91,7 @@ impl Color { ///

pub const NAVY: Color = Color::rgb(0.0, 0.0, 0.5); ///
+ #[doc(alias = "transparent")] pub const NONE: Color = Color::rgba(0.0, 0.0, 0.0, 0.0); ///
pub const OLIVE: Color = Color::rgb(0.5, 0.5, 0.0); @@ -259,37 +260,29 @@ impl Color { let hex = hex.as_ref(); let hex = hex.strip_prefix('#').unwrap_or(hex); - // RGB - if hex.len() == 3 { - let mut data = [0; 6]; - for (i, ch) in hex.chars().enumerate() { - data[i * 2] = ch as u8; - data[i * 2 + 1] = ch as u8; + match *hex.as_bytes() { + // RGB + [r, g, b] => { + let [r, g, b, ..] = decode_hex([r, r, g, g, b, b])?; + Ok(Color::rgb_u8(r, g, b)) } - return decode_rgb(&data); - } - - // RGBA - if hex.len() == 4 { - let mut data = [0; 8]; - for (i, ch) in hex.chars().enumerate() { - data[i * 2] = ch as u8; - data[i * 2 + 1] = ch as u8; + // RGBA + [r, g, b, a] => { + let [r, g, b, a, ..] = decode_hex([r, r, g, g, b, b, a, a])?; + Ok(Color::rgba_u8(r, g, b, a)) } - return decode_rgba(&data); - } - - // RRGGBB - if hex.len() == 6 { - return decode_rgb(hex.as_bytes()); - } - - // RRGGBBAA - if hex.len() == 8 { - return decode_rgba(hex.as_bytes()); + // RRGGBB + [r1, r2, g1, g2, b1, b2] => { + let [r, g, b, ..] = decode_hex([r1, r2, g1, g2, b1, b2])?; + Ok(Color::rgb_u8(r, g, b)) + } + // RRGGBBAA + [r1, r2, g1, g2, b1, b2, a1, a2] => { + let [r, g, b, a, ..] = decode_hex([r1, r2, g1, g2, b1, b2, a1, a2])?; + Ok(Color::rgba_u8(r, g, b, a)) + } + _ => Err(HexColorError::Length), } - - Err(HexColorError::Length) } /// New `Color` from sRGB colorspace. @@ -1336,38 +1329,49 @@ impl encase::private::CreateFrom for Color { impl encase::ShaderSize for Color {} -#[derive(Debug, Error)] +#[derive(Debug, Error, PartialEq, Eq)] pub enum HexColorError { #[error("Unexpected length of hex string")] Length, - #[error("Error parsing hex value")] - Hex(#[from] hex::FromHexError), + #[error("Invalid hex char")] + Char(char), } -fn decode_rgb(data: &[u8]) -> Result { - let mut buf = [0; 3]; - match hex::decode_to_slice(data, &mut buf) { - Ok(_) => { - let r = buf[0] as f32 / 255.0; - let g = buf[1] as f32 / 255.0; - let b = buf[2] as f32 / 255.0; - Ok(Color::rgb(r, g, b)) - } - Err(err) => Err(HexColorError::Hex(err)), +/// Converts hex bytes to an array of RGB\[A\] components +/// +/// # Example +/// For RGB: *b"ffffff" -> [255, 255, 255, ..] +/// For RGBA: *b"E2E2E2FF" -> [226, 226, 226, 255, ..] +const fn decode_hex(mut bytes: [u8; N]) -> Result<[u8; N], HexColorError> { + let mut i = 0; + while i < bytes.len() { + // Convert single hex digit to u8 + let val = match hex_value(bytes[i]) { + Ok(val) => val, + Err(byte) => return Err(HexColorError::Char(byte as char)), + }; + bytes[i] = val; + i += 1; } + // Modify the original bytes to give an `N / 2` length result + i = 0; + while i < bytes.len() / 2 { + // Convert pairs of u8 to R/G/B/A + // e.g `ff` -> [102, 102] -> [15, 15] = 255 + bytes[i] = bytes[i * 2] * 16 + bytes[i * 2 + 1]; + i += 1; + } + Ok(bytes) } -fn decode_rgba(data: &[u8]) -> Result { - let mut buf = [0; 4]; - match hex::decode_to_slice(data, &mut buf) { - Ok(_) => { - let r = buf[0] as f32 / 255.0; - let g = buf[1] as f32 / 255.0; - let b = buf[2] as f32 / 255.0; - let a = buf[3] as f32 / 255.0; - Ok(Color::rgba(r, g, b, a)) - } - Err(err) => Err(HexColorError::Hex(err)), +/// Parse a single hex digit (a-f/A-F/0-9) as a `u8` +const fn hex_value(b: u8) -> Result { + match b { + b'0'..=b'9' => Ok(b - b'0'), + b'A'..=b'F' => Ok(b - b'A' + 10), + b'a'..=b'f' => Ok(b - b'a' + 10), + // Wrong hex digit + _ => Err(b), } } @@ -1377,29 +1381,21 @@ mod tests { #[test] fn hex_color() { - assert_eq!(Color::hex("FFF").unwrap(), Color::rgb(1.0, 1.0, 1.0)); - assert_eq!(Color::hex("000").unwrap(), Color::rgb(0.0, 0.0, 0.0)); - assert!(Color::hex("---").is_err()); - - assert_eq!(Color::hex("FFFF").unwrap(), Color::rgba(1.0, 1.0, 1.0, 1.0)); - assert_eq!(Color::hex("0000").unwrap(), Color::rgba(0.0, 0.0, 0.0, 0.0)); - assert!(Color::hex("----").is_err()); - - assert_eq!(Color::hex("FFFFFF").unwrap(), Color::rgb(1.0, 1.0, 1.0)); - assert_eq!(Color::hex("000000").unwrap(), Color::rgb(0.0, 0.0, 0.0)); - assert!(Color::hex("------").is_err()); - - assert_eq!( - Color::hex("FFFFFFFF").unwrap(), - Color::rgba(1.0, 1.0, 1.0, 1.0) - ); - assert_eq!( - Color::hex("00000000").unwrap(), - Color::rgba(0.0, 0.0, 0.0, 0.0) - ); - assert!(Color::hex("--------").is_err()); - - assert!(Color::hex("1234567890").is_err()); + assert_eq!(Color::hex("FFF"), Ok(Color::WHITE)); + assert_eq!(Color::hex("FFFF"), Ok(Color::WHITE)); + assert_eq!(Color::hex("FFFFFF"), Ok(Color::WHITE)); + assert_eq!(Color::hex("FFFFFFFF"), Ok(Color::WHITE)); + assert_eq!(Color::hex("000"), Ok(Color::BLACK)); + assert_eq!(Color::hex("000F"), Ok(Color::BLACK)); + assert_eq!(Color::hex("000000"), Ok(Color::BLACK)); + assert_eq!(Color::hex("000000FF"), Ok(Color::BLACK)); + assert_eq!(Color::hex("03a9f4"), Ok(Color::rgb_u8(3, 169, 244))); + assert_eq!(Color::hex("yy"), Err(HexColorError::Length)); + assert_eq!(Color::hex("yyy"), Err(HexColorError::Char('y'))); + assert_eq!(Color::hex("#f2a"), Ok(Color::rgb_u8(255, 34, 170))); + assert_eq!(Color::hex("#e23030"), Ok(Color::rgb_u8(226, 48, 48))); + assert_eq!(Color::hex("#ff"), Err(HexColorError::Length)); + assert_eq!(Color::hex("##fff"), Err(HexColorError::Char('#'))); } #[test] diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index b3d4ba665a55a..24ad4d70ebc6e 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -1,10 +1,7 @@ use crate::MainWorld; use bevy_ecs::{ prelude::*, - system::{ - ReadOnlySystemParam, ResState, SystemMeta, SystemParam, SystemParamItem, SystemParamState, - SystemState, - }, + system::{ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemState}, }; use std::ops::{Deref, DerefMut}; @@ -49,42 +46,42 @@ where item: SystemParamItem<'w, 's, P>, } -impl<'w, 's, P> SystemParam for Extract<'w, 's, P> -where - P: ReadOnlySystemParam, -{ - type State = ExtractState

; -} - #[doc(hidden)] pub struct ExtractState { state: SystemState

, - main_world_state: ResState, + main_world_state: as SystemParam>::State, } -// SAFETY: only accesses MainWorld resource with read only system params using ResState, -// which is initialized in init() -unsafe impl SystemParamState for ExtractState

+// SAFETY: The only `World` access (`Res`) is read-only. +unsafe impl

ReadOnlySystemParam for Extract<'_, '_, P> where P: ReadOnlySystemParam {} + +// SAFETY: The only `World` access is properly registered by `Res::init_state`. +// This call will also ensure that there are no conflicts with prior params. +unsafe impl

SystemParam for Extract<'_, '_, P> where - P: ReadOnlySystemParam + 'static, + P: ReadOnlySystemParam, { + type State = ExtractState

; type Item<'w, 's> = Extract<'w, 's, P>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let mut main_world = world.resource_mut::(); - Self { + ExtractState { state: SystemState::new(&mut main_world), - main_world_state: ResState::init(world, system_meta), + main_world_state: Res::::init_state(world, system_meta), } } unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - let main_world = ResState::::get_param( + // SAFETY: + // - The caller ensures that `world` is the same one that `init_state` was called with. + // - The caller ensures that no other `SystemParam`s will conflict with the accesses we have registered. + let main_world = Res::::get_param( &mut state.main_world_state, system_meta, world, @@ -95,7 +92,7 @@ where } } -impl<'w, 's, P: SystemParam> Deref for Extract<'w, 's, P> +impl<'w, 's, P> Deref for Extract<'w, 's, P> where P: ReadOnlySystemParam, { @@ -107,7 +104,7 @@ where } } -impl<'w, 's, P: SystemParam> DerefMut for Extract<'w, 's, P> +impl<'w, 's, P> DerefMut for Extract<'w, 's, P> where P: ReadOnlySystemParam, { @@ -117,7 +114,7 @@ where } } -impl<'a, 'w, 's, P: SystemParam> IntoIterator for &'a Extract<'w, 's, P> +impl<'a, 'w, 's, P> IntoIterator for &'a Extract<'w, 's, P> where P: ReadOnlySystemParam, &'a SystemParamItem<'w, 's, P>: IntoIterator, diff --git a/crates/bevy_render/src/extract_resource.rs b/crates/bevy_render/src/extract_resource.rs index 3fa92750421c8..af726c5c8a38d 100644 --- a/crates/bevy_render/src/extract_resource.rs +++ b/crates/bevy_render/src/extract_resource.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use bevy_app::{App, Plugin}; +use bevy_ecs::change_detection::DetectChanges; #[cfg(debug_assertions)] use bevy_ecs::system::Local; use bevy_ecs::system::{Commands, Res, ResMut, Resource}; diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 715c1a1f9548b..0eca1967fb916 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -10,8 +10,8 @@ mod extract_param; pub mod extract_resource; pub mod globals; pub mod mesh; +pub mod pipelined_rendering; pub mod primitives; -pub mod rangefinder; pub mod render_asset; pub mod render_graph; pub mod render_phase; @@ -38,6 +38,7 @@ pub mod prelude { }; } +use bevy_window::{PrimaryWindow, RawHandleWrapper}; use globals::GlobalsPlugin; pub use once_cell; @@ -51,7 +52,7 @@ use crate::{ }; use bevy_app::{App, AppLabel, Plugin}; use bevy_asset::{AddAsset, AssetServer}; -use bevy_ecs::prelude::*; +use bevy_ecs::{prelude::*, system::SystemState}; use bevy_utils::tracing::debug; use std::{ any::TypeId, @@ -72,6 +73,9 @@ pub enum RenderStage { /// running the next frame while rendering the current frame. Extract, + /// A stage for applying the commands from the [`Extract`] stage + ExtractCommands, + /// Prepare render resources from the extracted data for the GPU. Prepare, @@ -92,6 +96,10 @@ pub enum RenderStage { Cleanup, } +/// Resource for holding the extract stage of the rendering schedule. +#[derive(Resource)] +pub struct ExtractStage(pub SystemStage); + /// The simulation [`World`] of the application, stored as a resource. /// This resource is only available during [`RenderStage::Extract`] and not /// during command application of that stage. @@ -135,17 +143,17 @@ impl Plugin for RenderPlugin { .init_asset_loader::() .init_debug_asset_loader::(); + let mut system_state: SystemState>> = + SystemState::new(&mut app.world); + let primary_window = system_state.get(&app.world); + if let Some(backends) = self.wgpu_settings.backends { - let windows = app.world.resource_mut::(); let instance = wgpu::Instance::new(backends); - - let surface = windows - .get_primary() - .and_then(|window| window.raw_handle()) - .map(|wrapper| unsafe { - let handle = wrapper.get_handle(); - instance.create_surface(&handle) - }); + let surface = primary_window.get_single().ok().map(|wrapper| unsafe { + // SAFETY: Plugins should be set up on the main thread. + let handle = wrapper.get_handle(); + instance.create_surface(&handle) + }); let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: self.wgpu_settings.power_preference, @@ -187,18 +195,29 @@ impl Plugin for RenderPlugin { // after access to the main world is removed // See also https://github.com/bevyengine/bevy/issues/5082 extract_stage.set_apply_buffers(false); + + // This stage applies the commands from the extract stage while the render schedule + // is running in parallel with the main app. + let mut extract_commands_stage = SystemStage::parallel(); + extract_commands_stage.add_system(apply_extract_commands.at_start()); render_app .add_stage(RenderStage::Extract, extract_stage) + .add_stage(RenderStage::ExtractCommands, extract_commands_stage) .add_stage(RenderStage::Prepare, SystemStage::parallel()) .add_stage(RenderStage::Queue, SystemStage::parallel()) .add_stage(RenderStage::PhaseSort, SystemStage::parallel()) .add_stage( RenderStage::Render, SystemStage::parallel() + // Note: Must run before `render_system` in order to + // processed newly queued pipelines. .with_system(PipelineCache::process_pipeline_queue_system) .with_system(render_system.at_end()), ) - .add_stage(RenderStage::Cleanup, SystemStage::parallel()) + .add_stage( + RenderStage::Cleanup, + SystemStage::parallel().with_system(World::clear_entities.at_end()), + ) .init_resource::() .insert_resource(RenderInstance(instance)) .insert_resource(device) @@ -214,7 +233,7 @@ impl Plugin for RenderPlugin { app.add_sub_app(RenderApp, render_app, move |app_world, render_app| { #[cfg(feature = "trace")] - let _render_span = bevy_utils::tracing::info_span!("renderer subapp").entered(); + let _render_span = bevy_utils::tracing::info_span!("extract main app to render subapp").entered(); { #[cfg(feature = "trace")] let _stage_span = @@ -248,78 +267,6 @@ impl Plugin for RenderPlugin { // extract extract(app_world, render_app); } - - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "prepare").entered(); - - // prepare - let prepare = render_app - .schedule - .get_stage_mut::(RenderStage::Prepare) - .unwrap(); - prepare.run(&mut render_app.world); - } - - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "queue").entered(); - - // queue - let queue = render_app - .schedule - .get_stage_mut::(RenderStage::Queue) - .unwrap(); - queue.run(&mut render_app.world); - } - - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "sort").entered(); - - // phase sort - let phase_sort = render_app - .schedule - .get_stage_mut::(RenderStage::PhaseSort) - .unwrap(); - phase_sort.run(&mut render_app.world); - } - - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "render").entered(); - - // render - let render = render_app - .schedule - .get_stage_mut::(RenderStage::Render) - .unwrap(); - render.run(&mut render_app.world); - } - - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "cleanup").entered(); - - // cleanup - let cleanup = render_app - .schedule - .get_stage_mut::(RenderStage::Cleanup) - .unwrap(); - cleanup.run(&mut render_app.world); - } - { - #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("stage", name = "clear_entities").entered(); - - render_app.world.clear_entities(); - } }); } @@ -335,6 +282,20 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::(); } + + fn setup(&self, app: &mut App) { + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + // move the extract stage to a resource so render_app.run() does not run it. + let stage = render_app + .schedule + .remove_stage(RenderStage::Extract) + .unwrap() + .downcast::() + .unwrap(); + + render_app.world.insert_resource(ExtractStage(*stage)); + } + } } /// A "scratch" world used to avoid allocating new worlds every frame when @@ -345,25 +306,25 @@ struct ScratchMainWorld(World); /// Executes the [`Extract`](RenderStage::Extract) stage of the renderer. /// This updates the render world with the extracted ECS data of the current frame. fn extract(app_world: &mut World, render_app: &mut App) { - let extract = render_app - .schedule - .get_stage_mut::(RenderStage::Extract) - .unwrap(); - - // temporarily add the app world to the render world as a resource - let scratch_world = app_world.remove_resource::().unwrap(); - let inserted_world = std::mem::replace(app_world, scratch_world.0); - let running_world = &mut render_app.world; - running_world.insert_resource(MainWorld(inserted_world)); - - extract.run(running_world); - // move the app world back, as if nothing happened. - let inserted_world = running_world.remove_resource::().unwrap(); - let scratch_world = std::mem::replace(app_world, inserted_world.0); - app_world.insert_resource(ScratchMainWorld(scratch_world)); - - // Note: We apply buffers (read, Commands) after the `MainWorld` has been removed from the render app's world - // so that in future, pipelining will be able to do this too without any code relying on it. - // see - extract.apply_buffers(running_world); + render_app + .world + .resource_scope(|render_world, mut extract_stage: Mut| { + // temporarily add the app world to the render world as a resource + let scratch_world = app_world.remove_resource::().unwrap(); + let inserted_world = std::mem::replace(app_world, scratch_world.0); + render_world.insert_resource(MainWorld(inserted_world)); + + extract_stage.0.run(render_world); + // move the app world back, as if nothing happened. + let inserted_world = render_world.remove_resource::().unwrap(); + let scratch_world = std::mem::replace(app_world, inserted_world.0); + app_world.insert_resource(ScratchMainWorld(scratch_world)); + }); +} + +// system for render app to apply the extract commands +fn apply_extract_commands(world: &mut World) { + world.resource_scope(|world, mut extract_stage: Mut| { + extract_stage.0.apply_buffers(world); + }); } diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs new file mode 100644 index 0000000000000..63c0a0cfb74b6 --- /dev/null +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -0,0 +1,155 @@ +use async_channel::{Receiver, Sender}; + +use bevy_app::{App, AppLabel, Plugin, SubApp}; +use bevy_ecs::{ + schedule::{MainThreadExecutor, StageLabel, SystemStage}, + system::Resource, + world::{Mut, World}, +}; +use bevy_tasks::ComputeTaskPool; + +use crate::RenderApp; + +/// A Label for the sub app that runs the parts of pipelined rendering that need to run on the main thread. +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)] +pub struct RenderExtractApp; + +/// Labels for stages in the [`RenderExtractApp`] sub app. These will run after rendering has started. +#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)] +pub enum RenderExtractStage { + /// When pipelined rendering is enabled this stage runs after the render schedule starts, but + /// before I/O processing and the main app schedule. This can be useful for something like + /// frame pacing. + BeforeIoAfterRenderStart, +} + +/// Channel to send the render app from the main thread to the rendering thread +#[derive(Resource)] +pub struct MainToRenderAppSender(pub Sender); + +/// Channel to send the render app from the render thread to the main thread +#[derive(Resource)] +pub struct RenderToMainAppReceiver(pub Receiver); + +/// The [`PipelinedRenderingPlugin`] can be added to your application to enable pipelined rendering. +/// This moves rendering into a different thread, so that the Nth frame's rendering can +/// be run at the same time as the N + 1 frame's simulation. +/// +/// ```text +/// |--------------------|--------------------|--------------------|--------------------| +/// | simulation thread | frame 1 simulation | frame 2 simulation | frame 3 simulation | +/// |--------------------|--------------------|--------------------|--------------------| +/// | rendering thread | | frame 1 rendering | frame 2 rendering | +/// |--------------------|--------------------|--------------------|--------------------| +/// ``` +/// +/// The plugin is dependent on the [`crate::RenderApp`] added by [`crate::RenderPlugin`] and so must +/// be added after that plugin. If it is not added after, the plugin will do nothing. +/// +/// A single frame of execution looks something like below +/// +/// ```text +/// |-------------------------------------------------------------------| +/// | | BeforeIoAfterRenderStart | winit events | main schedule | +/// | extract |---------------------------------------------------------| +/// | | extract commands | rendering schedule | +/// |-------------------------------------------------------------------| +/// ``` +/// +/// - `extract` is the stage where data is copied from the main world to the render world. +/// This is run on the main app's thread. +/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the +/// main schedule can start sooner. +/// - Then the `rendering schedule` is run. See [`crate::RenderStage`] for the available stages. +/// - In parallel to the rendering thread we first run the [`RenderExtractStage::BeforeIoAfterRenderStart`] stage. By +/// default this stage is empty. But is useful if you need something to run before I/O processing. +/// - Next all the `winit events` are processed. +/// - And finally the `main app schedule` is run. +/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again. +#[derive(Default)] +pub struct PipelinedRenderingPlugin; + +impl Plugin for PipelinedRenderingPlugin { + fn build(&self, app: &mut App) { + // Don't add RenderExtractApp if RenderApp isn't initialized. + if app.get_sub_app(RenderApp).is_err() { + return; + } + app.insert_resource(MainThreadExecutor::new()); + + let mut sub_app = App::empty(); + sub_app.add_stage( + RenderExtractStage::BeforeIoAfterRenderStart, + SystemStage::parallel(), + ); + app.add_sub_app(RenderExtractApp, sub_app, update_rendering); + } + + // Sets up the render thread and inserts resources into the main app used for controlling the render thread. + fn setup(&self, app: &mut App) { + // skip setting up when headless + if app.get_sub_app(RenderExtractApp).is_err() { + return; + } + + let (app_to_render_sender, app_to_render_receiver) = async_channel::bounded::(1); + let (render_to_app_sender, render_to_app_receiver) = async_channel::bounded::(1); + + let mut render_app = app + .remove_sub_app(RenderApp) + .expect("Unable to get RenderApp. Another plugin may have removed the RenderApp before PipelinedRenderingPlugin"); + + // clone main thread executor to render world + let executor = app.world.get_resource::().unwrap(); + render_app.app.world.insert_resource(executor.clone()); + + render_to_app_sender.send_blocking(render_app).unwrap(); + + app.insert_resource(MainToRenderAppSender(app_to_render_sender)); + app.insert_resource(RenderToMainAppReceiver(render_to_app_receiver)); + + std::thread::spawn(move || { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("render thread").entered(); + + loop { + // run a scope here to allow main world to use this thread while it's waiting for the render app + let mut render_app = ComputeTaskPool::get() + .scope(|s| { + s.spawn(async { app_to_render_receiver.recv().await.unwrap() }); + }) + .pop() + .unwrap(); + + #[cfg(feature = "trace")] + let _sub_app_span = + bevy_utils::tracing::info_span!("sub app", name = ?RenderApp).entered(); + render_app.run(); + render_to_app_sender.send_blocking(render_app).unwrap(); + } + }); + } +} + +// This function waits for the rendering world to be received, +// runs extract, and then sends the rendering world back to the render thread. +fn update_rendering(app_world: &mut World, _sub_app: &mut App) { + app_world.resource_scope(|world, main_thread_executor: Mut| { + // we use a scope here to run any main thread tasks that the render world still needs to run + // while we wait for the render world to be received. + let mut render_app = ComputeTaskPool::get() + .scope_with_executor(true, Some(&*main_thread_executor.0), |s| { + s.spawn(async { + let receiver = world.get_resource::().unwrap(); + receiver.0.recv().await.unwrap() + }); + }) + .pop() + .unwrap(); + + render_app.extract(world); + + let sender = world.resource::(); + sender.0.send_blocking(render_app).unwrap(); + }); +} diff --git a/crates/bevy_render/src/render_graph/node_slot.rs b/crates/bevy_render/src/render_graph/node_slot.rs index 5f04794b5e353..fea24d2f473b4 100644 --- a/crates/bevy_render/src/render_graph/node_slot.rs +++ b/crates/bevy_render/src/render_graph/node_slot.rs @@ -115,7 +115,7 @@ impl From<&'static str> for SlotLabel { impl From> for SlotLabel { fn from(value: Cow<'static, str>) -> Self { - SlotLabel::Name(value.clone()) + SlotLabel::Name(value) } } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index d0a60d529d57c..f0421b1b64b47 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -1,34 +1,30 @@ -use crate::{ - render_phase::TrackedRenderPass, - render_resource::{CachedRenderPipelineId, PipelineCache}, -}; +use crate::render_phase::{PhaseItem, TrackedRenderPass}; use bevy_app::App; use bevy_ecs::{ all_tuples, entity::Entity, query::{QueryState, ROQueryItem, ReadOnlyWorldQuery}, - system::{ - lifetimeless::SRes, ReadOnlySystemParam, Resource, SystemParam, SystemParamItem, - SystemState, - }, + system::{ReadOnlySystemParam, Resource, SystemParam, SystemParamItem, SystemState}, world::World, }; use bevy_utils::HashMap; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use std::{any::TypeId, fmt::Debug, hash::Hash, ops::Range}; +use std::{any::TypeId, fmt::Debug, hash::Hash}; -/// A draw function which is used to draw a specific [`PhaseItem`]. +/// A draw function used to draw [`PhaseItem`]s. +/// +/// The draw function can retrieve and query the required ECS data from the render world. /// -/// They are the general form of drawing items, whereas [`RenderCommands`](RenderCommand) -/// are more modular. +/// This trait can either be implemented directly or implicitly composed out of multiple modular +/// [`RenderCommand`]s. For more details and an example see the [`RenderCommand`] documentation. pub trait Draw: Send + Sync + 'static { /// Prepares the draw function to be used. This is called once and only once before the phase - /// begins. There may be zero or more `draw` calls following a call to this function.. + /// begins. There may be zero or more `draw` calls following a call to this function. /// Implementing this is optional. #[allow(unused_variables)] fn prepare(&mut self, world: &'_ World) {} - /// Draws the [`PhaseItem`] by issuing draw calls via the [`TrackedRenderPass`]. + /// Draws a [`PhaseItem`] by issuing zero or more `draw` calls via the [`TrackedRenderPass`]. fn draw<'w>( &mut self, world: &'w World, @@ -38,64 +34,33 @@ pub trait Draw: Send + Sync + 'static { ); } -/// An item which will be drawn to the screen. A phase item should be queued up for rendering -/// during the [`RenderStage::Queue`](crate::RenderStage::Queue) stage. -/// Afterwards it will be sorted and rendered automatically in the -/// [`RenderStage::PhaseSort`](crate::RenderStage::PhaseSort) stage and -/// [`RenderStage::Render`](crate::RenderStage::Render) stage, respectively. -pub trait PhaseItem: Sized + Send + Sync + 'static { - /// The type used for ordering the items. The smallest values are drawn first. - type SortKey: Ord; - fn entity(&self) -> Entity; - - /// Determines the order in which the items are drawn during the corresponding [`RenderPhase`](super::RenderPhase). - fn sort_key(&self) -> Self::SortKey; - /// Specifies the [`Draw`] function used to render the item. - fn draw_function(&self) -> DrawFunctionId; - - /// Sorts a slice of phase items into render order. Generally if the same type - /// implements [`BatchedPhaseItem`], this should use a stable sort like [`slice::sort_by_key`]. - /// In almost all other cases, this should not be altered from the default, - /// which uses a unstable sort, as this provides the best balance of CPU and GPU - /// performance. - /// - /// Implementers can optionally not sort the list at all. This is generally advisable if and - /// only if the renderer supports a depth prepass, which is by default not supported by - /// the rest of Bevy's first party rendering crates. Even then, this may have a negative - /// impact on GPU-side performance due to overdraw. - /// - /// It's advised to always profile for performance changes when changing this implementation. - #[inline] - fn sort(items: &mut [Self]) { - items.sort_unstable_by_key(|item| item.sort_key()); - } -} - // TODO: make this generic? /// An identifier for a [`Draw`] function stored in [`DrawFunctions`]. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct DrawFunctionId(u32); -/// Stores all draw functions for the [`PhaseItem`] type. -/// For retrieval they are associated with their [`TypeId`]. +/// Stores all [`Draw`] functions for the [`PhaseItem`] type. +/// +/// For retrieval, the [`Draw`] functions are mapped to their respective [`TypeId`]s. pub struct DrawFunctionsInternal { pub draw_functions: Vec>>, pub indices: HashMap, } impl DrawFunctionsInternal

{ + /// Prepares all draw function. This is called once and only once before the phase begins. pub fn prepare(&mut self, world: &World) { for function in &mut self.draw_functions { function.prepare(world); } } - /// Adds the [`Draw`] function and associates it to its own type. + /// Adds the [`Draw`] function and maps it to its own type. pub fn add>(&mut self, draw_function: T) -> DrawFunctionId { self.add_with::(draw_function) } - /// Adds the [`Draw`] function and associates it to the type `T` + /// Adds the [`Draw`] function and maps it to the type `T` pub fn add_with>(&mut self, draw_function: D) -> DrawFunctionId { let id = DrawFunctionId(self.draw_functions.len().try_into().unwrap()); self.draw_functions.push(Box::new(draw_function)); @@ -118,7 +83,7 @@ impl DrawFunctionsInternal

{ /// Fallible wrapper for [`Self::get_id()`] /// /// ## Panics - /// If the id doesn't exist it will panic + /// If the id doesn't exist, this function will panic. pub fn id(&self) -> DrawFunctionId { self.get_id::().unwrap_or_else(|| { panic!( @@ -131,6 +96,7 @@ impl DrawFunctionsInternal

{ } /// Stores all draw functions for the [`PhaseItem`] type hidden behind a reader-writer lock. +/// /// To access them the [`DrawFunctions::read`] and [`DrawFunctions::write`] methods are used. #[derive(Resource)] pub struct DrawFunctions { @@ -160,15 +126,26 @@ impl DrawFunctions

{ } } -/// [`RenderCommand`] is a trait that runs an ECS query and produces one or more -/// [`TrackedRenderPass`] calls. Types implementing this trait can be composed (as tuples). +/// [`RenderCommand`]s are modular standardized pieces of render logic that can be composed into +/// [`Draw`] functions. /// -/// They can be registered as a [`Draw`] function via the +/// To turn a stateless render command into a usable draw function it has to be wrapped by a +/// [`RenderCommandState`]. +/// This is done automatically when registering a render command as a [`Draw`] function via the /// [`AddRenderCommand::add_render_command`] method. /// +/// Compared to the draw function the required ECS data is fetched automatically +/// (by the [`RenderCommandState`]) from the render world. +/// Therefore the three types [`Param`](RenderCommand::Param), +/// [`ViewWorldQuery`](RenderCommand::ViewWorldQuery) and +/// [`ItemWorldQuery`](RenderCommand::ItemWorldQuery) are used. +/// They specify which information is required to execute the render command. +/// +/// Multiple render commands can be combined together by wrapping them in a tuple. +/// /// # Example /// The `DrawPbr` draw function is created from the following render command -/// tuple. Const generics are used to set specific bind group locations: +/// tuple. Const generics are used to set specific bind group locations: /// /// ```ignore /// pub type DrawPbr = ( @@ -180,13 +157,24 @@ impl DrawFunctions

{ /// ); /// ``` pub trait RenderCommand { - /// Specifies all ECS data required by [`RenderCommand::render`]. + /// Specifies the general ECS data (e.g. resources) required by [`RenderCommand::render`]. + /// /// All parameters have to be read only. type Param: SystemParam + 'static; + /// Specifies the ECS data of the view entity required by [`RenderCommand::render`]. + /// + /// The view entity refers to the camera, or shadow-casting light, etc. from which the phase + /// item will be rendered from. + /// All components have to be accessed read only. type ViewWorldQuery: ReadOnlyWorldQuery; + /// Specifies the ECS data of the item entity required by [`RenderCommand::render`]. + /// + /// The item is the entity that will be rendered for the corresponding view. + /// All components have to be accessed read only. type ItemWorldQuery: ReadOnlyWorldQuery; - /// Renders the [`PhaseItem`] by issuing draw calls via the [`TrackedRenderPass`]. + /// Renders a [`PhaseItem`] by recording commands (e.g. setting pipelines, binding bind groups, + /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, view: ROQueryItem<'w, Self::ViewWorldQuery>, @@ -196,86 +184,12 @@ pub trait RenderCommand { ) -> RenderCommandResult; } +/// The result of a [`RenderCommand`]. pub enum RenderCommandResult { Success, Failure, } -pub trait CachedRenderPipelinePhaseItem: PhaseItem { - fn cached_pipeline(&self) -> CachedRenderPipelineId; -} - -/// A [`PhaseItem`] that can be batched dynamically. -/// -/// Batching is an optimization that regroups multiple items in the same vertex buffer -/// to render them in a single draw call. -/// -/// If this is implemented on a type, the implementation of [`PhaseItem::sort`] should -/// be changed to implement a stable sort, or incorrect/suboptimal batching may result. -pub trait BatchedPhaseItem: PhaseItem { - /// Range in the vertex buffer of this item - fn batch_range(&self) -> &Option>; - - /// Range in the vertex buffer of this item - fn batch_range_mut(&mut self) -> &mut Option>; - - /// Batches another item within this item if they are compatible. - /// Items can be batched together if they have the same entity, and consecutive ranges. - /// If batching is successful, the `other` item should be discarded from the render pass. - #[inline] - fn add_to_batch(&mut self, other: &Self) -> BatchResult { - let self_entity = self.entity(); - if let (Some(self_batch_range), Some(other_batch_range)) = ( - self.batch_range_mut().as_mut(), - other.batch_range().as_ref(), - ) { - // If the items are compatible, join their range into `self` - if self_entity == other.entity() { - if self_batch_range.end == other_batch_range.start { - self_batch_range.end = other_batch_range.end; - return BatchResult::Success; - } else if self_batch_range.start == other_batch_range.end { - self_batch_range.start = other_batch_range.start; - return BatchResult::Success; - } - } - } - BatchResult::IncompatibleItems - } -} - -pub enum BatchResult { - /// The `other` item was batched into `self` - Success, - /// `self` and `other` cannot be batched together - IncompatibleItems, -} - -pub struct SetItemPipeline; -impl RenderCommand

for SetItemPipeline { - type Param = SRes; - type ViewWorldQuery = (); - type ItemWorldQuery = (); - #[inline] - fn render<'w>( - item: &P, - _view: (), - _entity: (), - pipeline_cache: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - if let Some(pipeline) = pipeline_cache - .into_inner() - .get_render_pipeline(item.cached_pipeline()) - { - pass.set_render_pipeline(pipeline); - RenderCommandResult::Success - } else { - RenderCommandResult::Failure - } - } -} - macro_rules! render_command_tuple_impl { ($(($name: ident, $view: ident, $entity: ident)),*) => { impl),*> RenderCommand

for ($($name,)*) { @@ -303,7 +217,9 @@ macro_rules! render_command_tuple_impl { all_tuples!(render_command_tuple_impl, 0, 15, C, V, E); /// Wraps a [`RenderCommand`] into a state so that it can be used as a [`Draw`] function. -/// Therefore the [`RenderCommand::Param`] is queried from the ECS and passed to the command. +/// +/// The [`RenderCommand::Param`], [`RenderCommand::ViewWorldQuery`] and +/// [`RenderCommand::ItemWorldQuery`] are fetched from the ECS and passed to the command. pub struct RenderCommandState> { state: SystemState, view: QueryState, @@ -311,6 +227,7 @@ pub struct RenderCommandState> { } impl> RenderCommandState { + /// Creates a new [`RenderCommandState`] for the [`RenderCommand`]. pub fn new(world: &mut World) -> Self { Self { state: SystemState::new(world), @@ -324,12 +241,15 @@ impl + Send + Sync + 'static> Draw

for Rend where C::Param: ReadOnlySystemParam, { + /// Prepares the render command to be used. This is called once and only once before the phase + /// begins. There may be zero or more `draw` calls following a call to this function. fn prepare(&mut self, world: &'_ World) { + self.state.update_archetypes(world); self.view.update_archetypes(world); self.entity.update_archetypes(world); } - /// Prepares the ECS parameters for the wrapped [`RenderCommand`] and then renders it. + /// Fetches the ECS parameters for the wrapped [`RenderCommand`] and then renders it. fn draw<'w>( &mut self, world: &'w World, @@ -337,9 +257,10 @@ where view: Entity, item: &P, ) { - let param = self.state.get(world); + let param = self.state.get_manual(world); let view = self.view.get_manual(world, view).unwrap(); let entity = self.entity.get_manual(world, item.entity()).unwrap(); + // TODO: handle/log `RenderCommand` failure C::render(item, view, entity, param, pass); } } diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index 907712e6788a6..f228e46ef2690 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -5,14 +5,20 @@ use crate::{ BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId, ShaderStages, }, + renderer::RenderDevice, }; -use bevy_utils::tracing::trace; +use bevy_utils::{default, tracing::trace}; use std::ops::Range; use wgpu::{IndexFormat, RenderPass}; +use wgpu_hal::{MAX_BIND_GROUPS, MAX_VERTEX_BUFFERS}; -/// Tracks the current [`TrackedRenderPass`] state to ensure draw calls are valid. +/// Tracks the state of a [`TrackedRenderPass`]. +/// +/// This is used to skip redundant operations on the [`TrackedRenderPass`] (e.g. setting an already +/// set pipeline, binding an already bound bind group). These operations can otherwise be fairly +/// costly due to IO to the GPU, so deduplicating these calls results in a speedup. #[derive(Debug, Default)] -pub struct DrawState { +struct DrawState { pipeline: Option, bind_groups: Vec<(Option, Vec)>, vertex_buffers: Vec>, @@ -20,20 +26,34 @@ pub struct DrawState { } impl DrawState { + /// Marks the `pipeline` as bound. + pub fn set_pipeline(&mut self, pipeline: RenderPipelineId) { + // TODO: do these need to be cleared? + // self.bind_groups.clear(); + // self.vertex_buffers.clear(); + // self.index_buffer = None; + self.pipeline = Some(pipeline); + } + + /// Checks, whether the `pipeline` is already bound. + pub fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { + self.pipeline == Some(pipeline) + } + + /// Marks the `bind_group` as bound to the `index`. pub fn set_bind_group( &mut self, index: usize, bind_group: BindGroupId, dynamic_indices: &[u32], ) { - if index >= self.bind_groups.len() { - self.bind_groups.resize(index + 1, (None, Vec::new())); - } - self.bind_groups[index].0 = Some(bind_group); - self.bind_groups[index].1.clear(); - self.bind_groups[index].1.extend(dynamic_indices); + let group = &mut self.bind_groups[index]; + group.0 = Some(bind_group); + group.1.clear(); + group.1.extend(dynamic_indices); } + /// Checks, whether the `bind_group` is already bound to the `index`. pub fn is_bind_group_set( &self, index: usize, @@ -47,13 +67,12 @@ impl DrawState { } } + /// Marks the vertex `buffer` as bound to the `index`. pub fn set_vertex_buffer(&mut self, index: usize, buffer: BufferId, offset: u64) { - if index >= self.vertex_buffers.len() { - self.vertex_buffers.resize(index + 1, None); - } self.vertex_buffers[index] = Some((buffer, offset)); } + /// Checks, whether the vertex `buffer` is already bound to the `index`. pub fn is_vertex_buffer_set(&self, index: usize, buffer: BufferId, offset: u64) -> bool { if let Some(current) = self.vertex_buffers.get(index) { *current == Some((buffer, offset)) @@ -62,10 +81,12 @@ impl DrawState { } } + /// Marks the index `buffer` as bound. pub fn set_index_buffer(&mut self, buffer: BufferId, offset: u64, index_format: IndexFormat) { self.index_buffer = Some((buffer, offset, index_format)); } + /// Checks, whether the index `buffer` is already bound. pub fn is_index_buffer_set( &self, buffer: BufferId, @@ -74,22 +95,11 @@ impl DrawState { ) -> bool { self.index_buffer == Some((buffer, offset, index_format)) } - - pub fn is_pipeline_set(&self, pipeline: RenderPipelineId) -> bool { - self.pipeline == Some(pipeline) - } - - pub fn set_pipeline(&mut self, pipeline: RenderPipelineId) { - // TODO: do these need to be cleared? - // self.bind_groups.clear(); - // self.vertex_buffers.clear(); - // self.index_buffer = None; - self.pipeline = Some(pipeline); - } } -/// A [`RenderPass`], which tracks the current pipeline state to ensure all draw calls are valid. -/// It is used to set the current [`RenderPipeline`], [`BindGroups`](BindGroup) and buffers. +/// A [`RenderPass`], which tracks the current pipeline state to skip redundant operations. +/// +/// It is used to set the current [`RenderPipeline`], [`BindGroup`]s and [`Buffer`]s. /// After all requirements are specified, draw calls can be issued. pub struct TrackedRenderPass<'a> { pass: RenderPass<'a>, @@ -98,9 +108,16 @@ pub struct TrackedRenderPass<'a> { impl<'a> TrackedRenderPass<'a> { /// Tracks the supplied render pass. - pub fn new(pass: RenderPass<'a>) -> Self { + pub fn new(device: &RenderDevice, pass: RenderPass<'a>) -> Self { + let limits = device.limits(); + let max_bind_groups = limits.max_bind_groups as usize; + let max_vertex_buffers = limits.max_vertex_buffers as usize; Self { - state: DrawState::default(), + state: DrawState { + bind_groups: vec![(None, Vec::new()); max_bind_groups.min(MAX_BIND_GROUPS)], + vertex_buffers: vec![None; max_vertex_buffers.min(MAX_VERTEX_BUFFERS)], + ..default() + }, pass, } } @@ -117,8 +134,13 @@ impl<'a> TrackedRenderPass<'a> { self.state.set_pipeline(pipeline.id()); } - /// Sets the active [`BindGroup`] for a given bind group index. The bind group layout in the - /// active pipeline when any `draw()` function is called must match the layout of this `bind group`. + /// Sets the active bind group for a given bind group index. The bind group layout + /// in the active pipeline when any `draw()` function is called must match the layout of + /// this bind group. + /// + /// If the bind group have dynamic offsets, provide them in binding order. + /// These offsets have to be aligned to [`WgpuLimits::min_uniform_buffer_offset_alignment`](crate::settings::WgpuLimits::min_uniform_buffer_offset_alignment) + /// or [`WgpuLimits::min_storage_buffer_offset_alignment`](crate::settings::WgpuLimits::min_storage_buffer_offset_alignment) appropriately. pub fn set_bind_group( &mut self, index: usize, @@ -152,11 +174,14 @@ impl<'a> TrackedRenderPass<'a> { /// Assign a vertex buffer to a slot. /// - /// Subsequent calls to [`TrackedRenderPass::draw`] and [`TrackedRenderPass::draw_indexed`] - /// will use the buffer referenced by `buffer_slice` as one of the source vertex buffer(s). + /// Subsequent calls to [`draw`] and [`draw_indexed`] on this + /// [`RenderPass`] will use `buffer` as one of the source vertex buffers. /// /// The `slot_index` refers to the index of the matching descriptor in /// [`VertexState::buffers`](crate::render_resource::VertexState::buffers). + /// + /// [`draw`]: TrackedRenderPass::draw + /// [`draw_indexed`]: TrackedRenderPass::draw_indexed pub fn set_vertex_buffer(&mut self, slot_index: usize, buffer_slice: BufferSlice<'a>) { let offset = buffer_slice.offset(); if self @@ -233,7 +258,8 @@ impl<'a> TrackedRenderPass<'a> { self.pass.draw_indexed(indices, base_vertex, instances); } - /// Draws primitives from the active vertex buffer(s) based on the contents of the `indirect_buffer`. + /// Draws primitives from the active vertex buffer(s) based on the contents of the + /// `indirect_buffer`. /// /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// @@ -257,8 +283,8 @@ impl<'a> TrackedRenderPass<'a> { /// Draws indexed primitives using the active index buffer and the active vertex buffers, /// based on the contents of the `indirect_buffer`. /// - /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the active - /// vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// /// The structure expected in `indirect_buffer` is the following: /// @@ -283,8 +309,8 @@ impl<'a> TrackedRenderPass<'a> { .draw_indexed_indirect(indirect_buffer, indirect_offset); } - /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of the `indirect_buffer`. - /// `count` draw calls are issued. + /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of the + /// `indirect_buffer`.`count` draw calls are issued. /// /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// @@ -316,11 +342,13 @@ impl<'a> TrackedRenderPass<'a> { .multi_draw_indirect(indirect_buffer, indirect_offset, count); } - /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of the `indirect_buffer`. + /// Dispatches multiple draw calls from the active vertex buffer(s) based on the contents of + /// the `indirect_buffer`. /// The count buffer is read to determine how many draws to issue. /// - /// The indirect buffer must be long enough to account for `max_count` draws, however only `count` elements - /// will be read, where `count` is the value read from `count_buffer` capped at `max_count`. + /// The indirect buffer must be long enough to account for `max_count` draws, however only + /// `count` elements will be read, where `count` is the value read from `count_buffer` capped + /// at `max_count`. /// /// The active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// @@ -364,8 +392,8 @@ impl<'a> TrackedRenderPass<'a> { /// Dispatches multiple draw calls from the active index buffer and the active vertex buffers, /// based on the contents of the `indirect_buffer`. `count` draw calls are issued. /// - /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the active - /// vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// @@ -397,13 +425,15 @@ impl<'a> TrackedRenderPass<'a> { } /// Dispatches multiple draw calls from the active index buffer and the active vertex buffers, - /// based on the contents of the `indirect_buffer`. The count buffer is read to determine how many draws to issue. + /// based on the contents of the `indirect_buffer`. + /// The count buffer is read to determine how many draws to issue. /// - /// The indirect buffer must be long enough to account for `max_count` draws, however only `count` elements - /// will be read, where `count` is the value read from `count_buffer` capped at `max_count`. + /// The indirect buffer must be long enough to account for `max_count` draws, however only + /// `count` elements will be read, where `count` is the value read from `count_buffer` capped + /// at `max_count`. /// - /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the active - /// vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. + /// The active index buffer can be set with [`TrackedRenderPass::set_index_buffer`], while the + /// active vertex buffers can be set with [`TrackedRenderPass::set_vertex_buffer`]. /// /// `indirect_buffer` should contain `count` tightly packed elements of the following structure: /// @@ -497,7 +527,7 @@ impl<'a> TrackedRenderPass<'a> { .set_viewport(x, y, width, height, min_depth, max_depth); } - /// Set the rendering viewport to the given [`Camera`](crate::camera::Viewport) [`Viewport`]. + /// Set the rendering viewport to the given camera [`Viewport`]. /// /// Subsequent draw calls will be projected into that viewport. pub fn set_camera_viewport(&mut self, viewport: &Viewport) { @@ -561,6 +591,9 @@ impl<'a> TrackedRenderPass<'a> { self.pass.pop_debug_group(); } + /// Sets the blend color as used by some of the blending modes. + /// + /// Subsequent blending tests will test against this value. pub fn set_blend_constant(&mut self, color: Color) { trace!("set blend constant: {:?}", color); self.pass.set_blend_constant(wgpu::Color::from(color)); diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index b0b502422b928..54226a5d3b1d6 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -1,14 +1,55 @@ +//! The modular rendering abstraction responsible for queuing, preparing, sorting and drawing +//! entities as part of separate render phases. +//! +//! In Bevy each view (camera, or shadow-casting light, etc.) has one or multiple [`RenderPhase`]s +//! (e.g. opaque, transparent, shadow, etc). +//! They are used to queue entities for rendering. +//! Multiple phases might be required due to different sorting/batching behaviours +//! (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +//! the rendered texture of the previous phase (e.g. for screen-space reflections). +//! +//! To draw an entity, a corresponding [`PhaseItem`] has to be added to one or multiple of these +//! render phases for each view that it is visible in. +//! This must be done in the [`RenderStage::Queue`](crate::RenderStage::Queue). +//! After that the render phase sorts them in the +//! [`RenderStage::PhaseSort`](crate::RenderStage::PhaseSort). +//! Finally the items are rendered using a single [`TrackedRenderPass`], during the +//! [`RenderStage::Render`](crate::RenderStage::Render). +//! +//! Therefore each phase item is assigned a [`Draw`] function. +//! These set up the state of the [`TrackedRenderPass`] (i.e. select the +//! [`RenderPipeline`](crate::render_resource::RenderPipeline), configure the +//! [`BindGroup`](crate::render_resource::BindGroup)s, etc.) and then issue a draw call, +//! for the corresponding item. +//! +//! The [`Draw`] function trait can either be implemented directly or such a function can be +//! created by composing multiple [`RenderCommand`]s. + mod draw; mod draw_state; +mod rangefinder; -use bevy_ecs::entity::Entity; pub use draw::*; pub use draw_state::*; +pub use rangefinder::*; -use bevy_ecs::prelude::{Component, Query}; -use bevy_ecs::world::World; +use crate::render_resource::{CachedRenderPipelineId, PipelineCache}; +use bevy_ecs::{ + prelude::*, + system::{lifetimeless::SRes, SystemParamItem}, +}; +use std::ops::Range; -/// A resource to collect and sort draw requests for specific [`PhaseItems`](PhaseItem). +/// A collection of all rendering instructions, that will be executed by the GPU, for a +/// single render phase for a single view. +/// +/// Each view (camera, or shadow-casting light, etc.) can have one or multiple render phases. +/// They are used to queue entities for rendering. +/// Multiple phases might be required due to different sorting/batching behaviours +/// (e.g. opaque: front to back, transparent: back to front) or because one phase depends on +/// the rendered texture of the previous phase (e.g. for screen-space reflections). +/// All [`PhaseItem`]s are then rendered using a single [`TrackedRenderPass`]. +/// The render pass might be reused for multiple phases to reduce GPU overhead. #[derive(Component)] pub struct RenderPhase { pub items: Vec, @@ -27,11 +68,12 @@ impl RenderPhase { self.items.push(item); } - /// Sorts all of its [`PhaseItems`](PhaseItem). + /// Sorts all of its [`PhaseItem`]s. pub fn sort(&mut self) { I::sort(&mut self.items); } + /// Renders all of its [`PhaseItem`]s using their corresponding draw functions. pub fn render<'w>( &self, render_pass: &mut TrackedRenderPass<'w>, @@ -76,7 +118,138 @@ impl RenderPhase { } } -/// This system sorts all [`RenderPhases`](RenderPhase) for the [`PhaseItem`] type. +/// An item (entity of the render world) which will be drawn to a texture or the screen, +/// as part of a [`RenderPhase`]. +/// +/// The data required for rendering an entity is extracted from the main world in the +/// [`RenderStage::Extract`](crate::RenderStage::Extract). +/// Then it has to be queued up for rendering during the +/// [`RenderStage::Queue`](crate::RenderStage::Queue), by adding a corresponding phase item to +/// a render phase. +/// Afterwards it will be sorted and rendered automatically in the +/// [`RenderStage::PhaseSort`](crate::RenderStage::PhaseSort) and +/// [`RenderStage::Render`](crate::RenderStage::Render), respectively. +pub trait PhaseItem: Sized + Send + Sync + 'static { + /// The type used for ordering the items. The smallest values are drawn first. + /// This order can be calculated using the [`ViewRangefinder3d`], + /// based on the view-space `Z` value of the corresponding view matrix. + type SortKey: Ord; + + /// The corresponding entity that will be drawn. + /// + /// This is used to fetch the render data of the entity, required by the draw function, + /// from the render world . + fn entity(&self) -> Entity; + + /// Determines the order in which the items are drawn. + fn sort_key(&self) -> Self::SortKey; + + /// Specifies the [`Draw`] function used to render the item. + fn draw_function(&self) -> DrawFunctionId; + + /// Sorts a slice of phase items into render order. Generally if the same type + /// implements [`BatchedPhaseItem`], this should use a stable sort like [`slice::sort_by_key`]. + /// In almost all other cases, this should not be altered from the default, + /// which uses a unstable sort, as this provides the best balance of CPU and GPU + /// performance. + /// + /// Implementers can optionally not sort the list at all. This is generally advisable if and + /// only if the renderer supports a depth prepass, which is by default not supported by + /// the rest of Bevy's first party rendering crates. Even then, this may have a negative + /// impact on GPU-side performance due to overdraw. + /// + /// It's advised to always profile for performance changes when changing this implementation. + #[inline] + fn sort(items: &mut [Self]) { + items.sort_unstable_by_key(|item| item.sort_key()); + } +} + +/// A [`PhaseItem`] item, that automatically sets the appropriate render pipeline, +/// cached in the [`PipelineCache`]. +/// +/// You can use the [`SetItemPipeline`] render command to set the pipeline for this item. +pub trait CachedRenderPipelinePhaseItem: PhaseItem { + /// The id of the render pipeline, cached in the [`PipelineCache`], that will be used to draw + /// this phase item. + fn cached_pipeline(&self) -> CachedRenderPipelineId; +} + +/// A [`RenderCommand`] that sets the pipeline for the [`CachedRenderPipelinePhaseItem`]. +pub struct SetItemPipeline; + +impl RenderCommand

for SetItemPipeline { + type Param = SRes; + type ViewWorldQuery = (); + type ItemWorldQuery = (); + #[inline] + fn render<'w>( + item: &P, + _view: (), + _entity: (), + pipeline_cache: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + if let Some(pipeline) = pipeline_cache + .into_inner() + .get_render_pipeline(item.cached_pipeline()) + { + pass.set_render_pipeline(pipeline); + RenderCommandResult::Success + } else { + RenderCommandResult::Failure + } + } +} + +/// A [`PhaseItem`] that can be batched dynamically. +/// +/// Batching is an optimization that regroups multiple items in the same vertex buffer +/// to render them in a single draw call. +/// +/// If this is implemented on a type, the implementation of [`PhaseItem::sort`] should +/// be changed to implement a stable sort, or incorrect/suboptimal batching may result. +pub trait BatchedPhaseItem: PhaseItem { + /// Range in the vertex buffer of this item. + fn batch_range(&self) -> &Option>; + + /// Range in the vertex buffer of this item. + fn batch_range_mut(&mut self) -> &mut Option>; + + /// Batches another item within this item if they are compatible. + /// Items can be batched together if they have the same entity, and consecutive ranges. + /// If batching is successful, the `other` item should be discarded from the render pass. + #[inline] + fn add_to_batch(&mut self, other: &Self) -> BatchResult { + let self_entity = self.entity(); + if let (Some(self_batch_range), Some(other_batch_range)) = ( + self.batch_range_mut().as_mut(), + other.batch_range().as_ref(), + ) { + // If the items are compatible, join their range into `self` + if self_entity == other.entity() { + if self_batch_range.end == other_batch_range.start { + self_batch_range.end = other_batch_range.end; + return BatchResult::Success; + } else if self_batch_range.start == other_batch_range.end { + self_batch_range.start = other_batch_range.start; + return BatchResult::Success; + } + } + } + BatchResult::IncompatibleItems + } +} + +/// The result of a batching operation. +pub enum BatchResult { + /// The `other` item was batched into `self` + Success, + /// `self` and `other` cannot be batched together + IncompatibleItems, +} + +/// This system sorts the [`PhaseItem`]s of all [`RenderPhase`]s of this type. pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { for mut phase in &mut render_phases { phase.sort(); @@ -92,11 +265,9 @@ pub fn batch_phase_system(mut render_phases: Query<&mut Ren #[cfg(test)] mod tests { - use std::ops::Range; - - use bevy_ecs::entity::Entity; - use super::*; + use bevy_ecs::entity::Entity; + use std::ops::Range; #[test] fn batching() { @@ -108,7 +279,7 @@ mod tests { impl PhaseItem for TestPhaseItem { type SortKey = (); - fn entity(&self) -> bevy_ecs::entity::Entity { + fn entity(&self) -> Entity { self.entity } @@ -119,11 +290,11 @@ mod tests { } } impl BatchedPhaseItem for TestPhaseItem { - fn batch_range(&self) -> &Option> { + fn batch_range(&self) -> &Option> { &self.batch_range } - fn batch_range_mut(&mut self) -> &mut Option> { + fn batch_range_mut(&mut self) -> &mut Option> { &mut self.batch_range } } diff --git a/crates/bevy_render/src/rangefinder.rs b/crates/bevy_render/src/render_phase/rangefinder.rs similarity index 89% rename from crates/bevy_render/src/rangefinder.rs rename to crates/bevy_render/src/render_phase/rangefinder.rs index c12ab3af16b11..797782b9ccf66 100644 --- a/crates/bevy_render/src/rangefinder.rs +++ b/crates/bevy_render/src/render_phase/rangefinder.rs @@ -6,15 +6,16 @@ pub struct ViewRangefinder3d { } impl ViewRangefinder3d { - /// Creates a 3D rangefinder for a view matrix + /// Creates a 3D rangefinder for a view matrix. pub fn from_view_matrix(view_matrix: &Mat4) -> ViewRangefinder3d { let inverse_view_matrix = view_matrix.inverse(); + ViewRangefinder3d { inverse_view_row_2: inverse_view_matrix.row(2), } } - /// Calculates the distance, or view-space `Z` value, for a transform + /// Calculates the distance, or view-space `Z` value, for the given `transform`. #[inline] pub fn distance(&self, transform: &Mat4) -> f32 { // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix diff --git a/crates/bevy_render/src/render_resource/bind_group.rs b/crates/bevy_render/src/render_resource/bind_group.rs index 5bc13b1d1f47a..46e30771b03bd 100644 --- a/crates/bevy_render/src/render_resource/bind_group.rs +++ b/crates/bevy_render/src/render_resource/bind_group.rs @@ -82,6 +82,8 @@ impl Deref for BindGroup { /// #[texture(1)] /// #[sampler(2)] /// color_texture: Handle, +/// #[storage(3, read_only)] +/// values: Vec, /// } /// ``` /// @@ -94,6 +96,8 @@ impl Deref for BindGroup { /// var color_texture: texture_2d; /// @group(1) @binding(2) /// var color_sampler: sampler; +/// @group(1) @binding(3) +/// var values: array; /// ``` /// Note that the "group" index is determined by the usage context. It is not defined in [`AsBindGroup`]. For example, in Bevy material bind groups /// are generally bound to group 1. @@ -132,6 +136,15 @@ impl Deref for BindGroup { /// | `sampler_type` = "..." | `"filtering"`, `"non_filtering"`, `"comparison"`. | `"filtering"` | /// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | /// +/// * `storage(BINDING_INDEX, arguments)` +/// * The field will be converted to a shader-compatible type using the [`ShaderType`] trait, written to a [`Buffer`], and bound as a storage buffer. +/// * It supports and optional `read_only` parameter. Defaults to false if not present. +/// +/// | Arguments | Values | Default | +/// |------------------------|-------------------------------------------------------------------------|----------------------| +/// | `visibility(...)` | `all`, `none`, or a list-combination of `vertex`, `fragment`, `compute` | `vertex`, `fragment` | +/// | `read_only` | if present then value is true, otherwise false | `false` | +/// /// Note that fields without field-level binding attributes will be ignored. /// ``` /// # use bevy_render::{color::Color, render_resource::AsBindGroup}; diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 0209c2e8c9d97..36c54e6043e7d 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -17,6 +17,7 @@ use bevy_utils::{ tracing::{debug, error}, Entry, HashMap, HashSet, }; +use parking_lot::Mutex; use std::{hash::Hash, iter::FusedIterator, mem, ops::Deref}; use thiserror::Error; use wgpu::{PipelineLayoutDescriptor, VertexBufferLayout as RawVertexBufferLayout}; @@ -218,7 +219,7 @@ impl ShaderCache { let error = render_device.wgpu_device().pop_error_scope(); // `now_or_never` will return Some if the future is ready and None otherwise. - // On native platforms, wgpu will yield the error immediatly while on wasm it may take longer since the browser APIs are asynchronous. + // On native platforms, wgpu will yield the error immediately while on wasm it may take longer since the browser APIs are asynchronous. // So to keep the complexity of the ShaderCache low, we will only catch this error early on native platforms, // and on wasm the error will be handled by wgpu and crash the application. if let Some(Some(wgpu::Error::Validation { description, .. })) = @@ -343,6 +344,7 @@ pub struct PipelineCache { device: RenderDevice, pipelines: Vec, waiting_pipelines: HashSet, + new_pipelines: Mutex>, } impl PipelineCache { @@ -357,6 +359,7 @@ impl PipelineCache { layout_cache: default(), shader_cache: default(), waiting_pipelines: default(), + new_pipelines: default(), pipelines: default(), } } @@ -455,15 +458,15 @@ impl PipelineCache { /// [`get_render_pipeline_state()`]: PipelineCache::get_render_pipeline_state /// [`get_render_pipeline()`]: PipelineCache::get_render_pipeline pub fn queue_render_pipeline( - &mut self, + &self, descriptor: RenderPipelineDescriptor, ) -> CachedRenderPipelineId { - let id = CachedRenderPipelineId(self.pipelines.len()); - self.pipelines.push(CachedPipeline { + let mut new_pipelines = self.new_pipelines.lock(); + let id = CachedRenderPipelineId(self.pipelines.len() + new_pipelines.len()); + new_pipelines.push(CachedPipeline { descriptor: PipelineDescriptor::RenderPipelineDescriptor(Box::new(descriptor)), state: CachedPipelineState::Queued, }); - self.waiting_pipelines.insert(id.0); id } @@ -481,15 +484,15 @@ impl PipelineCache { /// [`get_compute_pipeline_state()`]: PipelineCache::get_compute_pipeline_state /// [`get_compute_pipeline()`]: PipelineCache::get_compute_pipeline pub fn queue_compute_pipeline( - &mut self, + &self, descriptor: ComputePipelineDescriptor, ) -> CachedComputePipelineId { - let id = CachedComputePipelineId(self.pipelines.len()); - self.pipelines.push(CachedPipeline { + let mut new_pipelines = self.new_pipelines.lock(); + let id = CachedComputePipelineId(self.pipelines.len() + new_pipelines.len()); + new_pipelines.push(CachedPipeline { descriptor: PipelineDescriptor::ComputePipelineDescriptor(Box::new(descriptor)), state: CachedPipelineState::Queued, }); - self.waiting_pipelines.insert(id.0); id } @@ -632,9 +635,18 @@ impl PipelineCache { /// /// [`RenderStage::Render`]: crate::RenderStage::Render pub fn process_queue(&mut self) { - let waiting_pipelines = mem::take(&mut self.waiting_pipelines); + let mut waiting_pipelines = mem::take(&mut self.waiting_pipelines); let mut pipelines = mem::take(&mut self.pipelines); + { + let mut new_pipelines = self.new_pipelines.lock(); + for new_pipeline in new_pipelines.drain(..) { + let id = pipelines.len(); + pipelines.push(new_pipeline); + waiting_pipelines.insert(id); + } + } + for id in waiting_pipelines { let pipeline = &mut pipelines[id]; if matches!(pipeline.state, CachedPipelineState::Ok(_)) { diff --git a/crates/bevy_render/src/render_resource/pipeline_specializer.rs b/crates/bevy_render/src/render_resource/pipeline_specializer.rs index d685ca7f228a5..10035133adcec 100644 --- a/crates/bevy_render/src/render_resource/pipeline_specializer.rs +++ b/crates/bevy_render/src/render_resource/pipeline_specializer.rs @@ -33,7 +33,7 @@ impl Default for SpecializedRenderPipelines { impl SpecializedRenderPipelines { pub fn specialize( &mut self, - cache: &mut PipelineCache, + cache: &PipelineCache, specialize_pipeline: &S, key: S::Key, ) -> CachedRenderPipelineId { @@ -103,7 +103,7 @@ impl SpecializedMeshPipelines { #[inline] pub fn specialize( &mut self, - cache: &mut PipelineCache, + cache: &PipelineCache, specialize_pipeline: &S, key: S::Key, layout: &MeshVertexBufferLayout, diff --git a/crates/bevy_render/src/render_resource/resource_macros.rs b/crates/bevy_render/src/render_resource/resource_macros.rs index e27380426a169..26aaceb4fc92f 100644 --- a/crates/bevy_render/src/render_resource/resource_macros.rs +++ b/crates/bevy_render/src/render_resource/resource_macros.rs @@ -124,7 +124,7 @@ macro_rules! render_resource_wrapper { macro_rules! define_atomic_id { ($atomic_id_type:ident) => { #[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] - pub struct $atomic_id_type(u32); + pub struct $atomic_id_type(core::num::NonZeroU32); // We use new instead of default to indicate that each ID created will be unique. #[allow(clippy::new_without_default)] @@ -134,15 +134,13 @@ macro_rules! define_atomic_id { static COUNTER: AtomicU32 = AtomicU32::new(1); - match COUNTER.fetch_add(1, Ordering::Relaxed) { - 0 => { - panic!( - "The system ran out of unique `{}`s.", - stringify!($atomic_id_type) - ); - } - id => Self(id), - } + let counter = COUNTER.fetch_add(1, Ordering::Relaxed); + Self(core::num::NonZeroU32::new(counter).unwrap_or_else(|| { + panic!( + "The system ran out of unique `{}`s.", + stringify!($atomic_id_type) + ); + })) } } }; diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index 1513d1c4e5697..e11c61f6ca064 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -58,18 +58,12 @@ impl RenderGraphRunner { queue: &wgpu::Queue, world: &World, ) -> Result<(), RenderGraphRunnerError> { - let command_encoder = - render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); - let mut render_context = RenderContext { - render_device, - command_encoder, - }; - + let mut render_context = RenderContext::new(render_device); Self::run_graph(graph, None, &mut render_context, world, &[])?; { #[cfg(feature = "trace")] let _span = info_span!("submit_graph_commands").entered(); - queue.submit(vec![render_context.command_encoder.finish()]); + queue.submit(render_context.finish()); } Ok(()) } @@ -115,7 +109,7 @@ impl RenderGraphRunner { return Err(RenderGraphRunnerError::MissingInput { slot_index: i, slot_name: input_slot.name.clone(), - graph_name: graph_name.clone(), + graph_name, }); } } diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index aa362a3d9b2ed..5885003bfb76e 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -8,6 +8,8 @@ pub use render_device::*; use crate::{ render_graph::RenderGraph, + render_phase::TrackedRenderPass, + render_resource::RenderPassDescriptor, settings::{WgpuSettings, WgpuSettingsPriority}, view::{ExtractedWindows, ViewTarget}, }; @@ -15,7 +17,9 @@ use bevy_ecs::prelude::*; use bevy_time::TimeSender; use bevy_utils::Instant; use std::sync::Arc; -use wgpu::{Adapter, AdapterInfo, CommandEncoder, Instance, Queue, RequestAdapterOptions}; +use wgpu::{ + Adapter, AdapterInfo, CommandBuffer, CommandEncoder, Instance, Queue, RequestAdapterOptions, +}; /// Updates the [`RenderGraph`] with all of its nodes and then runs it to render the entire frame. pub fn render_system(world: &mut World) { @@ -276,6 +280,68 @@ pub async fn initialize_renderer( /// The [`RenderDevice`] is used to create render resources and the /// the [`CommandEncoder`] is used to record a series of GPU operations. pub struct RenderContext { - pub render_device: RenderDevice, - pub command_encoder: CommandEncoder, + render_device: RenderDevice, + command_encoder: Option, + command_buffers: Vec, +} + +impl RenderContext { + /// Creates a new [`RenderContext`] from a [`RenderDevice`]. + pub fn new(render_device: RenderDevice) -> Self { + Self { + render_device, + command_encoder: None, + command_buffers: Vec::new(), + } + } + + /// Gets the underlying [`RenderDevice`]. + pub fn render_device(&self) -> &RenderDevice { + &self.render_device + } + + /// Gets the current [`CommandEncoder`]. + pub fn command_encoder(&mut self) -> &mut CommandEncoder { + self.command_encoder.get_or_insert_with(|| { + self.render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()) + }) + } + + /// Creates a new [`TrackedRenderPass`] for the context, + /// configured using the provided `descriptor`. + pub fn begin_tracked_render_pass<'a>( + &'a mut self, + descriptor: RenderPassDescriptor<'a, '_>, + ) -> TrackedRenderPass<'a> { + // Cannot use command_encoder() as we need to split the borrow on self + let command_encoder = self.command_encoder.get_or_insert_with(|| { + self.render_device + .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()) + }); + let render_pass = command_encoder.begin_render_pass(&descriptor); + TrackedRenderPass::new(&self.render_device, render_pass) + } + + /// Append a [`CommandBuffer`] to the queue. + /// + /// If present, this will flush the currently unflushed [`CommandEncoder`] + /// into a [`CommandBuffer`] into the queue before append the provided + /// buffer. + pub fn add_command_buffer(&mut self, command_buffer: CommandBuffer) { + self.flush_encoder(); + self.command_buffers.push(command_buffer); + } + + /// Finalizes the queue and returns the queue of [`CommandBuffer`]s. + pub fn finish(mut self) -> Vec { + self.flush_encoder(); + self.command_buffers + } + + fn flush_encoder(&mut self) { + if let Some(encoder) = self.command_encoder.take() { + self.command_buffers.push(encoder.finish()); + } + } } diff --git a/crates/bevy_render/src/texture/basis.rs b/crates/bevy_render/src/texture/basis.rs index b404c4d38689b..886a0cb9bb7df 100644 --- a/crates/bevy_render/src/texture/basis.rs +++ b/crates/bevy_render/src/texture/basis.rs @@ -33,14 +33,12 @@ pub fn basis_buffer_to_image( let basis_texture_format = transcoder.basis_texture_format(buffer); if !basis_texture_format.can_transcode_to_format(transcode_format) { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?} cannot be transcoded to {:?}", - basis_texture_format, transcode_format + "{basis_texture_format:?} cannot be transcoded to {transcode_format:?}", ))); } transcoder.prepare_transcoding(buffer).map_err(|_| { TextureError::TranscodeError(format!( - "Failed to prepare for transcoding from {:?}", - basis_texture_format + "Failed to prepare for transcoding from {basis_texture_format:?}", )) })?; let mut transcoded = Vec::new(); @@ -49,8 +47,7 @@ pub fn basis_buffer_to_image( let texture_type = transcoder.basis_texture_type(buffer); if texture_type == BasisTextureType::TextureTypeCubemapArray && image_count % 6 != 0 { return Err(TextureError::InvalidData(format!( - "Basis file with cube map array texture with non-modulo 6 number of images: {}", - image_count, + "Basis file with cube map array texture with non-modulo 6 number of images: {image_count}", ))); } @@ -74,10 +71,7 @@ pub fn basis_buffer_to_image( let mip_level_count = transcoder.image_level_count(buffer, image_index); if mip_level_count != image0_mip_level_count { return Err(TextureError::InvalidData(format!( - "Array or volume texture has inconsistent number of mip levels. Image {} has {} but image 0 has {}", - image_index, - mip_level_count, - image0_mip_level_count, + "Array or volume texture has inconsistent number of mip levels. Image {image_index} has {mip_level_count} but image 0 has {image0_mip_level_count}", ))); } for level_index in 0..mip_level_count { @@ -94,8 +88,7 @@ pub fn basis_buffer_to_image( ) .map_err(|error| { TextureError::TranscodeError(format!( - "Failed to transcode mip level {} from {:?} to {:?}: {:?}", - level_index, basis_texture_format, transcode_format, error + "Failed to transcode mip level {level_index} from {basis_texture_format:?} to {transcode_format:?}: {error:?}", )) })?; transcoded.append(&mut data); @@ -118,8 +111,7 @@ pub fn basis_buffer_to_image( BasisTextureType::TextureTypeVolume => TextureDimension::D3, basis_texture_type => { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - basis_texture_type + "{basis_texture_type:?}", ))) } }; diff --git a/crates/bevy_render/src/texture/dds.rs b/crates/bevy_render/src/texture/dds.rs index 5d3e0efe647c6..bd008a3600fdf 100644 --- a/crates/bevy_render/src/texture/dds.rs +++ b/crates/bevy_render/src/texture/dds.rs @@ -14,8 +14,7 @@ pub fn dds_buffer_to_image( let texture_format = dds_format_to_texture_format(&dds, is_srgb)?; if !supported_compressed_formats.supports(texture_format) { return Err(TextureError::UnsupportedTextureFormat(format!( - "Format not supported by this GPU: {:?}", - texture_format + "Format not supported by this GPU: {texture_format:?}", ))); } let mut image = Image::default(); @@ -116,8 +115,7 @@ pub fn dds_format_to_texture_format( | D3DFormat::YUY2 | D3DFormat::CXV8U8 => { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - d3d_format + "{d3d_format:?}", ))) } } @@ -227,8 +225,7 @@ pub fn dds_format_to_texture_format( } _ => { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - dxgi_format + "{dxgi_format:?}", ))) } } diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index 6f0a32e3604ac..9b8ef0c500498 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -1,13 +1,17 @@ use crate::{render_resource::*, texture::DefaultImageSampler}; -use bevy_derive::Deref; -use bevy_ecs::{prelude::FromWorld, system::Resource}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{FromWorld, Res, ResMut}, + system::{Resource, SystemParam}, +}; use bevy_math::Vec2; +use bevy_utils::HashMap; use wgpu::{Extent3d, TextureDimension, TextureFormat}; use crate::{ prelude::Image, renderer::{RenderDevice, RenderQueue}, - texture::{BevyDefault, GpuImage, ImageSampler}, + texture::{image::TextureFormatPixelInfo, BevyDefault, GpuImage, ImageSampler}, }; /// A [`RenderApp`](crate::RenderApp) resource that contains the default "fallback image", @@ -17,36 +21,117 @@ use crate::{ #[derive(Resource, Deref)] pub struct FallbackImage(GpuImage); +fn fallback_image_new( + render_device: &RenderDevice, + render_queue: &RenderQueue, + default_sampler: &DefaultImageSampler, + format: TextureFormat, + samples: u32, +) -> GpuImage { + // TODO make this configurable + let data = vec![255; format.pixel_size()]; + + let mut image = Image::new_fill(Extent3d::default(), TextureDimension::D2, &data, format); + image.texture_descriptor.sample_count = samples; + image.texture_descriptor.usage |= TextureUsages::RENDER_ATTACHMENT; + + // We can't create textures with data when it's a depth texture or when using multiple samples + let texture = if format.describe().sample_type == TextureSampleType::Depth || samples > 1 { + render_device.create_texture(&image.texture_descriptor) + } else { + render_device.create_texture_with_data(render_queue, &image.texture_descriptor, &image.data) + }; + + let texture_view = texture.create_view(&TextureViewDescriptor::default()); + let sampler = match image.sampler_descriptor { + ImageSampler::Default => (**default_sampler).clone(), + ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor), + }; + GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size: Vec2::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), + } +} + impl FromWorld for FallbackImage { fn from_world(world: &mut bevy_ecs::prelude::World) -> Self { let render_device = world.resource::(); let render_queue = world.resource::(); let default_sampler = world.resource::(); - let image = Image::new_fill( - Extent3d::default(), - TextureDimension::D2, - &[255u8; 4], - TextureFormat::bevy_default(), - ); - let texture = render_device.create_texture_with_data( + Self(fallback_image_new( + render_device, render_queue, - &image.texture_descriptor, - &image.data, - ); - let texture_view = texture.create_view(&TextureViewDescriptor::default()); - let sampler = match image.sampler_descriptor { - ImageSampler::Default => (**default_sampler).clone(), - ImageSampler::Descriptor(descriptor) => render_device.create_sampler(&descriptor), - }; - Self(GpuImage { - texture, - texture_view, - texture_format: image.texture_descriptor.format, - sampler, - size: Vec2::new( - image.texture_descriptor.size.width as f32, - image.texture_descriptor.size.height as f32, - ), + default_sampler, + TextureFormat::bevy_default(), + 1, + )) + } +} + +// TODO these could be combined in one FallbackImage cache. + +/// A Cache of fallback textures that uses the sample count as a key +/// +/// # WARNING +/// Images using MSAA with sample count > 1 are not initialized with data, therefore, +/// you shouldn't sample them before writing data to them first. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct FallbackImageMsaaCache(HashMap); + +/// A Cache of fallback depth textures that uses the sample count as a key +/// +/// # WARNING +/// Detph images are never initialized with data, therefore, +/// you shouldn't sample them before writing data to them first. +#[derive(Resource, Deref, DerefMut, Default)] +pub struct FallbackImageDepthCache(HashMap); + +#[derive(SystemParam)] +pub struct FallbackImagesMsaa<'w> { + cache: ResMut<'w, FallbackImageMsaaCache>, + render_device: Res<'w, RenderDevice>, + render_queue: Res<'w, RenderQueue>, + default_sampler: Res<'w, DefaultImageSampler>, +} + +impl<'w> FallbackImagesMsaa<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32) -> &GpuImage { + self.cache.entry(sample_count).or_insert_with(|| { + fallback_image_new( + &self.render_device, + &self.render_queue, + &self.default_sampler, + TextureFormat::bevy_default(), + sample_count, + ) + }) + } +} + +#[derive(SystemParam)] +pub struct FallbackImagesDepth<'w> { + cache: ResMut<'w, FallbackImageDepthCache>, + render_device: Res<'w, RenderDevice>, + render_queue: Res<'w, RenderQueue>, + default_sampler: Res<'w, DefaultImageSampler>, +} + +impl<'w> FallbackImagesDepth<'w> { + pub fn image_for_samplecount(&mut self, sample_count: u32) -> &GpuImage { + self.cache.entry(sample_count).or_insert_with(|| { + fallback_image_new( + &self.render_device, + &self.render_queue, + &self.default_sampler, + TextureFormat::Depth32Float, + sample_count, + ) }) } } diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_render/src/texture/ktx2.rs index 17cb8b435c423..9ad1ec9dbf5f3 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_render/src/texture/ktx2.rs @@ -51,8 +51,7 @@ pub fn ktx2_buffer_to_image( let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed).map_err(|err| { TextureError::SuperDecompressionError(format!( - "Failed to decompress {:?} for mip {}: {:?}", - supercompression_scheme, _level, err + "Failed to decompress {supercompression_scheme:?} for mip {_level}: {err:?}", )) })?; levels.push(decompressed); @@ -65,16 +64,14 @@ pub fn ktx2_buffer_to_image( let mut decompressed = Vec::new(); decoder.read_to_end(&mut decompressed).map_err(|err| { TextureError::SuperDecompressionError(format!( - "Failed to decompress {:?} for mip {}: {:?}", - supercompression_scheme, _level, err + "Failed to decompress {supercompression_scheme:?} for mip {_level}: {err:?}", )) })?; levels.push(decompressed); } _ => { return Err(TextureError::SuperDecompressionError(format!( - "Unsupported supercompression scheme: {:?}", - supercompression_scheme + "Unsupported supercompression scheme: {supercompression_scheme:?}", ))); } } @@ -159,8 +156,7 @@ pub fn ktx2_buffer_to_image( .map(|mut transcoded_level| transcoded[level].append(&mut transcoded_level)) .map_err(|error| { TextureError::SuperDecompressionError(format!( - "Failed to transcode mip level {} from UASTC to {:?}: {:?}", - level, transcode_block_format, error + "Failed to transcode mip level {level} from UASTC to {transcode_block_format:?}: {error:?}", )) })?; offset += level_bytes; @@ -193,8 +189,7 @@ pub fn ktx2_buffer_to_image( })?; if !supported_compressed_formats.supports(texture_format) { return Err(TextureError::UnsupportedTextureFormat(format!( - "Format not supported by this GPU: {:?}", - texture_format + "Format not supported by this GPU: {texture_format:?}", ))); } @@ -510,8 +505,7 @@ pub fn ktx2_dfd_to_texture_format( }, v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported sample bit length for RGBSDA 1-channel format: {}", - v + "Unsupported sample bit length for RGBSDA 1-channel format: {v}", ))); } } @@ -593,8 +587,7 @@ pub fn ktx2_dfd_to_texture_format( }, v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported sample bit length for RGBSDA 2-channel format: {}", - v + "Unsupported sample bit length for RGBSDA 2-channel format: {v}", ))); } } @@ -829,16 +822,14 @@ pub fn ktx2_dfd_to_texture_format( }, v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported sample bit length for RGBSDA 4-channel format: {}", - v + "Unsupported sample bit length for RGBSDA 4-channel format: {v}", ))); } } } v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported channel count for RGBSDA format: {}", - v + "Unsupported channel count for RGBSDA format: {v}", ))); } } @@ -954,16 +945,14 @@ pub fn ktx2_dfd_to_texture_format( }, v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported sample bit length for XYZW 4-channel format: {}", - v + "Unsupported sample bit length for XYZW 4-channel format: {v}", ))); } } } v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported channel count for XYZW format: {}", - v + "Unsupported channel count for XYZW format: {v}", ))); } } @@ -1088,8 +1077,7 @@ pub fn ktx2_dfd_to_texture_format( } v => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Unsupported channel count for ETC2 format: {}", - v + "Unsupported channel count for ETC2 format: {v}", ))); } }, @@ -1148,8 +1136,7 @@ pub fn ktx2_dfd_to_texture_format( 6 => DataFormat::Rg, channel_type => { return Err(TextureError::UnsupportedTextureFormat(format!( - "Invalid KTX2 UASTC channel type: {}", - channel_type + "Invalid KTX2 UASTC channel type: {channel_type}", ))) } }), @@ -1177,8 +1164,7 @@ pub fn ktx2_format_to_texture_format( ktx2::Format::R8_UNORM | ktx2::Format::R8_SRGB => { if is_srgb { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - ktx2_format + "{ktx2_format:?}" ))); } TextureFormat::R8Unorm @@ -1189,8 +1175,7 @@ pub fn ktx2_format_to_texture_format( ktx2::Format::R8G8_UNORM | ktx2::Format::R8G8_SRGB => { if is_srgb { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - ktx2_format + "{ktx2_format:?}" ))); } TextureFormat::Rg8Unorm @@ -1456,8 +1441,7 @@ pub fn ktx2_format_to_texture_format( } _ => { return Err(TextureError::UnsupportedTextureFormat(format!( - "{:?}", - ktx2_format + "{ktx2_format:?}" ))) } }) diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 89637d730b2fc..0517bf6a288de 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -102,6 +102,8 @@ impl Plugin for ImagePlugin { .insert_resource(DefaultImageSampler(default_sampler)) .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() .add_system_to_stage(RenderStage::Cleanup, update_texture_cache_system); } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index bc0c12b023135..4584f5359f6e8 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -8,8 +8,8 @@ use crate::{ camera::ExtractedCamera, extract_resource::{ExtractResource, ExtractResourcePlugin}, prelude::Image, - rangefinder::ViewRangefinder3d, render_asset::RenderAssets, + render_phase::ViewRangefinder3d, render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, TextureCache}, @@ -56,30 +56,34 @@ impl Plugin for ViewPlugin { /// Configuration resource for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). /// +/// The number of samples to run for Multi-Sample Anti-Aliasing. Higher numbers result in +/// smoother edges. +/// Defaults to 4. +/// +/// Note that WGPU currently only supports 1 or 4 samples. +/// Ultimately we plan on supporting whatever is natively supported on a given device. +/// Check out this issue for more info: +/// /// # Example /// ``` /// # use bevy_app::prelude::App; /// # use bevy_render::prelude::Msaa; /// App::new() -/// .insert_resource(Msaa { samples: 4 }) +/// .insert_resource(Msaa::default()) /// .run(); /// ``` -#[derive(Resource, Clone, ExtractResource, Reflect)] +#[derive(Resource, Default, Clone, Copy, ExtractResource, Reflect, PartialEq, PartialOrd)] #[reflect(Resource)] -pub struct Msaa { - /// The number of samples to run for Multi-Sample Anti-Aliasing. Higher numbers result in - /// smoother edges. - /// Defaults to 4. - /// - /// Note that WGPU currently only supports 1 or 4 samples. - /// Ultimately we plan on supporting whatever is natively supported on a given device. - /// Check out this issue for more info: - pub samples: u32, +pub enum Msaa { + Off = 1, + #[default] + Sample4 = 4, } -impl Default for Msaa { - fn default() -> Self { - Self { samples: 4 } +impl Msaa { + #[inline] + pub fn samples(&self) -> u32 { + *self as u32 } } @@ -285,10 +289,10 @@ fn prepare_view_targets( ) { let mut textures = HashMap::default(); for (entity, camera, view) in cameras.iter() { - if let Some(target_size) = camera.physical_target_size { + if let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target) { if let (Some(out_texture_view), Some(out_texture_format)) = ( - camera.target.get_texture_view(&windows, &images), - camera.target.get_texture_format(&windows, &images), + target.get_texture_view(&windows, &images), + target.get_texture_format(&windows, &images), ) { let size = Extent3d { width: target_size.x, @@ -334,7 +338,7 @@ fn prepare_view_targets( }, ) .default_view, - sampled: (msaa.samples > 1).then(|| { + sampled: (msaa.samples() > 1).then(|| { texture_cache .get( &render_device, @@ -342,7 +346,7 @@ fn prepare_view_targets( label: Some("main_texture_sampled"), size, mip_level_count: 1, - sample_count: msaa.samples, + sample_count: msaa.samples(), dimension: TextureDimension::D2, format: main_texture_format, usage: TextureUsages::RENDER_ATTACHMENT, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index e4a57217c1bc1..30f28b8638c4c 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -212,7 +212,7 @@ impl Plugin for VisibilityPlugin { app.add_system_to_stage( CoreStage::PostUpdate, - calculate_bounds.label(CalculateBounds), + calculate_bounds.label(CalculateBounds).before_commands(), ) .add_system_to_stage( CoreStage::PostUpdate, @@ -252,7 +252,6 @@ impl Plugin for VisibilityPlugin { CoreStage::PostUpdate, check_visibility .label(CheckVisibility) - .after(CalculateBounds) .after(UpdateOrthographicFrusta) .after(UpdatePerspectiveFrusta) .after(UpdateProjectionFrusta) @@ -351,9 +350,6 @@ fn propagate_recursive( Ok(()) } -// the batch size used for check_visibility, chosen because this number tends to perform well -const VISIBLE_ENTITIES_QUERY_BATCH_SIZE: usize = 1024; - /// System updating the visibility of entities each frame. /// /// The system is labelled with [`VisibilitySystems::CheckVisibility`]. Each frame, it updates the @@ -377,9 +373,9 @@ pub fn check_visibility( ) { for (mut visible_entities, frustum, maybe_view_mask) in &mut view_query { let view_mask = maybe_view_mask.copied().unwrap_or_default(); + visible_entities.entities.clear(); - visible_aabb_query.par_for_each_mut( - VISIBLE_ENTITIES_QUERY_BATCH_SIZE, + visible_aabb_query.par_iter_mut().for_each_mut( |( entity, mut computed_visibility, @@ -424,8 +420,7 @@ pub fn check_visibility( }, ); - visible_no_aabb_query.par_for_each_mut( - VISIBLE_ENTITIES_QUERY_BATCH_SIZE, + visible_no_aabb_query.par_iter_mut().for_each_mut( |(entity, mut computed_visibility, maybe_entity_mask)| { // skip computing visibility for entities that are configured to be hidden. is_visible_in_view has already been set to false // in visibility_propagate_system diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index 8e6f82c66c911..957455747f1ff 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -7,7 +7,7 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_window::{ - CompositeAlphaMode, PresentMode, RawHandleWrapper, WindowClosed, WindowId, Windows, + CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, }; use std::ops::{Deref, DerefMut}; use wgpu::TextureFormat; @@ -29,7 +29,7 @@ impl Plugin for WindowRenderPlugin { render_app .init_resource::() .init_resource::() - .init_resource::() + .init_non_send_resource::() .add_system_to_stage(RenderStage::Extract, extract_windows) .add_system_to_stage( RenderStage::Prepare, @@ -40,8 +40,9 @@ impl Plugin for WindowRenderPlugin { } pub struct ExtractedWindow { - pub id: WindowId, - pub raw_handle: Option, + /// An entity that contains the components in [`Window`]. + pub entity: Entity, + pub handle: RawHandleWrapper, pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, @@ -54,11 +55,12 @@ pub struct ExtractedWindow { #[derive(Default, Resource)] pub struct ExtractedWindows { - pub windows: HashMap, + pub primary: Option, + pub windows: HashMap, } impl Deref for ExtractedWindows { - type Target = HashMap; + type Target = HashMap; fn deref(&self) -> &Self::Target { &self.windows @@ -74,36 +76,37 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, mut closed: Extract>, - windows: Extract>, + windows: Extract)>>, ) { - for window in windows.iter() { + for (entity, window, handle, primary) in windows.iter() { + if primary.is_some() { + extracted_windows.primary = Some(entity); + } + let (new_width, new_height) = ( - window.physical_width().max(1), - window.physical_height().max(1), + window.resolution.physical_width().max(1), + window.resolution.physical_height().max(1), ); - let new_present_mode = window.present_mode(); - let mut extracted_window = - extracted_windows - .entry(window.id()) - .or_insert(ExtractedWindow { - id: window.id(), - raw_handle: window.raw_handle(), - physical_width: new_width, - physical_height: new_height, - present_mode: window.present_mode(), - swap_chain_texture: None, - swap_chain_texture_format: None, - size_changed: false, - present_mode_changed: false, - alpha_mode: window.alpha_mode(), - }); + let mut extracted_window = extracted_windows.entry(entity).or_insert(ExtractedWindow { + entity, + handle: handle.clone(), + physical_width: new_width, + physical_height: new_height, + present_mode: window.present_mode, + swap_chain_texture: None, + size_changed: false, + swap_chain_texture_format: None, + present_mode_changed: false, + alpha_mode: window.composite_alpha_mode, + }); // NOTE: Drop the swap chain frame here extracted_window.swap_chain_texture = None; extracted_window.size_changed = new_width != extracted_window.physical_width || new_height != extracted_window.physical_height; - extracted_window.present_mode_changed = new_present_mode != extracted_window.present_mode; + extracted_window.present_mode_changed = + window.present_mode != extracted_window.present_mode; if extracted_window.size_changed { debug!( @@ -120,13 +123,14 @@ fn extract_windows( if extracted_window.present_mode_changed { debug!( "Window Present Mode changed from {:?} to {:?}", - extracted_window.present_mode, new_present_mode + extracted_window.present_mode, window.present_mode ); - extracted_window.present_mode = new_present_mode; + extracted_window.present_mode = window.present_mode; } } + for closed_window in closed.iter() { - extracted_windows.remove(&closed_window.id); + extracted_windows.remove(&closed_window.window); } } @@ -137,9 +141,9 @@ struct SurfaceData { #[derive(Resource, Default)] pub struct WindowSurfaces { - surfaces: HashMap, + surfaces: HashMap, /// List of windows that we have already called the initial `configure_surface` for - configured_windows: HashSet, + configured_windows: HashSet, } /// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering. @@ -173,27 +177,20 @@ pub fn prepare_windows( render_instance: Res, render_adapter: Res, ) { - for window in windows - .windows - .values_mut() - // value of raw_handle is only None in synthetic tests - .filter(|x| x.raw_handle.is_some()) - { + for window in windows.windows.values_mut() { let window_surfaces = window_surfaces.deref_mut(); let surface_data = window_surfaces .surfaces - .entry(window.id) + .entry(window.entity) .or_insert_with(|| unsafe { // NOTE: On some OSes this MUST be called from the main thread. - let surface = render_instance - .create_surface(&window.raw_handle.as_ref().unwrap().get_handle()); + let surface = render_instance.create_surface(&window.handle.get_handle()); let format = *surface .get_supported_formats(&render_adapter) .get(0) .unwrap_or_else(|| { panic!( - "No supported formats found for surface {:?} on adapter {:?}", - surface, render_adapter + "No supported formats found for surface {surface:?} on adapter {render_adapter:?}" ) }); SurfaceData { surface, format } @@ -237,7 +234,7 @@ pub fn prepare_windows( }) }; - let not_already_configured = window_surfaces.configured_windows.insert(window.id); + let not_already_configured = window_surfaces.configured_windows.insert(window.entity); let surface = &surface_data.surface; if not_already_configured || window.size_changed || window.present_mode_changed { diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index 5ea65a8714eab..98bf10e27f5b8 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -74,7 +74,7 @@ impl<'w> DynamicSceneBuilder<'w> { self.extract_entities(std::iter::once(entity)) } - /// Despawns all enitities with no components. + /// Despawns all entities with no components. /// /// These were likely created because none of their components were present in the provided type registry upon extraction. pub fn remove_empty_entities(&mut self) -> &mut Self { diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index f8f581a609d65..7a0c8f851b33b 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -12,7 +12,7 @@ use bevy_utils::{tracing::error, HashMap}; use thiserror::Error; use uuid::Uuid; -/// Informations about a scene instance. +/// Information about a scene instance. #[derive(Debug)] pub struct InstanceInfo { /// Mapping of entities from the scene world to the instance world. diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index e1896d988c206..51b8a5ebce0e7 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -1,7 +1,10 @@ use crate::{DynamicEntity, DynamicScene}; use anyhow::Result; use bevy_reflect::serde::{TypedReflectDeserializer, TypedReflectSerializer}; -use bevy_reflect::{serde::UntypedReflectDeserializer, Reflect, TypeRegistry, TypeRegistryArc}; +use bevy_reflect::{ + serde::{TypeRegistrationDeserializer, UntypedReflectDeserializer}, + Reflect, TypeRegistry, TypeRegistryArc, +}; use bevy_utils::HashSet; use serde::ser::SerializeMap; use serde::{ @@ -9,7 +12,6 @@ use serde::{ ser::SerializeStruct, Deserialize, Deserializer, Serialize, Serializer, }; -use std::borrow::Cow; use std::fmt::Formatter; pub const SCENE_STRUCT: &str = "Scene"; @@ -353,15 +355,16 @@ impl<'a, 'de> Visitor<'de> for ComponentVisitor<'a> { { let mut added = HashSet::new(); let mut components = Vec::new(); - while let Some(BorrowableCowStr(key)) = map.next_key()? { - if !added.insert(key.clone()) { - return Err(Error::custom(format!("duplicate component: `{key}`"))); + while let Some(registration) = + map.next_key_seed(TypeRegistrationDeserializer::new(self.registry))? + { + if !added.insert(registration.type_id()) { + return Err(Error::custom(format_args!( + "duplicate component: `{}`", + registration.type_name() + ))); } - let registration = self - .registry - .get_with_name(&key) - .ok_or_else(|| Error::custom(format!("no registration found for `{key}`")))?; components.push( map.next_value_seed(TypedReflectDeserializer::new(registration, self.registry))?, ); @@ -385,13 +388,6 @@ impl<'a, 'de> Visitor<'de> for ComponentVisitor<'a> { } } -/// Helper struct for deserializing strings without allocating (when possible). -/// -/// Based on [this comment](https://github.com/bevyengine/bevy/pull/6894#discussion_r1045069010). -#[derive(Deserialize)] -#[serde(transparent)] -struct BorrowableCowStr<'a>(#[serde(borrow)] Cow<'a, str>); - #[cfg(test)] mod tests { use crate::serde::{SceneDeserializer, SceneSerializer}; @@ -692,9 +688,7 @@ mod tests { expected .reflect_partial_eq(received.as_ref()) .unwrap_or_default(), - "components did not match: (expected: `{:?}`, received: `{:?}`)", - expected, - received + "components did not match: (expected: `{expected:?}`, received: `{received:?}`)", ); } } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 0a2a0b35ee116..c5f0e0fb5d72b 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -218,6 +218,18 @@ where } } +impl Clone for Material2dPipeline { + fn clone(&self) -> Self { + Self { + mesh2d_pipeline: self.mesh2d_pipeline.clone(), + material2d_layout: self.material2d_layout.clone(), + vertex_shader: self.vertex_shader.clone(), + fragment_shader: self.fragment_shader.clone(), + marker: PhantomData, + } + } +} + impl SpecializedMeshPipeline for Material2dPipeline where M::Data: PartialEq + Eq + Hash + Clone, @@ -307,7 +319,7 @@ pub fn queue_material2d_meshes( transparent_draw_functions: Res>, material2d_pipeline: Res>, mut pipelines: ResMut>>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, msaa: Res, render_meshes: Res>, render_materials: Res>, @@ -328,7 +340,7 @@ pub fn queue_material2d_meshes( for (view, visible_entities, tonemapping, mut transparent_phase) in &mut views { let draw_transparent_pbr = transparent_draw_functions.read().id::>(); - let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples) + let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { @@ -351,7 +363,7 @@ pub fn queue_material2d_meshes( | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); let pipeline_id = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &material2d_pipeline, Material2dKey { mesh_key, diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index e7df0c1c323ca..112c066605bc0 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -451,7 +451,7 @@ pub fn queue_sprites( view_uniforms: Res, sprite_pipeline: Res, mut pipelines: ResMut>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, mut image_bind_groups: ResMut, gpu_images: Res>, msaa: Res, @@ -474,7 +474,7 @@ pub fn queue_sprites( }; } - let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples); + let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples()); if let Some(view_binding) = view_uniforms.uniforms.binding() { let sprite_meta = &mut sprite_meta; @@ -528,12 +528,12 @@ pub fn queue_sprites( } } let pipeline = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &sprite_pipeline, view_key | SpritePipelineKey::from_colored(false), ); let colored_pipeline = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &sprite_pipeline, view_key | SpritePipelineKey::from_colored(true), ); @@ -549,7 +549,7 @@ pub fn queue_sprites( }; let mut current_batch_entity = Entity::PLACEHOLDER; let mut current_image_size = Vec2::ZERO; - // Add a phase item for each sprite, and detect when succesive items can be batched. + // Add a phase item for each sprite, and detect when successive items can be batched. // Spawn an entity with a `SpriteBatch` component for each possible batch. // Compatible items share the same entity. // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index da2a6de1c2cf1..3a3e85a4caf8a 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -24,7 +24,7 @@ pub struct Sprite { /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). /// It defaults to `Anchor::Center`. -#[derive(Debug, Clone, Default, Reflect)] +#[derive(Component, Debug, Clone, Default, Reflect)] #[doc(alias = "pivot")] pub enum Anchor { #[default] diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 802f6c267b7cf..b5e340c2d65ec 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -15,13 +15,18 @@ pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; #[cfg(target_arch = "wasm32")] mod single_threaded_task_pool; #[cfg(target_arch = "wasm32")] -pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder}; +pub use single_threaded_task_pool::{Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; mod usages; #[cfg(not(target_arch = "wasm32"))] pub use usages::tick_global_task_pools_on_main_thread; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}; +#[cfg(not(target_arch = "wasm32"))] +mod thread_executor; +#[cfg(not(target_arch = "wasm32"))] +pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; + mod iter; pub use iter::ParallelIterator; diff --git a/crates/bevy_tasks/src/single_threaded_task_pool.rs b/crates/bevy_tasks/src/single_threaded_task_pool.rs index 8fa37f4f2361b..9b77d8fd3bb2c 100644 --- a/crates/bevy_tasks/src/single_threaded_task_pool.rs +++ b/crates/bevy_tasks/src/single_threaded_task_pool.rs @@ -9,6 +9,20 @@ use std::{ #[derive(Debug, Default, Clone)] pub struct TaskPoolBuilder {} +/// This is a dummy struct for wasm support to provide the same api as with the multithreaded +/// task pool. In the case of the multithreaded task pool this struct is used to spawn +/// tasks on a specific thread. But the wasm task pool just calls +/// [`wasm_bindgen_futures::spawn_local`] for spawning which just runs tasks on the main thread +/// and so the [`ThreadExecutor`] does nothing. +#[derive(Default)] +pub struct ThreadExecutor<'a>(PhantomData<&'a ()>); +impl<'a> ThreadExecutor<'a> { + /// Creates a new `ThreadExecutor` + pub fn new() -> Self { + Self(PhantomData::default()) + } +} + impl TaskPoolBuilder { /// Creates a new TaskPoolBuilder instance pub fn new() -> Self { @@ -63,6 +77,24 @@ impl TaskPool { /// /// This is similar to `rayon::scope` and `crossbeam::scope` pub fn scope<'env, F, T>(&self, f: F) -> Vec + where + F: for<'scope> FnOnce(&'env mut Scope<'scope, 'env, T>), + T: Send + 'static, + { + self.scope_with_executor(false, None, f) + } + + /// Allows spawning non-`static futures on the thread pool. The function takes a callback, + /// passing a scope object into it. The scope object provided to the callback can be used + /// to spawn tasks. This function will await the completion of all tasks before returning. + /// + /// This is similar to `rayon::scope` and `crossbeam::scope` + pub fn scope_with_executor<'env, F, T>( + &self, + _tick_task_pool_executor: bool, + _thread_executor: Option<&ThreadExecutor>, + f: F, + ) -> Vec where F: for<'scope> FnOnce(&'env mut Scope<'scope, 'env, T>), T: Send + 'static, diff --git a/crates/bevy_tasks/src/task_pool.rs b/crates/bevy_tasks/src/task_pool.rs index 83bc92843f00d..250bfba91f72c 100644 --- a/crates/bevy_tasks/src/task_pool.rs +++ b/crates/bevy_tasks/src/task_pool.rs @@ -2,15 +2,19 @@ use std::{ future::Future, marker::PhantomData, mem, + panic::AssertUnwindSafe, sync::Arc, thread::{self, JoinHandle}, }; use async_task::FallibleTask; use concurrent_queue::ConcurrentQueue; -use futures_lite::{future, pin, FutureExt}; +use futures_lite::{future, FutureExt}; -use crate::Task; +use crate::{ + thread_executor::{ThreadExecutor, ThreadExecutorTicker}, + Task, +}; struct CallOnDrop(Option>); @@ -108,6 +112,7 @@ pub struct TaskPool { impl TaskPool { thread_local! { static LOCAL_EXECUTOR: async_executor::LocalExecutor<'static> = async_executor::LocalExecutor::new(); + static THREAD_EXECUTOR: ThreadExecutor<'static> = ThreadExecutor::new(); } /// Create a `TaskPool` with the default configuration. @@ -265,8 +270,51 @@ impl TaskPool { /// }); /// }); /// } - /// pub fn scope<'env, F, T>(&self, f: F) -> Vec + where + F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>), + T: Send + 'static, + { + Self::THREAD_EXECUTOR + .with(|thread_executor| self.scope_with_executor_inner(true, thread_executor, f)) + } + + /// This allows passing an external executor to spawn tasks on. When you pass an external executor + /// [`Scope::spawn_on_scope`] spawns is then run on the thread that [`ThreadExecutor`] is being ticked on. + /// If [`None`] is passed the scope will use a [`ThreadExecutor`] that is ticked on the current thread. + /// + /// When `tick_task_pool_executor` is set to `true`, the multithreaded task stealing executor is ticked on the scope + /// thread. Disabling this can be useful when finishing the scope is latency sensitive. Pulling tasks from + /// global excutor can run tasks unrelated to the scope and delay when the scope returns. + /// + /// See [`Self::scope`] for more details in general about how scopes work. + pub fn scope_with_executor<'env, F, T>( + &self, + tick_task_pool_executor: bool, + thread_executor: Option<&ThreadExecutor>, + f: F, + ) -> Vec + where + F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>), + T: Send + 'static, + { + // If a `thread_executor` is passed use that. Otherwise get the `thread_executor` stored + // in the `THREAD_EXECUTOR` thread local. + if let Some(thread_executor) = thread_executor { + self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f) + } else { + Self::THREAD_EXECUTOR.with(|thread_executor| { + self.scope_with_executor_inner(tick_task_pool_executor, thread_executor, f) + }) + } + } + + fn scope_with_executor_inner<'env, F, T>( + &self, + tick_task_pool_executor: bool, + thread_executor: &ThreadExecutor, + f: F, + ) -> Vec where F: for<'scope> FnOnce(&'scope Scope<'scope, 'env, T>), T: Send + 'static, @@ -278,16 +326,15 @@ impl TaskPool { // transmute the lifetimes to 'env here to appease the compiler as it is unable to validate safety. let executor: &async_executor::Executor = &self.executor; let executor: &'env async_executor::Executor = unsafe { mem::transmute(executor) }; - let task_scope_executor = &async_executor::Executor::default(); - let task_scope_executor: &'env async_executor::Executor = - unsafe { mem::transmute(task_scope_executor) }; + let thread_executor: &'env ThreadExecutor<'env> = + unsafe { mem::transmute(thread_executor) }; let spawned: ConcurrentQueue> = ConcurrentQueue::unbounded(); let spawned_ref: &'env ConcurrentQueue> = unsafe { mem::transmute(&spawned) }; let scope = Scope { executor, - task_scope_executor, + thread_executor, spawned: spawned_ref, scope: PhantomData, env: PhantomData, @@ -300,30 +347,89 @@ impl TaskPool { if spawned.is_empty() { Vec::new() } else { - let get_results = async { - let mut results = Vec::with_capacity(spawned_ref.len()); - while let Ok(task) = spawned_ref.pop() { - results.push(task.await.unwrap()); - } + future::block_on(async move { + let get_results = async { + let mut results = Vec::with_capacity(spawned_ref.len()); + while let Ok(task) = spawned_ref.pop() { + results.push(task.await.unwrap()); + } + results + }; - results - }; + let tick_task_pool_executor = tick_task_pool_executor || self.threads.is_empty(); + if let Some(thread_ticker) = thread_executor.ticker() { + if tick_task_pool_executor { + Self::execute_local_global(thread_ticker, executor, get_results).await + } else { + Self::execute_local(thread_ticker, get_results).await + } + } else if tick_task_pool_executor { + Self::execute_global(executor, get_results).await + } else { + get_results.await + } + }) + } + } - // Pin the futures on the stack. - pin!(get_results); + #[inline] + async fn execute_local_global<'scope, 'ticker, T>( + thread_ticker: ThreadExecutorTicker<'scope, 'ticker>, + executor: &'scope async_executor::Executor<'scope>, + get_results: impl Future>, + ) -> Vec { + // we restart the executors if a task errors. if a scoped + // task errors it will panic the scope on the call to get_results + let execute_forever = async move { + loop { + let tick_forever = async { + loop { + thread_ticker.tick().await; + } + }; + // we don't care if it errors. If a scoped task errors it will propagate + // to get_results + let _result = AssertUnwindSafe(executor.run(tick_forever)) + .catch_unwind() + .await + .is_ok(); + } + }; + execute_forever.or(get_results).await + } + #[inline] + async fn execute_local<'scope, 'ticker, T>( + thread_ticker: ThreadExecutorTicker<'scope, 'ticker>, + get_results: impl Future>, + ) -> Vec { + let execute_forever = async { loop { - if let Some(result) = future::block_on(future::poll_once(&mut get_results)) { - break result; + let tick_forever = async { + loop { + thread_ticker.tick().await; + } }; + let _result = AssertUnwindSafe(tick_forever).catch_unwind().await.is_ok(); + } + }; + execute_forever.or(get_results).await + } - std::panic::catch_unwind(|| { - executor.try_tick(); - task_scope_executor.try_tick(); - }) - .ok(); + #[inline] + async fn execute_global<'scope, T>( + executor: &'scope async_executor::Executor<'scope>, + get_results: impl Future>, + ) -> Vec { + let execute_forever = async { + loop { + let _result = AssertUnwindSafe(executor.run(std::future::pending::<()>())) + .catch_unwind() + .await + .is_ok(); } - } + }; + execute_forever.or(get_results).await } /// Spawns a static future onto the thread pool. The returned Task is a future. It can also be @@ -395,7 +501,7 @@ impl Drop for TaskPool { #[derive(Debug)] pub struct Scope<'scope, 'env: 'scope, T> { executor: &'scope async_executor::Executor<'scope>, - task_scope_executor: &'scope async_executor::Executor<'scope>, + thread_executor: &'scope ThreadExecutor<'scope>, spawned: &'scope ConcurrentQueue>, // make `Scope` invariant over 'scope and 'env scope: PhantomData<&'scope mut &'scope ()>, @@ -414,7 +520,7 @@ impl<'scope, 'env, T: Send + 'scope> Scope<'scope, 'env, T> { pub fn spawn + 'scope + Send>(&self, f: Fut) { let task = self.executor.spawn(f).fallible(); // ConcurrentQueue only errors when closed or full, but we never - // close and use an unbouded queue, so it is safe to unwrap + // close and use an unbounded queue, so it is safe to unwrap self.spawned.push(task).unwrap(); } @@ -425,9 +531,9 @@ impl<'scope, 'env, T: Send + 'scope> Scope<'scope, 'env, T> { /// /// For more information, see [`TaskPool::scope`]. pub fn spawn_on_scope + 'scope + Send>(&self, f: Fut) { - let task = self.task_scope_executor.spawn(f).fallible(); + let task = self.thread_executor.spawn(f).fallible(); // ConcurrentQueue only errors when closed or full, but we never - // close and use an unbouded queue, so it is safe to unwrap + // close and use an unbounded queue, so it is safe to unwrap self.spawned.push(task).unwrap(); } } diff --git a/crates/bevy_tasks/src/thread_executor.rs b/crates/bevy_tasks/src/thread_executor.rs new file mode 100644 index 0000000000000..0ba66571db982 --- /dev/null +++ b/crates/bevy_tasks/src/thread_executor.rs @@ -0,0 +1,128 @@ +use std::{ + marker::PhantomData, + thread::{self, ThreadId}, +}; + +use async_executor::{Executor, Task}; +use futures_lite::Future; + +/// An executor that can only be ticked on the thread it was instantiated on. But +/// can spawn `Send` tasks from other threads. +/// +/// # Example +/// ```rust +/// # use std::sync::{Arc, atomic::{AtomicI32, Ordering}}; +/// use bevy_tasks::ThreadExecutor; +/// +/// let thread_executor = ThreadExecutor::new(); +/// let count = Arc::new(AtomicI32::new(0)); +/// +/// // create some owned values that can be moved into another thread +/// let count_clone = count.clone(); +/// +/// std::thread::scope(|scope| { +/// scope.spawn(|| { +/// // we cannot get the ticker from another thread +/// let not_thread_ticker = thread_executor.ticker(); +/// assert!(not_thread_ticker.is_none()); +/// +/// // but we can spawn tasks from another thread +/// thread_executor.spawn(async move { +/// count_clone.fetch_add(1, Ordering::Relaxed); +/// }).detach(); +/// }); +/// }); +/// +/// // the tasks do not make progress unless the executor is manually ticked +/// assert_eq!(count.load(Ordering::Relaxed), 0); +/// +/// // tick the ticker until task finishes +/// let thread_ticker = thread_executor.ticker().unwrap(); +/// thread_ticker.try_tick(); +/// assert_eq!(count.load(Ordering::Relaxed), 1); +/// ``` +#[derive(Debug)] +pub struct ThreadExecutor<'task> { + executor: Executor<'task>, + thread_id: ThreadId, +} + +impl<'task> Default for ThreadExecutor<'task> { + fn default() -> Self { + Self { + executor: Executor::new(), + thread_id: thread::current().id(), + } + } +} + +impl<'task> ThreadExecutor<'task> { + /// create a new [`ThreadExecutor`] + pub fn new() -> Self { + Self::default() + } + + /// Spawn a task on the thread executor + pub fn spawn( + &self, + future: impl Future + Send + 'task, + ) -> Task { + self.executor.spawn(future) + } + + /// Gets the [`ThreadExecutorTicker`] for this executor. + /// Use this to tick the executor. + /// It only returns the ticker if it's on the thread the executor was created on + /// and returns `None` otherwise. + pub fn ticker<'ticker>(&'ticker self) -> Option> { + if thread::current().id() == self.thread_id { + return Some(ThreadExecutorTicker { + executor: &self.executor, + _marker: PhantomData::default(), + }); + } + None + } +} + +/// Used to tick the [`ThreadExecutor`]. The executor does not +/// make progress unless it is manually ticked on the thread it was +/// created on. +#[derive(Debug)] +pub struct ThreadExecutorTicker<'task, 'ticker> { + executor: &'ticker Executor<'task>, + // make type not send or sync + _marker: PhantomData<*const ()>, +} +impl<'task, 'ticker> ThreadExecutorTicker<'task, 'ticker> { + /// Tick the thread executor. + pub async fn tick(&self) { + self.executor.tick().await; + } + + /// Synchronously try to tick a task on the executor. + /// Returns false if if does not find a task to tick. + pub fn try_tick(&self) -> bool { + self.executor.try_tick() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + + #[test] + fn test_ticker() { + let executor = Arc::new(ThreadExecutor::new()); + let ticker = executor.ticker(); + assert!(ticker.is_some()); + + std::thread::scope(|s| { + s.spawn(|| { + let ticker = executor.ticker(); + assert!(ticker.is_none()); + }); + }); + } +} diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index b3b8e3d79f018..a96d25dd66923 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -5,12 +5,13 @@ use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::tracing::warn; use glyph_brush_layout::{ - FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, SectionText, ToSectionText, + BuiltInLineBreaker, FontId, GlyphPositioner, Layout, SectionGeometry, SectionGlyph, + SectionText, ToSectionText, }; use crate::{ - error::TextError, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, TextAlignment, - TextSettings, YAxisOrientation, + error::TextError, BreakLineOn, Font, FontAtlasSet, FontAtlasWarning, GlyphAtlasInfo, + TextAlignment, TextSettings, YAxisOrientation, }; pub struct GlyphBrush { @@ -35,14 +36,18 @@ impl GlyphBrush { sections: &[S], bounds: Vec2, text_alignment: TextAlignment, + linebreak_behaviour: BreakLineOn, ) -> Result, TextError> { let geom = SectionGeometry { bounds: (bounds.x, bounds.y), ..Default::default() }; + + let lbb: BuiltInLineBreaker = linebreak_behaviour.into(); + let section_glyphs = Layout::default() - .h_align(text_alignment.horizontal.into()) - .v_align(text_alignment.vertical.into()) + .h_align(text_alignment.into()) + .line_breaker(lbb) .calculate_glyphs(&self.fonts, &geom, sections); Ok(section_glyphs) } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 79f6909289a5e..84844ec5024ef 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -20,10 +20,7 @@ pub use text2d::*; pub mod prelude { #[doc(hidden)] - pub use crate::{ - Font, HorizontalAlign, Text, Text2dBundle, TextAlignment, TextError, TextSection, - TextStyle, VerticalAlign, - }; + pub use crate::{Font, Text, Text2dBundle, TextAlignment, TextError, TextSection, TextStyle}; } use bevy_app::prelude::*; @@ -77,9 +74,8 @@ impl Plugin for TextPlugin { .register_type::() .register_type::>() .register_type::() + .register_type::() .register_type::() - .register_type::() - .register_type::() .init_asset_loader::() .init_resource::() .init_resource::() diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 3726f57fd4038..ac29c6b7c9f87 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -10,8 +10,8 @@ use bevy_utils::HashMap; use glyph_brush_layout::{FontId, SectionText}; use crate::{ - error::TextError, glyph_brush::GlyphBrush, scale_value, Font, FontAtlasSet, FontAtlasWarning, - PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, + error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, FontAtlasSet, + FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, YAxisOrientation, }; #[derive(Default, Resource)] @@ -45,6 +45,7 @@ impl TextPipeline { sections: &[TextSection], scale_factor: f64, text_alignment: TextAlignment, + linebreak_behaviour: BreakLineOn, bounds: Vec2, font_atlas_set_storage: &mut Assets, texture_atlases: &mut Assets, @@ -75,9 +76,9 @@ impl TextPipeline { }) .collect::, _>>()?; - let section_glyphs = self - .brush - .compute_glyphs(§ions, bounds, text_alignment)?; + let section_glyphs = + self.brush + .compute_glyphs(§ions, bounds, text_alignment, linebreak_behaviour)?; if section_glyphs.is_empty() { return Ok(TextLayoutInfo::default()); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 7a8dc176c6349..a6ca996f828a0 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -2,15 +2,30 @@ use bevy_asset::Handle; use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; use bevy_reflect::{prelude::*, FromReflect}; use bevy_render::color::Color; +use bevy_utils::default; use serde::{Deserialize, Serialize}; use crate::Font; -#[derive(Component, Debug, Default, Clone, Reflect)] +#[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct Text { pub sections: Vec, + /// The text's internal alignment. + /// Should not affect its position within a container. pub alignment: TextAlignment, + /// How the text should linebreak when running out of the bounds determined by max_size + pub linebreak_behaviour: BreakLineOn, +} + +impl Default for Text { + fn default() -> Self { + Self { + sections: Default::default(), + alignment: TextAlignment::Left, + linebreak_behaviour: BreakLineOn::WordBoundary, + } + } } impl Text { @@ -19,7 +34,7 @@ impl Text { /// ``` /// # use bevy_asset::Handle; /// # use bevy_render::color::Color; - /// # use bevy_text::{Font, Text, TextAlignment, TextStyle, HorizontalAlign, VerticalAlign}; + /// # use bevy_text::{Font, Text, TextStyle, TextAlignment}; /// # /// # let font_handle: Handle = Default::default(); /// # @@ -42,12 +57,12 @@ impl Text { /// color: Color::WHITE, /// }, /// ) // You can still add an alignment. - /// .with_alignment(TextAlignment::CENTER); + /// .with_alignment(TextAlignment::Center); /// ``` pub fn from_section(value: impl Into, style: TextStyle) -> Self { Self { sections: vec![TextSection::new(value, style)], - alignment: Default::default(), + ..default() } } @@ -82,7 +97,7 @@ impl Text { pub fn from_sections(sections: impl IntoIterator) -> Self { Self { sections: sections.into_iter().collect(), - alignment: Default::default(), + ..default() } } @@ -117,78 +132,10 @@ impl TextSection { } } -#[derive(Debug, Clone, Copy, Reflect)] -pub struct TextAlignment { - pub vertical: VerticalAlign, - pub horizontal: HorizontalAlign, -} - -impl TextAlignment { - /// A [`TextAlignment`] set to the top-left. - pub const TOP_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to the top-center. - pub const TOP_CENTER: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the top-right. - pub const TOP_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Top, - horizontal: HorizontalAlign::Right, - }; - - /// A [`TextAlignment`] set to center the center-left. - pub const CENTER_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to center on both axes. - pub const CENTER: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the center-right. - pub const CENTER_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Center, - horizontal: HorizontalAlign::Right, - }; - - /// A [`TextAlignment`] set to the bottom-left. - pub const BOTTOM_LEFT: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Left, - }; - - /// A [`TextAlignment`] set to the bottom-center. - pub const BOTTOM_CENTER: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Center, - }; - - /// A [`TextAlignment`] set to the bottom-right. - pub const BOTTOM_RIGHT: Self = TextAlignment { - vertical: VerticalAlign::Bottom, - horizontal: HorizontalAlign::Right, - }; -} - -impl Default for TextAlignment { - fn default() -> Self { - TextAlignment::TOP_LEFT - } -} - /// Describes horizontal alignment preference for positioning & bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] -pub enum HorizontalAlign { +pub enum TextAlignment { /// Leftmost character is immediately to the right of the render position.
/// Bounds start from the render position and advance rightwards. Left, @@ -200,35 +147,12 @@ pub enum HorizontalAlign { Right, } -impl From for glyph_brush_layout::HorizontalAlign { - fn from(val: HorizontalAlign) -> Self { - match val { - HorizontalAlign::Left => glyph_brush_layout::HorizontalAlign::Left, - HorizontalAlign::Center => glyph_brush_layout::HorizontalAlign::Center, - HorizontalAlign::Right => glyph_brush_layout::HorizontalAlign::Right, - } - } -} - -/// Describes vertical alignment preference for positioning & bounds. Currently a placeholder -/// for future functionality. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] -#[reflect(Serialize, Deserialize)] -pub enum VerticalAlign { - /// Characters/bounds start underneath the render position and progress downwards. - Top, - /// Characters/bounds center at the render position and progress outward equally. - Center, - /// Characters/bounds start above the render position and progress upward. - Bottom, -} - -impl From for glyph_brush_layout::VerticalAlign { - fn from(val: VerticalAlign) -> Self { +impl From for glyph_brush_layout::HorizontalAlign { + fn from(val: TextAlignment) -> Self { match val { - VerticalAlign::Top => glyph_brush_layout::VerticalAlign::Top, - VerticalAlign::Center => glyph_brush_layout::VerticalAlign::Center, - VerticalAlign::Bottom => glyph_brush_layout::VerticalAlign::Bottom, + TextAlignment::Left => glyph_brush_layout::HorizontalAlign::Left, + TextAlignment::Center => glyph_brush_layout::HorizontalAlign::Center, + TextAlignment::Right => glyph_brush_layout::HorizontalAlign::Right, } } } @@ -249,3 +173,26 @@ impl Default for TextStyle { } } } + +/// Determines how lines will be broken when preventing text from running out of bounds. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[reflect(Serialize, Deserialize)] +pub enum BreakLineOn { + /// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/). + /// Lines will be broken up at the nearest suitable word boundary, usually a space. + /// This behaviour suits most cases, as it keeps words intact across linebreaks. + WordBoundary, + /// Lines will be broken without discrimination on any character that would leave bounds. + /// This is closer to the behaviour one might expect from text in a terminal. + /// However it may lead to words being broken up across linebreaks. + AnyCharacter, +} + +impl From for glyph_brush_layout::BuiltInLineBreaker { + fn from(val: BreakLineOn) -> Self { + match val { + BreakLineOn::WordBoundary => glyph_brush_layout::BuiltInLineBreaker::UnicodeLineBreaker, + BreakLineOn::AnyCharacter => glyph_brush_layout::BuiltInLineBreaker::AnyCharLineBreaker, + } + } +} diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 1143755e1931b..56da32ade4e8b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,10 +1,11 @@ use bevy_asset::Assets; use bevy_ecs::{ bundle::Bundle, + change_detection::{DetectChanges, Ref}, component::Component, entity::Entity, event::EventReader, - query::Changed, + prelude::With, reflect::ReflectComponent, system::{Commands, Local, Query, Res, ResMut}, }; @@ -19,20 +20,13 @@ use bevy_render::{ use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; -use bevy_window::{WindowId, WindowScaleFactorChanged, Windows}; +use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use crate::{ - Font, FontAtlasSet, FontAtlasWarning, HorizontalAlign, Text, TextError, TextLayoutInfo, - TextPipeline, TextSettings, VerticalAlign, YAxisOrientation, + Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, + TextSettings, YAxisOrientation, }; -/// The calculated size of text drawn in 2D scene. -#[derive(Component, Default, Copy, Clone, Debug, Reflect)] -#[reflect(Component)] -pub struct Text2dSize { - pub size: Vec2, -} - /// The maximum width and height of text. The text will wrap according to the specified size. /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the /// specified `TextAlignment`. @@ -47,21 +41,27 @@ pub struct Text2dBounds { } impl Default for Text2dBounds { + #[inline] fn default() -> Self { - Self { - size: Vec2::new(f32::MAX, f32::MAX), - } + Self::UNBOUNDED } } +impl Text2dBounds { + /// Unbounded text will not be truncated or wrapped. + pub const UNBOUNDED: Self = Self { + size: Vec2::splat(f32::INFINITY), + }; +} + /// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] pub struct Text2dBundle { pub text: Text, + pub text_anchor: Anchor, pub transform: Transform, pub global_transform: GlobalTransform, - pub text_2d_size: Text2dSize, pub text_2d_bounds: Text2dBounds, pub visibility: Visibility, pub computed_visibility: ComputedVisibility, @@ -70,39 +70,34 @@ pub struct Text2dBundle { pub fn extract_text2d_sprite( mut extracted_sprites: ResMut, texture_atlases: Extract>>, - windows: Extract>, + windows: Extract>>, text2d_query: Extract< Query<( Entity, &ComputedVisibility, &Text, &TextLayoutInfo, + &Anchor, &GlobalTransform, - &Text2dSize, )>, >, ) { - let scale_factor = windows.scale_factor(WindowId::primary()) as f32; + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor() as f32) + .unwrap_or(1.0); - for (entity, computed_visibility, text, text_layout_info, text_transform, calculated_size) in + for (entity, computed_visibility, text, text_layout_info, anchor, text_transform) in text2d_query.iter() { if !computed_visibility.is_visible() { continue; } - let (width, height) = (calculated_size.size.x, calculated_size.size.y); let text_glyphs = &text_layout_info.glyphs; - let alignment_offset = match text.alignment.vertical { - VerticalAlign::Top => Vec3::new(0.0, -height, 0.0), - VerticalAlign::Center => Vec3::new(0.0, -height * 0.5, 0.0), - VerticalAlign::Bottom => Vec3::ZERO, - } + match text.alignment.horizontal { - HorizontalAlign::Left => Vec3::ZERO, - HorizontalAlign::Center => Vec3::new(-width * 0.5, 0.0, 0.0), - HorizontalAlign::Right => Vec3::new(-width, 0.0, 0.0), - }; - + let text_anchor = anchor.as_vec() * Vec2::new(1., -1.) - 0.5; + let alignment_offset = text_layout_info.size * text_anchor; let mut color = Color::WHITE; let mut current_section = usize::MAX; for text_glyph in text_glyphs { @@ -120,10 +115,9 @@ pub fn extract_text2d_sprite( let index = text_glyph.atlas_info.glyph_index; let rect = Some(atlas.textures[index]); - let glyph_transform = Transform::from_translation( - alignment_offset * scale_factor + text_glyph.position.extend(0.), - ); - // NOTE: Should match `bevy_ui::render::extract_text_uinodes` + let glyph_transform = + Transform::from_translation((alignment_offset + text_glyph.position).extend(0.)); + let transform = *text_transform * GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())) * glyph_transform; @@ -157,43 +151,42 @@ pub fn update_text2d_layout( mut queue: Local>, mut textures: ResMut>, fonts: Res>, - windows: Res, text_settings: Res, mut font_atlas_warning: ResMut, + windows: Query<&Window, With>, mut scale_factor_changed: EventReader, mut texture_atlases: ResMut>, mut font_atlas_set_storage: ResMut>, mut text_pipeline: ResMut, mut text_query: Query<( Entity, - Changed, - &Text, - Option<&Text2dBounds>, - &mut Text2dSize, + Ref, + &Text2dBounds, Option<&mut TextLayoutInfo>, )>, ) { // We need to consume the entire iterator, hence `last` let factor_changed = scale_factor_changed.iter().last().is_some(); - let scale_factor = windows.scale_factor(WindowId::primary()); - for (entity, text_changed, text, maybe_bounds, mut calculated_size, text_layout_info) in - &mut text_query - { - if factor_changed || text_changed || queue.remove(&entity) { - let text_bounds = match maybe_bounds { - Some(bounds) => Vec2::new( - scale_value(bounds.size.x, scale_factor), - scale_value(bounds.size.y, scale_factor), - ), - None => Vec2::new(f32::MAX, f32::MAX), - }; + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor()) + .unwrap_or(1.0); + + for (entity, text, bounds, text_layout_info) in &mut text_query { + if factor_changed || text.is_changed() || queue.remove(&entity) { + let text_bounds = Vec2::new( + scale_value(bounds.size.x, scale_factor), + scale_value(bounds.size.y, scale_factor), + ); match text_pipeline.queue_text( &fonts, &text.sections, scale_factor, text.alignment, + text.linebreak_behaviour, text_bounds, &mut font_atlas_set_storage, &mut texture_atlases, @@ -210,18 +203,12 @@ pub fn update_text2d_layout( Err(e @ TextError::FailedToAddGlyph(_)) => { panic!("Fatal error when processing text: {e}."); } - Ok(info) => { - calculated_size.size = Vec2::new( - scale_value(info.size.x, 1. / scale_factor), - scale_value(info.size.y, 1. / scale_factor), - ); - match text_layout_info { - Some(mut t) => *t = info, - None => { - commands.entity(entity).insert(info); - } + Ok(info) => match text_layout_info { + Some(mut t) => *t = info, + None => { + commands.entity(entity).insert(info); } - } + }, } } } diff --git a/crates/bevy_time/src/fixed_timestep.rs b/crates/bevy_time/src/fixed_timestep.rs index 8a63f67f92bb6..fa355706581bb 100644 --- a/crates/bevy_time/src/fixed_timestep.rs +++ b/crates/bevy_time/src/fixed_timestep.rs @@ -177,6 +177,10 @@ impl System for FixedTimestep { Cow::Borrowed(std::any::type_name::()) } + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + fn archetype_component_access(&self) -> &Access { self.internal_system.archetype_component_access() } diff --git a/crates/bevy_time/src/stopwatch.rs b/crates/bevy_time/src/stopwatch.rs index dcc0cfb9f2969..636ea19ebed2f 100644 --- a/crates/bevy_time/src/stopwatch.rs +++ b/crates/bevy_time/src/stopwatch.rs @@ -187,7 +187,7 @@ impl Stopwatch { self.paused } - /// Resets the stopwatch. The reset doesn’t affect the paused state of the stopwatch. + /// Resets the stopwatch. The reset doesn't affect the paused state of the stopwatch. /// /// # Examples /// ``` diff --git a/crates/bevy_transform/src/commands.rs b/crates/bevy_transform/src/commands.rs new file mode 100644 index 0000000000000..0fa7234ca099f --- /dev/null +++ b/crates/bevy_transform/src/commands.rs @@ -0,0 +1,101 @@ +//! Extension to [`EntityCommands`] to modify [`bevy_hierarchy`] hierarchies +//! while preserving [`GlobalTransform`]. + +use bevy_ecs::{prelude::Entity, system::Command, system::EntityCommands, world::World}; +use bevy_hierarchy::{AddChild, RemoveParent}; + +#[cfg(doc)] +use bevy_hierarchy::BuildChildren; + +use crate::{GlobalTransform, Transform}; + +/// Command similar to [`AddChild`], but updating the child transform to keep +/// it at the same [`GlobalTransform`]. +/// +/// You most likely want to use [`BuildChildrenTransformExt::set_parent_in_place`] +/// method on [`EntityCommands`] instead. +pub struct AddChildInPlace { + /// Parent entity to add the child to. + pub parent: Entity, + /// Child entity to add. + pub child: Entity, +} +impl Command for AddChildInPlace { + fn write(self, world: &mut World) { + let hierarchy_command = AddChild { + child: self.child, + parent: self.parent, + }; + hierarchy_command.write(world); + // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. + let mut update_transform = || { + let parent = *world.get_entity(self.parent)?.get::()?; + let child_global = *world.get_entity(self.child)?.get::()?; + let mut child_entity = world.get_entity_mut(self.child)?; + let mut child = child_entity.get_mut::()?; + *child = child_global.reparented_to(&parent); + Some(()) + }; + update_transform(); + } +} +/// Command similar to [`RemoveParent`], but updating the child transform to keep +/// it at the same [`GlobalTransform`]. +/// +/// You most likely want to use [`BuildChildrenTransformExt::remove_parent_in_place`] +/// method on [`EntityCommands`] instead. +pub struct RemoveParentInPlace { + /// `Entity` whose parent must be removed. + pub child: Entity, +} +impl Command for RemoveParentInPlace { + fn write(self, world: &mut World) { + let hierarchy_command = RemoveParent { child: self.child }; + hierarchy_command.write(world); + // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. + let mut update_transform = || { + let child_global = *world.get_entity(self.child)?.get::()?; + let mut child_entity = world.get_entity_mut(self.child)?; + let mut child = child_entity.get_mut::()?; + *child = child_global.compute_transform(); + Some(()) + }; + update_transform(); + } +} +/// Collection of methods similar to [`BuildChildren`], but preserving each +/// entity's [`GlobalTransform`]. +pub trait BuildChildrenTransformExt { + /// Change this entity's parent while preserving this entity's [`GlobalTransform`] + /// by updating its [`Transform`]. + /// + /// See [`BuildChildren::set_parent`] for a method that doesn't update the + /// [`Transform`]. + /// + /// Note that both the hierarchy and transform updates will only execute + /// at the end of the current stage. + fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self; + + /// Make this entity parentless while preserving this entity's [`GlobalTransform`] + /// by updating its [`Transform`] to be equal to its current [`GlobalTransform`]. + /// + /// See [`BuildChildren::remove_parent`] for a method that doesn't update the + /// [`Transform`]. + /// + /// Note that both the hierarchy and transform updates will only execute + /// at the end of the current stage. + fn remove_parent_in_place(&mut self) -> &mut Self; +} +impl<'w, 's, 'a> BuildChildrenTransformExt for EntityCommands<'w, 's, 'a> { + fn remove_parent_in_place(&mut self) -> &mut Self { + let child = self.id(); + self.commands().add(RemoveParentInPlace { child }); + self + } + + fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self { + let child = self.id(); + self.commands().add(AddChildInPlace { child, parent }); + self + } +} diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index bbf059cf25dfd..71355f2693285 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -8,6 +8,8 @@ use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect}; /// Describe the position of an entity relative to the reference frame. /// /// * To place or move an entity, you should set its [`Transform`]. +/// * [`GlobalTransform`] is fully managed by bevy, you cannot mutate it, use +/// [`Transform`] instead. /// * To get the global transform of an entity, you should get its [`GlobalTransform`]. /// * For transform hierarchies to work correctly, you must have both a [`Transform`] and a [`GlobalTransform`]. /// * You may use the [`TransformBundle`](crate::TransformBundle) to guarantee this. @@ -26,11 +28,14 @@ use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect}; /// update the [`Transform`] of an entity in this stage or after, you will notice a 1 frame lag /// before the [`GlobalTransform`] is updated. /// +/// Third party plugins should use [`transform_propagate_system_set`](crate::transform_propagate_system_set) +/// to control when transforms are propagated from parents to children. +/// /// # Examples /// -/// - [`global_vs_local_translation`] +/// - [`transform`] /// -/// [`global_vs_local_translation`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/global_vs_local_translation.rs +/// [`transform`]: https://github.com/bevyengine/bevy/blob/latest/examples/transforms/transform.rs #[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, FromReflect)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[reflect(Component, Default, PartialEq)] @@ -169,12 +174,6 @@ impl GlobalTransform { self.0.translation.into() } - /// Mutably access the internal translation. - #[inline] - pub fn translation_mut(&mut self) -> &mut Vec3A { - &mut self.0.translation - } - /// Get the translation as a [`Vec3A`]. #[inline] pub fn translation_vec3a(&self) -> Vec3A { diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index 0c09757573aeb..c68f01b2e0b5b 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -2,6 +2,7 @@ #![warn(clippy::undocumented_unsafe_blocks)] #![doc = include_str!("../README.md")] +pub mod commands; /// The basic components of the transform crate pub mod components; mod systems; @@ -9,7 +10,9 @@ mod systems; #[doc(hidden)] pub mod prelude { #[doc(hidden)] - pub use crate::{components::*, TransformBundle, TransformPlugin}; + pub use crate::{ + commands::BuildChildrenTransformExt, components::*, TransformBundle, TransformPlugin, + }; } use bevy_app::prelude::*; @@ -79,6 +82,13 @@ pub enum TransformSystem { TransformPropagate, } +/// Transform propagation system set for third party plugins use +pub fn transform_propagate_system_set() -> SystemSet { + SystemSet::new() + .with_system(systems::sync_simple_transforms) + .with_system(systems::propagate_transforms) +} + /// The base plugin for handling [`Transform`] components #[derive(Default)] pub struct TransformPlugin; diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 53de43a51357f..4de47a3656dfc 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -1,54 +1,57 @@ use crate::components::{GlobalTransform, Transform}; -use bevy_ecs::prelude::{Changed, Entity, Query, With, Without}; +use bevy_ecs::{ + change_detection::Ref, + prelude::{Changed, DetectChanges, Entity, Query, With, Without}, +}; use bevy_hierarchy::{Children, Parent}; /// Update [`GlobalTransform`] component of entities that aren't in the hierarchy +/// +/// Third party plugins should use [`transform_propagate_system_set`](crate::transform_propagate_system_set) +/// to propagate transforms correctly. pub fn sync_simple_transforms( mut query: Query< (&Transform, &mut GlobalTransform), (Changed, Without, Without), >, ) { - query.par_for_each_mut(1024, |(transform, mut global_transform)| { - *global_transform = GlobalTransform::from(*transform); - }); + query + .par_iter_mut() + .for_each_mut(|(transform, mut global_transform)| { + *global_transform = GlobalTransform::from(*transform); + }); } /// Update [`GlobalTransform`] component of entities based on entity hierarchy and /// [`Transform`] component. +/// +/// Third party plugins should use [`transform_propagate_system_set`](crate::transform_propagate_system_set) +/// to propagate transforms correctly. pub fn propagate_transforms( mut root_query: Query< - ( - Entity, - &Children, - &Transform, - Changed, - Changed, - &mut GlobalTransform, - ), + (Entity, &Children, Ref, &mut GlobalTransform), Without, >, - transform_query: Query<(&Transform, Changed, &mut GlobalTransform), With>, - parent_query: Query<&Parent>, - children_query: Query<(&Children, Changed), (With, With)>, + transform_query: Query<(Ref, &mut GlobalTransform, Option<&Children>), With>, + parent_query: Query<(Entity, Ref)>, ) { - root_query.par_for_each_mut( - // The differing depths and sizes of hierarchy trees causes the work for each root to be - // different. A batch size of 1 ensures that each tree gets it's own task and multiple - // large trees are not clumped together. - 1, - |(entity, children, transform, mut changed, children_changed, mut global_transform)| { + root_query.par_iter_mut().for_each_mut( + |(entity, children, transform, mut global_transform)| { + let changed = transform.is_changed(); if changed { *global_transform = GlobalTransform::from(*transform); } - // If our `Children` has changed, we need to recalculate everything below us - changed |= children_changed; - - for child in children.iter() { + for (child, actual_parent) in parent_query.iter_many(children) { + assert_eq!( + actual_parent.get(), entity, + "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" + ); // SAFETY: - // - We may operate as if the hierarchy is consistent, since `propagate_recursive` will panic before continuing - // to propagate if it encounters an entity with inconsistent parentage. + // - `child` must have consistent parentage, or the above assertion would panic. + // Since `child` is parented to a root entity, the entire hierarchy leading to it is consistent. + // - We may operate as if all descendants are consistent, since `propagate_recursive` will panic before + // continuing to propagate if it encounters an entity with inconsistent parentage. // - Since each root entity is unique and the hierarchy is consistent and forest-like, // other root entities' `propagate_recursive` calls will not conflict with this one. // - Since this is the only place where `transform_query` gets used, there will be no conflicting fetches elsewhere. @@ -57,10 +60,8 @@ pub fn propagate_transforms( &global_transform, &transform_query, &parent_query, - &children_query, - entity, - *child, - changed, + child, + changed || actual_parent.is_changed(), ); } } @@ -72,38 +73,30 @@ pub fn propagate_transforms( /// /// # Panics /// -/// If `entity` or any of its descendants have a malformed hierarchy. -/// The panic will occur before propagating the transforms of any malformed entities and their descendants. +/// If `entity`'s descendants have a malformed hierarchy, this function will panic occur before propagating +/// the transforms of any malformed entities and their descendants. /// /// # Safety /// -/// While this function is running, `unsafe_transform_query` must not have any fetches for `entity`, +/// - While this function is running, `transform_query` must not have any fetches for `entity`, /// nor any of its descendants. +/// - The caller must ensure that the hierarchy leading to `entity` +/// is well-formed and must remain as a tree or a forest. Each entity must have at most one parent. unsafe fn propagate_recursive( parent: &GlobalTransform, - unsafe_transform_query: &Query< - (&Transform, Changed, &mut GlobalTransform), + transform_query: &Query< + (Ref, &mut GlobalTransform, Option<&Children>), With, >, - parent_query: &Query<&Parent>, - children_query: &Query<(&Children, Changed), (With, With)>, - expected_parent: Entity, + parent_query: &Query<(Entity, Ref)>, entity: Entity, mut changed: bool, ) { - let Ok(actual_parent) = parent_query.get(entity) else { - panic!("Propagated child for {:?} has no Parent component!", entity); - }; - assert_eq!( - actual_parent.get(), expected_parent, - "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" - ); - - let global_matrix = { - let Ok((transform, transform_changed, mut global_transform)) = + let (global_matrix, children) = { + let Ok((transform, mut global_transform, children)) = // SAFETY: This call cannot create aliased mutable references. // - The top level iteration parallelizes on the roots of the hierarchy. - // - The above assertion ensures that each child has one and only one unique parent throughout the entire + // - The caller ensures that each child has one and only one unique parent throughout the entire // hierarchy. // // For example, consider the following malformed hierarchy: @@ -127,34 +120,35 @@ unsafe fn propagate_recursive( // // Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting // to mutably access E. - (unsafe { unsafe_transform_query.get_unchecked(entity) }) else { + (unsafe { transform_query.get_unchecked(entity) }) else { return; }; - changed |= transform_changed; + changed |= transform.is_changed(); if changed { *global_transform = parent.mul_transform(*transform); } - *global_transform + (*global_transform, children) }; - let Ok((children, changed_children)) = children_query.get(entity) else { - return - }; - // If our `Children` has changed, we need to recalculate everything below us - changed |= changed_children; - for child in children { - // SAFETY: The caller guarantees that `unsafe_transform_query` will not be fetched + let Some(children) = children else { return }; + for (child, actual_parent) in parent_query.iter_many(children) { + assert_eq!( + actual_parent.get(), entity, + "Malformed hierarchy. This probably means that your hierarchy has been improperly maintained, or contains a cycle" + ); + // SAFETY: The caller guarantees that `transform_query` will not be fetched // for any descendants of `entity`, so it is safe to call `propagate_recursive` for each child. + // + // The above assertion ensures that each child has one and only one unique parent throughout the + // entire hierarchy. unsafe { propagate_recursive( &global_matrix, - unsafe_transform_query, + transform_query, parent_query, - children_query, - entity, - *child, - changed, + child, + changed || actual_parent.is_changed(), ); } } diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/flex/mod.rs index 43715a8cbd4ae..8f058dd5887ae 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/crates/bevy_ui/src/flex/mod.rs @@ -2,6 +2,7 @@ mod convert; use crate::{CalculatedSize, Node, Style, UiScale}; use bevy_ecs::{ + change_detection::DetectChanges, entity::Entity, event::EventReader, query::{Changed, ReadOnlyWorldQuery, With, Without}, @@ -12,7 +13,7 @@ use bevy_log::warn; use bevy_math::Vec2; use bevy_transform::components::Transform; use bevy_utils::HashMap; -use bevy_window::{Window, WindowId, WindowScaleFactorChanged, Windows}; +use bevy_window::{PrimaryWindow, Window, WindowResolution, WindowScaleFactorChanged}; use std::fmt; use taffy::{ prelude::{AvailableSpace, Size}, @@ -22,7 +23,7 @@ use taffy::{ #[derive(Resource)] pub struct FlexSurface { entity_to_taffy: HashMap, - window_nodes: HashMap, + window_nodes: HashMap, taffy: Taffy, } @@ -35,7 +36,6 @@ unsafe impl Sync for FlexSurface {} fn _assert_send_sync_flex_surface_impl_safe() { fn _assert_send_sync() {} _assert_send_sync::>(); - _assert_send_sync::>(); // FIXME https://github.com/DioxusLabs/taffy/issues/146 // _assert_send_sync::(); } @@ -144,11 +144,11 @@ without UI components as a child of an entity with UI components, results may be } } - pub fn update_window(&mut self, window: &Window) { + pub fn update_window(&mut self, window: Entity, window_resolution: &WindowResolution) { let taffy = &mut self.taffy; let node = self .window_nodes - .entry(window.id()) + .entry(window) .or_insert_with(|| taffy.new_leaf(taffy::style::Style::default()).unwrap()); taffy @@ -156,8 +156,12 @@ without UI components as a child of an entity with UI components, results may be *node, taffy::style::Style { size: taffy::geometry::Size { - width: taffy::style::Dimension::Points(window.physical_width() as f32), - height: taffy::style::Dimension::Points(window.physical_height() as f32), + width: taffy::style::Dimension::Points( + window_resolution.physical_width() as f32 + ), + height: taffy::style::Dimension::Points( + window_resolution.physical_height() as f32, + ), }, ..Default::default() }, @@ -167,10 +171,10 @@ without UI components as a child of an entity with UI components, results may be pub fn set_window_children( &mut self, - window_id: WindowId, + parent_window: Entity, children: impl Iterator, ) { - let taffy_node = self.window_nodes.get(&window_id).unwrap(); + let taffy_node = self.window_nodes.get(&parent_window).unwrap(); let child_nodes = children .map(|e| *self.entity_to_taffy.get(&e).unwrap()) .collect::>(); @@ -217,7 +221,8 @@ pub enum FlexError { #[allow(clippy::too_many_arguments)] pub fn flex_node_system( - windows: Res, + primary_window: Query<(Entity, &Window), With>, + windows: Query<(Entity, &Window)>, ui_scale: Res, mut scale_factor_events: EventReader, mut flex_surface: ResMut, @@ -233,13 +238,20 @@ pub fn flex_node_system( mut node_transform_query: Query<(Entity, &mut Node, &mut Transform, Option<&Parent>)>, removed_nodes: RemovedComponents, ) { + // assume one window for time being... + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let (primary_window_entity, logical_to_physical_factor) = + if let Ok((entity, primary_window)) = primary_window.get_single() { + (entity, primary_window.resolution.scale_factor()) + } else { + return; + }; + // update window root nodes - for window in windows.iter() { - flex_surface.update_window(window); + for (entity, window) in windows.iter() { + flex_surface.update_window(entity, &window.resolution); } - // assume one window for time being... - let logical_to_physical_factor = windows.scale_factor(WindowId::primary()); let scale_factor = logical_to_physical_factor * ui_scale.scale; fn update_changed( @@ -272,9 +284,7 @@ pub fn flex_node_system( flex_surface.remove_entities(&removed_nodes); // update window children (for now assuming all Nodes live in the primary window) - if let Some(primary_window) = windows.get_primary() { - flex_surface.set_window_children(primary_window.id(), root_node_query.iter()); - } + flex_surface.set_window_children(primary_window_entity, root_node_query.iter()); // update and remove children for entity in &removed_children { diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index aa5327c13ad3a..b30062818dac1 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,8 +1,9 @@ use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - change_detection::DetectChanges, + change_detection::DetectChangesMut, entity::Entity, - prelude::Component, + prelude::{Component, With}, query::WorldQuery, reflect::ReflectComponent, system::{Local, Query, Res}, @@ -10,10 +11,10 @@ use bevy_ecs::{ use bevy_input::{mouse::MouseButton, touch::Touches, Input}; use bevy_math::Vec2; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; -use bevy_render::camera::{Camera, RenderTarget}; -use bevy_render::view::ComputedVisibility; +use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::ComputedVisibility}; use bevy_transform::components::GlobalTransform; -use bevy_window::Windows; + +use bevy_window::{PrimaryWindow, Window}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -52,6 +53,39 @@ impl Default for Interaction { } } +/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A None value means that the cursor position is unknown. +/// +/// It can be used alongside interaction to get the position of the press. +#[derive( + Component, + Deref, + DerefMut, + Copy, + Clone, + Default, + PartialEq, + Debug, + Reflect, + Serialize, + Deserialize, +)] +#[reflect(Component, Serialize, Deserialize, PartialEq)] +pub struct RelativeCursorPosition { + /// Cursor position relative to size and position of the Node. + pub normalized: Option, +} + +impl RelativeCursorPosition { + /// A helper function to check if the mouse is over the node + pub fn mouse_over(&self) -> bool { + self.normalized + .map(|position| (0.0..1.).contains(&position.x) && (0.0..1.).contains(&position.y)) + .unwrap_or(false) + } +} + /// Describes whether the node should block interactions with lower nodes #[derive(Component, Copy, Clone, Eq, PartialEq, Debug, Reflect, Serialize, Deserialize)] #[reflect(Component, Serialize, Deserialize, PartialEq)] @@ -63,7 +97,7 @@ pub enum FocusPolicy { } impl FocusPolicy { - const DEFAULT: Self = Self::Block; + const DEFAULT: Self = Self::Pass; } impl Default for FocusPolicy { @@ -86,6 +120,7 @@ pub struct NodeQuery { node: &'static Node, global_transform: &'static GlobalTransform, interaction: Option<&'static mut Interaction>, + relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, calculated_clip: Option<&'static CalculatedClip>, computed_visibility: Option<&'static ComputedVisibility>, @@ -94,15 +129,19 @@ pub struct NodeQuery { /// The system that sets Interaction for all UI elements based on the mouse cursor activity /// /// Entities with a hidden [`ComputedVisibility`] are always treated as released. +#[allow(clippy::too_many_arguments)] pub fn ui_focus_system( mut state: Local, camera: Query<(&Camera, Option<&UiCameraConfig>)>, - windows: Res, + windows: Query<&Window>, mouse_button_input: Res>, touches_input: Res, ui_stack: Res, mut node_query: Query, + primary_window: Query>, ) { + let primary_window = primary_window.iter().next(); + // reset entities that were both clicked and released in the last frame for entity in state.entities_to_reset.drain(..) { if let Ok(mut interaction) = node_query.get_component_mut::(entity) { @@ -132,18 +171,20 @@ pub fn ui_focus_system( .iter() .filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui)) .filter_map(|(camera, _)| { - if let RenderTarget::Window(window_id) = camera.target { + if let Some(NormalizedRenderTarget::Window(window_id)) = + camera.target.normalize(primary_window) + { Some(window_id) } else { None } }) - .filter_map(|window_id| windows.get(window_id)) - .filter(|window| window.is_focused()) - .find_map(|window| { - window.cursor_position().map(|mut cursor_pos| { - cursor_pos.y = window.height() - cursor_pos.y; - cursor_pos + .find_map(|window_ref| { + windows.get(window_ref.entity()).ok().and_then(|window| { + window.cursor_position().map(|mut cursor_pos| { + cursor_pos.y = window.height() - cursor_pos.y; + cursor_pos + }) }) }) .or_else(|| touches_input.first_pressed_position()); @@ -164,9 +205,7 @@ pub fn ui_focus_system( // Reset their interaction to None to avoid strange stuck state if let Some(mut interaction) = node.interaction { // We cannot simply set the interaction to None, as that will trigger change detection repeatedly - if *interaction != Interaction::None { - *interaction = Interaction::None; - } + interaction.set_if_neq(Interaction::None); } return None; @@ -177,20 +216,34 @@ pub fn ui_focus_system( let ui_position = position.truncate(); let extents = node.node.size() / 2.0; let mut min = ui_position - extents; - let mut max = ui_position + extents; if let Some(clip) = node.calculated_clip { min = Vec2::max(min, clip.clip.min); - max = Vec2::min(max, clip.clip.max); } - // if the current cursor position is within the bounds of the node, consider it for + + // The mouse position relative to the node + // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + let relative_cursor_position = cursor_position.map(|cursor_position| { + Vec2::new( + (cursor_position.x - min.x) / node.node.size().x, + (cursor_position.y - min.y) / node.node.size().y, + ) + }); + + // If the current cursor position is within the bounds of the node, consider it for // clicking - let contains_cursor = if let Some(cursor_position) = cursor_position { - (min.x..max.x).contains(&cursor_position.x) - && (min.y..max.y).contains(&cursor_position.y) - } else { - false + let relative_cursor_position_component = RelativeCursorPosition { + normalized: relative_cursor_position, }; + let contains_cursor = relative_cursor_position_component.mouse_over(); + + // Save the relative cursor position to the correct component + if let Some(mut node_relative_cursor_position_component) = + node.relative_cursor_position + { + *node_relative_cursor_position_component = relative_cursor_position_component; + } + if contains_cursor { Some(*entity) } else { diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 8fd7d49f99cb2..8369b33bfe0b0 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -96,7 +96,7 @@ pub struct ImageBundle { } /// A UI node that is text -#[derive(Bundle, Clone, Debug)] +#[derive(Bundle, Clone, Debug, Default)] pub struct TextBundle { /// Describes the size of the node pub node: Node, @@ -160,25 +160,8 @@ impl TextBundle { } } -impl Default for TextBundle { - fn default() -> Self { - TextBundle { - focus_policy: FocusPolicy::Pass, - text: Default::default(), - node: Default::default(), - calculated_size: Default::default(), - style: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - visibility: Default::default(), - computed_visibility: Default::default(), - z_index: Default::default(), - } - } -} - /// A UI node that is a button -#[derive(Bundle, Clone, Debug, Default)] +#[derive(Bundle, Clone, Debug)] pub struct ButtonBundle { /// Describes the size of the node pub node: Node, @@ -213,3 +196,22 @@ pub struct ButtonBundle { /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, } + +impl Default for ButtonBundle { + fn default() -> Self { + Self { + focus_policy: FocusPolicy::Block, + node: Default::default(), + button: Default::default(), + style: Default::default(), + interaction: Default::default(), + background_color: Default::default(), + image: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), + z_index: Default::default(), + } + } +} diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 595896c1fdfec..17d6719b4a083 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -2,6 +2,7 @@ mod pipeline; mod render_pass; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; +use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; @@ -29,7 +30,6 @@ use bevy_text::{Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; use bevy_utils::FloatOrd; use bevy_utils::HashMap; -use bevy_window::{WindowId, Windows}; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -176,7 +176,6 @@ pub struct ExtractedUiNode { pub clip: Option, pub flip_x: bool, pub flip_y: bool, - pub scale_factor: f32, } #[derive(Resource, Default)] @@ -188,7 +187,6 @@ pub fn extract_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, ui_stack: Extract>, - windows: Extract>, uinode_query: Extract< Query<( &Node, @@ -200,28 +198,25 @@ pub fn extract_uinodes( )>, >, ) { - let scale_factor = windows.scale_factor(WindowId::primary()) as f32; extracted_uinodes.uinodes.clear(); for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok((uinode, transform, color, maybe_image, visibility, clip)) = uinode_query.get(*entity) { - if !visibility.is_visible() { + // Skip invisible and completely transparent nodes + if !visibility.is_visible() || color.0.a() == 0.0 { continue; } + let (image, flip_x, flip_y) = if let Some(image) = maybe_image { + // Skip loading images + if !images.contains(&image.texture) { + continue; + } (image.texture.clone_weak(), image.flip_x, image.flip_y) } else { (DEFAULT_IMAGE_HANDLE.typed().clone_weak(), false, false) }; - // Skip loading images - if !images.contains(&image) { - continue; - } - // Skip completely transparent nodes - if color.0.a() == 0.0 { - continue; - } extracted_uinodes.uinodes.push(ExtractedUiNode { stack_index, @@ -236,7 +231,6 @@ pub fn extract_uinodes( clip: clip.map(|clip| clip.clip), flip_x, flip_y, - scale_factor, }); } } @@ -301,7 +295,7 @@ pub fn extract_default_ui_camera_view( pub fn extract_text_uinodes( mut extracted_uinodes: ResMut, texture_atlases: Extract>>, - windows: Extract>, + windows: Extract>>, ui_stack: Extract>, uinode_query: Extract< Query<( @@ -314,7 +308,12 @@ pub fn extract_text_uinodes( )>, >, ) { - let scale_factor = windows.scale_factor(WindowId::primary()) as f32; + // TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor() as f32) + .unwrap_or(1.0); + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok((uinode, global_transform, text, text_layout_info, visibility, clip)) = uinode_query.get(*entity) @@ -364,7 +363,6 @@ pub fn extract_text_uinodes( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - scale_factor, }); } } @@ -500,20 +498,20 @@ pub fn prepare_uinodes( let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); let mut uvs = [ Vec2::new( - uinode_rect.min.x + positions_diff[0].x * extracted_uinode.scale_factor, - uinode_rect.min.y + positions_diff[0].y * extracted_uinode.scale_factor, + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, ), Vec2::new( - uinode_rect.max.x + positions_diff[1].x * extracted_uinode.scale_factor, - uinode_rect.min.y + positions_diff[1].y * extracted_uinode.scale_factor, + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, ), Vec2::new( - uinode_rect.max.x + positions_diff[2].x * extracted_uinode.scale_factor, - uinode_rect.max.y + positions_diff[2].y * extracted_uinode.scale_factor, + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, ), Vec2::new( - uinode_rect.min.x + positions_diff[3].x * extracted_uinode.scale_factor, - uinode_rect.max.y + positions_diff[3].y * extracted_uinode.scale_factor, + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, ), ] .map(|pos| pos / atlas_extent); @@ -562,7 +560,7 @@ pub fn queue_uinodes( view_uniforms: Res, ui_pipeline: Res, mut pipelines: ResMut>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, mut image_bind_groups: ResMut, gpu_images: Res>, ui_batches: Query<(Entity, &UiBatch)>, @@ -591,7 +589,7 @@ pub fn queue_uinodes( let draw_ui_function = draw_functions.read().id::(); for (view, mut transparent_phase) in &mut views { let pipeline = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &ui_pipeline, UiPipelineKey { hdr: view.hdr }, ); diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index ee05cf6f8a21b..63ff0a47f989c 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -76,19 +76,14 @@ impl Node for UiPassNode { } else { input_view_entity }; - let pass_descriptor = RenderPassDescriptor { + let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { label: Some("ui_pass"), color_attachments: &[Some(target.get_unsampled_color_attachment(Operations { load: LoadOp::Load, store: true, }))], depth_stencil_attachment: None, - }; - - let render_pass = render_context - .command_encoder - .begin_render_pass(&pass_descriptor); - let mut render_pass = TrackedRenderPass::new(render_pass); + }); transparent_phase.render(&mut render_pass, world, view_entity); diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index a19f7802df8a6..31f232c4d1d85 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -157,7 +157,7 @@ impl Val { /// Returns a [`ValArithmeticError::NonEvaluateable`] if the [`Val`] is impossible to evaluate into [`Val::Px`]. /// Otherwise it returns an [`f32`] containing the evaluated value in pixels. /// - /// **Note:** If a [`Val::Px`] is evaluated, it's innver value returned unchanged. + /// **Note:** If a [`Val::Px`] is evaluated, it's inner value returned unchanged. pub fn evaluate(&self, size: f32) -> Result { match self { Val::Percent(value) => Ok(size * value / 100.0), @@ -554,7 +554,7 @@ impl Default for FlexWrap { pub struct CalculatedSize { /// The size of the node pub size: Size, - /// Whether to attempt to preserve the aspect ratio when determing the layout for this item + /// Whether to attempt to preserve the aspect ratio when determining the layout for this item pub preserve_aspect_ratio: bool, } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 8455a9455af13..28a3984edbc58 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -12,7 +12,7 @@ use bevy_text::{ Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, TextSettings, YAxisOrientation, }; -use bevy_window::Windows; +use bevy_window::{PrimaryWindow, Window}; #[derive(Debug, Default)] pub struct QueuedText { @@ -51,7 +51,7 @@ pub fn text_system( mut last_scale_factor: Local, mut textures: ResMut>, fonts: Res>, - windows: Res, + windows: Query<&Window, With>, text_settings: Res, mut font_atlas_warning: ResMut, ui_scale: Res, @@ -69,13 +69,11 @@ pub fn text_system( )>, )>, ) { - // TODO: This should support window-independent scale settings. - // See https://github.com/bevyengine/bevy/issues/5621 - let scale_factor = if let Some(window) = windows.get_primary() { - window.scale_factor() * ui_scale.scale - } else { - ui_scale.scale - }; + // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 + let scale_factor = windows + .get_single() + .map(|window| window.resolution.scale_factor()) + .unwrap_or(ui_scale.scale); let inv_scale_factor = 1. / scale_factor; @@ -122,6 +120,7 @@ pub fn text_system( &text.sections, scale_factor, text.alignment, + text.linebreak_behaviour, node_size, &mut font_atlas_set_storage, &mut texture_atlases, diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index a61ce8d16594d..5a019dbc38455 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -14,6 +14,8 @@ tracing = { version = "0.1", default-features = false, features = ["std"] } instant = { version = "0.1", features = ["wasm-bindgen"] } uuid = { version = "1.1", features = ["v4", "serde"] } hashbrown = { version = "0.12", features = ["serde"] } +petgraph = "0.6" +thiserror = "1.0" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = {version = "0.2.0", features = ["js"]} diff --git a/crates/bevy_utils/src/label.rs b/crates/bevy_utils/src/label.rs index 6c504b0283a30..d3e816552bb1e 100644 --- a/crates/bevy_utils/src/label.rs +++ b/crates/bevy_utils/src/label.rs @@ -60,6 +60,47 @@ where } } +/// Macro to define a new label trait +/// +/// # Example +/// +/// ``` +/// # use bevy_utils::define_boxed_label; +/// define_boxed_label!(MyNewLabelTrait); +/// ``` +#[macro_export] +macro_rules! define_boxed_label { + ($label_trait_name:ident) => { + /// A strongly-typed label. + pub trait $label_trait_name: + 'static + Send + Sync + ::std::fmt::Debug + ::bevy_utils::label::DynHash + { + #[doc(hidden)] + fn dyn_clone(&self) -> Box; + } + + impl PartialEq for dyn $label_trait_name { + fn eq(&self, other: &Self) -> bool { + self.dyn_eq(other.as_dyn_eq()) + } + } + + impl Eq for dyn $label_trait_name {} + + impl ::std::hash::Hash for dyn $label_trait_name { + fn hash(&self, state: &mut H) { + self.dyn_hash(state); + } + } + + impl Clone for Box { + fn clone(&self) -> Self { + self.dyn_clone() + } + } + }; +} + /// Macro to define a new label trait /// /// # Example diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 840c1fdf927f3..a6a4aebcb254c 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -15,6 +15,7 @@ pub mod label; mod short_names; pub use short_names::get_short_name; pub mod synccell; +pub mod syncunsafecell; mod default; mod float_ord; @@ -24,6 +25,8 @@ pub use default::default; pub use float_ord::*; pub use hashbrown; pub use instant::{Duration, Instant}; +pub use petgraph; +pub use thiserror; pub use tracing; pub use uuid::Uuid; @@ -34,11 +37,12 @@ use std::{ future::Future, hash::{BuildHasher, Hash, Hasher}, marker::PhantomData, + mem::ManuallyDrop, ops::Deref, pin::Pin, }; -/// An owned and dynamically typed Future used when you can’t statically type your result or need to add some indirection. +/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection. #[cfg(not(target_arch = "wasm32"))] pub type BoxedFuture<'a, T> = Pin + Send + 'a>>; @@ -233,3 +237,58 @@ impl PreHashMapExt for PreHashMap { + callback: ManuallyDrop, +} + +impl OnDrop { + /// Returns an object that will invoke the specified callback when dropped. + pub fn new(callback: F) -> Self { + Self { + callback: ManuallyDrop::new(callback), + } + } +} + +impl Drop for OnDrop { + fn drop(&mut self) { + // SAFETY: We may move out of `self`, since this instance can never be observed after it's dropped. + let callback = unsafe { ManuallyDrop::take(&mut self.callback) }; + callback(); + } +} diff --git a/crates/bevy_utils/src/syncunsafecell.rs b/crates/bevy_utils/src/syncunsafecell.rs new file mode 100644 index 0000000000000..07e2422d89a8a --- /dev/null +++ b/crates/bevy_utils/src/syncunsafecell.rs @@ -0,0 +1,122 @@ +//! A reimplementation of the currently unstable [`std::cell::SyncUnsafeCell`] +//! +//! [`std::cell::SyncUnsafeCell`]: https://doc.rust-lang.org/nightly/std/cell/struct.SyncUnsafeCell.html + +pub use core::cell::UnsafeCell; + +/// [`UnsafeCell`], but [`Sync`]. +/// +/// See [tracking issue](https://github.com/rust-lang/rust/issues/95439) for upcoming native impl, +/// which should replace this one entirely (except `from_mut`). +/// +/// This is just an `UnsafeCell`, except it implements `Sync` +/// if `T` implements `Sync`. +/// +/// `UnsafeCell` doesn't implement `Sync`, to prevent accidental mis-use. +/// You can use `SyncUnsafeCell` instead of `UnsafeCell` to allow it to be +/// shared between threads, if that's intentional. +/// Providing proper synchronization is still the task of the user, +/// making this type just as unsafe to use. +/// +/// See [`UnsafeCell`] for details. +#[repr(transparent)] +pub struct SyncUnsafeCell { + value: UnsafeCell, +} + +// SAFETY: `T` is Sync, caller is responsible for upholding rust safety rules +unsafe impl Sync for SyncUnsafeCell {} + +impl SyncUnsafeCell { + /// Constructs a new instance of `SyncUnsafeCell` which will wrap the specified value. + #[inline] + pub const fn new(value: T) -> Self { + Self { + value: UnsafeCell::new(value), + } + } + + /// Unwraps the value. + #[inline] + pub fn into_inner(self) -> T { + self.value.into_inner() + } +} + +impl SyncUnsafeCell { + /// Gets a mutable pointer to the wrapped value. + /// + /// This can be cast to a pointer of any kind. + /// Ensure that the access is unique (no active references, mutable or not) + /// when casting to `&mut T`, and ensure that there are no mutations + /// or mutable aliases going on when casting to `&T` + #[inline] + pub const fn get(&self) -> *mut T { + self.value.get() + } + + /// Returns a mutable reference to the underlying data. + /// + /// This call borrows the `SyncUnsafeCell` mutably (at compile-time) which + /// guarantees that we possess the only reference. + #[inline] + pub fn get_mut(&mut self) -> &mut T { + self.value.get_mut() + } + + /// Gets a mutable pointer to the wrapped value. + /// + /// See [`UnsafeCell::get`] for details. + #[inline] + pub const fn raw_get(this: *const Self) -> *mut T { + // We can just cast the pointer from `SyncUnsafeCell` to `T` because + // of #[repr(transparent)] on both SyncUnsafeCell and UnsafeCell. + // See UnsafeCell::raw_get. + this as *const T as *mut T + } + + #[inline] + /// Returns a `&SyncUnsafeCell` from a `&mut T`. + pub fn from_mut(t: &mut T) -> &SyncUnsafeCell { + // SAFETY: `&mut` ensures unique access, and `UnsafeCell` and `SyncUnsafeCell` + // have #[repr(transparent)] + unsafe { &*(t as *mut T as *const SyncUnsafeCell) } + } +} + +impl SyncUnsafeCell<[T]> { + /// Returns a `&[SyncUnsafeCell]` from a `&SyncUnsafeCell<[T]>`. + /// # Examples + /// + /// ``` + /// # use bevy_utils::syncunsafecell::SyncUnsafeCell; + /// + /// let slice: &mut [i32] = &mut [1, 2, 3]; + /// let cell_slice: &SyncUnsafeCell<[i32]> = SyncUnsafeCell::from_mut(slice); + /// let slice_cell: &[SyncUnsafeCell] = cell_slice.as_slice_of_cells(); + /// + /// assert_eq!(slice_cell.len(), 3); + /// ``` + pub fn as_slice_of_cells(&self) -> &[SyncUnsafeCell] { + // SAFETY: `UnsafeCell` and `SyncUnsafeCell` have #[repr(transparent)] + // therefore: + // - `SyncUnsafeCell` has the same layout as `T` + // - `SyncUnsafeCell<[T]>` has the same layout as `[T]` + // - `SyncUnsafeCell<[T]>` has the same layout as `[SyncUnsafeCell]` + unsafe { &*(self as *const SyncUnsafeCell<[T]> as *const [SyncUnsafeCell]) } + } +} + +impl Default for SyncUnsafeCell { + /// Creates an `SyncUnsafeCell`, with the `Default` value for T. + fn default() -> SyncUnsafeCell { + SyncUnsafeCell::new(Default::default()) + } +} + +impl From for SyncUnsafeCell { + /// Creates a new `SyncUnsafeCell` containing the given value. + fn from(t: T) -> SyncUnsafeCell { + SyncUnsafeCell::new(t) + } +} diff --git a/crates/bevy_window/src/cursor.rs b/crates/bevy_window/src/cursor.rs index de1fa563ba96b..17e4dce56b892 100644 --- a/crates/bevy_window/src/cursor.rs +++ b/crates/bevy_window/src/cursor.rs @@ -1,12 +1,23 @@ +use bevy_reflect::{prelude::ReflectDefault, FromReflect, Reflect}; + +#[cfg(feature = "serialize")] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; + /// The icon to display for a window's cursor. /// /// Examples of all of these cursors can be found [here](https://www.w3schools.com/cssref/playit.asp?filename=playcss_cursor). /// This `enum` is simply a copy of a similar `enum` found in [`winit`](https://docs.rs/winit/latest/winit/window/enum.CursorIcon.html). /// `winit`, in turn, mostly copied cursor types available in the browser. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[derive(Default, Debug, Hash, PartialEq, Eq, Clone, Copy, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] pub enum CursorIcon { /// The platform-dependent default cursor. + #[default] Default, /// A simple crosshair. Crosshair, diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 6c8b0e10ba8f5..0141420020811 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use super::{WindowDescriptor, WindowId}; +use bevy_ecs::entity::Entity; use bevy_math::{IVec2, Vec2}; use bevy_reflect::{FromReflect, Reflect}; @@ -16,26 +16,15 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; reflect(Serialize, Deserialize) )] pub struct WindowResized { - pub id: WindowId, + /// Window that has changed. + pub window: Entity, /// The new logical width of the window. pub width: f32, /// The new logical height of the window. pub height: f32, } -/// An event that indicates that a new window should be created. -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct CreateWindow { - pub id: WindowId, - pub descriptor: WindowDescriptor, -} - +// TODO: This would redraw all windows ? If yes, update docs to reflect this /// An event that indicates the window should redraw, even if its control flow is set to `Wait` and /// there have been no window events. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] @@ -49,8 +38,7 @@ pub struct RequestRedraw; /// An event that is sent whenever a new window is created. /// -/// To create a new window, send a [`CreateWindow`] event - this -/// event will be sent in the handler for that event. +/// To create a new window, spawn an entity with a [`crate::Window`] on it. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -59,20 +47,20 @@ pub struct RequestRedraw; reflect(Serialize, Deserialize) )] pub struct WindowCreated { - pub id: WindowId, + /// Window that has been created. + pub window: Entity, } /// An event that is sent whenever the operating systems requests that a window /// be closed. This will be sent when the close button of the window is pressed. /// /// If the default [`WindowPlugin`] is used, these events are handled -/// by [closing] the corresponding [`Window`]. +/// by closing the corresponding [`Window`]. /// To disable this behaviour, set `close_when_requested` on the [`WindowPlugin`] /// to `false`. /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -/// [closing]: crate::Window::close #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -81,13 +69,12 @@ pub struct WindowCreated { reflect(Serialize, Deserialize) )] pub struct WindowCloseRequested { - pub id: WindowId, + /// Window to close. + pub window: Entity, } -/// An event that is sent whenever a window is closed. This will be sent by the -/// handler for [`Window::close`]. -/// -/// [`Window::close`]: crate::Window::close +/// An event that is sent whenever a window is closed. This will be sent when +/// the window entity loses its `Window` component or is despawned. #[derive(Debug, Clone, PartialEq, Eq, Reflect, FromReflect)] #[reflect(Debug, PartialEq)] #[cfg_attr( @@ -96,10 +83,13 @@ pub struct WindowCloseRequested { reflect(Serialize, Deserialize) )] pub struct WindowClosed { - pub id: WindowId, + /// Window that has been closed. + /// + /// Note that this entity probably no longer exists + /// by the time this event is received. + pub window: Entity, } - -/// An event reporting that the mouse cursor has moved on a window. +/// An event reporting that the mouse cursor has moved inside a window. /// /// The event is sent only if the cursor is over one of the application's windows. /// It is the translated version of [`WindowEvent::CursorMoved`] from the `winit` crate. @@ -116,10 +106,9 @@ pub struct WindowClosed { reflect(Serialize, Deserialize) )] pub struct CursorMoved { - /// The identifier of the window the cursor has moved on. - pub id: WindowId, - - /// The position of the cursor, in window coordinates. + /// Window that the cursor moved inside. + pub window: Entity, + /// The cursor position in logical pixels. pub position: Vec2, } @@ -132,7 +121,8 @@ pub struct CursorMoved { reflect(Serialize, Deserialize) )] pub struct CursorEntered { - pub id: WindowId, + /// Window that the cursor entered. + pub window: Entity, } /// An event that is sent whenever the user's cursor leaves a window. @@ -144,7 +134,8 @@ pub struct CursorEntered { reflect(Serialize, Deserialize) )] pub struct CursorLeft { - pub id: WindowId, + /// Window that the cursor left. + pub window: Entity, } /// An event that is sent whenever a window receives a character from the OS or underlying system. @@ -156,7 +147,9 @@ pub struct CursorLeft { reflect(Serialize, Deserialize) )] pub struct ReceivedCharacter { - pub id: WindowId, + /// Window that received the character. + pub window: Entity, + /// Received character. pub char: char, } @@ -169,7 +162,9 @@ pub struct ReceivedCharacter { reflect(Serialize, Deserialize) )] pub struct WindowFocused { - pub id: WindowId, + /// Window that changed focus. + pub window: Entity, + /// Whether it was focused (true) or lost focused (false). pub focused: bool, } @@ -182,7 +177,9 @@ pub struct WindowFocused { reflect(Serialize, Deserialize) )] pub struct WindowScaleFactorChanged { - pub id: WindowId, + /// Window that had it's scale factor changed. + pub window: Entity, + /// The new scale factor. pub scale_factor: f64, } @@ -195,7 +192,9 @@ pub struct WindowScaleFactorChanged { reflect(Serialize, Deserialize) )] pub struct WindowBackendScaleFactorChanged { - pub id: WindowId, + /// Window that had it's scale factor changed by the backend. + pub window: Entity, + /// The new scale factor. pub scale_factor: f64, } @@ -208,11 +207,27 @@ pub struct WindowBackendScaleFactorChanged { reflect(Serialize, Deserialize) )] pub enum FileDragAndDrop { - DroppedFile { id: WindowId, path_buf: PathBuf }, - - HoveredFile { id: WindowId, path_buf: PathBuf }, - - HoveredFileCancelled { id: WindowId }, + /// File is being dropped into a window. + DroppedFile { + /// Window the file was dropped into. + window: Entity, + /// Path to the file that was dropped in. + path_buf: PathBuf, + }, + + /// File is currently being hovered over a window. + HoveredFile { + /// Window a file is possibly going to be dropped into. + window: Entity, + /// Path to the file that might be dropped in. + path_buf: PathBuf, + }, + + /// File hovering was cancelled. + HoveredFileCancelled { + /// Window that had a cancelled file drop. + window: Entity, + }, } /// An event that is sent when a window is repositioned in physical pixels. @@ -224,6 +239,8 @@ pub enum FileDragAndDrop { reflect(Serialize, Deserialize) )] pub struct WindowMoved { - pub id: WindowId, + /// Window that moved. + pub entity: Entity, + /// Where the window moved to in physical pixels. pub position: IVec2, } diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 1632c06042d66..65d4af553921f 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -4,34 +4,33 @@ mod event; mod raw_handle; mod system; mod window; -mod windows; pub use crate::raw_handle::*; + pub use cursor::*; pub use event::*; pub use system::*; pub use window::*; -pub use windows::*; pub mod prelude { #[doc(hidden)] pub use crate::{ CursorEntered, CursorIcon, CursorLeft, CursorMoved, FileDragAndDrop, MonitorSelection, - ReceivedCharacter, Window, WindowDescriptor, WindowMode, WindowMoved, WindowPlugin, - WindowPosition, Windows, + ReceivedCharacter, Window, WindowMoved, WindowPlugin, WindowPosition, + WindowResizeConstraints, }; } -use bevy_app::prelude::*; -use bevy_ecs::schedule::{IntoSystemDescriptor, SystemLabel}; use std::path::PathBuf; +use bevy_app::prelude::*; +use bevy_ecs::schedule::SystemLabel; + impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { - window: Default::default(), - add_primary_window: true, - exit_on_all_closed: true, + primary_window: Some(Window::default()), + exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } } @@ -39,21 +38,26 @@ impl Default for WindowPlugin { /// A [`Plugin`] that defines an interface for windowing support in Bevy. pub struct WindowPlugin { - pub window: WindowDescriptor, - /// Whether to create a window when added. + /// Settings for the primary window. This will be spawned by + /// default, if you want to run without a primary window you should + /// set this to `None`. /// /// Note that if there are no windows, by default the App will exit, /// due to [`exit_on_all_closed`]. - pub add_primary_window: bool, + pub primary_window: Option, + /// Whether to exit the app when there are no open windows. /// /// If disabling this, ensure that you send the [`bevy_app::AppExit`] /// event when the app should exit. If this does not occur, you will /// create 'headless' processes (processes without windows), which may - /// surprise your users. It is recommended to leave this setting as `true`. + /// surprise your users. It is recommended to leave this setting to + /// either [`ExitCondition::OnAllClosed`] or [`ExitCondition::OnPrimaryClosed`]. /// - /// If true, this plugin will add [`exit_on_all_closed`] to [`CoreStage::PostUpdate`]. - pub exit_on_all_closed: bool, + /// [`ExitCondition::OnAllClosed`] will add [`exit_on_all_closed`] to [`CoreStage::Update`]. + /// [`ExitCondition::OnPrimaryClosed`] will add [`exit_on_primary_closed`] to [`CoreStage::Update`]. + pub exit_condition: ExitCondition, + /// Whether to close windows when they are requested to be closed (i.e. /// when the close button is pressed). /// @@ -65,8 +69,8 @@ pub struct WindowPlugin { impl Plugin for WindowPlugin { fn build(&self, app: &mut App) { + // User convenience events app.add_event::() - .add_event::() .add_event::() .add_event::() .add_event::() @@ -79,29 +83,30 @@ impl Plugin for WindowPlugin { .add_event::() .add_event::() .add_event::() - .add_event::() - .init_resource::(); - - if self.add_primary_window { - app.world.send_event(CreateWindow { - id: WindowId::primary(), - descriptor: self.window.clone(), - }); + .add_event::(); + + if let Some(primary_window) = &self.primary_window { + app.world + .spawn(primary_window.clone()) + .insert(PrimaryWindow); } - if self.exit_on_all_closed { - app.add_system_to_stage( - CoreStage::PostUpdate, - exit_on_all_closed.after(ModifiesWindows), - ); + match self.exit_condition { + ExitCondition::OnPrimaryClosed => { + app.add_system_to_stage(CoreStage::PostUpdate, exit_on_primary_closed); + } + ExitCondition::OnAllClosed => { + app.add_system_to_stage(CoreStage::PostUpdate, exit_on_all_closed); + } + ExitCondition::DontExit => {} } + if self.close_when_requested { app.add_system(close_when_requested); } // Register event types app.register_type::() - .register_type::() .register_type::() .register_type::() .register_type::() @@ -117,18 +122,41 @@ impl Plugin for WindowPlugin { .register_type::(); // Register window descriptor and related types - app.register_type::() - .register_type::() - .register_type::() - .register_type::() + app.register_type::() + .register_type::() + .register_type::() .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::() - .register_type::(); + .register_type::(); // Register `PathBuf` as it's used by `FileDragAndDrop` app.register_type::(); } } +/// System Label marking when changes are applied to windows #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub struct ModifiesWindows; + +/// Defines the specific conditions the application should exit on +#[derive(Clone)] +pub enum ExitCondition { + /// Close application when the primary window is closed + /// + /// The plugin will add [`exit_on_primary_closed`] to [`CoreStage::Update`]. + OnPrimaryClosed, + /// Close application when all windows are closed + /// + /// The plugin will add [`exit_on_all_closed`] to [`CoreStage::Update`]. + OnAllClosed, + /// Keep application running headless even after closing all windows + /// + /// If selecting this, ensure that you send the [`bevy_app::AppExit`] + /// event when the app should exit. If this does not occur, you will + /// create 'headless' processes (processes without windows), which may + /// surprise your users. + DontExit, +} diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index 0e495d2c138b5..6c535605991f0 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -1,3 +1,4 @@ +use bevy_ecs::prelude::Component; use raw_window_handle::{ HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, }; @@ -7,7 +8,7 @@ use raw_window_handle::{ /// Depending on the platform, the underlying pointer-containing handle cannot be used on all threads, /// and so we cannot simply make it (or any type that has a safe operation to get a [`RawWindowHandle`] or [`RawDisplayHandle`]) /// thread-safe. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Component)] pub struct RawHandleWrapper { pub window_handle: RawWindowHandle, pub display_handle: RawDisplayHandle, diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index 4cf79fa54997e..ddd2e726409b7 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -1,4 +1,4 @@ -use crate::{Window, WindowCloseRequested, Windows}; +use crate::{PrimaryWindow, Window, WindowCloseRequested}; use bevy_app::AppExit; use bevy_ecs::prelude::*; @@ -11,8 +11,24 @@ use bevy_input::{keyboard::KeyCode, Input}; /// Ensure that you read the caveats documented on that field if doing so. /// /// [`WindowPlugin`]: crate::WindowPlugin -pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Res) { - if windows.iter().count() == 0 { +pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Query<&Window>) { + if windows.is_empty() { + bevy_utils::tracing::info!("No windows are open, exiting"); + app_exit_events.send(AppExit); + } +} + +/// Exit the application when the primary window has been closed +/// +/// This system is added by the [`WindowPlugin`] +/// +/// [`WindowPlugin`]: crate::WindowPlugin +pub fn exit_on_primary_closed( + mut app_exit_events: EventWriter, + windows: Query<(), (With, With)>, +) { + if windows.is_empty() { + bevy_utils::tracing::info!("Primary windows was closed, exiting"); app_exit_events.send(AppExit); } } @@ -24,22 +40,27 @@ pub fn exit_on_all_closed(mut app_exit_events: EventWriter, windows: Re /// Ensure that you read the caveats documented on that field if doing so. /// /// [`WindowPlugin`]: crate::WindowPlugin -pub fn close_when_requested( - mut windows: ResMut, - mut closed: EventReader, -) { +pub fn close_when_requested(mut commands: Commands, mut closed: EventReader) { for event in closed.iter() { - windows.get_mut(event.id).map(Window::close); + commands.entity(event.window).despawn(); } } /// Close the focused window whenever the escape key (Esc) is pressed /// /// This is useful for examples or prototyping. -pub fn close_on_esc(mut windows: ResMut, input: Res>) { - if input.just_pressed(KeyCode::Escape) { - if let Some(window) = windows.get_focused_mut() { - window.close(); +pub fn close_on_esc( + mut commands: Commands, + focused_windows: Query<(Entity, &Window)>, + input: Res>, +) { + for (window, focus) in focused_windows.iter() { + if !focus.focused { + continue; + } + + if input.just_pressed(KeyCode::Escape) { + commands.entity(window).despawn(); } } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 17b54b508a59c..ec29a7e9692aa 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1,137 +1,259 @@ -use bevy_math::{DVec2, IVec2, UVec2, Vec2}; +use bevy_ecs::{ + entity::{Entity, EntityMap, MapEntities, MapEntitiesError}, + prelude::{Component, ReflectComponent}, +}; +use bevy_math::{DVec2, IVec2, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect}; -use bevy_utils::{tracing::warn, Uuid}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -/// A unique ID for a [`Window`]. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Reflect, FromReflect)] -#[reflect_value(Debug, PartialEq, Hash, Default)] +use bevy_utils::tracing::warn; + +use crate::CursorIcon; + +/// Marker component for the window considered the primary window. +/// +/// Currently this is assumed to only exist on 1 entity at a time. +#[derive(Default, Debug, Component, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Reflect)] +#[reflect(Component)] +pub struct PrimaryWindow; + +/// Reference to a window, whether it be a direct link to a specific entity or +/// a more vague defaulting choice. +#[repr(C)] +#[derive(Default, Copy, Clone, Debug, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), - reflect_value(Serialize, Deserialize) + reflect(Serialize, Deserialize) )] -pub struct WindowId(Uuid); +pub enum WindowRef { + /// This will be linked to the primary window that is created by default + /// in the [`WindowPlugin`](crate::WindowPlugin::primary_window). + #[default] + Primary, + /// A more direct link to a window entity. + /// + /// Use this if you want to reference a secondary/tertiary/... window. + /// + /// To create a new window you can spawn an entity with a [`Window`], + /// then you can use that entity here for usage in cameras. + Entity(Entity), +} -/// Presentation mode for a window. -/// -/// The presentation mode specifies when a frame is presented to the window. The `Fifo` -/// option corresponds to a traditional `VSync`, where the framerate is capped by the -/// display refresh rate. Both `Immediate` and `Mailbox` are low-latency and are not -/// capped by the refresh rate, but may not be available on all platforms. Tearing -/// may be observed with `Immediate` mode, but will not be observed with `Mailbox` or -/// `Fifo`. -/// -/// `AutoVsync` or `AutoNoVsync` will gracefully fallback to `Fifo` when unavailable. -/// -/// `Immediate` or `Mailbox` will panic if not supported by the platform. +impl WindowRef { + /// Normalize the window reference so that it can be compared to other window references. + pub fn normalize(&self, primary_window: Option) -> Option { + let entity = match self { + Self::Primary => primary_window, + Self::Entity(entity) => Some(*entity), + }; + + entity.map(NormalizedWindowRef) + } +} + +impl MapEntities for WindowRef { + fn map_entities(&mut self, entity_map: &EntityMap) -> Result<(), MapEntitiesError> { + match self { + Self::Entity(entity) => { + *entity = entity_map.get(*entity)?; + Ok(()) + } + Self::Primary => Ok(()), + } + } +} + +/// A flattened representation of a window reference for equality/hashing purposes. /// -/// The presentation mode may be declared in the [`WindowDescriptor`](WindowDescriptor) using [`WindowDescriptor::present_mode`](WindowDescriptor::present_mode) -/// or updated on a [`Window`](Window) using [`set_present_mode`](Window::set_present_mode). +/// For most purposes you probably want to use the unnormalized version [`WindowRef`]. #[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Hash)] -#[doc(alias = "vsync")] -pub enum PresentMode { - /// Chooses FifoRelaxed -> Fifo based on availability. - /// - /// Because of the fallback behavior, it is supported everywhere. - AutoVsync = 0, - /// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability. - /// - /// Because of the fallback behavior, it is supported everywhere. - AutoNoVsync = 1, - /// The presentation engine does **not** wait for a vertical blanking period and - /// the request is presented immediately. This is a low-latency presentation mode, - /// but visible tearing may be observed. Not optimal for mobile. - /// - /// Selecting this variant will panic if not supported, it is preferred to use - /// [`PresentMode::AutoNoVsync`]. - Immediate = 2, - /// The presentation engine waits for the next vertical blanking period to update - /// the current image, but frames may be submitted without delay. This is a low-latency - /// presentation mode and visible tearing will **not** be observed. Not optimal for mobile. - /// - /// Selecting this variant will panic if not supported, it is preferred to use - /// [`PresentMode::AutoNoVsync`]. - Mailbox = 3, - /// The presentation engine waits for the next vertical blanking period to update - /// the current image. The framerate will be capped at the display refresh rate, - /// corresponding to the `VSync`. Tearing cannot be observed. Optimal for mobile. - Fifo = 4, // NOTE: The explicit ordinal values mirror wgpu. +pub struct NormalizedWindowRef(Entity); + +impl NormalizedWindowRef { + /// Fetch the entity of this window reference + pub fn entity(&self) -> Entity { + self.0 + } } -/// Specifies how the alpha channel of the textures should be handled during compositing. -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] +/// Define how a window will be created and how it will behave. +#[derive(Component, Debug, Clone, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Hash)] -pub enum CompositeAlphaMode { - /// Chooses either `Opaque` or `Inherit` automatically,depending on the - /// `alpha_mode` that the current surface can support. - Auto = 0, - /// The alpha channel, if it exists, of the textures is ignored in the - /// compositing process. Instead, the textures is treated as if it has a - /// constant alpha of 1.0. - Opaque = 1, - /// The alpha channel, if it exists, of the textures is respected in the - /// compositing process. The non-alpha channels of the textures are - /// expected to already be multiplied by the alpha channel by the - /// application. - PreMultiplied = 2, - /// The alpha channel, if it exists, of the textures is respected in the - /// compositing process. The non-alpha channels of the textures are not - /// expected to already be multiplied by the alpha channel by the - /// application; instead, the compositor will multiply the non-alpha - /// channels of the texture by the alpha channel during compositing. - PostMultiplied = 3, - /// The alpha channel, if it exists, of the textures is unknown for processing - /// during compositing. Instead, the application is responsible for setting - /// the composite alpha blending mode using native WSI command. If not set, - /// then a platform-specific default will be used. - Inherit = 4, +#[reflect(Component, Default)] +pub struct Window { + /// The cursor of this window. + pub cursor: Cursor, + /// What presentation mode to give the window. + pub present_mode: PresentMode, + /// Which fullscreen or windowing mode should be used? + pub mode: WindowMode, + /// Where the window should be placed. + pub position: WindowPosition, + /// What resolution the window should have. + pub resolution: WindowResolution, + /// Stores the title of the window. + pub title: String, + /// How the alpha channel of textures should be handled while compositing. + pub composite_alpha_mode: CompositeAlphaMode, + /// Which size limits to give the window. + pub resize_constraints: WindowResizeConstraints, + /// Should the window be resizable? + /// + /// Note: This does not stop the program from fullscreening/setting + /// the size programmatically. + pub resizable: bool, + /// Should the window have decorations enabled? + /// + /// (Decorations are the minimize, maximize, and close buttons on desktop apps) + /// + // ## Platform-specific + // + // **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. + pub decorations: bool, + /// Should the window be transparent? + /// + /// Defines whether the background of the window should be transparent. + /// + /// ## Platform-specific + /// - iOS / Android / Web: Unsupported. + /// - macOS X: Not working as expected. + /// - Windows 11: Not working as expected + /// macOS X transparent works with winit out of the box, so this issue might be related to: + /// Windows 11 is related to + pub transparent: bool, + /// Should the window start focused? + pub focused: bool, + /// Should the window always be on top of other windows? + /// + /// ## Platform-specific + /// + /// - iOS / Android / Web / Wayland: Unsupported. + pub always_on_top: bool, + /// The "html canvas" element selector. + /// + /// If set, this selector will be used to find a matching html canvas element, + /// rather than creating a new one. + /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). + /// + /// This value has no effect on non-web platforms. + pub canvas: Option, + /// Whether or not to fit the canvas element's size to its parent element's size. + /// + /// **Warning**: this will not behave as expected for parents that set their size according to the size of their + /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this + /// feature, ensure the parent's size is not affected by its children. + /// + /// This value has no effect on non-web platforms. + pub fit_canvas_to_parent: bool, + /// Stores internal state that isn't directly accessible. + pub internal: InternalWindowState, +} + +impl Default for Window { + fn default() -> Self { + Self { + title: "Bevy App".to_owned(), + cursor: Default::default(), + present_mode: Default::default(), + mode: Default::default(), + position: Default::default(), + resolution: Default::default(), + internal: Default::default(), + composite_alpha_mode: Default::default(), + resize_constraints: Default::default(), + resizable: true, + decorations: true, + transparent: false, + focused: true, + always_on_top: false, + fit_canvas_to_parent: false, + canvas: None, + } + } } -impl WindowId { - /// Creates a new [`WindowId`]. - pub fn new() -> Self { - WindowId(Uuid::new_v4()) +impl Window { + /// Setting this to true will attempt to maximize the window. + /// + /// Setting it to false will attempt to un-maximize the window. + pub fn set_maximized(&mut self, maximized: bool) { + self.internal.maximize_request = Some(maximized); } - /// The [`WindowId`] for the primary window. - pub const fn primary() -> Self { - WindowId(Uuid::from_u128(0)) + + /// Setting this to true will attempt to minimize the window. + /// + /// Setting it to false will attempt to un-minimize the window. + pub fn set_minimized(&mut self, minimized: bool) { + self.internal.minimize_request = Some(minimized); } - /// Get whether or not this [`WindowId`] is for the primary window. - pub fn is_primary(&self) -> bool { - *self == WindowId::primary() + + /// The window's client area width in logical pixels. + #[inline] + pub fn width(&self) -> f32 { + self.resolution.width() } -} -use crate::CursorIcon; -use std::fmt; + /// The window's client area height in logical pixels. + #[inline] + pub fn height(&self) -> f32 { + self.resolution.height() + } -use crate::raw_handle::RawHandleWrapper; + /// The window's client area width in physical pixels. + #[inline] + pub fn physical_width(&self) -> u32 { + self.resolution.physical_width() + } -impl fmt::Display for WindowId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.as_simple().fmt(f) + /// The window's client area height in physical pixels. + #[inline] + pub fn physical_height(&self) -> u32 { + self.resolution.physical_height() } -} -impl Default for WindowId { - fn default() -> Self { - WindowId::primary() + /// The window's scale factor. + #[inline] + pub fn scale_factor(&self) -> f64 { + self.resolution.scale_factor() + } + + /// The cursor position in this window + #[inline] + pub fn cursor_position(&self) -> Option { + self.cursor + .physical_position + .map(|position| (position / self.scale_factor()).as_vec2()) + } + + /// The physical cursor position in this window + #[inline] + pub fn physical_cursor_position(&self) -> Option { + self.cursor + .physical_position + .map(|position| position.as_vec2()) + } + + /// Set the cursor position in this window + pub fn set_cursor_position(&mut self, position: Option) { + self.cursor.physical_position = position.map(|p| p.as_dvec2() * self.scale_factor()); + } + + /// Set the physical cursor position in this window + pub fn set_physical_cursor_position(&mut self, position: Option) { + self.cursor.physical_position = position; } } @@ -150,9 +272,13 @@ impl Default for WindowId { )] #[reflect(Debug, PartialEq, Default)] pub struct WindowResizeConstraints { + /// The minimum width the window can have. pub min_width: f32, + /// The minimum height the window can have. pub min_height: f32, + /// The maximum width the window can have. pub max_width: f32, + /// The maximum height the window can have. pub max_height: f32, } @@ -168,6 +294,9 @@ impl Default for WindowResizeConstraints { } impl WindowResizeConstraints { + /// Checks if the constraints are valid. + /// + /// Will output warnings if it isn't. #[must_use] pub fn check_constraints(&self) -> Self { let WindowResizeConstraints { @@ -201,10 +330,101 @@ impl WindowResizeConstraints { } } -/// An operating system window that can present content and receive user input. -/// -/// To create a window, use a [`EventWriter`](`crate::CreateWindow`). -/// +/// Stores data about the window's cursor. +#[derive(Debug, Copy, Clone, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, Default)] +pub struct Cursor { + /// Get the current [`CursorIcon`] while inside the window. + pub icon: CursorIcon, + + /// Whether the cursor is visible or not. + /// + /// ## Platform-specific + /// + /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. + /// To stop the cursor from leaving the window, change [`Cursor::grab_mode`] to [`CursorGrabMode::Locked`] or [`CursorGrabMode::Confined`] + /// - **`macOS`**: The cursor is hidden only when the window is focused. + /// - **`iOS`** and **`Android`** do not have cursors + pub visible: bool, + + /// Whether or not the cursor is locked. + /// + /// ## Platform-specific + /// + /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] + /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] + /// - **`iOS/Android`** don't have cursors. + /// + /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. + pub grab_mode: CursorGrabMode, + + /// Set whether or not mouse events within *this* window are captured or fall through to the Window below. + /// + /// ## Platform-specific + /// + /// - iOS / Android / Web / X11: Unsupported. + pub hit_test: bool, + + /// The position of this window's cursor. + physical_position: Option, +} + +impl Default for Cursor { + fn default() -> Self { + Cursor { + icon: CursorIcon::Default, + visible: true, + grab_mode: CursorGrabMode::None, + hit_test: true, + physical_position: None, + } + } +} + +/// Defines where window should be placed at on creation. +#[derive(Default, Debug, Clone, Copy, PartialEq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq)] +pub enum WindowPosition { + /// Position will be set by the window manager + #[default] + Automatic, + /// Window will be centered on the selected monitor + /// + /// Note that this does not account for window decorations. + Centered(MonitorSelection), + /// The window's top-left corner will be placed at the specified position (in physical pixels) + /// + /// (0,0) represents top-left corner of screen space. + At(IVec2), +} + +impl WindowPosition { + /// Creates a new [`WindowPosition`] at a position. + pub fn new(position: IVec2) -> Self { + Self::At(position) + } + + /// Set the position to a specific point. + pub fn set(&mut self, position: IVec2) { + *self = WindowPosition::At(position); + } + + /// Set the window to a specific monitor. + pub fn center(&mut self, monitor: MonitorSelection) { + *self = WindowPosition::Centered(monitor); + } +} + /// ## Window Sizes /// /// There are three sizes associated with a window. The physical size which is @@ -218,308 +438,57 @@ impl WindowResizeConstraints { /// requested size due to operating system limits on the window size, or the /// quantization of the logical size when converting the physical size to the /// logical size through the scaling factor. -/// -/// ## Accessing a `Window` from a system -/// -/// To access a `Window` from a system, use [`bevy_ecs::change_detection::ResMut`]`<`[`crate::Windows`]`>`. -/// -/// ### Example -/// ```no_run -/// # use bevy_app::App; -/// # use bevy_window::Windows; -/// # use bevy_ecs::change_detection::ResMut; -/// # fn main(){ -/// # App::new().add_system(access_window_system).run(); -/// # } -/// fn access_window_system(mut windows: ResMut){ -/// for mut window in windows.iter_mut() { -/// window.set_title(String::from("Yay, I'm a window!")); -/// } -/// } -/// ``` -/// To test code that uses `Window`s, one can test it with varying `Window` parameters by -/// creating `WindowResizeConstraints` or `WindowDescriptor` structures. -/// values by setting -/// -/// ``` -/// # use bevy_utils::default; -/// # use bevy_window::{Window, WindowCommand, WindowDescriptor, WindowId, WindowResizeConstraints}; -/// # fn compute_window_area(w: &Window) -> f32 { -/// # w.width() * w.height() -/// # } -/// # fn grow_window_to_text_size(_window: &mut Window, _text: &str) {} -/// # fn set_new_title(window: &mut Window, text: String) { window.set_title(text); } -/// # fn a_window_resize_test() { -/// let resize_constraints = WindowResizeConstraints { -/// min_width: 400.0, -/// min_height: 300.0, -/// max_width: 1280.0, -/// max_height: 1024.0, -/// }; -/// let window_descriptor = WindowDescriptor { -/// width: 800.0, -/// height: 600.0, -/// resizable: true, -/// resize_constraints, -/// ..default() -/// }; -/// let mut window = Window::new( -/// WindowId::new(), -/// &window_descriptor, -/// 100, // physical_width -/// 100, // physical_height -/// 1.0, // scale_factor -/// None, None); -/// -/// let area = compute_window_area(&window); -/// assert_eq!(area, 100.0 * 100.0); -/// -/// grow_window_to_text_size(&mut window, "very long text that does not wrap"); -/// assert_eq!(window.physical_width(), window.requested_width() as u32); -/// grow_window_to_text_size(&mut window, "very long text that does wrap, creating a maximum width window"); -/// assert_eq!(window.physical_width(), window.requested_width() as u32); -/// -/// set_new_title(&mut window, "new title".to_string()); -/// let mut found_command = false; -/// for command in window.drain_commands() { -/// if command == (WindowCommand::SetTitle{ title: "new title".to_string() }) { -/// found_command = true; -/// break; -/// } -/// } -/// assert_eq!(found_command, true); -/// } -/// ``` -#[derive(Debug)] -pub struct Window { - id: WindowId, - requested_width: f32, - requested_height: f32, +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct WindowResolution { physical_width: u32, physical_height: u32, - resize_constraints: WindowResizeConstraints, - position: Option, scale_factor_override: Option, - backend_scale_factor: f64, - title: String, - present_mode: PresentMode, - resizable: bool, - decorations: bool, - cursor_icon: CursorIcon, - cursor_visible: bool, - cursor_grab_mode: CursorGrabMode, - hittest: bool, - physical_cursor_position: Option, - raw_handle: Option, - focused: bool, - mode: WindowMode, - canvas: Option, - fit_canvas_to_parent: bool, - command_queue: Vec, - alpha_mode: CompositeAlphaMode, - always_on_top: bool, + scale_factor: f64, } -/// A command to be sent to a window. -/// -/// Bevy apps don't interact with this `enum` directly. Instead, they should use the methods on [`Window`]. -/// This `enum` is meant for authors of windowing plugins. See the documentation on [`crate::WindowPlugin`] for more information. -#[derive(Debug, PartialEq)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub enum WindowCommand { - /// Set the window's [`WindowMode`]. - SetWindowMode { - mode: WindowMode, - resolution: UVec2, - }, - /// Set the window's title. - SetTitle { - title: String, - }, - /// Set the window's scale factor. - SetScaleFactor { - scale_factor: f64, - }, - /// Set the window's resolution. - SetResolution { - logical_resolution: Vec2, - scale_factor: f64, - }, - /// Set the window's [`PresentMode`]. - SetPresentMode { - present_mode: PresentMode, - }, - /// Set whether or not the window is resizable. - SetResizable { - resizable: bool, - }, - /// Set whether or not the window has decorations. - /// - /// Examples of decorations include the close, full screen, and minimize buttons - SetDecorations { - decorations: bool, - }, - /// Set whether or not the cursor's position is locked. - SetCursorGrabMode { - grab_mode: CursorGrabMode, - }, - /// Set the cursor's [`CursorIcon`]. - SetCursorIcon { - icon: CursorIcon, - }, - /// Set whether or not the cursor is visible. - SetCursorVisibility { - visible: bool, - }, - /// Set the cursor's position. - SetCursorPosition { - position: Vec2, - }, - /// Set whether or not mouse events within *this* window are captured, or fall through to the Window below. - SetCursorHitTest { - hittest: bool, - }, - /// Set whether or not the window is maximized. - SetMaximized { - maximized: bool, - }, - /// Set whether or not the window is minimized. - SetMinimized { - minimized: bool, - }, - /// Set the window's position on the selected monitor. - SetPosition { - monitor_selection: MonitorSelection, - position: IVec2, - }, - /// Sets the position of the window to be in the center of the selected monitor. - Center(MonitorSelection), - /// Set the window's [`WindowResizeConstraints`] - SetResizeConstraints { - resize_constraints: WindowResizeConstraints, - }, - /// Set whether the window is always on top. - SetAlwaysOnTop { - always_on_top: bool, - }, - Close, + +impl Default for WindowResolution { + fn default() -> Self { + WindowResolution { + physical_width: 1280, + physical_height: 720, + scale_factor_override: None, + scale_factor: 1.0, + } + } } -/// Defines if and how the cursor is grabbed. -/// -/// Use this enum with [`Window::set_cursor_grab_mode`] to grab the cursor. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -#[reflect(Debug, PartialEq)] -pub enum CursorGrabMode { - /// The cursor can freely leave the window. - None, - /// The cursor is confined to the window area. - Confined, - /// The cursor is locked inside the window area to a certain position. - Locked, -} - -/// Defines the way a window is displayed. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -#[reflect(Debug, PartialEq)] -pub enum WindowMode { - /// Creates a window that uses the given size. - Windowed, - /// Creates a borderless window that uses the full size of the screen. - BorderlessFullscreen, - /// Creates a fullscreen window that will render at desktop resolution. - /// - /// The app will use the closest supported size from the given size and scale it to fit the screen. - SizedFullscreen, - /// Creates a fullscreen window that uses the maximum supported size. - Fullscreen, -} - -impl Window { - /// Creates a new [`Window`]. - pub fn new( - id: WindowId, - window_descriptor: &WindowDescriptor, - physical_width: u32, - physical_height: u32, - scale_factor: f64, - position: Option, - raw_handle: Option, - ) -> Self { - Window { - id, - requested_width: window_descriptor.width, - requested_height: window_descriptor.height, - position, - physical_width, - physical_height, - resize_constraints: window_descriptor.resize_constraints, - scale_factor_override: window_descriptor.scale_factor_override, - backend_scale_factor: scale_factor, - title: window_descriptor.title.clone(), - present_mode: window_descriptor.present_mode, - resizable: window_descriptor.resizable, - decorations: window_descriptor.decorations, - cursor_visible: window_descriptor.cursor_visible, - cursor_grab_mode: window_descriptor.cursor_grab_mode, - cursor_icon: CursorIcon::Default, - hittest: true, - physical_cursor_position: None, - raw_handle, - focused: false, - mode: window_descriptor.mode, - canvas: window_descriptor.canvas.clone(), - fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, - command_queue: Vec::new(), - alpha_mode: window_descriptor.alpha_mode, - always_on_top: window_descriptor.always_on_top, +impl WindowResolution { + /// Creates a new [`WindowResolution`]. + pub fn new(logical_width: f32, logical_height: f32) -> Self { + Self { + physical_width: logical_width as u32, + physical_height: logical_height as u32, + ..Default::default() } } - /// Get the window's [`WindowId`]. - #[inline] - pub fn id(&self) -> WindowId { - self.id + + /// Builder method for adding a scale factor override to the resolution. + pub fn with_scale_factor_override(mut self, scale_factor_override: f64) -> Self { + self.scale_factor_override = Some(scale_factor_override); + self } - /// The current logical width of the window's client area. + /// The window's client area width in logical pixels. #[inline] pub fn width(&self) -> f32 { - (self.physical_width as f64 / self.scale_factor()) as f32 + (self.physical_width() as f64 / self.scale_factor()) as f32 } - /// The current logical height of the window's client area. + /// The window's client area width in logical pixels. #[inline] pub fn height(&self) -> f32 { - (self.physical_height as f64 / self.scale_factor()) as f32 - } - - /// The requested window client area width in logical pixels from window - /// creation or the last call to [`set_resolution`](Window::set_resolution). - /// - /// This may differ from the actual width depending on OS size limits and - /// the scaling factor for high DPI monitors. - #[inline] - pub fn requested_width(&self) -> f32 { - self.requested_width - } - - /// The requested window client area height in logical pixels from window - /// creation or the last call to [`set_resolution`](Window::set_resolution). - /// - /// This may differ from the actual width depending on OS size limits and - /// the scaling factor for high DPI monitors. - #[inline] - pub fn requested_height(&self) -> f32 { - self.requested_height + (self.physical_height() as f64 / self.scale_factor()) as f32 } /// The window's client area width in physical pixels. @@ -534,422 +503,147 @@ impl Window { self.physical_height } - /// The window's client resize constraint in logical pixels. - #[inline] - pub fn resize_constraints(&self) -> WindowResizeConstraints { - self.resize_constraints - } - - /// The window's client position in physical pixels. - #[inline] - pub fn position(&self) -> Option { - self.position - } - /// Set whether or not the window is maximized. - #[inline] - pub fn set_maximized(&mut self, maximized: bool) { - self.command_queue - .push(WindowCommand::SetMaximized { maximized }); - } - - /// Sets the window to minimized or back. - /// - /// # Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Wayland: Un-minimize is unsupported. - #[inline] - pub fn set_minimized(&mut self, minimized: bool) { - self.command_queue - .push(WindowCommand::SetMinimized { minimized }); - } - - /// Sets the `position` of the window on the selected `monitor` in physical pixels. - /// - /// This automatically un-maximizes the window if it's maximized. - /// - /// # Platform-specific - /// - /// - iOS: Can only be called on the main thread. Sets the top left coordinates of the window in - /// the screen space coordinate system. - /// - Web: Sets the top-left coordinates relative to the viewport. - /// - Android / Wayland: Unsupported. - #[inline] - pub fn set_position(&mut self, monitor: MonitorSelection, position: IVec2) { - self.command_queue.push(WindowCommand::SetPosition { - monitor_selection: monitor, - position, - }); - } - - /// Modifies the position of the window to be in the center of the current monitor - /// - /// # Platform-specific - /// - iOS: Can only be called on the main thread. - /// - Web / Android / Wayland: Unsupported. - #[inline] - pub fn center_window(&mut self, monitor_selection: MonitorSelection) { - self.command_queue - .push(WindowCommand::Center(monitor_selection)); - } - - /// Modifies the minimum and maximum window bounds for resizing in logical pixels. - #[inline] - pub fn set_resize_constraints(&mut self, resize_constraints: WindowResizeConstraints) { - self.command_queue - .push(WindowCommand::SetResizeConstraints { resize_constraints }); - } - - /// Request the OS to resize the window such the client area matches the specified - /// width and height. - #[allow(clippy::float_cmp)] - pub fn set_resolution(&mut self, width: f32, height: f32) { - if self.requested_width == width && self.requested_height == height { - return; - } - - self.requested_width = width; - self.requested_height = height; - self.command_queue.push(WindowCommand::SetResolution { - logical_resolution: Vec2::new(self.requested_width, self.requested_height), - scale_factor: self.scale_factor(), - }); - } - - /// Override the os-reported scaling factor. - #[allow(clippy::float_cmp)] - pub fn set_scale_factor_override(&mut self, scale_factor: Option) { - if self.scale_factor_override == scale_factor { - return; - } - - self.scale_factor_override = scale_factor; - self.command_queue.push(WindowCommand::SetScaleFactor { - scale_factor: self.scale_factor(), - }); - self.command_queue.push(WindowCommand::SetResolution { - logical_resolution: Vec2::new(self.requested_width, self.requested_height), - scale_factor: self.scale_factor(), - }); - } - - #[allow(missing_docs)] - #[inline] - pub fn update_scale_factor_from_backend(&mut self, scale_factor: f64) { - self.backend_scale_factor = scale_factor; - } - - #[allow(missing_docs)] - #[inline] - pub fn update_actual_size_from_backend(&mut self, physical_width: u32, physical_height: u32) { - self.physical_width = physical_width; - self.physical_height = physical_height; - } - - #[allow(missing_docs)] - #[inline] - pub fn update_actual_position_from_backend(&mut self, position: IVec2) { - self.position = Some(position); - } - /// The ratio of physical pixels to logical pixels /// /// `physical_pixels = logical_pixels * scale_factor` pub fn scale_factor(&self) -> f64 { self.scale_factor_override - .unwrap_or(self.backend_scale_factor) + .unwrap_or_else(|| self.base_scale_factor()) } /// The window scale factor as reported by the window backend. /// - /// This value is unaffected by [`scale_factor_override`](Window::scale_factor_override). + /// This value is unaffected by [`WindowResolution::scale_factor_override`]. #[inline] - pub fn backend_scale_factor(&self) -> f64 { - self.backend_scale_factor + pub fn base_scale_factor(&self) -> f64 { + self.scale_factor } - /// The scale factor set with [`set_scale_factor_override`](Window::set_scale_factor_override). + + /// The scale factor set with [`WindowResolution::set_scale_factor_override`]. /// /// This value may be different from the scale factor reported by the window backend. #[inline] pub fn scale_factor_override(&self) -> Option { self.scale_factor_override } - /// Get the window's title. - #[inline] - pub fn title(&self) -> &str { - &self.title - } - /// Set the window's title. - pub fn set_title(&mut self, title: String) { - self.title = title.to_string(); - self.command_queue.push(WindowCommand::SetTitle { title }); - } - - #[inline] - #[doc(alias = "vsync")] - /// Get the window's [`PresentMode`]. - pub fn present_mode(&self) -> PresentMode { - self.present_mode - } + /// Set the window's logical resolution. #[inline] - /// Get the window's [`CompositeAlphaMode`]. - pub fn alpha_mode(&self) -> CompositeAlphaMode { - self.alpha_mode + pub fn set(&mut self, width: f32, height: f32) { + self.set_physical_resolution( + (width as f64 * self.scale_factor()) as u32, + (height as f64 * self.scale_factor()) as u32, + ); } - #[inline] - #[doc(alias = "set_vsync")] - /// Set the window's [`PresentMode`]. - pub fn set_present_mode(&mut self, present_mode: PresentMode) { - self.present_mode = present_mode; - self.command_queue - .push(WindowCommand::SetPresentMode { present_mode }); - } - /// Get whether or not the window is resizable. - #[inline] - pub fn resizable(&self) -> bool { - self.resizable - } - /// Set whether or not the window is resizable. - pub fn set_resizable(&mut self, resizable: bool) { - self.resizable = resizable; - self.command_queue - .push(WindowCommand::SetResizable { resizable }); - } - /// Get whether or not decorations are enabled. - /// - /// (Decorations are the minimize, maximize, and close buttons on desktop apps) - /// - /// ## Platform-specific - /// - /// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. - #[inline] - pub fn decorations(&self) -> bool { - self.decorations - } - /// Set whether or not decorations are enabled. - /// - /// (Decorations are the minimize, maximize, and close buttons on desktop apps) - /// - /// ## Platform-specific - /// - /// **`iOS`**, **`Android`**, and the **`Web`** do not have decorations. - pub fn set_decorations(&mut self, decorations: bool) { - self.decorations = decorations; - self.command_queue - .push(WindowCommand::SetDecorations { decorations }); - } - /// Get whether or how the cursor is grabbed. - /// - /// ## Platform-specific - /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] - /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] - /// - **`iOS/Android`** don't have cursors. - /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, it's possible the value returned here is not the same as the one actually sent to winit. - #[inline] - pub fn cursor_grab_mode(&self) -> CursorGrabMode { - self.cursor_grab_mode - } - /// Set whether and how the cursor is grabbed. - /// - /// This doesn't hide the cursor. For that, use [`set_cursor_visibility`](Window::set_cursor_visibility) - /// - /// ## Platform-specific - /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] - /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] - /// - **`iOS/Android`** don't have cursors. - /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. - pub fn set_cursor_grab_mode(&mut self, grab_mode: CursorGrabMode) { - self.cursor_grab_mode = grab_mode; - self.command_queue - .push(WindowCommand::SetCursorGrabMode { grab_mode }); - } - /// Get whether or not the cursor is visible. - /// - /// ## Platform-specific - /// - /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. To stop the cursor from leaving the window, use [`set_cursor_grab_mode`](Window::set_cursor_grab_mode). - /// - **`macOS`**: The cursor is hidden only when the window is focused. - /// - **`iOS`** and **`Android`** do not have cursors - #[inline] - pub fn cursor_visible(&self) -> bool { - self.cursor_visible - } - /// Set whether or not the cursor is visible. - /// - /// ## Platform-specific + /// Set the window's physical resolution. /// - /// - **`Windows`**, **`X11`**, and **`Wayland`**: The cursor is hidden only when inside the window. To stop the cursor from leaving the window, use [`set_cursor_grab_mode`](Window::set_cursor_grab_mode). - /// - **`macOS`**: The cursor is hidden only when the window is focused. - /// - **`iOS`** and **`Android`** do not have cursors - pub fn set_cursor_visibility(&mut self, visible_mode: bool) { - self.cursor_visible = visible_mode; - self.command_queue.push(WindowCommand::SetCursorVisibility { - visible: visible_mode, - }); - } - /// Get the current [`CursorIcon`] + /// This will ignore the scale factor setting, so most of the time you should + /// prefer to use [`WindowResolution::set`]. #[inline] - pub fn cursor_icon(&self) -> CursorIcon { - self.cursor_icon - } - /// Set the [`CursorIcon`] - pub fn set_cursor_icon(&mut self, icon: CursorIcon) { - self.command_queue - .push(WindowCommand::SetCursorIcon { icon }); + pub fn set_physical_resolution(&mut self, width: u32, height: u32) { + self.physical_width = width; + self.physical_height = height; } - /// The current mouse position, in physical pixels. + /// Set the window's scale factor, this may get overriden by the backend. #[inline] - pub fn physical_cursor_position(&self) -> Option { - self.physical_cursor_position + pub fn set_scale_factor(&mut self, scale_factor: f64) { + let (width, height) = (self.width(), self.height()); + self.scale_factor = scale_factor; + self.set(width, height); } - /// The current mouse position, in logical pixels, taking into account the screen scale factor. - #[inline] - #[doc(alias = "mouse position")] - pub fn cursor_position(&self) -> Option { - self.physical_cursor_position - .map(|p| (p / self.scale_factor()).as_vec2()) - } - /// Set the cursor's position - pub fn set_cursor_position(&mut self, position: Vec2) { - self.command_queue - .push(WindowCommand::SetCursorPosition { position }); - } - /// Modifies whether the window catches cursor events. - /// - /// If true, the window will catch the cursor events. - /// If false, events are passed through the window such that any other window behind it receives them. By default hittest is enabled. - pub fn set_cursor_hittest(&mut self, hittest: bool) { - self.hittest = hittest; - self.command_queue - .push(WindowCommand::SetCursorHitTest { hittest }); - } - /// Get whether or not the hittest is active. + /// Set the window's scale factor, this will be used over what the backend decides. #[inline] - pub fn hittest(&self) -> bool { - self.hittest - } - #[allow(missing_docs)] - #[inline] - pub fn update_focused_status_from_backend(&mut self, focused: bool) { - self.focused = focused; + pub fn set_scale_factor_override(&mut self, scale_factor_override: Option) { + let (width, height) = (self.width(), self.height()); + self.scale_factor_override = scale_factor_override; + self.set(width, height); } +} - #[allow(missing_docs)] - #[inline] - pub fn update_cursor_physical_position_from_backend(&mut self, cursor_position: Option) { - self.physical_cursor_position = cursor_position; - } - /// Get the window's [`WindowMode`] - #[inline] - pub fn mode(&self) -> WindowMode { - self.mode - } - /// Set the window's [`WindowMode`] - pub fn set_mode(&mut self, mode: WindowMode) { - self.mode = mode; - self.command_queue.push(WindowCommand::SetWindowMode { - mode, - resolution: UVec2::new(self.physical_width, self.physical_height), - }); - } - /// Get whether or not the window is always on top. - #[inline] - pub fn always_on_top(&self) -> bool { - self.always_on_top +impl From<(I, I)> for WindowResolution +where + I: Into, +{ + fn from((width, height): (I, I)) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - /// Set whether of not the window is always on top. - pub fn set_always_on_top(&mut self, always_on_top: bool) { - self.always_on_top = always_on_top; - self.command_queue - .push(WindowCommand::SetAlwaysOnTop { always_on_top }); - } - /// Close the operating system window corresponding to this [`Window`]. - /// - /// This will also lead to this [`Window`] being removed from the - /// [`Windows`] resource. - /// - /// If the default [`WindowPlugin`] is used, when no windows are - /// open, the [app will exit](bevy_app::AppExit). - /// To disable this behaviour, set `exit_on_all_closed` on the [`WindowPlugin`] - /// to `false` - /// - /// [`Windows`]: crate::Windows - /// [`WindowPlugin`]: crate::WindowPlugin - pub fn close(&mut self) { - self.command_queue.push(WindowCommand::Close); - } - #[inline] - pub fn drain_commands(&mut self) -> impl Iterator + '_ { - self.command_queue.drain(..) - } - /// Get whether or not the window has focus. - /// - /// A window loses focus when the user switches to another window, and regains focus when the user uses the window again - #[inline] - pub fn is_focused(&self) -> bool { - self.focused - } - /// Get the [`RawHandleWrapper`] corresponding to this window if set. - /// - /// During normal use, this can be safely unwrapped; the value should only be [`None`] when synthetically constructed for tests. - pub fn raw_handle(&self) -> Option { - self.raw_handle.as_ref().cloned() +impl From<[I; 2]> for WindowResolution +where + I: Into, +{ + fn from([width, height]: [I; 2]) -> WindowResolution { + WindowResolution::new(width.into(), height.into()) } +} - /// The "html canvas" element selector. - /// - /// If set, this selector will be used to find a matching html canvas element, - /// rather than creating a new one. - /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). - /// - /// This value has no effect on non-web platforms. - #[inline] - pub fn canvas(&self) -> Option<&str> { - self.canvas.as_deref() +impl From for WindowResolution { + fn from(res: bevy_math::Vec2) -> WindowResolution { + WindowResolution::new(res.x, res.y) } +} - /// Whether or not to fit the canvas element's size to its parent element's size. - /// - /// **Warning**: this will not behave as expected for parents that set their size according to the size of their - /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this - /// feature, ensure the parent's size is not affected by its children. - /// - /// This value has no effect on non-web platforms. - #[inline] - pub fn fit_canvas_to_parent(&self) -> bool { - self.fit_canvas_to_parent +impl From for WindowResolution { + fn from(res: bevy_math::DVec2) -> WindowResolution { + WindowResolution::new(res.x as f32, res.y as f32) } } -/// Defines where window should be placed at on creation. -#[derive(Debug, Clone, Copy, PartialEq, Reflect, FromReflect)] +/// Defines if and how the cursor is grabbed. +/// +/// ## Platform-specific +/// +/// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] +/// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] +/// - **`iOS/Android`** don't have cursors. +/// +/// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq)] -pub enum WindowPosition { - /// The position will be set by the window manager. - Automatic, - /// Center the window on the monitor. - /// - /// The monitor to center the window on can be selected with the `monitor` field in `WindowDescriptor`. - Centered, - /// The window's top-left corner will be placed at the specified position in pixels. - /// - /// (0,0) represents top-left corner of the monitor. - /// - /// The monitor to position the window on can be selected with the `monitor` field in `WindowDescriptor`. - At(Vec2), +#[reflect(Debug, PartialEq, Default)] +pub enum CursorGrabMode { + /// The cursor can freely leave the window. + #[default] + None, + /// The cursor is confined to the window area. + Confined, + /// The cursor is locked inside the window area to a certain position. + Locked, +} + +/// Stores internal state that isn't directly accessible. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Default)] +pub struct InternalWindowState { + /// If this is true then next frame we will ask to minimize the window. + minimize_request: Option, + /// If this is true then next frame we will ask to maximize/un-maximize the window depending on `maximized`. + maximize_request: Option, +} + +impl InternalWindowState { + /// Consumes the current maximize request, if it exists. This should only be called by window backends. + pub fn take_maximize_request(&mut self) -> Option { + self.maximize_request.take() + } + + /// Consumes the current minimize request, if it exists. This should only be called by window backends. + pub fn take_minimize_request(&mut self) -> Option { + self.minimize_request.take() + } } /// Defines which monitor to use. @@ -971,129 +665,110 @@ pub enum MonitorSelection { Index(usize), } -/// Describes the information needed for creating a window. +/// Presentation mode for a window. /// -/// This should be set up before adding the [`WindowPlugin`](crate::WindowPlugin). -/// Most of these settings can also later be configured through the [`Window`](crate::Window) resource. +/// The presentation mode specifies when a frame is presented to the window. The `Fifo` +/// option corresponds to a traditional `VSync`, where the framerate is capped by the +/// display refresh rate. Both `Immediate` and `Mailbox` are low-latency and are not +/// capped by the refresh rate, but may not be available on all platforms. Tearing +/// may be observed with `Immediate` mode, but will not be observed with `Mailbox` or +/// `Fifo`. /// -/// See [`examples/window/window_settings.rs`] for usage. +/// `AutoVsync` or `AutoNoVsync` will gracefully fallback to `Fifo` when unavailable. /// -/// [`examples/window/window_settings.rs`]: https://github.com/bevyengine/bevy/blob/latest/examples/window/window_settings.rs -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +/// `Immediate` or `Mailbox` will panic if not supported by the platform. +#[repr(C)] +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect, FromReflect)] #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Default)] -pub struct WindowDescriptor { - /// The requested logical width of the window's client area. - /// - /// May vary from the physical width due to different pixel density on different monitors. - pub width: f32, - /// The requested logical height of the window's client area. - /// - /// May vary from the physical height due to different pixel density on different monitors. - pub height: f32, - /// The position on the screen that the window will be placed at. - /// - /// The monitor to place the window on can be selected with the `monitor` field. - /// - /// Ignored if `mode` is set to something other than [`WindowMode::Windowed`] - /// - /// `WindowPosition::Automatic` will be overridden with `WindowPosition::At(Vec2::ZERO)` if a specific monitor is selected. - pub position: WindowPosition, - /// The monitor to place the window on. - pub monitor: MonitorSelection, - /// Sets minimum and maximum resize limits. - pub resize_constraints: WindowResizeConstraints, - /// Overrides the window's ratio of physical pixels to logical pixels. - /// - /// If there are some scaling problems on X11 try to set this option to `Some(1.0)`. - pub scale_factor_override: Option, - /// Sets the title that displays on the window top bar, on the system task bar and other OS specific places. - /// - /// ## Platform-specific - /// - Web: Unsupported. - pub title: String, - /// Controls when a frame is presented to the screen. - #[doc(alias = "vsync")] - /// The window's [`PresentMode`]. - /// - /// Used to select whether or not VSync is used - pub present_mode: PresentMode, - /// Sets whether the window is resizable. - /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - pub resizable: bool, - /// Sets whether the window should have borders and bars. - pub decorations: bool, - /// Sets whether the cursor is visible when the window has focus. - pub cursor_visible: bool, - /// Sets whether and how the window grabs the cursor. - pub cursor_grab_mode: CursorGrabMode, - /// Sets whether or not the window listens for 'hits' of mouse activity over _this_ window. - pub hittest: bool, - /// Sets the [`WindowMode`](crate::WindowMode). - /// - /// The monitor to go fullscreen on can be selected with the `monitor` field. - pub mode: WindowMode, - /// Sets whether the background of the window should be transparent. - /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - macOS: Not working as expected. See [Bevy #6330](https://github.com/bevyengine/bevy/issues/6330). - /// - Linux (Wayland): Not working as expected. See [Bevy #5779](https://github.com/bevyengine/bevy/issues/5779). - pub transparent: bool, - /// The "html canvas" element selector. - /// - /// If set, this selector will be used to find a matching html canvas element, - /// rather than creating a new one. - /// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector). +#[reflect(Debug, PartialEq, Hash)] +#[doc(alias = "vsync")] +pub enum PresentMode { + /// Chooses FifoRelaxed -> Fifo based on availability. /// - /// This value has no effect on non-web platforms. - pub canvas: Option, - /// Whether or not to fit the canvas element's size to its parent element's size. + /// Because of the fallback behavior, it is supported everywhere. + AutoVsync = 0, + /// Chooses Immediate -> Mailbox -> Fifo (on web) based on availability. /// - /// **Warning**: this will not behave as expected for parents that set their size according to the size of their - /// children. This creates a "feedback loop" that will result in the canvas growing on each resize. When using this - /// feature, ensure the parent's size is not affected by its children. + /// Because of the fallback behavior, it is supported everywhere. + AutoNoVsync = 1, + /// The presentation engine does **not** wait for a vertical blanking period and + /// the request is presented immediately. This is a low-latency presentation mode, + /// but visible tearing may be observed. Not optimal for mobile. /// - /// This value has no effect on non-web platforms. - pub fit_canvas_to_parent: bool, - /// Specifies how the alpha channel of the textures should be handled during compositing. - pub alpha_mode: CompositeAlphaMode, - /// Sets the window to always be on top of other windows. + /// Selecting this variant will panic if not supported, it is preferred to use + /// [`PresentMode::AutoNoVsync`]. + Immediate = 2, + /// The presentation engine waits for the next vertical blanking period to update + /// the current image, but frames may be submitted without delay. This is a low-latency + /// presentation mode and visible tearing will **not** be observed. Not optimal for mobile. /// - /// ## Platform-specific - /// - iOS / Android / Web: Unsupported. - /// - Linux (Wayland): Unsupported. - pub always_on_top: bool, + /// Selecting this variant will panic if not supported, it is preferred to use + /// [`PresentMode::AutoNoVsync`]. + Mailbox = 3, + /// The presentation engine waits for the next vertical blanking period to update + /// the current image. The framerate will be capped at the display refresh rate, + /// corresponding to the `VSync`. Tearing cannot be observed. Optimal for mobile. + #[default] + Fifo = 4, // NOTE: The explicit ordinal values mirror wgpu. } -impl Default for WindowDescriptor { - fn default() -> Self { - WindowDescriptor { - title: "app".to_string(), - width: 1280., - height: 720., - position: WindowPosition::Automatic, - monitor: MonitorSelection::Current, - resize_constraints: WindowResizeConstraints::default(), - scale_factor_override: None, - present_mode: PresentMode::Fifo, - resizable: true, - decorations: true, - cursor_grab_mode: CursorGrabMode::None, - cursor_visible: true, - hittest: true, - mode: WindowMode::Windowed, - transparent: false, - canvas: None, - fit_canvas_to_parent: false, - alpha_mode: CompositeAlphaMode::Auto, - always_on_top: false, - } - } +/// Specifies how the alpha channel of the textures should be handled during compositing. +#[repr(C)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq, Hash)] +pub enum CompositeAlphaMode { + /// Chooses either `Opaque` or `Inherit` automatically, depending on the + /// `alpha_mode` that the current surface can support. + #[default] + Auto = 0, + /// The alpha channel, if it exists, of the textures is ignored in the + /// compositing process. Instead, the textures is treated as if it has a + /// constant alpha of 1.0. + Opaque = 1, + /// The alpha channel, if it exists, of the textures is respected in the + /// compositing process. The non-alpha channels of the textures are + /// expected to already be multiplied by the alpha channel by the + /// application. + PreMultiplied = 2, + /// The alpha channel, if it exists, of the textures is respected in the + /// compositing process. The non-alpha channels of the textures are not + /// expected to already be multiplied by the alpha channel by the + /// application; instead, the compositor will multiply the non-alpha + /// channels of the texture by the alpha channel during compositing. + PostMultiplied = 3, + /// The alpha channel, if it exists, of the textures is unknown for processing + /// during compositing. Instead, the application is responsible for setting + /// the composite alpha blending mode using native WSI command. If not set, + /// then a platform-specific default will be used. + Inherit = 4, +} + +/// Defines the way a window is displayed +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Reflect, FromReflect)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[reflect(Debug, PartialEq)] +pub enum WindowMode { + /// Creates a window that uses the given size. + #[default] + Windowed, + /// Creates a borderless window that uses the full size of the screen. + BorderlessFullscreen, + /// Creates a fullscreen window that will render at desktop resolution. The app will use the closest supported size + /// from the given size and scale it to fit the screen. + SizedFullscreen, + /// Creates a fullscreen window that uses the maximum supported size. + Fullscreen, } diff --git a/crates/bevy_window/src/windows.rs b/crates/bevy_window/src/windows.rs deleted file mode 100644 index 5ffbf524c1423..0000000000000 --- a/crates/bevy_window/src/windows.rs +++ /dev/null @@ -1,88 +0,0 @@ -use super::{Window, WindowId}; -use bevy_ecs::prelude::Resource; -use bevy_utils::HashMap; - -/// A collection of [`Window`]s with unique [`WindowId`]s. -#[derive(Debug, Default, Resource)] -pub struct Windows { - windows: HashMap, -} - -impl Windows { - /// Add the provided [`Window`] to the [`Windows`] resource. - pub fn add(&mut self, window: Window) { - self.windows.insert(window.id(), window); - } - - /// Get a reference to the [`Window`] of `id`. - pub fn get(&self, id: WindowId) -> Option<&Window> { - self.windows.get(&id) - } - - /// Get a mutable reference to the provided [`WindowId`]. - pub fn get_mut(&mut self, id: WindowId) -> Option<&mut Window> { - self.windows.get_mut(&id) - } - - /// Get a reference to the primary [`Window`]. - pub fn get_primary(&self) -> Option<&Window> { - self.get(WindowId::primary()) - } - - /// Get a reference to the primary [`Window`]. - /// - /// # Panics - /// - /// Panics if the primary window does not exist in [`Windows`]. - pub fn primary(&self) -> &Window { - self.get_primary().expect("Primary window does not exist") - } - - /// Get a mutable reference to the primary [`Window`]. - pub fn get_primary_mut(&mut self) -> Option<&mut Window> { - self.get_mut(WindowId::primary()) - } - - /// Get a mutable reference to the primary [`Window`]. - /// - /// # Panics - /// - /// Panics if the primary window does not exist in [`Windows`]. - pub fn primary_mut(&mut self) -> &mut Window { - self.get_primary_mut() - .expect("Primary window does not exist") - } - - /// Get a reference to the focused [`Window`]. - pub fn get_focused(&self) -> Option<&Window> { - self.windows.values().find(|window| window.is_focused()) - } - - /// Get a mutable reference to the focused [`Window`]. - pub fn get_focused_mut(&mut self) -> Option<&mut Window> { - self.windows.values_mut().find(|window| window.is_focused()) - } - - /// Returns the scale factor for the [`Window`] of `id`, or `1.0` if the window does not exist. - pub fn scale_factor(&self, id: WindowId) -> f64 { - if let Some(window) = self.get(id) { - window.scale_factor() - } else { - 1.0 - } - } - - /// An iterator over all registered [`Window`]s. - pub fn iter(&self) -> impl Iterator { - self.windows.values() - } - - /// A mutable iterator over all registered [`Window`]s. - pub fn iter_mut(&mut self) -> impl Iterator { - self.windows.values_mut() - } - - pub fn remove(&mut self, id: WindowId) -> Option { - self.windows.remove(&id) - } -} diff --git a/crates/bevy_winit/src/converters.rs b/crates/bevy_winit/src/converters.rs index 6f2f23e785761..2e6d0cf7ce3dc 100644 --- a/crates/bevy_winit/src/converters.rs +++ b/crates/bevy_winit/src/converters.rs @@ -33,7 +33,7 @@ pub fn convert_mouse_button(mouse_button: winit::event::MouseButton) -> MouseBut pub fn convert_touch_input( touch_input: winit::event::Touch, - location: winit::dpi::LogicalPosition, + location: winit::dpi::LogicalPosition, ) -> TouchInput { TouchInput { phase: match touch_input.phase { @@ -42,7 +42,7 @@ pub fn convert_touch_input( winit::event::TouchPhase::Ended => TouchPhase::Ended, winit::event::TouchPhase::Cancelled => TouchPhase::Cancelled, }, - position: Vec2::new(location.x, location.y), + position: Vec2::new(location.x as f32, location.y as f32), force: touch_input.force.map(|f| match f { winit::event::Force::Calibrated { force, diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 749d256882500..84303caaa1c99 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,266 +1,112 @@ mod converters; +mod system; #[cfg(target_arch = "wasm32")] mod web_resize; mod winit_config; mod winit_windows; -use winit::window::CursorGrabMode; +use bevy_ecs::system::{SystemParam, SystemState}; +use system::{changed_window, create_window, despawn_window}; + pub use winit_config::*; pub use winit_windows::*; use bevy_app::{App, AppExit, CoreStage, Plugin}; +use bevy_ecs::event::{Events, ManualEventReader}; use bevy_ecs::prelude::*; -use bevy_ecs::{ - event::{Events, ManualEventReader}, - world::World, +use bevy_input::{ + keyboard::KeyboardInput, + mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, + touch::TouchInput, }; -use bevy_input::mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}; -use bevy_math::{ivec2, DVec2, UVec2, Vec2}; +use bevy_math::{ivec2, DVec2, Vec2}; use bevy_utils::{ - tracing::{error, info, trace, warn}, + tracing::{trace, warn}, Instant, }; use bevy_window::{ - CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, - ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, - WindowClosed, WindowCreated, WindowFocused, WindowMoved, WindowResized, - WindowScaleFactorChanged, Windows, + CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, ReceivedCharacter, + RequestRedraw, Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, + WindowFocused, WindowMoved, WindowResized, WindowScaleFactorChanged, }; use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition}, event::{self, DeviceEvent, Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget}, }; +use crate::system::WinitWindowInfo; +#[cfg(target_arch = "wasm32")] +use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin}; + #[derive(Default)] pub struct WinitPlugin; impl Plugin for WinitPlugin { fn build(&self, app: &mut App) { + let event_loop = EventLoop::new(); + app.insert_non_send_resource(event_loop); + app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) - .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); - #[cfg(target_arch = "wasm32")] - app.add_plugin(web_resize::CanvasParentResizePlugin); - let event_loop = EventLoop::new(); - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] - let mut create_window_reader = WinitCreateWindowReader::default(); - #[cfg(any(target_os = "android", target_os = "ios", target_os = "macos"))] - let create_window_reader = WinitCreateWindowReader::default(); - // Note that we create a window here "early" because WASM/WebGL requires the window to exist prior to initializing - // the renderer. - // And for ios and macos, we should not create window early, all ui related code should be executed inside - // UIApplicationMain/NSApplicationMain. - #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] - handle_create_window_events(&mut app.world, &event_loop, &mut create_window_reader.0); - app.insert_resource(create_window_reader) - .insert_non_send_resource(event_loop); - } -} - -fn change_window( - mut winit_windows: NonSendMut, - mut windows: ResMut, - mut window_dpi_changed_events: EventWriter, - mut window_close_events: EventWriter, -) { - let mut removed_windows = vec![]; - for bevy_window in windows.iter_mut() { - let id = bevy_window.id(); - for command in bevy_window.drain_commands() { - match command { - bevy_window::WindowCommand::SetWindowMode { - mode, - resolution: - UVec2 { - x: width, - y: height, - }, - } => { - let window = winit_windows.get_window(id).unwrap(); - match mode { - bevy_window::WindowMode::BorderlessFullscreen => { - window - .set_fullscreen(Some(winit::window::Fullscreen::Borderless(None))); - } - bevy_window::WindowMode::Fullscreen => { - window.set_fullscreen(Some(winit::window::Fullscreen::Exclusive( - get_best_videomode(&window.current_monitor().unwrap()), - ))); - } - bevy_window::WindowMode::SizedFullscreen => window.set_fullscreen(Some( - winit::window::Fullscreen::Exclusive(get_fitting_videomode( - &window.current_monitor().unwrap(), - width, - height, - )), - )), - bevy_window::WindowMode::Windowed => window.set_fullscreen(None), - } - } - bevy_window::WindowCommand::SetTitle { title } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_title(&title); - } - bevy_window::WindowCommand::SetScaleFactor { scale_factor } => { - window_dpi_changed_events.send(WindowScaleFactorChanged { id, scale_factor }); - } - bevy_window::WindowCommand::SetResolution { - logical_resolution: - Vec2 { - x: width, - y: height, - }, - scale_factor, - } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_inner_size( - winit::dpi::LogicalSize::new(width, height) - .to_physical::(scale_factor), - ); - } - bevy_window::WindowCommand::SetPresentMode { .. } => (), - bevy_window::WindowCommand::SetResizable { resizable } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_resizable(resizable); - } - bevy_window::WindowCommand::SetDecorations { decorations } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_decorations(decorations); - } - bevy_window::WindowCommand::SetCursorIcon { icon } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_icon(converters::convert_cursor_icon(icon)); - } - bevy_window::WindowCommand::SetCursorGrabMode { grab_mode } => { - let window = winit_windows.get_window(id).unwrap(); - match grab_mode { - bevy_window::CursorGrabMode::None => { - window.set_cursor_grab(CursorGrabMode::None) - } - bevy_window::CursorGrabMode::Confined => window - .set_cursor_grab(CursorGrabMode::Confined) - .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Locked)), - bevy_window::CursorGrabMode::Locked => window - .set_cursor_grab(CursorGrabMode::Locked) - .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Confined)), - } - .unwrap_or_else(|e| error!("Unable to un/grab cursor: {}", e)); - } - bevy_window::WindowCommand::SetCursorVisibility { visible } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_visible(visible); - } - bevy_window::WindowCommand::SetCursorPosition { position } => { - let window = winit_windows.get_window(id).unwrap(); - let inner_size = window.inner_size().to_logical::(window.scale_factor()); - window - .set_cursor_position(LogicalPosition::new( - position.x, - inner_size.height - position.y, - )) - .unwrap_or_else(|e| error!("Unable to set cursor position: {}", e)); - } - bevy_window::WindowCommand::SetMaximized { maximized } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_maximized(maximized); - } - bevy_window::WindowCommand::SetMinimized { minimized } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_minimized(minimized); - } - bevy_window::WindowCommand::SetPosition { - monitor_selection, - position, - } => { - let window = winit_windows.get_window(id).unwrap(); - - use bevy_window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => window.current_monitor(), - Primary => window.primary_monitor(), - Index(i) => window.available_monitors().nth(i), - }; - if let Some(monitor) = maybe_monitor { - let monitor_position = DVec2::from(<(_, _)>::from(monitor.position())); - let position = monitor_position + position.as_dvec2(); + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .label(ModifiesWindows) + .with_system(changed_window) + .with_system(despawn_window), + ); - window.set_outer_position(LogicalPosition::new(position.x, position.y)); - } else { - warn!("Couldn't get monitor selected with: {monitor_selection:?}"); - } - } - bevy_window::WindowCommand::Center(monitor_selection) => { - let window = winit_windows.get_window(id).unwrap(); - - use bevy_window::MonitorSelection::*; - let maybe_monitor = match monitor_selection { - Current => window.current_monitor(), - Primary => window.primary_monitor(), - Index(i) => window.available_monitors().nth(i), - }; - - if let Some(monitor) = maybe_monitor { - let monitor_size = monitor.size(); - let monitor_position = monitor.position().cast::(); + #[cfg(target_arch = "wasm32")] + app.add_plugin(CanvasParentResizePlugin); - let window_size = window.outer_size(); + #[cfg(not(target_arch = "wasm32"))] + let mut create_window_system_state: SystemState<( + Commands, + NonSendMut>, + Query<(Entity, &mut Window)>, + EventWriter, + NonSendMut, + )> = SystemState::from_world(&mut app.world); - window.set_outer_position(PhysicalPosition { - x: monitor_size.width.saturating_sub(window_size.width) as f64 / 2. - + monitor_position.x, - y: monitor_size.height.saturating_sub(window_size.height) as f64 / 2. - + monitor_position.y, - }); - } else { - warn!("Couldn't get monitor selected with: {monitor_selection:?}"); - } - } - bevy_window::WindowCommand::SetResizeConstraints { resize_constraints } => { - let window = winit_windows.get_window(id).unwrap(); - let constraints = resize_constraints.check_constraints(); - let min_inner_size = LogicalSize { - width: constraints.min_width, - height: constraints.min_height, - }; - let max_inner_size = LogicalSize { - width: constraints.max_width, - height: constraints.max_height, - }; + #[cfg(target_arch = "wasm32")] + let mut create_window_system_state: SystemState<( + Commands, + NonSendMut>, + Query<(Entity, &mut Window)>, + EventWriter, + NonSendMut, + ResMut, + )> = SystemState::from_world(&mut app.world); - window.set_min_inner_size(Some(min_inner_size)); - if constraints.max_width.is_finite() && constraints.max_height.is_finite() { - window.set_max_inner_size(Some(max_inner_size)); - } - } - bevy_window::WindowCommand::SetAlwaysOnTop { always_on_top } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_always_on_top(always_on_top); - } - bevy_window::WindowCommand::SetCursorHitTest { hittest } => { - let window = winit_windows.get_window(id).unwrap(); - window.set_cursor_hittest(hittest).unwrap(); - } - bevy_window::WindowCommand::Close => { - // Since we have borrowed `windows` to iterate through them, we can't remove the window from it. - // Add the removal requests to a queue to solve this - removed_windows.push(id); - // No need to run any further commands - this drops the rest of the commands, although the `bevy_window::Window` will be dropped later anyway - break; - } - } - } - } - if !removed_windows.is_empty() { - for id in removed_windows { - // Close the OS window. (The `Drop` impl actually closes the window) - let _ = winit_windows.remove_window(id); - // Clean up our own data structures - windows.remove(id); - window_close_events.send(WindowClosed { id }); + // And for ios and macos, we should not create window early, all ui related code should be executed inside + // UIApplicationMain/NSApplicationMain. + #[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))] + { + #[cfg(not(target_arch = "wasm32"))] + let (commands, event_loop, mut new_windows, event_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let (commands, event_loop, mut new_windows, event_writer, winit_windows, event_channel) = + create_window_system_state.get_mut(&mut app.world); + + // Here we need to create a winit-window and give it a WindowHandle which the renderer can use. + // It needs to be spawned before the start of the startup-stage, so we cannot use a regular system. + // Instead we need to create the window and spawn it using direct world access + create_window( + commands, + &event_loop, + new_windows.iter_mut(), + event_writer, + winit_windows, + #[cfg(target_arch = "wasm32")] + event_channel, + ); } + + create_window_system_state.apply(&mut app.world); } } @@ -307,8 +153,30 @@ where panic!("Run return is not supported on this platform!") } -pub fn winit_runner(app: App) { - winit_runner_with(app); +#[derive(SystemParam)] +struct WindowEvents<'w> { + window_resized: EventWriter<'w, WindowResized>, + window_close_requested: EventWriter<'w, WindowCloseRequested>, + window_scale_factor_changed: EventWriter<'w, WindowScaleFactorChanged>, + window_backend_scale_factor_changed: EventWriter<'w, WindowBackendScaleFactorChanged>, + window_focused: EventWriter<'w, WindowFocused>, + window_moved: EventWriter<'w, WindowMoved>, +} + +#[derive(SystemParam)] +struct InputEvents<'w> { + keyboard_input: EventWriter<'w, KeyboardInput>, + character_input: EventWriter<'w, ReceivedCharacter>, + mouse_button_input: EventWriter<'w, MouseButtonInput>, + mouse_wheel_input: EventWriter<'w, MouseWheel>, + touch_input: EventWriter<'w, TouchInput>, +} + +#[derive(SystemParam)] +struct CursorEvents<'w> { + cursor_moved: EventWriter<'w, CursorMoved>, + cursor_entered: EventWriter<'w, CursorEntered>, + cursor_left: EventWriter<'w, CursorLeft>, } // #[cfg(any( @@ -347,19 +215,13 @@ impl Default for WinitPersistentState { } } -#[derive(Default, Resource)] -struct WinitCreateWindowReader(ManualEventReader); - -pub fn winit_runner_with(mut app: App) { +pub fn winit_runner(mut app: App) { + // We remove this so that we have ownership over it. let mut event_loop = app .world .remove_non_send_resource::>() .unwrap(); - let mut create_window_event_reader = app - .world - .remove_resource::() - .unwrap() - .0; + let mut app_exit_event_reader = ManualEventReader::::default(); let mut redraw_event_reader = ManualEventReader::::default(); let mut winit_state = WinitPersistentState::default(); @@ -370,23 +232,80 @@ pub fn winit_runner_with(mut app: App) { trace!("Entering winit event loop"); + let mut focused_window_state: SystemState<(Res, Query<&Window>)> = + SystemState::from_world(&mut app.world); + + #[cfg(not(target_arch = "wasm32"))] + let mut create_window_system_state: SystemState<( + Commands, + Query<(Entity, &mut Window), Added>, + EventWriter, + NonSendMut, + )> = SystemState::from_world(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let mut create_window_system_state: SystemState<( + Commands, + Query<(Entity, &mut Window), Added>, + EventWriter, + NonSendMut, + ResMut, + )> = SystemState::from_world(&mut app.world); + let event_handler = move |event: Event<()>, event_loop: &EventLoopWindowTarget<()>, control_flow: &mut ControlFlow| { #[cfg(feature = "trace")] let _span = bevy_utils::tracing::info_span!("winit event_handler").entered(); + + if let Some(app_exit_events) = app.world.get_resource::>() { + if app_exit_event_reader.iter(app_exit_events).last().is_some() { + *control_flow = ControlFlow::Exit; + return; + } + } + + { + #[cfg(not(target_arch = "wasm32"))] + let (commands, mut new_windows, created_window_writer, winit_windows) = + create_window_system_state.get_mut(&mut app.world); + + #[cfg(target_arch = "wasm32")] + let ( + commands, + mut new_windows, + created_window_writer, + winit_windows, + canvas_parent_resize_channel, + ) = create_window_system_state.get_mut(&mut app.world); + + // Responsible for creating new windows + create_window( + commands, + event_loop, + new_windows.iter_mut(), + created_window_writer, + winit_windows, + #[cfg(target_arch = "wasm32")] + canvas_parent_resize_channel, + ); + + create_window_system_state.apply(&mut app.world); + } + match event { event::Event::NewEvents(start) => { - let winit_config = app.world.resource::(); - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + + let app_focused = window_focused_query.iter().any(|window| window.focused); + // Check if either the `WaitUntil` timeout was triggered by winit, or that same // amount of time has elapsed since the last app update. This manual check is needed // because we don't know if the criteria for an app update were met until the end of // the frame. let auto_timeout_reached = matches!(start, StartCause::ResumeTimeReached { .. }); let now = Instant::now(); - let manual_timeout_reached = match winit_config.update_mode(focused) { + let manual_timeout_reached = match winit_config.update_mode(app_focused) { UpdateMode::Continuous => false, UpdateMode::Reactive { max_wait } | UpdateMode::ReactiveLowPower { max_wait } => { @@ -402,81 +321,128 @@ pub fn winit_runner_with(mut app: App) { window_id: winit_window_id, .. } => { - let world = app.world.cell(); - let winit_windows = world.non_send_resource_mut::(); - let mut windows = world.resource_mut::(); - let window_id = - if let Some(window_id) = winit_windows.get_window_id(winit_window_id) { - window_id + // Fetch and prepare details from the world + let mut system_state: SystemState<( + NonSend, + Query<(&mut Window, &mut WinitWindowInfo)>, + WindowEvents, + InputEvents, + CursorEvents, + EventWriter, + )> = SystemState::new(&mut app.world); + let ( + winit_windows, + mut window_query, + mut window_events, + mut input_events, + mut cursor_events, + mut file_drag_and_drop_events, + ) = system_state.get_mut(&mut app.world); + + // Entity of this window + let window_entity = + if let Some(entity) = winit_windows.get_window_entity(winit_window_id) { + entity } else { warn!( - "Skipped event for unknown winit Window Id {:?}", - winit_window_id + "Skipped event {:?} for unknown winit Window Id {:?}", + event, winit_window_id + ); + return; + }; + + let (mut window, mut info) = + if let Ok((window, info)) = window_query.get_mut(window_entity) { + (window, info) + } else { + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event ); return; }; - let Some(window) = windows.get_mut(window_id) else { - // If we're here, this window was previously opened - info!("Skipped event for closed window: {:?}", window_id); - return; - }; winit_state.low_power_event = true; match event { WindowEvent::Resized(size) => { - window.update_actual_size_from_backend(size.width, size.height); - world.send_event(WindowResized { - id: window_id, + window + .resolution + .set_physical_resolution(size.width, size.height); + info.last_winit_size = size; + + window_events.window_resized.send(WindowResized { + window: window_entity, width: window.width(), height: window.height(), }); } WindowEvent::CloseRequested => { - world.send_event(WindowCloseRequested { id: window_id }); + window_events + .window_close_requested + .send(WindowCloseRequested { + window: window_entity, + }); } WindowEvent::KeyboardInput { ref input, .. } => { - world.send_event(converters::convert_keyboard_input(input)); + input_events + .keyboard_input + .send(converters::convert_keyboard_input(input)); } WindowEvent::CursorMoved { position, .. } => { - let winit_window = winit_windows.get_window(window_id).unwrap(); - let inner_size = winit_window.inner_size(); - - // move origin to bottom left - let y_position = inner_size.height as f64 - position.y; + let physical_position = DVec2::new( + position.x, + // Flip the coordinate space from winit's context to our context. + window.resolution.physical_height() as f64 - position.y, + ); - let physical_position = DVec2::new(position.x, y_position); + // bypassing change detection to not trigger feedback loop with system `changed_window` + // this system change the cursor position in winit window - .update_cursor_physical_position_from_backend(Some(physical_position)); + .bypass_change_detection() + .set_physical_cursor_position(Some(physical_position)); - world.send_event(CursorMoved { - id: window_id, - position: (physical_position / window.scale_factor()).as_vec2(), + cursor_events.cursor_moved.send(CursorMoved { + window: window_entity, + position: (physical_position / window.resolution.scale_factor()) + .as_vec2(), }); } WindowEvent::CursorEntered { .. } => { - world.send_event(CursorEntered { id: window_id }); + cursor_events.cursor_entered.send(CursorEntered { + window: window_entity, + }); } WindowEvent::CursorLeft { .. } => { - window.update_cursor_physical_position_from_backend(None); - world.send_event(CursorLeft { id: window_id }); + // Component + if let Ok((mut window, _)) = window_query.get_mut(window_entity) { + // bypassing change detection to not trigger feedback loop with system `changed_window` + // this system change the cursor position in winit + window + .bypass_change_detection() + .set_physical_cursor_position(None); + } + + cursor_events.cursor_left.send(CursorLeft { + window: window_entity, + }); } WindowEvent::MouseInput { state, button, .. } => { - world.send_event(MouseButtonInput { + input_events.mouse_button_input.send(MouseButtonInput { button: converters::convert_mouse_button(button), state: converters::convert_element_state(state), }); } WindowEvent::MouseWheel { delta, .. } => match delta { event::MouseScrollDelta::LineDelta(x, y) => { - world.send_event(MouseWheel { + input_events.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Line, x, y, }); } event::MouseScrollDelta::PixelDelta(p) => { - world.send_event(MouseWheel { + input_events.mouse_wheel_input.send(MouseWheel { unit: MouseScrollUnit::Pixel, x: p.x as f32, y: p.y as f32, @@ -484,12 +450,23 @@ pub fn winit_runner_with(mut app: App) { } }, WindowEvent::Touch(touch) => { - let location = touch.location.to_logical(window.scale_factor()); - world.send_event(converters::convert_touch_input(touch, location)); + let mut location = + touch.location.to_logical(window.resolution.scale_factor()); + + // On a mobile window, the start is from the top while on PC/Linux/OSX from + // bottom + if cfg!(target_os = "android") || cfg!(target_os = "ios") { + location.y = window.height() as f64 - location.y; + } + + // Event + input_events + .touch_input + .send(converters::convert_touch_input(touch, location)); } WindowEvent::ReceivedCharacter(c) => { - world.send_event(ReceivedCharacter { - id: window_id, + input_events.character_input.send(ReceivedCharacter { + window: window_entity, char: c, }); } @@ -497,73 +474,84 @@ pub fn winit_runner_with(mut app: App) { scale_factor, new_inner_size, } => { - world.send_event(WindowBackendScaleFactorChanged { - id: window_id, - scale_factor, - }); - let prior_factor = window.scale_factor(); - window.update_scale_factor_from_backend(scale_factor); - let new_factor = window.scale_factor(); - if let Some(forced_factor) = window.scale_factor_override() { + window_events.window_backend_scale_factor_changed.send( + WindowBackendScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); + + let prior_factor = window.resolution.scale_factor(); + window.resolution.set_scale_factor(scale_factor); + let new_factor = window.resolution.scale_factor(); + + if let Some(forced_factor) = window.resolution.scale_factor_override() { // If there is a scale factor override, then force that to be used // Otherwise, use the OS suggested size // We have already told the OS about our resize constraints, so // the new_inner_size should take those into account - *new_inner_size = winit::dpi::LogicalSize::new( - window.requested_width(), - window.requested_height(), - ) - .to_physical::(forced_factor); + *new_inner_size = + winit::dpi::LogicalSize::new(window.width(), window.height()) + .to_physical::(forced_factor); + // TODO: Should this not trigger a WindowsScaleFactorChanged? } else if approx::relative_ne!(new_factor, prior_factor) { - world.send_event(WindowScaleFactorChanged { - id: window_id, - scale_factor, - }); + // Trigger a change event if they are approximately different + window_events.window_scale_factor_changed.send( + WindowScaleFactorChanged { + window: window_entity, + scale_factor, + }, + ); } - let new_logical_width = new_inner_size.width as f64 / new_factor; - let new_logical_height = new_inner_size.height as f64 / new_factor; - if approx::relative_ne!(window.width() as f64, new_logical_width) - || approx::relative_ne!(window.height() as f64, new_logical_height) + let new_logical_width = (new_inner_size.width as f64 / new_factor) as f32; + let new_logical_height = (new_inner_size.height as f64 / new_factor) as f32; + if approx::relative_ne!(window.width(), new_logical_width) + || approx::relative_ne!(window.height(), new_logical_height) { - world.send_event(WindowResized { - id: window_id, - width: new_logical_width as f32, - height: new_logical_height as f32, + window_events.window_resized.send(WindowResized { + window: window_entity, + width: new_logical_width, + height: new_logical_height, }); } - window.update_actual_size_from_backend( - new_inner_size.width, - new_inner_size.height, - ); + window + .resolution + .set_physical_resolution(new_inner_size.width, new_inner_size.height); } WindowEvent::Focused(focused) => { - window.update_focused_status_from_backend(focused); - world.send_event(WindowFocused { - id: window_id, + // Component + window.focused = focused; + + window_events.window_focused.send(WindowFocused { + window: window_entity, focused, }); } WindowEvent::DroppedFile(path_buf) => { - world.send_event(FileDragAndDrop::DroppedFile { - id: window_id, + file_drag_and_drop_events.send(FileDragAndDrop::DroppedFile { + window: window_entity, path_buf, }); } WindowEvent::HoveredFile(path_buf) => { - world.send_event(FileDragAndDrop::HoveredFile { - id: window_id, + file_drag_and_drop_events.send(FileDragAndDrop::HoveredFile { + window: window_entity, path_buf, }); } WindowEvent::HoveredFileCancelled => { - world.send_event(FileDragAndDrop::HoveredFileCancelled { id: window_id }); + file_drag_and_drop_events.send(FileDragAndDrop::HoveredFileCancelled { + window: window_entity, + }); } WindowEvent::Moved(position) => { let position = ivec2(position.x, position.y); - window.update_actual_position_from_backend(position); - world.send_event(WindowMoved { - id: window_id, + + window.position.set(position); + + window_events.window_moved.send(WindowMoved { + entity: window_entity, position, }); } @@ -574,8 +562,12 @@ pub fn winit_runner_with(mut app: App) { event: DeviceEvent::MouseMotion { delta: (x, y) }, .. } => { - app.world.send_event(MouseMotion { - delta: DVec2 { x, y }.as_vec2(), + let mut system_state: SystemState> = + SystemState::new(&mut app.world); + let mut mouse_motion = system_state.get_mut(&mut app.world); + + mouse_motion.send(MouseMotion { + delta: Vec2::new(x as f32, y as f32), }); } event::Event::Suspended => { @@ -585,16 +577,12 @@ pub fn winit_runner_with(mut app: App) { winit_state.active = true; } event::Event::MainEventsCleared => { - handle_create_window_events( - &mut app.world, - event_loop, - &mut create_window_event_reader, - ); - let winit_config = app.world.resource::(); + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + let update = if winit_state.active { - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); - match winit_config.update_mode(focused) { + // True if _any_ windows are currently being focused + let app_focused = window_focused_query.iter().any(|window| window.focused); + match winit_config.update_mode(app_focused) { UpdateMode::Continuous | UpdateMode::Reactive { .. } => true, UpdateMode::ReactiveLowPower { .. } => { winit_state.low_power_event @@ -605,6 +593,7 @@ pub fn winit_runner_with(mut app: App) { } else { false }; + if update { winit_state.last_update = Instant::now(); app.update(); @@ -612,12 +601,15 @@ pub fn winit_runner_with(mut app: App) { } Event::RedrawEventsCleared => { { - let winit_config = app.world.resource::(); - let windows = app.world.resource::(); - let focused = windows.iter().any(|w| w.is_focused()); + // Fetch from world + let (winit_config, window_focused_query) = focused_window_state.get(&app.world); + + // True if _any_ windows are currently being focused + let app_focused = window_focused_query.iter().any(|window| window.focused); + let now = Instant::now(); use UpdateMode::*; - *control_flow = match winit_config.update_mode(focused) { + *control_flow = match winit_config.update_mode(app_focused) { Continuous => ControlFlow::Poll, Reactive { max_wait } | ReactiveLowPower { max_wait } => { if let Some(instant) = now.checked_add(*max_wait) { @@ -628,6 +620,7 @@ pub fn winit_runner_with(mut app: App) { } }; } + // This block needs to run after `app.update()` in `MainEventsCleared`. Otherwise, // we won't be able to see redraw requests until the next event, defeating the // purpose of a redraw request! @@ -638,64 +631,18 @@ pub fn winit_runner_with(mut app: App) { redraw = true; } } - if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.iter(app_exit_events).last().is_some() { - *control_flow = ControlFlow::Exit; - } - } + winit_state.redraw_request_sent = redraw; } + _ => (), } }; + // If true, returns control from Winit back to the main Bevy loop if return_from_run { run_return(&mut event_loop, event_handler); } else { run(event_loop, event_handler); } } - -fn handle_create_window_events( - world: &mut World, - event_loop: &EventLoopWindowTarget<()>, - create_window_event_reader: &mut ManualEventReader, -) { - let world = world.cell(); - let mut winit_windows = world.non_send_resource_mut::(); - let mut windows = world.resource_mut::(); - let create_window_events = world.resource::>(); - for create_window_event in create_window_event_reader.iter(&create_window_events) { - let window = winit_windows.create_window( - event_loop, - create_window_event.id, - &create_window_event.descriptor, - ); - // This event is already sent on windows, x11, and xwayland. - // TODO: we aren't yet sure about native wayland, so we might be able to exclude it, - // but sending a duplicate event isn't problematic, as windows already does this. - #[cfg(not(any(target_os = "windows", target_feature = "x11")))] - world.send_event(WindowResized { - id: create_window_event.id, - width: window.width(), - height: window.height(), - }); - windows.add(window); - world.send_event(WindowCreated { - id: create_window_event.id, - }); - - #[cfg(target_arch = "wasm32")] - { - let channel = world.resource_mut::(); - if create_window_event.descriptor.fit_canvas_to_parent { - let selector = if let Some(selector) = &create_window_event.descriptor.canvas { - selector - } else { - web_resize::WINIT_CANVAS_SELECTOR - }; - channel.listen_to_selector(create_window_event.id, selector); - } - } - } -} diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs new file mode 100644 index 0000000000000..2c1fe1875cf95 --- /dev/null +++ b/crates/bevy_winit/src/system.rs @@ -0,0 +1,284 @@ +use bevy_ecs::{ + entity::Entity, + event::EventWriter, + prelude::{Changed, Component, Resource}, + system::{Commands, NonSendMut, Query, RemovedComponents}, + world::Mut, +}; +use bevy_utils::{ + tracing::{error, info, warn}, + HashMap, +}; +use bevy_window::{RawHandleWrapper, Window, WindowClosed, WindowCreated}; +use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + +use winit::{ + dpi::{LogicalSize, PhysicalPosition, PhysicalSize}, + event_loop::EventLoopWindowTarget, +}; + +#[cfg(target_arch = "wasm32")] +use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; +use crate::{converters, get_best_videomode, get_fitting_videomode, WinitWindows}; +#[cfg(target_arch = "wasm32")] +use bevy_ecs::system::ResMut; + +/// System responsible for creating new windows whenever a `Window` component is added +/// to an entity. +/// +/// This will default any necessary components if they are not already added. +pub(crate) fn create_window<'a>( + mut commands: Commands, + event_loop: &EventLoopWindowTarget<()>, + created_windows: impl Iterator)>, + mut event_writer: EventWriter, + mut winit_windows: NonSendMut, + #[cfg(target_arch = "wasm32")] event_channel: ResMut, +) { + for (entity, mut component) in created_windows { + if winit_windows.get_window(entity).is_some() { + continue; + } + + info!( + "Creating new window {:?} ({:?})", + component.title.as_str(), + entity + ); + + let winit_window = winit_windows.create_window(event_loop, entity, &component); + let current_size = winit_window.inner_size(); + component + .resolution + .set_scale_factor(winit_window.scale_factor()); + commands + .entity(entity) + .insert(RawHandleWrapper { + window_handle: winit_window.raw_window_handle(), + display_handle: winit_window.raw_display_handle(), + }) + .insert(WinitWindowInfo { + previous: component.clone(), + last_winit_size: PhysicalSize { + width: current_size.width, + height: current_size.height, + }, + }); + + #[cfg(target_arch = "wasm32")] + { + if component.fit_canvas_to_parent { + let selector = if let Some(selector) = &component.canvas { + selector + } else { + WINIT_CANVAS_SELECTOR + }; + event_channel.listen_to_selector(entity, selector); + } + } + + event_writer.send(WindowCreated { window: entity }); + } +} + +/// Cache for closing windows so we can get better debug information. +#[derive(Debug, Clone, Resource)] +pub struct WindowTitleCache(HashMap); + +pub(crate) fn despawn_window( + closed: RemovedComponents, + mut close_events: EventWriter, + mut winit_windows: NonSendMut, +) { + for window in closed.iter() { + info!("Closing window {:?}", window); + + winit_windows.remove_window(window); + close_events.send(WindowClosed { window }); + } +} + +/// Previous state of the window so we can check sub-portions of what actually was changed. +#[derive(Debug, Clone, Component)] +pub struct WinitWindowInfo { + pub previous: Window, + pub last_winit_size: PhysicalSize, +} + +// Detect changes to the window and update the winit window accordingly. +// +// Notes: +// - [`Window::present_mode`] and [`Window::composite_alpha_mode`] updating should be handled in the bevy render crate. +// - [`Window::transparent`] currently cannot be updated after startup for winit. +// - [`Window::canvas`] currently cannot be updated after startup, not entirely sure if it would work well with the +// event channel stuff. +pub(crate) fn changed_window( + mut changed_windows: Query<(Entity, &mut Window, &mut WinitWindowInfo), Changed>, + winit_windows: NonSendMut, +) { + for (entity, mut window, mut info) in &mut changed_windows { + let previous = &info.previous; + + if let Some(winit_window) = winit_windows.get_window(entity) { + if window.title != previous.title { + winit_window.set_title(window.title.as_str()); + } + + if window.mode != previous.mode { + let new_mode = match window.mode { + bevy_window::WindowMode::BorderlessFullscreen => { + Some(winit::window::Fullscreen::Borderless(None)) + } + bevy_window::WindowMode::Fullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_best_videomode( + &winit_window.current_monitor().unwrap(), + ))) + } + bevy_window::WindowMode::SizedFullscreen => { + Some(winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &winit_window.current_monitor().unwrap(), + window.width() as u32, + window.height() as u32, + ))) + } + bevy_window::WindowMode::Windowed => None, + }; + + if winit_window.fullscreen() != new_mode { + winit_window.set_fullscreen(new_mode); + } + } + if window.resolution != previous.resolution { + let physical_size = PhysicalSize::new( + window.resolution.physical_width(), + window.resolution.physical_height(), + ); + // Prevents "window.resolution values set from a winit resize event" from + // being set here, creating feedback loops. + if physical_size != info.last_winit_size { + winit_window.set_inner_size(physical_size); + } + } + + if window.physical_cursor_position() != previous.physical_cursor_position() { + if let Some(physical_position) = window.physical_cursor_position() { + let inner_size = winit_window.inner_size(); + + let position = PhysicalPosition::new( + physical_position.x, + // Flip the coordinate space back to winit's context. + inner_size.height as f32 - physical_position.y, + ); + + if let Err(err) = winit_window.set_cursor_position(position) { + error!("could not set cursor position: {:?}", err); + } + } + } + + if window.cursor.icon != previous.cursor.icon { + winit_window.set_cursor_icon(converters::convert_cursor_icon(window.cursor.icon)); + } + + if window.cursor.grab_mode != previous.cursor.grab_mode { + crate::winit_windows::attempt_grab(winit_window, window.cursor.grab_mode); + } + + if window.cursor.visible != previous.cursor.visible { + winit_window.set_cursor_visible(window.cursor.visible); + } + + if window.cursor.hit_test != previous.cursor.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(window.cursor.hit_test) { + window.cursor.hit_test = previous.cursor.hit_test; + warn!( + "Could not set cursor hit test for window {:?}: {:?}", + window.title, err + ); + } + } + + if window.decorations != previous.decorations + && window.decorations != winit_window.is_decorated() + { + winit_window.set_decorations(window.decorations); + } + + if window.resizable != previous.resizable + && window.resizable != winit_window.is_resizable() + { + winit_window.set_resizable(window.resizable); + } + + if window.resize_constraints != previous.resize_constraints { + let constraints = window.resize_constraints.check_constraints(); + let min_inner_size = LogicalSize { + width: constraints.min_width, + height: constraints.min_height, + }; + let max_inner_size = LogicalSize { + width: constraints.max_width, + height: constraints.max_height, + }; + + winit_window.set_min_inner_size(Some(min_inner_size)); + if constraints.max_width.is_finite() && constraints.max_height.is_finite() { + winit_window.set_max_inner_size(Some(max_inner_size)); + } + } + + if window.position != previous.position { + if let Some(position) = crate::winit_window_position( + &window.position, + &window.resolution, + winit_window.available_monitors(), + winit_window.primary_monitor(), + winit_window.current_monitor(), + ) { + let should_set = match winit_window.outer_position() { + Ok(current_position) => current_position != position, + _ => true, + }; + + if should_set { + winit_window.set_outer_position(position); + } + } + } + + if let Some(maximized) = window.internal.take_maximize_request() { + winit_window.set_maximized(maximized); + } + + if let Some(minimized) = window.internal.take_minimize_request() { + winit_window.set_minimized(minimized); + } + + if window.focused != previous.focused && window.focused { + winit_window.focus_window(); + } + + if window.always_on_top != previous.always_on_top { + winit_window.set_always_on_top(window.always_on_top); + } + + // Currently unsupported changes + if window.transparent != previous.transparent { + window.transparent = previous.transparent; + warn!( + "Winit does not currently support updating transparency after window creation." + ); + } + + #[cfg(target_arch = "wasm32")] + if window.canvas != previous.canvas { + window.canvas = previous.canvas.clone(); + warn!( + "Bevy currently doesn't support modifying the window canvas after initialization." + ); + } + + info.previous = window.clone(); + } + } +} diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index 7e289afdfc675..52b55a83d6d26 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -1,7 +1,6 @@ use crate::WinitWindows; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::WindowId; use crossbeam_channel::{Receiver, Sender}; use wasm_bindgen::JsCast; use winit::dpi::LogicalSize; @@ -17,7 +16,7 @@ impl Plugin for CanvasParentResizePlugin { struct ResizeEvent { size: LogicalSize, - window_id: WindowId, + window: Entity, } #[derive(Resource)] @@ -31,7 +30,7 @@ fn canvas_parent_resize_event_handler( resize_events: Res, ) { for event in resize_events.receiver.try_iter() { - if let Some(window) = winit_windows.get_window(event.window_id) { + if let Some(window) = winit_windows.get_window(event.window) { window.set_inner_size(event.size); } } @@ -59,12 +58,12 @@ impl Default for CanvasParentResizeEventChannel { } impl CanvasParentResizeEventChannel { - pub(crate) fn listen_to_selector(&self, window_id: WindowId, selector: &str) { + pub(crate) fn listen_to_selector(&self, window: Entity, selector: &str) { let sender = self.sender.clone(); let owned_selector = selector.to_string(); let resize = move || { if let Some(size) = get_size(&owned_selector) { - sender.send(ResizeEvent { size, window_id }).unwrap(); + sender.send(ResizeEvent { size, window }).unwrap(); } }; diff --git a/crates/bevy_winit/src/winit_config.rs b/crates/bevy_winit/src/winit_config.rs index c1db2b2fd27e6..40bfd1f87323b 100644 --- a/crates/bevy_winit/src/winit_config.rs +++ b/crates/bevy_winit/src/winit_config.rs @@ -4,15 +4,28 @@ use bevy_utils::Duration; /// A resource for configuring usage of the `rust_winit` library. #[derive(Debug, Resource)] pub struct WinitSettings { - /// Configures the winit library to return control to the main thread after the - /// [run](bevy_app::App::run) loop is exited. Winit strongly recommends avoiding this when - /// possible. Before using this please read and understand the - /// [caveats](winit::platform::run_return::EventLoopExtRunReturn::run_return) in the winit - /// documentation. + /// Configures `winit` to return control to the caller after exiting the + /// event loop, enabling [`App::run()`](bevy_app::App::run()) to return. /// - /// This feature is only available on desktop `target_os` configurations. Namely `windows`, - /// `macos`, `linux`, `dragonfly`, `freebsd`, `netbsd`, and `openbsd`. If set to true on an - /// unsupported platform [run](bevy_app::App::run) will panic. + /// By default, [`return_from_run`](Self::return_from_run) is `false` and *Bevy* + /// will use `winit`'s + /// [`EventLoop::run()`](https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run) + /// to initiate the event loop. + /// [`EventLoop::run()`](https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run) + /// will never return but will terminate the process after the event loop exits. + /// + /// Setting [`return_from_run`](Self::return_from_run) to `true` will cause *Bevy* + /// to use `winit`'s + /// [`EventLoopExtRunReturn::run_return()`](https://docs.rs/winit/latest/winit/platform/run_return/trait.EventLoopExtRunReturn.html#tymethod.run_return) + /// instead which is strongly discouraged by the `winit` authors. + /// + /// # Supported platforms + /// + /// This feature is only available on the following desktop `target_os` configurations: + /// `windows`, `macos`, `linux`, `dragonfly`, `freebsd`, `netbsd`, and `openbsd`. + /// + /// Setting [`return_from_run`](Self::return_from_run) to `true` on + /// unsupported platforms will cause [`App::run()`](bevy_app::App::run()) to panic! pub return_from_run: bool, /// Configures how the winit event loop updates while the window is focused. pub focused_mode: UpdateMode, diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 38ff59d348daa..2f7dda6702f7b 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,20 +1,18 @@ -use bevy_math::{DVec2, IVec2}; -use bevy_utils::HashMap; -use bevy_window::{ - CursorGrabMode, MonitorSelection, RawHandleWrapper, Window, WindowDescriptor, WindowId, - WindowMode, -}; -use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; +use bevy_ecs::entity::Entity; + +use bevy_utils::{tracing::warn, HashMap}; +use bevy_window::{CursorGrabMode, Window, WindowMode, WindowPosition, WindowResolution}; + use winit::{ - dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, - window::Fullscreen, + dpi::{LogicalSize, PhysicalPosition}, + monitor::MonitorHandle, }; #[derive(Debug, Default)] pub struct WinitWindows { pub windows: HashMap, - pub window_id_to_winit: HashMap, - pub winit_to_window_id: HashMap, + pub entity_to_winit: HashMap, + pub winit_to_entity: HashMap, // Some winit functions, such as `set_window_icon` can only be used from the main thread. If // they are used in another thread, the app will hang. This marker ensures `WinitWindows` is // only ever accessed with bevy's non-send functions and in NonSend systems. @@ -25,45 +23,40 @@ impl WinitWindows { pub fn create_window( &mut self, event_loop: &winit::event_loop::EventLoopWindowTarget<()>, - window_id: WindowId, - window_descriptor: &WindowDescriptor, - ) -> Window { + entity: Entity, + window: &Window, + ) -> &winit::window::Window { let mut winit_window_builder = winit::window::WindowBuilder::new(); - let &WindowDescriptor { - width, - height, - position, - monitor, - scale_factor_override, - .. - } = window_descriptor; - - let logical_size = LogicalSize::new(width, height); - - let monitor = match monitor { - MonitorSelection::Current => None, - MonitorSelection::Primary => event_loop.primary_monitor(), - MonitorSelection::Index(i) => event_loop.available_monitors().nth(i), - }; - - let selected_or_primary_monitor = monitor.clone().or_else(|| event_loop.primary_monitor()); - - winit_window_builder = match window_descriptor.mode { - WindowMode::BorderlessFullscreen => winit_window_builder - .with_fullscreen(Some(Fullscreen::Borderless(selected_or_primary_monitor))), - WindowMode::Fullscreen => winit_window_builder.with_fullscreen(Some( - Fullscreen::Exclusive(get_best_videomode(&selected_or_primary_monitor.unwrap())), + winit_window_builder = match window.mode { + WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some( + winit::window::Fullscreen::Borderless(event_loop.primary_monitor()), )), + WindowMode::Fullscreen => { + winit_window_builder.with_fullscreen(Some(winit::window::Fullscreen::Exclusive( + get_best_videomode(&event_loop.primary_monitor().unwrap()), + ))) + } WindowMode::SizedFullscreen => winit_window_builder.with_fullscreen(Some( - Fullscreen::Exclusive(get_fitting_videomode( - &selected_or_primary_monitor.unwrap(), - window_descriptor.width as u32, - window_descriptor.height as u32, + winit::window::Fullscreen::Exclusive(get_fitting_videomode( + &event_loop.primary_monitor().unwrap(), + window.width() as u32, + window.height() as u32, )), )), WindowMode::Windowed => { - if let Some(sf) = scale_factor_override { + if let Some(position) = winit_window_position( + &window.position, + &window.resolution, + event_loop.available_monitors(), + event_loop.primary_monitor(), + None, + ) { + winit_window_builder = winit_window_builder.with_position(position); + } + + let logical_size = LogicalSize::new(window.width(), window.height()); + if let Some(sf) = window.resolution.scale_factor_override() { winit_window_builder.with_inner_size(logical_size.to_physical::(sf)) } else { winit_window_builder.with_inner_size(logical_size) @@ -72,12 +65,12 @@ impl WinitWindows { }; winit_window_builder = winit_window_builder - .with_resizable(window_descriptor.resizable) - .with_decorations(window_descriptor.decorations) - .with_transparent(window_descriptor.transparent) - .with_always_on_top(window_descriptor.always_on_top); + .with_always_on_top(window.always_on_top) + .with_resizable(window.resizable) + .with_decorations(window.decorations) + .with_transparent(window.transparent); - let constraints = window_descriptor.resize_constraints.check_constraints(); + let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, height: constraints.min_height, @@ -97,14 +90,14 @@ impl WinitWindows { }; #[allow(unused_mut)] - let mut winit_window_builder = winit_window_builder.with_title(&window_descriptor.title); + let mut winit_window_builder = winit_window_builder.with_title(window.title.as_str()); #[cfg(target_arch = "wasm32")] { use wasm_bindgen::JsCast; use winit::platform::web::WindowBuilderExtWebSys; - if let Some(selector) = &window_descriptor.canvas { + if let Some(selector) = &window.canvas { let window = web_sys::window().unwrap(); let document = window.document().unwrap(); let canvas = document @@ -121,59 +114,21 @@ impl WinitWindows { let winit_window = winit_window_builder.build(event_loop).unwrap(); - if window_descriptor.mode == WindowMode::Windowed { - use bevy_window::WindowPosition::*; - match position { - Automatic => { - if let Some(monitor) = monitor { - winit_window.set_outer_position(monitor.position()); - } - } - Centered => { - if let Some(monitor) = monitor.or_else(|| winit_window.current_monitor()) { - let monitor_position = monitor.position().cast::(); - let size = monitor.size(); - - // Logical to physical window size - let PhysicalSize:: { width, height } = - logical_size.to_physical(monitor.scale_factor()); - - let position = PhysicalPosition { - x: size.width.saturating_sub(width) as f64 / 2. + monitor_position.x, - y: size.height.saturating_sub(height) as f64 / 2. + monitor_position.y, - }; - - winit_window.set_outer_position(position); - } - } - At(position) => { - if let Some(monitor) = monitor.or_else(|| winit_window.current_monitor()) { - let monitor_position = DVec2::from(<(_, _)>::from(monitor.position())); - let position = monitor_position + position.as_dvec2(); - - if let Some(sf) = scale_factor_override { - winit_window.set_outer_position( - LogicalPosition::new(position.x, position.y).to_physical::(sf), - ); - } else { - winit_window - .set_outer_position(LogicalPosition::new(position.x, position.y)); - } - } - } - } + // Do not set the grab mode on window creation if it's none, this can fail on mobile + if window.cursor.grab_mode != CursorGrabMode::None { + attempt_grab(&winit_window, window.cursor.grab_mode); } - winit_window.set_cursor_visible(window_descriptor.cursor_visible); + winit_window.set_cursor_visible(window.cursor.visible); - self.window_id_to_winit.insert(window_id, winit_window.id()); - self.winit_to_window_id.insert(winit_window.id(), window_id); + self.entity_to_winit.insert(entity, winit_window.id()); + self.winit_to_entity.insert(winit_window.id(), entity); #[cfg(target_arch = "wasm32")] { use winit::platform::web::WindowExtWebSys; - if window_descriptor.canvas.is_none() { + if window.canvas.is_none() { let canvas = winit_window.canvas(); let window = web_sys::window().unwrap(); @@ -185,45 +140,31 @@ impl WinitWindows { } } - let position = winit_window - .outer_position() - .ok() - .map(|position| IVec2::new(position.x, position.y)); - let inner_size = winit_window.inner_size(); - let scale_factor = winit_window.scale_factor(); - let raw_handle = RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }; - self.windows.insert(winit_window.id(), winit_window); - let mut window = Window::new( - window_id, - window_descriptor, - inner_size.width, - inner_size.height, - scale_factor, - position, - Some(raw_handle), - ); - // Do not set the grab mode on window creation if it's none, this can fail on mobile - if window_descriptor.cursor_grab_mode != CursorGrabMode::None { - window.set_cursor_grab_mode(window_descriptor.cursor_grab_mode); - } - window + self.windows + .entry(winit_window.id()) + .insert(winit_window) + .into_mut() } - pub fn get_window(&self, id: WindowId) -> Option<&winit::window::Window> { - self.window_id_to_winit - .get(&id) - .and_then(|id| self.windows.get(id)) + /// Get the winit window that is associated with our entity. + pub fn get_window(&self, entity: Entity) -> Option<&winit::window::Window> { + self.entity_to_winit + .get(&entity) + .and_then(|winit_id| self.windows.get(winit_id)) } - pub fn get_window_id(&self, id: winit::window::WindowId) -> Option { - self.winit_to_window_id.get(&id).cloned() + /// Get the entity associated with the winit window id. + /// + /// This is mostly just an intermediary step between us and winit. + pub fn get_window_entity(&self, winit_id: winit::window::WindowId) -> Option { + self.winit_to_entity.get(&winit_id).cloned() } - pub fn remove_window(&mut self, id: WindowId) -> Option { - let winit_id = self.window_id_to_winit.remove(&id)?; + /// Remove a window from winit. + /// + /// This should mostly just be called when the window is closing. + pub fn remove_window(&mut self, entity: Entity) -> Option { + let winit_id = self.entity_to_winit.remove(&entity)?; // Don't remove from winit_to_window_id, to track that we used to know about this winit window self.windows.remove(&winit_id) } @@ -278,3 +219,89 @@ pub fn get_best_videomode(monitor: &winit::monitor::MonitorHandle) -> winit::mon modes.first().unwrap().clone() } + +pub(crate) fn attempt_grab(winit_window: &winit::window::Window, grab_mode: CursorGrabMode) { + let grab_result = match grab_mode { + bevy_window::CursorGrabMode::None => { + winit_window.set_cursor_grab(winit::window::CursorGrabMode::None) + } + bevy_window::CursorGrabMode::Confined => winit_window + .set_cursor_grab(winit::window::CursorGrabMode::Confined) + .or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Locked)), + bevy_window::CursorGrabMode::Locked => winit_window + .set_cursor_grab(winit::window::CursorGrabMode::Locked) + .or_else(|_e| winit_window.set_cursor_grab(winit::window::CursorGrabMode::Confined)), + }; + + if let Err(err) = grab_result { + let err_desc = match grab_mode { + bevy_window::CursorGrabMode::Confined | bevy_window::CursorGrabMode::Locked => "grab", + bevy_window::CursorGrabMode::None => "ungrab", + }; + + bevy_utils::tracing::error!("Unable to {} cursor: {}", err_desc, err); + } +} + +// Ideally we could generify this across window backends, but we only really have winit atm +// so whatever. +pub fn winit_window_position( + position: &WindowPosition, + resolution: &WindowResolution, + mut available_monitors: impl Iterator, + primary_monitor: Option, + current_monitor: Option, +) -> Option> { + match position { + WindowPosition::Automatic => { + /* Window manager will handle position */ + None + } + WindowPosition::Centered(monitor_selection) => { + use bevy_window::MonitorSelection::*; + let maybe_monitor = match monitor_selection { + Current => { + if current_monitor.is_none() { + warn!("Can't select current monitor on window creation or cannot find current monitor!"); + } + current_monitor + } + Primary => primary_monitor, + Index(n) => available_monitors.nth(*n), + }; + + if let Some(monitor) = maybe_monitor { + let screen_size = monitor.size(); + + let scale_factor = resolution.base_scale_factor(); + + // Logical to physical window size + let (width, height): (u32, u32) = + LogicalSize::new(resolution.width(), resolution.height()) + .to_physical::(scale_factor) + .into(); + + let position = PhysicalPosition { + x: screen_size.width.saturating_sub(width) as f64 / 2. + + monitor.position().x as f64, + y: screen_size.height.saturating_sub(height) as f64 / 2. + + monitor.position().y as f64, + }; + + Some(position.cast::()) + } else { + warn!("Couldn't get monitor selected with: {monitor_selection:?}"); + None + } + } + WindowPosition::At(position) => { + Some(PhysicalPosition::new(position[0] as f64, position[1] as f64).cast::()) + } + } +} + +// WARNING: this only works under the assumption that wasm runtime is single threaded +#[cfg(target_arch = "wasm32")] +unsafe impl Send for WinitWindows {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for WinitWindows {} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8d0db4604380e..8934ad65ba63b 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -39,6 +39,13 @@ |flac|FLAC audio format support. It's included in bevy_audio feature.| |mp3|MP3 audio format support.| |wav|WAV audio format support.| +|symphonia-aac|AAC audio format support by Symphonia. For more details, see `symphonia-all`.| +|symphonia-all|AAC, FLAC, MP4, MP3, Vorbis, and WAV support by Symphonia. Add support for parsing multiple file formats using a single crate instead of compiling different crates. The other non-`symphonia` features are disabled when its corresponding `symphonia` feature is enabled. [Link to `symphonia` documentation](https://docs.rs/symphonia/latest/symphonia/). More information about this topic can be found [here](https://github.com/bevyengine/bevy/pull/6388#discussion_r1009622883) | +|symphonia-flac|FLAC audio format support by Symphonia. For more details, see `symphonia-all`.| +|symphonia-isomp4|MP4 audio format support by Symphonia. For more details, see `symphonia-all`.| +|symphonia-mp3|MP3 audio format support by Symphonia. For more details, see `symphonia-all`.| +|symphonia-vorbis|Vorbis audio format support by Symphonia. For more details, see `symphonia-all`.| +|symphonia-wav|WAV audio format support by Symphonia. For more details, see `symphonia-all`.| |serialize|Enables serialization of `bevy_input` types.| |wayland|Enable this to use Wayland display server protocol other than X11.| |subpixel_glyph_atlas|Enable this to cache glyphs using subpixel accuracy. This increases texture memory usage as each position requires a separate sprite in the glyph atlas, but provide more accurate character spacing.| diff --git a/docs/profiling.md b/docs/profiling.md index 67db06825e02a..b1dd8ba20e8df 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -23,7 +23,7 @@ The [Tracy profiling tool](https://github.com/wolfpld/tracy) is: There are binaries available for Windows, and installation / build instructions for other operating systems can be found in the [Tracy documentation PDF](https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf). -It has a command line capture tool that can record the execution of graphical applications, saving it as a profile file. Tracy has a GUI to inspect these profile files. The GUI app also supports live capture, showing you in real time the trace of your app. +It has a command line capture tool that can record the execution of graphical applications, saving it as a profile file. Tracy has a GUI to inspect these profile files. The GUI app also supports live capture, showing you in real time the trace of your app. The version of tracy must be matched to the version of tracing-tracy used in bevy. A compatibility table can be found on [crates.io](https://crates.io/crates/tracing-tracy) and the version used can be found [here](https://github.com/bevyengine/bevy/blob/latest/crates/bevy_log/Cargo.toml#L21). In one terminal, run: `./capture-release -o my_capture.tracy` diff --git a/docs/the_bevy_organization.md b/docs/the_bevy_organization.md new file mode 100644 index 0000000000000..0e9d8a787a5d6 --- /dev/null +++ b/docs/the_bevy_organization.md @@ -0,0 +1,73 @@ +# The Bevy Organization + +The Bevy Organization is the group of people responsible for stewarding the Bevy project. It handles things like merging pull requests, choosing project direction, managing bugs / issues / feature requests, running the Bevy website, controlling access to secrets, defining and enforcing best practices, etc. + +Note that you _do not_ need to be a member of the Bevy Organization to contribute to Bevy. Community contributors (this means you) can freely open issues, submit pull requests, and review pull requests. + +The Bevy Organization is currently broken up into the following roles: + +## Project Lead + +Project Leads have the final call on all design and code changes within Bevy. This is to ensure a coherent vision and consistent quality of code. They are responsible for representing the project publicly and interacting with other entities (companies, organizations, etc) on behalf of the project. They choose how the project is organized, which includes how responsibility is delegated. Project Leads implicitly have the power of other roles (Maintainer, Subject Matter Expert, etc). + +@cart is, for now, our singular project lead. @cart tries to be accountable: open to new ideas and to changing his mind in the face of compelling arguments or community consensus. + +## Maintainer + +Maintainers have merge rights in Bevy repos. They assess the scope of pull requests and whether they fit into the Bevy project's vision. They also serve as representatives of the Bevy project and are often the interface between the Bevy community and the Bevy project. They assist the Project Leads in moderating the community, handling administrative tasks, defining best practices, choosing project direction, and deciding how the project is organized. + +Maintainers abide by the following rules when merging pull requests: + +1. Trivial PRs can be merged without approvals. +2. Relatively uncontroversial PRs can be merged following approval from at least two other community members with appropriate expertise. +3. Controversial PRs cannot be merged unless they have the approval of a Project Lead or two Subject Matter Experts (in the "area" of the PR). +4. If two Maintainers have approved a controversial PR they can "start the clock" on a PR by adding it to [this queue](https://github.com/orgs/bevyengine/projects/6). If 45 days elapse without SME or Project Lead action (approval, feedback or an explicit request to defer), the PR can be merged by maintainers. + +We choose new Maintainers carefully and only after they have proven themselves in the Bevy community. Maintainers must have a proven track record of the following: + +1. **A strong understanding of the Bevy project as a whole**: our vision, our development process, and our community +2. **Solid technical skills and code contributions across most engine areas**: Maintainers must be able to evaluate the scope of pull requests, provide complete code reviews, ensure the appropriate people have signed off on a PR, and decide if changes align with our vision for Bevy. This can only be done if Maintainers are technical experts, both generically across engine subject areas, and more specifically in the Bevy codebase. +3. **Great social skills**: Maintainers regularly deal with and resolve "community issues". They must always present a professional and friendly face. They are representatives of the project and their actions directly reflect our goals and values. Working with them should not be painful. +4. **Thorough reviews of other peoples' PRs**: Maintainers are the last line of defense when protecting project vision and code quality. They are also often the first people new contributors interact with. They must have a history of leaving thorough and helpful code reviews. +5. **Ethical and trustworthy behavior**: Maintainers are granted significant administrative permissions. They must be trustable. + +To make it easy to reach consensus, hold a high quality bar, and synchronize vision, we intentionally keep the Maintainer team small. We choose new maintainers carefully and only after they have proven themselves in the Bevy community. + +If you are interested in a Maintainer role and believe you meet these criteria, reach out to one of our Project Leads or Maintainers. One month after every Bevy release Maintainers and Project Leads will evaluate the need for new roles, review candidates, and vote. Bringing in a new Maintainer requires unanimous support from all Project Leads and Maintainers. + +Check out the [Bevy People](https://bevyengine.org/community/people/#the-bevy-organization) page for the current list of maintainers. + +## Subject Matter Expert (SME) + +Subject Matter Experts are members of the Bevy Organization that have proven themselves to be experts in a given development area (Rendering, Assets, ECS, UI, etc) and have a solid understanding of the Bevy Organization's vision for that area. They are great people to reach out to if you have questions about a given area of Bevy. + +SME approvals count as "votes" on controversial PRs (provided the PR is in their "subject area"). This includes [RFCs](https://github.com/bevyengine/rfcs). If a controversial PR has two votes from Subject Matter Experts in that PR's area, it can be merged without Project Lead approval. If a SME creates a PR in their subject area, this does count as a vote. However, Project Leads have the right to revert changes merged this way, so it is each SME's responsibility to ensure they have synced up with the Project Lead's vision. Additionally, when approving a design, consensus between SMEs and Project Leads (and ideally most of the wider Bevy community) is heavily encouraged. Merging without consensus risks fractured project vision and/or ping-ponging between designs. The "larger" the impact of a design, the more critical it is to establish consensus. + +We choose new SMEs carefully and only after they have proven themselves in the Bevy community. SMEs must have a proven track record of the following: + +1. **Designing and contributing to foundational pieces in their subject area**: SMEs are responsible for building and extending the foundations of a given subject area. They must have a history of doing this before becoming an SME. +2. **Thorough reviews of other peoples' PRs in their subject area**: Within a subject area, SMEs are responsible for guiding people in the correct technical direction and only approving things aligned with that vision. They must have a history of doing this before becoming an SME. +3. **Great social skills**: Within a subject area, SMEs are responsible for reviewing peoples' code, communicating project vision, and establishing consensus. They are representatives of the project and their actions directly reflect our goals and values. Working with them should not be painful. + +To make it easy to reach consensus, hold a high quality bar, and synchronize vision, we intentionally keep the number of SMEs in a given area small: 2 is the absolute minimum (to allow voting to occur), 3 is preferred, and 4 will be allowed in some cases. Bevy Organization members can be SMEs in more than one area, and Maintainers can also be SMEs. + +If you are interested in a SME role and believe you meet these criteria, reach out to one of our Project Leads or Maintainers. One month after every Bevy release Maintainers and Project Leads will evaluate the need for new roles, review candidates, and vote. Bringing in a new SME requires the support of the Project Leads and half of the Maintainers (however unanimous support is preferred). + +Check out the [Bevy People](https://bevyengine.org/community/people/#the-bevy-organization) page for the current list of SMEs. + +## Bevy Org Member / Triage Team + +[Bevy Org members](https://github.com/orgs/bevyengine/people) are contributors who: + +1. Have actively engaged with Bevy development. +2. Have demonstrated themselves to be polite and welcoming representatives of the project with an understanding of our goals and direction. +3. Have asked to join the Bevy Org. Reach out to @cart on [Discord](https://discord.gg/bevy) or email us at bevyengine@gmail.com if you are interested. Everyone is welcome to do this. We generally accept membership requests, so don't hesitate if you are interested! + +All Bevy Org members are also Triage Team members. The Triage Team can label and close issues and PRs but do not have merge rights or any special authority within the community. +Org members also have the ability to use `bors try`, causing our build system to evaluate the PR again and run additional (more computationally expensive) CI steps to verify that the PR is ready to merge. + +## Role Rotation + +All Bevy Organization roles (excluding the Triage Team) have the potential for "role rotation". Roles like Project Lead, Maintainer, and SME are intentionally kept in limited supply to ensure a cohesive project vision. However these roles can be taxing, and qualified motivated people deserve a chance to lead. To resolve these issues, we plan on building in "role rotation". What this looks like hasn't yet been determined (as this issue hasn't come up yet and we are still in the process of scaling out our team), but we will try to appropriately balance the needs and desires of both current and future leaders, while also ensuring consistent vision and continuity for Bevy. + +Additionally, if you are currently holding a role that you can no longer "meaningfully engage with", please reach out to the Project Leads and Maintainers about rotating out. We intentionally keep leadership roles in short supply to make it easier to establish consensus and encourage a cohesive project vision. If you hold a role but don't engage with it, you are preventing other qualified people from driving the project forward. Note that leaving a role doesn't need to be permanent. If you need to rotate out because your life is currently busy with work / life / school / etc, but later you find more time, we can discuss rotating back in! diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 4147a4a4966f4..3fff2d26c637f 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -312,7 +312,7 @@ pub fn queue_colored_mesh2d( transparent_draw_functions: Res>, colored_mesh2d_pipeline: Res, mut pipelines: ResMut>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, msaa: Res, render_meshes: Res>, colored_mesh2d: Query<(&Mesh2dHandle, &Mesh2dUniform), With>, @@ -329,7 +329,7 @@ pub fn queue_colored_mesh2d( for (visible_entities, mut transparent_phase, view) in &mut views { let draw_colored_mesh2d = transparent_draw_functions.read().id::(); - let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples) + let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); // Queue all entities visible to that view @@ -343,7 +343,7 @@ pub fn queue_colored_mesh2d( } let pipeline_id = - pipelines.specialize(&mut pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key); + pipelines.specialize(&pipeline_cache, &colored_mesh2d_pipeline, mesh2d_key); let mesh_z = mesh2d_uniform.transform.w_axis.z; transparent_phase.add(Transparent2d { diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 176c89b91e0f7..e920ab5062557 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -5,7 +5,10 @@ //! For an example on how to render text as part of a user interface, independent from the world //! viewport, you may want to look at `2d/contributors.rs` or `ui/text.rs`. -use bevy::{prelude::*, text::Text2dBounds}; +use bevy::{ + prelude::*, + text::{BreakLineOn, Text2dBounds}, +}; fn main() { App::new() @@ -29,11 +32,11 @@ struct AnimateScale; fn setup(mut commands: Commands, asset_server: Res) { let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let text_style = TextStyle { - font, + font: font.clone(), font_size: 60.0, color: Color::WHITE, }; - let text_alignment = TextAlignment::CENTER; + let text_alignment = TextAlignment::Center; // 2d camera commands.spawn(Camera2dBundle::default()); // Demonstrate changing translation @@ -56,39 +59,80 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate changing scale commands.spawn(( Text2dBundle { - text: Text::from_section("scale", text_style.clone()).with_alignment(text_alignment), + text: Text::from_section("scale", text_style).with_alignment(text_alignment), ..default() }, AnimateScale, )); // Demonstrate text wrapping + let slightly_smaller_text_style = TextStyle { + font, + font_size: 42.0, + color: Color::WHITE, + }; let box_size = Vec2::new(300.0, 200.0); let box_position = Vec2::new(0.0, -250.0); - commands.spawn(SpriteBundle { - sprite: Sprite { - color: Color::rgb(0.25, 0.25, 0.75), - custom_size: Some(Vec2::new(box_size.x, box_size.y)), + commands + .spawn(SpriteBundle { + sprite: Sprite { + color: Color::rgb(0.25, 0.25, 0.75), + custom_size: Some(Vec2::new(box_size.x, box_size.y)), + ..default() + }, + transform: Transform::from_translation(box_position.extend(0.0)), ..default() - }, - transform: Transform::from_translation(box_position.extend(0.0)), - ..default() - }); - commands.spawn(Text2dBundle { - text: Text::from_section("this text wraps in the box", text_style), - text_2d_bounds: Text2dBounds { - // Wrap text in the rectangle - size: box_size, - }, - // We align text to the top-left, so this transform is the top-left corner of our text. The - // box is centered at box_position, so it is necessary to move by half of the box size to - // keep the text in the box. - transform: Transform::from_xyz( - box_position.x - box_size.x / 2.0, - box_position.y + box_size.y / 2.0, - 1.0, - ), - ..default() - }); + }) + .with_children(|builder| { + builder.spawn(Text2dBundle { + text: Text { + sections: vec![TextSection::new( + "this text wraps in the box\n(Unicode linebreaks)", + slightly_smaller_text_style.clone(), + )], + alignment: TextAlignment::Left, + linebreak_behaviour: BreakLineOn::WordBoundary, + }, + text_2d_bounds: Text2dBounds { + // Wrap text in the rectangle + size: box_size, + }, + // ensure the text is drawn on top of the box + transform: Transform::from_translation(Vec3::Z), + ..default() + }); + }); + + let other_box_size = Vec2::new(300.0, 200.0); + let other_box_position = Vec2::new(320.0, -250.0); + commands + .spawn(SpriteBundle { + sprite: Sprite { + color: Color::rgb(0.20, 0.3, 0.70), + custom_size: Some(Vec2::new(other_box_size.x, other_box_size.y)), + ..default() + }, + transform: Transform::from_translation(other_box_position.extend(0.0)), + ..default() + }) + .with_children(|builder| { + builder.spawn(Text2dBundle { + text: Text { + sections: vec![TextSection::new( + "this text wraps in the box\n(AnyCharacter linebreaks)", + slightly_smaller_text_style.clone(), + )], + alignment: TextAlignment::Left, + linebreak_behaviour: BreakLineOn::AnyCharacter, + }, + text_2d_bounds: Text2dBounds { + // Wrap text in the rectangle + size: other_box_size, + }, + // ensure the text is drawn on top of the box + transform: Transform::from_translation(Vec3::Z), + ..default() + }); + }); } fn animate_translation( diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs new file mode 100644 index 0000000000000..30076743e5fb1 --- /dev/null +++ b/examples/3d/blend_modes.rs @@ -0,0 +1,378 @@ +//! This example showcases different blend modes. +//! +//! ## Controls +//! +//! | Key Binding | Action | +//! |:-------------------|:------------------------------------| +//! | `Up` / `Down` | Increase / Decrease Alpha | +//! | `Left` / `Right` | Rotate Camera | +//! | `H` | Toggle HDR | +//! | `Spacebar` | Toggle Unlit | +//! | `C` | Randomize Colors | + +use bevy::prelude::*; +use rand::random; + +fn main() { + let mut app = App::new(); + + app.add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(example_control_system); + + // Unfortunately, MSAA and HDR are not supported simultaneously under WebGL. + // Since this example uses HDR, we must disable MSAA for WASM builds, at least + // until WebGPU is ready and no longer behind a feature flag in Web browsers. + #[cfg(target_arch = "wasm32")] + app.insert_resource(Msaa { samples: 1 }); // Default is 4 samples (MSAA on) + + app.run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + asset_server: Res, +) { + let base_color = Color::rgba(0.9, 0.2, 0.3, 1.0); + let icosphere_mesh = meshes.add( + Mesh::try_from(shape::Icosphere { + radius: 0.9, + subdivisions: 7, + }) + .unwrap(), + ); + + // Opaque + let opaque = commands + .spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color, + alpha_mode: AlphaMode::Opaque, + ..default() + }), + transform: Transform::from_xyz(-4.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + unlit: true, + color: true, + }, + )) + .id(); + + // Blend + let blend = commands + .spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color, + alpha_mode: AlphaMode::Blend, + ..default() + }), + transform: Transform::from_xyz(-2.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + unlit: true, + color: true, + }, + )) + .id(); + + // Premultiplied + let premultiplied = commands + .spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color, + alpha_mode: AlphaMode::Premultiplied, + ..default() + }), + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + unlit: true, + color: true, + }, + )) + .id(); + + // Add + let add = commands + .spawn(( + PbrBundle { + mesh: icosphere_mesh.clone(), + material: materials.add(StandardMaterial { + base_color, + alpha_mode: AlphaMode::Add, + ..default() + }), + transform: Transform::from_xyz(2.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + unlit: true, + color: true, + }, + )) + .id(); + + // Multiply + let multiply = commands + .spawn(( + PbrBundle { + mesh: icosphere_mesh, + material: materials.add(StandardMaterial { + base_color, + alpha_mode: AlphaMode::Multiply, + ..default() + }), + transform: Transform::from_xyz(4.0, 0.0, 0.0), + ..default() + }, + ExampleControls { + unlit: true, + color: true, + }, + )) + .id(); + + // Chessboard Plane + let black_material = materials.add(Color::BLACK.into()); + let white_material = materials.add(Color::WHITE.into()); + let plane_mesh = meshes.add(shape::Plane { size: 2.0 }.into()); + + for x in -3..4 { + for z in -3..4 { + commands.spawn(( + PbrBundle { + mesh: plane_mesh.clone(), + material: if (x + z) % 2 == 0 { + black_material.clone() + } else { + white_material.clone() + }, + transform: Transform::from_xyz(x as f32 * 2.0, -1.0, z as f32 * 2.0), + ..default() + }, + ExampleControls { + unlit: false, + color: true, + }, + )); + } + } + + // Light + commands.spawn(PointLightBundle { + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + + // Camera + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 2.5, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Controls Text + let text_style = TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 18.0, + color: Color::BLACK, + }; + + let label_text_style = TextStyle { + font: asset_server.load("fonts/FiraMono-Medium.ttf"), + font_size: 25.0, + color: Color::ORANGE, + }; + + commands.spawn( + TextBundle::from_section( + "Up / Down — Increase / Decrease Alpha\nLeft / Right — Rotate Camera\nH - Toggle HDR\nSpacebar — Toggle Unlit\nC — Randomize Colors", + text_style.clone(), + ) + .with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(10.0), + left: Val::Px(10.0), + ..default() + }, + ..default() + }), + ); + + commands.spawn(( + TextBundle::from_section("", text_style).with_style(Style { + position_type: PositionType::Absolute, + position: UiRect { + top: Val::Px(10.0), + right: Val::Px(10.0), + ..default() + }, + ..default() + }), + ExampleDisplay, + )); + + commands.spawn(( + TextBundle::from_section("┌─ Opaque\n│\n│\n│\n│", label_text_style.clone()).with_style( + Style { + position_type: PositionType::Absolute, + ..default() + }, + ), + ExampleLabel { entity: opaque }, + )); + + commands.spawn(( + TextBundle::from_section("┌─ Blend\n│\n│\n│", label_text_style.clone()).with_style(Style { + position_type: PositionType::Absolute, + ..default() + }), + ExampleLabel { entity: blend }, + )); + + commands.spawn(( + TextBundle::from_section("┌─ Premultiplied\n│\n│", label_text_style.clone()).with_style( + Style { + position_type: PositionType::Absolute, + ..default() + }, + ), + ExampleLabel { + entity: premultiplied, + }, + )); + + commands.spawn(( + TextBundle::from_section("┌─ Add\n│", label_text_style.clone()).with_style(Style { + position_type: PositionType::Absolute, + ..default() + }), + ExampleLabel { entity: add }, + )); + + commands.spawn(( + TextBundle::from_section("┌─ Multiply", label_text_style).with_style(Style { + position_type: PositionType::Absolute, + ..default() + }), + ExampleLabel { entity: multiply }, + )); +} + +#[derive(Component)] +struct ExampleControls { + unlit: bool, + color: bool, +} + +#[derive(Component)] +struct ExampleLabel { + entity: Entity, +} + +struct ExampleState { + alpha: f32, + unlit: bool, +} + +#[derive(Component)] +struct ExampleDisplay; + +impl Default for ExampleState { + fn default() -> Self { + ExampleState { + alpha: 0.9, + unlit: false, + } + } +} + +#[allow(clippy::too_many_arguments)] +fn example_control_system( + mut materials: ResMut>, + controllable: Query<(&Handle, &ExampleControls)>, + mut camera: Query<(&mut Camera, &mut Transform, &GlobalTransform), With>, + mut labels: Query<(&mut Style, &ExampleLabel)>, + mut display: Query<&mut Text, With>, + labelled: Query<&GlobalTransform>, + mut state: Local, + time: Res

::Item<'w, 's>; // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. unsafe impl<'w, 's, Q: ReadOnlyWorldQuery + 'static, F: ReadOnlyWorldQuery + 'static> @@ -175,13 +188,14 @@ unsafe impl<'w, 's, Q: ReadOnlyWorldQuery + 'static, F: ReadOnlyWorldQuery + 'st } // SAFETY: Relevant query ComponentId and ArchetypeComponentId access is applied to SystemMeta. If -// this QueryState conflicts with any prior access, a panic will occur. -unsafe impl SystemParamState - for QueryState +// this Query conflicts with any prior access, a panic will occur. +unsafe impl SystemParam + for Query<'_, '_, Q, F> { + type State = QueryState; type Item<'w, 's> = Query<'w, 's, Q, F>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let state = QueryState::new(world); assert_component_access_compatibility( &system_meta.name, @@ -200,21 +214,27 @@ unsafe impl SystemPara state } - fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { - self.new_archetype(archetype); + fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { + state.new_archetype(archetype); system_meta .archetype_component_access - .extend(&self.archetype_component_access); + .extend(&state.archetype_component_access); } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - Query::new(world, state, system_meta.last_change_tick, change_tick) + Query::new( + world, + state, + system_meta.last_change_tick, + change_tick, + false, + ) } } @@ -235,8 +255,7 @@ fn assert_component_access_compatibility( .map(|component_id| world.components.get_info(component_id).unwrap().name()) .collect::>(); let accesses = conflicting_components.join(", "); - panic!("error[B0001]: Query<{}, {}> in system {} accesses component(s) {} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`.", - query_type, filter_type, system_name, accesses); + panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s) {accesses} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`."); } /// A collection of potentially conflicting [`SystemParam`]s allowed by disjoint access. @@ -279,7 +298,7 @@ fn assert_component_access_compatibility( /// # bad_system_system.run((), &mut world); /// ``` /// -/// Conflicing `SystemParam`s like these can be placed in a `ParamSet`, +/// Conflicting `SystemParam`s like these can be placed in a `ParamSet`, /// which leverages the borrow checker to ensure that only one of the contained parameters are accessed at a given time. /// /// ``` @@ -353,8 +372,6 @@ pub struct ParamSet<'w, 's, T: SystemParam> { system_meta: SystemMeta, change_tick: u32, } -/// The [`SystemParamState`] of [`ParamSet`]. -pub struct ParamSetState(T); impl_param_set!(); @@ -391,123 +408,16 @@ impl_param_set!(); /// ``` pub trait Resource: Send + Sync + 'static {} -/// Shared borrow of a [`Resource`]. -/// -/// See the [`Resource`] documentation for usage. -/// -/// If you need a unique mutable borrow, use [`ResMut`] instead. -/// -/// # Panics -/// -/// Panics when used as a [`SystemParameter`](SystemParam) if the resource does not exist. -/// -/// Use `Option>` instead if the resource might not always exist. -pub struct Res<'w, T: Resource> { - value: &'w T, - added: &'w Tick, - changed: &'w Tick, - last_change_tick: u32, - change_tick: u32, -} - // SAFETY: Res only reads a single World resource -unsafe impl<'w, T: Resource> ReadOnlySystemParam for Res<'w, T> {} - -impl<'w, T: Resource> Debug for Res<'w, T> -where - T: Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Res").field(&self.value).finish() - } -} - -impl<'w, T: Resource> Res<'w, T> { - // no it shouldn't clippy - #[allow(clippy::should_implement_trait)] - pub fn clone(this: &Self) -> Self { - Self { - value: this.value, - added: this.added, - changed: this.changed, - last_change_tick: this.last_change_tick, - change_tick: this.change_tick, - } - } - - /// Returns `true` if the resource was added after the system last ran. - pub fn is_added(&self) -> bool { - self.added - .is_older_than(self.last_change_tick, self.change_tick) - } - - /// Returns `true` if the resource was added or mutably dereferenced after the system last ran. - pub fn is_changed(&self) -> bool { - self.changed - .is_older_than(self.last_change_tick, self.change_tick) - } - - pub fn into_inner(self) -> &'w T { - self.value - } -} - -impl<'w, T: Resource> Deref for Res<'w, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - self.value - } -} - -impl<'w, T: Resource> AsRef for Res<'w, T> { - #[inline] - fn as_ref(&self) -> &T { - self.deref() - } -} - -impl<'w, T: Resource> From> for Res<'w, T> { - fn from(res: ResMut<'w, T>) -> Self { - Self { - value: res.value, - added: res.ticks.added, - changed: res.ticks.changed, - change_tick: res.ticks.change_tick, - last_change_tick: res.ticks.last_change_tick, - } - } -} - -impl<'w, 'a, T: Resource> IntoIterator for &'a Res<'w, T> -where - &'a T: IntoIterator, -{ - type Item = <&'a T as IntoIterator>::Item; - type IntoIter = <&'a T as IntoIterator>::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.value.into_iter() - } -} - -/// The [`SystemParamState`] of [`Res`]. -#[doc(hidden)] -pub struct ResState { - component_id: ComponentId, - marker: PhantomData, -} - -impl<'a, T: Resource> SystemParam for Res<'a, T> { - type State = ResState; -} +unsafe impl<'a, T: Resource> ReadOnlySystemParam for Res<'a, T> {} // SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. -unsafe impl SystemParamState for ResState { +unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { + type State = ComponentId; type Item<'w, 's> = Res<'w, T>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let component_id = world.initialize_resource::(); let combined_access = system_meta.component_access_set.combined_access(); assert!( @@ -526,21 +436,19 @@ unsafe impl SystemParamState for ResState { system_meta .archetype_component_access .add_read(archetype_component_id); - Self { - component_id, - marker: PhantomData, - } + + component_id } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { let (ptr, ticks) = world - .get_resource_with_ticks(state.component_id) + .get_resource_with_ticks(component_id) .unwrap_or_else(|| { panic!( "Resource requested by {} does not exist: {}", @@ -550,71 +458,56 @@ unsafe impl SystemParamState for ResState { }); Res { value: ptr.deref(), - added: ticks.added.deref(), - changed: ticks.changed.deref(), - last_change_tick: system_meta.last_change_tick, - change_tick, + ticks: Ticks { + added: ticks.added.deref(), + changed: ticks.changed.deref(), + last_change_tick: system_meta.last_change_tick, + change_tick, + }, } } } -/// The [`SystemParamState`] of [`Option>`]. -/// See: [`Res`] -#[doc(hidden)] -pub struct OptionResState(ResState); - -impl<'a, T: Resource> SystemParam for Option> { - type State = OptionResState; -} - // SAFETY: Only reads a single World resource unsafe impl<'a, T: Resource> ReadOnlySystemParam for Option> {} -// SAFETY: this impl defers to `ResState`, which initializes -// and validates the correct world access -unsafe impl SystemParamState for OptionResState { +// SAFETY: this impl defers to `Res`, which initializes and validates the correct world access. +unsafe impl<'a, T: Resource> SystemParam for Option> { + type State = ComponentId; type Item<'w, 's> = Option>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self(ResState::init(world, system_meta)) + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + Res::::init_state(world, system_meta) } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { world - .get_resource_with_ticks(state.0.component_id) + .get_resource_with_ticks(component_id) .map(|(ptr, ticks)| Res { value: ptr.deref(), - added: ticks.added.deref(), - changed: ticks.changed.deref(), - last_change_tick: system_meta.last_change_tick, - change_tick, + ticks: Ticks { + added: ticks.added.deref(), + changed: ticks.changed.deref(), + last_change_tick: system_meta.last_change_tick, + change_tick, + }, }) } } -/// The [`SystemParamState`] of [`ResMut`]. -#[doc(hidden)] -pub struct ResMutState { - component_id: ComponentId, - marker: PhantomData, -} - -impl<'a, T: Resource> SystemParam for ResMut<'a, T> { - type State = ResMutState; -} - // SAFETY: Res ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this Res // conflicts with any prior access, a panic will occur. -unsafe impl SystemParamState for ResMutState { +unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { + type State = ComponentId; type Item<'w, 's> = ResMut<'w, T>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let component_id = world.initialize_resource::(); let combined_access = system_meta.component_access_set.combined_access(); if combined_access.has_write(component_id) { @@ -636,21 +529,19 @@ unsafe impl SystemParamState for ResMutState { system_meta .archetype_component_access .add_write(archetype_component_id); - Self { - component_id, - marker: PhantomData, - } + + component_id } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { let value = world - .get_resource_unchecked_mut_with_id(state.component_id) + .get_resource_unchecked_mut_with_id(component_id) .unwrap_or_else(|| { panic!( "Resource requested by {} does not exist: {}", @@ -660,7 +551,7 @@ unsafe impl SystemParamState for ResMutState { }); ResMut { value: value.value, - ticks: Ticks { + ticks: TicksMut { added: value.ticks.added, changed: value.ticks.changed, last_change_tick: system_meta.last_change_tick, @@ -670,36 +561,27 @@ unsafe impl SystemParamState for ResMutState { } } -/// The [`SystemParamState`] of [`Option>`]. -/// See: [`ResMut`] -#[doc(hidden)] -pub struct OptionResMutState(ResMutState); - -impl<'a, T: Resource> SystemParam for Option> { - type State = OptionResMutState; -} - -// SAFETY: this impl defers to `ResMutState`, which initializes -// and validates the correct world access -unsafe impl SystemParamState for OptionResMutState { +// SAFETY: this impl defers to `ResMut`, which initializes and validates the correct world access. +unsafe impl<'a, T: Resource> SystemParam for Option> { + type State = ComponentId; type Item<'w, 's> = Option>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self(ResMutState::init(world, system_meta)) + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + ResMut::::init_state(world, system_meta) } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { world - .get_resource_unchecked_mut_with_id(state.0.component_id) + .get_resource_unchecked_mut_with_id(component_id) .map(|value| ResMut { value: value.value, - ticks: Ticks { + ticks: TicksMut { added: value.ticks.added, changed: value.ticks.changed, last_change_tick: system_meta.last_change_tick, @@ -709,32 +591,29 @@ unsafe impl SystemParamState for OptionResMutState { } } -impl<'w, 's> SystemParam for Commands<'w, 's> { - type State = CommandQueue; -} - // SAFETY: Commands only accesses internal state unsafe impl<'w, 's> ReadOnlySystemParam for Commands<'w, 's> {} -// SAFETY: only local state is accessed -unsafe impl SystemParamState for CommandQueue { +// SAFETY: `Commands::get_param` does not access the world. +unsafe impl SystemParam for Commands<'_, '_> { + type State = CommandQueue; type Item<'w, 's> = Commands<'w, 's>; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { Default::default() } - fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { #[cfg(feature = "trace")] let _system_span = bevy_utils::tracing::info_span!("system_commands", name = _system_meta.name()) .entered(); - self.apply(world); + state.apply(world); } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -746,19 +625,12 @@ unsafe impl SystemParamState for CommandQueue { /// SAFETY: only reads world unsafe impl<'w> ReadOnlySystemParam for &'w World {} -/// The [`SystemParamState`] of [`&World`](crate::world::World). -#[doc(hidden)] -pub struct WorldState; - -impl<'w> SystemParam for &'w World { - type State = WorldState; -} - // SAFETY: `read_all` access is set and conflicts result in a panic -unsafe impl SystemParamState for WorldState { +unsafe impl SystemParam for &'_ World { + type State = (); type Item<'w, 's> = &'w World; - fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { let mut access = Access::default(); access.read_all(); if !system_meta @@ -780,12 +652,10 @@ unsafe impl SystemParamState for WorldState { panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); } system_meta.component_access_set.add(filtered_access); - - WorldState } unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -798,6 +668,10 @@ unsafe impl SystemParamState for WorldState { /// /// A local may only be accessed by the system itself and is therefore not visible to other systems. /// If two or more systems specify the same local type each will have their own unique local. +/// If multiple [`SystemParam`]s within the same system each specify the same local type +/// each will get their own distinct data storage. +/// +/// The supplied lifetime parameter is the [`SystemParam`]s `'s` lifetime. /// /// # Examples /// @@ -837,12 +711,12 @@ unsafe impl SystemParamState for WorldState { /// // .add_system(reset_to_system(my_config)) /// # assert_is_system(reset_to_system(Config(10))); /// ``` -pub struct Local<'a, T: FromWorld + Send + 'static>(pub(crate) &'a mut T); +pub struct Local<'s, T: FromWorld + Send + 'static>(pub(crate) &'s mut T); // SAFETY: Local only accesses internal state -unsafe impl<'a, T: FromWorld + Send + 'static> ReadOnlySystemParam for Local<'a, T> {} +unsafe impl<'s, T: FromWorld + Send + 'static> ReadOnlySystemParam for Local<'s, T> {} -impl<'a, T: FromWorld + Send + Sync + 'static> Debug for Local<'a, T> +impl<'s, T: FromWorld + Send + Sync + 'static> Debug for Local<'s, T> where T: Debug, { @@ -851,7 +725,7 @@ where } } -impl<'a, T: FromWorld + Send + Sync + 'static> Deref for Local<'a, T> { +impl<'s, T: FromWorld + Send + Sync + 'static> Deref for Local<'s, T> { type Target = T; #[inline] @@ -860,14 +734,14 @@ impl<'a, T: FromWorld + Send + Sync + 'static> Deref for Local<'a, T> { } } -impl<'a, T: FromWorld + Send + Sync + 'static> DerefMut for Local<'a, T> { +impl<'s, T: FromWorld + Send + Sync + 'static> DerefMut for Local<'s, T> { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { self.0 } } -impl<'w, 'a, T: FromWorld + Send + 'static> IntoIterator for &'a Local<'w, T> +impl<'s, 'a, T: FromWorld + Send + 'static> IntoIterator for &'a Local<'s, T> where &'a T: IntoIterator, { @@ -879,7 +753,7 @@ where } } -impl<'w, 'a, T: FromWorld + Send + 'static> IntoIterator for &'a mut Local<'w, T> +impl<'s, 'a, T: FromWorld + Send + 'static> IntoIterator for &'a mut Local<'s, T> where &'a mut T: IntoIterator, { @@ -891,30 +765,23 @@ where } } -/// The [`SystemParamState`] of [`Local`]. -#[doc(hidden)] -pub struct LocalState(pub(crate) SyncCell); - -impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { - type State = LocalState; -} - // SAFETY: only local state is accessed -unsafe impl SystemParamState for LocalState { +unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { + type State = SyncCell; type Item<'w, 's> = Local<'s, T>; - fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self(SyncCell::new(T::from_world(world))) + fn init_state(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + SyncCell::new(T::from_world(world)) } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, _change_tick: u32, ) -> Self::Item<'w, 's> { - Local(state.0.get()) + Local(state.get()) } } @@ -976,39 +843,26 @@ impl<'a, T: Component> IntoIterator for &'a RemovedComponents<'a, T> { // SAFETY: Only reads World components unsafe impl<'a, T: Component> ReadOnlySystemParam for RemovedComponents<'a, T> {} -/// The [`SystemParamState`] of [`RemovedComponents`]. -#[doc(hidden)] -pub struct RemovedComponentsState { - component_id: ComponentId, - marker: PhantomData, -} - -impl<'a, T: Component> SystemParam for RemovedComponents<'a, T> { - type State = RemovedComponentsState; -} - // SAFETY: no component access. removed component entity collections can be read in parallel and are // never mutably borrowed during system execution -unsafe impl SystemParamState for RemovedComponentsState { +unsafe impl<'a, T: Component> SystemParam for RemovedComponents<'a, T> { + type State = ComponentId; type Item<'w, 's> = RemovedComponents<'w, T>; - fn init(world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self { - component_id: world.init_component::(), - marker: PhantomData, - } + fn init_state(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + world.init_component::() } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, ) -> Self::Item<'w, 's> { RemovedComponents { world, - component_id: state.component_id, + component_id, marker: PhantomData, } } @@ -1079,23 +933,13 @@ impl<'a, T> From> for NonSend<'a, T> { } } -/// The [`SystemParamState`] of [`NonSend`]. -#[doc(hidden)] -pub struct NonSendState { - component_id: ComponentId, - marker: PhantomData T>, -} - -impl<'a, T: 'static> SystemParam for NonSend<'a, T> { - type State = NonSendState; -} - // SAFETY: NonSendComponentId and ArchetypeComponentId access is applied to SystemMeta. If this // NonSend conflicts with any prior access, a panic will occur. -unsafe impl SystemParamState for NonSendState { +unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { + type State = ComponentId; type Item<'w, 's> = NonSend<'w, T>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { system_meta.set_non_send(); let component_id = world.initialize_non_send_resource::(); @@ -1111,27 +955,24 @@ unsafe impl SystemParamState for NonSendState { .add_unfiltered_read(component_id); let archetype_component_id = world - .get_resource_archetype_component_id(component_id) + .get_non_send_archetype_component_id(component_id) .unwrap(); system_meta .archetype_component_access .add_read(archetype_component_id); - Self { - component_id, - marker: PhantomData, - } + + component_id } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - world.validate_non_send_access::(); let (ptr, ticks) = world - .get_resource_with_ticks(state.component_id) + .get_non_send_with_ticks(component_id) .unwrap_or_else(|| { panic!( "Non-send resource requested by {} does not exist: {}", @@ -1149,37 +990,27 @@ unsafe impl SystemParamState for NonSendState { } } -/// The [`SystemParamState`] of [`Option>`]. -/// See: [`NonSend`] -#[doc(hidden)] -pub struct OptionNonSendState(NonSendState); - -impl<'w, T: 'static> SystemParam for Option> { - type State = OptionNonSendState; -} - -// SAFETY: Only reads a single non-send resource -unsafe impl<'w, T: 'static> ReadOnlySystemParam for Option> {} +// SAFETY: Only reads a single World non-send resource +unsafe impl ReadOnlySystemParam for Option> {} -// SAFETY: this impl defers to `NonSendState`, which initializes -// and validates the correct world access -unsafe impl SystemParamState for OptionNonSendState { +// SAFETY: this impl defers to `NonSend`, which initializes and validates the correct world access. +unsafe impl SystemParam for Option> { + type State = ComponentId; type Item<'w, 's> = Option>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self(NonSendState::init(world, system_meta)) + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + NonSend::::init_state(world, system_meta) } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - world.validate_non_send_access::(); world - .get_resource_with_ticks(state.0.component_id) + .get_non_send_with_ticks(component_id) .map(|(ptr, ticks)| NonSend { value: ptr.deref(), ticks: ticks.read(), @@ -1189,23 +1020,13 @@ unsafe impl SystemParamState for OptionNonSendState { } } -/// The [`SystemParamState`] of [`NonSendMut`]. -#[doc(hidden)] -pub struct NonSendMutState { - component_id: ComponentId, - marker: PhantomData T>, -} - -impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { - type State = NonSendMutState; -} - // SAFETY: NonSendMut ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this // NonSendMut conflicts with any prior access, a panic will occur. -unsafe impl SystemParamState for NonSendMutState { +unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { + type State = ComponentId; type Item<'w, 's> = NonSendMut<'w, T>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { system_meta.set_non_send(); let component_id = world.initialize_non_send_resource::(); @@ -1224,27 +1045,24 @@ unsafe impl SystemParamState for NonSendMutState { .add_unfiltered_write(component_id); let archetype_component_id = world - .get_resource_archetype_component_id(component_id) + .get_non_send_archetype_component_id(component_id) .unwrap(); system_meta .archetype_component_access .add_write(archetype_component_id); - Self { - component_id, - marker: PhantomData, - } + + component_id } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - world.validate_non_send_access::(); let (ptr, ticks) = world - .get_resource_with_ticks(state.component_id) + .get_non_send_with_ticks(component_id) .unwrap_or_else(|| { panic!( "Non-send resource requested by {} does not exist: {}", @@ -1254,68 +1072,49 @@ unsafe impl SystemParamState for NonSendMutState { }); NonSendMut { value: ptr.assert_unique().deref_mut(), - ticks: Ticks::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), + ticks: TicksMut::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), } } } -/// The [`SystemParamState`] of [`Option>`]. -/// See: [`NonSendMut`] -#[doc(hidden)] -pub struct OptionNonSendMutState(NonSendMutState); - -impl<'a, T: 'static> SystemParam for Option> { - type State = OptionNonSendMutState; -} - -// SAFETY: this impl defers to `NonSendMutState`, which initializes -// and validates the correct world access -unsafe impl SystemParamState for OptionNonSendMutState { +// SAFETY: this impl defers to `NonSendMut`, which initializes and validates the correct world access. +unsafe impl<'a, T: 'static> SystemParam for Option> { + type State = ComponentId; type Item<'w, 's> = Option>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self(NonSendMutState::init(world, system_meta)) + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + NonSendMut::::init_state(world, system_meta) } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, change_tick: u32, ) -> Self::Item<'w, 's> { - world.validate_non_send_access::(); world - .get_resource_with_ticks(state.0.component_id) + .get_non_send_with_ticks(component_id) .map(|(ptr, ticks)| NonSendMut { value: ptr.assert_unique().deref_mut(), - ticks: Ticks::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), + ticks: TicksMut::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), }) } } -impl<'a> SystemParam for &'a Archetypes { - type State = ArchetypesState; -} - // SAFETY: Only reads World archetypes unsafe impl<'a> ReadOnlySystemParam for &'a Archetypes {} -/// The [`SystemParamState`] of [`Archetypes`]. -#[doc(hidden)] -pub struct ArchetypesState; - // SAFETY: no component value access -unsafe impl SystemParamState for ArchetypesState { +unsafe impl<'a> SystemParam for &'a Archetypes { + type State = (); type Item<'w, 's> = &'w Archetypes; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self - } + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} #[inline] unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -1324,28 +1123,19 @@ unsafe impl SystemParamState for ArchetypesState { } } -impl<'a> SystemParam for &'a Components { - type State = ComponentsState; -} - // SAFETY: Only reads World components unsafe impl<'a> ReadOnlySystemParam for &'a Components {} -/// The [`SystemParamState`] of [`Components`]. -#[doc(hidden)] -pub struct ComponentsState; - // SAFETY: no component value access -unsafe impl SystemParamState for ComponentsState { +unsafe impl<'a> SystemParam for &'a Components { + type State = (); type Item<'w, 's> = &'w Components; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self - } + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} #[inline] unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -1354,28 +1144,19 @@ unsafe impl SystemParamState for ComponentsState { } } -impl<'a> SystemParam for &'a Entities { - type State = EntitiesState; -} - // SAFETY: Only reads World entities unsafe impl<'a> ReadOnlySystemParam for &'a Entities {} -/// The [`SystemParamState`] of [`Entities`]. -#[doc(hidden)] -pub struct EntitiesState; - // SAFETY: no component value access -unsafe impl SystemParamState for EntitiesState { +unsafe impl<'a> SystemParam for &'a Entities { + type State = (); type Item<'w, 's> = &'w Entities; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self - } + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} #[inline] unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -1384,28 +1165,19 @@ unsafe impl SystemParamState for EntitiesState { } } -impl<'a> SystemParam for &'a Bundles { - type State = BundlesState; -} - // SAFETY: Only reads World bundles unsafe impl<'a> ReadOnlySystemParam for &'a Bundles {} -/// The [`SystemParamState`] of [`Bundles`]. -#[doc(hidden)] -pub struct BundlesState; - // SAFETY: no component value access -unsafe impl SystemParamState for BundlesState { +unsafe impl<'a> SystemParam for &'a Bundles { + type State = (); type Item<'w, 's> = &'w Bundles; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self - } + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} #[inline] unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, _change_tick: u32, @@ -1446,24 +1218,15 @@ impl SystemChangeTick { // SAFETY: Only reads internal system state unsafe impl ReadOnlySystemParam for SystemChangeTick {} -impl SystemParam for SystemChangeTick { - type State = SystemChangeTickState; -} - -/// The [`SystemParamState`] of [`SystemChangeTick`]. -#[doc(hidden)] -pub struct SystemChangeTickState {} - -// SAFETY: `SystemParamTickState` doesn't require any world access -unsafe impl SystemParamState for SystemChangeTickState { +// SAFETY: `SystemChangeTick` doesn't require any world access +unsafe impl SystemParam for SystemChangeTick { + type State = (); type Item<'w, 's> = SystemChangeTick; - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - Self {} - } + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} unsafe fn get_param<'w, 's>( - _state: &'s mut Self, + _state: &'s mut Self::State, system_meta: &SystemMeta, _world: &'w World, change_tick: u32, @@ -1522,79 +1285,60 @@ impl<'s> std::fmt::Display for SystemName<'s> { } } -impl<'s> SystemParam for SystemName<'s> { - type State = SystemNameState; -} - -// SAFETY: Only reads internal system state -unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} - -/// The [`SystemParamState`] of [`SystemName`]. -#[doc(hidden)] -pub struct SystemNameState { - name: Cow<'static, str>, -} - // SAFETY: no component value access -unsafe impl SystemParamState for SystemNameState { +unsafe impl SystemParam for SystemName<'_> { + type State = Cow<'static, str>; type Item<'w, 's> = SystemName<'s>; - fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self { - name: system_meta.name.clone(), - } + fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + system_meta.name.clone() } #[inline] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + name: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, _change_tick: u32, ) -> Self::Item<'w, 's> { - SystemName { - name: state.name.as_ref(), - } + SystemName { name } } } +// SAFETY: Only reads internal system state +unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} + macro_rules! impl_system_param_tuple { ($($param: ident),*) => { - impl<$($param: SystemParam),*> SystemParam for ($($param,)*) { - type State = ($($param::State,)*); - } - // SAFETY: tuple consists only of ReadOnlySystemParams unsafe impl<$($param: ReadOnlySystemParam),*> ReadOnlySystemParam for ($($param,)*) {} - - // SAFETY: implementors of each `SystemParamState` in the tuple have validated their impls + // SAFETY: implementors of each `SystemParam` in the tuple have validated their impls #[allow(clippy::undocumented_unsafe_blocks)] // false positive by clippy #[allow(non_snake_case)] - unsafe impl<$($param: SystemParamState),*> SystemParamState for ($($param,)*) { + unsafe impl<$($param: SystemParam),*> SystemParam for ($($param,)*) { + type State = ($($param::State,)*); type Item<'w, 's> = ($($param::Item::<'w, 's>,)*); #[inline] - fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self { - (($($param::init(_world, _system_meta),)*)) + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + (($($param::init_state(_world, _system_meta),)*)) } #[inline] - fn new_archetype(&mut self, _archetype: &Archetype, _system_meta: &mut SystemMeta) { - let ($($param,)*) = self; - $($param.new_archetype(_archetype, _system_meta);)* + fn new_archetype(($($param,)*): &mut Self::State, _archetype: &Archetype, _system_meta: &mut SystemMeta) { + $($param::new_archetype($param, _archetype, _system_meta);)* } #[inline] - fn apply(&mut self, _system_meta: &SystemMeta, _world: &mut World) { - let ($($param,)*) = self; - $($param.apply(_system_meta, _world);)* + fn apply(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, _world: &mut World) { + $($param::apply($param, _system_meta, _world);)* } #[inline] #[allow(clippy::unused_unit)] unsafe fn get_param<'w, 's>( - state: &'s mut Self, + state: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, _change_tick: u32, @@ -1694,48 +1438,37 @@ impl<'w, 's, P: SystemParam> StaticSystemParam<'w, 's, P> { } } -/// The [`SystemParamState`] of [`StaticSystemParam`]. -#[doc(hidden)] -pub struct StaticSystemParamState(S, PhantomData P>); - // SAFETY: This doesn't add any more reads, and the delegated fetch confirms it unsafe impl<'w, 's, P: ReadOnlySystemParam + 'static> ReadOnlySystemParam for StaticSystemParam<'w, 's, P> { } -impl<'world, 'state, P: SystemParam + 'static> SystemParam - for StaticSystemParam<'world, 'state, P> -{ - type State = StaticSystemParamState; -} - -// SAFETY: all methods are just delegated to `S`'s `SystemParamState` implementation -unsafe impl + 'static> SystemParamState - for StaticSystemParamState -{ +// SAFETY: all methods are just delegated to `P`'s `SystemParam` implementation +unsafe impl SystemParam for StaticSystemParam<'_, '_, P> { + type State = P::State; type Item<'world, 'state> = StaticSystemParam<'world, 'state, P>; - fn init(world: &mut World, system_meta: &mut SystemMeta) -> Self { - Self(S::init(world, system_meta), PhantomData) + fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + P::init_state(world, system_meta) } - fn new_archetype(&mut self, archetype: &Archetype, system_meta: &mut SystemMeta) { - self.0.new_archetype(archetype, system_meta); + fn new_archetype(state: &mut Self::State, archetype: &Archetype, system_meta: &mut SystemMeta) { + P::new_archetype(state, archetype, system_meta); } - fn apply(&mut self, system_meta: &SystemMeta, world: &mut World) { - self.0.apply(system_meta, world); + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { + P::apply(state, system_meta, world); } unsafe fn get_param<'world, 'state>( - state: &'state mut Self, + state: &'state mut Self::State, system_meta: &SystemMeta, world: &'world World, change_tick: u32, ) -> Self::Item<'world, 'state> { - // SAFETY: We properly delegate SystemParamState - StaticSystemParam(S::get_param(&mut state.0, system_meta, world, change_tick)) + // SAFETY: Defer to the safety of P::SystemParam + StaticSystemParam(P::get_param(state, system_meta, world, change_tick)) } } @@ -1801,6 +1534,13 @@ mod tests { crate::system::assert_is_system(long_system); } + #[derive(SystemParam)] + struct MyParam<'w, T: Resource, Marker: 'static> { + _foo: Res<'w, T>, + #[system_param(ignore)] + marker: PhantomData, + } + #[derive(SystemParam)] pub struct UnitParam; @@ -1815,4 +1555,13 @@ mod tests { #[derive(SystemParam)] pub struct EncapsulatedParam<'w>(Res<'w, PrivateResource>); + + // regression test for https://github.com/bevyengine/bevy/issues/7103. + #[derive(SystemParam)] + pub struct WhereParam<'w, 's, Q> + where + Q: 'static + WorldQuery, + { + _q: Query<'w, 's, Q, ()>, + } } diff --git a/crates/bevy_ecs/src/system/system_piping.rs b/crates/bevy_ecs/src/system/system_piping.rs index dd343461b9e5f..efd545a46a30f 100644 --- a/crates/bevy_ecs/src/system/system_piping.rs +++ b/crates/bevy_ecs/src/system/system_piping.rs @@ -5,7 +5,7 @@ use crate::{ system::{IntoSystem, System}, world::World, }; -use std::borrow::Cow; +use std::{any::TypeId, borrow::Cow}; /// A [`System`] created by piping the output of the first system into the input of the second. /// @@ -77,6 +77,10 @@ impl> System for PipeSystem< self.name.clone() } + fn type_id(&self) -> TypeId { + TypeId::of::<(SystemA, SystemB)>() + } + fn archetype_component_access(&self) -> &Access { &self.archetype_component_access } @@ -141,6 +145,18 @@ impl> System for PipeSystem< self.system_a.set_last_change_tick(last_change_tick); self.system_b.set_last_change_tick(last_change_tick); } + + fn default_labels(&self) -> Vec { + let mut labels = self.system_a.default_labels(); + labels.extend(&self.system_b.default_labels()); + labels + } + + fn default_system_sets(&self) -> Vec> { + let mut system_sets = self.system_a.default_system_sets(); + system_sets.extend_from_slice(&self.system_b.default_system_sets()); + system_sets + } } /// An extension trait providing the [`IntoPipeSystem::pipe`] method to pass input from one system into the next. @@ -433,8 +449,15 @@ pub mod adapter { unimplemented!() } + /// Mocks an exclusive system that takes an input and returns an output. + fn exclusive_in_out(_: In, _: &mut World) -> B { + unimplemented!() + } + assert_is_system(returning::>.pipe(unwrap)); assert_is_system(returning::>.pipe(ignore)); assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap)); + assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error)); + assert_is_system(returning::.pipe(exclusive_in_out::)); } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 251945d711b09..a165678516987 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,13 +1,12 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes}, bundle::{Bundle, BundleInfo}, - change_detection::{MutUntyped, Ticks}, + change_detection::{MutUntyped, TicksMut}, component::{ Component, ComponentId, ComponentStorage, ComponentTicks, Components, StorageType, - TickCells, }, entity::{Entities, Entity, EntityLocation}, - storage::{Column, ComponentSparseSet, SparseSet, Storages}, + storage::{SparseSet, Storages}, world::{Mut, World}, }; use bevy_ptr::{OwningPtr, Ptr}; @@ -23,8 +22,17 @@ pub struct EntityRef<'w> { } impl<'w> EntityRef<'w> { + /// # Safety + /// + /// - `entity` must be valid for `world`: the generation should match that of the entity at the same index. + /// - `location` must be sourced from `world`'s `Entities` and must exactly match the location for `entity` + /// + /// The above is trivially satisfied if `location` was sourced from `world.entities().get(entity)`. #[inline] - pub(crate) fn new(world: &'w World, entity: Entity, location: EntityLocation) -> Self { + pub(crate) unsafe fn new(world: &'w World, entity: Entity, location: EntityLocation) -> Self { + debug_assert!(world.entities().contains(entity)); + debug_assert_eq!(world.entities().get(entity), Some(location)); + Self { world, entity, @@ -72,17 +80,18 @@ impl<'w> EntityRef<'w> { pub fn get(&self) -> Option<&'w T> { // SAFETY: // - entity location and entity is valid - // - returned component is of type T // - the storage type provided is correct for T + // - world access is immutable, lifetime tied to `&self` unsafe { - get_component_with_type( - self.world, - TypeId::of::(), - T::Storage::STORAGE_TYPE, - self.entity, - self.location, - ) - .map(|value| value.deref::()) + self.world + .get_component_with_type( + TypeId::of::(), + T::Storage::STORAGE_TYPE, + self.entity, + self.location, + ) + // SAFETY: returned component is of type T + .map(|value| value.deref::()) } } @@ -92,10 +101,10 @@ impl<'w> EntityRef<'w> { pub fn get_change_ticks(&self) -> Option { // SAFETY: // - entity location and entity is valid + // - world access is immutable, lifetime tied to `&self` // - the storage type provided is correct for T unsafe { - get_ticks_with_type( - self.world, + self.world.get_ticks_with_type( TypeId::of::(), T::Storage::STORAGE_TYPE, self.entity, @@ -112,15 +121,13 @@ impl<'w> EntityRef<'w> { /// compile time.** #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { - if !self.contains_id(component_id) { - return None; - } - let info = self.world.components().get_info(component_id)?; - // SAFETY: Entity location is valid and component_id exists. + // SAFETY: + // - entity location and entity is valid + // - world access is immutable, lifetime tied to `&self` + // - the storage type provided is correct for T unsafe { - get_ticks( - self.world, + self.world.get_ticks( component_id, info.storage_type(), self.entity, @@ -149,20 +156,20 @@ impl<'w> EntityRef<'w> { // - entity location and entity is valid // - returned component is of type T // - the storage type provided is correct for T - get_component_and_ticks_with_type( - self.world, - TypeId::of::(), - T::Storage::STORAGE_TYPE, - self.entity, - self.location, - ) - .map(|(value, ticks)| Mut { - // SAFETY: - // - returned component is of type T - // - Caller guarentees that this reference will not alias. - value: value.assert_unique().deref_mut::(), - ticks: Ticks::from_tick_cells(ticks, last_change_tick, change_tick), - }) + self.world + .get_component_and_ticks_with_type( + TypeId::of::(), + T::Storage::STORAGE_TYPE, + self.entity, + self.location, + ) + .map(|(value, ticks)| Mut { + // SAFETY: + // - returned component is of type T + // - Caller guarantees that this reference will not alias. + value: value.assert_unique().deref_mut::(), + ticks: TicksMut::from_tick_cells(ticks, last_change_tick, change_tick), + }) } } @@ -179,12 +186,11 @@ impl<'w> EntityRef<'w> { pub fn get_by_id(&self, component_id: ComponentId) -> Option> { let info = self.world.components().get_info(component_id)?; // SAFETY: - // - entity_location is valid, - // - component_id is valid as checked by the line above + // - entity_location and entity are valid + // . component_id is valid as checked by the line above // - the storage type is accurate as checked by the fetched ComponentInfo unsafe { - get_component( - self.world, + self.world.get_component( component_id, info.storage_type(), self.entity, @@ -196,7 +202,9 @@ impl<'w> EntityRef<'w> { impl<'w> From> for EntityRef<'w> { fn from(entity_mut: EntityMut<'w>) -> EntityRef<'w> { - EntityRef::new(entity_mut.world, entity_mut.entity, entity_mut.location) + // SAFETY: the safety invariants on EntityMut and EntityRef are identical + // and EntityMut is promised to be valid by construction. + unsafe { EntityRef::new(entity_mut.world, entity_mut.entity, entity_mut.location) } } } @@ -209,13 +217,20 @@ pub struct EntityMut<'w> { impl<'w> EntityMut<'w> { /// # Safety - /// entity and location _must_ be valid + /// + /// - `entity` must be valid for `world`: the generation should match that of the entity at the same index. + /// - `location` must be sourced from `world`'s `Entities` and must exactly match the location for `entity` + /// + /// The above is trivially satisfied if `location` was sourced from `world.entities().get(entity)`. #[inline] pub(crate) unsafe fn new( world: &'w mut World, entity: Entity, location: EntityLocation, ) -> Self { + debug_assert!(world.entities().contains(entity)); + debug_assert_eq!(world.entities().get(entity), Some(location)); + EntityMut { world, entity, @@ -257,19 +272,19 @@ impl<'w> EntityMut<'w> { #[inline] pub fn get(&self) -> Option<&'_ T> { // SAFETY: - // - lifetimes enforce correct usage of returned borrow - // - entity location and entity is valid - // - returned component is of type T + // - entity location is valid + // - world access is immutable, lifetime tied to `&self` // - the storage type provided is correct for T unsafe { - get_component_with_type( - self.world, - TypeId::of::(), - T::Storage::STORAGE_TYPE, - self.entity, - self.location, - ) - .map(|value| value.deref::()) + self.world + .get_component_with_type( + TypeId::of::(), + T::Storage::STORAGE_TYPE, + self.entity, + self.location, + ) + // SAFETY: returned component is of type T + .map(|value| value.deref::()) } } @@ -284,11 +299,11 @@ impl<'w> EntityMut<'w> { #[inline] pub fn get_change_ticks(&self) -> Option { // SAFETY: - // - entity location and entity is valid + // - entity location is valid + // - world access is immutable, lifetime tied to `&self` // - the storage type provided is correct for T unsafe { - get_ticks_with_type( - self.world, + self.world.get_ticks_with_type( TypeId::of::(), T::Storage::STORAGE_TYPE, self.entity, @@ -305,15 +320,13 @@ impl<'w> EntityMut<'w> { /// compile time.** #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { - if !self.contains_id(component_id) { - return None; - } - let info = self.world.components().get_info(component_id)?; - // SAFETY: Entity location is valid and component_id exists. + // SAFETY: + // - entity location is valid + // - world access is immutable, lifetime tied to `&self` + // - the storage type provided is correct for T unsafe { - get_ticks( - self.world, + self.world.get_ticks( component_id, info.storage_type(), self.entity, @@ -338,21 +351,21 @@ impl<'w> EntityMut<'w> { // - entity location and entity is valid // - returned component is of type T // - the storage type provided is correct for T - get_component_and_ticks_with_type( - self.world, - TypeId::of::(), - T::Storage::STORAGE_TYPE, - self.entity, - self.location, - ) - .map(|(value, ticks)| Mut { - value: value.assert_unique().deref_mut::(), - ticks: Ticks::from_tick_cells( - ticks, - self.world.last_change_tick(), - self.world.read_change_tick(), - ), - }) + self.world + .get_component_and_ticks_with_type( + TypeId::of::(), + T::Storage::STORAGE_TYPE, + self.entity, + self.location, + ) + .map(|(value, ticks)| Mut { + value: value.assert_unique().deref_mut::(), + ticks: TicksMut::from_tick_cells( + ticks, + self.world.last_change_tick(), + self.world.read_change_tick(), + ), + }) } /// Adds a [`Bundle`] of components to the entity. @@ -417,10 +430,13 @@ impl<'w> EntityMut<'w> { let result = unsafe { T::from_components(storages, &mut |storages| { let component_id = bundle_components.next().unwrap(); - // SAFETY: entity location is valid and table row is removed below + // SAFETY: + // - entity location is valid + // - table row is removed below, without dropping the contents + // - `components` comes from the same world as `storages` take_component( - components, storages, + components, removed_components, component_id, entity, @@ -671,8 +687,7 @@ impl<'w> EntityMut<'w> { // - component_id is valid as checked by the line above // - the storage type is accurate as checked by the fetched ComponentInfo unsafe { - get_component( - self.world, + self.world.get_component( component_id, info.storage_type(), self.entity, @@ -697,213 +712,6 @@ impl<'w> EntityMut<'w> { } } -#[inline] -fn fetch_table( - world: &World, - location: EntityLocation, - component_id: ComponentId, -) -> Option<&Column> { - world.storages.tables[location.table_id].get_column(component_id) -} - -#[inline] -fn fetch_sparse_set(world: &World, component_id: ComponentId) -> Option<&ComponentSparseSet> { - world.storages.sparse_sets.get(component_id) -} - -// TODO: move to Storages? -/// Get a raw pointer to a particular [`Component`] on a particular [`Entity`] in the provided [`World`]. -/// -/// # Safety -/// - `location` must be within bounds of the given archetype and table and `entity` must exist inside -/// the archetype and table -/// - `component_id` must be valid -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -#[inline] -pub(crate) unsafe fn get_component( - world: &World, - component_id: ComponentId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option> { - match storage_type { - StorageType::Table => { - let components = fetch_table(world, location, component_id)?; - // SAFETY: archetypes only store valid table_rows and the stored component type is T - Some(components.get_data_unchecked(location.table_row)) - } - StorageType::SparseSet => fetch_sparse_set(world, component_id)?.get(entity), - } -} - -// TODO: move to Storages? -/// Get a raw pointer to the [`ComponentTicks`] of a particular [`Component`] on a particular [`Entity`] in the provided [World]. -/// -/// # Safety -/// - Caller must ensure that `component_id` is valid -/// - `location` must be within bounds of the given archetype and `entity` must exist inside -/// the archetype -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -#[inline] -unsafe fn get_component_and_ticks( - world: &World, - component_id: ComponentId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option<(Ptr<'_>, TickCells<'_>)> { - match storage_type { - StorageType::Table => { - let components = fetch_table(world, location, component_id)?; - // SAFETY: archetypes only store valid table_rows and the stored component type is T - Some(( - components.get_data_unchecked(location.table_row), - TickCells { - added: components.get_added_ticks_unchecked(location.table_row), - changed: components.get_changed_ticks_unchecked(location.table_row), - }, - )) - } - StorageType::SparseSet => fetch_sparse_set(world, component_id)?.get_with_ticks(entity), - } -} - -/// # Safety -/// - `entity_location` must be within bounds of the given archetype and `entity` must exist inside -/// the archetype -/// - `component_id` must be valid -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -unsafe fn get_ticks( - world: &World, - component_id: ComponentId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option { - match storage_type { - StorageType::Table => { - let components = fetch_table(world, location, component_id)?; - // SAFETY: archetypes only store valid table_rows and the stored component type is T - Some(components.get_ticks_unchecked(location.table_row)) - } - StorageType::SparseSet => fetch_sparse_set(world, component_id)?.get_ticks(entity), - } -} - -// TODO: move to Storages? -/// Moves component data out of storage. -/// -/// This function leaves the underlying memory unchanged, but the component behind -/// returned pointer is semantically owned by the caller and will not be dropped in its original location. -/// Caller is responsible to drop component data behind returned pointer. -/// -/// # Safety -/// - `location` must be within bounds of the given archetype and table and `entity` must exist inside the archetype -/// and table. -/// - `component_id` must be valid -/// - The relevant table row **must be removed** by the caller once all components are taken -#[inline] -unsafe fn take_component<'a>( - components: &Components, - storages: &'a mut Storages, - removed_components: &mut SparseSet>, - component_id: ComponentId, - entity: Entity, - location: EntityLocation, -) -> OwningPtr<'a> { - let component_info = components.get_info_unchecked(component_id); - let removed_components = removed_components.get_or_insert_with(component_id, Vec::new); - removed_components.push(entity); - match component_info.storage_type() { - StorageType::Table => { - let table = &mut storages.tables[location.table_id]; - // SAFETY: archetypes will always point to valid columns - let components = table.get_column_mut(component_id).unwrap(); - // SAFETY: archetypes only store valid table_rows and the stored component type is T - components - .get_data_unchecked_mut(location.table_row) - .promote() - } - StorageType::SparseSet => storages - .sparse_sets - .get_mut(component_id) - .unwrap() - .remove_and_forget(entity) - .unwrap(), - } -} - -/// Get a raw pointer to a particular [`Component`] by [`TypeId`] on a particular [`Entity`] in the provided [`World`]. -/// -/// # Safety -/// - `entity_location` must be within bounds of the given archetype and `entity` must exist inside -/// the archetype -/// - `type_id` must be correspond to a type that implements [`Component`] -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -#[inline] -unsafe fn get_component_with_type( - world: &World, - type_id: TypeId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option> { - get_component( - world, - world.components.get_id(type_id)?, - storage_type, - entity, - location, - ) -} - -/// Get a raw pointer to the [`ComponentTicks`] of a particular [`Component`] by [`TypeId`] on a particular [`Entity`] in the provided [`World`]. -/// -/// # Safety -/// - `entity_location` must be within bounds of the given archetype and `entity` must exist inside -/// the archetype -/// - `type_id` must be correspond to a type that implements [`Component`] -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -#[inline] -unsafe fn get_component_and_ticks_with_type( - world: &World, - type_id: TypeId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option<(Ptr<'_>, TickCells<'_>)> { - get_component_and_ticks( - world, - world.components.get_id(type_id)?, - storage_type, - entity, - location, - ) -} - -/// # Safety -/// - `entity_location` must be within bounds of the given archetype and `entity` must exist inside -/// the archetype -/// - `type_id` must be correspond to a type that implements [`Component`] -/// - `storage_type` must accurately reflect where the components for `component_id` are stored. -#[inline] -unsafe fn get_ticks_with_type( - world: &World, - type_id: TypeId, - storage_type: StorageType, - entity: Entity, - location: EntityLocation, -) -> Option { - get_ticks( - world, - world.components.get_id(type_id)?, - storage_type, - entity, - location, - ) -} - fn contains_component_with_type(world: &World, type_id: TypeId, location: EntityLocation) -> bool { if let Some(component_id) = world.components.get_id(type_id) { contains_component_with_id(world, component_id, location) @@ -1044,21 +852,26 @@ pub(crate) unsafe fn get_mut( entity: Entity, location: EntityLocation, ) -> Option> { - // SAFETY: world access is unique, entity location is valid, and returned component is of type - // T let change_tick = world.change_tick(); let last_change_tick = world.last_change_tick(); - get_component_and_ticks_with_type( - world, - TypeId::of::(), - T::Storage::STORAGE_TYPE, - entity, - location, - ) - .map(|(value, ticks)| Mut { - value: value.assert_unique().deref_mut::(), - ticks: Ticks::from_tick_cells(ticks, last_change_tick, change_tick), - }) + // SAFETY: + // - world access is unique + // - entity location is valid + // - and returned component is of type T + world + .get_component_and_ticks_with_type( + TypeId::of::(), + T::Storage::STORAGE_TYPE, + entity, + location, + ) + .map(|(value, ticks)| Mut { + // SAFETY: + // - world access is unique and ties world lifetime to `Mut` lifetime + // - `value` is of type `T` + value: value.assert_unique().deref_mut::(), + ticks: TicksMut::from_tick_cells(ticks, last_change_tick, change_tick), + }) } // SAFETY: EntityLocation must be valid, component_id must be valid @@ -1068,16 +881,66 @@ pub(crate) unsafe fn get_mut_by_id( entity: Entity, location: EntityLocation, component_id: ComponentId, -) -> Option { +) -> Option> { let change_tick = world.change_tick(); + // SAFETY: component_id is valid let info = world.components.get_info_unchecked(component_id); - // SAFETY: world access is unique, entity location and component_id required to be valid - get_component_and_ticks(world, component_id, info.storage_type(), entity, location).map( - |(value, ticks)| MutUntyped { + // SAFETY: + // - world access is unique + // - entity location is valid + // - and returned component is of type T + world + .get_component_and_ticks(component_id, info.storage_type(), entity, location) + .map(|(value, ticks)| MutUntyped { + // SAFETY: world access is unique and ties world lifetime to `MutUntyped` lifetime value: value.assert_unique(), - ticks: Ticks::from_tick_cells(ticks, world.last_change_tick(), change_tick), - }, - ) + ticks: TicksMut::from_tick_cells(ticks, world.last_change_tick(), change_tick), + }) +} + +/// Moves component data out of storage. +/// +/// This function leaves the underlying memory unchanged, but the component behind +/// returned pointer is semantically owned by the caller and will not be dropped in its original location. +/// Caller is responsible to drop component data behind returned pointer. +/// +/// # Safety +/// - `location.table_row` must be in bounds of column of component id `component_id` +/// - `component_id` must be valid +/// - `components` must come from the same world as `self` +/// - The relevant table row **must be removed** by the caller once all components are taken, without dropping the value +#[inline] +pub(crate) unsafe fn take_component<'a>( + storages: &'a mut Storages, + components: &Components, + removed_components: &mut SparseSet>, + component_id: ComponentId, + entity: Entity, + location: EntityLocation, +) -> OwningPtr<'a> { + // SAFETY: caller promises component_id to be valid + let component_info = components.get_info_unchecked(component_id); + let removed_components = removed_components.get_or_insert_with(component_id, Vec::new); + removed_components.push(entity); + match component_info.storage_type() { + StorageType::Table => { + let table = &mut storages.tables[location.table_id]; + let components = table.get_column_mut(component_id).unwrap(); + // SAFETY: + // - archetypes only store valid table_rows + // - index is in bounds as promised by caller + // - promote is safe because the caller promises to remove the table row without dropping it immediately afterwards + components + .get_data_unchecked_mut(location.table_row) + .promote() + } + StorageType::SparseSet => storages + .sparse_sets + .get_mut(component_id) + .unwrap() + .remove_and_forget(entity) + .unwrap(), + } } #[cfg(test)] diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index 6e2b8efc0f222..c65da1b81a7e1 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,6 +1,7 @@ +use crate::storage::SparseSetIndex; use std::sync::atomic::{AtomicUsize, Ordering}; -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] // We use usize here because that is the largest `Atomic` we want to require /// A unique identifier for a [`super::World`]. // Note that this *is* used by external crates as well as for internal safety checks @@ -26,6 +27,17 @@ impl WorldId { } } +impl SparseSetIndex for WorldId { + #[inline] + fn sparse_set_index(&self) -> usize { + self.0 + } + + fn get_sparse_set_index(value: usize) -> Self { + Self(value) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index e4569f6be5220..dd09a5341aaa4 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -2,22 +2,24 @@ mod entity_ref; mod spawn_batch; mod world_cell; -pub use crate::change_detection::Mut; -pub use entity_ref::*; +pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; +pub use entity_ref::{EntityMut, EntityRef}; pub use spawn_batch::*; pub use world_cell::*; use crate::{ archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, bundle::{Bundle, BundleInserter, BundleSpawner, Bundles}, - change_detection::{MutUntyped, Ticks}, + change_detection::{MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentId, ComponentInfo, Components, TickCells, + Component, ComponentDescriptor, ComponentId, ComponentInfo, ComponentTicks, Components, + StorageType, TickCells, }, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, event::{Event, Events}, - query::{QueryState, ReadOnlyWorldQuery, WorldQuery}, - storage::{ResourceData, SparseSet, Storages}, + ptr::UnsafeCellDeref, + query::{DebugCheckedUnwrap, QueryState, ReadOnlyWorldQuery, WorldQuery}, + storage::{Column, ComponentSparseSet, ResourceData, SparseSet, Storages, TableRow}, system::Resource, }; use bevy_ptr::{OwningPtr, Ptr}; @@ -60,9 +62,9 @@ pub struct World { pub(crate) removed_components: SparseSet>, /// Access cache used by [WorldCell]. pub(crate) archetype_component_access: ArchetypeComponentAccess, - main_thread_validator: MainThreadValidator, pub(crate) change_tick: AtomicU32, pub(crate) last_change_tick: u32, + pub(crate) last_check_tick: u32, } impl Default for World { @@ -76,11 +78,11 @@ impl Default for World { bundles: Default::default(), removed_components: Default::default(), archetype_component_access: Default::default(), - main_thread_validator: Default::default(), // Default value is `1`, and `last_change_tick`s default to `0`, such that changes // are detected on first system runs and for direct world queries. change_tick: AtomicU32::new(1), last_change_tick: 0, + last_check_tick: 0, } } } @@ -317,7 +319,10 @@ impl World { #[inline] pub fn get_entity(&self, entity: Entity) -> Option { let location = self.entities.get(entity)?; - Some(EntityRef::new(self, entity, location)) + // SAFETY: if the Entity is invalid, the function returns early. + // Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists. + let entity_ref = unsafe { EntityRef::new(self, entity, location) }; + Some(entity_ref) } /// Returns an [`Entity`] iterator of current entities. @@ -331,13 +336,16 @@ impl World { .iter() .enumerate() .map(|(archetype_row, archetype_entity)| { + let entity = archetype_entity.entity(); let location = EntityLocation { archetype_id: archetype.id(), archetype_row: ArchetypeRow::new(archetype_row), table_id: archetype.table_id(), table_row: archetype_entity.table_row(), }; - EntityRef::new(self, archetype_entity.entity(), location) + + // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored + unsafe { EntityRef::new(self, entity, location) } }) }) } @@ -565,8 +573,10 @@ impl World { /// ``` #[inline] pub fn get_mut(&mut self, entity: Entity) -> Option> { - // SAFETY: lifetimes enforce correct usage of returned borrow - unsafe { get_mut(self, entity, self.get_entity(entity)?.location()) } + // SAFETY: + // - lifetimes enforce correct usage of returned borrow + // - entity location is checked in `get_entity` + unsafe { entity_ref::get_mut(self, entity, self.get_entity(entity)?.location()) } } /// Despawns the given `entity`, if it exists. This will also remove all of the entity's @@ -759,19 +769,31 @@ impl World { } } - /// Inserts a new resource with standard starting values. + /// Initializes a new resource and returns the [`ComponentId`] created for it. /// /// If the resource already exists, nothing happens. /// /// The value given by the [`FromWorld::from_world`] method will be used. - /// Note that any resource with the `Default` trait automatically implements `FromWorld`, + /// Note that any resource with the [`Default`] trait automatically implements [`FromWorld`], /// and those default values will be here instead. #[inline] - pub fn init_resource(&mut self) { - if !self.contains_resource::() { - let resource = R::from_world(self); - self.insert_resource(resource); + pub fn init_resource(&mut self) -> ComponentId { + let component_id = self.components.init_resource::(); + if self + .storages + .resources + .get(component_id) + .map_or(true, |data| !data.is_present()) + { + let value = R::from_world(self); + OwningPtr::make(value, |ptr| { + // SAFETY: component_id was just initialized and corresponds to resource of type R. + unsafe { + self.insert_resource_by_id(component_id, ptr); + } + }); } + component_id } /// Inserts a new resource with the given `value`. @@ -783,14 +805,14 @@ impl World { pub fn insert_resource(&mut self, value: R) { let component_id = self.components.init_resource::(); OwningPtr::make(value, |ptr| { - // SAFETY: component_id was just initialized and corresponds to resource of type R + // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { self.insert_resource_by_id(component_id, ptr); } }); } - /// Inserts a new non-send resource with standard starting values. + /// Initializes a new non-send resource and returns the [`ComponentId`] created for it. /// /// If the resource already exists, nothing happens. /// @@ -802,11 +824,23 @@ impl World { /// /// Panics if called from a thread other than the main thread. #[inline] - pub fn init_non_send_resource(&mut self) { - if !self.contains_resource::() { - let resource = R::from_world(self); - self.insert_non_send_resource(resource); + pub fn init_non_send_resource(&mut self) -> ComponentId { + let component_id = self.components.init_non_send::(); + if self + .storages + .non_send_resources + .get(component_id) + .map_or(true, |data| !data.is_present()) + { + let value = R::from_world(self); + OwningPtr::make(value, |ptr| { + // SAFETY: component_id was just initialized and corresponds to resource of type R. + unsafe { + self.insert_non_send_by_id(component_id, ptr); + } + }); } + component_id } /// Inserts a new non-send resource with the given `value`. @@ -816,16 +850,15 @@ impl World { /// Systems with `NonSend` resources are always scheduled on the main thread. /// /// # Panics - /// - /// Panics if called from a thread other than the main thread. + /// If a value is already present, this function will panic if called + /// from a different thread than where the original value was inserted from. #[inline] pub fn insert_non_send_resource(&mut self, value: R) { - self.validate_non_send_access::(); let component_id = self.components.init_non_send::(); OwningPtr::make(value, |ptr| { - // SAFETY: component_id was just initialized and corresponds to resource of type R + // SAFETY: component_id was just initialized and corresponds to resource of type R. unsafe { - self.insert_resource_by_id(component_id, ptr); + self.insert_non_send_by_id(component_id, ptr); } }); } @@ -833,37 +866,51 @@ impl World { /// Removes the resource of a given type and returns it, if it exists. Otherwise returns [None]. #[inline] pub fn remove_resource(&mut self) -> Option { - // SAFETY: R is Send + Sync - unsafe { self.remove_resource_unchecked() } + let component_id = self.components.get_resource_id(TypeId::of::())?; + let (ptr, _) = self.storages.resources.get_mut(component_id)?.remove()?; + // SAFETY: `component_id` was gotten via looking up the `R` type + unsafe { Some(ptr.read::()) } } + /// Removes a `!Send` resource from the world and returns it, if present. + /// + /// `NonSend` resources cannot be sent across threads, + /// and do not need the `Send + Sync` bounds. + /// Systems with `NonSend` resources are always scheduled on the main thread. + /// + /// Returns `None` if a value was not previously present. + /// + /// # Panics + /// If a value is present, this function will panic if called from a different + /// thread than where the value was inserted from. #[inline] pub fn remove_non_send_resource(&mut self) -> Option { - self.validate_non_send_access::(); - // SAFETY: we are on main thread - unsafe { self.remove_resource_unchecked() } + let component_id = self.components.get_resource_id(TypeId::of::())?; + let (ptr, _) = self + .storages + .non_send_resources + .get_mut(component_id)? + .remove()?; + // SAFETY: `component_id` was gotten via looking up the `R` type + unsafe { Some(ptr.read::()) } } + /// Returns `true` if a resource of type `R` exists. Otherwise returns `false`. #[inline] - /// # Safety - /// Only remove `NonSend` resources from the main thread - /// as they cannot be sent across threads - #[allow(unused_unsafe)] - pub unsafe fn remove_resource_unchecked(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; - // SAFETY: the resource is of type R and the value is returned back to the caller. - unsafe { - let (ptr, _) = self.storages.resources.get_mut(component_id)?.remove()?; - Some(ptr.read::()) - } + pub fn contains_resource(&self) -> bool { + self.components + .get_resource_id(TypeId::of::()) + .and_then(|component_id| self.storages.resources.get(component_id)) + .map(|info| info.is_present()) + .unwrap_or(false) } /// Returns `true` if a resource of type `R` exists. Otherwise returns `false`. #[inline] - pub fn contains_resource(&self) -> bool { + pub fn contains_non_send(&self) -> bool { self.components .get_resource_id(TypeId::of::()) - .and_then(|component_id| self.storages.resources.get(component_id)) + .and_then(|component_id| self.storages.non_send_resources.get(component_id)) .map(|info| info.is_present()) .unwrap_or(false) } @@ -947,7 +994,6 @@ impl World { unsafe { self.get_resource_unchecked_mut() } } - // PERF: optimize this to avoid redundant lookups /// Gets a mutable reference to the resource of type `T` if it exists, /// otherwise inserts the resource using the result of calling `func`. #[inline] @@ -955,10 +1001,27 @@ impl World { &mut self, func: impl FnOnce() -> R, ) -> Mut<'_, R> { - if !self.contains_resource::() { - self.insert_resource(func()); + let change_tick = self.change_tick(); + let last_change_tick = self.last_change_tick(); + + let component_id = self.components.init_resource::(); + let data = self.initialize_resource_internal(component_id); + if !data.is_present() { + OwningPtr::make(func(), |ptr| { + // SAFETY: component_id was just initialized and corresponds to resource of type R. + unsafe { + data.insert(ptr, change_tick); + } + }); } - self.resource_mut() + + // SAFETY: The resource must be present, as we would have inserted it if it was empty. + let data = unsafe { + data.get_mut(last_change_tick, change_tick) + .debug_checked_unwrap() + }; + // SAFETY: The underlying type of the resource is `R`. + unsafe { data.with_type::() } } /// Gets a mutable reference to the resource of the given type, if it exists @@ -979,6 +1042,8 @@ impl World { /// /// Panics if the resource does not exist. /// Use [`get_non_send_resource`](World::get_non_send_resource) instead if you want to handle this case. + /// + /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] #[track_caller] pub fn non_send_resource(&self) -> &R { @@ -999,6 +1064,8 @@ impl World { /// /// Panics if the resource does not exist. /// Use [`get_non_send_resource_mut`](World::get_non_send_resource_mut) instead if you want to handle this case. + /// + /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] #[track_caller] pub fn non_send_resource_mut(&mut self) -> Mut<'_, R> { @@ -1014,7 +1081,10 @@ impl World { } /// Gets a reference to the non-send resource of the given type, if it exists. - /// Otherwise returns [None] + /// Otherwise returns [None]. + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] pub fn get_non_send_resource(&self) -> Option<&R> { let component_id = self.components.get_resource_id(TypeId::of::())?; @@ -1024,6 +1094,9 @@ impl World { /// Gets a mutable reference to the non-send resource of the given type, if it exists. /// Otherwise returns [None] + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] pub fn get_non_send_resource_mut(&mut self) -> Option> { // SAFETY: unique world access @@ -1033,6 +1106,9 @@ impl World { /// Gets a mutable reference to the non-send resource of the given type, if it exists. /// Otherwise returns [None] /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + /// /// # Safety /// This will allow aliased mutable access to the given non-send resource type. The caller must /// ensure that there is either only one mutable access or multiple immutable accesses at a time. @@ -1051,6 +1127,21 @@ impl World { self.storages.resources.get(component_id)?.get_with_ticks() } + // Shorthand helper function for getting the data and change ticks for a resource. + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + pub(crate) fn get_non_send_with_ticks( + &self, + component_id: ComponentId, + ) -> Option<(Ptr<'_>, TickCells<'_>)> { + self.storages + .non_send_resources + .get(component_id)? + .get_with_ticks() + } + // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. #[inline] pub(crate) fn get_resource_archetype_component_id( @@ -1061,6 +1152,16 @@ impl World { Some(resource.id()) } + // Shorthand helper function for getting the [`ArchetypeComponentId`] for a resource. + #[inline] + pub(crate) fn get_non_send_archetype_component_id( + &self, + component_id: ComponentId, + ) -> Option { + let resource = self.storages.non_send_resources.get(component_id)?; + Some(resource.id()) + } + /// For a given batch of ([Entity], [Bundle]) pairs, either spawns each [Entity] with the given /// bundle (if the entity does not exist), or inserts the [Bundle] (if the entity already exists). /// This is faster than doing equivalent operations one-by-one. @@ -1207,13 +1308,7 @@ impl World { /// }); /// assert_eq!(world.get_resource::().unwrap().0, 2); /// ``` - pub fn resource_scope< - R: 'static, /* The resource doesn't need to be Send nor Sync. */ - U, - >( - &mut self, - f: impl FnOnce(&mut World, Mut) -> U, - ) -> U { + pub fn resource_scope(&mut self, f: impl FnOnce(&mut World, Mut) -> U) -> U { let last_change_tick = self.last_change_tick(); let change_tick = self.change_tick(); @@ -1222,24 +1317,18 @@ impl World { .get_resource_id(TypeId::of::()) .unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::())); // If the resource isn't send and sync, validate that we are on the main thread, so that we can access it. - let component_info = self.components().get_info(component_id).unwrap(); - if !component_info.is_send_and_sync() { - self.validate_non_send_access::(); - } - let (ptr, mut ticks) = self .storages .resources .get_mut(component_id) - // SAFETY: The type R is Send and Sync or we've already validated that we're on the main thread. - .and_then(|info| unsafe { info.remove() }) + .and_then(|info| info.remove()) .unwrap_or_else(|| panic!("resource does not exist: {}", std::any::type_name::())); // Read the value onto the stack to avoid potential mut aliasing. // SAFETY: pointer is of type R let mut value = unsafe { ptr.read::() }; let value_mut = Mut { value: &mut value, - ticks: Ticks { + ticks: TicksMut { added: &mut ticks.added, changed: &mut ticks.changed, last_change_tick, @@ -1320,7 +1409,11 @@ impl World { let (ptr, ticks) = self.get_resource_with_ticks(component_id)?; Some(Mut { value: ptr.assert_unique().deref_mut(), - ticks: Ticks::from_tick_cells(ticks, self.last_change_tick(), self.read_change_tick()), + ticks: TicksMut::from_tick_cells( + ticks, + self.last_change_tick(), + self.read_change_tick(), + ), }) } @@ -1331,8 +1424,13 @@ impl World { &self, component_id: ComponentId, ) -> Option<&R> { - self.validate_non_send_access::(); - self.get_resource_with_id(component_id) + Some( + self.storages + .non_send_resources + .get(component_id)? + .get_data()? + .deref::(), + ) } /// # Safety @@ -1343,8 +1441,20 @@ impl World { &self, component_id: ComponentId, ) -> Option> { - self.validate_non_send_access::(); - self.get_resource_unchecked_mut_with_id(component_id) + let (ptr, ticks) = self + .storages + .non_send_resources + .get(component_id)? + .get_with_ticks()?; + Some(Mut { + value: ptr.assert_unique().deref_mut(), + ticks: TicksMut { + added: ticks.added.deref_mut(), + changed: ticks.changed.deref_mut(), + last_change_tick: self.last_change_tick(), + change_tick: self.read_change_tick(), + }, + }) } /// Inserts a new resource with the given `value`. Will replace the value if it already existed. @@ -1353,8 +1463,7 @@ impl World { /// use this in cases where the actual types are not known at compile time.** /// /// # Safety - /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world - /// `component_id` must exist in this [`World`] + /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world. #[inline] pub unsafe fn insert_resource_by_id( &mut self, @@ -1363,18 +1472,43 @@ impl World { ) { let change_tick = self.change_tick(); - // SAFETY: component_id is valid, ensured by caller + // SAFETY: value is valid for component_id, ensured by caller self.initialize_resource_internal(component_id) .insert(value, change_tick); } + /// Inserts a new `!Send` resource with the given `value`. Will replace the value if it already + /// existed. + /// + /// **You should prefer to use the typed API [`World::insert_non_send_resource`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// If a value is already present, this function will panic if not called from the same + /// thread that the original value was inserted from. + /// /// # Safety - /// `component_id` must be valid for this world + /// The value referenced by `value` must be valid for the given [`ComponentId`] of this world. + #[inline] + pub unsafe fn insert_non_send_by_id( + &mut self, + component_id: ComponentId, + value: OwningPtr<'_>, + ) { + let change_tick = self.change_tick(); + + // SAFETY: value is valid for component_id, ensured by caller + self.initialize_non_send_internal(component_id) + .insert(value, change_tick); + } + + /// # Panics + /// Panics if `component_id` is not registered as a `Send` component type in this `World` #[inline] - unsafe fn initialize_resource_internal( + fn initialize_resource_internal( &mut self, component_id: ComponentId, - ) -> &mut ResourceData { + ) -> &mut ResourceData { let archetype_component_count = &mut self.archetypes.archetype_component_count; self.storages .resources @@ -1385,36 +1519,35 @@ impl World { }) } + /// # Panics + /// panics if `component_id` is not registered in this world + #[inline] + fn initialize_non_send_internal( + &mut self, + component_id: ComponentId, + ) -> &mut ResourceData { + let archetype_component_count = &mut self.archetypes.archetype_component_count; + self.storages + .non_send_resources + .initialize_with(component_id, &self.components, || { + let id = ArchetypeComponentId::new(*archetype_component_count); + *archetype_component_count += 1; + id + }) + } + pub(crate) fn initialize_resource(&mut self) -> ComponentId { let component_id = self.components.init_resource::(); - // SAFETY: resource initialized above - unsafe { self.initialize_resource_internal(component_id) }; + self.initialize_resource_internal(component_id); component_id } pub(crate) fn initialize_non_send_resource(&mut self) -> ComponentId { let component_id = self.components.init_non_send::(); - // SAFETY: resource initialized above - unsafe { self.initialize_resource_internal(component_id) }; + self.initialize_non_send_internal(component_id); component_id } - pub(crate) fn validate_non_send_access(&self) { - assert!( - self.main_thread_validator.is_main_thread(), - "attempted to access NonSend resource {} off of the main thread", - std::any::type_name::(), - ); - } - - pub(crate) fn validate_non_send_access_untyped(&self, name: &str) { - assert!( - self.main_thread_validator.is_main_thread(), - "attempted to access NonSend resource {} off of the main thread", - name - ); - } - /// Empties queued entities and adds them to the empty [Archetype](crate::archetype::Archetype). /// This should be called before doing operations that might operate on queued entities, /// such as inserting a [Component]. @@ -1460,21 +1593,65 @@ impl World { self.last_change_tick } + /// Iterates all component change ticks and clamps any older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). + /// This prevents overflow and thus prevents false positives. + /// + /// **Note:** Does nothing if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`](crate::change_detection::CHECK_TICK_THRESHOLD) + /// times since the previous pass. + // TODO: benchmark and optimize pub fn check_change_ticks(&mut self) { - // Iterate over all component change ticks, clamping their age to max age - // PERF: parallelize let change_tick = self.change_tick(); - self.storages.tables.check_change_ticks(change_tick); - self.storages.sparse_sets.check_change_ticks(change_tick); - self.storages.resources.check_change_ticks(change_tick); + if change_tick.wrapping_sub(self.last_check_tick) < CHECK_TICK_THRESHOLD { + return; + } + + let Storages { + ref mut tables, + ref mut sparse_sets, + ref mut resources, + ref mut non_send_resources, + } = self.storages; + + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("check component ticks").entered(); + tables.check_change_ticks(change_tick); + sparse_sets.check_change_ticks(change_tick); + resources.check_change_ticks(change_tick); + non_send_resources.check_change_ticks(change_tick); + + if let Some(mut schedules) = self.get_resource_mut::() { + schedules.check_change_ticks(change_tick); + } + + self.last_check_tick = change_tick; + } + + /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), + /// invalidating all [`Entity`] and resource fetches such as [`Res`](crate::system::Res), [`ResMut`](crate::system::ResMut) + pub fn clear_all(&mut self) { + self.clear_entities(); + self.clear_resources(); } + /// Despawns all entities in this [`World`]. pub fn clear_entities(&mut self) { self.storages.tables.clear(); self.storages.sparse_sets.clear(); self.archetypes.clear_entities(); self.entities.clear(); } + + /// Clears all resources in this [`World`]. + /// + /// **Note:** Any resource fetch to this [World] will fail unless they are re-initialized, + /// including engine-internal resources that are only initialized on app/world construction. + /// + /// This can easily cause systems expecting certain resources to immediately start panicking. + /// Use with caution. + pub fn clear_resources(&mut self) { + self.storages.resources.clear(); + self.storages.non_send_resources.clear(); + } } impl World { @@ -1486,10 +1663,6 @@ impl World { /// use this in cases where the actual types are not known at compile time.** #[inline] pub fn get_resource_by_id(&self, component_id: ComponentId) -> Option> { - let info = self.components.get_info(component_id)?; - if !info.is_send_and_sync() { - self.validate_non_send_access_untyped(info.name()); - } self.storages.resources.get(component_id)?.get_data() } @@ -1501,19 +1674,58 @@ impl World { /// use this in cases where the actual types are not known at compile time.** #[inline] pub fn get_resource_mut_by_id(&mut self, component_id: ComponentId) -> Option> { - let info = self.components.get_info(component_id)?; - if !info.is_send_and_sync() { - self.validate_non_send_access_untyped(info.name()); - } - let change_tick = self.change_tick(); - let (ptr, ticks) = self.get_resource_with_ticks(component_id)?; - // SAFETY: This function has exclusive access to the world so nothing aliases `ticks`. - // - index is in-bounds because the column is initialized and non-empty - // - no other reference to the ticks of the same row can exist at the same time - let ticks = unsafe { Ticks::from_tick_cells(ticks, self.last_change_tick(), change_tick) }; + let ticks = + // SAFETY: This function has exclusive access to the world so nothing aliases `ticks`. + // - index is in-bounds because the column is initialized and non-empty + // - no other reference to the ticks of the same row can exist at the same time + unsafe { TicksMut::from_tick_cells(ticks, self.last_change_tick(), change_tick) }; + + Some(MutUntyped { + // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. + value: unsafe { ptr.assert_unique() }, + ticks, + }) + } + + /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. + /// The returned pointer must not be used to modify the resource, and must not be + /// dereferenced after the immutable borrow of the [`World`] ends. + /// + /// **You should prefer to use the typed API [`World::get_resource`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + pub fn get_non_send_by_id(&self, component_id: ComponentId) -> Option> { + self.storages + .non_send_resources + .get(component_id)? + .get_data() + } + + /// Gets a `!Send` resource to the resource with the id [`ComponentId`] if it exists. + /// The returned pointer may be used to modify the resource, as long as the mutable borrow + /// of the [`World`] is still valid. + /// + /// **You should prefer to use the typed API [`World::get_resource_mut`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + #[inline] + pub fn get_non_send_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + let change_tick = self.change_tick(); + let (ptr, ticks) = self.get_non_send_with_ticks(component_id)?; + + let ticks = + // SAFETY: This function has exclusive access to the world so nothing aliases `ticks`. + // - index is in-bounds because the column is initialized and non-empty + // - no other reference to the ticks of the same row can exist at the same time + unsafe { TicksMut::from_tick_cells(ticks, self.last_change_tick(), change_tick) }; Some(MutUntyped { // SAFETY: This function has exclusive access to the world so nothing aliases `ptr`. @@ -1527,17 +1739,25 @@ impl World { /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only /// use this in cases where the actual types are not known at compile time.** pub fn remove_resource_by_id(&mut self, component_id: ComponentId) -> Option<()> { - let info = self.components.get_info(component_id)?; - if !info.is_send_and_sync() { - self.validate_non_send_access_untyped(info.name()); - } - // SAFETY: The underlying type is Send and Sync or we've already validated we're on the main thread - unsafe { - self.storages - .resources - .get_mut(component_id)? - .remove_and_drop(); - } + self.storages + .resources + .get_mut(component_id)? + .remove_and_drop(); + Some(()) + } + + /// Removes the resource of a given type, if it exists. Otherwise returns [None]. + /// + /// **You should prefer to use the typed API [`World::remove_resource`] where possible and only + /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. + pub fn remove_non_send_by_id(&mut self, component_id: ComponentId) -> Option<()> { + self.storages + .non_send_resources + .get_mut(component_id)? + .remove_and_drop(); Some(()) } @@ -1546,6 +1766,9 @@ impl World { /// /// **You should prefer to use the typed API [`World::get_mut`] where possible and only /// use this in cases where the actual types are not known at compile time.** + /// + /// # Panics + /// This function will panic if it isn't called from the same thread that the resource was inserted from. #[inline] pub fn get_by_id(&self, entity: Entity, component_id: ComponentId) -> Option> { let info = self.components().get_info(component_id)?; @@ -1554,8 +1777,7 @@ impl World { // - component_id is valid as checked by the line above // - the storage type is accurate as checked by the fetched ComponentInfo unsafe { - get_component( - self, + self.get_component( component_id, info.storage_type(), entity, @@ -1578,7 +1800,7 @@ impl World { self.components().get_info(component_id)?; // SAFETY: entity_location is valid, component_id is valid as checked by the line above unsafe { - get_mut_by_id( + entity_ref::get_mut_by_id( self, entity, self.get_entity(entity)?.location(), @@ -1588,6 +1810,170 @@ impl World { } } +impl World { + /// Get a raw pointer to a particular [`Component`](crate::component::Component) and its [`ComponentTicks`] identified by their [`TypeId`] + /// + /// # Safety + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - `location` must refer to an archetype that contains `entity` + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_component_and_ticks_with_type( + &self, + type_id: TypeId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option<(Ptr<'_>, TickCells<'_>)> { + let component_id = self.components.get_id(type_id)?; + // SAFETY: component_id is valid, the rest is deferred to caller + self.get_component_and_ticks(component_id, storage_type, entity, location) + } + + /// Get a raw pointer to a particular [`Component`](crate::component::Component) and its [`ComponentTicks`] + /// + /// # Safety + /// - `location` must refer to an archetype that contains `entity` + /// - `component_id` must be valid + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_component_and_ticks( + &self, + component_id: ComponentId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option<(Ptr<'_>, TickCells<'_>)> { + match storage_type { + StorageType::Table => { + let (components, table_row) = self.fetch_table(location, component_id)?; + + // SAFETY: archetypes only store valid table_rows and caller ensure aliasing rules + Some(( + components.get_data_unchecked(table_row), + TickCells { + added: components.get_added_ticks_unchecked(table_row), + changed: components.get_changed_ticks_unchecked(table_row), + }, + )) + } + StorageType::SparseSet => self.fetch_sparse_set(component_id)?.get_with_ticks(entity), + } + } + + /// Get a raw pointer to a particular [`Component`](crate::component::Component) on a particular [`Entity`], identified by the component's type + /// + /// # Safety + /// - `location` must refer to an archetype that contains `entity` + /// the archetype + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_component_with_type( + &self, + type_id: TypeId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option> { + let component_id = self.components.get_id(type_id)?; + // SAFETY: component_id is valid, the rest is deferred to caller + self.get_component(component_id, storage_type, entity, location) + } + + /// Get a raw pointer to a particular [`Component`](crate::component::Component) on a particular [`Entity`] in the provided [`World`](crate::world::World). + /// + /// # Safety + /// - `location` must refer to an archetype that contains `entity` + /// the archetype + /// - `component_id` must be valid + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_component( + &self, + component_id: ComponentId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option> { + // SAFETY: component_id exists and is therefore valid + match storage_type { + StorageType::Table => { + let (components, table_row) = self.fetch_table(location, component_id)?; + // SAFETY: archetypes only store valid table_rows and caller ensure aliasing rules + Some(components.get_data_unchecked(table_row)) + } + StorageType::SparseSet => self.fetch_sparse_set(component_id)?.get(entity), + } + } + + /// Get a raw pointer to the [`ComponentTicks`] on a particular [`Entity`], identified by the component's [`TypeId`] + /// + /// # Safety + /// - `location` must refer to an archetype that contains `entity` + /// the archetype + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_ticks_with_type( + &self, + type_id: TypeId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option { + let component_id = self.components.get_id(type_id)?; + // SAFETY: component_id is valid, the rest is deferred to caller + self.get_ticks(component_id, storage_type, entity, location) + } + + /// Get a raw pointer to the [`ComponentTicks`] on a particular [`Entity`] + /// + /// # Safety + /// - `location` must refer to an archetype that contains `entity` + /// the archetype + /// - `component_id` must be valid + /// - `storage_type` must accurately reflect where the components for `component_id` are stored. + /// - the caller must ensure that no aliasing rules are violated + #[inline] + pub(crate) unsafe fn get_ticks( + &self, + component_id: ComponentId, + storage_type: StorageType, + entity: Entity, + location: EntityLocation, + ) -> Option { + match storage_type { + StorageType::Table => { + let (components, table_row) = self.fetch_table(location, component_id)?; + // SAFETY: archetypes only store valid table_rows and caller ensure aliasing rules + Some(components.get_ticks_unchecked(table_row)) + } + StorageType::SparseSet => self.fetch_sparse_set(component_id)?.get_ticks(entity), + } + } + + #[inline] + fn fetch_table( + &self, + location: EntityLocation, + component_id: ComponentId, + ) -> Option<(&Column, TableRow)> { + let archetype = &self.archetypes[location.archetype_id]; + let table = &self.storages.tables[archetype.table_id()]; + let components = table.get_column(component_id)?; + let table_row = archetype.entity_table_row(location.archetype_row); + Some((components, table_row)) + } + + #[inline] + fn fetch_sparse_set(&self, component_id: ComponentId) -> Option<&ComponentSparseSet> { + self.storages.sparse_sets.get(component_id) + } +} + impl fmt::Debug for World { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("World") @@ -1622,29 +2008,11 @@ impl FromWorld for T { } } -struct MainThreadValidator { - main_thread: std::thread::ThreadId, -} - -impl MainThreadValidator { - fn is_main_thread(&self) -> bool { - self.main_thread == std::thread::current().id() - } -} - -impl Default for MainThreadValidator { - fn default() -> Self { - Self { - main_thread: std::thread::current().id(), - } - } -} - #[cfg(test)] mod tests { - use super::World; + use super::{FromWorld, World}; use crate::{ - change_detection::DetectChanges, + change_detection::DetectChangesMut, component::{ComponentDescriptor, ComponentInfo, StorageType}, ptr::OwningPtr, system::Resource, @@ -1864,6 +2232,41 @@ mod tests { assert_eq!(DROP_COUNT.load(std::sync::atomic::Ordering::SeqCst), 1); } + #[derive(Resource)] + struct TestFromWorld(u32); + impl FromWorld for TestFromWorld { + fn from_world(world: &mut World) -> Self { + let b = world.resource::(); + Self(b.0) + } + } + + #[test] + fn init_resource_does_not_overwrite() { + let mut world = World::new(); + world.insert_resource(TestResource(0)); + world.init_resource::(); + world.insert_resource(TestResource(1)); + world.init_resource::(); + + let resource = world.resource::(); + + assert_eq!(resource.0, 0); + } + + #[test] + fn init_non_send_resource_does_not_overwrite() { + let mut world = World::new(); + world.insert_resource(TestResource(0)); + world.init_non_send_resource::(); + world.insert_resource(TestResource(1)); + world.init_non_send_resource::(); + + let resource = world.non_send_resource::(); + + assert_eq!(resource.0, 0); + } + #[derive(Component)] struct Foo; diff --git a/crates/bevy_ecs/src/world/world_cell.rs b/crates/bevy_ecs/src/world/world_cell.rs index 5f0fb5aaa5a74..8e6699e076694 100644 --- a/crates/bevy_ecs/src/world/world_cell.rs +++ b/crates/bevy_ecs/src/world/world_cell.rs @@ -256,7 +256,7 @@ impl<'w> WorldCell<'w> { let component_id = self.world.components.get_resource_id(TypeId::of::())?; let archetype_component_id = self .world - .get_resource_archetype_component_id(component_id)?; + .get_non_send_archetype_component_id(component_id)?; WorldBorrow::try_new( // SAFETY: ComponentId matches TypeId || unsafe { self.world.get_non_send_with_id(component_id) }, @@ -289,7 +289,7 @@ impl<'w> WorldCell<'w> { let component_id = self.world.components.get_resource_id(TypeId::of::())?; let archetype_component_id = self .world - .get_resource_archetype_component_id(component_id)?; + .get_non_send_archetype_component_id(component_id)?; WorldBorrowMut::try_new( // SAFETY: ComponentId matches TypeId and access is checked by WorldBorrowMut || unsafe { self.world.get_non_send_unchecked_mut_with_id(component_id) }, diff --git a/crates/bevy_encase_derive/src/lib.rs b/crates/bevy_encase_derive/src/lib.rs index 0ef5c43817358..d57be9f85c7c5 100644 --- a/crates/bevy_encase_derive/src/lib.rs +++ b/crates/bevy_encase_derive/src/lib.rs @@ -1,23 +1,12 @@ use bevy_macro_utils::BevyManifest; use encase_derive_impl::{implement, syn}; -const BEVY: &str = "bevy"; -const BEVY_RENDER: &str = "bevy_render"; const ENCASE: &str = "encase"; fn bevy_encase_path() -> syn::Path { let bevy_manifest = BevyManifest::default(); bevy_manifest - .maybe_get_path(BEVY) - .map(|bevy_path| { - let mut segments = bevy_path.segments; - segments.push(BevyManifest::parse_str("render")); - syn::Path { - leading_colon: None, - segments, - } - }) - .or_else(|| bevy_manifest.maybe_get_path(BEVY_RENDER)) + .get_subcrate("render") .map(|bevy_render_path| { let mut segments = bevy_render_path.segments; segments.push(BevyManifest::parse_str("render_resource")); diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs index d0bb67c5c28c6..de1d0afb3c733 100644 --- a/crates/bevy_gilrs/src/gilrs_system.rs +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -1,30 +1,45 @@ use crate::converter::{convert_axis, convert_button, convert_gamepad_id}; use bevy_ecs::event::EventWriter; -use bevy_ecs::system::{NonSend, NonSendMut}; -use bevy_input::gamepad::GamepadInfo; -use bevy_input::{gamepad::GamepadEventRaw, prelude::*}; +use bevy_ecs::system::{NonSend, NonSendMut, Res}; +use bevy_input::gamepad::{ + GamepadAxisChangedEvent, GamepadButtonChangedEvent, GamepadConnection, GamepadConnectionEvent, + GamepadSettings, +}; +use bevy_input::gamepad::{GamepadEvent, GamepadInfo}; +use bevy_input::prelude::{GamepadAxis, GamepadButton}; +use bevy_input::Axis; use gilrs::{ev::filter::axis_dpad_to_button, EventType, Filter, Gilrs}; -pub fn gilrs_event_startup_system(gilrs: NonSend, mut events: EventWriter) { +pub fn gilrs_event_startup_system( + gilrs: NonSend, + mut connection_events: EventWriter, +) { for (id, gamepad) in gilrs.gamepads() { let info = GamepadInfo { name: gamepad.name().into(), }; - events.send(GamepadEventRaw::new( - convert_gamepad_id(id), - GamepadEventType::Connected(info), - )); + connection_events.send(GamepadConnectionEvent { + gamepad: convert_gamepad_id(id), + connection: GamepadConnection::Connected(info), + }); } } -pub fn gilrs_event_system(mut gilrs: NonSendMut, mut events: EventWriter) { +pub fn gilrs_event_system( + mut gilrs: NonSendMut, + mut events: EventWriter, + gamepad_axis: Res>, + gamepad_buttons: Res>, + gamepad_settings: Res, +) { while let Some(gilrs_event) = gilrs .next_event() .filter_ev(&axis_dpad_to_button, &mut gilrs) { gilrs.update(&gilrs_event); + let gamepad = convert_gamepad_id(gilrs_event.id); match gilrs_event.event { EventType::Connected => { let pad = gilrs.gamepad(gilrs_event.id); @@ -32,31 +47,39 @@ pub fn gilrs_event_system(mut gilrs: NonSendMut, mut events: EventWriter< name: pad.name().into(), }; - events.send(GamepadEventRaw::new( - convert_gamepad_id(gilrs_event.id), - GamepadEventType::Connected(info), - )); + events.send( + GamepadConnectionEvent::new(gamepad, GamepadConnection::Connected(info)).into(), + ); } - EventType::Disconnected => { - events.send(GamepadEventRaw::new( - convert_gamepad_id(gilrs_event.id), - GamepadEventType::Disconnected, - )); - } - EventType::ButtonChanged(gilrs_button, value, _) => { + EventType::Disconnected => events + .send(GamepadConnectionEvent::new(gamepad, GamepadConnection::Disconnected).into()), + EventType::ButtonChanged(gilrs_button, raw_value, _) => { if let Some(button_type) = convert_button(gilrs_button) { - events.send(GamepadEventRaw::new( - convert_gamepad_id(gilrs_event.id), - GamepadEventType::ButtonChanged(button_type, value), - )); + let button = GamepadButton::new(gamepad, button_type); + let old_value = gamepad_buttons.get(button); + let button_settings = gamepad_settings.get_button_axis_settings(button); + + // Only send events that pass the user-defined change threshold + if let Some(filtered_value) = button_settings.filter(raw_value, old_value) { + events.send( + GamepadButtonChangedEvent::new(gamepad, button_type, filtered_value) + .into(), + ); + } } } - EventType::AxisChanged(gilrs_axis, value, _) => { + EventType::AxisChanged(gilrs_axis, raw_value, _) => { if let Some(axis_type) = convert_axis(gilrs_axis) { - events.send(GamepadEventRaw::new( - convert_gamepad_id(gilrs_event.id), - GamepadEventType::AxisChanged(axis_type, value), - )); + let axis = GamepadAxis::new(gamepad, axis_type); + let old_value = gamepad_axis.get(axis); + let axis_settings = gamepad_settings.get_axis_settings(axis); + + // Only send events that pass the user-defined change threshold + if let Some(filtered_value) = axis_settings.filter(raw_value, old_value) { + events.send( + GamepadAxisChangedEvent::new(gamepad, axis_type, filtered_value).into(), + ); + } } } _ => (), diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 9e2c328348d04..395667a6e78d1 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -140,6 +140,14 @@ fn remove_children(parent: Entity, children: &[Entity], world: &mut World) { } } +fn clear_children(parent: Entity, world: &mut World) { + if let Some(children) = world.entity_mut(parent).remove::() { + for &child in &children.0 { + world.entity_mut(child).remove::(); + } + } +} + /// Command that adds a child to an entity #[derive(Debug)] pub struct AddChild { @@ -196,9 +204,34 @@ impl Command for RemoveChildren { } } +/// Command that clear all children from an entity. +pub struct ClearChildren { + parent: Entity, +} + +impl Command for ClearChildren { + fn write(self, world: &mut World) { + clear_children(self.parent, world); + } +} + +/// Command that clear all children from an entity. And replace with the given children. +pub struct ReplaceChildren { + parent: Entity, + children: SmallVec<[Entity; 8]>, +} + +impl Command for ReplaceChildren { + fn write(self, world: &mut World) { + clear_children(self.parent, world); + world.entity_mut(self.parent).push_children(&self.children); + } +} + /// Command that removes the parent of an entity, and removes that entity from the parent's [`Children`]. pub struct RemoveParent { - child: Entity, + /// `Entity` whose parent must be removed. + pub child: Entity, } impl Command for RemoveParent { @@ -267,6 +300,10 @@ pub trait BuildChildren { /// will have those children removed from its list. Removing all children from a parent causes its /// [`Children`] component to be removed from the entity. fn add_child(&mut self, child: Entity) -> &mut Self; + /// Removes all children from this entity. The [`Children`] component will be removed if it exists, otherwise this does nothing. + fn clear_children(&mut self) -> &mut Self; + /// Removes all current children from this entity, replacing them with the specified list of entities. + fn replace_children(&mut self, children: &[Entity]) -> &mut Self; /// Sets the parent of this entity. fn set_parent(&mut self, parent: Entity) -> &mut Self; /// Removes the parent of this entity. @@ -324,6 +361,21 @@ impl<'w, 's, 'a> BuildChildren for EntityCommands<'w, 's, 'a> { self } + fn clear_children(&mut self) -> &mut Self { + let parent = self.id(); + self.commands().add(ClearChildren { parent }); + self + } + + fn replace_children(&mut self, children: &[Entity]) -> &mut Self { + let parent = self.id(); + self.commands().add(ReplaceChildren { + children: SmallVec::from(children), + parent, + }); + self + } + fn set_parent(&mut self, parent: Entity) -> &mut Self { let child = self.id(); self.commands().add(AddChild { child, parent }); @@ -727,6 +779,88 @@ mod tests { assert!(world.get::(child4).is_none()); } + #[test] + fn push_and_clear_children_commands() { + let mut world = World::default(); + let entities = world + .spawn_batch(vec![(C(1),), (C(2),), (C(3),), (C(4),), (C(5),)]) + .collect::>(); + + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + commands.entity(entities[0]).push_children(&entities[1..3]); + } + queue.apply(&mut world); + + let parent = entities[0]; + let child1 = entities[1]; + let child2 = entities[2]; + + let expected_children: SmallVec<[Entity; 8]> = smallvec![child1, child2]; + assert_eq!( + world.get::(parent).unwrap().0.clone(), + expected_children + ); + assert_eq!(*world.get::(child1).unwrap(), Parent(parent)); + assert_eq!(*world.get::(child2).unwrap(), Parent(parent)); + + { + let mut commands = Commands::new(&mut queue, &world); + commands.entity(parent).clear_children(); + } + queue.apply(&mut world); + + assert!(world.get::(parent).is_none()); + + assert!(world.get::(child1).is_none()); + assert!(world.get::(child2).is_none()); + } + + #[test] + fn push_and_replace_children_commands() { + let mut world = World::default(); + let entities = world + .spawn_batch(vec![(C(1),), (C(2),), (C(3),), (C(4),), (C(5),)]) + .collect::>(); + + let mut queue = CommandQueue::default(); + { + let mut commands = Commands::new(&mut queue, &world); + commands.entity(entities[0]).push_children(&entities[1..3]); + } + queue.apply(&mut world); + + let parent = entities[0]; + let child1 = entities[1]; + let child2 = entities[2]; + let child4 = entities[4]; + + let expected_children: SmallVec<[Entity; 8]> = smallvec![child1, child2]; + assert_eq!( + world.get::(parent).unwrap().0.clone(), + expected_children + ); + assert_eq!(*world.get::(child1).unwrap(), Parent(parent)); + assert_eq!(*world.get::(child2).unwrap(), Parent(parent)); + + let replace_children = [child1, child4]; + { + let mut commands = Commands::new(&mut queue, &world); + commands.entity(parent).replace_children(&replace_children); + } + queue.apply(&mut world); + + let expected_children: SmallVec<[Entity; 8]> = smallvec![child1, child4]; + assert_eq!( + world.get::(parent).unwrap().0.clone(), + expected_children + ); + assert_eq!(*world.get::(child1).unwrap(), Parent(parent)); + assert_eq!(*world.get::(child4).unwrap(), Parent(parent)); + assert!(world.get::(child2).is_none()); + } + #[test] fn push_and_insert_and_remove_children_world() { let mut world = World::default(); diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index df81324387b2b..db1a67b5e88b1 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -172,7 +172,7 @@ mod tests { // Add a child to the grandparent (the "parent"), which will get deleted parent .spawn((N("Parent, to be deleted".to_owned()), Idx(3))) - // All descendents of the "parent" should also be deleted. + // All descendants of the "parent" should also be deleted. .with_children(|parent| { parent .spawn((N("First Child, to be deleted".to_owned()), Idx(4))) diff --git a/crates/bevy_input/src/axis.rs b/crates/bevy_input/src/axis.rs index 228365f16bdff..6c88145da13b7 100644 --- a/crates/bevy_input/src/axis.rs +++ b/crates/bevy_input/src/axis.rs @@ -52,6 +52,10 @@ where pub fn remove(&mut self, input_device: T) -> Option { self.axis_data.remove(&input_device) } + /// Returns an iterator of all the input devices that have position data + pub fn devices(&self) -> impl ExactSizeIterator { + self.axis_data.keys() + } } #[cfg(test)] @@ -108,4 +112,34 @@ mod tests { assert_eq!(expected, actual); } } + + #[test] + fn test_axis_devices() { + let mut axis = Axis::::default(); + assert_eq!(axis.devices().count(), 0); + + axis.set( + GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger), + 0.1, + ); + assert_eq!(axis.devices().count(), 1); + + axis.set( + GamepadButton::new(Gamepad::new(1), GamepadButtonType::LeftTrigger), + 0.5, + ); + assert_eq!(axis.devices().count(), 2); + + axis.set( + GamepadButton::new(Gamepad::new(1), GamepadButtonType::RightTrigger), + -0.1, + ); + assert_eq!(axis.devices().count(), 2); + + axis.remove(GamepadButton::new( + Gamepad::new(1), + GamepadButtonType::RightTrigger, + )); + assert_eq!(axis.devices().count(), 1); + } } diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index c4f1ea22da099..cc8101d6810a6 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -1,7 +1,7 @@ use crate::{Axis, Input}; use bevy_ecs::event::{EventReader, EventWriter}; use bevy_ecs::{ - change_detection::DetectChanges, + change_detection::DetectChangesMut, system::{Res, ResMut, Resource}, }; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect}; @@ -65,8 +65,8 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// ## Usage /// /// The primary way to access the individual connected gamepads is done through the [`Gamepads`] -/// `bevy` resource. It is also used inside of [`GamepadEvent`]s and [`GamepadEventRaw`]s to distinguish -/// which gamepad an event corresponds to. +/// `bevy` resource. It is also used inside of [`GamepadConnectionEvent`]s to correspond a gamepad +/// with a connection event. /// /// ## Note /// @@ -111,8 +111,7 @@ pub struct GamepadInfo { /// ## Updating /// /// The [`Gamepad`]s are registered and deregistered in the [`gamepad_connection_system`] -/// whenever a [`GamepadEventType::Connected`] or [`GamepadEventType::Disconnected`] -/// event is received. +/// whenever a [`GamepadConnectionEvent`] is received. #[derive(Resource, Default, Debug)] pub struct Gamepads { /// The collection of the connected [`Gamepad`]s. @@ -145,184 +144,12 @@ impl Gamepads { } } -/// The data contained in a [`GamepadEvent`] or [`GamepadEventRaw`]. -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub enum GamepadEventType { - /// A [`Gamepad`] has been connected. - Connected(GamepadInfo), - /// A [`Gamepad`] has been disconnected. - Disconnected, - - /// The value of a [`Gamepad`] button has changed. - ButtonChanged(GamepadButtonType, f32), - /// The value of a [`Gamepad`] axis has changed. - AxisChanged(GamepadAxisType, f32), -} - -/// An event of a [`Gamepad`]. -/// -/// This event is the translated version of the [`GamepadEventRaw`]. It is available to -/// the end user and can be used for game logic. -/// -/// ## Differences -/// -/// The difference between the [`GamepadEventRaw`] and the [`GamepadEvent`] is that the -/// former respects user defined [`GamepadSettings`] for the gamepad inputs when translating it -/// to the latter. The former also updates the [`Input`], [`Axis`], -/// and [`Axis`] resources accordingly. -/// -/// ## Gamepad input mocking -/// -/// When mocking gamepad input, use [`GamepadEventRaw`]s instead of [`GamepadEvent`]s. -/// Otherwise [`GamepadSettings`] won't be respected and the [`Input`], -/// [`Axis`], and [`Axis`] resources won't be updated correctly. -/// -/// An example for gamepad input mocking can be seen in the documentation of the [`GamepadEventRaw`]. -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct GamepadEvent { - /// The gamepad this event corresponds to. - pub gamepad: Gamepad, - /// The type of the event. - pub event_type: GamepadEventType, -} - -impl GamepadEvent { - /// Creates a new [`GamepadEvent`]. - pub fn new(gamepad: Gamepad, event_type: GamepadEventType) -> Self { - Self { - gamepad, - event_type, - } - } -} - -/// A raw event of a [`Gamepad`]. -/// -/// This event is the translated version of the `EventType` from the `GilRs` crate. -/// It is available to the end user and can be used for game logic. -/// -/// ## Differences -/// -/// The difference between the `EventType` from the `GilRs` crate and the [`GamepadEventRaw`] -/// is that the latter has less events, because the button pressing logic is handled through the generic -/// [`Input`] instead of through events. -/// -/// The difference between the [`GamepadEventRaw`] and the [`GamepadEvent`] can be seen in the documentation -/// of the [`GamepadEvent`]. -/// -/// ## Gamepad input mocking -/// -/// The following example showcases how to mock gamepad input by manually sending [`GamepadEventRaw`]s. -/// -/// ``` -/// # use bevy_input::prelude::*; -/// # use bevy_input::InputPlugin; -/// # use bevy_input::gamepad::{GamepadEventRaw, GamepadInfo}; -/// # use bevy_app::prelude::*; -/// # use bevy_ecs::prelude::*; -/// #[derive(Resource)] -/// struct MyResource(bool); -/// -/// // This system sets the bool inside `MyResource` to `true` if the `South` button of the first gamepad is pressed. -/// fn change_resource_on_gamepad_button_press( -/// mut my_resource: ResMut, -/// gamepads: Res, -/// button_inputs: ResMut>, -/// ) { -/// let gamepad = gamepads.iter().next().unwrap(); -/// let gamepad_button = GamepadButton::new(gamepad, GamepadButtonType::South); -/// -/// my_resource.0 = button_inputs.pressed(gamepad_button); -/// } -/// -/// // Create our app. -/// let mut app = App::new(); -/// -/// // Add the input plugin and the system/resource we want to test. -/// app.add_plugin(InputPlugin) -/// .insert_resource(MyResource(false)) -/// .add_system(change_resource_on_gamepad_button_press); -/// -/// // Define our dummy gamepad input data. -/// let gamepad = Gamepad::new(0); -/// let button_type = GamepadButtonType::South; -/// -/// // Send the gamepad connected event to mark our gamepad as connected. -/// // This updates the `Gamepads` resource accordingly. -/// let info = GamepadInfo { name: "Mock Gamepad".into() }; -/// app.world.send_event(GamepadEventRaw::new(gamepad, GamepadEventType::Connected(info))); -/// -/// // Send the gamepad input event to mark the `South` gamepad button as pressed. -/// // This updates the `Input` resource accordingly. -/// app.world.send_event(GamepadEventRaw::new( -/// gamepad, -/// GamepadEventType::ButtonChanged(button_type, 1.0) -/// )); -/// -/// // Advance the execution of the schedule by a single cycle. -/// app.update(); -/// -/// // At this point you can check if your game logic corresponded correctly to the gamepad input. -/// // In this example we are checking if the bool in `MyResource` was updated from `false` to `true`. -/// assert!(app.world.resource::().0); -/// -/// // Send the gamepad input event to mark the `South` gamepad button as released. -/// // This updates the `Input` resource accordingly. -/// app.world.send_event(GamepadEventRaw::new( -/// gamepad, -/// GamepadEventType::ButtonChanged(button_type, 0.0) -/// )); -/// -/// // Advance the execution of the schedule by another cycle. -/// app.update(); -/// -/// // Check if the bool in `MyResource` was updated from `true` to `false`. -/// assert!(!app.world.resource::().0); -/// # -/// # bevy_ecs::system::assert_is_system(change_resource_on_gamepad_button_press); -/// ``` -#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] -#[reflect(Debug, PartialEq)] -#[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), - reflect(Serialize, Deserialize) -)] -pub struct GamepadEventRaw { - /// The gamepad this event corresponds to. - pub gamepad: Gamepad, - /// The type of the event. - pub event_type: GamepadEventType, -} - -impl GamepadEventRaw { - /// Creates a new [`GamepadEventRaw`]. - pub fn new(gamepad: Gamepad, event_type: GamepadEventType) -> Self { - Self { - gamepad, - event_type, - } - } -} - /// A type of a [`GamepadButton`]. /// /// ## Usage /// /// This is used to determine which button has changed its value when receiving a -/// [`GamepadEventType::ButtonChanged`]. It is also used in the [`GamepadButton`] +/// [`GamepadButtonChangedEvent`]. It is also used in the [`GamepadButton`] /// which in turn is used to create the [`Input`] or /// [`Axis`] `bevy` resources. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect, FromReflect)] @@ -385,11 +212,11 @@ pub enum GamepadButtonType { /// ## Usage /// /// It is used as the generic `T` value of an [`Input`] and [`Axis`] to create `bevy` resources. These -/// resources store the data of the buttons and axes of a gamepad and can be accessed inside of a system. +/// resources store the data of the buttons of a gamepad and can be accessed inside of a system. /// /// ## Updating /// -/// The resources are updated inside of the [`gamepad_event_system`]. +/// The gamepad button resources are updated inside of the [`gamepad_button_event_system`]. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect, FromReflect)] #[reflect(Debug, Hash, PartialEq)] #[cfg_attr( @@ -430,7 +257,7 @@ impl GamepadButton { /// ## Usage /// /// This is used to determine which axis has changed its value when receiving a -/// [`GamepadEventType::AxisChanged`]. It is also used in the [`GamepadAxis`] +/// [`GamepadAxisChangedEvent`]. It is also used in the [`GamepadAxis`] /// which in turn is used to create the [`Axis`] `bevy` resource. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect, FromReflect)] #[reflect(Debug, Hash, PartialEq)] @@ -462,12 +289,12 @@ pub enum GamepadAxisType { /// /// ## Usage /// -/// It is used as the generic `T` value of an [`Axis`] to create a `bevy` resource. This resource -/// stores the data of the axes of a gamepad and can be accessed inside of a system. +/// It is used as the generic `T` value of an [`Axis`] to create `bevy` resources. These +/// resources store the data of the axes of a gamepad and can be accessed inside of a system. /// /// ## Updating /// -/// The resource is updated inside of the [`gamepad_event_system`]. +/// The gamepad axes resources are updated inside of the [`gamepad_axis_event_system`]. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Reflect, FromReflect)] #[reflect(Debug, Hash, PartialEq)] #[cfg_attr( @@ -510,8 +337,9 @@ impl GamepadAxis { /// /// ## Note /// -/// The [`GamepadSettings`] are used inside of the [`gamepad_event_system`], but are never written to -/// inside of `bevy`. To modify these settings, mutate the corresponding resource. +/// The [`GamepadSettings`] are used inside of `bevy_gilrs` to determine when raw gamepad events from `girls`, +/// should register as a [`GamepadEvent`]. Events that don't meet the change thresholds defined in [`GamepadSettings`] +/// will not register. To modify these settings, mutate the corresponding resource. #[derive(Resource, Default, Debug, Reflect, FromReflect)] #[reflect(Debug, Default)] pub struct GamepadSettings { @@ -755,7 +583,7 @@ impl ButtonSettings { /// Otherwise, values will not be rounded. /// /// The valid range is `[-1.0, 1.0]`. -#[derive(Debug, Clone, Reflect, FromReflect)] +#[derive(Debug, Clone, Reflect, FromReflect, PartialEq)] #[reflect(Debug, Default)] pub struct AxisSettings { /// Values that are higher than `livezone_upperbound` will be rounded up to -1.0. @@ -783,7 +611,7 @@ impl Default for AxisSettings { } impl AxisSettings { - /// Creates a new `AxisSettings` instance. + /// Creates a new [`AxisSettings`] instance. /// /// # Arguments /// @@ -794,9 +622,10 @@ impl AxisSettings { /// + `threshold` - the minimum value by which input must change before the change is registered. /// /// Restrictions: - /// + `-1.0 <= ``livezone_lowerbound`` <= ``deadzone_lowerbound`` <= 0.0 <= ``deadzone_upperbound`` <= - /// ``livezone_upperbound`` <= 1.0` - /// + `0.0 <= ``threshold`` <= 2.0` + /// + /// + `-1.0 <= livezone_lowerbound <= deadzone_lowerbound <= 0.0` + /// + `0.0 <= deadzone_upperbound <= livezone_upperbound <= 1.0` + /// + `0.0 <= threshold <= 2.0` /// /// # Errors /// @@ -818,11 +647,11 @@ impl AxisSettings { Err(AxisSettingsError::DeadZoneLowerBoundOutOfRange( deadzone_lowerbound, )) - } else if !(-1.0..=0.0).contains(&deadzone_upperbound) { + } else if !(0.0..=1.0).contains(&deadzone_upperbound) { Err(AxisSettingsError::DeadZoneUpperBoundOutOfRange( deadzone_upperbound, )) - } else if !(-1.0..=0.0).contains(&livezone_upperbound) { + } else if !(0.0..=1.0).contains(&livezone_upperbound) { Err(AxisSettingsError::LiveZoneUpperBoundOutOfRange( livezone_upperbound, )) @@ -864,7 +693,7 @@ impl AxisSettings { /// /// If the value passed is less than the dead zone upper bound, /// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`. - /// If the value passsed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`. + /// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::LiveZoneUpperBoundOutOfRange`. pub fn try_set_livezone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { if !(0.0..=1.0).contains(&value) { Err(AxisSettingsError::LiveZoneUpperBoundOutOfRange(value)) @@ -901,7 +730,7 @@ impl AxisSettings { /// /// If the value passed is greater than the live zone upper bound, /// returns `AxisSettingsError::DeadZoneUpperBoundGreaterThanLiveZoneUpperBound`. - /// If the value passsed is not in range [0.0..=1.0], returns `AxisSettingsError::DeadZoneUpperBoundOutOfRange`. + /// If the value passed is not in range [0.0..=1.0], returns `AxisSettingsError::DeadZoneUpperBoundOutOfRange`. pub fn try_set_deadzone_upperbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { if !(0.0..=1.0).contains(&value) { Err(AxisSettingsError::DeadZoneUpperBoundOutOfRange(value)) @@ -939,7 +768,7 @@ impl AxisSettings { /// /// If the value passed is less than the dead zone lower bound, /// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`. - /// If the value passsed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`. + /// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::LiveZoneLowerBoundOutOfRange`. pub fn try_set_livezone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { if !(-1.0..=0.0).contains(&value) { Err(AxisSettingsError::LiveZoneLowerBoundOutOfRange(value)) @@ -977,7 +806,7 @@ impl AxisSettings { /// /// If the value passed is less than the live zone lower bound, /// returns `AxisSettingsError::LiveZoneLowerBoundGreaterThanDeadZoneLowerBound`. - /// If the value passsed is not in range [-1.0..=0.0], returns `AxisSettingsError::DeadZoneLowerBoundOutOfRange`. + /// If the value passed is not in range [-1.0..=0.0], returns `AxisSettingsError::DeadZoneLowerBoundOutOfRange`. pub fn try_set_deadzone_lowerbound(&mut self, value: f32) -> Result<(), AxisSettingsError> { if !(-1.0..=0.0).contains(&value) { Err(AxisSettingsError::DeadZoneLowerBoundOutOfRange(value)) @@ -1032,25 +861,40 @@ impl AxisSettings { self.threshold } - fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = - if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound { - 0.0 - } else if new_value >= self.livezone_upperbound { - 1.0 - } else if new_value <= self.livezone_lowerbound { - -1.0 - } else { - new_value - }; - - if let Some(old_value) = old_value { - if (new_value - old_value).abs() <= self.threshold { - return None; - } + /// Clamps the `raw_value` according to the `AxisSettings`. + pub fn clamp(&self, new_value: f32) -> f32 { + if self.deadzone_lowerbound <= new_value && new_value <= self.deadzone_upperbound { + 0.0 + } else if new_value >= self.livezone_upperbound { + 1.0 + } else if new_value <= self.livezone_lowerbound { + -1.0 + } else { + new_value + } + } + + /// Determines whether the change from `old_value` to `new_value` should + /// be registered as a change, according to the `AxisSettings`. + fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { + if old_value.is_none() { + return true; } - Some(new_value) + f32::abs(new_value - old_value.unwrap()) > self.threshold + } + + /// Filters the `new_value` based on the `old_value`, according to the [`AxisSettings`]. + /// + /// Returns the clamped `new_value` if the change exceeds the settings threshold, + /// and `None` otherwise. + pub fn filter(&self, new_value: f32, old_value: Option) -> Option { + let new_value = self.clamp(new_value); + + if self.should_register_change(new_value, old_value) { + return Some(new_value); + } + None } } @@ -1069,7 +913,7 @@ impl AxisSettings { /// /// ## Updating /// -/// The current value of a button is received through the [`GamepadEvent`]s or [`GamepadEventRaw`]s. +/// The current value of a button is received through the [`GamepadButtonChangedEvent`]. #[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(Debug, Default)] pub struct ButtonAxisSettings { @@ -1092,137 +936,273 @@ impl Default for ButtonAxisSettings { } impl ButtonAxisSettings { - /// Filters the `new_value` according to the specified settings. + /// Clamps the `raw_value` according to the specified settings. /// - /// If the `new_value` is: + /// If the `raw_value` is: /// - lower than or equal to `low` it will be rounded to 0.0. /// - higher than or equal to `high` it will be rounded to 1.0. /// - Otherwise it will not be rounded. - /// - /// If the difference between the calculated value and the `old_value` is lower or - /// equal to the `threshold`, [`None`] will be returned. - fn filter(&self, new_value: f32, old_value: Option) -> Option { - let new_value = if new_value <= self.low { - 0.0 - } else if new_value >= self.high { - 1.0 - } else { - new_value - }; + fn clamp(&self, raw_value: f32) -> f32 { + if raw_value <= self.low { + return 0.0; + } + if raw_value >= self.high { + return 1.0; + } - if let Some(old_value) = old_value { - if (new_value - old_value).abs() <= self.threshold { - return None; - } + raw_value + } + + /// Determines whether the change from an `old_value` to a `new_value` should + /// be registered as a change event, according to the specified settings. + fn should_register_change(&self, new_value: f32, old_value: Option) -> bool { + if old_value.is_none() { + return true; } - Some(new_value) + f32::abs(new_value - old_value.unwrap()) > self.threshold + } + + /// Filters the `new_value` based on the `old_value`, according to the `ButtonAxisSettings`. + /// + /// Returns the clamped `new_value`, according to the [`ButtonAxisSettings`], if the change + /// exceeds the settings threshold, and `None` otherwise. + pub fn filter(&self, new_value: f32, old_value: Option) -> Option { + let new_value = self.clamp(new_value); + + if self.should_register_change(new_value, old_value) { + return Some(new_value); + } + None } } -/// Monitors gamepad connection and disconnection events and updates the [`Gamepads`] resource accordingly. +/// Handles [`GamepadConnectionEvent`]s and updates gamepad resources. +/// +/// Updates the [`Gamepads`] resource and resets and/or initializes +/// the [`Axis`] and [`Input`] resources. /// /// ## Note /// /// Whenever a [`Gamepad`] connects or disconnects, an information gets printed to the console using the [`info!`] macro. pub fn gamepad_connection_system( mut gamepads: ResMut, - mut gamepad_event: EventReader, + mut connection_events: EventReader, + mut axis: ResMut>, + mut button_axis: ResMut>, + mut button_input: ResMut>, ) { - for event in gamepad_event.iter() { - match &event.event_type { - GamepadEventType::Connected(info) => { - gamepads.register(event.gamepad, info.clone()); - info!("{:?} Connected", event.gamepad); + for connection_event in connection_events.iter() { + let gamepad = connection_event.gamepad; + + if let GamepadConnection::Connected(info) = &connection_event.connection { + gamepads.register(gamepad, info.clone()); + info!("{:?} Connected", gamepad); + + for button_type in &ALL_BUTTON_TYPES { + let gamepad_button = GamepadButton::new(gamepad, *button_type); + button_input.reset(gamepad_button); + button_axis.set(gamepad_button, 0.0); } + for axis_type in &ALL_AXIS_TYPES { + axis.set(GamepadAxis::new(gamepad, *axis_type), 0.0); + } + } else { + gamepads.deregister(gamepad); + info!("{:?} Disconnected", gamepad); - GamepadEventType::Disconnected => { - gamepads.deregister(event.gamepad); - info!("{:?} Disconnected", event.gamepad); + for button_type in &ALL_BUTTON_TYPES { + let gamepad_button = GamepadButton::new(gamepad, *button_type); + button_input.reset(gamepad_button); + button_axis.remove(gamepad_button); + } + for axis_type in &ALL_AXIS_TYPES { + axis.remove(GamepadAxis::new(gamepad, *axis_type)); } - _ => (), } } } -/// Modifies the gamepad resources and sends out gamepad events. -/// -/// The resources [`Input`], [`Axis`], and [`Axis`] are updated -/// and the [`GamepadEvent`]s are sent according to the received [`GamepadEventRaw`]s respecting the [`GamepadSettings`]. -/// -/// ## Differences -/// -/// The main difference between the events and the resources is that the latter allows you to check specific -/// buttons or axes, rather than reading the events one at a time. This is done through convenient functions -/// like [`Input::pressed`], [`Input::just_pressed`], [`Input::just_released`], and [`Axis::get`]. -pub fn gamepad_event_system( +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum GamepadConnection { + Connected(GamepadInfo), + Disconnected, +} + +/// A Gamepad connection event. Created when a connection to a gamepad +/// is established and when a gamepad is disconnected. +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadConnectionEvent { + /// The gamepad whose connection status changed. + pub gamepad: Gamepad, + /// The change in the gamepads connection. + pub connection: GamepadConnection, +} + +impl GamepadConnectionEvent { + pub fn new(gamepad: Gamepad, connection: GamepadConnection) -> Self { + Self { + gamepad, + connection, + } + } + + pub fn connected(&self) -> bool { + matches!(self.connection, GamepadConnection::Connected(_)) + } + + pub fn disconnected(&self) -> bool { + !self.connected() + } +} + +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadAxisChangedEvent { + pub gamepad: Gamepad, + pub axis_type: GamepadAxisType, + pub value: f32, +} + +impl GamepadAxisChangedEvent { + pub fn new(gamepad: Gamepad, axis_type: GamepadAxisType, value: f32) -> Self { + Self { + gamepad, + axis_type, + value, + } + } +} + +/// Gamepad event for when the "value" (amount of pressure) on the button +/// changes by an amount larger than the threshold defined in [`GamepadSettings`]. +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct GamepadButtonChangedEvent { + pub gamepad: Gamepad, + pub button_type: GamepadButtonType, + pub value: f32, +} + +impl GamepadButtonChangedEvent { + pub fn new(gamepad: Gamepad, button_type: GamepadButtonType, value: f32) -> Self { + Self { + gamepad, + button_type, + value, + } + } +} + +/// Uses [`GamepadAxisChangedEvent`]s to update the relevant `Input` and `Axis` values. +pub fn gamepad_axis_event_system( + mut gamepad_axis: ResMut>, + mut axis_events: EventReader, +) { + for axis_event in axis_events.iter() { + let axis = GamepadAxis::new(axis_event.gamepad, axis_event.axis_type); + gamepad_axis.set(axis, axis_event.value); + } +} + +/// Uses [`GamepadButtonChangedEvent`]s to update the relevant `Input` and `Axis` values. +pub fn gamepad_button_event_system( + mut button_events: EventReader, mut button_input: ResMut>, - mut axis: ResMut>, mut button_axis: ResMut>, - mut raw_events: EventReader, - mut events: EventWriter, settings: Res, ) { - button_input.bypass_change_detection().clear(); - for event in raw_events.iter() { - match &event.event_type { - GamepadEventType::Connected(_) => { - events.send(GamepadEvent::new(event.gamepad, event.event_type.clone())); - for button_type in &ALL_BUTTON_TYPES { - let gamepad_button = GamepadButton::new(event.gamepad, *button_type); - button_input.reset(gamepad_button); - button_axis.set(gamepad_button, 0.0); - } - for axis_type in &ALL_AXIS_TYPES { - axis.set(GamepadAxis::new(event.gamepad, *axis_type), 0.0); - } - } - GamepadEventType::Disconnected => { - events.send(GamepadEvent::new(event.gamepad, event.event_type.clone())); - for button_type in &ALL_BUTTON_TYPES { - let gamepad_button = GamepadButton::new(event.gamepad, *button_type); - button_input.reset(gamepad_button); - button_axis.remove(gamepad_button); - } - for axis_type in &ALL_AXIS_TYPES { - axis.remove(GamepadAxis::new(event.gamepad, *axis_type)); - } - } - GamepadEventType::AxisChanged(axis_type, value) => { - let gamepad_axis = GamepadAxis::new(event.gamepad, *axis_type); - if let Some(filtered_value) = settings - .get_axis_settings(gamepad_axis) - .filter(*value, axis.get(gamepad_axis)) - { - axis.set(gamepad_axis, filtered_value); - events.send(GamepadEvent::new( - event.gamepad, - GamepadEventType::AxisChanged(*axis_type, filtered_value), - )); - } - } - GamepadEventType::ButtonChanged(button_type, value) => { - let gamepad_button = GamepadButton::new(event.gamepad, *button_type); - if let Some(filtered_value) = settings - .get_button_axis_settings(gamepad_button) - .filter(*value, button_axis.get(gamepad_button)) - { - button_axis.set(gamepad_button, filtered_value); - events.send(GamepadEvent::new( - event.gamepad, - GamepadEventType::ButtonChanged(*button_type, filtered_value), - )); - } + for button_event in button_events.iter() { + let button = GamepadButton::new(button_event.gamepad, button_event.button_type); + let value = button_event.value; + let button_property = settings.get_button_settings(button); + + if button_property.is_released(value) { + // We don't have to check if the button was previously pressed + // because that check is performed within Input::release() + button_input.release(button); + } else if button_property.is_pressed(value) { + button_input.press(button); + }; - let button_property = settings.get_button_settings(gamepad_button); - if button_input.pressed(gamepad_button) { - if button_property.is_released(*value) { - button_input.release(gamepad_button); - } - } else if button_property.is_pressed(*value) { - button_input.press(gamepad_button); - } + button_axis.set(button, value); + } +} + +/// A gamepad event. +/// +/// This event type is used over the [`GamepadConnectionEvent`], +/// [`GamepadButtonChangedEvent`] and [`GamepadAxisChangedEvent`] when +/// the in-frame relative ordering of events is important. +#[derive(Debug, Clone, PartialEq, Reflect, FromReflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub enum GamepadEvent { + Connection(GamepadConnectionEvent), + Button(GamepadButtonChangedEvent), + Axis(GamepadAxisChangedEvent), +} + +impl From for GamepadEvent { + fn from(value: GamepadConnectionEvent) -> Self { + Self::Connection(value) + } +} + +impl From for GamepadEvent { + fn from(value: GamepadButtonChangedEvent) -> Self { + Self::Button(value) + } +} + +impl From for GamepadEvent { + fn from(value: GamepadAxisChangedEvent) -> Self { + Self::Axis(value) + } +} + +/// Splits the [`GamepadEvent`] event stream into it's component events. +pub fn gamepad_event_system( + mut gamepad_events: EventReader, + mut connection_events: EventWriter, + mut button_events: EventWriter, + mut axis_events: EventWriter, + mut button_input: ResMut>, +) { + button_input.bypass_change_detection().clear(); + for gamepad_event in gamepad_events.iter() { + match gamepad_event { + GamepadEvent::Connection(connection_event) => { + connection_events.send(connection_event.clone()); } + GamepadEvent::Button(button_event) => button_events.send(button_event.clone()), + GamepadEvent::Axis(axis_event) => axis_events.send(axis_event.clone()), } } } @@ -1275,8 +1255,7 @@ mod tests { let actual = settings.filter(new_value, old_value); assert_eq!( expected, actual, - "Testing filtering for {:?} with new_value = {:?}, old_value = {:?}", - settings, new_value, old_value + "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", ); } @@ -1331,8 +1310,7 @@ mod tests { let actual = settings.filter(new_value, old_value); assert_eq!( expected, actual, - "Testing filtering for {:?} with new_value = {:?}, old_value = {:?}", - settings, new_value, old_value + "Testing filtering for {settings:?} with new_value = {new_value:?}, old_value = {old_value:?}", ); } @@ -1417,8 +1395,7 @@ mod tests { assert_eq!( expected, actual, - "testing ButtonSettings::is_pressed() for value: {}", - value + "testing ButtonSettings::is_pressed() for value: {value}", ); } } @@ -1443,8 +1420,7 @@ mod tests { assert_eq!( expected, actual, - "testing ButtonSettings::is_released() for value: {}", - value + "testing ButtonSettings::is_released() for value: {value}", ); } } @@ -1469,8 +1445,7 @@ mod tests { } Err(_) => { panic!( - "ButtonSettings::new({}, {}) should be valid ", - press_threshold, release_threshold + "ButtonSettings::new({press_threshold}, {release_threshold}) should be valid" ); } } @@ -1495,8 +1470,7 @@ mod tests { match bs { Ok(_) => { panic!( - "ButtonSettings::new({}, {}) should be invalid", - press_threshold, release_threshold + "ButtonSettings::new({press_threshold}, {release_threshold}) should be invalid" ); } Err(err_code) => match err_code { @@ -1514,6 +1488,16 @@ mod tests { #[test] fn test_try_out_of_range_axis_settings() { let mut axis_settings = AxisSettings::default(); + assert_eq!( + AxisSettings::new(-0.95, -0.05, 0.05, 0.95, 0.001), + Ok(AxisSettings { + livezone_lowerbound: -0.95, + deadzone_lowerbound: -0.05, + deadzone_upperbound: 0.05, + livezone_upperbound: 0.95, + threshold: 0.001, + }) + ); assert_eq!( Err(AxisSettingsError::LiveZoneLowerBoundOutOfRange(-2.0)), axis_settings.try_set_livezone_lowerbound(-2.0) diff --git a/crates/bevy_input/src/input.rs b/crates/bevy_input/src/input.rs index 3d1a1ecf5a9ad..a22145ea64f5d 100644 --- a/crates/bevy_input/src/input.rs +++ b/crates/bevy_input/src/input.rs @@ -36,11 +36,11 @@ use bevy_ecs::schedule::State; /// * Call the [`Input::clear`] method at each frame start, before processing events. /// /// Note: Calling `clear` from a [`ResMut`] will trigger change detection. -/// It may be preferable to use [`DetectChanges::bypass_change_detection`] +/// It may be preferable to use [`DetectChangesMut::bypass_change_detection`] /// to avoid causing the resource to always be marked as changed. /// ///[`ResMut`]: bevy_ecs::system::ResMut -///[`DetectChanges::bypass_change_detection`]: bevy_ecs::change_detection::DetectChanges::bypass_change_detection +///[`DetectChangesMut::bypass_change_detection`]: bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection #[derive(Debug, Clone, Resource, Reflect)] #[reflect(Default)] pub struct Input { diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 021646015be7c..83b35fe47e0a8 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -1,5 +1,5 @@ use crate::{ButtonState, Input}; -use bevy_ecs::{change_detection::DetectChanges, event::EventReader, system::ResMut}; +use bevy_ecs::{change_detection::DetectChangesMut, event::EventReader, system::ResMut}; use bevy_reflect::{FromReflect, Reflect}; #[cfg(feature = "serialize")] diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 23583667ae973..fccc2027f56b4 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -12,8 +12,7 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ gamepad::{ - Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, GamepadEvent, - GamepadEventType, Gamepads, + Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, Gamepads, }, keyboard::{KeyCode, ScanCode}, mouse::MouseButton, @@ -23,7 +22,7 @@ pub mod prelude { } use bevy_app::prelude::*; -use bevy_ecs::schedule::{IntoSystemDescriptor, SystemLabel}; +use bevy_ecs::schedule::{IntoSystemDescriptor, SystemLabel, SystemSet}; use bevy_reflect::{FromReflect, Reflect}; use keyboard::{keyboard_input_system, KeyCode, KeyboardInput, ScanCode}; use mouse::{ @@ -33,9 +32,11 @@ use mouse::{ use touch::{touch_screen_input_system, ForceTouch, TouchInput, TouchPhase, Touches}; use gamepad::{ - gamepad_connection_system, gamepad_event_system, AxisSettings, ButtonAxisSettings, - ButtonSettings, Gamepad, GamepadAxis, GamepadAxisType, GamepadButton, GamepadButtonType, - GamepadEvent, GamepadEventRaw, GamepadEventType, GamepadSettings, Gamepads, + gamepad_axis_event_system, gamepad_button_event_system, gamepad_connection_system, + gamepad_event_system, AxisSettings, ButtonAxisSettings, ButtonSettings, Gamepad, GamepadAxis, + GamepadAxisChangedEvent, GamepadAxisType, GamepadButton, GamepadButtonChangedEvent, + GamepadButtonType, GamepadConnection, GamepadConnectionEvent, GamepadEvent, GamepadSettings, + Gamepads, }; #[cfg(feature = "serialize")] @@ -69,20 +70,23 @@ impl Plugin for InputPlugin { mouse_button_input_system.label(InputSystem), ) // gamepad + .add_event::() + .add_event::() + .add_event::() .add_event::() - .add_event::() .init_resource::() .init_resource::() .init_resource::>() .init_resource::>() .init_resource::>() - .add_system_to_stage( - CoreStage::PreUpdate, - gamepad_event_system.label(InputSystem), - ) - .add_system_to_stage( + .add_system_set_to_stage( CoreStage::PreUpdate, - gamepad_connection_system.after(InputSystem), + SystemSet::new() + .with_system(gamepad_event_system) + .with_system(gamepad_button_event_system.after(gamepad_event_system)) + .with_system(gamepad_axis_event_system.after(gamepad_event_system)) + .with_system(gamepad_connection_system.after(gamepad_event_system)) + .label(InputSystem), ) // touch .add_event::() @@ -114,9 +118,7 @@ impl Plugin for InputPlugin { // Register gamepad types app.register_type::() - .register_type::() - .register_type::() - .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index bf86ed55b22ea..9fbac1ce937ff 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -1,5 +1,5 @@ use crate::{ButtonState, Input}; -use bevy_ecs::{change_detection::DetectChanges, event::EventReader, system::ResMut}; +use bevy_ecs::{change_detection::DetectChangesMut, event::EventReader, system::ResMut}; use bevy_math::Vec2; use bevy_reflect::{FromReflect, Reflect}; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index ac371a3d4a82a..a51d1a7ff0737 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -42,6 +42,13 @@ flac = ["bevy_audio/flac"] mp3 = ["bevy_audio/mp3"] vorbis = ["bevy_audio/vorbis"] wav = ["bevy_audio/wav"] +symphonia-aac = ["bevy_audio/symphonia-aac"] +symphonia-all = ["bevy_audio/symphonia-all"] +symphonia-flac = ["bevy_audio/symphonia-flac"] +symphonia-isomp4 = ["bevy_audio/symphonia-isomp4"] +symphonia-mp3 = ["bevy_audio/symphonia-mp3"] +symphonia-vorbis = ["bevy_audio/symphonia-vorbis"] +symphonia-wav = ["bevy_audio/symphonia-wav"] # Enable watching file system for asset hot reload filesystem_watcher = ["bevy_asset/filesystem_watcher"] diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 1c8ee63d53aee..7bd49c761dc05 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -68,6 +68,12 @@ impl PluginGroup for DefaultPlugins { // NOTE: Load this after renderer initialization so that it knows about the supported // compressed texture formats .add(bevy_render::texture::ImagePlugin::default()); + + #[cfg(not(target_arch = "wasm32"))] + { + group = group + .add(bevy_render::pipelined_rendering::PipelinedRenderingPlugin::default()); + } } #[cfg(feature = "bevy_core_pipeline")] diff --git a/crates/bevy_macro_utils/src/attrs.rs b/crates/bevy_macro_utils/src/attrs.rs index 34acff08f1e16..496ce42257ec2 100644 --- a/crates/bevy_macro_utils/src/attrs.rs +++ b/crates/bevy_macro_utils/src/attrs.rs @@ -24,10 +24,7 @@ pub fn get_lit_str(attr_name: Symbol, lit: &syn::Lit) -> syn::Result<&syn::LitSt } else { Err(syn::Error::new_spanned( lit, - format!( - "expected {} attribute to be a string: `{} = \"...\"`", - attr_name, attr_name - ), + format!("expected {attr_name} attribute to be a string: `{attr_name} = \"...\"`"), )) } } @@ -38,10 +35,7 @@ pub fn get_lit_bool(attr_name: Symbol, lit: &syn::Lit) -> syn::Result { } else { Err(syn::Error::new_spanned( lit, - format!( - "expected {} attribute to be a bool value, `true` or `false`: `{} = ...`", - attr_name, attr_name - ), + format!("expected {attr_name} attribute to be a bool value, `true` or `false`: `{attr_name} = ...`"), )) } } diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index 0af86d052a946..890e3b468a310 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -32,12 +32,11 @@ impl Default for BevyManifest { } } } +const BEVY: &str = "bevy"; +const BEVY_INTERNAL: &str = "bevy_internal"; impl BevyManifest { pub fn maybe_get_path(&self, name: &str) -> Option { - const BEVY: &str = "bevy"; - const BEVY_INTERNAL: &str = "bevy_internal"; - fn dep_package(dep: &Value) -> Option<&str> { if dep.as_str().is_some() { None @@ -103,6 +102,83 @@ impl BevyManifest { pub fn parse_str(path: &str) -> T { syn::parse(path.parse::().unwrap()).unwrap() } + + pub fn get_subcrate(&self, subcrate: &str) -> Option { + self.maybe_get_path(BEVY) + .map(|bevy_path| { + let mut segments = bevy_path.segments; + segments.push(BevyManifest::parse_str(subcrate)); + syn::Path { + leading_colon: None, + segments, + } + }) + .or_else(|| self.maybe_get_path(&format!("bevy_{subcrate}"))) + } +} + +/// Derive a label trait +/// +/// # Args +/// +/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait +/// - `trait_path`: The path [`syn::Path`] to the label trait +pub fn derive_boxed_label(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream { + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { + where_token: Default::default(), + predicates: Default::default(), + }); + where_clause.predicates.push( + syn::parse2(quote! { + Self: 'static + Send + Sync + Clone + Eq + ::std::fmt::Debug + ::std::hash::Hash + }) + .unwrap(), + ); + + (quote! { + impl #impl_generics #trait_path for #ident #ty_generics #where_clause { + fn dyn_clone(&self) -> std::boxed::Box { + std::boxed::Box::new(std::clone::Clone::clone(self)) + } + } + }) + .into() +} + +/// Derive a label trait +/// +/// # Args +/// +/// - `input`: The [`syn::DeriveInput`] for struct that is deriving the label trait +/// - `trait_path`: The path [`syn::Path`] to the label trait +pub fn derive_set(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream { + let ident = input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { + where_token: Default::default(), + predicates: Default::default(), + }); + where_clause.predicates.push( + syn::parse2(quote! { + Self: 'static + Send + Sync + Clone + Eq + ::std::fmt::Debug + ::std::hash::Hash + }) + .unwrap(), + ); + + (quote! { + impl #impl_generics #trait_path for #ident #ty_generics #where_clause { + fn is_system_type(&self) -> bool { + false + } + + fn dyn_clone(&self) -> std::boxed::Box { + std::boxed::Box::new(std::clone::Clone::clone(self)) + } + } + }) + .into() } /// Derive a label trait diff --git a/crates/bevy_pbr/src/alpha.rs b/crates/bevy_pbr/src/alpha.rs index b42a78bd8ad4d..8aff7db46d6e8 100644 --- a/crates/bevy_pbr/src/alpha.rs +++ b/crates/bevy_pbr/src/alpha.rs @@ -23,6 +23,30 @@ pub enum AlphaMode { /// Standard alpha-blending is used to blend the fragment's color /// with the color behind it. Blend, + /// Similar to [`AlphaMode::Blend`], however assumes RGB channel values are + /// [premultiplied](https://en.wikipedia.org/wiki/Alpha_compositing#Straight_versus_premultiplied). + /// + /// For otherwise constant RGB values, behaves more like [`AlphaMode::Blend`] for + /// alpha values closer to 1.0, and more like [`AlphaMode::Add`] for + /// alpha values closer to 0.0. + /// + /// Can be used to avoid “border” or “outline” artifacts that can occur + /// when using plain alpha-blended textures. + Premultiplied, + /// Combines the color of the fragments with the colors behind them in an + /// additive process, (i.e. like light) producing lighter results. + /// + /// Black produces no effect. Alpha values can be used to modulate the result. + /// + /// Useful for effects like holograms, ghosts, lasers and other energy beams. + Add, + /// Combines the color of the fragments with the colors behind them in a + /// multiplicative process, (i.e. like pigments) producing darker results. + /// + /// White produces no effect. Alpha values can be used to modulate the result. + /// + /// Useful for effects like stained glass, window tint film and some colored liquids. + Multiply, } impl Eq for AlphaMode {} diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index cd7c04cbfbd69..992d2504f1d6d 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -5,13 +5,16 @@ mod bundle; mod light; mod material; mod pbr_material; +mod prepass; mod render; pub use alpha::*; +use bevy_utils::default; pub use bundle::*; pub use light::*; pub use material::*; pub use pbr_material::*; +pub use prepass::*; pub use render::*; use bevy_window::ModifiesWindows; @@ -67,14 +70,27 @@ pub const SHADOWS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11350275143789590502); pub const PBR_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4805239651767701046); +pub const PBR_PREPASS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9407115064344201137); pub const PBR_FUNCTIONS_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292); pub const SHADOW_SHADER_HANDLE: HandleUntyped = HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1836745567947005696); /// Sets up the entire PBR infrastructure of bevy. -#[derive(Default)] -pub struct PbrPlugin; +pub struct PbrPlugin { + /// Controls if the prepass is enabled for the StandardMaterial. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + pub prepass_enabled: bool, +} + +impl Default for PbrPlugin { + fn default() -> Self { + Self { + prepass_enabled: true, + } + } +} impl Plugin for PbrPlugin { fn build(&self, app: &mut App) { @@ -122,6 +138,12 @@ impl Plugin for PbrPlugin { "render/depth.wgsl", Shader::from_wgsl ); + load_internal_asset!( + app, + PBR_PREPASS_SHADER_HANDLE, + "render/pbr_prepass.wgsl", + Shader::from_wgsl + ); app.register_type::() .register_type::() @@ -135,7 +157,10 @@ impl Plugin for PbrPlugin { .register_type::() .register_type::() .add_plugin(MeshRenderPlugin) - .add_plugin(MaterialPlugin::::default()) + .add_plugin(MaterialPlugin:: { + prepass_enabled: self.prepass_enabled, + ..default() + }) .init_resource::() .init_resource::() .init_resource::() @@ -166,7 +191,7 @@ impl Plugin for PbrPlugin { .after(VisibilitySystems::CheckVisibility) .after(TransformSystem::TransformPropagate) // We assume that no entity will be both a directional light and a spot light, - // so these systems will run indepdently of one another. + // so these systems will run independently of one another. // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(update_spot_light_frusta), ) @@ -189,7 +214,6 @@ impl Plugin for PbrPlugin { check_light_mesh_visibility .label(SimulationLightSystems::CheckLightVisibility) .after(TransformSystem::TransformPropagate) - .after(VisibilitySystems::CalculateBounds) .after(SimulationLightSystems::UpdateLightFrusta) // NOTE: This MUST be scheduled AFTER the core renderer visibility check // because that resets entity ComputedVisibility for the first view diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light.rs index 69188850f535b..abad3c9b395d8 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light.rs @@ -303,7 +303,7 @@ pub enum SimulationLightSystems { // Some inspiration was taken from “Practical Clustered Shading” which is part 2 of: // https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/ // (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.) -// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa’s Siggraph 2016 talk about Doom 2016: +// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016: // http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf /// Configure the far z-plane mode used for the furthest depth slice for clustered forward @@ -550,7 +550,7 @@ fn view_z_to_z_slice( // NOTE: had to use -view_z to make it positive else log(negative) is nan ((-view_z).ln() * cluster_factors.x - cluster_factors.y + 1.0) as u32 }; - // NOTE: We use min as we may limit the far z plane used for clustering to be closeer than + // NOTE: We use min as we may limit the far z plane used for clustering to be closer than // the furthest thing being drawn. This means that we need to limit to the maximum cluster. z_slice.min(z_slices - 1) } @@ -1437,7 +1437,7 @@ fn project_to_plane_z(z_light: Sphere, z_plane: Plane) -> Option { Some(Sphere { center: Vec3A::from(z_light.center.xy().extend(z)), // hypotenuse length = radius - // pythagorus = (distance to plane)^2 + b^2 = radius^2 + // pythagoras = (distance to plane)^2 + b^2 = radius^2 radius: (z_light.radius * z_light.radius - distance_to_plane * distance_to_plane).sqrt(), }) } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index d10fac03a684e..a3039f08cd151 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,6 +1,6 @@ use crate::{ - AlphaMode, DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, SetMeshBindGroup, - SetMeshViewBindGroup, + AlphaMode, DrawMesh, MeshPipeline, MeshPipelineKey, MeshUniform, PrepassPlugin, + SetMeshBindGroup, SetMeshViewBindGroup, }; use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; @@ -133,6 +133,19 @@ pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'stat 0.0 } + /// Returns this material's prepass vertex shader. If [`ShaderRef::Default`] is returned, the default prepass vertex shader + /// will be used. + fn prepass_vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this material's prepass fragment shader. If [`ShaderRef::Default`] is returned, the default prepass fragment shader + /// will be used. + #[allow(unused_variables)] + fn prepass_fragment_shader() -> ShaderRef { + ShaderRef::Default + } + /// Customizes the default [`RenderPipelineDescriptor`] for a specific entity using the entity's /// [`MaterialPipelineKey`] and [`MeshVertexBufferLayout`] as input. #[allow(unused_variables)] @@ -149,11 +162,22 @@ pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + Sized + 'stat /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] /// asset type. -pub struct MaterialPlugin(PhantomData); +pub struct MaterialPlugin { + /// Controls if the prepass is enabled for the Material. + /// For more information about what a prepass is, see the [`bevy_core_pipeline::prepass`] docs. + /// + /// When it is enabled, it will automatically add the [`PrepassPlugin`] + /// required to make the prepass work on this Material. + pub prepass_enabled: bool, + pub _marker: PhantomData, +} impl Default for MaterialPlugin { fn default() -> Self { - Self(Default::default()) + Self { + prepass_enabled: true, + _marker: Default::default(), + } } } @@ -164,6 +188,7 @@ where fn build(&self, app: &mut App) { app.add_asset::() .add_plugin(ExtractComponentPlugin::>::extract_visible()); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .add_render_command::>() @@ -180,6 +205,10 @@ where ) .add_system_to_stage(RenderStage::Queue, queue_material_meshes::); } + + if self.prepass_enabled { + app.add_plugin(PrepassPlugin::::default()); + } } } @@ -232,6 +261,18 @@ pub struct MaterialPipeline { marker: PhantomData, } +impl Clone for MaterialPipeline { + fn clone(&self) -> Self { + Self { + mesh_pipeline: self.mesh_pipeline.clone(), + material_layout: self.material_layout.clone(), + vertex_shader: self.vertex_shader.clone(), + fragment_shader: self.fragment_shader.clone(), + marker: PhantomData, + } + } +} + impl SpecializedMeshPipeline for MaterialPipeline where M::Data: PartialEq + Eq + Hash + Clone, @@ -321,7 +362,7 @@ pub fn queue_material_meshes( transparent_draw_functions: Res>, material_pipeline: Res>, mut pipelines: ResMut>>, - mut pipeline_cache: ResMut, + pipeline_cache: Res, msaa: Res, render_meshes: Res>, render_materials: Res>, @@ -350,8 +391,8 @@ pub fn queue_material_meshes( let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); let draw_transparent_pbr = transparent_draw_functions.read().id::>(); - let mut view_key = - MeshPipelineKey::from_msaa_samples(msaa.samples) | MeshPipelineKey::from_hdr(view.hdr); + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()) + | MeshPipelineKey::from_hdr(view.hdr); if let Some(Tonemapping::Enabled { deband_dither }) = tonemapping { if !view.hdr { @@ -374,12 +415,18 @@ pub fn queue_material_meshes( MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | view_key; let alpha_mode = material.properties.alpha_mode; - if let AlphaMode::Blend = alpha_mode { - mesh_key |= MeshPipelineKey::TRANSPARENT_MAIN_PASS; + if let AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add = + alpha_mode + { + // Blend, Premultiplied and Add all share the same pipeline key + // They're made distinct in the PBR shader, via `premultiply_alpha()` + mesh_key |= MeshPipelineKey::BLEND_PREMULTIPLIED_ALPHA; + } else if let AlphaMode::Multiply = alpha_mode { + mesh_key |= MeshPipelineKey::BLEND_MULTIPLY; } let pipeline_id = pipelines.specialize( - &mut pipeline_cache, + &pipeline_cache, &material_pipeline, MaterialPipelineKey { mesh_key, @@ -414,7 +461,10 @@ pub fn queue_material_meshes( distance, }); } - AlphaMode::Blend => { + AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::Multiply => { transparent_phase.add(Transparent3d { entity: *visible_entity, draw_function: draw_transparent_pbr, diff --git a/crates/bevy_pbr/src/pbr_material.rs b/crates/bevy_pbr/src/pbr_material.rs index da179b7ccd095..2e65e3e835413 100644 --- a/crates/bevy_pbr/src/pbr_material.rs +++ b/crates/bevy_pbr/src/pbr_material.rs @@ -1,4 +1,7 @@ -use crate::{AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_SHADER_HANDLE}; +use crate::{ + AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, PBR_PREPASS_SHADER_HANDLE, + PBR_SHADER_HANDLE, +}; use bevy_asset::Handle; use bevy_math::Vec4; use bevy_reflect::{std_traits::ReflectDefault, FromReflect, Reflect, TypeUuid}; @@ -295,16 +298,25 @@ bitflags::bitflags! { const OCCLUSION_TEXTURE = (1 << 3); const DOUBLE_SIDED = (1 << 4); const UNLIT = (1 << 5); - const ALPHA_MODE_OPAQUE = (1 << 6); - const ALPHA_MODE_MASK = (1 << 7); - const ALPHA_MODE_BLEND = (1 << 8); - const TWO_COMPONENT_NORMAL_MAP = (1 << 9); - const FLIP_NORMAL_MAP_Y = (1 << 10); + const TWO_COMPONENT_NORMAL_MAP = (1 << 6); + const FLIP_NORMAL_MAP_Y = (1 << 7); + const ALPHA_MODE_RESERVED_BITS = (Self::ALPHA_MODE_MASK_BITS << Self::ALPHA_MODE_SHIFT_BITS); // ← Bitmask reserving bits for the `AlphaMode` + const ALPHA_MODE_OPAQUE = (0 << Self::ALPHA_MODE_SHIFT_BITS); // ← Values are just sequential values bitshifted into + const ALPHA_MODE_MASK = (1 << Self::ALPHA_MODE_SHIFT_BITS); // the bitmask, and can range from 0 to 7. + const ALPHA_MODE_BLEND = (2 << Self::ALPHA_MODE_SHIFT_BITS); // + const ALPHA_MODE_PREMULTIPLIED = (3 << Self::ALPHA_MODE_SHIFT_BITS); // + const ALPHA_MODE_ADD = (4 << Self::ALPHA_MODE_SHIFT_BITS); // Right now only values 0–5 are used, which still gives + const ALPHA_MODE_MULTIPLY = (5 << Self::ALPHA_MODE_SHIFT_BITS); // ← us "room" for two more modes without adding more bits const NONE = 0; const UNINITIALIZED = 0xFFFF; } } +impl StandardMaterialFlags { + const ALPHA_MODE_MASK_BITS: u32 = 0b111; + const ALPHA_MODE_SHIFT_BITS: u32 = 32 - Self::ALPHA_MODE_MASK_BITS.count_ones(); +} + /// The GPU representation of the uniform data of a [`StandardMaterial`]. #[derive(Clone, Default, ShaderType)] pub struct StandardMaterialUniform { @@ -377,6 +389,9 @@ impl AsBindGroupShaderType for StandardMaterial { flags |= StandardMaterialFlags::ALPHA_MODE_MASK; } AlphaMode::Blend => flags |= StandardMaterialFlags::ALPHA_MODE_BLEND, + AlphaMode::Premultiplied => flags |= StandardMaterialFlags::ALPHA_MODE_PREMULTIPLIED, + AlphaMode::Add => flags |= StandardMaterialFlags::ALPHA_MODE_ADD, + AlphaMode::Multiply => flags |= StandardMaterialFlags::ALPHA_MODE_MULTIPLY, }; StandardMaterialUniform { @@ -414,12 +429,11 @@ impl Material for StandardMaterial { key: MaterialPipelineKey, ) -> Result<(), SpecializedMeshPipelineError> { if key.bind_group_data.normal_map { - descriptor - .fragment - .as_mut() - .unwrap() - .shader_defs - .push("STANDARDMATERIAL_NORMAL_MAP".into()); + if let Some(fragment) = descriptor.fragment.as_mut() { + fragment + .shader_defs + .push("STANDARDMATERIAL_NORMAL_MAP".into()); + } } descriptor.primitive.cull_mode = key.bind_group_data.cull_mode; if let Some(label) = &mut descriptor.label { @@ -428,6 +442,10 @@ impl Material for StandardMaterial { Ok(()) } + fn prepass_fragment_shader() -> ShaderRef { + PBR_PREPASS_SHADER_HANDLE.typed().into() + } + fn fragment_shader() -> ShaderRef { PBR_SHADER_HANDLE.typed().into() } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs new file mode 100644 index 0000000000000..8202409d4aa99 --- /dev/null +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -0,0 +1,611 @@ +use bevy_app::Plugin; +use bevy_asset::{load_internal_asset, AssetServer, Handle, HandleUntyped}; +use bevy_core_pipeline::{ + prelude::Camera3d, + prepass::{ + AlphaMask3dPrepass, DepthPrepass, NormalPrepass, Opaque3dPrepass, ViewPrepassTextures, + DEPTH_PREPASS_FORMAT, NORMAL_PREPASS_FORMAT, + }, +}; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{ + lifetimeless::{Read, SRes}, + Commands, Query, Res, ResMut, Resource, SystemParamItem, + }, + world::{FromWorld, World}, +}; +use bevy_reflect::TypeUuid; +use bevy_render::{ + camera::ExtractedCamera, + mesh::MeshVertexBufferLayout, + prelude::{Camera, Mesh}, + render_asset::RenderAssets, + render_phase::{ + sort_phase_system, AddRenderCommand, DrawFunctions, PhaseItem, RenderCommand, + RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, + }, + render_resource::{ + BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingType, BlendState, BufferBindingType, ColorTargetState, + ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, Extent3d, FragmentState, + FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState, + RenderPipelineDescriptor, Shader, ShaderDefVal, ShaderRef, ShaderStages, ShaderType, + SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines, + StencilFaceState, StencilState, TextureDescriptor, TextureDimension, TextureFormat, + TextureUsages, VertexState, + }, + renderer::RenderDevice, + texture::TextureCache, + view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities}, + Extract, RenderApp, RenderStage, +}; +use bevy_utils::{tracing::error, HashMap}; + +use crate::{ + AlphaMode, DrawMesh, Material, MaterialPipeline, MaterialPipelineKey, MeshPipeline, + MeshPipelineKey, MeshUniform, RenderMaterials, SetMaterialBindGroup, SetMeshBindGroup, + MAX_DIRECTIONAL_LIGHTS, +}; + +use std::{hash::Hash, marker::PhantomData}; + +pub const PREPASS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 921124473254008983); + +pub const PREPASS_BINDINGS_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 5533152893177403494); + +pub struct PrepassPlugin(PhantomData); + +impl Default for PrepassPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for PrepassPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + PREPASS_SHADER_HANDLE, + "prepass.wgsl", + Shader::from_wgsl + ); + + load_internal_asset!( + app, + PREPASS_BINDINGS_SHADER_HANDLE, + "prepass_bindings.wgsl", + Shader::from_wgsl + ); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + render_app + .add_system_to_stage(RenderStage::Extract, extract_camera_prepass_phase) + .add_system_to_stage(RenderStage::Prepare, prepare_prepass_textures) + .add_system_to_stage(RenderStage::Queue, queue_prepass_view_bind_group::) + .add_system_to_stage(RenderStage::Queue, queue_prepass_material_meshes::) + .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage( + RenderStage::PhaseSort, + sort_phase_system::, + ) + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::() + .init_resource::>>() + .add_render_command::>() + .add_render_command::>(); + } +} + +#[derive(Resource)] +pub struct PrepassPipeline { + pub view_layout: BindGroupLayout, + pub mesh_layout: BindGroupLayout, + pub skinned_mesh_layout: BindGroupLayout, + pub material_layout: BindGroupLayout, + pub material_vertex_shader: Option>, + pub material_fragment_shader: Option>, + pub material_pipeline: MaterialPipeline, + _marker: PhantomData, +} + +impl FromWorld for PrepassPipeline { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let asset_server = world.resource::(); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[ + // View + BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }, + ], + label: Some("prepass_view_layout"), + }); + + let mesh_pipeline = world.resource::(); + + PrepassPipeline { + view_layout, + mesh_layout: mesh_pipeline.mesh_layout.clone(), + skinned_mesh_layout: mesh_pipeline.skinned_mesh_layout.clone(), + material_vertex_shader: match M::prepass_vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + material_fragment_shader: match M::prepass_fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + material_layout: M::bind_group_layout(render_device), + material_pipeline: world.resource::>().clone(), + _marker: PhantomData, + } + } +} + +impl SpecializedMeshPipeline for PrepassPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = MaterialPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let mut bind_group_layout = vec![self.view_layout.clone()]; + let mut shader_defs = Vec::new(); + let mut vertex_attributes = Vec::new(); + + // NOTE: Eventually, it would be nice to only add this when the shaders are overloaded by the Material. + // The main limitation right now is that bind group order is hardcoded in shaders. + bind_group_layout.insert(1, self.material_layout.clone()); + + if key.mesh_key.contains(MeshPipelineKey::DEPTH_PREPASS) { + shader_defs.push("DEPTH_PREPASS".into()); + } + + if key.mesh_key.contains(MeshPipelineKey::ALPHA_MASK) { + shader_defs.push("ALPHA_MASK".into()); + } + + if layout.contains(Mesh::ATTRIBUTE_POSITION) { + shader_defs.push("VERTEX_POSITIONS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); + } + + shader_defs.push(ShaderDefVal::Int( + "MAX_DIRECTIONAL_LIGHTS".to_string(), + MAX_DIRECTIONAL_LIGHTS as i32, + )); + + if layout.contains(Mesh::ATTRIBUTE_UV_0) { + shader_defs.push("VERTEX_UVS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_UV_0.at_shader_location(1)); + } + + if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(2)); + shader_defs.push("NORMAL_PREPASS".into()); + + if layout.contains(Mesh::ATTRIBUTE_TANGENT) { + shader_defs.push("VERTEX_TANGENTS".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); + } + } + + if layout.contains(Mesh::ATTRIBUTE_JOINT_INDEX) + && layout.contains(Mesh::ATTRIBUTE_JOINT_WEIGHT) + { + shader_defs.push("SKINNED".into()); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_INDEX.at_shader_location(4)); + vertex_attributes.push(Mesh::ATTRIBUTE_JOINT_WEIGHT.at_shader_location(5)); + bind_group_layout.insert(2, self.skinned_mesh_layout.clone()); + } else { + bind_group_layout.insert(2, self.mesh_layout.clone()); + } + + let vertex_buffer_layout = layout.get_layout(&vertex_attributes)?; + + // The fragment shader is only used when the normal prepass is enabled or the material uses an alpha mask + let fragment = if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) + || key.mesh_key.contains(MeshPipelineKey::ALPHA_MASK) + { + // Use the fragment shader from the material if present + let frag_shader_handle = if let Some(handle) = &self.material_fragment_shader { + handle.clone() + } else { + PREPASS_SHADER_HANDLE.typed::() + }; + + let mut targets = vec![]; + // When the normal prepass is enabled we need a target to be able to write to it. + if key.mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { + targets.push(Some(ColorTargetState { + format: TextureFormat::Rgb10a2Unorm, + blend: Some(BlendState::REPLACE), + write_mask: ColorWrites::ALL, + })); + } + + Some(FragmentState { + shader: frag_shader_handle, + entry_point: "fragment".into(), + shader_defs: shader_defs.clone(), + targets, + }) + } else { + None + }; + + // Use the vertex shader from the material if present + let vert_shader_handle = if let Some(handle) = &self.material_vertex_shader { + handle.clone() + } else { + PREPASS_SHADER_HANDLE.typed::() + }; + + let mut descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: vert_shader_handle, + entry_point: "vertex".into(), + shader_defs, + buffers: vec![vertex_buffer_layout], + }, + fragment, + layout: Some(bind_group_layout), + primitive: PrimitiveState { + topology: key.mesh_key.primitive_topology(), + strip_index_format: None, + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + }, + depth_stencil: Some(DepthStencilState { + format: DEPTH_PREPASS_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::GreaterEqual, + stencil: StencilState { + front: StencilFaceState::IGNORE, + back: StencilFaceState::IGNORE, + read_mask: 0, + write_mask: 0, + }, + bias: DepthBiasState { + constant: 0, + slope_scale: 0.0, + clamp: 0.0, + }, + }), + multisample: MultisampleState { + count: key.mesh_key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("prepass_pipeline".into()), + }; + + // This is a bit risky because it's possible to change something that would + // break the prepass but be fine in the main pass. + // Since this api is pretty low-level it doesn't matter that much, but it is a potential issue. + M::specialize(&self.material_pipeline, &mut descriptor, layout, key)?; + + Ok(descriptor) + } +} + +// Extract the render phases for the prepass +pub fn extract_camera_prepass_phase( + mut commands: Commands, + cameras_3d: Extract< + Query< + ( + Entity, + &Camera, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + ), + With, + >, + >, +) { + for (entity, camera, depth_prepass, normal_prepass) in cameras_3d.iter() { + if !camera.is_active { + continue; + } + + let mut entity = commands.get_or_spawn(entity); + if depth_prepass.is_some() || normal_prepass.is_some() { + entity.insert(( + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + if depth_prepass.is_some() { + entity.insert(DepthPrepass); + } + if normal_prepass.is_some() { + entity.insert(NormalPrepass); + } + } +} + +// Prepares the textures used by the prepass +pub fn prepare_prepass_textures( + mut commands: Commands, + mut texture_cache: ResMut, + msaa: Res, + render_device: Res, + views_3d: Query< + ( + Entity, + &ExtractedCamera, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + ), + ( + With>, + With>, + ), + >, +) { + let mut depth_textures = HashMap::default(); + let mut normal_textures = HashMap::default(); + for (entity, camera, depth_prepass, normal_prepass) in &views_3d { + let Some(physical_target_size) = camera.physical_target_size else { + continue; + }; + + let size = Extent3d { + depth_or_array_layers: 1, + width: physical_target_size.x, + height: physical_target_size.y, + }; + + let cached_depth_texture = depth_prepass.is_some().then(|| { + depth_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + let descriptor = TextureDescriptor { + label: Some("prepass_depth_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: DEPTH_PREPASS_FORMAT, + usage: TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }; + texture_cache.get(&render_device, descriptor) + }) + .clone() + }); + + let cached_normals_texture = normal_prepass.is_some().then(|| { + normal_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("prepass_normal_texture"), + size, + mip_level_count: 1, + sample_count: msaa.samples(), + dimension: TextureDimension::D2, + format: NORMAL_PREPASS_FORMAT, + usage: TextureUsages::RENDER_ATTACHMENT + | TextureUsages::TEXTURE_BINDING, + }, + ) + }) + .clone() + }); + + commands.entity(entity).insert(ViewPrepassTextures { + depth: cached_depth_texture, + normal: cached_normals_texture, + size, + }); + } +} + +#[derive(Default, Resource)] +pub struct PrepassViewBindGroup { + bind_group: Option, +} + +pub fn queue_prepass_view_bind_group( + render_device: Res, + prepass_pipeline: Res>, + view_uniforms: Res, + mut prepass_view_bind_group: ResMut, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + prepass_view_bind_group.bind_group = + Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: view_binding, + }], + label: Some("prepass_view_bind_group"), + layout: &prepass_pipeline.view_layout, + })); + } +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_prepass_material_meshes( + opaque_draw_functions: Res>, + alpha_mask_draw_functions: Res>, + prepass_pipeline: Res>, + mut pipelines: ResMut>>, + pipeline_cache: Res, + msaa: Res, + render_meshes: Res>, + render_materials: Res>, + material_meshes: Query<(&Handle, &Handle, &MeshUniform)>, + mut views: Query<( + &ExtractedView, + &VisibleEntities, + &mut RenderPhase, + &mut RenderPhase, + Option<&DepthPrepass>, + Option<&NormalPrepass>, + )>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let opaque_draw_prepass = opaque_draw_functions + .read() + .get_id::>() + .unwrap(); + let alpha_mask_draw_prepass = alpha_mask_draw_functions + .read() + .get_id::>() + .unwrap(); + for ( + view, + visible_entities, + mut opaque_phase, + mut alpha_mask_phase, + depth_prepass, + normal_prepass, + ) in &mut views + { + let mut view_key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + if depth_prepass.is_some() { + view_key |= MeshPipelineKey::DEPTH_PREPASS; + } + if normal_prepass.is_some() { + view_key |= MeshPipelineKey::NORMAL_PREPASS; + } + + let rangefinder = view.rangefinder3d(); + + for visible_entity in &visible_entities.entities { + let Ok((material_handle, mesh_handle, mesh_uniform)) = material_meshes.get(*visible_entity) else { + continue; + }; + + let (Some(material), Some(mesh)) = ( + render_materials.get(material_handle), + render_meshes.get(mesh_handle), + ) else { + continue; + }; + + let mut mesh_key = + MeshPipelineKey::from_primitive_topology(mesh.primitive_topology) | view_key; + let alpha_mode = material.properties.alpha_mode; + match alpha_mode { + AlphaMode::Opaque => {} + AlphaMode::Mask(_) => mesh_key |= MeshPipelineKey::ALPHA_MASK, + AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::Multiply => continue, + } + + let pipeline_id = pipelines.specialize( + &pipeline_cache, + &prepass_pipeline, + MaterialPipelineKey { + mesh_key, + bind_group_data: material.key.clone(), + }, + &mesh.layout, + ); + let pipeline_id = match pipeline_id { + Ok(id) => id, + Err(err) => { + error!("{}", err); + continue; + } + }; + + let distance = + rangefinder.distance(&mesh_uniform.transform) + material.properties.depth_bias; + match alpha_mode { + AlphaMode::Opaque => { + opaque_phase.add(Opaque3dPrepass { + entity: *visible_entity, + draw_function: opaque_draw_prepass, + pipeline_id, + distance, + }); + } + AlphaMode::Mask(_) => { + alpha_mask_phase.add(AlphaMask3dPrepass { + entity: *visible_entity, + draw_function: alpha_mask_draw_prepass, + pipeline_id, + distance, + }); + } + AlphaMode::Blend + | AlphaMode::Premultiplied + | AlphaMode::Add + | AlphaMode::Multiply => {} + } + } + } +} + +pub struct SetPrepassViewBindGroup; +impl RenderCommand