Compare commits

...

151 Commits

Author SHA1 Message Date
François
5c759a1be8
Release 0.14.2 2024-09-06 18:33:25 +02:00
François Mockers
79b248c7ce
fix imports in example ui_texture_slice_flip_and_tile (#15064)
# Objective

- don't use a sub crate in an example

## Solution

- fix imports
2024-09-06 18:33:25 +02:00
ickshonpe
cd9fec3547
UI outlines radius (#15018)
Fixes #13479

This also fixes the gaps you can sometimes observe in outlines
(screenshot from main, not this PR):

<img width="636" alt="outline-gaps"
src="https://github.com/user-attachments/assets/c11dae24-20f5-4aea-8ffc-1894ad2a2b79">

The outline around the last item in each section has vertical gaps.

Draw the outlines with corner radius using the existing border rendering
for uinodes. The outline radius is very simple to calculate. We just
take the computed border radius of the node, and if it's greater than
zero, add it to the distance from the edge of the node to the outer edge
of the node's outline.

---

<img width="634" alt="outlines-radius"
src="https://github.com/user-attachments/assets/1ecda26c-65c5-41ef-87e4-5d9171ddc3ae">

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
2024-09-05 22:02:42 +02:00
ickshonpe
dd929ad867
UI texture slice texture flipping reimplementation (#15034)
# Objective

Fixes #15032

## Solution

Reimplement support for the `flip_x` and `flip_y` fields.
This doesn't flip the border geometry, I'm not really sure whether that
is desirable or not.
Also fixes a bug that was causing the side and center slices to tile
incorrectly.

### Testing

```
cargo run --example ui_texture_slice_flip_and_tile
```

## Showcase
<img width="787" alt="nearest"
src="https://github.com/user-attachments/assets/bc044bae-1748-42ba-92b5-0500c87264f6">
With tiling need to use nearest filtering to avoid bleeding between the
slices.

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-05 22:01:10 +02:00
ickshonpe
6231ed02dd
Ignore clicks on uinodes outside of rounded corners (#14957)
Fixes #14941

1. Add a `resolved_border_radius` field to `Node` to hold the resolved
border radius values.
2. Remove the border radius calculations from the UI's extraction
functions.
4. Compute the border radius during UI relayouts in `ui_layout_system`
and store them in `Node`.
5. New `pick_rounded_rect` function based on the border radius SDF from
`ui.wgsl`.
6. Use `pick_rounded_rect` in `focus` and `picking_backend` to check if
the pointer is hovering UI nodes with rounded corners.
---

```
cargo run --example button
```

https://github.com/user-attachments/assets/ea951a64-17ef-455e-b5c9-a2e6f6360648

Modified button example with buttons with different corner radius:

```
use bevy::{color::palettes::basic::*, prelude::*, winit::WinitSettings};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Only run the app when there is user input. This will significantly reduce CPU/GPU use.
        .insert_resource(WinitSettings::desktop_app())
        .add_systems(Startup, setup)
        .add_systems(Update, button_system)
        .run();
}

const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);

fn button_system(
    mut interaction_query: Query<
        (
            &Interaction,
            &mut BackgroundColor,
            &mut BorderColor,
            &Children,
        ),
        (Changed<Interaction>, With<Button>),
    >,
    mut text_query: Query<&mut Text>,
) {
    for (interaction, mut color, mut border_color, children) in &mut interaction_query {
        let mut text = text_query.get_mut(children[0]).unwrap();
        match *interaction {
            Interaction::Pressed => {
                text.sections[0].value = "Press".to_string();
                *color = PRESSED_BUTTON.into();
                border_color.0 = RED.into();
            }
            Interaction::Hovered => {
                text.sections[0].value = "Hover".to_string();
                *color = HOVERED_BUTTON.into();
                border_color.0 = Color::WHITE;
            }
            Interaction::None => {
                text.sections[0].value = "Button".to_string();
                *color = NORMAL_BUTTON.into();
                border_color.0 = Color::BLACK;
            }
        }
    }
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    // ui camera
    commands.spawn(Camera2dBundle::default());
    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                row_gap: Val::Px(10.),
                ..default()
            },
            ..default()
        })
        .with_children(|parent| {
            for border_radius in [
                BorderRadius {
                    top_left: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    top_right: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    bottom_right: Val::ZERO,
                    ..BorderRadius::MAX
                },
                BorderRadius {
                    bottom_left: Val::ZERO,
                    ..BorderRadius::MAX
                },
            ] {
                parent
                    .spawn(ButtonBundle {
                        style: Style {
                            width: Val::Px(150.0),
                            height: Val::Px(65.0),
                            border: UiRect::all(Val::Px(5.0)),
                            // horizontally center child text
                            justify_content: JustifyContent::Center,
                            // vertically center child text
                            align_items: AlignItems::Center,
                            ..default()
                        },
                        border_color: BorderColor(Color::BLACK),
                        border_radius,
                        background_color: NORMAL_BUTTON.into(),
                        ..default()
                    })
                    .with_child(TextBundle::from_section(
                        "Button",
                        TextStyle {
                            font: asset_server.load("fonts/FiraSans-Bold.ttf"),
                            font_size: 40.0,
                            color: Color::srgb(0.9, 0.9, 0.9),
                        },
                    ));
            }
        });
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Matty <weatherleymatthew@gmail.com>
2024-09-05 22:01:05 +02:00
ickshonpe
9afe666f6d
UI texture atlas slice shader (#14990)
Fixes https://github.com/bevyengine/bevy/issues/14183

Reimplement the UI texture atlas slicer using a shader.

The problems with #14183 could be fixed more simply by hacking around
with the coordinates and scaling but that way is very fragile and might
get broken again the next time we make changes to the layout
calculations. A shader based solution is more robust, it's impossible
for gaps to appear between the image slices with these changes as we're
only drawing a single quad.

I've not tried any benchmarks yet but it should much more efficient as
well, in the worst cases even hundreds or thousands of times faster.

Maybe could have used the UiMaterialPipeline. I wrote the shader first
and used fat vertices and then realised it wouldn't work that way with a
UiMaterial. If it's rewritten it so it puts all the slice geometry in
uniform buffer, then it might work? Adding the uniform buffer would
probably make the shader more complicated though, so don't know if it's
even worth it. Instancing is another alternative.

The examples are working and it seems to match the old API correctly but
I've not used the texture atlas slicing API for anything before, I
reviewed the PR but that was back in January.

Needs a review by someone who knows the rendering pipeline and wgsl
really well because I don't really have any idea what I'm doing.
2024-09-05 21:39:43 +02:00
ickshonpe
489c1e1a87
Resolve UI outlines using the correct target's viewport size (#14947)
`resolve_outlines_system` wasn't updated when multi-window support was
added and it always uses the size of the primary window when resolving
viewport coords, regardless of the layout's camera target.

Fixes #14945

It's awkward to get the viewport size of the target for an individual
node without walking the tree or adding extra fields to `Node`, so I
removed `resolve_outlines_system` and instead the outline values are
updated in `ui_layout_system`.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-05 21:37:24 +02:00
MichiRecRoom
a4ea708b69
Don't require going through bevy_animation::prelude to get to certain items in bevy_animation (#14979)
# Objective
* Fixes https://github.com/bevyengine/bevy/issues/14889

## Solution
Exposes `bevy_animation::{animatable, graph, transition}` to the world.

## Testing
- Did you test these changes? If so, how?
- These changes do not need testing, as they do not modify/add/remove
any functionality.
- ~~Are there any parts that need more testing?~~
- ~~How can other people (reviewers) test your changes? Is there
anything specific they need to know?~~
- ~~If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?~~

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-05 21:35:40 +02:00
François Mockers
ff3d632f31
don't use padding for layout (#14944)
# Objective

- Fixes #14792 
- Padding is already handled by taffy, don't handle it also on Bevy side

## Solution

- Remove extra computation added in
https://github.com/bevyengine/bevy/pull/14777
2024-09-05 21:35:40 +02:00
akimakinai
7b33038393
Fix Gizmo joint rendering in webgpu (#14721)
# Objective

- Gizmo rendering on WebGPU has been fixed by #14653, but gizmo joints
still cause error
(https://github.com/bevyengine/bevy/issues/14696#issuecomment-2283689669)
when enabled.

## Solution

- Applies the same fix as #14653 to Gizmo joints.

I'm noob and just copied their solution, please correct me if I did
something wrong.

## Testing

- Tested 2d-gizmos and 3d-gizmos examples in WebGPU on Chrome. No
rendering errors, and the gizmo joints are apparently rendered ok.
2024-09-05 21:35:40 +02:00
Luca Della Vedova
d7b6e81fb2
Fix commands not being Send / Sync in 0.14 (#14392)
# Objective

Fixes Commands not being `Send` or `Sync` anymore in 0.14 by
implementing `Send` and `Sync` for `RawCommandQueue`.

## Solution

Reference discussion in
[discord](https://discord.com/channels/691052431525675048/691052431974465548/1259464518539411570).
It seems that in https://github.com/bevyengine/bevy/pull/13249, when
adding a `RawCommandQueue` variant to the `InternalQueue`, the `Send /
Sync` traits were not implemented for it, which bubbled up all the way
to `Commands` not being `Send / Sync` anymore.
I am not very familiar with the ECS internals so I can't say whether the
`RawCommandQueue` is safe to be shared between threads, but I know for
sure that before the linked PR `Commands` were indeed `Send` and `Sync`
so that PR broke "some workflows" (mandatory
[xkcd](https://xkcd.com/1172/)).

## Testing

This PR itself includes a compile test to make sure `Commands` will
implement `Send` and `Sync`. The test itself fails without the
implementation and succeeds with it.
Furthermore, if I cherry pick the test to a previous release (i.e. 0.13)
it indeed succeeds, showing that this is a regression specific to 0.14.

---------

Signed-off-by: Luca Della Vedova <lucadv@intrinsic.ai>
2024-09-05 21:35:40 +02:00
Nihilistas
6a4b48ba6b
#14143 - fix bevy_ui padding (#14777)
# Objective

fixes #14143

## Solution

- removed the temporary blocker if statement when setting padding in
`Style`
- adjusted the `layout_location` and `layout_size` so they use
`layout.padding` which we already get from Taffy

## Testing

- this is the test code I used:
```rust
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
){
    let font = asset_server.load("fonts/FiraSans-Bold.ttf");
    commands.spawn(Camera2dBundle::default());

    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Px(200.),
                height: Val::Px(100.),
                align_items: AlignItems::Center,
                justify_content: JustifyContent::Center,
                align_self: AlignSelf::Center,
                justify_self: JustifySelf::Center,
                ..Default::default()
            },
            background_color: BackgroundColor(Color::srgb(0.,1., 1.)),
            ..Default::default()
        })
        .with_children(|builder| {
            builder.spawn((TextBundle::from_section(
                    "Hello World",
                    TextStyle {
                        font,
                        font_size: 32.0,
                        color: Color::WHITE,
                        },
                ).with_style(Style {
                    padding: UiRect::all(Val::Px(10.)),
                    width: Val::Px(100.),
                    height: Val::Px(100.),
                    ..Default::default()
                }).with_background_color(Color::srgb(1.,0., 0.)),
            ));
            // spawn an image bundle
            builder.spawn(ImageBundle {
                style: Style {
                    padding: UiRect::all(Val::Px(10.)),
                    width: Val::Px(100.),
                    height: Val::Px(100.),
                    ..Default::default()
                },
                image: asset_server.load("square.png").into(),
                ..Default::default()
            });
        });
}
```

- I tested 5 cases: 10px padding from all sides, and 10px padding from
left, right, bottom, and top separately

- **For reviewers**: please check more cases or try to run it on some
more complicated real-world UI

## Showcase

<img width="374" alt="Screenshot 2024-08-16 at 09 28 04"
src="https://github.com/user-attachments/assets/59b85b00-e255-4669-be13-a287ef35d4d9">
<img width="288" alt="Screenshot 2024-08-16 at 09 28 47"
src="https://github.com/user-attachments/assets/170a79b1-ec9c-45f9-82f5-ba7fa4029334">
<img width="274" alt="Screenshot 2024-08-16 at 09 45 16"
src="https://github.com/user-attachments/assets/e3fd9b59-b41f-427d-8c07-5acdf1dc5ecf">
<img width="292" alt="Screenshot 2024-08-16 at 09 45 36"
src="https://github.com/user-attachments/assets/c4f708aa-3f0d-4ff3-b779-0d4ed5f6ba73">
<img width="261" alt="Screenshot 2024-08-16 at 09 45 58"
src="https://github.com/user-attachments/assets/eba1e26f-04ca-4178-87c8-3a79daff3a9a">

---------

Co-authored-by: dpeke <dpekelis@funstage.com>
2024-09-05 21:35:40 +02:00
robtfm
650e7c9eb4
apply finished animations (#14743)
# Objective

fix #14742

## Solution

the issue arises because "finished" animations (where current time >=
last keyframe time) are not applied at all.
when transitioning from a finished animation to another later-indexed
anim, the transition kind-of works because the finished anim is skipped,
then the new anim is applied with a lower weight (weight / total_weight)
when transitioning from a finished animation to another earlier-indexed
anim, the transition is instant as the new anim is applied with 1.0 (as
weight == total_weight for the first applied), then the finished
animation is skipped.

to fix this we can always apply every animation based on the nearest 2
keyframes, and clamp the interpolation between them to [0,1].

pros:
- finished animations can be transitioned out of correctly
- blended animations where some curves have a last-keyframe before the
end of the animation will blend properly
- animations will actually finish on their last keyframe, rather than a
fraction of a render-frame before the end

cons:
- we have to re-apply finished animations every frame whether it's
necessary or not. i can't see a way to avoid this.
2024-09-05 21:35:40 +02:00
eckz
0793d05256
Making DynamicEnum::is_dynamic() return true (#14732)
# Objective

- Right now `DynamicEnum::is_dynamic()` is returning `false`. I don't
think this was expected, since the rest of `Dynamic*` types return
`true`.

## Solution

- Making `DynamicEnum::is_dynamic()` return true

## Testing

- Added an extra unit test to verify that `.is_dynamic()` returns
`true`.
2024-09-05 21:35:40 +02:00
AFKessen
dcb506ea70
Fix 3D Gizmo webgpu rendering (#14653)
# Objective

The changes made in https://github.com/bevyengine/bevy/pull/12252
introduced an previously fixed bug in webgpu rendering.

## Solution

This fix is based on https://github.com/bevyengine/bevy/pull/8910 and
applies the same vertex buffer layout assignment for the LineGizmo
Pipeline.

## Testing

- Tested the 3D Gizmo example in webgpu and webgl environments

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-09-05 21:35:40 +02:00
charlotte
c661da1d42
Fix TAA on camera with viewport (#14582)
# Objective

Closes #14526 

## Solution

The history texture was being created incorrectly with the viewport size
rather than target size. When viewport < target, this meant that the
render attachments would differer in size which causes a wgpu validation
error.

## Testing

Example in linked issue works.
2024-09-05 21:35:40 +02:00
Ben Frankel
216dba23cb
Explicitly order CameraUpdateSystem before UiSystem::Prepare (#14609)
Fixes https://github.com/bevyengine/bevy/issues/14277.

May also fix https://github.com/bevyengine/bevy/issues/14255, needs
verification.

Explicitly order `CameraUpdateSystem` before `UiSystem::Prepare`, so
that when the window resizes, `camera_system` will update the `Camera`'s
viewport size before `ui_layout_system` also reacts to the window resize
and tries to read the new `Camera` viewport size to set UI node sizes
accordingly.

I tested that explicitly ordering `CameraUpdateSystem` _after_ triggers
the buggy behavior, and explicitly ordering it _before_ does not trigger
the buggy behavior or crash the app (which also demonstrates that the
system sets are ambiguous).

---

`CameraUpdateSystem` is now explicitly ordered before
`UiSystem::Prepare` instead of being ambiguous with it.
2024-09-05 21:35:36 +02:00
Brian Reavis
7d63efe4c2
Don’t prepare lights (and shadow map textures) for 2D cameras (#14574)
# Objective

When running the Metal debugger I noticed that 2D cameras have shadow
map textures from `bevy_pbr` built for them. For a 2560x1440 2D camera,
this PR saves about 40mb of texture memory.


![image](https://github.com/user-attachments/assets/925e9392-2721-41bb-83e9-25c84fd563cd)


![image](https://github.com/user-attachments/assets/0cc3c0a9-cbf7-431c-b444-952c28d4e9d0)


## Solution

- Added `With<Camera3d>` filter to the appropriate view queries.

## Testing

- This is a trivial fix (the examples still work)
2024-09-05 21:34:05 +02:00
Lixou
8f17fdafc3
Make AnimationPlayer::start and ::play work accordingly to documentation (#14546)
# Objective

While scrolling through the animation crate, I was confused by the docs
and code for the two methods. One does nothing for resetting an
animation, the other just resets the weights for whatever reason.

## Solution

Made the functions work accordingly to their documentation.
`start` now replays the animation.
And `play` doesn't reset the weight anymore. I have no clue why it
should. `play` is there to don't do anything to an already existing
animation.

## Testing

I tested the current 0.14 code with bevy playground in the Animated Fox
exampled and changed it such that on pressing space, either `play` or
`start` would be called. Neither changed anything.
I then inlined the function for start there and it restarted the
animation, so it should work.

---

## Migration Guide

`AnimationPlayer::start` now correspondingly to its docs restarts a
running animation.
`AnimationPlayer::play` doesn't reset the weight anymore.
2024-09-05 21:34:05 +02:00
Ben Frankel
dff6471049
Fix hue mixing for Lcha and Oklcha (#14468)
# Objective

Fix erroneous hue mixing in `Lcha` and `Oklcha`. Purple + Red == Green
is the current behavior.

## Solution

Use `crate::color_ops::lerp_hue` to handle the wrap-around at 360
degrees, the same way that `Hsla`, `Hsva`, and `Hwba` do it.

## Testing

Game jamming, but tested that the workaround below produces
correct-looking colors in my jam game.
2024-09-05 21:34:05 +02:00
Hannes Karppila
5e2c04516c
Fix repeated animation transition bug (#14411)
# Objective

Fixes #13910

When a transition is over, the animation is stopped. There was a race
condition; if an animation was started while it also had an active
transition, the transition ending would then incorrectly stop the newly
added animation.

## Solution

When starting an animation, cancel any previous transition for the same
animation.

## Testing

The changes were tested manually, mainly by using the `animated_fox`
example. I also tested with changes from
https://github.com/bevyengine/bevy/pull/13909.

I'd like to have an unit test for this as well, but it seems quite
complex to do, as I'm not sure how I would detect an incorrectly paused
animation.

Reviewers can follow the instructions in #13910 to reproduce.

Tested on macos 14.4 (M3 processor) Should be platform-independent,
though.
2024-09-05 21:34:05 +02:00
charlotte
3830e71076
Set scissor on upscale to match camera viewport (#14287)
# Objective

When the user renders multiple cameras to the same output texture, it
can sometimes be confusing what `ClearColorConfig` is necessary for each
camera to avoid overwriting the previous camera's output. This is
particular true in cases where the user uses mixed HDR cameras, which
means that their scene is being rendered to different internal textures.

## Solution

When a view has a configured viewport, set the GPU scissor in the
upscaling node so we don't overwrite areas that were written to by other
cameras.

## Testing

Ran the `split_screen` example.
2024-09-05 21:34:05 +02:00
François
d65eb39277
Release 0.14.1 2024-08-02 20:35:38 +02:00
François
c217238c5e
clippy happy 2024-08-02 20:35:38 +02:00
Alessio Marchi
61c683fb6a
feat: add insert_after and insert_startup_before (#13941)
# Objective

Fixes #13866 

## Solution

Add `insert_before` in **FixedMainScheduleOrder** and
**MainScheduleOrder**, add `insert_startup_before` in
**MainScheduleOrder**, applying the same logic as `insert_after`, except
for parameters naming and insertion index.
2024-08-02 19:55:33 +02:00
James O'Brien
833ee3f577
Skip batching for phase items from other pipelines (#14296)
# Objective

- Fix #14295

## Solution

- Early out when `GFBD::get_index_and_compare_data` returns None.

## Testing

- Tested on a selection of examples including `many_foxes` and
`3d_shapes`.
- Resolved the original issue in `bevy_vector_shapes`.
2024-08-02 19:26:47 +02:00
JJJimbo1
d8886408bf
fix asymmetrical 9-slicing (#14148)
# Objective

Fixes #14147.

## Solution

Modify the slicing checks and algorithm to fully allow asymmetrical
textures to work.
Some opinionated code cleanup.

## Testing

Tested using the ui_texture_slice example and a custom asymmetrical
texture.

Before:

![asymmetrical_texture_slice_before](https://github.com/bevyengine/bevy/assets/88861660/00dafce1-904a-41ac-b5d9-faaf087b0681)

After:

![asymmetrical_texture_slice_after](https://github.com/bevyengine/bevy/assets/88861660/f3d742f3-6157-4d35-b383-aee4b8f6e7d0)

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-08-02 19:26:47 +02:00
Brezak
3a6176b6cb
Properly handle repeated window close requests (#14573)
# Objective

Spamming the window close button on window may trigger a panic.

```
thread 'main' panicked at <Bevy repo>\crates\bevy_ecs\src\system\commands\mod.rs:1320:13:
error[B0003]: Could not insert a bundle (of type `bevy_window:🪟:ClosingWindow`) for entity 0v1#4294967296 because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Encountered a panic when applying buffers for system `bevy_window::system::close_when_requested`!
2024-08-01T15:00:29.742612Z  WARN bevy_ecs::world::command_queue: CommandQueue has un-applied commands being dropped. Did you forget to call SystemState::apply?
Encountered a panic in system `bevy_app::main_schedule::Main::run_main`!
error: process didn't exit successfully: `target\debug\bevy.exe` (exit code: 101)
```

## Solution

Don't panic when trying to insert the `ClosingWindow` component into a
entity.

## Testing

Found and tested on windows. I haven't checked if this bug happens on
linux or macos.
For testing I ran this code:

```rust
use std::{thread, time::Duration};

use bevy::prelude::*;

fn lag() {
    thread::sleep(Duration::from_millis(300));
}

fn main() -> AppExit {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Update, lag)
        .run()
}
```

Then spammed the window close button. The panic no longer occurs.
2024-08-02 19:26:47 +02:00
Tamás Kiss
ebfe545f79
fix issue with phantom ui node children (#14490)
# Objective

The `ui_layout_system` relies on change detection to sync parent-child
relation to taffy. The children need to by synced before node removal to
avoid trying to set deleted nodes as children (due to how the different
queries collect entities). This however may leave nodes that were
removed set as children to other nodes in special cases.

Fixes #11385

## Solution

The solution is simply to re-sync the changed children after the nodes
are removed.

## Testing

Tested with `sickle_ui` where docking zone highlights would end up
glitched when docking was done in a certain manner:
- run the `docking_zone_splits` example
- pop out a tab from the top
- dock the floating panel in the center right
- grab another tab and try to hover the original static docking zone:
the highlight is semi-stuck
- (NOTE: sometimes it worked even without the fix due to scheduling
order not producing the bugged query results)

After the fix, the issue is no longer present.

NOTE: The performance impact should be minimal, as the child sync relies
on change detection. The change detection was also the reason the parent
nodes remained "stuck" with the phantom children if no other update were
done to them.
2024-08-02 19:26:47 +02:00
Sarthak Singh
0886e6a302
Disabled usage of the POLYGON_MODE_LINE gpu feature in the examples (#14402)
Fixes #14353
Fixes #14371

---------

Signed-off-by: Sarthak Singh <sarthak.singh99@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com>
2024-08-02 19:26:47 +02:00
Giacomo Stevanato
587cffdcde
Fix bevy_render's image dependency version (#14505)
# Objective

- `bevy_render` depends on `image 0.25` but uses `image::ImageReader`
which was added only in `image 0.25.2`
- users that have `image 0.25` in their `Cargo.lock` and update to the
latest `bevy_render` may thus get a compilation due to this (at least I
did)

## Solution

- Properly set the correct minimum version of `image` that `bevy_render`
depends on.
2024-08-02 19:25:42 +02:00
charlotte
27cafdae9b
Fix breaking image 0.25.2 release. (#14421)
Deprecated item breaking ci:
https://github.com/image-rs/image/releases/tag/v0.25.2. See
https://github.com/bevyengine/bevy/actions/runs/10030764981/job/27720434072?pr=14419
for example.
2024-08-02 19:25:42 +02:00
Brian Reavis
680c994100
Fix TextureCache memory leak and add is_empty() method (#14480)
# Objective

Fix a memory leak in `TextureCache` caused by the internal HashMap never
having unused entries cleared.

This isn't a giant memory leak, given the unused entries are simply
empty vectors. Though, if someone goes and resizes a window a bunch, it
can lead to hundreds/thousands of TextureDescriptor keys adding up in
the hashmap – which isn't ideal.

## Solution

- Only retain hashmap entries that still have textures.
- I also added an `is_empty()` method to `TextureCache`, which is useful
for 3rd-party higher-level caches that might have individual caches by
view entity or texture type, for example.

## Testing

- Verified the examples still work (this is a trivial change)
2024-08-02 19:24:54 +02:00
BD103
c4ea4776c4
Fix bevy_winit not building with serialize feature (#14469)
# Objective

- `bevy_winit` fails to build with just the `serialize` feature.
- Caught by [`flag-frenzy`](https://github.com/TheBevyFlock/flag-frenzy)
in [this
run](https://github.com/TheBevyFlock/flag-frenzy/actions/runs/10087486444/job/27891723948),
using the new, nuanced configuration system!

## Solution

- It was failing because `bevy_winit` did not pass the `serialize` flag
to two of its dependencies: `bevy_input` and `bevy_window`.
- To fix this, add these crates to the feature flag.

## Testing

```bash
# On Linux, you must also specify a backend: `x11` or `wayland`.
# You can do this with `-F serialize,x11`, etc.
cargo check -p bevy_winit --no-default-features -F serialize
```
2024-08-02 19:24:54 +02:00
NiseVoid
9daf16bb87
Handle 0 height in prepare_bloom_textures (#14423)
# Objective

- Fix a confusing panic when the viewport width is non-zero and the
height is 0, `prepare_bloom_textures` tries to create a `4294967295x1`
texture.

## Solution

- Avoid dividing by zero
- Apps still crash after this, but now on a more reasonable error about
the zero-size viewport

## Testing

- I isolated and tested the math. A height of 0 sets `mip_height_ratio`
to `inf`, causing the width to explode if it isn't also 0
2024-08-02 19:24:54 +02:00
BD103
2870d89d5c
Fix bevy_gltf PBR features not enabling corresponding bevy_pbr flags (#14486)
# Objective

- `bevy_gltf` does not build with only the
`pbr_multi_layer_material_textures` or `pbr_anisotropy_texture`
features.
- Caught by [`flag-frenzy`](https://github.com/TheBevyFlock/flag-frenzy)
in [this
run](https://github.com/TheBevyFlock/flag-frenzy/actions/runs/10087486444/job/27891723948).

## Solution

- This error was due to the feature not enabling the corresponding
feature in `bevy_pbr`. Adding these flags as a dependency fixes this
error.

## Testing

The following commands fail on `main`, but pass with this PR:

```bash
cargo check -p bevy_gltf --no-default-features -F pbr_multi_layer_material_textures
cargo check -p bevy_gltf --no-default-features -F pbr_anisotropy_texture
```
2024-08-02 19:24:54 +02:00
Al M.
2e577bcdc9
Fix single keyframe animations. (#14344)
# Objective

For clips with more than one curve, only the first was being applied if
there is only one keyframe in it.

## Solution

Continue!
2024-08-02 19:24:54 +02:00
Peter Hayman
6882420c7f
Add some missing reflect attributes (#14259)
# Objective

- Some types are missing reflection attributes, which means we can't use
them in scene serialization etc.
- Effected types
   - `BorderRadius`
   - `AnimationTransitions`
   - `OnAdd`
   - `OnInsert`
   - `OnRemove`
- My use-case for `OnAdd` etc to derive reflect is 'Serializable
Observer Components'. Add the component, save the scene, then the
observer is re-added on scene load.

```rust
#[derive(Reflect)]
struct MySerializeableObserver<T: Event>(#[reflect(ignore)]PhantomData<T>);

impl<T: Event> Component for MySerializeableObserver<T> {
  const STORAGE_TYPE: StorageType  = StorageType::Table;
    fn register_component_hooks(hooks: &mut ComponentHooks) {
      hooks.on_add(|mut world, entity, _| {
        world
          .commands()
          .entity(entity)
          .observe(|_trigger: Trigger<T>| {
            println!("it triggered etc.");
          });
    });
  }
}
```

## Solution

- Add the missing traits

---
2024-08-02 19:20:44 +02:00
Sou1gh0st
df3fcbd116
Fix incorrect function calls to hsv_to_rgb in render debug code. (#14260)
# Objective

- Fixes https://github.com/bevyengine/bevy/issues/14139

## Solution

- correct the input parameters at these call sites.

## Testing

1. Use a 3D scene example with PBR lighting and shadows enabled, such as
the `shadow_caster_receiver` and `load_gltf` example, for testing.
2. Enable relevant shader defines in crates/bevy_pbr/src/pbr_material.rs
for the StandardMaterial.
```rust
impl Material for StandardMaterial {
    // ...
    fn specialize(
            _pipeline: &MaterialPipeline<Self>,
            descriptor: &mut RenderPipelineDescriptor,
            _layout: &MeshVertexBufferLayoutRef,
            key: MaterialPipelineKey<Self>,
        ) -> Result<(), SpecializedMeshPipelineError> {
            // ...
            // shader_defs.push("CLUSTERED_FORWARD_DEBUG_Z_SLICES".into());
            // shader_defs.push("CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY".into());
            shader_defs.push("DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES".into());
            // ...
    }
}
``` 

## Showcase
### CLUSTERED_FORWARD_DEBUG_Z_SLICES
- example: examples/3d/shadow_caster_receiver.rs

![Screenshot2024_07_10_143150](https://github.com/bevyengine/bevy/assets/6300263/fbd12712-5cb9-489d-a7d1-ed55f72fb234)

### CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY
- example: examples/3d/shadow_caster_receiver.rs

![Screenshot2024_07_10_143312](https://github.com/bevyengine/bevy/assets/6300263/8eca5d7a-27b6-4ff5-9f8d-d10b49b3f990)

### DIRECTIONAL_LIGHT_SHADOW_MAP_DEBUG_CASCADES
For this one, we need to use a large scene and modity the
`CascadeShadowConfigBuilder`, here is a simple patch for the `load_gltf`
example:
```
diff --git a/examples/3d/load_gltf.rs b/examples/3d/load_gltf.rs
index 358446238..9403aa288 100644
--- a/examples/3d/load_gltf.rs
+++ b/examples/3d/load_gltf.rs
@@ -18,7 +18,7 @@ fn main() {
 fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
     commands.spawn((
         Camera3dBundle {
-            transform: Transform::from_xyz(0.7, 0.7, 1.0)
+            transform: Transform::from_xyz(0.7, 0.7, 2.0)
                 .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
             ..default()
         },
@@ -39,30 +39,40 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
         // We also adjusted the shadow map to be larger since we're
         // only using a single cascade.
         cascade_shadow_config: CascadeShadowConfigBuilder {
-            num_cascades: 1,
-            maximum_distance: 1.6,
+            num_cascades: 5,
+            maximum_distance: 20.0,
             ..default()
         }
         .into(),
         ..default()
     });
+
     commands.spawn(SceneBundle {
         scene: asset_server
             .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
         ..default()
     });
+
+    for i in 1..=10 {
+        commands.spawn(SceneBundle {
+            scene: asset_server
+                .load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
+            transform: Transform::from_xyz(i as f32 * 0.5, 0.0, i as f32 * -2.0),
+            ..default()
+        });
+    }
 }
 
 fn animate_light_direction(
     time: Res<Time>,
     mut query: Query<&mut Transform, With<DirectionalLight>>,
 ) {
-    for mut transform in &mut query {
-        transform.rotation = Quat::from_euler(
-            EulerRot::ZYX,
-            0.0,
-            time.elapsed_seconds() * PI / 5.0,
-            -FRAC_PI_4,
-        );
-    }
+    // for mut transform in &mut query {
+    //     transform.rotation = Quat::from_euler(
+    //         EulerRot::ZYX,
+    //         0.0,
+    //         time.elapsed_seconds() * PI / 5.0,
+    //         -FRAC_PI_4,
+    //     );
+    // }
 }
``` 

![Screenshot2024_07_10_145737](https://github.com/bevyengine/bevy/assets/6300263/c5c71894-f9f7-45fa-9b4f-598e324b42d0)

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
2024-08-02 19:19:53 +02:00
François Mockers
295ed1fdb4
fix building cargo_gltf with feature dds (#14360)
# Objective

- Building bevy_gltf with feature dds fails:
```
> cargo build -p bevy_gltf --features dds
   Compiling bevy_core_pipeline v0.15.0-dev (crates/bevy_core_pipeline)
error[E0061]: this function takes 7 arguments but 6 arguments were supplied
   --> crates/bevy_core_pipeline/src/tonemapping/mod.rs:442:5
    |
442 |     Image::from_buffer(
    |     ^^^^^^^^^^^^^^^^^^
...
445 |         bytes,
    |         ----- an argument of type `std::string::String` is missing
    |
note: associated function defined here
   --> crates/bevy_render/src/texture/image.rs:709:12
    |
709 |     pub fn from_buffer(
    |            ^^^^^^^^^^^
help: provide the argument
    |
442 |     Image::from_buffer(/* std::string::String */, bytes, image_type, CompressedImageFormats::NONE, false, image_sampler, RenderAssetUsages::RENDER_WORLD)
    |                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For more information about this error, try `rustc --explain E0061`.
error: could not compile `bevy_core_pipeline` (lib) due to 1 previous error
```
- If you're fixing a specific issue, say "Fixes #X".

## Solution

- enable dds feature in bevy_core_pipeline

## Testing

- `cargo build -p bevy_gltf --features dds`
2024-08-02 19:19:24 +02:00
Sludge
420ca6c43c
Make Viewport::default() return a 1x1 viewport (#14372)
# Objective

- The current default viewport crashes bevy due to a wgpu validation
error, this PR fixes that
- Fixes https://github.com/bevyengine/bevy/issues/14355

## Solution

- `Viewport::default()` now returns a 1x1 viewport

## Testing

- I modified the `3d_viewport_to_world` example to use
`Viewport::default()`, and it works as expected (only the top-left pixel
is rendered)
2024-08-02 19:19:14 +02:00
Lura
42412f3500
Fix error/typo in SMAA shader (#14338)
# Objective

- Actually use the value assigned to `d_xz`, like in [the original SMAA
implementation](https://github.com/iryoku/smaa/blob/master/SMAA.hlsl#L960).
This not already being the case was likely a mistake when converting
from HLSL to WGSL

## Solution

- Use `d_xz.x` and `d_xz.y` instead of `d.x` and `d.z`

## Testing

- Quickly tested on Windows 11, `x86_64-pc-windows-gnu` `1.79.0` with
the latest NVIDIA drivers. App runs with SMAA enabled and everything
seems to work as intended
- I didn't observe any major visual difference between this and the
previous version, though this should be more correct as it matches the
original SMAA implementation
2024-08-02 19:19:14 +02:00
BD103
70a0c211ff
Fix bevy_window failing with serialize feature (#14298)
# Objective

- [`flag-frenzy`](https://github.com/TheBevyFlock/flag-frenzy) found an
issue where `bevy_window` would fail to build when its `serialize`
feature is enabled.
- See
[here](https://github.com/TheBevyFlock/flag-frenzy/actions/runs/9924187577/job/27415224405)
for the specific log.

## Solution

- Turns out it was failing because the `bevy_ecs/serialize` feature was
not enabled. This error can be fixed by adding the flag as a dependency.

## Testing

```bash
cargo check -p bevy_window -F serialize
# Or if you're very cool...
flag-frenzy --manifest-path path/to/bevy/Cargo.toml --config config -p bevy_window
```
2024-08-02 19:18:25 +02:00
Alix Bott
728c5b98d4
Fix overflow in RenderLayers::iter_layers (#14264)
# Objective

- Fixes overflow when calling `RenderLayers::iter_layers` on layers of
the form `k * 64 - 1`
- Causes a panic in debug mode, and an infinite iterator in release mode

## Solution

- Use `u64::checked_shr` instead of `>>=`

## Testing

- Added a test case for this: `render_layer_iter_no_overflow`
2024-08-02 19:17:57 +02:00
MiniaczQ
524fb01457
Make initial StateTransition run before PreStartup (#14208)
# Objective

- Fixes #14206 

## Solution

- Run initial `StateTransition` as a startup schedule before
`PreStartup`, instead of running it inside `Startup` as an exclusive
system.

Related discord discussion:

https://discord.com/channels/691052431525675048/692572690833473578/1259543775668207678

## Testing

Reproduction now works correctly:

```rs
use bevy::prelude::*;

#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Hash, States)]
enum AppState {
    #[default]
    Menu,
    InGame,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<AppState>()
        .add_systems(Startup, setup)
        .add_systems(OnEnter(AppState::Menu), enter_menu_state)
        .run();
}

fn setup(mut next_state: ResMut<NextState<AppState>>) {
    next_state.set(AppState::Menu);
}

fn enter_menu_state() {
    println!("Entered menu state");
}
```


![image](https://github.com/bevyengine/bevy/assets/13040204/96d7a533-c439-4c0b-8f15-49f620903ce1)


---

## Changelog

- Initial `StateTransition` runs before `PreStartup` instead of inside
`Startup`.
2024-08-02 19:17:57 +02:00
Periwink
7f3fea9a5b
Allow observer systems to have outputs (#14159)
Fixes https://github.com/bevyengine/bevy/issues/14157

- Update the ObserverSystem traits to accept an `Out` parameter

- Added a test where an observer system has a non-empty output which is
piped into another system

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-08-02 19:16:46 +02:00
Matty
d0583c8b54
Fix swapped docs for Rot2::rotation_to/from_y (#14307)
# Objective

Fixes #14301 

## Solution

Swap them so that they are no longer swapped.
2024-08-02 19:15:56 +02:00
MiniaczQ
4bd56b6da1
Dirty fix for App hanging when windows are invisible on WindowsOS (#14155)
# Objective

- Fixes #14135 

## Solution

- If no windows are visible, app updates will run regardless of redraw
call result.

This a relatively dirty fix, a more robust solution is desired in the
long run:
https://github.com/bevyengine/bevy/issues/1343#issuecomment-770091684

https://discord.com/channels/691052431525675048/1253771396832821270/1258805997011730472
The solution would disconnect rendering from app updates.

## Testing

- `window_settings` now works

## Other platforms

Not a problem on Linux:
https://discord.com/channels/691052431525675048/692572690833473578/1259526650622640160
Not a problem on MacOS:
https://discord.com/channels/691052431525675048/692572690833473578/1259563986148659272
2024-08-02 19:12:05 +02:00
IQuick 143
0e1858bc4f
fix: Possible NaN due to denormalised quaternions in AABB implementations for round shapes. (#14240)
# Objective

With an unlucky denormalised quaternion (or just a regular very
denormalised quaternion), it's possible to obtain NaN values for AABB's
in shapes which rely on an AABB for a disk.

## Solution

Add an additional `.max(Vec3::ZERO)` clamp to get rid of negative values
arising due to numerical errors.
Fixup some unnecessary calculations and improve variable names in
relevant code, aiming for consistency.

## Discussion

These two (nontrivial) lines of code are repeated at least 5 times,
maybe they could be their own method.
2024-08-02 19:12:04 +02:00
Torstein Grindvik
1bc5ecda9b
bevy_input: allow use without bevy_reflect (#14167)
Allow use of `bevy_input` types without needing `bevy_reflect`.

Make `bevy_reflect` within `bevy_input` optional. It's compiled in by
default.
Turn on reflect in dependencies as well when this feature is on.

- Did you test these changes? If so, how?

I did a `cargo hack -p bevy_input --each-feature build`.

Signed-off-by: Torstein Grindvik <torstein.grindvik@muybridge.com>
Co-authored-by: Torstein Grindvik <torstein.grindvik@muybridge.com>
2024-08-02 19:11:13 +02:00
Litttle_fish
5d9e44b9dc
disable gpu preprocessing on android with Adreno 730 GPU and earilier (#14176)
# Objective

Fix #14146 

## Solution

Expansion of #13323 , excluded Adreno 730 and earlier.

## Testing

Tested on android device(Adreno 730) that used to crash
2024-08-02 18:58:31 +02:00
Jan Hohenheim
e941264b6f
Optimize unnecessary normalizations for Transform::local_{xyz} (#14171)
Note that `GlobalTransform` already does it like this for `right`,
`left`, etc. so I didn't have to touch that one
2024-08-02 18:58:31 +02:00
Mike
7ed1f6a9b6
use Display for entity id in log_components (#14164)
# Objective

- Cleanup a doubled `Entity` in log components

```
// Before
2024-07-05T19:54:09.082773Z  INFO bevy_ecs::system::commands: Entity Entity { index: 2, generation: 1 }: ["bevy_transform::components::transform::Transform"]

// After
2024-07-05T19:54:09.082773Z  INFO bevy_ecs::system::commands: Entity 2v1: ["bevy_transform::components::transform::Transform"]
```

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
2024-08-02 18:58:30 +02:00
Mike
c6b80c5664
add entity to error message (#14163)
# Objective

- There was a new warning added about having an unstyled child in the ui
hierarchy. Debugging the new error is pretty hard without any info about
which entity is.

## Solution

- Add the entity id to the warning.

```text
// Before
2024-07-05T19:40:59.904014Z  WARN bevy_ui::layout::ui_surface: Unstyled child in a UI entity hierarchy. You are using an entity without UI components as a child of an entity with UI components, results may be unexpected.

//After
2024-07-05T19:40:59.904014Z  WARN bevy_ui::layout::ui_surface: Unstyled child `3v1` in a UI entity hierarchy. You are using an entity without UI components as a child of an entity with UI components, results may be unexpected.
```

## Changelog

- add entity id to ui surface warning
2024-08-02 18:58:30 +02:00
Brandon Reinhart
4275669b07
impl Reflect + Clone for StateScoped (#14156)
# Objective

- Expand the flexibilty of StateScoped by adding Reflect and Clone
- This lets StateScoped be used in Clone Bundles, for example

```rust
#[derive(Component, Reflect, Clone)]
pub struct StateScoped<S: States>(pub S);
```

Notes:
- States are already Clone.
- Type registration is up to the user, but this is commonly the case
with reflected generic types.

## Testing

- Ran the examples.
2024-08-02 18:58:30 +02:00
François Mockers
a6fde1059c
EmptyPathStream is only used in android/wasm32 (#14200)
# Objective

- `EmptyPathStream` is only used in android and wasm32
- This now makes rust nightly warn

## Solution

- flag the struct to only be present when needed
- also change how `MorphTargetNames` is used because that makes rust
happier?
2024-08-02 18:58:30 +02:00
François Mockers
b231ebbc19
Release 0.14.0 version bump (#14126)
# Objective

- Bump the version before the release

- This should not be merged until ready for the release to have prettier
git history and tags
2024-07-03 18:22:10 -07:00
Rob Parrett
201dd62a74
Fix border color in ui_texture_slice and ui_texture_atlas_slice examples. (#14121)
# Objective

Fixes #14120

`ui_texture_slice` and `ui_texture_atlas_slice` were working as
intended, so undo the changes.

## Solution

Partially revert https://github.com/bevyengine/bevy/pull/14115 for
`ui_texture_slice` and `ui_texture_atlas_slice`.

## Testing

Ran those two examples, confirmed the border color is the thing that
changes when buttons are hovered.
2024-07-03 17:28:47 +02:00
François Mockers
ff070da7e2
fix remaining issues with background color in examples (#14115)
# Objective

- Fixes #14097

## Solution

- Switching the uses of `UiImage` in examples to `BackgroundColor` when
needed
2024-07-03 07:52:06 +02:00
Gagnus
1db0214f24
Added feature switch to default Standard Material's new anisotropy texture to off (#14048)
# Objective

- Standard Material is starting to run out of samplers (currently uses
13 with no additional features off, I think in 0.13 it was 12).
- This change adds a new feature switch, modelled on the other ones
which add features to Standard Material, to turn off the new anisotropy
feature by default.

## Solution

- feature + texture define

## Testing

- Anisotropy example still works fine
- Other samples work fine
- Standard Material now takes 12 samplers by default on my Mac instead
of 13

## Migration Guide

- Add feature pbr_anisotropy_texture if you are using that texture in
any standard materials.

---------

Co-authored-by: John Payne <20407779+johngpayne@users.noreply.github.com>
2024-07-02 22:10:00 +02:00
Mincong Lu
309c224ca8
Added get_main_animation for AnimationTransitions (#14104)
# Objective

Added a getter for the main animation of `AnimationTransitions`.

## Solution

Added `get_main_animation` for `AnimationTransitions`.
2024-07-02 22:10:00 +02:00
Vic
24fdad3a36
add missing mention of sort_unstable_by_key in QuerySortedIter docs (#14108)
# Objective

There is a missing mention of `sort_unstable_by_key` in the
`QuerySortedIter` docs.

## Solution

Add it.
2024-07-02 22:10:00 +02:00
Jan Hohenheim
e7333510c3
Fix push_children inserting a Children component even when no children are supplied (#14109)
# Objective

The Bevy API around manipulating hierarchies removes `Children` if the
operation results in an entity having no children. This means that
`Children` is guaranteed to hold actual children. However, the following
code unexpectedly inserts empty `Children`:

```rust
commands.entity(entity).with_children(|_| {});
```

This was discovered by @Jondolf:
https://discord.com/channels/691052431525675048/1124043933886976171/1257660865625325800

## Solution

- `with_children` is now a noop when no children were passed

## Testing

- Added a regression test
2024-07-02 22:10:00 +02:00
Alice Cecile
31b861401c
Backport #14083 (deregister events) to 0.14 branch (#14114)
Changes by @lee-orr. Fixes #14113.

Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
2024-07-02 16:01:57 -04:00
Joseph
99c465dcb5
Clarify the difference between default render layers and none render layers (#14075)
# Objective

It's not always obvious what the default value for `RenderLayers`
represents. It is documented, but since it's an implementation of a
trait method the documentation may or may not be shown depending on the
IDE.

## Solution

Add documentation to the `none` method that explicitly calls out the
difference.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-07-02 07:18:21 +02:00
Joseph
d7a0cc6bce
Support operations for render layers and fix equality comparisons (#13310)
# Objective

Allow combining render layers with a more-ergonomic syntax than
`RenderLayers::from_iter(a.iter().chain(b.iter()))`.

## Solution

Add the `or` operation (and corresponding `const` method) to allow
computing the union of a set of render layers. While we're here, also
added `and` and `xor` operations. Someone might find them useful

## Testing

Added a simple unit test.
2024-07-02 07:18:21 +02:00
Aevyrie
fda2e4b59c
Fix compile failure in WASM without wgpu backend (#14081)
# Objective

- When no wgpu backend is selected, there should be a clear explanation.
- Fix a regression in 0.14 when not using default features. I hit this
compile failure when trying to build bevy_framepace for 0.14.0-rc.4
```
error[E0432]: unresolved import `crate::core_3d::DEPTH_TEXTURE_SAMPLING_SUPPORTED`
  --> /Users/aevyrie/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_core_pipeline-0.14.0-rc.4/src/dof/mod.rs:59:19
   |
59 |         Camera3d, DEPTH_TEXTURE_SAMPLING_SUPPORTED,
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `DEPTH_TEXTURE_SAMPLING_SUPPORTED` in `core_3d`
   |
note: found an item that was configured out
  --> /Users/aevyrie/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_core_pipeline-0.14.0-rc.4/src/core_3d/mod.rs:53:11
   |
53 | pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false;
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: found an item that was configured out
  --> /Users/aevyrie/.cargo/registry/src/index.crates.io-6f17d22bba15001f/bevy_core_pipeline-0.14.0-rc.4/src/core_3d/mod.rs:63:11
   |
63 | pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true;
   |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

## Solution

- Ensure that `DEPTH_TEXTURE_SAMPLING_SUPPORTED` is either `true` or
`false`, it shouldn't be completely missing.

## Testing

- Building on WASM without default features, which now seemingly no
longer includes webgl, will panic on startup with a message saying that
no wgpu backend was selected. This is much more helpful than the compile
time failure:
```
No wgpu backend feature that is implemented for the target platform was enabled
```
- I can see an argument for making this a compile time failure, however
the current failure mode is very confusing for novice users, and
provides no clues for how to fix it. If we want this to fail at compile
time, we should do it in a way that fails with a helpful message,
similar to what this PR acheives.
2024-07-02 07:18:20 +02:00
François Mockers
dc56614b86
only run one update per frame drawn (#14023)
# Objective

- Fixes #13965 

## Solution

- Don't run multiple updates for a single frame
2024-06-30 10:01:10 +02:00
François Mockers
4a05c737a2
don't put previous skin/morph in the morphed_skinned_mesh_layout (#14065)
# Objective

- Fixes #14059
- `morphed_skinned_mesh_layout` is the same as
`morphed_skinned_motion_mesh_layout` but shouldn't have the skin / morph
from previous frame, as they're used for motion

## Solution

- Remove the extra entries

## Testing

- Run with the glTF file reproducing #14059, it works
2024-06-29 03:29:45 +02:00
François
036d0026be
Release Candidate 0.14.0-rc.4 2024-06-27 23:29:08 +02:00
Periwink
73b43aa6cf
Fix error in AnyOf (#14027)
# Objective

- Fixes a correctness error introduced in
https://github.com/bevyengine/bevy/pull/14013 ...

## Solution

I've been playing around a lot of with the access code and I realized
that I introduced a soundness error when trying to simplify the code.
When we have a `Or<(With<A>, With<B>)>` filter, we cannot call
```
  let mut intermediate = FilteredAccess::default();
  $name::update_component_access($name, &mut intermediate);
  _new_access.append_or(&intermediate);
```
because that's just equivalent to adding the new components as `Or`
clauses.
For example if the existing `filter_sets` was `vec![With<C>]`, we would
then get `vec![With<C>, With<A>, With<B>]` which translates to `A or B
or C`.
Instead what we want is `(A and B) or (A and C)`, so we need to have
each new OR clause compose with the existing access like so:
```
let mut intermediate = _access.clone();
// if we previously had a With<C> in the filter_set, this will become `With<C> AND With<A>`
$name::update_component_access($name, &mut intermediate);
_new_access.append_or(&intermediate);
```

## Testing

- Added a unit test that is broken in main, but passes in this PR
2024-06-27 22:47:36 +02:00
Tamás Kiss
20638f3a10
add PartialEq to Outline (#14055)
# Objective

`sickle_ui` needs `PartialEq` on components to turn them into animatable
style attributes.

## Solution

All properties of Outline is already `PartialEq`, add derive on
`Outline` as well.

## Testing

- used `sickle_ui` to test if it can be made animatable
2024-06-27 22:47:36 +02:00
Patrick Walton
26f7313212
Allow phase items not associated with meshes to be binned. (#14029)
As reported in #14004, many third-party plugins, such as Hanabi, enqueue
entities that don't have meshes into render phases. However, the
introduction of indirect mode added a dependency on mesh-specific data,
breaking this workflow. This is because GPU preprocessing requires that
the render phases manage indirect draw parameters, which don't apply to
objects that aren't meshes. The existing code skips over binned entities
that don't have indirect draw parameters, which causes the rendering to
be skipped for such objects.

To support this workflow, this commit adds a new field,
`non_mesh_items`, to `BinnedRenderPhase`. This field contains a simple
list of (bin key, entity) pairs. After drawing batchable and unbatchable
objects, the non-mesh items are drawn one after another. Bevy itself
doesn't enqueue any items into this list; it exists solely for the
application and/or plugins to use.

Additionally, this commit switches the asset ID in the standard bin keys
to be an untyped asset ID rather than that of a mesh. This allows more
flexibility, allowing bins to be keyed off any type of asset.

This patch adds a new example, `custom_phase_item`, which simultaneously
serves to demonstrate how to use this new feature and to act as a
regression test so this doesn't break again.

Fixes #14004.

## Changelog

### Added

* `BinnedRenderPhase` now contains a `non_mesh_items` field for plugins
to add custom items to.
2024-06-27 19:41:42 +02:00
Chris Russell
bea8823aa9
Add missing StaticSystemParam::queue implementation. (#14051)
# Objective

`StaticSystemParam` should delegate all `SystemParam` methods to the
inner param, but it looks like it was missed when the new `queue()`
method was added in #10839.

## Solution

Implement `StaticSystemParam::queue()` to delegate to the inner param.
2024-06-27 19:41:42 +02:00
Vic
7b98db6d7c
add missing sort_unstable_by_key to QueryIter (#14040)
# Objective

`QueryIter::sort_unstable_by_key` is missing.

## Solution

Add `QueryIter::sort_unstable_by_key`.

## Testing

Added the new method to existing test.

## Changelog

Added `QueryIter::sort_unstable_by_key`.
2024-06-27 19:41:42 +02:00
Arseny Kapoulkine
7c603874bf
Fix incorrect computation of mips for cluster occlusion lookup (#14042)
The comment was incorrect - we are already looking at the pyramid
texture so we do not need to transform the size in any way. Doing that
resulted in a mip that was too fine to be selected in certain cases,
which resulted in a 2x2 pixel footprint not actually fully covering the
cluster sphere - sometimes this could lead to a non-conservative depth
value being computed which resulted in the cluster being marked as
invisible incorrectly.
2024-06-27 19:41:42 +02:00
Arseny Kapoulkine
5d7da827b7
Improve MeshletMesh::from_mesh performance further (#14038)
This change updates meshopt-rs to 0.3 to take advantage of the newly
added sparse simplification mode: by default, simplifier assumes that
the entire mesh is simplified and runs a set of calculations that are
O(vertex count), but in our case we simplify many small mesh subsets
which is inefficient.

Sparse mode instead assumes that the simplified subset is only using a
portion of the vertex buffer, and optimizes accordingly. This changes
the meaning of the error (as it becomes relative to the subset, in our
case a meshlet group); to ensure consistent error selection, we also use
the ErrorAbsolute mode which allows us to operate in mesh coordinate
space.

Additionally, meshopt 0.3 runs optimizeMeshlet automatically as part of
`build_meshlets` so we no longer need to call it ourselves.

This reduces the time to build meshlet representation for Stanford Bunny
mesh from ~1.65s to ~0.45s (3.7x) in optimized builds.
2024-06-27 07:05:24 +02:00
Arseny Kapoulkine
4f0b0e0989
Make meshlet processing deterministic (#13913)
This is a followup to https://github.com/bevyengine/bevy/pull/13904
based on the discussion there, and switches two HashMaps that used
meshlet ids as keys to Vec.

In addition to a small further performance boost for `from_mesh` (1.66s
=> 1.60s), this makes processing deterministic modulo threading issues
wrt CRT rand described in the linked PR. This is valuable for debugging,
as you can visually or programmatically inspect the meshlet distribution
before/after making changes that should not change the output, whereas
previously every asset rebuild would change the meshlet structure.

Tested with https://github.com/bevyengine/bevy/pull/13431; after this
change, the visual output of meshlets is consistent between asset
rebuilds, and the MD5 of the output GLB file does not change either,
which was not the case before.
2024-06-27 07:05:24 +02:00
Arseny Kapoulkine
cc1764772e
Improve MeshletMesh::from_mesh performance (#13904)
This change reworks `find_connected_meshlets` to scale more linearly
with the mesh size, which significantly reduces the cost of building
meshlet representations. As a small extra complexity reduction, it moves
`simplify_scale` call out of the loop so that it's called once (it only
depends on the vertex data => is safe to cache).

The new implementation of connectivity analysis builds edge=>meshlet
list data structure, which allows us to only iterate through
`tuple_combinations` of a (usually) small list. There is still some
redundancy as if two meshlets share two edges, they will be represented
in the meshlet lists twice, but it's overall much faster.

Since the hash traversal is non-deterministic, to keep this part of the
algorithm deterministic for reproducible results we sort the output
adjacency lists.

Overall this reduces the time to process bunny mesh from ~4.2s to ~1.7s
when using release; in unoptimized builds the delta is even more
significant.

This was tested by using https://github.com/bevyengine/bevy/pull/13431
and:

a) comparing the result of `find_connected_meshlets` using old and new
code; they are equal in all steps of the clustering process
b) comparing the rendered result of the old code vs new code *after*
making the rest of the algorithm deterministic: right now the loop that
iterates through the result of `group_meshlets()` call executes in
different order between program runs. This is orthogonal to this change
and can be fixed separately.

Note: a future change can shrink the processing time further from ~1.7s
to ~0.4s with a small diff but that requires an update to meshopt crate
which is pending in https://github.com/gwihlidal/meshopt-rs/pull/42.
This change is independent.
2024-06-27 07:05:24 +02:00
Michael "Scott" McBee
99db59c176
Have WindowPosition::Centered take scale_factor_override into account (#13949)
# Objective

Fixes #8916 

My game has a low resolution pixel art style, and I use
`.with_scale_factor_override()` to make the window larger.
`WindowPosition::Centered` doesn't work for me.

## Solution

If `scale_factor_override` is set, use that over `monitor.scale_factor`

## Testing

Tested on Windows 11 with an Nvidia GPU:

### Main

![image](https://github.com/bevyengine/bevy/assets/3324533/5f9ae90e-b65a-48d9-b601-117df8f08a28)

### This PR

![image](https://github.com/bevyengine/bevy/assets/3324533/cd860611-7b6a-4ae5-b690-28d9ba8ea6ad)
2024-06-26 21:57:12 +02:00
Martin Svanberg
9a4de9c54d
Fix typo in CPU adapter warning (#14037)
An annoying typo slipped through in #13780
2024-06-26 21:56:23 +02:00
Joseph
a6feb5ba74
Emit a warning if the result of EntityCommand::with_entity is not used (#14028)
When using combinators such as `EntityCommand::with_entity` to build
commands, it can be easy to forget to apply that command, leading to
dead code. In many cases this doesn't even lead to an unused variable
warning, which can make these mistakes difficult to track down

Annotate the method with `#[must_use]`

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-06-26 21:56:23 +02:00
François Mockers
e65e4be98c
fix examples color_grading and mobile after BackgroundColor changes (#14033)
# Objective

- #14017 changed how `UiImage` and `BackgroundColor` work
- one change was missed in example `color_grading`, another in the
mobile example

## Solution

- Change it in the examples
2024-06-26 21:55:00 +02:00
François Mockers
27aa9d2b7e
example showcase: keep the order of the shaders imported (#14035)
# Objective

- After #13908, shaders imported are collected
- this is done with a hashset, so order is random between executions

## Solution

- Don't use a hashset, and keep the order they were found in the example
2024-06-26 21:55:00 +02:00
Martin Svanberg
f8e165be0a
Print warning when using llvmpipe (#13780)
# Objective

Numerous people have been confused that Bevy runs slowly, when the
reason is that the `llvmpipe` software rendered is being used.

## Solution

Printing a warning could reduce the confusion.
2024-06-26 21:55:00 +02:00
François Mockers
4736fe0dea
don't crash without features bevy_pbr, ktx2, zstd (#14020)
# Objective

- Fixes #13728 

## Solution

- add a new feature `smaa_luts`. if enables, it also enables `ktx2` and
`zstd`. if not, it doesn't load the files but use placeholders instead
- adds all the resources needed in the same places that system that uses
them are added.
2024-06-26 21:55:00 +02:00
Periwink
f89f7f306c
AnyOf soundness fix (#14013)
# Objective
Fixes https://github.com/bevyengine/bevy/issues/13993 
PR inspired by https://github.com/bevyengine/bevy/pull/14007 to
accomplish the same thing, but maybe in a clearer fashion.

@Gingeh feel free to take my changes and add them to your PR, I don't
want to steal any credit

---------

Co-authored-by: Gingeh <39150378+Gingeh@users.noreply.github.com>
Co-authored-by: Bob Gardner <rgardner@inworld.ai>
Co-authored-by: Martín Maita <47983254+mnmaita@users.noreply.github.com>
2024-06-26 02:31:20 +02:00
Alice Cecile
82f01569e8
Make default behavior for BackgroundColor and BorderColor more intuitive (#14017)
# Objective

In Bevy 0.13, `BackgroundColor` simply tinted the image of any
`UiImage`. This was confusing: in every other case (e.g. Text), this
added a solid square behind the element. #11165 changed this, but
removed `BackgroundColor` from `ImageBundle` to avoid confusion, since
the semantic meaning had changed.

However, this resulted in a serious UX downgrade / inconsistency, as
this behavior was no longer part of the bundle (unlike for `TextBundle`
or `NodeBundle`), leaving users with a relatively frustrating upgrade
path.

Additionally, adding both `BackgroundColor` and `UiImage` resulted in a
bizarre effect, where the background color was seemingly ignored as it
was covered by a solid white placeholder image.

Fixes #13969.

## Solution

Per @viridia's design:

> - if you don't specify a background color, it's transparent.
> - if you don't specify an image color, it's white (because it's a
multiplier).
> - if you don't specify an image, no image is drawn.
> - if you specify both a background color and an image color, they are
independent.
> - the background color is drawn behind the image (in whatever pixels
are transparent)

As laid out by @benfrankel, this involves:

1. Changing the default `UiImage` to use a transparent texture but a
pure white tint.
2. Adding `UiImage::solid_color` to quickly set placeholder images.
3. Changing the default `BorderColor` and `BackgroundColor` to
transparent.
4. Removing the default overrides for these values in the other assorted
UI bundles.
5. Adding `BackgroundColor` back to `ImageBundle` and `ButtonBundle`.
6. Adding a 1x1 `Image::transparent`, which can be accessed from
`Assets<Image>` via the `TRANSPARENT_IMAGE_HANDLE` constant.

Huge thanks to everyone who helped out with the design in the linked
issue and [the Discord
thread](https://discord.com/channels/691052431525675048/1255209923890118697/1255209999278280844):
this was very much a joint design.

@cart helped me figure out how to set the UiImage's default texture to a
transparent 1x1 image, which is a much nicer fix.

## Testing

I've checked the examples modified by this PR, and the `ui` example as
well just to be sure.

## Migration Guide

- `BackgroundColor` no longer tints the color of images in `ImageBundle`
or `ButtonBundle`. Set `UiImage::color` to tint images instead.
- The default texture for `UiImage` is now a transparent white square.
Use `UiImage::solid_color` to quickly draw debug images.
- The default value for `BackgroundColor` and `BorderColor` is now
transparent. Set the color to white manually to return to previous
behavior.
2024-06-26 00:16:30 +02:00
JMS55
65daab8517
Fix MeshletMesh material system ordering (#14016)
# Objective
- Fixes #13811 (probably, I lost my test code...)

## Solution
- Turns out that Queue and PrepareAssets are _not_ ordered. We should
probably either rethink our system sets (again), or improve the
documentation here. For reference, I've included the current ordering
below.
- The `prepare_meshlet_meshes_X` systems need to run after
`prepare_assets::<PreparedMaterial<M>>`, and have also been moved to
QueueMeshes.

```rust
schedule.configure_sets(
    (
        ExtractCommands,
        ManageViews,
        Queue,
        PhaseSort,
        Prepare,
        Render,
        Cleanup,
    )
        .chain(),
);

schedule.configure_sets((ExtractCommands, PrepareAssets, Prepare).chain());
schedule.configure_sets(QueueMeshes.in_set(Queue).after(prepare_assets::<GpuMesh>));
schedule.configure_sets(
    (PrepareResources, PrepareResourcesFlush, PrepareBindGroups)
        .chain()
        .in_set(Prepare),
);
```

## Testing
- Ambiguity checker to make sure I don't have ambiguous system ordering
2024-06-26 00:16:22 +02:00
Alice Cecile
9eb547ec1f
Don't show .to_bits in Display impl for Entity (#14011)
accurate for debugging. To ensure that its can still be readily logged
in error messages and inspectors, this PR added a more concise and
human-friendly `Display` impl.

However, users found this form too verbose: the `to_bits` information
was unhelpful and too long. Fixes #13980.

- Don't include `Entity::to_bits` in the `Display` implementation for
`Entity`. This information can readily be accessed and logged for users
who need it.
- Also clean up the implementation of `Display` for `DebugName`,
introduced in https://github.com/bevyengine/bevy/pull/13760, to simply
use the new `Display` impl (since this was the desired format there).

I've updated an existing test to verify the output of `Entity::display`.

---------

Co-authored-by: Kristoffer Søholm <k.soeholm@gmail.com>
2024-06-26 00:16:22 +02:00
Sou1gh0st
8c1b9a6c18
feat(bevy_app): expose an API to perform updates for a specific sub-app. (#14009)
# Objective

- Fixes https://github.com/bevyengine/bevy/issues/14003

## Solution

- Expose an API to perform updates for a specific sub-app, so we can
avoid mutable borrow the app twice.

## Testing

- I have tested the API by modifying the code in the `many_lights`
example with the following changes:
```rust
impl Plugin for LogVisibleLights {
    fn build(&self, app: &mut App) {
        let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
            return;
        };

        render_app.add_systems(Render, print_visible_light_count.in_set(RenderSet::Prepare));
    }

    fn finish(&self, app: &mut App) {
        app.update_sub_app_by_label(RenderApp);
    }
}
```

---

## Changelog
- add the `update_sub_app_by_label` API to `App` and `SubApps`.

---------

Co-authored-by: Jan Hohenheim <jan@hohenheim.ch>
2024-06-26 00:13:24 +02:00
Tamás Kiss
a0e7429363
fix panic: invalid SlotMap key used (#13990)
# Objective

Tight, in-frame generation, re-parenting, despawning, etc., UI
operations could sometime lead taffy to panic (invalid SlotMap key used)
when an entity with an invalid state later despawned.

Fixes #12403 

## Solution

Move the `remove_entities` call after children updates.

## Testing

`sickle_ui` had a case that always caused the panic. Tested before this
change, after this change, and before the change again to make sure the
error is there without the fix. The fix worked. Test steps and used
commit described in issue #12403.

I have also ran every bevy UI example, though none of them deal with
entity re-parenting or removal. No regression detected on them.

Tested on Windows only.
2024-06-26 00:13:24 +02:00
Bob Gardner
a41ed7822f
Fixing par_read targetting 0.14 branch (#14014)
Fix for par_read bugs retargetting 0.14.

Remakes https://github.com/bevyengine/bevy/pull/13836.
2024-06-26 00:09:00 +02:00
JMS55
b56a693c34
Fix meshlet interactions with regular shading passes (#13816)
* Fixes https://github.com/bevyengine/bevy/issues/13813
* Fixes https://github.com/bevyengine/bevy/issues/13810

Tested a combined scene with both regular meshes and meshlet meshes
with:
* Regular forward setup
* Forward + normal/motion vector prepasses
* Deferred (with depth prepass since that's required) 
* Deferred + depth/normal/motion vector prepasses

Still broken:
* Using meshlet meshes rendering in deferred and regular meshes
rendering in forward + depth/normal prepass. I don't know how to fix
this at the moment, so for now I've just add instructions to not mix
them.
2024-06-21 21:32:33 +02:00
Carter Anderson
073db8cf36
Make Observer::with_event (and other variants) unsafe (#13954)
# Objective

`with_event` will result in unsafe casting of event data of the given
type to the type expected by the Observer system. This is inherently
unsafe.

## Solution

Flag `Observer::with_event` and `ObserverDescriptor::with_events` as
unsafe. This will not affect normal workflows as `with_event` is
intended for very specific (largely internal) use cases.

This _should_ be backported to 0.14 before release.

---

## Changelog

- `Observer::with_event` is now unsafe.
- Rename `ObserverDescriptor::with_triggers` to
`ObserverDescriptor::with_events` and make it unsafe.
2024-06-21 20:53:13 +02:00
François Mockers
8af12c8775
apply window scale to window size when creating it (#13967)
# Objective

- Fixes #13702
- When creating a new window, its scale was changed to match the one
returned by winit, but its size was not which resulted in an incorrect
size until the event with the correct size was received, at least 1
frame later

## Solution

- Apply the window scale to its size when creating it
2024-06-21 20:27:55 +02:00
charlotte
e73063e1d5
Correctly check physical size when updating winit (#13942)
Fixes #13701

After `winit` upgrade to `0.31`, windows were no longer correctly
resizing. This appears to just have been a simple mistake, where the new
physical size was being sourced from the `winit` window rather than on
the incoming `Window` component.

## Testing

Tested on macOS, but I'm curious whether this was also broken on other
platforms.
2024-06-21 19:36:48 +02:00
Chris Biscardi
cf66df1d0d
Use a ship in Transform::align example (#13935)
# Objective

The documentation for
[`Transform::align`](https://docs.rs/bevy/0.14.0-rc.3/bevy/transform/components/struct.Transform.html#method.align)
mentions a hypothetical ship model. Showing this concretely would be a
nice improvement over using a cube.

> For example, if a spaceship model has its nose pointing in the
X-direction in its own local coordinates and its dorsal fin pointing in
the Y-direction, then align(Dir3::X, v, Dir3::Y, w) will make the
spaceship’s nose point in the direction of v, while the dorsal fin does
its best to point in the direction w.


## Solution

This commit makes the ship less hypothetical by using a kenney ship
model in the example.

The local axes for the ship needed to change to accommodate the gltf, so
the hypothetical in the documentation and this example's local axes
don't necessarily match. Docs use `align(Dir3::X, v, Dir3::Y, w)` and
this example now uses `(Vec3::NEG_Z, *first, Vec3::X, *second)`.

I manually modified the `craft_speederD` Node's `translation` to be
0,0,0 in the gltf file, which means it now differs from kenney's
original model.

Original ship from: https://kenney.nl/assets/space-kit

## Testing

```
cargo run --example align
```

![screenshot-2024-06-19-at-14 27
05@2x](https://github.com/bevyengine/bevy/assets/551247/ab1afc8f-76b2-42b6-b455-f0d1c77cfed7)
![screenshot-2024-06-19-at-14 27
12@2x](https://github.com/bevyengine/bevy/assets/551247/4a01031c-4ea1-43ab-8078-3656db67efe0)
![screenshot-2024-06-19-at-14 27
20@2x](https://github.com/bevyengine/bevy/assets/551247/06830f38-ba2b-4e3a-a265-2d10f9ea9de9)
2024-06-21 19:36:48 +02:00
MiniaczQ
783fc29cd3
Move StateTransitionSteps registration to states plugin (#13939)
# Objective

Fixes #13920

## Solution

As described in the issue.

## Testing

Moved a custom transition plugin in example before any of the app-state
methods.
2024-06-21 19:36:48 +02:00
Shane Celis
47ad37ec84
bug: Fix 9-slice textures with asymmetric borders. (#13921)
# Objective

Fix a 9-slice asymmetric border issue that
[QueenOfSquiggles](https://blobfox.coffee/@queenofsquiggles/112639035165575222)
found. Here's the behavior before:

<img width="340" alt="the-bug"
src="https://github.com/bevyengine/bevy/assets/54390/81ff1847-b2ea-4578-9fd0-af6ee96c5438">

## Solution

Here's the behavior with the fix.

<img width="327" alt="the-fix"
src="https://github.com/bevyengine/bevy/assets/54390/33a4e3f0-b6a8-448e-9654-1197218ea11d">


## Testing

I used QueenOfSquiggles
[repo](https://github.com/QueenOfSquiggles/my-bevy-learning-project) to
exercise the code. I manually went through a number of variations of the
border and caught a few other issues after the first pass. I added some
code to create random borders and though they often looked funny there
weren't any gaps like before.

### Unit Tests

I did add some tests to `slicer.rs` mostly as an exploratory programming
exercise. So they currently act as a limited, incomplete,
"golden-file"-ish approach. Perhaps they're not worth keeping.

In order to write the tests, I did add a `PartialEq` derive for
`TextureSlice`.

I only tested these changes on macOS.

---

## Changelog

Make 9-slice textures work with asymmetric borders.
2024-06-21 19:36:48 +02:00
AndrewDanial
472a6e8283
Shader code paths (#13908)
# Objective

Add extra metadata for the shader examples that contains the location of
their associated shader file(s). This is to be used for the bevy website
shader examples so that the shader code is underneath the rust code.

## Solution

Parse the example rust files for mentions of `.wgsl`, `.frag`, and
`.vert`, then append the found paths to a field called
`shader_code_paths` in the generated `index.md`s for each shader
example.
2024-06-19 18:54:28 +02:00
James O'Brien
5d5e67fa35
Update observer archetype flags for sparse components (#13886)
# Objective

- Fixes #13885 

## Solution

- Update the flags correctly on archetype creation

## Testing

- Added `observer_order_insert_remove_sparse` to catch regressions.
2024-06-19 04:14:58 +02:00
Jan Hohenheim
b74b3b2123
Add first person view model example (#13828)
# Objective

A very common way to organize a first-person view is to split it into
two kinds of models:

 - The *view model* is the model that represents the player's body.
 - The *world model* is everything else.

The reason for this distinction is that these two models should be
rendered with different FOVs.
The view model is typically designed and animated with a very specific
FOV in mind, so it is
generally *fixed* and cannot be changed by a player. The world model, on
the other hand, should
be able to change its FOV to accommodate the player's preferences for
the following reasons:
- *Accessibility*: How prone is the player to motion sickness? A wider
FOV can help.
- *Tactical preference*: Does the player want to see more of the
battlefield?
 Or have a more zoomed-in view for precision aiming?
- *Physical considerations*: How well does the in-game FOV match the
player's real-world FOV?
Are they sitting in front of a monitor or playing on a TV in the living
room? How big is the screen?

## Solution

I've added an example implementing the described setup as follows.

The `Player` is an entity holding two cameras, one for each model. The
view model camera has a fixed
FOV of 70 degrees, while the world model camera has a variable FOV that
can be changed by the player.

 I use different `RenderLayers` to select what to render.

- The world model camera has no explicit `RenderLayers` component, so it
uses the layer 0.
All static objects in the scene are also on layer 0 for the same reason.
- The view model camera has a `RenderLayers` component with layer 1, so
it only renders objects
explicitly assigned to layer 1. The arm of the player is one such
object.
The order of the view model camera is additionally bumped to 1 to ensure
it renders on top of the world model.
- The light source in the scene must illuminate both the view model and
the world model, so it is
 assigned to both layers 0 and 1.

To better see the effect, the player can move the camera by dragging
their mouse and change the world model's FOV with the arrow keys. The
arrow up key maps to "decrease FOV" and the arrow down key maps to
"increase FOV". This sounds backwards on paper, but is more intuitive
when actually changing the FOV in-game since a decrease in FOV looks
like a zoom-in.
I intentionally do not allow changing the view model's FOV even though
it would be illustrative because that would be an anti-pattern and bloat
the code a bit.

The example is called `first_person_view_model` and not just
`first_person` because I want to highlight that this is not a simple
flycam, but actually renders the player.

## Testing

Default FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/8c2e804f-fac2-48c7-8a22-d85af999dfb2">

Decreased FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/1733b3e5-f583-4214-a454-3554e3cbd066">

Increased FOV:
<img width="1392" alt="image"
src="https://github.com/bevyengine/bevy/assets/9047632/0b0640e6-5743-46f6-a79a-7181ba9678e8">

Note that the white bar on the right represents the player's arm, which
is more obvious in-game because you can move the camera around.
The box on top is there to make sure that the view model is receiving
shadows.

I tested only on macOS.

---

## Changelog

I don't think new examples go in here, do they?

## Caveat

The solution used here was implemented with help by @robtfm on
[Discord](https://discord.com/channels/691052431525675048/866787577687310356/1241019224491561000):
> shadow maps are specific to lights, not to layers
> if you want shadows from some meshes that are not visible, you could
have light on layer 1+2, meshes on layer 2, camera on layer 1 (for
example)
> but this might change in future, it's not exactly an intended feature

In other words, the example code as-is is not guaranteed to work in the
future. I want to bring this up because the use-case presented here is
extremely common in first-person games and important for accessibility.
It would be good to have a blessed and easy way of how to achieve it.

I'm also not happy about how I get the `perspective` variable in
`change_fov`. Very open to suggestions :)

## Related issues

- Addresses parts of #12658
- Addresses parts of #12588

---------

Co-authored-by: Pascal Hertleif <killercup@gmail.com>
2024-06-19 04:14:58 +02:00
hut
4608708d8d
Fix phantom key presses in winit on focus change (#13299) (#13696)
# Objective

Fixes #13299

On Linux/X11, changing focus into a winit window will produce winit
KeyboardInput events with a "is_synthetic=true" flag that are not
intended to be used. Bevy erroneously passes them on to the user,
resulting in phantom key presses.

## Solution

This patch properly filters out winit KeyboardInput events with
"is_synthetic=true".

For example, pressing Alt+Tab to focus a bevy winit window results in a
permanently stuck Tab key until the user presses Tab once again to
produce a winit KeyboardInput release event. The Tab key press event
that causes this problem is "synthetic", should not be used according to
the winit devs, and simply ignoring it fixes this problem.

Synthetic key **releases** are still evaluated though, as they are
essential for correct release key handling. For example, if the user
binds the key combination Alt+1 to the action "move the window to
workspace 1", places the bevy game in workspace 2, focuses the game and
presses Alt+1, then the key release event for the "1" key will be
synthetic. If we would filter out all synthetic keys, the bevy game
would think that the 1 key remains pressed forever, until the user
manually presses+releases the key again inside bevy.

Reference:
https://docs.rs/winit/0.30.0/winit/event/enum.WindowEvent.html#variant.KeyboardInput.field.is_synthetic
Relevant discussion: https://github.com/rust-windowing/winit/issues/3543

## Testing

Tested with the "keyboard_input_events" example. Entering/exiting the
window with various keys, as well as changing its workspace, produces
the correct press/release events.
2024-06-19 04:14:58 +02:00
François Mockers
f2e66726b6
observers example doesn't follow standards (#13884)
# Objective

- Observers example is using an unseeded random, prints text to console
and doesn't display text as other examples

## Solution

- use seeded random
- log instead of printing
- use common settings for UI text
2024-06-19 04:14:58 +02:00
Kristoffer Søholm
165f399489
Make time_system public (#13879)
# Objective

If `time_system` isn't public you cannot order systems relative to it in
the `TimeSystem` set.

## Solution

Make it public
2024-06-19 04:14:58 +02:00
MiniaczQ
6cf04c213b
Warn about missing StatesPlugin when installing states (#13877)
# Objective

- Fixes #13874

## Solution

- Confirm that the `StatesPlugin` is installed when trying to add
states.
- Skipped for state scoped entities, since those will warn about missing
states.
2024-06-19 04:14:58 +02:00
Lee-Orr
1c838ff6b3
remove inaccurate warning from in_state (#13862)
# Objective
Fixes #13854

## Solution
Removed the inaccurate warning. This was done for a few reasons:

- States not existing is now a valid "state" (for lack of a better term)
- Other run conditions don't provide an equivalent warning
2024-06-19 04:14:58 +02:00
François
03f63e72cf
Release Candidate 0.14.0-rc.3 2024-06-16 17:50:19 +02:00
François Mockers
55ee49b7ad
text position: use size instead of bounds (#13858)
# Objective

- #13846 introduced a bug where text not bound was not displayed

## Solution

- bounds are infinite
- use computed size instead, that already should be using the available
bounds
2024-06-16 17:45:07 +02:00
Martín Maita
eca8220761
Generalised ECS reactivity with Observers (#10839) (#13873)
# Objective

- Fixes #13825 

## Solution

- Cherry picked and fixed non-trivial conflicts to be able to merge
#10839 into the 0.14 release branch.

Link to PR: https://github.com/bevyengine/bevy/pull/10839

Co-authored-by: James O'Brien <james.obrien@drafly.net>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: MiniaczQ <xnetroidpl@gmail.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
2024-06-16 11:44:08 -04:00
MiniaczQ
5ed296ff03
Restore overwrite capabilities of insert_state (#13848)
# Objective

- Fixes #13844
- Warn user when initializing state multiple times

## Solution

- `insert_state` will overwrite previously initialized state value,
reset transition events and re-insert it's own transition event.
- `init_state`, `add_sub_state`, `add_computed_state` are idempotent, so
calling them multiple times will emit a warning.

## Testing

- 2 tests confirming overwrite works.
- Given the example from #13844
```rs
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_state(AppState::A)
        .insert_state(AppState::B)
        .add_systems(OnEnter(AppState::A), setup_a)
        .add_systems(OnEnter(AppState::B), setup_b)
        .add_systems(OnExit(AppState::A), cleanup_a)
        .add_systems(OnExit(AppState::B), cleanup_b)
        .run();
}

#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
enum AppState {
    A,
    B,
}

fn setup_a() {
    info!("setting up A");
}

fn setup_b() {
    info!("setting up B");
}

fn cleanup_a() {
    info!("cleaning up A");
}

fn cleanup_b() {
    info!("cleaning up B");
}
```

We get the following result:
```
INFO states: setting up B
```
which matches our expectations.
2024-06-15 11:02:44 +02:00
Kristoffer Søholm
cc4681fc44
Fix is_plugin_added::<Self>() being true during build (#13817)
# Objective

Fixes #13815 

## Solution

Move insertion of the plugin name to after build is called.

## Testing

I added a regression test

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <mockersf@gmail.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
2024-06-14 21:52:29 +02:00
robtfm
ccfae7ebe7
fix non-exact text h-alignment (#13846)
# Objective

when a parent container is auto-sized, text alignments `Center` and
`Right` don't align to the center and right properly. fix it

## Solution

ab_glyph positions return +/- values from an anchor point. we currently
transform them to positive values from the min-x of the glyphs, and then
offset from the left of the bounds. instead, we can keep the negative
values as ab_glyph intended and offset from the left/middle/right of the
bounds as appropriate.

## Testing

texts with align left, center, right, all contained in the purple boxes:
before (0.14.0-rc.2):
![Screenshot 2024-06-14
165456](https://github.com/bevyengine/bevy/assets/50659922/90fb73b0-d8bd-4ae8-abf3-7106eafc93ba)

after:

![Screenshot 2024-06-14
164449](https://github.com/bevyengine/bevy/assets/50659922/0a75ff09-b51d-4fbe-a491-b655a145c08b)

code:
```rs
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    for (left, justify) in [
        (100.0, JustifyText::Left),
        (500.0, JustifyText::Center),
        (900.0, JustifyText::Right),
    ] {
        commands
        // container
        .spawn(NodeBundle {
            style: Style {
                flex_direction: FlexDirection::Column,
                position_type: PositionType::Absolute,
                left: Val::Px(left),
                top: Val::Px(100.0),
                width: Val::Px(300.0),
                ..Default::default()
            },
            ..Default::default()
        })
        .with_children(|commands| {
            commands.spawn(NodeBundle{
                style: Style {
                    flex_direction: FlexDirection::Row,
                    height: Val::Px(75.0),
                    ..Default::default()
                },
                background_color: Color::srgb(1.0, 0.0, 1.0).into(),
                ..Default::default()
            }).with_children(|commands| {
                // a div that reduces the available size
                commands.spawn(NodeBundle {
                    style: Style {
                        width: Val::Px(75.0),
                        ..Default::default()
                    },
                    background_color: Color::srgb(0.0, 1.0, 0.0).into(),
                    ..Default::default()
                });

                // text with width=auto, but actual size will not be what it expcets due to the sibling div above
                commands.spawn(TextBundle {
                    text: Text::from_section("Some text that wraps onto a second line", Default::default()).with_justify(justify),
                    style: Style {
                        align_self: AlignSelf::Center,
                        ..Default::default()
                    },
                    ..Default::default()
                });
            });
        });
    }
}
```
2024-06-14 21:47:43 +02:00
Elabajaba
1ea1b76c75
Wgpu 0.20 (#13186)
Currently blocked on https://github.com/gfx-rs/wgpu/issues/5774

# Objective

Update to wgpu 0.20

## Solution

Update to wgpu 0.20 and naga_oil 0.14.

## Testing

Tested a few different examples on linux (vulkan, webgl2, webgpu) and
windows (dx12 + vulkan) and they worked.

---

## Changelog

- Updated to wgpu 0.20. Note that we don't currently support wgpu's new
pipeline overridable constants, as they don't work on web currently and
need some more changes to naga_oil (and are somewhat redundant with
naga_oil's shader defs). See wgpu's changelog for more
https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md#v0200-2024-04-28

## Migration Guide

TODO

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <mockersf@gmail.com>
2024-06-14 20:55:42 +02:00
Mike
ac50539aab
reduce the antialias strength (#13814)
# Objective

- Fixes #13807

## Solution

- Before this pr we antialiased between 0.5 and -0.5. This pr changes
things to antialias between 0.25 and -0.25. I tried slightly larger
ranges, but the edge between the boxes still showed. I'm not 100% sure
this is the correct solution, but from what I could find the range you
use is more art than science.

## Testing

- Ran rounded_borders example, the code in the linked issue, and the
testing example from #12702.

---

## Changelog

- reduce antialiasing in ui shader.
2024-06-14 20:55:41 +02:00
James Gayfer
727b0f6e27
Use dynamic uniform buffer in post processing example (#13540)
# Objective

While learning about shaders and pipelines, I found this example to be
misleading; it wasn't clear to me how the node knew what the correct
"instance" of `PostProcessSettings` we should send to the shader (as the
combination of `ExtractComponentPlugin` and `UniformComponentPlugin`
extracts + sends _all_ of our `PostProcessSetting` components to the
GPU).

The goal of this PR is to clarify how to target the view specific
`PostProcessSettings` in the shader when there are multiple cameras.

## Solution

To accomplish this, we can use a dynamic uniform buffer for
`PostProcessSettings`, querying for the relevant `DynamicUniformIndex`
in the `PostProcessNode` to get the relevant index to use with the bind
group.

While the example in its current state is _correct_, I believe that fact
that it's intended to showcase a per camera post processing effect
warrants a dynamic uniform buffer (even though in the context of this
example we have only one camera, and therefore no adverse behaviour).

## Testing

- Run the `post_processing` example before and after this change,
verifying they behave the same.

## Reviewer notes

This is my first PR to Bevy, and I'm by no means an expert in the world
of rendering (though I'm trying to learn all I can). If there's a better
way to do this / a reason not to take this route, I'd love to hear it!

Thanks in advance.
2024-06-14 20:55:41 +02:00
Nionidh
7c0b1b9029
Add missing plugins to doc of DefaultPlugins (#13833)
StatesPlugin and GizmoPlugin were missing from the doc comment of
DefaultPlugins. I am not sure whether this was for a reason, but i just
stumbled over it and it seemed off...

## Testing

I'm not sure how to test these changes?
2024-06-14 20:55:41 +02:00
Martín Maita
84be2b3f1e
Ensure that events are updated even when using a bare-bones Bevy App (#13808) (#13842)
# Objective

- Related to https://github.com/bevyengine/bevy/issues/13825

## Solution

- Cherry picked the merged PR and performed the necessary changes to
adapt it to the 0.14 release branch.

---------

As discovered in
https://github.com/Leafwing-Studios/leafwing-input-manager/issues/538,
there appears to be some real weirdness going on in how event updates
are processed between Bevy 0.13 and Bevy 0.14.

To identify the cause and prevent regression, I've added tests to
validate the intended behavior.
My initial suspicion was that this would be fixed by
https://github.com/bevyengine/bevy/pull/13762, but that doesn't seem to
be the case.

Instead, events appear to never be updated at all when using `bevy_app`
by itself. This is part of the problem resolved by
https://github.com/bevyengine/bevy/pull/11528, and introduced by
https://github.com/bevyengine/bevy/pull/10077.

After some investigation, it appears that `signal_event_update_system`
is never added using a bare-bones `App`, and so event updates are always
skipped.

This can be worked around by adding your own copy to a
later-in-the-frame schedule, but that's not a very good fix.

Ensure that if we're not using a `FixedUpdate` schedule, events are
always updated every frame.

To do this, I've modified the logic of `event_update_condition` and
`event_update_system` to clearly and correctly differentiate between the
two cases: where we're waiting for a "you should update now" signal and
where we simply don't care.

To encode this, I've added the `ShouldUpdateEvents` enum, replacing a
simple `bool` in `EventRegistry`'s `needs_update` field.

Now, both tests pass as expected, without having to manually add a
system!

I've written two parallel unit tests to cover the intended behavior:

1. Test that `iter_current_update_events` works as expected in
`bevy_ecs`.
2. Test that `iter_current_update_events` works as expected in
`bevy_app`

I've also added a test to verify that event updating works correctly in
the presence of a fixed main schedule, and a second test to verify that
fixed updating works at all to help future authors narrow down failures.

- [x] figure out why the `bevy_app` version of this test fails but the
`bevy_ecs` version does not
- [x] figure out why `EventRegistry::run_updates` isn't working properly
- [x] figure out why `EventRegistry::run_updates` is never getting
called
- [x] figure out why `event_update_condition` is always returning false
- [x] figure out why `EventRegistry::needs_update` is always false
- [x] verify that the problem is a missing `signal_events_update_system`

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Mike <mike.hsu@gmail.com>
2024-06-14 20:53:37 +02:00
CatThingy
eca7e87d47
Fix minor typos in query join docs (#13812)
# Objective

- Correct typos in docs for `Query::join`'s docs

## Solution

- Fix them

Co-authored-by: François Mockers <mockersf@gmail.com>
2024-06-12 01:47:01 +02:00
Mike
cdd1f71596
Revert "constrain WorldQuery::init_state argument to ComponentInitial… (#13804)
…izer (#13442)"

This reverts commit 5cfb063d4a.

- This PR broke bevy-trait-query, which needs to be able to write a
resource in init_state. See #13798 for more details.
- Note this doesn't fix everything as transmutes for bevy-trait-query
will still be broken,. But the current usage in that crate is UB, so we
need to find another solution.
2024-06-12 01:20:47 +02:00
Alice Cecile
92176ce576
Revert "Make FOG_ENABLED a shader_def instead of material flag (#13783)" (#13803)
This reverts commit 3ced49f672.

Relevant to https://github.com/bevyengine/bevy/issues/13802. This wasn't
done quite right and partially broke fog.

Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
2024-06-11 02:01:14 +02:00
Brezak
e14f3ba1aa
Add from_color to StandardMaterial and ColorMaterial (#13791)
# Objective

Closes #13738

## Solution

Added `from_color` to materials that would support it. Didn't add
`from_color` to `WireframeMaterial` as it doesn't seem we expect users
to be constructing them themselves.

## Testing

None

---

## Changelog

### Added

- `from_color` to `StandardMaterial` so you can construct this material
from any color type.
- `from_color` to `ColorMaterial` so you can construct this material
from any color type.
2024-06-11 02:01:14 +02:00
Lynn
9a82ecfcba
Custom primitives example (#13795)
# Objective

- Add a new example showcasing how to add custom primitives and what you
can do with them.

## Solution

- Added a new example `custom_primitives` with a 2D heart shape
primitive highlighting
  - `Bounded2d` by implementing and visualising bounding shapes,
  - `Measured2d` by implementing it,
  - `Meshable` to show the shape on the screen
- The example also includes an `Extrusion<Heart>` implementing
  - `Measured3d`,
  - `Bounded3d` using the `BoundedExtrusion` trait and
  - meshing using the `Extrudable` trait.

## Additional information

Here are two images of the heart and its extrusion:

![image_2024-06-10_194631194](https://github.com/bevyengine/bevy/assets/62256001/53f1836c-df74-4ba6-85e9-fabdafa94c66)
![Screenshot 2024-06-10
194609](https://github.com/bevyengine/bevy/assets/62256001/b1630e71-6e94-4293-b7b5-da8d9cc98faf)

---------

Co-authored-by: Jakub Marcowski <37378746+Chubercik@users.noreply.github.com>
2024-06-11 02:01:14 +02:00
JMS55
93f48edbc3
Fix meshlet vertex attribute interpolation (#13775)
# Objective

- Mikktspace requires that we normalize world normals/tangents _before_
interpolation across vertices, and then do _not_ normalize after. I had
it backwards.
- We do not (am not supposed to?) need a second set of barycentrics for
motion vectors. If you think about the typical raster pipeline, in the
vertex shader we calculate previous_world_position, and then it gets
interpolated using the current triangle's barycentrics.

## Solution

- Fix normal/tangent processing 
- Reuse barycentrics for motion vector calculations
- Not implementing this for 0.14, but long term I aim to remove explicit
vertex tangents and calculate them in the shader on the fly.

## Testing

- I tested out some of the normal maps we have in repo. Didn't seem to
make a difference, but mikktspace is all about correctness across
various baking tools. I probably just didn't have any of the ones that
would cause it to break.
- Didn't test motion vectors as there's a known bug with the depth
buffer and meshlets that I'm waiting on the render graph rewrite to fix.
2024-06-10 22:49:04 +02:00
Brezak
7cd90990f9
Poll system information in separate tasks (#13693)
Reading system information severely slows down the update loop.
Fixes #12848.

Read system info in a separate thread.

- Open the scene 3d example
- Add `FrameTimeDiagnosticsPlugin`, `SystemInformationDiagnosticsPlugin`
and `LogDiagnosticsPlugin` to the app.
- Add this system to the update schedule to disable Vsync on the main
window
```rust
fn change_window_mode(mut windows: Query<&mut Window, Added<Window>>) {
    for mut window in &mut windows {
        window.present_mode = PresentMode::AutoNoVsync;
    }
}
```
- Read the fps values in the console before and after this PR.

On my PC I went from around 50 fps to around 1150 fps.

---

- The `SystemInformationDiagnosticsPlugin` now reads system data
separate of the update cycle.

- The `EXPECTED_SYSTEM_INFORMATION_INTERVAL` constant which defines how
often we read system diagnostic data.

---------

Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
2024-06-10 22:30:56 +02:00
Chris Juchem
65dbfe249b
Remove extra call to clear_trackers (#13762)
Fixes #13758.

# Objective

Calling `update` on the main app already calls `clear_trackers`. Calling
it again in `SubApps::update` caused RemovedCompenet Events to be
cleared earlier than they should be.

## Solution

- Don't call clear_trackers an extra time.

## Testing

I manually tested the fix with this unit test: 
```
#[cfg(test)]
mod test {
    use crate::core::{FrameCount, FrameCountPlugin};
    use crate::prelude::*;

    #[test]
    fn test_next_frame_removal() {
        #[derive(Component)]
        struct Foo;

        #[derive(Resource)]
        struct RemovedCount(usize);

        let mut app = App::new();
        app.add_plugins(FrameCountPlugin);
        app.add_systems(Startup, |mut commands: Commands| {
            for _ in 0..100 {
                commands.spawn(Foo);
            }
            commands.insert_resource(RemovedCount(0));
        });

        app.add_systems(First, |counter: Res<FrameCount>| {
            println!("Frame {}:", counter.0)
        });

        fn detector_system(
            mut removals: RemovedComponents<Foo>,
            foos: Query<Entity, With<Foo>>,
            mut removed_c: ResMut<RemovedCount>,
        ) {
            for e in removals.read() {
                println!("  Detected removed Foo component for {e:?}");
                removed_c.0 += 1;
            }
            let c = foos.iter().count();
            println!("  Total Foos: {}", c);
            assert_eq!(c + removed_c.0, 100);
        }
        fn deleter_system(foos: Query<Entity, With<Foo>>, mut commands: Commands) {
            foos.iter().next().map(|e| {
                commands.entity(e).remove::<Foo>();
            });
        }
        app.add_systems(Update, (detector_system, deleter_system).chain());

        app.update();
        app.update();
        app.update();
        app.update();
    }
}
```
2024-06-10 20:23:06 +02:00
Periwink
effbcdfc92
Update serialize flag for bevy_ecs (#13740)
# Objective

There were some issues with the `serialize` feature:
- `bevy_app` had a `serialize` feature and a dependency on `serde` even
there is no usage of serde at all inside `bevy_app`
- the `bevy_app/serialize` feature enabled `bevy_ecs/serde`, which is
strange
- `bevy_internal/serialize` did not enable `bevy_app/serialize` so there
was no way of serializing an Entity in bevy 0.14

## Solution

- Remove `serde` and `bevy_app/serialize` 
- Add a `serialize` flag on `bevy_ecs` that enables `serde`
- ` bevy_internal/serialize` now enables `bevy_ecs/serialize`
2024-06-10 19:31:41 +02:00
IceSentry
3ced49f672
Make FOG_ENABLED a shader_def instead of material flag (#13783)
# Objective

- If the fog is disabled it still generates a useless branch which can
hurt performance

## Solution

- Make the flag a shader_def instead

## Testing

- I tested enabling/disabling fog works as expected per-material in the
fog example
- I also tested that scenes that don't add the FogSettings resource
still work correctly

## Review notes

I'm not sure how to handle the removed material flag. Right now I just
commented it out and added a not to reuse it instead of creating a new
one.
2024-06-10 19:31:41 +02:00
Lee-Orr
a3916b4af4
fix docs around StateTransition and remove references to `apply_sta… (#13772)
The documentation for the `State` resource still referenced the use of
`apply_state_transition` to manually force a state transition to occur,
and the question around how to force transitions had come up a few times
on discord.

This is a docs-only change, that does the following:
- Properly references `StateTransition` in the `MainSchedule` docs
- replace the explanations for applying `NextState` with ones that
explain the `StateTransition` schedule, and mentions the possibility of
calling it manually
- Add an example of calling `StateTransition` manually in the docs for
the state transition schedule itself.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-06-10 19:31:41 +02:00
Isaïe
c060e3e1fe
Clarify error message due to missing shader file (#13766)
# Objective

The error printed-out due to a missing shader file was confusing; This
PR changes the error message.

Fixes #13644 

## Solution

I replaced the confusing wording (`... shader is not loaded yet`) with a
clear explanation (`... shader could not be loaded`)

## Testing

> Did you test these changes? If so, how?

removing `assets/shaders/game_of_life.wgsl` & running its associated
example now produces the following error:

```
thread '<unnamed>' panicked at examples/shader/compute_shader_game_of_life.rs:233:25:
Initializing assets/shaders/game_of_life.wgsl:
Pipeline could not be compiled because the following shader could not be loaded: AssetId<bevy_render::render_resource::shader::Shader>{ index: 0, generation: 0}
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Encountered a panic in system `bevy_render::renderer::render_system`!
```

I don't think there are any tests expecting the previous error message,
so this change should not break anything.

> Are there any parts that need more testing?

If there was an intent behind the original message, this might need more
attention.

> How can other people (reviewers) test your changes? Is there anything
specific they need to know?

One should be able to preview the changes by running any example after
deleting/renaming their associated shader(s).

> If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?

N/A
2024-06-10 19:31:41 +02:00
MiniaczQ
854983dc7e
Add more granular system sets for state transition schedule ordering (#13763)
# Objective

Fixes #13711 

## Solution

Introduce smaller, generic system sets for each schedule variant, which
are ordered against other generic variants:
- `ExitSchedules<S>` - For `OnExit` schedules, runs from leaf states to
root states.
- `TransitionSchedules<S>` - For `OnTransition` schedules, runs in
arbitrary order.
- `EnterSchedules<S>` - For `OnEnter` schedules, runs from root states
to leaf states.

Also unified `ApplyStateTransition<S>` schedule which works in basically
the same way, just for internals.

## Testing

- One test that tests schedule execution order

---------

Co-authored-by: Lee-Orr <lee-orr@users.noreply.github.com>
2024-06-10 19:31:41 +02:00
JMS55
a944598812
Meshlet misc (#13761)
- Copy module docs so that they show up in the re-export
- Change meshlet_id to cluster_id in the debug visualization
- Small doc tweaks
2024-06-10 19:31:41 +02:00
Gagnus
cbebcb0d3f
Adds back in way to convert color to u8 array, implemented for the two RGB color types, also renames Color::linear to Color::to_linear. (#13759)
# Objective

One thing missing from the new Color implementation in 0.14 is the
ability to easily convert to a u8 representation of the rgb color.

(note this is a redo of PR https://github.com/bevyengine/bevy/pull/13739
as I needed to move the source branch

## Solution

I have added to_u8_array and to_u8_array_no_alpha to a new trait called
ColorToPacked to mirror the f32 conversions in ColorToComponents and
implemented the new trait for Srgba and LinearRgba.
To go with those I also added matching from_u8... functions and
converted a couple of cases that used ad-hoc implementations of that
conversion to use these.
After discussion on Discord of the experience of using the API I renamed
Color::linear to Color::to_linear, as without that it looks like a
constructor (like Color::rgb).
I also added to_srgba which is the other commonly converted to type of
color (for UI and 2D) to match to_linear.
Removed a redundant extra implementation of to_f32_array for LinearColor
as it is also supplied in ColorToComponents (I'm surprised that's
allowed?)

## Testing

Ran all tests and manually tested.
Added to_and_from_u8 to linear_rgba::tests

## Changelog

visible change is Color::linear becomes Color::to_linear.

---------

Co-authored-by: John Payne <20407779+johngpayne@users.noreply.github.com>
2024-06-10 19:31:41 +02:00
JMS55
8c5769ab92
Remove unused mip_bias parameter from apply_normal_mapping (#13752)
Mip bias is no longer used here
2024-06-10 19:31:41 +02:00
Joseph
1ae616fef1
Improve error handling for AssetServer::add_async (#13745)
# Objective

The method `AssetServer::add_async` (added in
https://github.com/bevyengine/bevy/pull/13700) requires a future that
returns an `AssetLoadError` error, which was a bit of an oversight on my
part, as that type of error only really makes sense in the context of
bevy's own asset loader -- returning it from user-defined futures isn't
very useful.

## Solution

Allow passing custom error types to `add_async`, which get cast into a
trait object matching the form of `AssetLoader::load`. If merged before
the next release this will not be a breaking change
2024-06-10 19:31:41 +02:00
Lynn
a2c5b0d415
Add segments to ExtrusionBuilder (#13719)
# Objective

- Add support for `segments` for extrusion-meshes, akin to what is
possible with cylinders

## Solution

- Added a `.segments(segments: usize)` function to `ExtrusionBuilder`.
- Implemented support for segments in the meshing algorithm.
- If you set `.segments(0)`, the meshing will fail, just like it does
with cylinders.

## Additional information

Here is a wireframe of some extrusions with 1, 2, 3, etc. segments:

![image_2024-06-06_233205114](https://github.com/bevyengine/bevy/assets/62256001/358081e2-172d-407b-8bdb-9cda88eb4664)

---------

Co-authored-by: Lynn Büttgenbach <62256001+solis-lumine-vorago@users.noreply.github.com>
2024-06-10 19:31:41 +02:00
Julian
be65fbb691
2D top-down camera example (#12720)
# Objective

This PR addresses the 2D part of #12658. I plan to separate the examples
and make one PR per camera example.

## Solution

Added a new top-down example composed of:

- [x] Player keyboard movements
- [x] UI for keyboard instructions
- [x] Colors and bloom effect to see the movement of the player
- [x] Camera smooth movement towards the player (lerp)

## Testing

```bash
cargo run --features="wayland,bevy/dynamic_linking" --example 2d_top_down_camera
```



https://github.com/bevyengine/bevy/assets/10638479/95db0587-e5e0-4f55-be11-97444b795793
2024-06-10 19:31:41 +02:00
Niklas Eicker
91db570d6e
Let init_non_send_resource require FromWorld instead of Default (#13779)
# Objective

- Let `init_non_send_resource` take `FromWorld` values again, not only
`Default`
- This reverts an unintended breaking change introduced in #9202

## Solution

- The resource initialized with `init_non_send_resource` requires
`FromWorld` again
2024-06-10 19:31:41 +02:00
thebluefish
621cd23ffc
Fix EntityCommands::despawn docs (#13774)
# Objective

The `EntityCommands::despawn` method was previously changed from
panicking behavior to a warning, but the docs continue to state that it
panics.

## Solution

- Removed panic section, copied warning blurb from `World::despawn`
- Adds a similar warning blurb to
`DespawnRecursiveExt::despawn_recursive` and
`DespawnRecursiveExt::despawn_descendants`
2024-06-09 20:52:52 +02:00
Chris Biscardi
7ae3c94b0f
view.inverse_clip_from_world should be world_from_clip (#13756)
As per the other changes in
https://github.com/bevyengine/bevy/pull/13489
`view.inverse_clip_from_world` should be `world_from_clip`.



# Objective

fixes #13749

## Solution

Modified lines.wgsl to use the right name as the current name does not
exist.

## Testing

I ran the 3d_gizmos example and pressed "p".

![screenshot-2024-06-08-at-13 21
22@2x](https://github.com/bevyengine/bevy/assets/551247/b8bfd3db-8273-4606-9dae-040764339883)

![screenshot-2024-06-08-at-13 21
26@2x](https://github.com/bevyengine/bevy/assets/551247/2619f1ae-ce83-44d7-a9fc-07e686950887)
2024-06-09 16:55:22 +02:00
Vitaliy Sapronenko
b14684ee12
Clear messed up feature flag on AppExitStates impl (#13737)
# Objective

- In #13649 additional method had been added to AppExitStates, but there
feature gate left for method in implementation for App at refactoring
stage.
- Fixes #13733 .

## Solution

- Removed the feature gate.

## Testing

- Ran reproducing example from #13733 with no compilation errors
2024-06-09 16:22:50 +02:00
charlotte
971723e4b4
13743 app exit hang (#13744)
Fixes #13743.

---------

Co-authored-by: Brezak <bezak.adam@proton.me>
2024-06-09 01:18:07 +02:00
Mincong Lu
4df95384ba
Make the component types of the new animation players clonable. (#13736)
# Objective

Some use cases might require holding onto the previous state of the
animation player for change detection.

## Solution

Added `clone` and `copy` implementation to most animation types. 
Added optimized `clone_from` implementations for the specific use case
of holding a `PreviousAnimationPlayer` component.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2024-06-09 01:18:07 +02:00
François Mockers
c71fa76f12
rename the crate bevy_state_macros_official back to its original name (#13732)
- Thanks to the original author we can now use the original name

- Use it
2024-06-09 01:18:07 +02:00
François Mockers
25306e7532
Document the Release Candidate process (#13718)
# Objective

- Document how to release a RC

## Solution

- Also allow CI to trigger on release branches
2024-06-09 01:18:07 +02:00
François Mockers
ce19b0f63f
Update crate metadata for bevy state (#13722)
- crate metadata for bevy_state has been copied from bevy_ecs

- Update it
2024-06-09 01:18:07 +02:00
François
79a93de606
Release Candidate 0.14.0-rc.2 2024-06-06 23:56:56 +02:00
François
5f2e927ae5
rename bevy_state_macros to bevy_state_macros_official 2024-06-06 23:55:47 +02:00
charlotte
43204447e8
Allow mix of hdr and non-hdr cameras to same render target (#13419)
Changes:
- Track whether an output texture has been written to yet and only clear
it on the first write.
- Use `ClearColorConfig` on `CameraOutputMode` instead of a raw
`LoadOp`.
- Track whether a output texture has been seen when specializing the
upscaling pipeline and use alpha blending for extra cameras rendering to
that texture that do not specify an explicit blend mode.

Fixes #6754

## Testing

Tested against provided test case in issue:

![image](https://github.com/bevyengine/bevy/assets/10366310/d066f069-87fb-4249-a4d9-b6cb1751971b)

---

## Changelog

- Allow cameras rendering to the same output texture with mixed hdr to
work correctly.

## Migration Guide

- - Change `CameraOutputMode` to use `ClearColorConfig` instead of
`LoadOp`.
2024-06-06 23:12:32 +02:00
François
2d11d9a48d
Release Candidate 0.14.0-rc.1 2024-06-06 22:34:20 +02:00
250 changed files with 9261 additions and 2560 deletions

View File

@ -6,6 +6,7 @@ on:
push:
branches:
- main
- release-*
env:
CARGO_TERM_COLOR: always

View File

@ -6,6 +6,7 @@ on:
push:
branches:
- main
- release-*
concurrency:
group: ${{github.workflow}}-${{github.ref}}

View File

@ -1,6 +1,6 @@
[package]
name = "bevy"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
categories = ["game-engines", "graphics", "gui", "rendering"]
description = "A refreshingly simple data-driven game engine and app framework"
@ -10,7 +10,7 @@ keywords = ["game", "engine", "gamedev", "graphics", "bevy"]
license = "MIT OR Apache-2.0"
repository = "https://github.com/bevyengine/bevy"
documentation = "https://docs.rs/bevy"
rust-version = "1.78.0"
rust-version = "1.79.0"
[workspace]
exclude = [
@ -38,6 +38,7 @@ undocumented_unsafe_blocks = "warn"
redundant_else = "warn"
match_same_arms = "warn"
semicolon_if_nothing_returned = "warn"
doc_lazy_continuation = "allow"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
@ -76,6 +77,7 @@ default = [
"bevy_gizmos",
"android_shared_stdcxx",
"tonemapping_luts",
"smaa_luts",
"default_font",
"webgl2",
"sysinfo_plugin",
@ -282,6 +284,9 @@ detailed_trace = ["bevy_internal/detailed_trace"]
# Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method on your `Camera2dBundle` or `Camera3dBundle`.
tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "zstd"]
# Include SMAA Look Up Tables KTX2 Files
smaa_luts = ["bevy_internal/smaa_luts"]
# Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)
accesskit_unix = ["bevy_internal/accesskit_unix"]
@ -308,6 +313,9 @@ pbr_multi_layer_material_textures = [
"bevy_internal/pbr_multi_layer_material_textures",
]
# Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs
pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"]
# Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.
webgl2 = ["bevy_internal/webgl"]
@ -339,11 +347,11 @@ ios_simulator = ["bevy_internal/ios_simulator"]
bevy_state = ["bevy_internal/bevy_state"]
[dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false }
bevy_internal = { path = "crates/bevy_internal", version = "0.14.2", default-features = false }
# WASM does not support dynamic linking.
[target.'cfg(not(target_family = "wasm"))'.dependencies]
bevy_dylib = { path = "crates/bevy_dylib", version = "0.14.0-dev", default-features = false, optional = true }
bevy_dylib = { path = "crates/bevy_dylib", version = "0.14.2", default-features = false, optional = true }
[dev-dependencies]
rand = "0.8.0"
@ -746,7 +754,8 @@ doc-scrape-examples = true
name = "Lines"
description = "Create a custom material to draw 3d lines"
category = "3D Rendering"
wasm = true
# WASM does not support the `POLYGON_MODE_LINE` feature.
wasm = false
[[example]]
name = "ssao"
@ -2534,6 +2543,17 @@ description = "Systems run in parallel, but their order isn't always determinist
category = "ECS (Entity Component System)"
wasm = false
[[example]]
name = "observers"
path = "examples/ecs/observers.rs"
doc-scrape-examples = true
[package.metadata.example.observers]
name = "Observers"
description = "Demonstrates observers that react to events (both built-in life-cycle events and custom events)"
category = "ECS (Entity Component System)"
wasm = true
[[example]]
name = "3d_rotation"
path = "examples/transforms/3d_rotation.rs"
@ -2833,6 +2853,17 @@ description = "Illustrates how to use 9 Slicing in UI"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "ui_texture_slice_flip_and_tile"
path = "examples/ui/ui_texture_slice_flip_and_tile.rs"
doc-scrape-examples = true
[package.metadata.example.ui_texture_slice_flip_and_tile]
name = "UI Texture Slice Flipping and Tiling"
description = "Illustrates how to flip and tile images with 9 Slicing in UI"
category = "UI (User Interface)"
wasm = true
[[example]]
name = "ui_texture_atlas_slice"
path = "examples/ui/ui_texture_atlas_slice.rs"
@ -3024,6 +3055,17 @@ description = "Demonstrates all the primitives which can be sampled."
category = "Math"
wasm = true
[[example]]
name = "custom_primitives"
path = "examples/math/custom_primitives.rs"
doc-scrape-examples = true
[package.metadata.example.custom_primitives]
name = "Custom Primitives"
description = "Demonstrates how to add custom primitives and useful traits for them."
category = "Math"
wasm = true
[[example]]
name = "random_sampling"
path = "examples/math/random_sampling.rs"
@ -3086,6 +3128,28 @@ path = "examples/dev_tools/fps_overlay.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]
[[example]]
name = "2d_top_down_camera"
path = "examples/camera/2d_top_down_camera.rs"
doc-scrape-examples = true
[package.metadata.example.2d_top_down_camera]
name = "2D top-down camera"
description = "A 2D top-down camera smoothly following player movements"
category = "Camera"
wasm = true
[[example]]
name = "first_person_view_model"
path = "examples/camera/first_person_view_model.rs"
doc-scrape-examples = true
[package.metadata.example.first_person_view_model]
name = "First person view model"
description = "A first-person camera that uses a world model and a view model with different field of views (FOV)"
category = "Camera"
wasm = true
[package.metadata.example.fps_overlay]
name = "FPS overlay"
description = "Demonstrates FPS overlay"
@ -3163,7 +3227,7 @@ wasm = true
name = "anisotropy"
path = "examples/3d/anisotropy.rs"
doc-scrape-examples = true
required-features = ["jpeg"]
required-features = ["jpeg", "pbr_anisotropy_texture"]
[package.metadata.example.anisotropy]
name = "Anisotropy"
@ -3171,6 +3235,17 @@ description = "Displays an example model with anisotropy"
category = "3D Rendering"
wasm = false
[[example]]
name = "custom_phase_item"
path = "examples/shader/custom_phase_item.rs"
doc-scrape-examples = true
[package.metadata.example.custom_phase_item]
name = "Custom phase item"
description = "Demonstrates how to enqueue custom draw commands in a render phase"
category = "Shaders"
wasm = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

View File

@ -0,0 +1,288 @@
{
"extensionsUsed": [
"KHR_materials_unlit"
],
"asset": {
"generator": "UniGLTF-1.27",
"version": "2.0"
},
"buffers": [
{
"uri": "craft_speederD_data.bin",
"byteLength": 20120
}
],
"bufferViews": [
{
"buffer": 0,
"byteOffset": 0,
"byteLength": 6096,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 6096,
"byteLength": 6096,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 12192,
"byteLength": 4064,
"target": 34962
},
{
"buffer": 0,
"byteOffset": 16256,
"byteLength": 732,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 16988,
"byteLength": 1368,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 18356,
"byteLength": 456,
"target": 34963
},
{
"buffer": 0,
"byteOffset": 18812,
"byteLength": 1308,
"target": 34963
}
],
"accessors": [
{
"bufferView": 0,
"byteOffset": 0,
"type": "VEC3",
"componentType": 5126,
"count": 508,
"max": [
1.4,
0.9,
1.11283529
],
"min": [
-1.4,
0,
-1.11283529
],
"normalized": false
},
{
"bufferView": 1,
"byteOffset": 0,
"type": "VEC3",
"componentType": 5126,
"count": 508,
"normalized": false
},
{
"bufferView": 2,
"byteOffset": 0,
"type": "VEC2",
"componentType": 5126,
"count": 508,
"normalized": false
},
{
"bufferView": 3,
"byteOffset": 0,
"type": "SCALAR",
"componentType": 5125,
"count": 183,
"normalized": false
},
{
"bufferView": 4,
"byteOffset": 0,
"type": "SCALAR",
"componentType": 5125,
"count": 342,
"normalized": false
},
{
"bufferView": 5,
"byteOffset": 0,
"type": "SCALAR",
"componentType": 5125,
"count": 114,
"normalized": false
},
{
"bufferView": 6,
"byteOffset": 0,
"type": "SCALAR",
"componentType": 5125,
"count": 327,
"normalized": false
}
],
"materials": [
{
"name": "metal",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.843137264,
0.870588243,
0.9098039,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"doubleSided": false,
"alphaMode": "OPAQUE"
},
{
"name": "metalDark",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.6750623,
0.7100219,
0.7735849,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"doubleSided": false,
"alphaMode": "OPAQUE"
},
{
"name": "dark",
"pbrMetallicRoughness": {
"baseColorFactor": [
0.274509817,
0.298039228,
0.34117648,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"doubleSided": false,
"alphaMode": "OPAQUE"
},
{
"name": "metalRed",
"pbrMetallicRoughness": {
"baseColorFactor": [
1,
0.628524244,
0.2028302,
1
],
"metallicFactor": 1,
"roughnessFactor": 1
},
"doubleSided": false,
"alphaMode": "OPAQUE"
}
],
"meshes": [
{
"name": "Mesh craft_speederD",
"primitives": [
{
"mode": 4,
"indices": 3,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
},
"material": 0
},
{
"mode": 4,
"indices": 4,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
},
"material": 1
},
{
"mode": 4,
"indices": 5,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
},
"material": 2
},
{
"mode": 4,
"indices": 6,
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
},
"material": 3
}
]
}
],
"nodes": [
{
"children": [
1
],
"name": "tmpParent",
"translation": [
0,
0,
0
],
"rotation": [
0,
0,
0,
1
],
"scale": [
1,
1,
1
]
},
{
"name": "craft_speederD",
"translation": [
0,
0,
0
],
"rotation": [
0,
0,
0,
1
],
"scale": [
1,
1,
1
],
"mesh": 0
}
],
"scenes": [
{
"nodes": [
1
]
}
],
"scene": 0
}

Binary file not shown.

View File

@ -49,7 +49,6 @@ fn fragment(
double_sided,
is_front,
Nt,
view.mip_bias,
);
#endif

View File

@ -0,0 +1,36 @@
// `custom_phase_item.wgsl`
//
// This shader goes with the `custom_phase_item` example. It demonstrates how to
// enqueue custom rendering logic in a `RenderPhase`.
// The GPU-side vertex structure.
struct Vertex {
// The world-space position of the vertex.
@location(0) position: vec3<f32>,
// The color of the vertex.
@location(1) color: vec3<f32>,
};
// Information passed from the vertex shader to the fragment shader.
struct VertexOutput {
// The clip-space position of the vertex.
@builtin(position) clip_position: vec4<f32>,
// The color of the vertex.
@location(0) color: vec3<f32>,
};
// The vertex shader entry point.
@vertex
fn vertex(vertex: Vertex) -> VertexOutput {
// Use an orthographic projection.
var vertex_output: VertexOutput;
vertex_output.clip_position = vec4(vertex.position.xyz, 1.0);
vertex_output.color = vertex.color;
return vertex_output;
}
// The fragment shader entry point.
@fragment
fn fragment(vertex_output: VertexOutput) -> @location(0) vec4<f32> {
return vec4(vertex_output.color, 1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_a11y"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides accessibility support for Bevy Engine"
homepage = "https://bevyengine.org"
@ -10,9 +10,9 @@ keywords = ["bevy", "accessibility", "a11y"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_derive = { path = "../bevy_derive", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
accesskit = "0.14"

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_animation"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides animation functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -10,23 +10,23 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_color = { path = "../bevy_color", version = "0.14.3" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_derive = { path = "../bevy_derive", version = "0.14.2" }
bevy_log = { path = "../bevy_log", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"bevy",
"petgraph",
] }
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.14.2" }
bevy_time = { path = "../bevy_time", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.2" }
# other
fixedbitset = "0.5"

View File

@ -1,3 +1,5 @@
//! Traits and type for interpolating between values.
use crate::util;
use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza};
use bevy_ecs::world::World;

66
crates/bevy_animation/src/lib.rs Normal file → Executable file
View File

@ -7,9 +7,9 @@
//! Animation for the game engine Bevy
mod animatable;
mod graph;
mod transition;
pub mod animatable;
pub mod graph;
pub mod transition;
mod util;
use std::cell::RefCell;
@ -155,6 +155,29 @@ impl VariableCurve {
Some(step_start)
}
/// Find the index of the keyframe at or before the current time.
///
/// Returns the first keyframe if the `seek_time` is before the first keyframe, and
/// the second-to-last keyframe if the `seek_time` is after the last keyframe.
/// Panics if there are less than 2 keyframes.
pub fn find_interpolation_start_keyframe(&self, seek_time: f32) -> usize {
// An Ok(keyframe_index) result means an exact result was found by binary search
// An Err result means the keyframe was not found, and the index is the keyframe
// PERF: finding the current keyframe can be optimised
let search_result = self
.keyframe_timestamps
.binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap());
// We want to find the index of the keyframe before the current time
// If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated.
match search_result {
// An exact match was found
Ok(i) => i.clamp(0, self.keyframe_timestamps.len() - 2),
// No exact match was found, so return the previous keyframe to interpolate from.
Err(i) => (i.saturating_sub(1)).clamp(0, self.keyframe_timestamps.len() - 2),
}
}
}
/// Interpolation method to use between keyframes.
@ -236,7 +259,7 @@ impl Hash for AnimationTargetId {
/// Note that each entity can only be animated by one animation player at a
/// time. However, you can change [`AnimationTarget`]'s `player` property at
/// runtime to change which player is responsible for animating the entity.
#[derive(Clone, Component, Reflect)]
#[derive(Clone, Copy, Component, Reflect)]
#[reflect(Component, MapEntities)]
pub struct AnimationTarget {
/// The ID of this animation target.
@ -326,7 +349,7 @@ pub enum RepeatAnimation {
/// playing, but is presently paused.
///
/// An stopped animation is considered no longer active.
#[derive(Debug, Reflect)]
#[derive(Debug, Clone, Copy, Reflect)]
pub struct ActiveAnimation {
/// The factor by which the weight from the [`AnimationGraph`] is multiplied.
weight: f32,
@ -515,6 +538,21 @@ pub struct AnimationPlayer {
blend_weights: HashMap<AnimationNodeIndex, f32>,
}
// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`.
impl Clone for AnimationPlayer {
fn clone(&self) -> Self {
Self {
active_animations: self.active_animations.clone(),
blend_weights: self.blend_weights.clone(),
}
}
fn clone_from(&mut self, source: &Self) {
self.active_animations.clone_from(&source.active_animations);
self.blend_weights.clone_from(&source.blend_weights);
}
}
/// The components that we might need to read or write during animation of each
/// animation target.
struct AnimationTargetContext<'a> {
@ -549,14 +587,14 @@ thread_local! {
impl AnimationPlayer {
/// Start playing an animation, restarting it if necessary.
pub fn start(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation {
self.active_animations.entry(animation).or_default()
let playing_animation = self.active_animations.entry(animation).or_default();
playing_animation.replay();
playing_animation
}
/// Start playing an animation, unless the requested animation is already playing.
pub fn play(&mut self, animation: AnimationNodeIndex) -> &mut ActiveAnimation {
let playing_animation = self.active_animations.entry(animation).or_default();
playing_animation.weight = 1.0;
playing_animation
self.active_animations.entry(animation).or_default()
}
/// Stops playing the given animation, removing it from the list of playing
@ -859,18 +897,16 @@ impl AnimationTargetContext<'_> {
// Some curves have only one keyframe used to set a transform
if curve.keyframe_timestamps.len() == 1 {
self.apply_single_keyframe(curve, weight);
return;
continue;
}
// Find the current keyframe
let Some(step_start) = curve.find_current_keyframe(seek_time) else {
return;
};
// Find the best keyframe to interpolate from
let step_start = curve.find_interpolation_start_keyframe(seek_time);
let timestamp_start = curve.keyframe_timestamps[step_start];
let timestamp_end = curve.keyframe_timestamps[step_start + 1];
// Compute how far we are through the keyframe, normalized to [0, 1]
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time);
let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time).clamp(0.0, 1.0);
self.apply_tweened_keyframe(
curve,

View File

@ -5,9 +5,10 @@
use bevy_ecs::{
component::Component,
reflect::ReflectComponent,
system::{Query, Res},
};
use bevy_reflect::Reflect;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_time::Time;
use bevy_utils::Duration;
@ -28,13 +29,29 @@ use crate::{graph::AnimationNodeIndex, ActiveAnimation, AnimationPlayer};
/// component to get confused about which animation is the "main" animation, and
/// transitions will usually be incorrect as a result.
#[derive(Component, Default, Reflect)]
#[reflect(Component, Default)]
pub struct AnimationTransitions {
main_animation: Option<AnimationNodeIndex>,
transitions: Vec<AnimationTransition>,
}
// This is needed since `#[derive(Clone)]` does not generate optimized `clone_from`.
impl Clone for AnimationTransitions {
fn clone(&self) -> Self {
Self {
main_animation: self.main_animation,
transitions: self.transitions.clone(),
}
}
fn clone_from(&mut self, source: &Self) {
self.main_animation = source.main_animation;
self.transitions.clone_from(&source.transitions);
}
}
/// An animation that is being faded out as part of a transition
#[derive(Debug, Reflect)]
#[derive(Debug, Clone, Copy, Reflect)]
pub struct AnimationTransition {
/// The current weight. Starts at 1.0 and goes to 0.0 during the fade-out.
current_weight: f32,
@ -75,9 +92,18 @@ impl AnimationTransitions {
}
}
self.main_animation = Some(new_animation);
// If already transitioning away from this animation, cancel the transition.
// Otherwise the transition ending would incorrectly stop the new animation.
self.transitions
.retain(|transition| transition.animation != new_animation);
player.start(new_animation)
}
/// Obtain the currently playing main animation.
pub fn get_main_animation(&self) -> Option<AnimationNodeIndex> {
self.main_animation
}
}
/// A system that alters the weight of currently-playing transitions based on

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_app"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides core App functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -13,18 +13,16 @@ trace = []
bevy_debug_stepping = []
default = ["bevy_reflect"]
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]
serialize = ["bevy_ecs/serde"]
[dependencies]
# bevy
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
# other
serde = { version = "1.0", features = ["derive"], optional = true }
downcast-rs = "1.2.0"
thiserror = "1.0"

View File

@ -8,7 +8,7 @@ use bevy_ecs::{
intern::Interned,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::SystemId,
system::{IntoObserverSystem, SystemId},
};
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
@ -424,9 +424,12 @@ impl App {
self
}
/// Inserts the [`!Send`](Send) resource into the app, initialized with its default value,
/// if there is no existing instance of `R`.
pub fn init_non_send_resource<R: 'static + Default>(&mut self) -> &mut Self {
/// Inserts the [`!Send`](Send) resource into the app if there is no existing instance of `R`.
///
/// `R` must implement [`FromWorld`].
/// If `R` implements [`Default`], [`FromWorld`] will be automatically implemented and
/// initialize the [`Resource`] with [`Default::default`].
pub fn init_non_send_resource<R: 'static + FromWorld>(&mut self) -> &mut Self {
self.world_mut().init_non_send_resource::<R>();
self
}
@ -436,12 +439,7 @@ impl App {
plugin: Box<dyn Plugin>,
) -> Result<&mut Self, AppError> {
debug!("added plugin: {}", plugin.name());
if plugin.is_unique()
&& !self
.main_mut()
.plugin_names
.insert(plugin.name().to_string())
{
if plugin.is_unique() && self.main_mut().plugin_names.contains(plugin.name()) {
Err(AppError::DuplicatePlugin {
plugin_name: plugin.name().to_string(),
})?;
@ -456,6 +454,9 @@ impl App {
self.main_mut().plugin_build_depth += 1;
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));
self.main_mut()
.plugin_names
.insert(plugin.name().to_string());
self.main_mut().plugin_build_depth -= 1;
if let Err(payload) = result {
@ -662,6 +663,11 @@ impl App {
self.sub_apps.sub_apps.remove(&label.intern())
}
/// Extract data from the main world into the [`SubApp`] with the given label and perform an update if it exists.
pub fn update_sub_app_by_label(&mut self, label: impl AppLabel) {
self.sub_apps.update_subapp_by_label(label);
}
/// Inserts a new `schedule` under the provided `label`, overwriting any existing
/// schedule with the same label.
pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self {
@ -828,6 +834,15 @@ impl App {
None
}
/// Spawns an [`Observer`] entity, which will watch for and respond to the given event.
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.world_mut().observe(observer);
self
}
}
type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
@ -916,11 +931,21 @@ impl Termination for AppExit {
#[cfg(test)]
mod tests {
use std::{marker::PhantomData, mem};
use std::{iter, marker::PhantomData, mem, sync::Mutex};
use bevy_ecs::{event::EventWriter, schedule::ScheduleLabel, system::Commands};
use bevy_ecs::{
change_detection::{DetectChanges, ResMut},
component::Component,
entity::Entity,
event::{Event, EventWriter, Events},
query::With,
removal_detection::RemovedComponents,
schedule::{IntoSystemConfigs, ScheduleLabel},
system::{Commands, Query, Resource},
world::{FromWorld, World},
};
use crate::{App, AppExit, Plugin, Update};
use crate::{App, AppExit, Plugin, SubApp, Update};
struct PluginA;
impl Plugin for PluginA {
@ -1123,6 +1148,62 @@ mod tests {
);
}
#[test]
fn test_update_clears_trackers_once() {
#[derive(Component, Copy, Clone)]
struct Foo;
let mut app = App::new();
app.world_mut().spawn_batch(iter::repeat(Foo).take(5));
fn despawn_one_foo(mut commands: Commands, foos: Query<Entity, With<Foo>>) {
if let Some(e) = foos.iter().next() {
commands.entity(e).despawn();
};
}
fn check_despawns(mut removed_foos: RemovedComponents<Foo>) {
let mut despawn_count = 0;
for _ in removed_foos.read() {
despawn_count += 1;
}
assert_eq!(despawn_count, 2);
}
app.add_systems(Update, despawn_one_foo);
app.update(); // Frame 0
app.update(); // Frame 1
app.add_systems(Update, check_despawns.after(despawn_one_foo));
app.update(); // Should see despawns from frames 1 & 2, but not frame 0
}
#[test]
fn test_extract_sees_changes() {
use super::AppLabel;
use crate::{self as bevy_app};
#[derive(AppLabel, Clone, Copy, Hash, PartialEq, Eq, Debug)]
struct MySubApp;
#[derive(Resource)]
struct Foo(usize);
let mut app = App::new();
app.world_mut().insert_resource(Foo(0));
app.add_systems(Update, |mut foo: ResMut<Foo>| {
foo.0 += 1;
});
let mut sub_app = SubApp::new();
sub_app.set_extract(|main_world, _sub_world| {
assert!(main_world.get_resource_ref::<Foo>().unwrap().is_changed());
});
app.insert_sub_app(MySubApp, sub_app);
app.update();
}
#[test]
fn runner_returns_correct_exit_code() {
fn raise_exits(mut exits: EventWriter<AppExit>) {
@ -1178,4 +1259,83 @@ mod tests {
// it's nice they're so small let's keep it that way.
assert_eq!(mem::size_of::<AppExit>(), mem::size_of::<u8>());
}
#[test]
fn initializing_resources_from_world() {
#[derive(Resource)]
struct TestResource;
impl FromWorld for TestResource {
fn from_world(_world: &mut World) -> Self {
TestResource
}
}
#[derive(Resource)]
struct NonSendTestResource {
_marker: PhantomData<Mutex<()>>,
}
impl FromWorld for NonSendTestResource {
fn from_world(_world: &mut World) -> Self {
NonSendTestResource {
_marker: PhantomData,
}
}
}
App::new()
.init_non_send_resource::<NonSendTestResource>()
.init_resource::<TestResource>();
}
#[test]
/// Plugin should not be considered inserted while it's being built
///
/// bug: <https://github.com/bevyengine/bevy/issues/13815>
fn plugin_should_not_be_added_during_build_time() {
pub struct Foo;
impl Plugin for Foo {
fn build(&self, app: &mut App) {
assert!(!app.is_plugin_added::<Self>());
}
}
App::new().add_plugins(Foo);
}
#[test]
fn events_should_be_updated_once_per_update() {
#[derive(Event, Clone)]
struct TestEvent;
let mut app = App::new();
app.add_event::<TestEvent>();
// Starts empty
let test_events = app.world().resource::<Events<TestEvent>>();
assert_eq!(test_events.len(), 0);
assert_eq!(test_events.iter_current_update_events().count(), 0);
app.update();
// Sending one event
app.world_mut().send_event(TestEvent);
let test_events = app.world().resource::<Events<TestEvent>>();
assert_eq!(test_events.len(), 1);
assert_eq!(test_events.iter_current_update_events().count(), 1);
app.update();
// Sending two events on the next frame
app.world_mut().send_event(TestEvent);
app.world_mut().send_event(TestEvent);
let test_events = app.world().resource::<Events<TestEvent>>();
assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3
assert_eq!(test_events.iter_current_update_events().count(), 2);
app.update();
// Sending zero events
let test_events = app.world().resource::<Events<TestEvent>>();
assert_eq!(test_events.len(), 2); // Events are double-buffered, so we see 2 + 0 = 2
assert_eq!(test_events.iter_current_update_events().count(), 0);
}
}

View File

@ -17,7 +17,7 @@ use bevy_ecs::{
/// Then it will run:
/// * [`First`]
/// * [`PreUpdate`]
/// * [`StateTransition`]
/// * [`StateTransition`](bevy_state::transition::StateTransition)
/// * [`RunFixedMainLoop`]
/// * This will run [`FixedMain`] zero to many times, based on how much time has elapsed.
/// * [`Update`]
@ -195,6 +195,16 @@ impl MainScheduleOrder {
self.labels.insert(index + 1, schedule.intern());
}
/// Adds the given `schedule` before the `before` schedule in the main list of schedules.
pub fn insert_before(&mut self, before: impl ScheduleLabel, schedule: impl ScheduleLabel) {
let index = self
.labels
.iter()
.position(|current| (**current).eq(&before))
.unwrap_or_else(|| panic!("Expected {before:?} to exist"));
self.labels.insert(index, schedule.intern());
}
/// Adds the given `schedule` after the `after` schedule in the list of startup schedules.
pub fn insert_startup_after(
&mut self,
@ -208,6 +218,20 @@ impl MainScheduleOrder {
.unwrap_or_else(|| panic!("Expected {after:?} to exist"));
self.startup_labels.insert(index + 1, schedule.intern());
}
/// Adds the given `schedule` before the `before` schedule in the list of startup schedules.
pub fn insert_startup_before(
&mut self,
before: impl ScheduleLabel,
schedule: impl ScheduleLabel,
) {
let index = self
.startup_labels
.iter()
.position(|current| (**current).eq(&before))
.unwrap_or_else(|| panic!("Expected {before:?} to exist"));
self.startup_labels.insert(index, schedule.intern());
}
}
impl Main {
@ -291,6 +315,16 @@ impl FixedMainScheduleOrder {
.unwrap_or_else(|| panic!("Expected {after:?} to exist"));
self.labels.insert(index + 1, schedule.intern());
}
/// Adds the given `schedule` before the `before` schedule
pub fn insert_before(&mut self, before: impl ScheduleLabel, schedule: impl ScheduleLabel) {
let index = self
.labels
.iter()
.position(|current| (**current).eq(&before))
.unwrap_or_else(|| panic!("Expected {before:?} to exist"));
self.labels.insert(index, schedule.intern());
}
}
impl FixedMain {

View File

@ -1,4 +1,4 @@
use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState};
use crate::{App, AppLabel, InternedAppLabel, Plugin, Plugins, PluginsState};
use bevy_ecs::{
event::EventRegistry,
prelude::*,
@ -125,7 +125,9 @@ impl SubApp {
}
/// Runs the default schedule.
pub fn update(&mut self) {
///
/// Does not clear internal trackers used for change detection.
pub fn run_default_schedule(&mut self) {
if self.is_building_plugins() {
panic!("SubApp::update() was called while a plugin was building.");
}
@ -133,6 +135,11 @@ impl SubApp {
if let Some(label) = self.update_schedule {
self.world.run_schedule(label);
}
}
/// Runs the default schedule and updates internal component trackers.
pub fn update(&mut self) {
self.run_default_schedule();
self.world.clear_trackers();
}
@ -421,7 +428,7 @@ impl SubApps {
{
#[cfg(feature = "trace")]
let _bevy_frame_update_span = info_span!("main app").entered();
self.main.update();
self.main.run_default_schedule();
}
for (_label, sub_app) in self.sub_apps.iter_mut() {
#[cfg(feature = "trace")]
@ -442,4 +449,12 @@ impl SubApps {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut SubApp> + '_ {
std::iter::once(&mut self.main).chain(self.sub_apps.values_mut())
}
/// Extract data from the main world into the [`SubApp`] with the given label and perform an update if it exists.
pub fn update_subapp_by_label(&mut self, label: impl AppLabel) {
if let Some(sub_app) = self.sub_apps.get_mut(&label.intern()) {
sub_app.extract(&mut self.main.world);
sub_app.update();
}
}
}

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_asset"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides asset functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -19,14 +19,14 @@ watch = []
trace = []
[dependencies]
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset_macros = { path = "macros", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset_macros = { path = "macros", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"uuid",
] }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
async-broadcast = "0.5"
async-fs = "2.0"
@ -43,7 +43,7 @@ thiserror = "1.0"
uuid = { version = "1.0", features = ["v4"] }
[target.'cfg(target_os = "android")'.dependencies]
bevy_winit = { path = "../bevy_winit", version = "0.14.0-dev" }
bevy_winit = { path = "../bevy_winit", version = "0.14.2" }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
@ -59,8 +59,8 @@ js-sys = "0.3"
notify-debouncer-full = { version = "0.3.1", optional = true }
[dev-dependencies]
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_log = { path = "../bevy_log", version = "0.14.2" }
[lints]
workspace = true

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_asset_macros"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Derive implementations for bevy_asset"
homepage = "https://bevyengine.org"
@ -12,7 +12,7 @@ keywords = ["bevy"]
proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.2" }
syn = "2.0"
proc-macro2 = "1.0"

View File

@ -288,13 +288,17 @@ impl Hash for UntypedAssetId {
}
}
impl Ord for UntypedAssetId {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.type_id()
.cmp(&other.type_id())
.then_with(|| self.internal().cmp(&other.internal()))
}
}
impl PartialOrd for UntypedAssetId {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
if self.type_id() != other.type_id() {
None
} else {
Some(self.internal().cmp(&other.internal()))
}
Some(self.cmp(other))
}
}

View File

@ -574,9 +574,11 @@ pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
meta_path
}
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
/// A [`PathBuf`] [`Stream`] implementation that immediately returns nothing.
struct EmptyPathStream;
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
impl Stream for EmptyPathStream {
type Item = PathBuf;

View File

@ -1511,6 +1511,7 @@ mod tests {
Empty,
}
#[allow(dead_code)]
#[derive(Asset, TypePath)]
pub struct StructTestAsset {
#[dependency]
@ -1519,6 +1520,7 @@ mod tests {
embedded: TestAsset,
}
#[allow(dead_code)]
#[derive(Asset, TypePath)]
pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);
}

View File

@ -718,9 +718,9 @@ impl AssetServer {
///
/// After the asset has been fully loaded, it will show up in the relevant [`Assets`] storage.
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
pub fn add_async<A: Asset>(
pub fn add_async<A: Asset, E: std::error::Error + Send + Sync + 'static>(
&self,
future: impl Future<Output = Result<A, AssetLoadError>> + Send + 'static,
future: impl Future<Output = Result<A, E>> + Send + 'static,
) -> Handle<A> {
let handle = self
.data
@ -741,12 +741,15 @@ impl AssetServer {
.unwrap();
}
Err(error) => {
let error = AddAsyncError {
error: Arc::new(error),
};
error!("{error}");
event_sender
.send(InternalAssetEvent::Failed {
id,
path: Default::default(),
error,
error: AssetLoadError::AddAsyncError(error),
})
.unwrap();
}
@ -1403,6 +1406,8 @@ pub enum AssetLoadError {
CannotLoadIgnoredAsset { path: AssetPath<'static> },
#[error(transparent)]
AssetLoaderError(#[from] AssetLoaderError),
#[error(transparent)]
AddAsyncError(#[from] AddAsyncError),
#[error("The file at '{}' does not contain the labeled asset '{}'; it contains the following {} assets: {}",
base_path,
label,
@ -1441,6 +1446,22 @@ impl AssetLoaderError {
}
}
#[derive(Error, Debug, Clone)]
#[error("An error occurred while resolving an asset added by `add_async`: {error}")]
pub struct AddAsyncError {
error: Arc<dyn std::error::Error + Send + Sync + 'static>,
}
impl PartialEq for AddAsyncError {
/// Equality comparison is not full (only through `TypeId`)
#[inline]
fn eq(&self, other: &Self) -> bool {
self.error.type_id() == other.error.type_id()
}
}
impl Eq for AddAsyncError {}
/// An error that occurs when an [`AssetLoader`] is not registered for a given extension.
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[error("no `AssetLoader` found{}", format_missing_asset_ext(.extensions))]

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_audio"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides audio functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -10,17 +10,17 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"bevy",
] }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_derive = { path = "../bevy_derive", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
# other
rodio = { version = "0.18", default-features = false }

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_color"
version = "0.14.0-dev"
version = "0.14.3"
edition = "2021"
description = "Types for representing and manipulating color values"
homepage = "https://bevyengine.org"
@ -10,14 +10,14 @@ keywords = ["bevy", "color"]
rust-version = "1.76.0"
[dependencies]
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"bevy",
] }
bytemuck = { version = "1", features = ["derive"] }
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "1.0"
wgpu-types = { version = "0.19", default-features = false, optional = true }
wgpu-types = { version = "0.20", default-features = false, optional = true }
encase = { version = "0.8", default-features = false }
[features]

View File

@ -73,7 +73,12 @@ impl StandardColor for Color {}
impl Color {
/// Return the color as a linear RGBA color.
pub fn linear(&self) -> LinearRgba {
pub fn to_linear(&self) -> LinearRgba {
(*self).into()
}
/// Return the color as an SRGBA color.
pub fn to_srgba(&self) -> Srgba {
(*self).into()
}

View File

@ -115,6 +115,18 @@ pub trait ColorToComponents {
fn from_vec3(color: Vec3) -> Self;
}
/// Trait with methods for converting colors to packed non-color types
pub trait ColorToPacked {
/// Convert to [u8; 4] where that makes sense (Srgba is most relevant)
fn to_u8_array(self) -> [u8; 4];
/// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
fn to_u8_array_no_alpha(self) -> [u8; 3];
/// Convert from [u8; 4] where that makes sense (Srgba is most relevant)
fn from_u8_array(color: [u8; 4]) -> Self;
/// Convert to [u8; 3] where that makes sense (Srgba is most relevant)
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self;
}
/// Utility function for interpolating hue values. This ensures that the interpolation
/// takes the shortest path around the color wheel, and that the result is always between
/// 0 and 360.

View File

@ -117,7 +117,7 @@ impl Mix for Lcha {
Self {
lightness: self.lightness * n_factor + other.lightness * factor,
chroma: self.chroma * n_factor + other.chroma * factor,
hue: self.hue * n_factor + other.hue * factor,
hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
alpha: self.alpha * n_factor + other.alpha * factor,
}
}

View File

@ -1,6 +1,6 @@
use crate::{
color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents,
Gray, Luminance, Mix, StandardColor,
ColorToPacked, Gray, Luminance, Mix, StandardColor,
};
use bevy_math::{Vec3, Vec4};
use bevy_reflect::prelude::*;
@ -149,24 +149,12 @@ impl LinearRgba {
}
}
/// Converts the color into a [f32; 4] array in RGBA order.
///
/// This is useful for passing the color to a shader.
pub fn to_f32_array(&self) -> [f32; 4] {
[self.red, self.green, self.blue, self.alpha]
}
/// Converts this color to a u32.
///
/// Maps the RGBA channels in RGBA order to a little-endian byte array (GPUs are little-endian).
/// `A` will be the most significant byte and `R` the least significant.
pub fn as_u32(&self) -> u32 {
u32::from_le_bytes([
(self.red * 255.0) as u8,
(self.green * 255.0) as u8,
(self.blue * 255.0) as u8,
(self.alpha * 255.0) as u8,
])
u32::from_le_bytes(self.to_u8_array())
}
}
@ -310,6 +298,25 @@ impl ColorToComponents for LinearRgba {
}
}
impl ColorToPacked for LinearRgba {
fn to_u8_array(self) -> [u8; 4] {
[self.red, self.green, self.blue, self.alpha]
.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn to_u8_array_no_alpha(self) -> [u8; 3] {
[self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn from_u8_array(color: [u8; 4]) -> Self {
Self::from_f32_array(color.map(|u| u as f32 / 255.0))
}
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
}
}
#[cfg(feature = "wgpu-types")]
impl From<LinearRgba> for wgpu_types::Color {
fn from(color: LinearRgba) -> Self {
@ -416,6 +423,34 @@ mod tests {
assert_eq!(a.distance_squared(&b), 1.0);
}
#[test]
fn to_and_from_u8() {
// from_u8_array
let a = LinearRgba::from_u8_array([255, 0, 0, 255]);
let b = LinearRgba::new(1.0, 0.0, 0.0, 1.0);
assert_eq!(a, b);
// from_u8_array_no_alpha
let a = LinearRgba::from_u8_array_no_alpha([255, 255, 0]);
let b = LinearRgba::rgb(1.0, 1.0, 0.0);
assert_eq!(a, b);
// to_u8_array
let a = LinearRgba::new(0.0, 0.0, 1.0, 1.0).to_u8_array();
let b = [0, 0, 255, 255];
assert_eq!(a, b);
// to_u8_array_no_alpha
let a = LinearRgba::rgb(0.0, 1.0, 1.0).to_u8_array_no_alpha();
let b = [0, 255, 255];
assert_eq!(a, b);
// clamping
let a = LinearRgba::rgb(0.0, 100.0, -100.0).to_u8_array_no_alpha();
let b = [0, 255, 0];
assert_eq!(a, b);
}
#[test]
fn darker_lighter() {
// Darker and lighter should be commutative.

View File

@ -113,7 +113,7 @@ impl Mix for Oklcha {
Self {
lightness: self.lightness * n_factor + other.lightness * factor,
chroma: self.chroma * n_factor + other.chroma * factor,
hue: self.hue * n_factor + other.hue * factor,
hue: crate::color_ops::lerp_hue(self.hue, other.hue, factor),
alpha: self.alpha * n_factor + other.alpha * factor,
}
}

View File

@ -1,7 +1,7 @@
use crate::color_difference::EuclideanDistance;
use crate::{
impl_componentwise_vector_space, Alpha, ColorToComponents, Gray, LinearRgba, Luminance, Mix,
StandardColor, Xyza,
impl_componentwise_vector_space, Alpha, ColorToComponents, ColorToPacked, Gray, LinearRgba,
Luminance, Mix, StandardColor, Xyza,
};
use bevy_math::{Vec3, Vec4};
use bevy_reflect::prelude::*;
@ -168,10 +168,7 @@ impl Srgba {
/// Convert this color to CSS-style hexadecimal notation.
pub fn to_hex(&self) -> String {
let r = (self.red * 255.0).round() as u8;
let g = (self.green * 255.0).round() as u8;
let b = (self.blue * 255.0).round() as u8;
let a = (self.alpha * 255.0).round() as u8;
let [r, g, b, a] = self.to_u8_array();
match a {
255 => format!("#{:02X}{:02X}{:02X}", r, g, b),
_ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a),
@ -189,7 +186,7 @@ impl Srgba {
/// See also [`Srgba::new`], [`Srgba::rgba_u8`], [`Srgba::hex`].
///
pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
Self::rgba_u8(r, g, b, u8::MAX)
Self::from_u8_array_no_alpha([r, g, b])
}
// Float operations in const fn are not stable yet
@ -206,12 +203,7 @@ impl Srgba {
/// See also [`Srgba::new`], [`Srgba::rgb_u8`], [`Srgba::hex`].
///
pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::new(
r as f32 / u8::MAX as f32,
g as f32 / u8::MAX as f32,
b as f32 / u8::MAX as f32,
a as f32 / u8::MAX as f32,
)
Self::from_u8_array([r, g, b, a])
}
/// Converts a non-linear sRGB value to a linear one via [gamma correction](https://en.wikipedia.org/wiki/Gamma_correction).
@ -373,6 +365,25 @@ impl ColorToComponents for Srgba {
}
}
impl ColorToPacked for Srgba {
fn to_u8_array(self) -> [u8; 4] {
[self.red, self.green, self.blue, self.alpha]
.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn to_u8_array_no_alpha(self) -> [u8; 3] {
[self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn from_u8_array(color: [u8; 4]) -> Self {
Self::from_f32_array(color.map(|u| u as f32 / 255.0))
}
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
}
}
impl From<LinearRgba> for Srgba {
#[inline]
fn from(value: LinearRgba) -> Self {

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_core"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides core functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -10,17 +10,17 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev", features = [
bevy_app = { path = "../bevy_app", version = "0.14.2", features = [
"bevy_reflect",
] }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", features = [
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2", features = [
"bevy_reflect",
] }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"bevy",
] }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
# other
serde = { version = "1.0", optional = true }

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_core_pipeline"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
authors = [
"Bevy Contributors <bevyengine@gmail.com>",
@ -18,20 +18,21 @@ trace = []
webgl = []
webgpu = []
tonemapping_luts = ["bevy_render/ktx2", "bevy_render/zstd"]
smaa_luts = ["bevy_render/ktx2", "bevy_render/zstd"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_color = { path = "../bevy_color", version = "0.14.3" }
bevy_derive = { path = "../bevy_derive", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2" }
bevy_render = { path = "../bevy_render", version = "0.14.2" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
serde = { version = "1", features = ["derive"] }
bitflags = "2.3"

View File

@ -338,7 +338,11 @@ fn prepare_bloom_textures(
{
// How many times we can halve the resolution minus one so we don't go unnecessarily low
let mip_count = MAX_MIP_DIMENSION.ilog2().max(2) - 1;
let mip_height_ratio = MAX_MIP_DIMENSION as f32 / height as f32;
let mip_height_ratio = if height != 0 {
MAX_MIP_DIMENSION as f32 / height as f32
} else {
0.
};
let texture_descriptor = TextureDescriptor {
label: Some("bloom_texture"),

View File

@ -49,7 +49,7 @@ pub const CORE_3D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
/// `sampler2DShadow` and will cheerfully generate invalid GLSL that tries to
/// perform non-percentage-closer-filtering with such a sampler. Therefore we
/// disable depth of field and screen space reflections entirely on WebGL 2.
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
#[cfg(not(any(feature = "webgpu", not(target_arch = "wasm32"))))]
pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = false;
/// True if multisampled depth textures are supported on this platform.
@ -64,7 +64,7 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true;
use std::ops::Range;
use bevy_asset::AssetId;
use bevy_asset::{AssetId, UntypedAssetId};
use bevy_color::LinearRgba;
pub use camera_3d::*;
pub use main_opaque_pass_3d_node::*;
@ -76,7 +76,6 @@ use bevy_math::FloatOrd;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
mesh::Mesh,
prelude::Msaa,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_phase::{
@ -136,6 +135,14 @@ impl Plugin for Core3dPlugin {
.init_resource::<DrawFunctions<AlphaMask3dPrepass>>()
.init_resource::<DrawFunctions<Opaque3dDeferred>>()
.init_resource::<DrawFunctions<AlphaMask3dDeferred>>()
.init_resource::<ViewBinnedRenderPhases<Opaque3d>>()
.init_resource::<ViewBinnedRenderPhases<AlphaMask3d>>()
.init_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>()
.init_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>()
.init_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>()
.init_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>()
.init_resource::<ViewSortedRenderPhases<Transmissive3d>>()
.init_resource::<ViewSortedRenderPhases<Transparent3d>>()
.add_systems(ExtractSchedule, extract_core_3d_camera_phases)
.add_systems(ExtractSchedule, extract_camera_prepass_phase)
.add_systems(
@ -213,7 +220,7 @@ pub struct Opaque3d {
pub extra_index: PhaseItemExtraIndex,
}
/// Data that must be identical in order to batch meshes together.
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque3dBinKey {
/// The identifier of the render pipeline.
@ -222,8 +229,11 @@ pub struct Opaque3dBinKey {
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The mesh.
pub asset_id: AssetId<Mesh>,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///

View File

@ -122,17 +122,17 @@ impl ViewNode for DeferredGBufferPrepassNode {
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _deferred_span = info_span!("deferred").entered();
let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_command_encoder"),
label: Some("deferred_prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("deferred"),
label: Some("deferred_prepass"),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
@ -144,24 +144,24 @@ impl ViewNode for DeferredGBufferPrepassNode {
}
// Opaque draws
if !opaque_deferred_phase.batchable_keys.is_empty()
|| !opaque_deferred_phase.unbatchable_keys.is_empty()
if !opaque_deferred_phase.batchable_mesh_keys.is_empty()
|| !opaque_deferred_phase.unbatchable_mesh_keys.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred").entered();
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
opaque_deferred_phase.render(&mut render_pass, world, view_entity);
}
// Alpha masked draws
if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred").entered();
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity);
}
drop(render_pass);
// Copy prepass depth to the main depth texture
// After rendering to the view depth texture, copy it to the prepass depth texture
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),

View File

@ -29,12 +29,11 @@ pub mod node;
use std::ops::Range;
use bevy_asset::AssetId;
use bevy_asset::UntypedAssetId;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_reflect::Reflect;
use bevy_render::{
mesh::Mesh,
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
PhaseItemExtraIndex,
@ -147,7 +146,7 @@ pub struct Opaque3dPrepass {
}
// TODO: Try interning these.
/// The data used to bin each opaque 3D mesh in the prepass and deferred pass.
/// The data used to bin each opaque 3D object in the prepass and deferred pass.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OpaqueNoLightmap3dBinKey {
/// The ID of the GPU pipeline.
@ -156,8 +155,8 @@ pub struct OpaqueNoLightmap3dBinKey {
/// The function used to draw the mesh.
pub draw_function: DrawFunctionId,
/// The ID of the mesh.
pub asset_id: AssetId<Mesh>,
/// The ID of the asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///

View File

@ -120,8 +120,8 @@ impl ViewNode for PrepassNode {
}
// Opaque draws
if !opaque_prepass_phase.batchable_keys.is_empty()
|| !opaque_prepass_phase.unbatchable_keys.is_empty()
if !opaque_prepass_phase.batchable_mesh_keys.is_empty()
|| !opaque_prepass_phase.unbatchable_mesh_keys.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_prepass").entered();
@ -162,7 +162,7 @@ impl ViewNode for PrepassNode {
pass_span.end(&mut render_pass);
drop(render_pass);
// Copy prepass depth to the main depth texture if deferred isn't going to
// After rendering to the view depth texture, copy it to the prepass depth texture if deferred isn't going to
if deferred_prepass.is_none() {
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(

View File

@ -24,7 +24,7 @@ use bevy_render::{
};
use prepass::{SkyboxPrepassPipeline, SKYBOX_PREPASS_SHADER_HANDLE};
use crate::core_3d::CORE_3D_DEPTH_FORMAT;
use crate::{core_3d::CORE_3D_DEPTH_FORMAT, prepass::PreviousViewUniforms};
const SKYBOX_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(55594763423201);
@ -53,6 +53,7 @@ impl Plugin for SkyboxPlugin {
render_app
.init_resource::<SpecializedRenderPipelines<SkyboxPipeline>>()
.init_resource::<SpecializedRenderPipelines<SkyboxPrepassPipeline>>()
.init_resource::<PreviousViewUniforms>()
.add_systems(
Render,
(

View File

@ -31,7 +31,9 @@
//! [SMAA]: https://www.iryoku.com/smaa/
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, load_internal_binary_asset, Handle};
#[cfg(feature = "smaa_luts")]
use bevy_asset::load_internal_binary_asset;
use bevy_asset::{load_internal_asset, Handle};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
@ -47,7 +49,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{
camera::ExtractedCamera,
extract_component::{ExtractComponent, ExtractComponentPlugin},
render_asset::{RenderAssetUsages, RenderAssets},
render_asset::RenderAssets,
render_graph::{
NodeRunError, RenderGraphApp as _, RenderGraphContext, ViewNode, ViewNodeRunner,
},
@ -65,15 +67,19 @@ use bevy_render::{
VertexState,
},
renderer::{RenderContext, RenderDevice, RenderQueue},
texture::{
BevyDefault, CachedTexture, CompressedImageFormats, GpuImage, Image, ImageFormat,
ImageSampler, ImageType, TextureCache,
},
texture::{BevyDefault, CachedTexture, GpuImage, Image, TextureCache},
view::{ExtractedView, ViewTarget},
Render, RenderApp, RenderSet,
};
#[cfg(feature = "smaa_luts")]
use bevy_render::{
render_asset::RenderAssetUsages,
texture::{CompressedImageFormats, ImageFormat, ImageSampler, ImageType},
};
use bevy_utils::prelude::default;
#[cfg(not(feature = "smaa_luts"))]
use crate::tonemapping::lut_placeholder;
use crate::{
core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d},
@ -287,6 +293,7 @@ impl Plugin for SmaaPlugin {
// Load the two lookup textures. These are compressed textures in KTX2
// format.
#[cfg(feature = "smaa_luts")]
load_internal_binary_asset!(
app,
SMAA_AREA_LUT_TEXTURE_HANDLE,
@ -304,6 +311,7 @@ impl Plugin for SmaaPlugin {
.expect("Failed to load SMAA area LUT")
);
#[cfg(feature = "smaa_luts")]
load_internal_binary_asset!(
app,
SMAA_SEARCH_LUT_TEXTURE_HANDLE,
@ -321,6 +329,16 @@ impl Plugin for SmaaPlugin {
.expect("Failed to load SMAA search LUT")
);
#[cfg(not(feature = "smaa_luts"))]
app.world_mut()
.resource_mut::<bevy_asset::Assets<Image>>()
.insert(SMAA_AREA_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
#[cfg(not(feature = "smaa_luts"))]
app.world_mut()
.resource_mut::<bevy_asset::Assets<Image>>()
.insert(SMAA_SEARCH_LUT_TEXTURE_HANDLE.id(), lut_placeholder());
app.add_plugins(ExtractComponentPlugin::<SmaaSettings>::default())
.register_type::<SmaaSettings>();

View File

@ -757,10 +757,10 @@ fn calculate_diag_weights(tex_coord: vec2<f32>, e: vec2<f32>, subsample_indices:
let d_xz = search_diag_2(tex_coord, vec2(-1.0, -1.0), &end);
if (textureSampleLevel(edges_texture, edges_sampler, tex_coord, 0.0, vec2(1, 0)).r > 0.0) {
let d_yw = search_diag_2(tex_coord, vec2(1.0, 1.0), &end);
d = vec4(d.x, d_yw.x, d.z, d_yw.y);
d = vec4(d_xz.x, d_yw.x, d_xz.y, d_yw.y);
d.y += f32(end.y > 0.9);
} else {
d = vec4(d.x, 0.0, d.z, 0.0);
d = vec4(d_xz.x, 0.0, d_xz.y, 0.0);
}
if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3

View File

@ -402,13 +402,13 @@ fn prepare_taa_history_textures(
views: Query<(Entity, &ExtractedCamera, &ExtractedView), With<TemporalAntiAliasSettings>>,
) {
for (entity, camera, view) in &views {
if let Some(physical_viewport_size) = camera.physical_viewport_size {
if let Some(physical_target_size) = camera.physical_target_size {
let mut texture_descriptor = TextureDescriptor {
label: None,
size: Extent3d {
depth_or_array_layers: 1,
width: physical_viewport_size.x,
height: physical_viewport_size.y,
width: physical_target_size.x,
height: physical_target_size.y,
},
mip_level_count: 1,
sample_count: 1,

View File

@ -4,6 +4,7 @@ use bevy_ecs::prelude::*;
use bevy_render::camera::{CameraOutputMode, ExtractedCamera};
use bevy_render::view::ViewTarget;
use bevy_render::{render_resource::*, Render, RenderApp, RenderSet};
use bevy_utils::HashSet;
mod node;
@ -32,16 +33,32 @@ fn prepare_view_upscaling_pipelines(
blit_pipeline: Res<BlitPipeline>,
view_targets: Query<(Entity, &ViewTarget, Option<&ExtractedCamera>)>,
) {
let mut output_textures = HashSet::new();
for (entity, view_target, camera) in view_targets.iter() {
let out_texture_id = view_target.out_texture().id();
let blend_state = if let Some(ExtractedCamera {
output_mode: CameraOutputMode::Write { blend_state, .. },
..
}) = camera
{
*blend_state
match *blend_state {
None => {
// If we've already seen this output for a camera and it doesn't have a output blend
// mode configured, default to alpha blend so that we don't accidentally overwrite
// the output texture
if output_textures.contains(&out_texture_id) {
Some(BlendState::ALPHA_BLENDING)
} else {
None
}
}
_ => *blend_state,
}
} else {
None
};
output_textures.insert(out_texture_id);
let key = BlitPipelineKey {
texture_format: view_target.out_texture_format(),
blend_state,

View File

@ -1,11 +1,11 @@
use crate::{blit::BlitPipeline, upscaling::ViewUpscalingPipeline};
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::camera::{ClearColor, ClearColorConfig};
use bevy_render::{
camera::{CameraOutputMode, ExtractedCamera},
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_resource::{
BindGroup, BindGroupEntries, LoadOp, Operations, PipelineCache, RenderPassColorAttachment,
RenderPassDescriptor, StoreOp, TextureViewId,
BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId,
},
renderer::RenderContext,
view::ViewTarget,
@ -33,19 +33,22 @@ impl ViewNode for UpscalingNode {
) -> Result<(), NodeRunError> {
let pipeline_cache = world.get_resource::<PipelineCache>().unwrap();
let blit_pipeline = world.get_resource::<BlitPipeline>().unwrap();
let clear_color_global = world.get_resource::<ClearColor>().unwrap();
let color_attachment_load_op = if let Some(camera) = camera {
let clear_color = if let Some(camera) = camera {
match camera.output_mode {
CameraOutputMode::Write {
color_attachment_load_op,
..
} => color_attachment_load_op,
CameraOutputMode::Write { clear_color, .. } => clear_color,
CameraOutputMode::Skip => return Ok(()),
}
} else {
LoadOp::Clear(Default::default())
ClearColorConfig::Default
};
let clear_color = match clear_color {
ClearColorConfig::Default => Some(clear_color_global.0),
ClearColorConfig::Custom(color) => Some(color),
ClearColorConfig::None => None,
};
let converted_clear_color = clear_color.map(|color| color.into());
let upscaled_texture = target.main_texture_view();
let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap();
@ -69,14 +72,9 @@ impl ViewNode for UpscalingNode {
let pass_descriptor = RenderPassDescriptor {
label: Some("upscaling_pass"),
color_attachments: &[Some(RenderPassColorAttachment {
view: target.out_texture(),
resolve_target: None,
ops: Operations {
load: color_attachment_load_op,
store: StoreOp::Store,
},
})],
color_attachments: &[Some(
target.out_texture_color_attachment(converted_clear_color),
)],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
@ -86,6 +84,14 @@ impl ViewNode for UpscalingNode {
.command_encoder()
.begin_render_pass(&pass_descriptor);
if let Some(camera) = camera {
if let Some(viewport) = &camera.viewport {
let size = viewport.physical_size;
let position = viewport.physical_position;
render_pass.set_scissor_rect(position.x, position.y, size.x, size.y);
}
}
render_pass.set_pipeline(pipeline);
render_pass.set_bind_group(0, bind_group, &[]);
render_pass.draw(0..3, 0..1);

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_derive"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides derive implementations for Bevy Engine"
homepage = "https://bevyengine.org"
@ -12,7 +12,7 @@ keywords = ["bevy"]
proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0-dev" }
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.2" }
quote = "1.0"
syn = { version = "2.0", features = ["full"] }

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_dev_tools"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Collection of developer tools for the Bevy Engine"
homepage = "https://bevyengine.org"
@ -15,28 +15,26 @@ bevy_ui_debug = []
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.14.0-dev", features = [
"bevy_text",
] }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.14.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.14.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_color = { path = "../bevy_color", version = "0.14.3" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.2" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_gizmos = { path = "../bevy_gizmos", version = "0.14.2" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.2" }
bevy_input = { path = "../bevy_input", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2" }
bevy_render = { path = "../bevy_render", version = "0.14.2" }
bevy_time = { path = "../bevy_time", version = "0.14.2" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_ui = { path = "../bevy_ui", version = "0.14.2", features = ["bevy_text"] }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_window = { path = "../bevy_window", version = "0.14.2" }
bevy_text = { path = "../bevy_text", version = "0.14.2" }
bevy_state = { path = "../bevy_state", version = "0.14.2" }
# other
serde = { version = "1.0", features = ["derive"], optional = true }

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_diagnostic"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides diagnostic functionality for Bevy Engine"
homepage = "https://bevyengine.org"
@ -15,11 +15,12 @@ sysinfo_plugin = ["sysinfo"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_time = { path = "../bevy_time", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
const-fnv1a-hash = "1.1.0"

View File

@ -4,6 +4,9 @@ use bevy_ecs::system::Resource;
/// Adds a System Information Diagnostic, specifically `cpu_usage` (in %) and `mem_usage` (in %)
///
/// Note that gathering system information is a time intensive task and therefore can't be done on every frame.
/// Any system diagnostics gathered by this plugin may not be current when you access them.
///
/// Supported targets:
/// * linux,
/// * windows,
@ -19,8 +22,7 @@ use bevy_ecs::system::Resource;
pub struct SystemInformationDiagnosticsPlugin;
impl Plugin for SystemInformationDiagnosticsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, internal::setup_system)
.add_systems(Update, internal::diagnostic_system);
internal::setup_plugin(app);
}
}
@ -58,6 +60,14 @@ pub struct SystemInfo {
))]
pub mod internal {
use bevy_ecs::{prelude::ResMut, system::Local};
use std::{
sync::{Arc, Mutex},
time::Instant,
};
use bevy_app::{App, First, Startup, Update};
use bevy_ecs::system::Resource;
use bevy_tasks::{available_parallelism, block_on, poll_once, AsyncComputeTaskPool, Task};
use bevy_utils::tracing::info;
use sysinfo::{CpuRefreshKind, MemoryRefreshKind, RefreshKind, System};
@ -67,41 +77,91 @@ pub mod internal {
const BYTES_TO_GIB: f64 = 1.0 / 1024.0 / 1024.0 / 1024.0;
pub(crate) fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
pub(super) fn setup_plugin(app: &mut App) {
app.add_systems(Startup, setup_system)
.add_systems(First, launch_diagnostic_tasks)
.add_systems(Update, read_diagnostic_tasks)
.init_resource::<SysinfoTasks>();
}
fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
diagnostics
.add(Diagnostic::new(SystemInformationDiagnosticsPlugin::CPU_USAGE).with_suffix("%"));
diagnostics
.add(Diagnostic::new(SystemInformationDiagnosticsPlugin::MEM_USAGE).with_suffix("%"));
}
pub(crate) fn diagnostic_system(
mut diagnostics: Diagnostics,
mut sysinfo: Local<Option<System>>,
struct SysinfoRefreshData {
current_cpu_usage: f64,
current_used_mem: f64,
}
#[derive(Resource, Default)]
struct SysinfoTasks {
tasks: Vec<Task<SysinfoRefreshData>>,
}
fn launch_diagnostic_tasks(
mut tasks: ResMut<SysinfoTasks>,
// TODO: Consider a fair mutex
mut sysinfo: Local<Option<Arc<Mutex<System>>>>,
// TODO: FromWorld for Instant?
mut last_refresh: Local<Option<Instant>>,
) {
if sysinfo.is_none() {
*sysinfo = Some(System::new_with_specifics(
let sysinfo = sysinfo.get_or_insert_with(|| {
Arc::new(Mutex::new(System::new_with_specifics(
RefreshKind::new()
.with_cpu(CpuRefreshKind::new().with_cpu_usage())
.with_memory(MemoryRefreshKind::everything()),
));
}
let Some(sys) = sysinfo.as_mut() else {
return;
};
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
sys.refresh_memory();
let current_cpu_usage = sys.global_cpu_info().cpu_usage();
// `memory()` fns return a value in bytes
let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB;
let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB;
let current_used_mem = used_mem / total_mem * 100.0;
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || {
current_cpu_usage as f64
)))
});
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || {
current_used_mem
let last_refresh = last_refresh.get_or_insert_with(Instant::now);
let thread_pool = AsyncComputeTaskPool::get();
// Only queue a new system refresh task when necessary
// Queueing earlier than that will not give new data
if last_refresh.elapsed() > sysinfo::MINIMUM_CPU_UPDATE_INTERVAL
// These tasks don't yield and will take up all of the task pool's
// threads if we don't limit their amount.
&& tasks.tasks.len() * 2 < available_parallelism()
{
let sys = Arc::clone(sysinfo);
let task = thread_pool.spawn(async move {
let mut sys = sys.lock().unwrap();
sys.refresh_cpu_specifics(CpuRefreshKind::new().with_cpu_usage());
sys.refresh_memory();
let current_cpu_usage = sys.global_cpu_info().cpu_usage().into();
// `memory()` fns return a value in bytes
let total_mem = sys.total_memory() as f64 / BYTES_TO_GIB;
let used_mem = sys.used_memory() as f64 / BYTES_TO_GIB;
let current_used_mem = used_mem / total_mem * 100.0;
SysinfoRefreshData {
current_cpu_usage,
current_used_mem,
}
});
tasks.tasks.push(task);
*last_refresh = Instant::now();
}
}
fn read_diagnostic_tasks(mut diagnostics: Diagnostics, mut tasks: ResMut<SysinfoTasks>) {
tasks.tasks.retain_mut(|task| {
let Some(data) = block_on(poll_once(task)) else {
return true;
};
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::CPU_USAGE, || {
data.current_cpu_usage
});
diagnostics.add_measurement(&SystemInformationDiagnosticsPlugin::MEM_USAGE, || {
data.current_used_mem
});
false
});
}
@ -145,12 +205,14 @@ pub mod internal {
not(feature = "dynamic_linking")
)))]
pub mod internal {
pub(crate) fn setup_system() {
bevy_utils::tracing::warn!("This platform and/or configuration is not supported!");
use bevy_app::{App, Startup};
pub(super) fn setup_plugin(app: &mut App) {
app.add_systems(Startup, setup_system);
}
pub(crate) fn diagnostic_system() {
// no-op
fn setup_system() {
bevy_utils::tracing::warn!("This platform and/or configuration is not supported!");
}
impl Default for super::SystemInfo {

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_dylib"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Force the Bevy Engine to be dynamically linked for faster linking"
homepage = "https://bevyengine.org"
@ -12,7 +12,7 @@ keywords = ["bevy"]
crate-type = ["dylib"]
[dependencies]
bevy_internal = { path = "../bevy_internal", version = "0.14.0-dev", default-features = false }
bevy_internal = { path = "../bevy_internal", version = "0.14.2", default-features = false }
[lints]
workspace = true

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_dynamic_plugin"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides dynamic plugin loading capabilities for non-wasm platforms"
homepage = "https://bevyengine.org"
@ -10,7 +10,7 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
# other
libloading = { version = "0.8" }

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_ecs"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Bevy Engine's entity component system"
homepage = "https://bevyengine.org"
@ -15,13 +15,14 @@ trace = []
multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"]
bevy_debug_stepping = []
default = ["bevy_reflect"]
serialize = ["dep:serde"]
[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_ecs_macros = { path = "macros", version = "0.14.0-dev" }
bevy_ptr = { path = "../bevy_ptr", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", optional = true }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_ecs_macros = { path = "macros", version = "0.14.2" }
petgraph = "0.6"
bitflags = "2.3"

View File

@ -307,4 +307,52 @@ fn reader(mut reader: EventReader<MyEvent>) {
A minimal set up using events can be seen in [`events.rs`](examples/events.rs).
### Observers
Observers are systems that listen for a "trigger" of a specific `Event`:
```rust
use bevy_ecs::prelude::*;
#[derive(Event)]
struct MyEvent {
message: String
}
let mut world = World::new();
world.observe(|trigger: Trigger<MyEvent>| {
println!("{}", trigger.event().message);
});
world.flush();
world.trigger(MyEvent {
message: "hello!".to_string(),
});
```
These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time!
Events can also be triggered to target specific entities:
```rust
use bevy_ecs::prelude::*;
#[derive(Event)]
struct Explode;
let mut world = World::new();
let entity = world.spawn_empty().id();
world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
println!("Entity {:?} goes BOOM!", trigger.entity());
commands.entity(trigger.entity()).despawn();
});
world.flush();
world.trigger_targets(Explode, entity);
```
[bevy]: https://bevyengine.org/

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_ecs_macros"
version = "0.14.0-dev"
version = "0.14.2"
description = "Bevy ECS Macros"
edition = "2021"
license = "MIT OR Apache-2.0"
@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0"
proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.2" }
syn = { version = "2.0", features = ["full"] }
quote = "1.0"

View File

@ -18,6 +18,10 @@ pub fn derive_event(input: TokenStream) -> TokenStream {
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {
}
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
}
})
}

View File

@ -74,6 +74,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
.collect::<Vec<_>>();
let mut field_component_ids = Vec::new();
let mut field_get_component_ids = Vec::new();
let mut field_get_components = Vec::new();
let mut field_from_components = Vec::new();
for (((i, field_type), field_kind), field) in field_type
@ -87,6 +88,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
field_component_ids.push(quote! {
<#field_type as #ecs_path::bundle::Bundle>::component_ids(components, storages, &mut *ids);
});
field_get_component_ids.push(quote! {
<#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);
});
match field {
Some(field) => {
field_get_components.push(quote! {
@ -133,6 +137,13 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
#(#field_component_ids)*
}
fn get_component_ids(
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
){
#(#field_get_component_ids)*
}
#[allow(unused_variables, non_snake_case)]
unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
where
@ -435,6 +446,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world);
}
fn queue(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: #path::world::DeferredWorld) {
<#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::queue(&mut state.state, system_meta, world);
}
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &#path::system::SystemMeta,

View File

@ -164,9 +164,9 @@ pub(crate) fn world_query_impl(
#( <#field_types>::update_component_access(&state.#named_field_idents, _access); )*
}
fn init_state(initializer: &mut #path::component::ComponentInitializer) -> #state_struct_name #user_ty_generics {
fn init_state(world: &mut #path::world::World) -> #state_struct_name #user_ty_generics {
#state_struct_name {
#(#named_field_idents: <#field_types>::init_state(initializer),)*
#(#named_field_idents: <#field_types>::init_state(world),)*
}
}

View File

@ -23,6 +23,7 @@ use crate::{
bundle::BundleId,
component::{ComponentId, Components, StorageType},
entity::{Entity, EntityLocation},
observer::Observers,
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
};
use std::{
@ -119,6 +120,7 @@ pub(crate) struct AddBundle {
/// For each component iterated in the same order as the source [`Bundle`](crate::bundle::Bundle),
/// indicate if the component is newly added to the target archetype or if it already existed
pub bundle_status: Vec<ComponentStatus>,
pub added: Vec<ComponentId>,
}
/// This trait is used to report the status of [`Bundle`](crate::bundle::Bundle) components
@ -202,12 +204,14 @@ impl Edges {
bundle_id: BundleId,
archetype_id: ArchetypeId,
bundle_status: Vec<ComponentStatus>,
added: Vec<ComponentId>,
) {
self.add_bundle.insert(
bundle_id,
AddBundle {
archetype_id,
bundle_status,
added,
},
);
}
@ -314,6 +318,9 @@ bitflags::bitflags! {
const ON_ADD_HOOK = (1 << 0);
const ON_INSERT_HOOK = (1 << 1);
const ON_REMOVE_HOOK = (1 << 2);
const ON_ADD_OBSERVER = (1 << 3);
const ON_INSERT_OBSERVER = (1 << 4);
const ON_REMOVE_OBSERVER = (1 << 5);
}
}
@ -335,6 +342,7 @@ pub struct Archetype {
impl Archetype {
pub(crate) fn new(
components: &Components,
observers: &Observers,
id: ArchetypeId,
table_id: TableId,
table_components: impl Iterator<Item = (ComponentId, ArchetypeComponentId)>,
@ -348,6 +356,7 @@ impl Archetype {
// SAFETY: We are creating an archetype that includes this component so it must exist
let info = unsafe { components.get_info_unchecked(component_id) };
info.update_archetype_flags(&mut flags);
observers.update_archetype_flags(component_id, &mut flags);
archetype_components.insert(
component_id,
ArchetypeComponentInfo {
@ -361,6 +370,7 @@ impl Archetype {
// SAFETY: We are creating an archetype that includes this component so it must exist
let info = unsafe { components.get_info_unchecked(component_id) };
info.update_archetype_flags(&mut flags);
observers.update_archetype_flags(component_id, &mut flags);
archetype_components.insert(
component_id,
ArchetypeComponentInfo {
@ -580,21 +590,45 @@ impl Archetype {
/// Returns true if any of the components in this archetype have `on_add` hooks
#[inline]
pub(crate) fn has_on_add(&self) -> bool {
pub fn has_add_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_HOOK)
}
/// Returns true if any of the components in this archetype have `on_insert` hooks
#[inline]
pub(crate) fn has_on_insert(&self) -> bool {
pub fn has_insert_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_HOOK)
}
/// Returns true if any of the components in this archetype have `on_remove` hooks
#[inline]
pub(crate) fn has_on_remove(&self) -> bool {
pub fn has_remove_hook(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_HOOK)
}
/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
///
/// [`OnAdd`]: crate::world::OnAdd
#[inline]
pub fn has_add_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
}
/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
///
/// [`OnInsert`]: crate::world::OnInsert
#[inline]
pub fn has_insert_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
}
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
///
/// [`OnRemove`]: crate::world::OnRemove
#[inline]
pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
}
}
/// The next [`ArchetypeId`] in an [`Archetypes`] collection.
@ -681,6 +715,7 @@ impl Archetypes {
unsafe {
archetypes.get_id_or_insert(
&Components::default(),
&Observers::default(),
TableId::empty(),
Vec::new(),
Vec::new(),
@ -782,6 +817,7 @@ impl Archetypes {
pub(crate) unsafe fn get_id_or_insert(
&mut self,
components: &Components,
observers: &Observers,
table_id: TableId,
table_components: Vec<ComponentId>,
sparse_set_components: Vec<ComponentId>,
@ -808,6 +844,7 @@ impl Archetypes {
(sparse_start..*archetype_component_count).map(ArchetypeComponentId);
archetypes.push(Archetype::new(
components,
observers,
id,
table_id,
table_components.into_iter().zip(table_archetype_components),
@ -832,6 +869,20 @@ impl Archetypes {
archetype.clear_entities();
}
}
pub(crate) fn update_flags(
&mut self,
component_id: ComponentId,
flags: ArchetypeFlags,
set: bool,
) {
// TODO: Refactor component index to speed this up.
for archetype in &mut self.archetypes {
if archetype.contains(component_id) {
archetype.flags.set(flags, set);
}
}
}
}
impl Index<RangeFrom<ArchetypeGeneration>> for Archetypes {

View File

@ -2,8 +2,9 @@
//!
//! This module contains the [`Bundle`] trait and some other helper types.
use std::any::TypeId;
pub use bevy_ecs_macros::Bundle;
use bevy_utils::{HashMap, HashSet, TypeIdMap};
use crate::{
archetype::{
@ -12,14 +13,15 @@ use crate::{
},
component::{Component, ComponentId, Components, StorageType, Tick},
entity::{Entities, Entity, EntityLocation},
observer::Observers,
prelude::World,
query::DebugCheckedUnwrap,
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
world::unsafe_world_cell::UnsafeWorldCell,
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT},
};
use bevy_ptr::{ConstNonNull, OwningPtr};
use bevy_utils::all_tuples;
use std::any::TypeId;
use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap};
use std::ptr::NonNull;
/// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity.
@ -155,6 +157,9 @@ pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static {
ids: &mut impl FnMut(ComponentId),
);
/// Gets this [`Bundle`]'s component ids. This will be [`None`] if the component has not been registered.
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>));
/// Calls `func`, which should return data for each component in the bundle, in the order of
/// this bundle's [`Component`]s
///
@ -204,6 +209,10 @@ unsafe impl<C: Component> Bundle for C {
// Safety: The id given in `component_ids` is for `Self`
unsafe { ptr.read() }
}
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)) {
ids(components.get_id(TypeId::of::<C>()));
}
}
impl<C: Component> DynamicBundle for C {
@ -227,6 +236,11 @@ macro_rules! tuple_impl {
$(<$name as Bundle>::component_ids(components, storages, ids);)*
}
#[allow(unused_variables)]
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)){
$(<$name as Bundle>::get_component_ids(components, ids);)*
}
#[allow(unused_variables, unused_mut)]
#[allow(clippy::unused_unit)]
unsafe fn from_components<T, F>(ctx: &mut T, func: &mut F) -> Self
@ -432,6 +446,7 @@ impl BundleInfo {
archetypes: &mut Archetypes,
storages: &mut Storages,
components: &Components,
observers: &Observers,
archetype_id: ArchetypeId,
) -> ArchetypeId {
if let Some(add_bundle_id) = archetypes[archetype_id].edges().get_add_bundle(self.id) {
@ -440,6 +455,7 @@ impl BundleInfo {
let mut new_table_components = Vec::new();
let mut new_sparse_set_components = Vec::new();
let mut bundle_status = Vec::with_capacity(self.component_ids.len());
let mut added = Vec::new();
let current_archetype = &mut archetypes[archetype_id];
for component_id in self.component_ids.iter().cloned() {
@ -447,6 +463,7 @@ impl BundleInfo {
bundle_status.push(ComponentStatus::Mutated);
} else {
bundle_status.push(ComponentStatus::Added);
added.push(component_id);
// SAFETY: component_id exists
let component_info = unsafe { components.get_info_unchecked(component_id) };
match component_info.storage_type() {
@ -459,7 +476,7 @@ impl BundleInfo {
if new_table_components.is_empty() && new_sparse_set_components.is_empty() {
let edges = current_archetype.edges_mut();
// the archetype does not change when we add this bundle
edges.insert_add_bundle(self.id, archetype_id, bundle_status);
edges.insert_add_bundle(self.id, archetype_id, bundle_status, added);
archetype_id
} else {
let table_id;
@ -498,6 +515,7 @@ impl BundleInfo {
// SAFETY: ids in self must be valid
let new_archetype_id = archetypes.get_id_or_insert(
components,
observers,
table_id,
table_components,
sparse_set_components,
@ -507,6 +525,7 @@ impl BundleInfo {
self.id,
new_archetype_id,
bundle_status,
added,
);
new_archetype_id
}
@ -567,6 +586,7 @@ impl<'w> BundleInserter<'w> {
&mut world.archetypes,
&mut world.storages,
&world.components,
&world.observers,
archetype_id,
);
if new_archetype_id == archetype_id {
@ -786,27 +806,21 @@ impl<'w> BundleInserter<'w> {
}
};
let new_archetype = &*new_archetype;
// SAFETY: We have no outstanding mutable references to world as they were dropped
let mut deferred_world = unsafe { self.world.into_deferred() };
if new_archetype.has_on_add() {
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe {
deferred_world.trigger_on_add(
entity,
bundle_info
.iter_components()
.zip(add_bundle.bundle_status.iter())
.filter(|(_, &status)| status == ComponentStatus::Added)
.map(|(id, _)| id),
);
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe {
deferred_world.trigger_on_add(new_archetype, entity, add_bundle.added.iter().cloned());
if new_archetype.has_add_observer() {
deferred_world.trigger_observers(ON_ADD, entity, add_bundle.added.iter().cloned());
}
deferred_world.trigger_on_insert(new_archetype, entity, bundle_info.iter_components());
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components());
}
}
if new_archetype.has_on_insert() {
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) }
}
new_location
@ -853,6 +867,7 @@ impl<'w> BundleSpawner<'w> {
&mut world.archetypes,
&mut world.storages,
&world.components,
&world.observers,
ArchetypeId::EMPTY,
);
let archetype = &mut world.archetypes[new_archetype_id];
@ -882,12 +897,12 @@ impl<'w> BundleSpawner<'w> {
entity: Entity,
bundle: T,
) -> EntityLocation {
let table = self.table.as_mut();
let archetype = self.archetype.as_mut();
// SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid
let bundle_info = self.bundle_info.as_ref();
// SAFETY: We do not make any structural changes to the archetype graph through self.world so this pointer always remain valid
let location = {
let table = self.table.as_mut();
let archetype = self.archetype.as_mut();
// SAFETY: Mutable references do not alias and will be dropped after this block
let (sparse_sets, entities) = {
let world = self.world.world_mut();
@ -910,16 +925,20 @@ impl<'w> BundleSpawner<'w> {
// SAFETY: We have no outstanding mutable references to world as they were dropped
let mut deferred_world = unsafe { self.world.into_deferred() };
if archetype.has_on_add() {
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe { deferred_world.trigger_on_add(entity, bundle_info.iter_components()) };
}
if archetype.has_on_insert() {
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe { deferred_world.trigger_on_insert(entity, bundle_info.iter_components()) };
}
// SAFETY: `DeferredWorld` cannot provide mutable access to `Archetypes`.
let archetype = self.archetype.as_ref();
// SAFETY: All components in the bundle are guaranteed to exist in the World
// as they must be initialized before creating the BundleInfo.
unsafe {
deferred_world.trigger_on_add(archetype, entity, bundle_info.iter_components());
if archetype.has_add_observer() {
deferred_world.trigger_observers(ON_ADD, entity, bundle_info.iter_components());
}
deferred_world.trigger_on_insert(archetype, entity, bundle_info.iter_components());
if archetype.has_insert_observer() {
deferred_world.trigger_observers(ON_INSERT, entity, bundle_info.iter_components());
}
};
location
}
@ -947,7 +966,7 @@ impl<'w> BundleSpawner<'w> {
#[inline]
pub(crate) unsafe fn flush_commands(&mut self) {
// SAFETY: pointers on self can be invalidated,
self.world.world_mut().flush_commands();
self.world.world_mut().flush();
}
}
@ -1223,13 +1242,13 @@ mod tests {
world
.register_component_hooks::<C>()
.on_add(|mut world, _, _| {
world.resource_mut::<R>().assert_order(2);
world.resource_mut::<R>().assert_order(3);
});
world
.register_component_hooks::<D>()
.on_add(|mut world, _, _| {
world.resource_mut::<R>().assert_order(3);
world.resource_mut::<R>().assert_order(2);
});
world.spawn(A).flush();

View File

@ -21,7 +21,6 @@ use std::{
borrow::Cow,
marker::PhantomData,
mem::needs_drop,
ops::Deref,
};
/// A data type that can be used to store data for an [entity].
@ -838,75 +837,6 @@ impl Components {
}
}
/// A wrapper over a mutable [`Components`] reference that allows for state initialization.
/// This can be obtained with [`World::component_initializer`].
pub struct ComponentInitializer<'w> {
pub(crate) components: &'w mut Components,
pub(crate) storages: &'w mut Storages,
}
impl<'w> Deref for ComponentInitializer<'w> {
type Target = Components;
fn deref(&self) -> &Components {
self.components
}
}
impl<'w> ComponentInitializer<'w> {
/// Initializes a component of type `T` with this instance.
/// If a component of this type has already been initialized, this will return
/// the ID of the pre-existing component.
///
/// # See also
///
/// * [`Components::component_id()`]
/// * [`Components::init_component_with_descriptor()`]
#[inline]
pub fn init_component<T: Component>(&mut self) -> ComponentId {
self.components.init_component::<T>(self.storages)
}
/// Initializes a component described by `descriptor`.
///
/// ## Note
///
/// If this method is called multiple times with identical descriptors, a distinct `ComponentId`
/// will be created for each one.
///
/// # See also
///
/// * [`Components::component_id()`]
/// * [`Components::init_component()`]
pub fn init_component_with_descriptor(
&mut self,
descriptor: ComponentDescriptor,
) -> ComponentId {
self.components
.init_component_with_descriptor(self.storages, descriptor)
}
/// Initializes a [`Resource`] of type `T` with this instance.
/// If a resource of this type has already been initialized, this will return
/// the ID of the pre-existing resource.
///
/// # See also
///
/// * [`Components::resource_id()`]
#[inline]
pub fn init_resource<T: Resource>(&mut self) -> ComponentId {
self.components.init_resource::<T>()
}
/// Initializes a [non-send resource](crate::system::NonSend) of type `T` with this instance.
/// If a resource of this type has already been initialized, this will return
/// the ID of the pre-existing resource.
#[inline]
pub fn init_non_send<T: Any>(&mut self) -> ComponentId {
self.components.init_non_send::<T>()
}
}
/// A value that tracks when a system ran relative to other systems.
/// This is used to power change detection.
///

View File

@ -38,7 +38,7 @@
mod map_entities;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
#[cfg(all(feature = "bevy_reflect", feature = "serde"))]
#[cfg(all(feature = "bevy_reflect", feature = "serialize"))]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
pub use map_entities::*;
@ -57,7 +57,7 @@ use crate::{
},
storage::{SparseSetIndex, TableId, TableRow},
};
#[cfg(feature = "serde")]
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};
use std::{fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering};
@ -146,7 +146,7 @@ type IdCursor = isize;
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy_reflect", reflect_value(Hash, PartialEq))]
#[cfg_attr(
all(feature = "bevy_reflect", feature = "serde"),
all(feature = "bevy_reflect", feature = "serialize"),
reflect_value(Serialize, Deserialize)
)]
// Alignment repr necessary to allow LLVM to better output
@ -368,7 +368,7 @@ impl From<Entity> for Identifier {
}
}
#[cfg(feature = "serde")]
#[cfg(feature = "serialize")]
impl Serialize for Entity {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
@ -378,7 +378,7 @@ impl Serialize for Entity {
}
}
#[cfg(feature = "serde")]
#[cfg(feature = "serialize")]
impl<'de> Deserialize<'de> for Entity {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
@ -392,13 +392,7 @@ impl<'de> Deserialize<'de> for Entity {
impl fmt::Display for Entity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}v{}|{}",
self.index(),
self.generation(),
self.to_bits()
)
write!(f, "{}v{}", self.index(), self.generation())
}
}
@ -1162,9 +1156,7 @@ mod tests {
fn entity_display() {
let entity = Entity::from_raw(42);
let string = format!("{}", entity);
let bits = entity.to_bits().to_string();
assert!(string.contains("42"));
assert!(string.contains("v1"));
assert!(string.contains(&bits));
}
}

View File

@ -1,11 +1,12 @@
//! Event handling types.
use crate as bevy_ecs;
#[cfg(feature = "multi_threaded")]
use crate::batching::BatchingStrategy;
use crate::change_detection::MutUntyped;
use crate::{
change_detection::{DetectChangesMut, Mut},
component::{ComponentId, Tick},
component::{Component, ComponentId, Tick},
system::{Local, Res, ResMut, Resource, SystemParam},
world::World,
};
@ -24,16 +25,30 @@ use std::{
slice::Iter,
};
/// A type that can be stored in an [`Events<E>`] resource
/// Something that "happens" and might be read / observed by app logic.
///
/// Events can be stored in an [`Events<E>`] resource
/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter.
///
/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run.
///
/// This trait can be derived.
///
/// Events implement the [`Component`] type (and they automatically do when they are derived). Events are (generally)
/// not directly inserted as components. More often, the [`ComponentId`] is used to identify the event type within the
/// context of the ECS.
///
/// Events must be thread-safe.
///
/// [`World`]: crate::world::World
/// [`ComponentId`]: crate::component::ComponentId
/// [`Observer`]: crate::observer::Observer
#[diagnostic::on_unimplemented(
message = "`{Self}` is not an `Event`",
label = "invalid `Event`",
note = "consider annotating `{Self}` with `#[derive(Event)]`"
)]
pub trait Event: Send + Sync + 'static {}
pub trait Event: Component {}
/// An `EventId` uniquely identifies an event stored in a specific [`World`].
///
@ -495,6 +510,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
/// assert_eq!(counter.into_inner(), 4950);
/// ```
///
#[cfg(feature = "multi_threaded")]
pub fn par_read(&mut self) -> EventParIter<'_, E> {
self.reader.par_read(&self.events)
}
@ -556,6 +572,11 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
///
/// # bevy_ecs::system::assert_is_system(my_system);
/// ```
/// # Observers
///
/// "Buffered" Events, such as those sent directly in [`Events`] or sent using [`EventWriter`], do _not_ automatically
/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will
/// be triggered, and if so, _when_ it will be triggered in the schedule.
///
/// # Concurrency
///
@ -588,6 +609,8 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> {
/// }
/// ```
/// Note that this is considered *non-idiomatic*, and should only be used when `EventWriter` will not work.
///
/// [`Observer`]: crate::observer::Observer
#[derive(SystemParam)]
pub struct EventWriter<'w, E: Event> {
events: ResMut<'w, Events<E>>,
@ -701,6 +724,7 @@ impl<E: Event> ManualEventReader<E> {
}
/// See [`EventReader::par_read`]
#[cfg(feature = "multi_threaded")]
pub fn par_read<'a>(&'a mut self, events: &'a Events<E>) -> EventParIter<'a, E> {
EventParIter::new(self, events)
}
@ -869,13 +893,16 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> {
}
/// A parallel iterator over `Event`s.
#[cfg(feature = "multi_threaded")]
#[derive(Debug)]
pub struct EventParIter<'a, E: Event> {
reader: &'a mut ManualEventReader<E>,
slices: [&'a [EventInstance<E>]; 2],
batching_strategy: BatchingStrategy,
unread: usize,
}
#[cfg(feature = "multi_threaded")]
impl<'a, E: Event> EventParIter<'a, E> {
/// Creates a new parallel iterator over `events` that have not yet been seen by `reader`.
pub fn new(reader: &'a mut ManualEventReader<E>, events: &'a Events<E>) -> Self {
@ -897,6 +924,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
reader,
slices: [a, b],
batching_strategy: BatchingStrategy::default(),
unread: unread_count,
}
}
@ -932,7 +960,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
/// initialized and run from the ECS scheduler, this should never panic.
///
/// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool
pub fn for_each_with_id<FN: Fn(&'a E, EventId<E>) + Send + Sync + Clone>(self, func: FN) {
pub fn for_each_with_id<FN: Fn(&'a E, EventId<E>) + Send + Sync + Clone>(mut self, func: FN) {
#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))]
{
self.into_iter().for_each(|(e, i)| func(e, i));
@ -962,6 +990,10 @@ impl<'a, E: Event> EventParIter<'a, E> {
});
}
});
// Events are guaranteed to be read at this point.
self.reader.last_event_count += self.unread;
self.unread = 0;
}
}
@ -976,6 +1008,7 @@ impl<'a, E: Event> EventParIter<'a, E> {
}
}
#[cfg(feature = "multi_threaded")]
impl<'a, E: Event> IntoIterator for EventParIter<'a, E> {
type IntoIter = EventIteratorWithId<'a, E>;
type Item = <Self::IntoIter as Iterator>::Item;
@ -1010,12 +1043,30 @@ struct RegisteredEvent {
/// to update all of the events.
#[derive(Resource, Default)]
pub struct EventRegistry {
needs_update: bool,
/// Should the events be updated?
///
/// This field is generally automatically updated by the [`signal_event_update_system`](crate::event::update::signal_event_update_system).
pub should_update: ShouldUpdateEvents,
event_updates: Vec<RegisteredEvent>,
}
/// Controls whether or not the events in an [`EventRegistry`] should be updated.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShouldUpdateEvents {
/// Without any fixed timestep, events should always be updated each frame.
#[default]
Always,
/// We need to wait until at least one pass of the fixed update schedules to update the events.
Waiting,
/// At least one pass of the fixed update schedules has occurred, and the events are ready to be updated.
Ready,
}
impl EventRegistry {
/// Registers an event type to be updated.
/// Registers an event type to be updated in a given [`World`]
///
/// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use
/// the existing instance.
pub fn register_event<T: Event>(world: &mut World) {
// By initializing the resource here, we can be sure that it is present,
// and receive the correct, up-to-date `ComponentId` even if it was previously removed.
@ -1033,6 +1084,16 @@ impl EventRegistry {
});
}
/// Removes an event from the world and it's associated [`EventRegistry`].
pub fn deregister_events<T: Event>(world: &mut World) {
let component_id = world.init_resource::<Events<T>>();
let mut registry = world.get_resource_or_insert_with(Self::default);
registry
.event_updates
.retain(|e| e.component_id != component_id);
world.remove_resource::<Events<T>>();
}
/// Updates all of the registered events in the World.
pub fn run_updates(&mut self, world: &mut World, last_change_tick: Tick) {
for registered_event in &mut self.event_updates {
@ -1058,9 +1119,12 @@ impl EventRegistry {
pub struct EventUpdates;
/// Signals the [`event_update_system`] to run after `FixedUpdate` systems.
///
/// This will change the behavior of the [`EventRegistry`] to only run after a fixed update cycle has passed.
/// Normally, this will simply run every frame.
pub fn signal_event_update_system(signal: Option<ResMut<EventRegistry>>) {
if let Some(mut registry) = signal {
registry.needs_update = true;
registry.should_update = ShouldUpdateEvents::Ready;
}
}
@ -1069,18 +1133,34 @@ pub fn event_update_system(world: &mut World, mut last_change_tick: Local<Tick>)
if world.contains_resource::<EventRegistry>() {
world.resource_scope(|world, mut registry: Mut<EventRegistry>| {
registry.run_updates(world, *last_change_tick);
// Disable the system until signal_event_update_system runs again.
registry.needs_update = false;
registry.should_update = match registry.should_update {
// If we're always updating, keep doing so.
ShouldUpdateEvents::Always => ShouldUpdateEvents::Always,
// Disable the system until signal_event_update_system runs again.
ShouldUpdateEvents::Waiting | ShouldUpdateEvents::Ready => {
ShouldUpdateEvents::Waiting
}
};
});
}
*last_change_tick = world.change_tick();
}
/// A run condition for [`event_update_system`].
pub fn event_update_condition(signal: Option<Res<EventRegistry>>) -> bool {
// If we haven't got a signal to update the events, but we *could* get such a signal
// return early and update the events later.
signal.map_or(false, |signal| signal.needs_update)
///
/// If [`signal_event_update_system`] has been run at least once,
/// we will wait for it to be run again before updating the events.
///
/// Otherwise, we will always update the events.
pub fn event_update_condition(maybe_signal: Option<Res<EventRegistry>>) -> bool {
match maybe_signal {
Some(signal) => match signal.should_update {
ShouldUpdateEvents::Always | ShouldUpdateEvents::Ready => true,
ShouldUpdateEvents::Waiting => false,
},
None => true,
}
}
/// [`Iterator`] over sent [`EventIds`](`EventId`) from a batch.
@ -1517,28 +1597,83 @@ mod tests {
#[cfg(feature = "multi_threaded")]
#[test]
fn test_events_par_iter() {
use std::{collections::HashSet, sync::mpsc};
use crate::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Resource)]
struct Counter(AtomicUsize);
let mut world = World::new();
world.init_resource::<Events<TestEvent>>();
for i in 0..100 {
world.send_event(TestEvent { i });
for _ in 0..100 {
world.send_event(TestEvent { i: 1 });
}
let mut schedule = Schedule::default();
schedule.add_systems(|mut events: EventReader<TestEvent>| {
let (tx, rx) = mpsc::channel();
events.par_read().for_each(|event| {
tx.send(event.i).unwrap();
});
drop(tx);
let observed: HashSet<_> = rx.into_iter().collect();
assert_eq!(observed, HashSet::from_iter(0..100));
});
schedule.add_systems(
|mut events: EventReader<TestEvent>, counter: ResMut<Counter>| {
events.par_read().for_each(|event| {
counter.0.fetch_add(event.i, Ordering::Relaxed);
});
},
);
world.insert_resource(Counter(AtomicUsize::new(0)));
schedule.run(&mut world);
let counter = world.remove_resource::<Counter>().unwrap();
assert_eq!(counter.0.into_inner(), 100);
world.insert_resource(Counter(AtomicUsize::new(0)));
schedule.run(&mut world);
let counter = world.remove_resource::<Counter>().unwrap();
assert_eq!(counter.0.into_inner(), 0);
}
#[test]
fn iter_current_update_events_iterates_over_current_events() {
#[derive(Event, Clone)]
struct TestEvent;
let mut test_events = Events::<TestEvent>::default();
// Starting empty
assert_eq!(test_events.len(), 0);
assert_eq!(test_events.iter_current_update_events().count(), 0);
test_events.update();
// Sending one event
test_events.send(TestEvent);
assert_eq!(test_events.len(), 1);
assert_eq!(test_events.iter_current_update_events().count(), 1);
test_events.update();
// Sending two events on the next frame
test_events.send(TestEvent);
test_events.send(TestEvent);
assert_eq!(test_events.len(), 3); // Events are double-buffered, so we see 1 + 2 = 3
assert_eq!(test_events.iter_current_update_events().count(), 2);
test_events.update();
// Sending zero events
assert_eq!(test_events.len(), 2); // Events are double-buffered, so we see 2 + 0 = 2
assert_eq!(test_events.iter_current_update_events().count(), 0);
}
#[test]
fn test_event_registry_can_add_and_remove_events_to_world() {
use bevy_ecs::prelude::*;
let mut world = World::new();
EventRegistry::register_event::<TestEvent>(&mut world);
let has_events = world.get_resource::<Events<TestEvent>>().is_some();
assert!(has_events, "Should have the events resource");
EventRegistry::deregister_events::<TestEvent>(&mut world);
let has_events = world.get_resource::<Events<TestEvent>>().is_some();
assert!(!has_events, "Should not have the events resource");
}
}

View File

@ -21,6 +21,7 @@ pub mod event;
pub mod identifier;
pub mod intern;
pub mod label;
pub mod observer;
pub mod query;
#[cfg(feature = "bevy_reflect")]
pub mod reflect;
@ -45,7 +46,8 @@ pub mod prelude {
change_detection::{DetectChanges, DetectChangesMut, Mut, Ref},
component::Component,
entity::{Entity, EntityMapper},
event::{Event, EventReader, EventWriter, Events},
event::{Event, EventReader, EventWriter, Events, ShouldUpdateEvents},
observer::{Observer, Trigger},
query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without},
removal_detection::RemovedComponents,
schedule::{
@ -57,7 +59,9 @@ pub mod prelude {
ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemBuilder,
SystemParamFunction,
},
world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World},
world::{
EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, World,
},
};
}

View File

@ -0,0 +1,42 @@
use crate::{
component::{Component, ComponentHooks, StorageType},
entity::Entity,
observer::ObserverState,
};
/// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to.
#[derive(Default)]
pub(crate) struct ObservedBy(pub(crate) Vec<Entity>);
impl Component for ObservedBy {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_remove(|mut world, entity, _| {
let observed_by = {
let mut component = world.get_mut::<ObservedBy>(entity).unwrap();
std::mem::take(&mut component.0)
};
for e in observed_by {
let (total_entities, despawned_watched_entities) = {
let Some(mut entity_mut) = world.get_entity_mut(e) else {
continue;
};
let Some(mut state) = entity_mut.get_mut::<ObserverState>() else {
continue;
};
state.despawned_watched_entities += 1;
(
state.descriptor.entities.len(),
state.despawned_watched_entities as usize,
)
};
// Despawn Observer if it has no more active sources.
if total_entities == despawned_watched_entities {
world.commands().entity(e).despawn();
}
}
});
}
}

View File

@ -0,0 +1,665 @@
//! Types for creating and storing [`Observer`]s
mod entity_observer;
mod runner;
mod trigger_event;
pub use runner::*;
pub use trigger_event::*;
use crate::observer::entity_observer::ObservedBy;
use crate::{archetype::ArchetypeFlags, system::IntoObserverSystem, world::*};
use crate::{component::ComponentId, prelude::*, world::DeferredWorld};
use bevy_ptr::Ptr;
use bevy_utils::{EntityHashMap, HashMap};
use std::marker::PhantomData;
/// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the
/// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well.
pub struct Trigger<'w, E, B: Bundle = ()> {
event: &'w mut E,
trigger: ObserverTrigger,
_marker: PhantomData<B>,
}
impl<'w, E, B: Bundle> Trigger<'w, E, B> {
/// Creates a new trigger for the given event and observer information.
pub fn new(event: &'w mut E, trigger: ObserverTrigger) -> Self {
Self {
event,
trigger,
_marker: PhantomData,
}
}
/// Returns the event type of this trigger.
pub fn event_type(&self) -> ComponentId {
self.trigger.event_type
}
/// Returns a reference to the triggered event.
pub fn event(&self) -> &E {
self.event
}
/// Returns a mutable reference to the triggered event.
pub fn event_mut(&mut self) -> &mut E {
self.event
}
/// Returns a pointer to the triggered event.
pub fn event_ptr(&self) -> Ptr {
Ptr::from(&self.event)
}
/// Returns the entity that triggered the observer, could be [`Entity::PLACEHOLDER`].
pub fn entity(&self) -> Entity {
self.trigger.entity
}
}
/// A description of what an [`Observer`] observes.
#[derive(Default, Clone)]
pub struct ObserverDescriptor {
/// The events the observer is watching.
events: Vec<ComponentId>,
/// The components the observer is watching.
components: Vec<ComponentId>,
/// The entities the observer is watching.
entities: Vec<Entity>,
}
impl ObserverDescriptor {
/// Add the given `events` to the descriptor.
/// # Safety
/// The type of each [`ComponentId`] in `events` _must_ match the actual value
/// of the event passed into the observer.
pub unsafe fn with_events(mut self, events: Vec<ComponentId>) -> Self {
self.events = events;
self
}
/// Add the given `components` to the descriptor.
pub fn with_components(mut self, components: Vec<ComponentId>) -> Self {
self.components = components;
self
}
/// Add the given `entities` to the descriptor.
pub fn with_entities(mut self, entities: Vec<Entity>) -> Self {
self.entities = entities;
self
}
pub(crate) fn merge(&mut self, descriptor: &ObserverDescriptor) {
self.events.extend(descriptor.events.iter().copied());
self.components
.extend(descriptor.components.iter().copied());
self.entities.extend(descriptor.entities.iter().copied());
}
}
/// Event trigger metadata for a given [`Observer`],
#[derive(Debug)]
pub struct ObserverTrigger {
/// The [`Entity`] of the observer handling the trigger.
pub observer: Entity,
/// The [`ComponentId`] the trigger targeted.
pub event_type: ComponentId,
/// The entity the trigger targeted.
pub entity: Entity,
}
// Map between an observer entity and its runner
type ObserverMap = EntityHashMap<Entity, ObserverRunner>;
/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component.
#[derive(Default, Debug)]
pub struct CachedComponentObservers {
// Observers listening to triggers targeting this component
map: ObserverMap,
// Observers listening to triggers targeting this component on a specific entity
entity_map: EntityHashMap<Entity, ObserverMap>,
}
/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger.
#[derive(Default, Debug)]
pub struct CachedObservers {
// Observers listening for any time this trigger is fired
map: ObserverMap,
// Observers listening for this trigger fired at a specific component
component_observers: HashMap<ComponentId, CachedComponentObservers>,
// Observers listening for this trigger fired at a specific entity
entity_observers: EntityHashMap<Entity, ObserverMap>,
}
/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers.
#[derive(Default, Debug)]
pub struct Observers {
// Cached ECS observers to save a lookup most common triggers.
on_add: CachedObservers,
on_insert: CachedObservers,
on_remove: CachedObservers,
// Map from trigger type to set of observers
cache: HashMap<ComponentId, CachedObservers>,
}
impl Observers {
pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers {
match event_type {
ON_ADD => &mut self.on_add,
ON_INSERT => &mut self.on_insert,
ON_REMOVE => &mut self.on_remove,
_ => self.cache.entry(event_type).or_default(),
}
}
pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> {
match event_type {
ON_ADD => Some(&self.on_add),
ON_INSERT => Some(&self.on_insert),
ON_REMOVE => Some(&self.on_remove),
_ => self.cache.get(&event_type),
}
}
/// This will run the observers of the given `event_type`, targeting the given `entity` and `components`.
pub(crate) fn invoke<T>(
mut world: DeferredWorld,
event_type: ComponentId,
entity: Entity,
components: impl Iterator<Item = ComponentId>,
data: &mut T,
) {
// SAFETY: You cannot get a mutable reference to `observers` from `DeferredWorld`
let (mut world, observers) = unsafe {
let world = world.as_unsafe_world_cell();
// SAFETY: There are no outstanding world references
world.increment_trigger_id();
let observers = world.observers();
let Some(observers) = observers.try_get_observers(event_type) else {
return;
};
// SAFETY: The only outstanding reference to world is `observers`
(world.into_deferred(), observers)
};
let mut trigger_observer = |(&observer, runner): (&Entity, &ObserverRunner)| {
(runner)(
world.reborrow(),
ObserverTrigger {
observer,
event_type,
entity,
},
data.into(),
);
};
// Trigger observers listening for any kind of this trigger
observers.map.iter().for_each(&mut trigger_observer);
// Trigger entity observers listening for this kind of trigger
if entity != Entity::PLACEHOLDER {
if let Some(map) = observers.entity_observers.get(&entity) {
map.iter().for_each(&mut trigger_observer);
}
}
// Trigger observers listening to this trigger targeting a specific component
components.for_each(|id| {
if let Some(component_observers) = observers.component_observers.get(&id) {
component_observers
.map
.iter()
.for_each(&mut trigger_observer);
if entity != Entity::PLACEHOLDER {
if let Some(map) = component_observers.entity_map.get(&entity) {
map.iter().for_each(&mut trigger_observer);
}
}
}
});
}
pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option<ArchetypeFlags> {
match event_type {
ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER),
ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER),
ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER),
_ => None,
}
}
pub(crate) fn update_archetype_flags(
&self,
component_id: ComponentId,
flags: &mut ArchetypeFlags,
) {
if self.on_add.component_observers.contains_key(&component_id) {
flags.insert(ArchetypeFlags::ON_ADD_OBSERVER);
}
if self
.on_insert
.component_observers
.contains_key(&component_id)
{
flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER);
}
if self
.on_remove
.component_observers
.contains_key(&component_id)
{
flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER);
}
}
}
impl World {
/// Spawn a "global" [`Observer`] and returns it's [`Entity`].
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
system: impl IntoObserverSystem<E, B, M>,
) -> EntityWorldMut {
self.spawn(Observer::new(system))
}
/// Triggers the given `event`, which will run any observers watching for it.
pub fn trigger(&mut self, event: impl Event) {
TriggerEvent { event, targets: () }.apply(self);
}
/// Triggers the given `event` for the given `targets`, which will run any observers watching for it.
pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) {
TriggerEvent { event, targets }.apply(self);
}
/// Register an observer to the cache, called when an observer is created
pub(crate) fn register_observer(&mut self, observer_entity: Entity) {
// SAFETY: References do not alias.
let (observer_state, archetypes, observers) = unsafe {
let observer_state: *const ObserverState =
self.get::<ObserverState>(observer_entity).unwrap();
// Populate ObservedBy for each observed entity.
for watched_entity in &(*observer_state).descriptor.entities {
let mut entity_mut = self.entity_mut(*watched_entity);
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default();
observed_by.0.push(observer_entity);
}
(&*observer_state, &mut self.archetypes, &mut self.observers)
};
let descriptor = &observer_state.descriptor;
for &event_type in &descriptor.events {
let cache = observers.get_observers(event_type);
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.map.insert(observer_entity, observer_state.runner);
} else if descriptor.components.is_empty() {
// Observer is not targeting any components so register it as an entity observer
for &watched_entity in &observer_state.descriptor.entities {
let map = cache.entity_observers.entry(watched_entity).or_default();
map.insert(observer_entity, observer_state.runner);
}
} else {
// Register observer for each watched component
for &component in &descriptor.components {
let observers =
cache
.component_observers
.entry(component)
.or_insert_with(|| {
if let Some(flag) = Observers::is_archetype_cached(event_type) {
archetypes.update_flags(component, flag, true);
}
CachedComponentObservers::default()
});
if descriptor.entities.is_empty() {
// Register for all triggers targeting the component
observers.map.insert(observer_entity, observer_state.runner);
} else {
// Register for each watched entity
for &watched_entity in &descriptor.entities {
let map = observers.entity_map.entry(watched_entity).or_default();
map.insert(observer_entity, observer_state.runner);
}
}
}
}
}
}
/// Remove the observer from the cache, called when an observer gets despawned
pub(crate) fn unregister_observer(&mut self, entity: Entity, descriptor: ObserverDescriptor) {
let archetypes = &mut self.archetypes;
let observers = &mut self.observers;
for &event_type in &descriptor.events {
let cache = observers.get_observers(event_type);
if descriptor.components.is_empty() && descriptor.entities.is_empty() {
cache.map.remove(&entity);
} else if descriptor.components.is_empty() {
for watched_entity in &descriptor.entities {
// This check should be unnecessary since this observer hasn't been unregistered yet
let Some(observers) = cache.entity_observers.get_mut(watched_entity) else {
continue;
};
observers.remove(&entity);
if observers.is_empty() {
cache.entity_observers.remove(watched_entity);
}
}
} else {
for component in &descriptor.components {
let Some(observers) = cache.component_observers.get_mut(component) else {
continue;
};
if descriptor.entities.is_empty() {
observers.map.remove(&entity);
} else {
for watched_entity in &descriptor.entities {
let Some(map) = observers.entity_map.get_mut(watched_entity) else {
continue;
};
map.remove(&entity);
if map.is_empty() {
observers.entity_map.remove(watched_entity);
}
}
}
if observers.map.is_empty() && observers.entity_map.is_empty() {
cache.component_observers.remove(component);
if let Some(flag) = Observers::is_archetype_cached(event_type) {
archetypes.update_flags(*component, flag, false);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use bevy_ptr::OwningPtr;
use crate as bevy_ecs;
use crate::observer::{EmitDynamicTrigger, Observer, ObserverDescriptor, ObserverState};
use crate::prelude::*;
#[derive(Component)]
struct A;
#[derive(Component)]
struct B;
#[derive(Component)]
struct C;
#[derive(Component)]
#[component(storage = "SparseSet")]
struct S;
#[derive(Event)]
struct EventA;
#[derive(Resource, Default)]
struct R(usize);
impl R {
#[track_caller]
fn assert_order(&mut self, count: usize) {
assert_eq!(count, self.0);
self.0 += 1;
}
}
#[test]
fn observer_order_spawn_despawn() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
let entity = world.spawn(A).id();
world.despawn(entity);
assert_eq!(3, world.resource::<R>().0);
}
#[test]
fn observer_order_insert_remove() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, A>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, A>, mut res: ResMut<R>| res.assert_order(2));
let mut entity = world.spawn_empty();
entity.insert(A);
entity.remove::<A>();
entity.flush();
assert_eq!(3, world.resource::<R>().0);
}
#[test]
fn observer_order_insert_remove_sparse() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(|_: Trigger<OnAdd, S>, mut res: ResMut<R>| res.assert_order(0));
world.observe(|_: Trigger<OnInsert, S>, mut res: ResMut<R>| res.assert_order(1));
world.observe(|_: Trigger<OnRemove, S>, mut res: ResMut<R>| res.assert_order(2));
let mut entity = world.spawn_empty();
entity.insert(S);
entity.remove::<S>();
entity.flush();
assert_eq!(3, world.resource::<R>().0);
}
#[test]
fn observer_order_recursive() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(
|obs: Trigger<OnAdd, A>, mut res: ResMut<R>, mut commands: Commands| {
res.assert_order(0);
commands.entity(obs.entity()).insert(B);
},
);
world.observe(
|obs: Trigger<OnRemove, A>, mut res: ResMut<R>, mut commands: Commands| {
res.assert_order(2);
commands.entity(obs.entity()).remove::<B>();
},
);
world.observe(
|obs: Trigger<OnAdd, B>, mut res: ResMut<R>, mut commands: Commands| {
res.assert_order(1);
commands.entity(obs.entity()).remove::<A>();
},
);
world.observe(|_: Trigger<OnRemove, B>, mut res: ResMut<R>| {
res.assert_order(3);
});
let entity = world.spawn(A).flush();
let entity = world.get_entity(entity).unwrap();
assert!(!entity.contains::<A>());
assert!(!entity.contains::<B>());
assert_eq!(4, world.resource::<R>().0);
}
#[test]
fn observer_multiple_listeners() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1);
world.observe(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1);
world.spawn(A).flush();
assert_eq!(2, world.resource::<R>().0);
// Our A entity plus our two observers
assert_eq!(world.entities().len(), 3);
}
#[test]
fn observer_multiple_events() {
let mut world = World::new();
world.init_resource::<R>();
let on_remove = world.init_component::<OnRemove>();
world.spawn(
// SAFETY: OnAdd and OnRemove are both unit types, so this is safe
unsafe {
Observer::new(|_: Trigger<OnAdd, A>, mut res: ResMut<R>| res.0 += 1)
.with_event(on_remove)
},
);
let entity = world.spawn(A).id();
world.despawn(entity);
assert_eq!(2, world.resource::<R>().0);
}
#[test]
fn observer_multiple_components() {
let mut world = World::new();
world.init_resource::<R>();
world.init_component::<A>();
world.init_component::<B>();
world.observe(|_: Trigger<OnAdd, (A, B)>, mut res: ResMut<R>| res.0 += 1);
let entity = world.spawn(A).id();
world.entity_mut(entity).insert(B);
world.flush();
assert_eq!(2, world.resource::<R>().0);
}
#[test]
fn observer_despawn() {
let mut world = World::new();
world.init_resource::<R>();
let observer = world
.observe(|_: Trigger<OnAdd, A>| panic!("Observer triggered after being despawned."))
.id();
world.despawn(observer);
world.spawn(A).flush();
}
#[test]
fn observer_multiple_matches() {
let mut world = World::new();
world.init_resource::<R>();
world.observe(|_: Trigger<OnAdd, (A, B)>, mut res: ResMut<R>| res.0 += 1);
world.spawn((A, B)).flush();
assert_eq!(1, world.resource::<R>().0);
}
#[test]
fn observer_no_target() {
let mut world = World::new();
world.init_resource::<R>();
world
.spawn_empty()
.observe(|_: Trigger<EventA>| panic!("Trigger routed to non-targeted entity."));
world.observe(move |obs: Trigger<EventA>, mut res: ResMut<R>| {
assert_eq!(obs.entity(), Entity::PLACEHOLDER);
res.0 += 1;
});
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
// and therefore does not automatically flush.
world.flush();
world.trigger(EventA);
world.flush();
assert_eq!(1, world.resource::<R>().0);
}
#[test]
fn observer_entity_routing() {
let mut world = World::new();
world.init_resource::<R>();
world
.spawn_empty()
.observe(|_: Trigger<EventA>| panic!("Trigger routed to non-targeted entity."));
let entity = world
.spawn_empty()
.observe(|_: Trigger<EventA>, mut res: ResMut<R>| res.0 += 1)
.id();
world.observe(move |obs: Trigger<EventA>, mut res: ResMut<R>| {
assert_eq!(obs.entity(), entity);
res.0 += 1;
});
// TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut
// and therefore does not automatically flush.
world.flush();
world.trigger_targets(EventA, entity);
world.flush();
assert_eq!(2, world.resource::<R>().0);
}
#[test]
fn observer_dynamic_component() {
let mut world = World::new();
world.init_resource::<R>();
let component_id = world.init_component::<A>();
world.spawn(
Observer::new(|_: Trigger<OnAdd>, mut res: ResMut<R>| res.0 += 1)
.with_component(component_id),
);
let mut entity = world.spawn_empty();
OwningPtr::make(A, |ptr| {
// SAFETY: we registered `component_id` above.
unsafe { entity.insert_by_id(component_id, ptr) };
});
let entity = entity.flush();
world.trigger_targets(EventA, entity);
world.flush();
assert_eq!(1, world.resource::<R>().0);
}
#[test]
fn observer_dynamic_trigger() {
let mut world = World::new();
world.init_resource::<R>();
let event_a = world.init_component::<EventA>();
world.spawn(ObserverState {
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
descriptor: unsafe { ObserverDescriptor::default().with_events(vec![event_a]) },
runner: |mut world, _trigger, _ptr| {
world.resource_mut::<R>().0 += 1;
},
..Default::default()
});
world.commands().add(
// SAFETY: we registered `event_a` above and it matches the type of TriggerA
unsafe { EmitDynamicTrigger::new_with_id(event_a, EventA, ()) },
);
world.flush();
assert_eq!(1, world.resource::<R>().0);
}
}

View File

@ -0,0 +1,412 @@
use crate::{
component::{ComponentHooks, ComponentId, StorageType},
observer::{ObserverDescriptor, ObserverTrigger},
prelude::*,
query::DebugCheckedUnwrap,
system::{IntoObserverSystem, ObserverSystem},
world::DeferredWorld,
};
use bevy_ptr::PtrMut;
/// Contains [`Observer`] information. This defines how a given observer behaves. It is the
/// "source of truth" for a given observer entity's behavior.
pub struct ObserverState {
pub(crate) descriptor: ObserverDescriptor,
pub(crate) runner: ObserverRunner,
pub(crate) last_trigger_id: u32,
pub(crate) despawned_watched_entities: u32,
}
impl Default for ObserverState {
fn default() -> Self {
Self {
runner: |_, _, _| {},
last_trigger_id: 0,
despawned_watched_entities: 0,
descriptor: Default::default(),
}
}
}
impl ObserverState {
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
/// is triggered.
pub fn with_event(mut self, event: ComponentId) -> Self {
self.descriptor.events.push(event);
self
}
/// Observe the given event list. This will cause the [`Observer`] to run whenever an event with any of the given [`ComponentId`]s
/// is triggered.
pub fn with_events(mut self, events: impl IntoIterator<Item = ComponentId>) -> Self {
self.descriptor.events.extend(events);
self
}
/// Observe the given [`Entity`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
/// for any [`Entity`] target in the list.
pub fn with_entities(mut self, entities: impl IntoIterator<Item = Entity>) -> Self {
self.descriptor.entities.extend(entities);
self
}
/// Observe the given [`ComponentId`] list. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
/// for any [`ComponentId`] target in the list.
pub fn with_components(mut self, components: impl IntoIterator<Item = ComponentId>) -> Self {
self.descriptor.components.extend(components);
self
}
}
impl Component for ObserverState {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|mut world, entity, _| {
world.commands().add(move |world: &mut World| {
world.register_observer(entity);
});
});
hooks.on_remove(|mut world, entity, _| {
let descriptor = std::mem::take(
&mut world
.entity_mut(entity)
.get_mut::<ObserverState>()
.unwrap()
.as_mut()
.descriptor,
);
world.commands().add(move |world: &mut World| {
world.unregister_observer(entity, descriptor);
});
});
}
}
/// Type for function that is run when an observer is triggered.
/// Typically refers to the default runner that runs the system stored in the associated [`ObserverSystemComponent`],
/// but can be overridden for custom behaviour.
pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut);
/// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer".
///
/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`].
///
/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific
/// point in the schedule.
///
/// # Usage
///
/// The simplest usage
/// of the observer pattern looks like this:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// #[derive(Event)]
/// struct Speak {
/// message: String,
/// }
///
/// world.observe(|trigger: Trigger<Speak>| {
/// println!("{}", trigger.event().message);
/// });
///
/// // Observers currently require a flush() to be registered. In the context of schedules,
/// // this will generally be done for you.
/// world.flush();
///
/// world.trigger(Speak {
/// message: "Hello!".into(),
/// });
/// ```
///
/// Notice that we used [`World::observe`]. This is just a shorthand for spawning an [`Observer`] manually:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # #[derive(Event)]
/// # struct Speak;
/// // These are functionally the same:
/// world.observe(|trigger: Trigger<Speak>| {});
/// world.spawn(Observer::new(|trigger: Trigger<Speak>| {}));
/// ```
///
/// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # #[derive(Event)]
/// # struct PrintNames;
/// # #[derive(Component, Debug)]
/// # struct Name;
/// world.observe(|trigger: Trigger<PrintNames>, names: Query<&Name>| {
/// for name in &names {
/// println!("{name:?}");
/// }
/// });
/// ```
///
/// Note that [`Trigger`] must always be the first parameter.
///
/// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # #[derive(Event)]
/// # struct SpawnThing;
/// # #[derive(Component, Debug)]
/// # struct Thing;
/// world.observe(|trigger: Trigger<SpawnThing>, mut commands: Commands| {
/// commands.spawn(Thing);
/// });
/// ```
///
/// Observers can also trigger new events:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # #[derive(Event)]
/// # struct A;
/// # #[derive(Event)]
/// # struct B;
/// world.observe(|trigger: Trigger<A>, mut commands: Commands| {
/// commands.trigger(B);
/// });
/// ```
///
/// When the commands are flushed (including these "nested triggers") they will be
/// recursively evaluated until there are no commands left, meaning nested triggers all
/// evaluate at the same time!
///
/// Events can be triggered for entities, which will be passed to the [`Observer`]:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # let entity = world.spawn_empty().id();
/// #[derive(Event)]
/// struct Explode;
///
/// world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
/// println!("Entity {:?} goes BOOM!", trigger.entity());
/// commands.entity(trigger.entity()).despawn();
/// });
///
/// world.flush();
///
/// world.trigger_targets(Explode, entity);
/// ```
///
/// You can trigger multiple entities at once:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # let e1 = world.spawn_empty().id();
/// # let e2 = world.spawn_empty().id();
/// # #[derive(Event)]
/// # struct Explode;
/// world.trigger_targets(Explode, [e1, e2]);
/// ```
///
/// Observers can also watch _specific_ entities, which enables you to assign entity-specific logic:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # #[derive(Component, Debug)]
/// # struct Name(String);
/// # let mut world = World::default();
/// # let e1 = world.spawn_empty().id();
/// # let e2 = world.spawn_empty().id();
/// # #[derive(Event)]
/// # struct Explode;
/// world.entity_mut(e1).observe(|trigger: Trigger<Explode>, mut commands: Commands| {
/// println!("Boom!");
/// commands.entity(trigger.entity()).despawn();
/// });
///
/// world.entity_mut(e2).observe(|trigger: Trigger<Explode>, mut commands: Commands| {
/// println!("The explosion fizzles! This entity is immune!");
/// });
/// ```
///
/// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned.
/// This protects against observer "garbage" building up over time.
///
/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again)
/// just shorthand for spawning an [`Observer`] directly:
///
/// ```
/// # use bevy_ecs::prelude::*;
/// # let mut world = World::default();
/// # let entity = world.spawn_empty().id();
/// # #[derive(Event)]
/// # struct Explode;
/// let mut observer = Observer::new(|trigger: Trigger<Explode>| {});
/// observer.watch_entity(entity);
/// world.spawn(observer);
/// ```
///
/// Note that the [`Observer`] component is not added to the entity it is observing. Observers should always be their own entities!
///
/// You can call [`Observer::watch_entity`] more than once, which allows you to watch multiple entities with the same [`Observer`].
///
/// When first added, [`Observer`] will also create an [`ObserverState`] component, which registers the observer with the [`World`] and
/// serves as the "source of truth" of the observer.
///
/// [`SystemParam`]: crate::system::SystemParam
pub struct Observer<T: 'static, B: Bundle> {
system: BoxedObserverSystem<T, B>,
descriptor: ObserverDescriptor,
}
impl<E: Event, B: Bundle> Observer<E, B> {
/// Creates a new [`Observer`], which defaults to a "global" observer. This means it will run whenever the event `E` is triggered
/// for _any_ entity (or no entity).
pub fn new<M>(system: impl IntoObserverSystem<E, B, M>) -> Self {
Self {
system: Box::new(IntoObserverSystem::into_system(system)),
descriptor: Default::default(),
}
}
/// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
/// for the `entity`.
pub fn with_entity(mut self, entity: Entity) -> Self {
self.descriptor.entities.push(entity);
self
}
/// Observe the given `entity`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
/// for the `entity`.
/// Note that if this is called _after_ an [`Observer`] is spawned, it will produce no effects.
pub fn watch_entity(&mut self, entity: Entity) {
self.descriptor.entities.push(entity);
}
/// Observe the given `component`. This will cause the [`Observer`] to run whenever the [`Event`] is triggered
/// with the given component target.
pub fn with_component(mut self, component: ComponentId) -> Self {
self.descriptor.components.push(component);
self
}
/// Observe the given `event`. This will cause the [`Observer`] to run whenever an event with the given [`ComponentId`]
/// is triggered.
/// # Safety
/// The type of the `event` [`ComponentId`] _must_ match the actual value
/// of the event passed into the observer system.
pub unsafe fn with_event(mut self, event: ComponentId) -> Self {
self.descriptor.events.push(event);
self
}
}
impl<E: Event, B: Bundle> Component for Observer<E, B> {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|mut world, entity, _| {
world.commands().add(move |world: &mut World| {
let event_type = world.init_component::<E>();
let mut components = Vec::new();
B::component_ids(&mut world.components, &mut world.storages, &mut |id| {
components.push(id);
});
let mut descriptor = ObserverDescriptor {
events: vec![event_type],
components,
..Default::default()
};
// Initialize System
let system: *mut dyn ObserverSystem<E, B> =
if let Some(mut observe) = world.get_mut::<Self>(entity) {
descriptor.merge(&observe.descriptor);
&mut *observe.system
} else {
return;
};
// SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias
unsafe {
(*system).initialize(world);
}
{
let mut entity = world.entity_mut(entity);
if let crate::world::Entry::Vacant(entry) = entity.entry::<ObserverState>() {
entry.insert(ObserverState {
descriptor,
runner: observer_system_runner::<E, B>,
..Default::default()
});
}
}
});
});
}
}
/// Equivalent to [`BoxedSystem`](crate::system::BoxedSystem) for [`ObserverSystem`].
pub type BoxedObserverSystem<E = (), B = ()> = Box<dyn ObserverSystem<E, B>>;
fn observer_system_runner<E: Event, B: Bundle>(
mut world: DeferredWorld,
observer_trigger: ObserverTrigger,
ptr: PtrMut,
) {
let world = world.as_unsafe_world_cell();
// SAFETY: Observer was triggered so must still exist in world
let observer_cell = unsafe {
world
.get_entity(observer_trigger.observer)
.debug_checked_unwrap()
};
// SAFETY: Observer was triggered so must have an `ObserverState`
let mut state = unsafe {
observer_cell
.get_mut::<ObserverState>()
.debug_checked_unwrap()
};
// TODO: Move this check into the observer cache to avoid dynamic dispatch
// SAFETY: We only access world metadata
let last_trigger = unsafe { world.world_metadata() }.last_trigger_id();
if state.last_trigger_id == last_trigger {
return;
}
state.last_trigger_id = last_trigger;
// SAFETY: Caller ensures `ptr` is castable to `&mut T`
let trigger: Trigger<E, B> = Trigger::new(unsafe { ptr.deref_mut() }, observer_trigger);
// SAFETY: the static lifetime is encapsulated in Trigger / cannot leak out.
// Additionally, IntoObserverSystem is only implemented for functions starting
// with for<'a> Trigger<'a>, meaning users cannot specify Trigger<'static> manually,
// allowing the Trigger<'static> to be moved outside of the context of the system.
// This transmute is obviously not ideal, but it is safe. Ideally we can remove the
// static constraint from ObserverSystem, but so far we have not found a way.
let trigger: Trigger<'static, E, B> = unsafe { std::mem::transmute(trigger) };
// SAFETY: Observer was triggered so must have an `ObserverSystemComponent`
let system = unsafe {
&mut observer_cell
.get_mut::<Observer<E, B>>()
.debug_checked_unwrap()
.system
};
system.update_archetype_component_access(world);
// SAFETY:
// - `update_archetype_component_access` was just called
// - there are no outstanding references to world except a private component
// - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld`
// - system is the same type erased system from above
unsafe {
system.run_unsafe(trigger, world);
system.queue_deferred(world.into_deferred());
}
}

View File

@ -0,0 +1,165 @@
use crate::{
component::ComponentId,
entity::Entity,
event::Event,
world::{Command, DeferredWorld, World},
};
/// A [`Command`] that emits a given trigger for a given set of targets.
pub struct TriggerEvent<E, Targets: TriggerTargets = ()> {
/// The event to trigger.
pub event: E,
/// The targets to trigger the event for.
pub targets: Targets,
}
impl<E: Event, Targets: TriggerTargets> Command for TriggerEvent<E, Targets> {
fn apply(mut self, world: &mut World) {
let event_type = world.init_component::<E>();
trigger_event(world, event_type, &mut self.event, self.targets);
}
}
/// Emit a trigger for a dynamic component id. This is unsafe and must be verified manually.
pub struct EmitDynamicTrigger<T, Targets: TriggerTargets = ()> {
event_type: ComponentId,
event_data: T,
targets: Targets,
}
impl<E, Targets: TriggerTargets> EmitDynamicTrigger<E, Targets> {
/// Sets the event type of the resulting trigger, used for dynamic triggers
/// # Safety
/// Caller must ensure that the component associated with `event_type` is accessible as E
pub unsafe fn new_with_id(event_type: ComponentId, event_data: E, targets: Targets) -> Self {
Self {
event_type,
event_data,
targets,
}
}
}
impl<E: Event, Targets: TriggerTargets> Command for EmitDynamicTrigger<E, Targets> {
fn apply(mut self, world: &mut World) {
trigger_event(world, self.event_type, &mut self.event_data, self.targets);
}
}
#[inline]
fn trigger_event<E, Targets: TriggerTargets>(
world: &mut World,
event_type: ComponentId,
event_data: &mut E,
targets: Targets,
) {
let mut world = DeferredWorld::from(world);
if targets.entities().len() == 0 {
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
unsafe {
world.trigger_observers_with_data(
event_type,
Entity::PLACEHOLDER,
targets.components(),
event_data,
);
};
} else {
for target in targets.entities() {
// SAFETY: T is accessible as the type represented by self.trigger, ensured in `Self::new`
unsafe {
world.trigger_observers_with_data(
event_type,
target,
targets.components(),
event_data,
);
};
}
}
}
/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`].
/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination
/// will run.
///
/// [`Trigger`]: crate::observer::Trigger
/// [`Observer`]: crate::observer::Observer
pub trait TriggerTargets: Send + Sync + 'static {
/// The components the trigger should target.
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId>;
/// The entities the trigger should target.
fn entities(&self) -> impl ExactSizeIterator<Item = Entity>;
}
impl TriggerTargets for () {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
}
}
impl TriggerTargets for Entity {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
std::iter::once(*self)
}
}
impl TriggerTargets for Vec<Entity> {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
self.iter().copied()
}
}
impl<const N: usize> TriggerTargets for [Entity; N] {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
[].into_iter()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
self.iter().copied()
}
}
impl TriggerTargets for ComponentId {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
std::iter::once(*self)
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
}
}
impl TriggerTargets for Vec<ComponentId> {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
self.iter().copied()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
}
}
impl<const N: usize> TriggerTargets for [ComponentId; N] {
fn components(&self) -> impl ExactSizeIterator<Item = ComponentId> {
self.iter().copied()
}
fn entities(&self) -> impl ExactSizeIterator<Item = Entity> {
[].into_iter()
}
}

View File

@ -647,6 +647,16 @@ impl<T: SparseSetIndex> FilteredAccessSet<T> {
.extend(filtered_access_set.filtered_accesses);
}
/// Marks the set as reading all possible indices of type T.
pub fn read_all(&mut self) {
self.combined_access.read_all();
}
/// Marks the set as writing all T.
pub fn write_all(&mut self) {
self.combined_access.write_all();
}
/// Removes all accesses stored in this set.
pub fn clear(&mut self) {
self.combined_access.clear();

View File

@ -43,9 +43,8 @@ pub struct QueryBuilder<'w, D: QueryData = (), F: QueryFilter = ()> {
impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
/// Creates a new builder with the accesses required for `Q` and `F`
pub fn new(world: &'w mut World) -> Self {
let initializer = &mut world.component_initializer();
let fetch_state = D::init_state(initializer);
let filter_state = F::init_state(initializer);
let fetch_state = D::init_state(world);
let filter_state = F::init_state(world);
let mut access = FilteredAccess::default();
D::update_component_access(&fetch_state, &mut access);
@ -96,7 +95,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
/// Adds accesses required for `T` to self.
pub fn data<T: QueryData>(&mut self) -> &mut Self {
let state = T::init_state(&mut self.world.component_initializer());
let state = T::init_state(self.world);
let mut access = FilteredAccess::default();
T::update_component_access(&state, &mut access);
self.extend_access(access);
@ -105,7 +104,7 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
/// Adds filter from `T` to self.
pub fn filter<T: QueryFilter>(&mut self) -> &mut Self {
let state = T::init_state(&mut self.world.component_initializer());
let state = T::init_state(self.world);
let mut access = FilteredAccess::default();
T::update_component_access(&state, &mut access);
self.extend_access(access);
@ -223,9 +222,8 @@ impl<'w, D: QueryData, F: QueryFilter> QueryBuilder<'w, D, F> {
pub fn transmute_filtered<NewD: QueryData, NewF: QueryFilter>(
&mut self,
) -> &mut QueryBuilder<'w, NewD, NewF> {
let initializer = &mut self.world.component_initializer();
let mut fetch_state = NewD::init_state(initializer);
let filter_state = NewF::init_state(initializer);
let mut fetch_state = NewD::init_state(self.world);
let filter_state = NewF::init_state(self.world);
NewD::set_access(&mut fetch_state, &self.access);

View File

@ -1,20 +1,20 @@
use crate::{
archetype::{Archetype, Archetypes},
change_detection::{Ticks, TicksMut},
component::{Component, ComponentId, ComponentInitializer, Components, StorageType, Tick},
component::{Component, ComponentId, Components, StorageType, Tick},
entity::{Entities, Entity, EntityLocation},
query::{Access, DebugCheckedUnwrap, FilteredAccess, WorldQuery},
storage::{ComponentSparseSet, Table, TableRow},
world::{
unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, FilteredEntityMut,
FilteredEntityRef, Mut, Ref,
FilteredEntityRef, Mut, Ref, World,
},
};
use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref};
use bevy_utils::all_tuples;
use std::{cell::UnsafeCell, marker::PhantomData};
/// Types that can be fetched from a [`World`](crate::world::World) using a [`Query`].
/// Types that can be fetched from a [`World`] using a [`Query`].
///
/// There are many types that natively implement this trait:
///
@ -335,7 +335,7 @@ unsafe impl WorldQuery for Entity {
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
fn init_state(_initializer: &mut ComponentInitializer) {}
fn init_state(_world: &mut World) {}
fn get_state(_components: &Components) -> Option<()> {
Some(())
@ -407,7 +407,7 @@ unsafe impl WorldQuery for EntityLocation {
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
fn init_state(_initializer: &mut ComponentInitializer) {}
fn init_state(_world: &mut World) {}
fn get_state(_components: &Components) -> Option<()> {
Some(())
@ -486,7 +486,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> {
access.read_all();
}
fn init_state(_initializer: &mut ComponentInitializer) {}
fn init_state(_world: &mut World) {}
fn get_state(_components: &Components) -> Option<()> {
Some(())
@ -562,7 +562,7 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> {
access.write_all();
}
fn init_state(_initializer: &mut ComponentInitializer) {}
fn init_state(_world: &mut World) {}
fn get_state(_components: &Components) -> Option<()> {
Some(())
@ -660,7 +660,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> {
filtered_access.access.extend(&state.access);
}
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {
fn init_state(_world: &mut World) -> Self::State {
FilteredAccess::default()
}
@ -772,7 +772,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> {
filtered_access.access.extend(&state.access);
}
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {
fn init_state(_world: &mut World) -> Self::State {
FilteredAccess::default()
}
@ -846,7 +846,7 @@ unsafe impl WorldQuery for &Archetype {
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
fn init_state(_initializer: &mut ComponentInitializer) {}
fn init_state(_world: &mut World) {}
fn get_state(_components: &Components) -> Option<()> {
Some(())
@ -995,8 +995,8 @@ unsafe impl<T: Component> WorldQuery for &T {
access.add_read(component_id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -1178,8 +1178,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> {
access.add_read(component_id);
}
fn init_state(initializer: &mut ComponentInitializer<'_>) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -1361,8 +1361,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T {
access.add_write(component_id);
}
fn init_state(initializer: &mut ComponentInitializer<'_>) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -1460,8 +1460,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> {
}
// Forwarded to `&mut T`
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
<&mut T as WorldQuery>::init_state(initializer)
fn init_state(world: &mut World) -> ComponentId {
<&mut T as WorldQuery>::init_state(world)
}
// Forwarded to `&mut T`
@ -1581,8 +1581,8 @@ unsafe impl<T: WorldQuery> WorldQuery for Option<T> {
access.extend_access(&intermediate);
}
fn init_state(initializer: &mut ComponentInitializer) -> T::State {
T::init_state(initializer)
fn init_state(world: &mut World) -> T::State {
T::init_state(world)
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -1736,8 +1736,8 @@ unsafe impl<T: Component> WorldQuery for Has<T> {
access.access_mut().add_archetypal(component_id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -1862,16 +1862,17 @@ macro_rules! impl_anytuple_fetch {
}
fn update_component_access(state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {
let ($($name,)*) = state;
let mut _new_access = _access.clone();
// update the filters (Or<(With<$name>,)>)
let ($($name,)*) = state;
let mut _not_first = false;
$(
if _not_first {
// we use an intermediate access because we only want to update the filter_sets, not the access
let mut intermediate = _access.clone();
$name::update_component_access($name, &mut intermediate);
_new_access.append_or(&intermediate);
_new_access.extend_access(&intermediate);
} else {
$name::update_component_access($name, &mut _new_access);
_new_access.required = _access.required.clone();
@ -1879,11 +1880,16 @@ macro_rules! impl_anytuple_fetch {
}
)*
*_access = _new_access;
_access.filter_sets = _new_access.filter_sets;
// update the access (add the read/writes)
// Option<T> updates the access but not the filter_sets
<($(Option<$name>,)*)>::update_component_access(state, _access);
}
#[allow(unused_variables)]
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
($($name::init_state(initializer),)*)
fn init_state(world: &mut World) -> Self::State {
($($name::init_state(world),)*)
}
#[allow(unused_variables)]
fn get_state(components: &Components) -> Option<Self::State> {
@ -1959,8 +1965,8 @@ unsafe impl<D: QueryData> WorldQuery for NopWorldQuery<D> {
fn update_component_access(_state: &D::State, _access: &mut FilteredAccess<ComponentId>) {}
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
D::init_state(initializer)
fn init_state(world: &mut World) -> Self::State {
D::init_state(world)
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -2026,7 +2032,7 @@ unsafe impl<T: ?Sized> WorldQuery for PhantomData<T> {
fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess<ComponentId>) {}
fn init_state(_initializer: &mut ComponentInitializer) -> Self::State {}
fn init_state(_world: &mut World) -> Self::State {}
fn get_state(_components: &Components) -> Option<Self::State> {
Some(())

View File

@ -1,10 +1,10 @@
use crate::{
archetype::Archetype,
component::{Component, ComponentId, ComponentInitializer, Components, StorageType, Tick},
component::{Component, ComponentId, Components, StorageType, Tick},
entity::Entity,
query::{DebugCheckedUnwrap, FilteredAccess, WorldQuery},
storage::{Column, ComponentSparseSet, Table, TableRow},
world::unsafe_world_cell::UnsafeWorldCell,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref};
use bevy_utils::all_tuples;
@ -183,8 +183,8 @@ unsafe impl<T: Component> WorldQuery for With<T> {
access.and_with(id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -291,8 +291,8 @@ unsafe impl<T: Component> WorldQuery for Without<T> {
access.and_without(id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -461,8 +461,8 @@ macro_rules! impl_or_query_filter {
*access = _new_access;
}
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
($($filter::init_state(initializer),)*)
fn init_state(world: &mut World) -> Self::State {
($($filter::init_state(world),)*)
}
fn get_state(components: &Components) -> Option<Self::State> {
@ -693,8 +693,8 @@ unsafe impl<T: Component> WorldQuery for Added<T> {
access.add_read(id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<ComponentId> {
@ -904,8 +904,8 @@ unsafe impl<T: Component> WorldQuery for Changed<T> {
access.add_read(id);
}
fn init_state(initializer: &mut ComponentInitializer) -> ComponentId {
initializer.init_component::<T>()
fn init_state(world: &mut World) -> ComponentId {
world.init_component::<T>()
}
fn get_state(components: &Components) -> Option<ComponentId> {

View File

@ -730,6 +730,77 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> {
}
}
/// Sorts all query items into a new iterator with a key extraction function over the query lens.
///
/// This sort is unstable (i.e., may reorder equal elements).
///
/// This uses [`slice::sort_unstable_by_key`] internally.
///
/// Defining the lens works like [`transmute_lens`](crate::system::Query::transmute_lens).
/// This includes the allowed parameter type changes listed under [allowed transmutes].
/// However, the lens uses the filter of the original query when present.
///
/// The sort is not cached across system runs.
///
/// [allowed transmutes]: crate::system::Query#allowed-transmutes
///
/// # Panics
///
/// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty.
pub fn sort_unstable_by_key<L: ReadOnlyQueryData + 'w, K>(
self,
mut f: impl FnMut(&L::Item<'w>) -> K,
) -> QuerySortedIter<
'w,
's,
D,
F,
impl ExactSizeIterator<Item = Entity> + DoubleEndedIterator + FusedIterator + 'w,
>
where
K: Ord,
{
// On the first successful iteration of `QueryIterationCursor`, `archetype_entities` or `table_entities`
// will be set to a non-zero value. The correctness of this method relies on this.
// I.e. this sort method will execute if and only if `next` on `QueryIterationCursor` of a
// non-empty `QueryIter` has not yet been called. When empty, this sort method will not panic.
if !self.cursor.archetype_entities.is_empty() || !self.cursor.table_entities.is_empty() {
panic!("it is not valid to call sort() after next()")
}
let world = self.world;
let query_lens_state = self
.query_state
.transmute_filtered::<(L, Entity), F>(world.components());
// SAFETY:
// `self.world` has permission to access the required components.
// The original query iter has not been iterated on, so no items are aliased from it.
let query_lens = unsafe {
query_lens_state.iter_unchecked_manual(
world,
world.last_change_tick(),
world.change_tick(),
)
};
let mut keyed_query: Vec<_> = query_lens.collect();
keyed_query.sort_unstable_by_key(|(lens, _)| f(lens));
let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity);
// SAFETY:
// `self.world` has permission to access the required components.
// Each lens query item is dropped before the respective actual query item is accessed.
unsafe {
QuerySortedIter::new(
world,
self.query_state,
entity_iter,
world.last_change_tick(),
world.change_tick(),
)
}
}
/// Sort all query items into a new iterator with a key extraction function over the query lens.
///
/// This sort is stable (i.e., does not reorder equal elements).
@ -874,7 +945,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Debug for QueryIter<'w, 's, D, F> {
///
/// This struct is created by the [`QueryIter::sort`], [`QueryIter::sort_unstable`],
/// [`QueryIter::sort_by`], [`QueryIter::sort_unstable_by`], [`QueryIter::sort_by_key`],
/// and [`QueryIter::sort_by_cached_key`] methods.
/// [`QueryIter::sort_unstable_by_key`], and [`QueryIter::sort_by_cached_key`] methods.
pub struct QuerySortedIter<'w, 's, D: QueryData, F: QueryFilter, I>
where
I: Iterator<Item = Entity>,
@ -1681,6 +1752,11 @@ mod tests {
.sort_by_key::<Entity, _>(|&e| e)
.collect::<Vec<_>>();
let sort_unstable_by_key = query
.iter(&world)
.sort_unstable_by_key::<Entity, _>(|&e| e)
.collect::<Vec<_>>();
let sort_by_cached_key = query
.iter(&world)
.sort_by_cached_key::<Entity, _>(|&e| e)
@ -1701,6 +1777,9 @@ mod tests {
let mut sort_by_key_v2 = query.iter(&world).collect::<Vec<_>>();
sort_by_key_v2.sort_by_key(|&e| e);
let mut sort_unstable_by_key_v2 = query.iter(&world).collect::<Vec<_>>();
sort_unstable_by_key_v2.sort_unstable_by_key(|&e| e);
let mut sort_by_cached_key_v2 = query.iter(&world).collect::<Vec<_>>();
sort_by_cached_key_v2.sort_by_cached_key(|&e| e);
@ -1709,6 +1788,7 @@ mod tests {
assert_eq!(sort_by, sort_by_v2);
assert_eq!(sort_unstable_by, sort_unstable_by_v2);
assert_eq!(sort_by_key, sort_by_key_v2);
assert_eq!(sort_unstable_by_key, sort_unstable_by_key_v2);
assert_eq!(sort_by_cached_key, sort_by_cached_key_v2);
}

View File

@ -178,9 +178,8 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
/// `new_archetype` and its variants must be called on all of the World's archetypes before the
/// state can return valid query results.
fn new_uninitialized(world: &mut World) -> Self {
let initializer = &mut world.component_initializer();
let fetch_state = D::init_state(initializer);
let filter_state = F::init_state(initializer);
let fetch_state = D::init_state(world);
let filter_state = F::init_state(world);
let mut component_access = FilteredAccess::default();
D::update_component_access(&fetch_state, &mut component_access);
@ -215,9 +214,8 @@ impl<D: QueryData, F: QueryFilter> QueryState<D, F> {
/// Creates a new [`QueryState`] from a given [`QueryBuilder`] and inherits its [`FilteredAccess`].
pub fn from_builder(builder: &mut QueryBuilder<D, F>) -> Self {
let initializer = &mut builder.world_mut().component_initializer();
let mut fetch_state = D::init_state(initializer);
let filter_state = F::init_state(initializer);
let mut fetch_state = D::init_state(builder.world_mut());
let filter_state = F::init_state(builder.world_mut());
D::set_access(&mut fetch_state, builder.access());
let mut state = Self {

View File

@ -1,10 +1,10 @@
use crate::{
archetype::Archetype,
component::{ComponentId, ComponentInitializer, Components, Tick},
component::{ComponentId, Components, Tick},
entity::Entity,
query::FilteredAccess,
storage::{Table, TableRow},
world::unsafe_world_cell::UnsafeWorldCell,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
use bevy_utils::all_tuples;
@ -79,7 +79,7 @@ pub unsafe trait WorldQuery {
///
/// # Safety
///
/// - `archetype` and `tables` must be from the same [`World`](crate::world::World) that [`WorldQuery::init_state`] was called on.
/// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on.
/// - `table` must correspond to `archetype`.
/// - `state` must be the [`State`](Self::State) that `fetch` was initialized with.
unsafe fn set_archetype<'w>(
@ -94,7 +94,7 @@ pub unsafe trait WorldQuery {
///
/// # Safety
///
/// - `table` must be from the same [`World`](crate::world::World) that [`WorldQuery::init_state`] was called on.
/// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on.
/// - `state` must be the [`State`](Self::State) that `fetch` was initialized with.
unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table);
@ -127,7 +127,7 @@ pub unsafe trait WorldQuery {
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>);
/// Creates and initializes a [`State`](WorldQuery::State) for this [`WorldQuery`] type.
fn init_state(initializer: &mut ComponentInitializer) -> Self::State;
fn init_state(world: &mut World) -> Self::State;
/// Attempts to initialize a [`State`](WorldQuery::State) for this [`WorldQuery`] type using read-only
/// access to [`Components`].
@ -213,8 +213,8 @@ macro_rules! impl_tuple_world_query {
$($name::update_component_access($name, _access);)*
}
#[allow(unused_variables)]
fn init_state(initializer: &mut ComponentInitializer) -> Self::State {
($($name::init_state(initializer),)*)
fn init_state(world: &mut World) -> Self::State {
($($name::init_state(world),)*)
}
#[allow(unused_variables)]
fn get_state(components: &Components) -> Option<Self::State> {

View File

@ -116,11 +116,11 @@ impl RemovedComponentEvents {
///
/// If you are using `bevy_ecs` as a standalone crate,
/// note that the `RemovedComponents` list will not be automatically cleared for you,
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers)
/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers).
///
/// For users of `bevy` and `bevy_app`, this is automatically done in `bevy_app::App::update`.
/// For the main world, [`World::clear_trackers`](World::clear_trackers) is run after the main schedule is run and after
/// `SubApp`'s have run.
/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is
/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`.
/// For the main world, this is delayed until after all `SubApp`s have run.
///
/// # Examples
///

View File

@ -127,6 +127,11 @@ where
self.system.apply_deferred(world);
}
#[inline]
fn queue_deferred(&mut self, world: crate::world::DeferredWorld) {
self.system.queue_deferred(world);
}
fn initialize(&mut self, world: &mut crate::prelude::World) {
self.system.initialize(world);
}

View File

@ -202,6 +202,12 @@ where
self.b.apply_deferred(world);
}
#[inline]
fn queue_deferred(&mut self, mut world: crate::world::DeferredWorld) {
self.a.queue_deferred(world.reborrow());
self.b.queue_deferred(world);
}
fn initialize(&mut self, world: &mut World) {
self.a.initialize(world);
self.b.initialize(world);

View File

@ -1,11 +1,13 @@
mod parallel_scope;
use super::{Deferred, IntoSystem, RegisterSystem, Resource};
use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource};
use crate::{
self as bevy_ecs,
bundle::Bundle,
component::ComponentId,
entity::{Entities, Entity},
event::Event,
observer::{Observer, TriggerEvent, TriggerTargets},
system::{RunSystemWithInput, SystemId},
world::command_queue::RawCommandQueue,
world::{Command, CommandQueue, EntityWorldMut, FromWorld, World},
@ -70,6 +72,12 @@ pub struct Commands<'w, 's> {
entities: &'w Entities,
}
// SAFETY: All commands [`Command`] implement [`Send`]
unsafe impl Send for Commands<'_, '_> {}
// SAFETY: `Commands` never gives access to the inner commands.
unsafe impl Sync for Commands<'_, '_> {}
const _: () = {
type __StructFieldsAlias<'w, 's> = (Deferred<'s, CommandQueue>, &'w Entities);
#[doc(hidden)]
@ -116,6 +124,17 @@ const _: () = {
world,
);
}
fn queue(
state: &mut Self::State,
system_meta: &bevy_ecs::system::SystemMeta,
world: bevy_ecs::world::DeferredWorld,
) {
<__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::queue(
&mut state.state,
system_meta,
world,
);
}
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
system_meta: &bevy_ecs::system::SystemMeta,
@ -150,7 +169,7 @@ impl<'w, 's> Commands<'w, 's> {
///
/// [system parameter]: crate::system::SystemParam
pub fn new(queue: &'s mut CommandQueue, world: &'w World) -> Self {
Self::new_from_entities(queue, world.entities())
Self::new_from_entities(queue, &world.entities)
}
/// Returns a new `Commands` instance from a [`CommandQueue`] and an [`Entities`] reference.
@ -735,6 +754,26 @@ impl<'w, 's> Commands<'w, 's> {
pub fn add<C: Command>(&mut self, command: C) {
self.push(command);
}
/// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that
/// isn't scoped to specific targets.
pub fn trigger(&mut self, event: impl Event) {
self.add(TriggerEvent { event, targets: () });
}
/// Sends a [`Trigger`] for the given targets. This will run any [`Observer`] of the `event` that
/// watches those targets.
pub fn trigger_targets(&mut self, event: impl Event, targets: impl TriggerTargets) {
self.add(TriggerEvent { event, targets });
}
/// Spawn an [`Observer`] and returns the [`EntityCommands`] associated with the entity that stores the observer.
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> EntityCommands {
self.spawn(Observer::new(observer))
}
}
/// A [`Command`] which gets executed for a given [`Entity`].
@ -794,6 +833,7 @@ pub trait EntityCommand<Marker = ()>: Send + 'static {
/// Executes this command for the given [`Entity`].
fn apply(self, id: Entity, world: &mut World);
/// Returns a [`Command`] which executes this [`EntityCommand`] for the given [`Entity`].
#[must_use = "commands do nothing unless applied to a `World`"]
fn with_entity(self, id: Entity) -> WithEntity<Marker, Self>
where
Self: Sized,
@ -1017,6 +1057,7 @@ impl EntityCommands<'_> {
}
/// Despawns the entity.
/// This will emit a warning if the entity does not exist.
///
/// See [`World::despawn`] for more details.
///
@ -1025,10 +1066,6 @@ impl EntityCommands<'_> {
/// This won't clean up external references to the entity (such as parent-child relationships
/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state.
///
/// # Panics
///
/// The command will panic when applied if the associated entity does not exist.
///
/// # Example
///
/// ```
@ -1128,6 +1165,15 @@ impl EntityCommands<'_> {
pub fn commands(&mut self) -> Commands {
self.commands.reborrow()
}
/// Creates an [`Observer`](crate::observer::Observer) listening for a trigger of type `T` that targets this entity.
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
system: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.add(observe(system));
self
}
}
impl<F> Command for F
@ -1285,7 +1331,17 @@ fn log_components(entity: Entity, world: &mut World) {
.into_iter()
.map(|component_info| component_info.name())
.collect();
info!("Entity {:?}: {:?}", entity, debug_infos);
info!("Entity {entity}: {debug_infos:?}");
}
fn observe<E: Event, B: Bundle, M>(
observer: impl IntoObserverSystem<E, B, M>,
) -> impl EntityCommand {
move |entity, world: &mut World| {
if let Some(mut entity) = world.get_entity_mut(entity) {
entity.observe(observer);
}
}
}
#[cfg(test)]
@ -1506,6 +1562,15 @@ mod tests {
assert!(world.contains_resource::<W<f64>>());
}
fn is_send<T: Send>() {}
fn is_sync<T: Sync>() {}
#[test]
fn test_commands_are_send_and_sync() {
is_send::<Commands>();
is_sync::<Commands>();
}
#[test]
fn append() {
let mut world = World::default();

View File

@ -110,7 +110,7 @@ where
);
let out = self.func.run(world, input, params);
world.flush_commands();
world.flush();
let change_tick = world.change_tick.get_mut();
self.system_meta.last_run.set(*change_tick);
*change_tick = change_tick.wrapping_add(1);
@ -126,6 +126,13 @@ where
// might have buffers to apply, but this is handled by `PipeSystem`.
}
#[inline]
fn queue_deferred(&mut self, _world: crate::world::DeferredWorld) {
// "pure" exclusive systems do not have any buffers to apply.
// Systems made by piping a normal system with an exclusive system
// might have buffers to apply, but this is handled by `PipeSystem`.
}
#[inline]
fn initialize(&mut self, world: &mut World) {
self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX);

View File

@ -5,7 +5,7 @@ use crate::{
query::{Access, FilteredAccessSet},
schedule::{InternedSystemSet, SystemSet},
system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem},
world::{unsafe_world_cell::UnsafeWorldCell, World, WorldId},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World, WorldId},
};
use bevy_utils::all_tuples;
@ -399,8 +399,8 @@ where
F: SystemParamFunction<Marker>,
{
func: F,
param_state: Option<<F::Param as SystemParam>::State>,
system_meta: SystemMeta,
pub(crate) param_state: Option<<F::Param as SystemParam>::State>,
pub(crate) system_meta: SystemMeta,
world_id: Option<WorldId>,
archetype_generation: ArchetypeGeneration,
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
@ -542,6 +542,12 @@ where
F::Param::apply(param_state, &self.system_meta, world);
}
#[inline]
fn queue_deferred(&mut self, world: DeferredWorld) {
let param_state = self.param_state.as_mut().expect(Self::PARAM_MESSAGE);
F::Param::queue(param_state, &self.system_meta, world);
}
#[inline]
fn initialize(&mut self, world: &mut World) {
if let Some(id) = self.world_id {

View File

@ -108,6 +108,7 @@ mod commands;
mod exclusive_function_system;
mod exclusive_system_param;
mod function_system;
mod observer_system;
mod query;
#[allow(clippy::module_inception)]
mod system;
@ -124,6 +125,7 @@ pub use commands::*;
pub use exclusive_function_system::*;
pub use exclusive_system_param::*;
pub use function_system::*;
pub use observer_system::*;
pub use query::*;
pub use system::*;
pub use system_name::*;
@ -554,6 +556,58 @@ mod tests {
run_system(&mut world, sys);
}
#[test]
fn any_of_working() {
fn sys(_: Query<AnyOf<(&mut A, &B)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn any_of_with_and_without_common() {
fn sys(_: Query<(&mut D, &C, AnyOf<(&A, &B)>)>, _: Query<&mut D, Without<C>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."]
fn any_of_with_mut_and_ref() {
fn sys(_: Query<AnyOf<(&mut A, &A)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."]
fn any_of_with_ref_and_mut() {
fn sys(_: Query<AnyOf<(&A, &mut A)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "&bevy_ecs::system::tests::A conflicts with a previous access in this query."]
fn any_of_with_mut_and_option() {
fn sys(_: Query<AnyOf<(&mut A, Option<&A>)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn any_of_with_entity_and_mut() {
fn sys(_: Query<AnyOf<(Entity, &mut A)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn any_of_with_empty_and_mut() {
fn sys(_: Query<AnyOf<((), &mut A)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
#[should_panic = "error[B0001]"]
fn any_of_has_no_filter_with() {
@ -562,6 +616,14 @@ mod tests {
run_system(&mut world, sys);
}
#[test]
#[should_panic = "&mut bevy_ecs::system::tests::A conflicts with a previous access in this query."]
fn any_of_with_conflicting() {
fn sys(_: Query<AnyOf<(&mut A, &mut A)>>) {}
let mut world = World::default();
run_system(&mut world, sys);
}
#[test]
fn any_of_has_filter_with_when_both_have_it() {
fn sys(_: Query<(AnyOf<(&A, &A)>, &mut B)>, _: Query<&mut B, Without<A>>) {}

View File

@ -0,0 +1,116 @@
use bevy_utils::all_tuples;
use crate::{
prelude::{Bundle, Trigger},
system::{System, SystemParam, SystemParamFunction, SystemParamItem},
};
use super::IntoSystem;
/// Implemented for systems that have an [`Observer`] as the first argument.
///
/// [`Observer`]: crate::observer::Observer
pub trait ObserverSystem<E: 'static, B: Bundle, Out = ()>:
System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static
{
}
impl<
E: 'static,
B: Bundle,
Out,
T: System<In = Trigger<'static, E, B>, Out = Out> + Send + 'static,
> ObserverSystem<E, B, Out> for T
{
}
/// Implemented for systems that convert into [`ObserverSystem`].
pub trait IntoObserverSystem<E: 'static, B: Bundle, M, Out = ()>: Send + 'static {
/// The type of [`System`] that this instance converts into.
type System: ObserverSystem<E, B, Out>;
/// Turns this value into its corresponding [`System`].
fn into_system(this: Self) -> Self::System;
}
impl<
S: IntoSystem<Trigger<'static, E, B>, Out, M> + Send + 'static,
M,
Out,
E: 'static,
B: Bundle,
> IntoObserverSystem<E, B, M, Out> for S
where
S::System: ObserverSystem<E, B, Out>,
{
type System = <S as IntoSystem<Trigger<'static, E, B>, Out, M>>::System;
fn into_system(this: Self) -> Self::System {
IntoSystem::into_system(this)
}
}
macro_rules! impl_system_function {
($($param: ident),*) => {
#[allow(non_snake_case)]
impl<E: 'static, B: Bundle, Out, Func: Send + Sync + 'static, $($param: SystemParam),*> SystemParamFunction<fn(Trigger<E, B>, $($param,)*)> for Func
where
for <'a> &'a mut Func:
FnMut(Trigger<E, B>, $($param),*) -> Out +
FnMut(Trigger<E, B>, $(SystemParamItem<$param>),*) -> Out, Out: 'static
{
type In = Trigger<'static, E, B>;
type Out = Out;
type Param = ($($param,)*);
#[inline]
fn run(&mut self, input: Trigger<'static, E, B>, param_value: SystemParamItem< ($($param,)*)>) -> Out {
#[allow(clippy::too_many_arguments)]
fn call_inner<E: 'static, B: Bundle, Out, $($param,)*>(
mut f: impl FnMut(Trigger<'static, E, B>, $($param,)*) -> Out,
input: Trigger<'static, E, B>,
$($param: $param,)*
) -> Out{
f(input, $($param,)*)
}
let ($($param,)*) = param_value;
call_inner(self, input, $($param),*)
}
}
}
}
all_tuples!(impl_system_function, 0, 16, F);
#[cfg(test)]
mod tests {
use crate::{
self as bevy_ecs,
event::Event,
observer::Trigger,
system::{In, IntoSystem},
world::World,
};
#[derive(Event)]
struct TriggerEvent;
#[test]
fn test_piped_observer_systems_no_input() {
fn a(_: Trigger<TriggerEvent>) {}
fn b() {}
let mut world = World::new();
world.observe(a.pipe(b));
}
#[test]
fn test_piped_observer_systems_with_inputs() {
fn a(_: Trigger<TriggerEvent>) -> u32 {
3
}
fn b(_: In<u32>) {}
let mut world = World::new();
world.observe(a.pipe(b));
}
}

View File

@ -1385,9 +1385,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Returns a [`QueryLens`] that can be used to get a query with the combined fetch.
///
/// For example, this can take a `Query<&A>` and a `Queryy<&B>` and return a `Query<&A, &B>`.
/// The returned query will only return items with both `A` and `B`. Note that since filter
/// are dropped, non-archetypal filters like `Added` and `Changed` will no be respected.
/// For example, this can take a `Query<&A>` and a `Query<&B>` and return a `Query<(&A, &B)>`.
/// The returned query will only return items with both `A` and `B`. Note that since filters
/// are dropped, non-archetypal filters like `Added` and `Changed` will not be respected.
/// To maintain or change filter terms see `Self::join_filtered`.
///
/// ## Example
@ -1446,7 +1446,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> {
/// Equivalent to [`Self::join`] but also includes a [`QueryFilter`] type.
///
/// Note that the lens with iterate a subset of the original queries tables
/// Note that the lens with iterate a subset of the original queries' tables
/// and archetypes. This means that additional archetypal query terms like
/// `With` and `Without` will not necessarily be respected and non-archetypal
/// terms like `Added` and `Changed` will only be respected if they are in

View File

@ -4,6 +4,7 @@ use core::fmt::Debug;
use crate::component::Tick;
use crate::schedule::InternedSystemSet;
use crate::world::unsafe_world_cell::UnsafeWorldCell;
use crate::world::DeferredWorld;
use crate::{archetype::ArchetypeComponentId, component::ComponentId, query::Access, world::World};
use std::any::TypeId;
@ -89,6 +90,10 @@ pub trait System: Send + Sync + 'static {
/// This is where [`Commands`](crate::system::Commands) get applied.
fn apply_deferred(&mut self, world: &mut World);
/// Enqueues any [`Deferred`](crate::system::Deferred) system parameters (or other system buffers)
/// of this system into the world's command buffer.
fn queue_deferred(&mut self, world: DeferredWorld);
/// Initialize the system.
fn initialize(&mut self, _world: &mut World);

View File

@ -11,7 +11,7 @@ use crate::{
ReadOnlyQueryData,
},
system::{Query, SystemMeta},
world::{unsafe_world_cell::UnsafeWorldCell, FromWorld, World},
world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World},
};
use bevy_ecs_macros::impl_param_set;
pub use bevy_ecs_macros::Resource;
@ -159,6 +159,11 @@ pub unsafe trait SystemParam: Sized {
#[allow(unused_variables)]
fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) {}
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
#[inline]
#[allow(unused_variables)]
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {}
/// Creates a parameter to be passed into a [`SystemParamFunction`].
///
/// [`SystemParamFunction`]: super::SystemParamFunction
@ -712,6 +717,27 @@ unsafe impl SystemParam for &'_ World {
}
}
/// SAFETY: `DeferredWorld` can read all components and resources but cannot be used to gain any other mutable references.
unsafe impl<'w> SystemParam for DeferredWorld<'w> {
type State = ();
type Item<'world, 'state> = DeferredWorld<'world>;
fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State {
system_meta.component_access_set.read_all();
system_meta.component_access_set.write_all();
system_meta.set_has_deferred();
}
unsafe fn get_param<'world, 'state>(
_state: &'state mut Self::State,
_system_meta: &SystemMeta,
world: UnsafeWorldCell<'world>,
_change_tick: Tick,
) -> Self::Item<'world, 'state> {
world.into_deferred()
}
}
/// A system local [`SystemParam`].
///
/// A local may only be accessed by the system itself and is therefore not visible to other systems.
@ -848,6 +874,8 @@ impl<'w, T: FromWorld + Send + 'static> BuildableSystemParam for Local<'w, T> {
pub trait SystemBuffer: FromWorld + Send + 'static {
/// Applies any deferred mutations to the [`World`].
fn apply(&mut self, system_meta: &SystemMeta, world: &mut World);
/// Queues any deferred mutations to be applied at the next [`apply_deferred`](crate::prelude::apply_deferred).
fn queue(&mut self, _system_meta: &SystemMeta, _world: DeferredWorld) {}
}
/// A [`SystemParam`] that stores a buffer which gets applied to the [`World`] during
@ -1012,6 +1040,10 @@ unsafe impl<T: SystemBuffer> SystemParam for Deferred<'_, T> {
state.get().apply(system_meta, world);
}
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
state.get().queue(system_meta, world);
}
unsafe fn get_param<'w, 's>(
state: &'s mut Self::State,
_system_meta: &SystemMeta,
@ -1424,6 +1456,11 @@ macro_rules! impl_system_param_tuple {
$($param::apply($param, _system_meta, _world);)*
}
#[inline]
fn queue(($($param,)*): &mut Self::State, _system_meta: &SystemMeta, mut _world: DeferredWorld) {
$($param::queue($param, _system_meta, _world.reborrow());)*
}
#[inline]
#[allow(clippy::unused_unit)]
unsafe fn get_param<'w, 's>(
@ -1572,6 +1609,10 @@ unsafe impl<P: SystemParam + 'static> SystemParam for StaticSystemParam<'_, '_,
P::apply(state, system_meta, world);
}
fn queue(state: &mut Self::State, system_meta: &SystemMeta, world: DeferredWorld) {
P::queue(state, system_meta, world);
}
unsafe fn get_param<'world, 'state>(
state: &'state mut Self::State,
system_meta: &SystemMeta,

View File

@ -3,6 +3,7 @@ use crate::system::{SystemBuffer, SystemMeta};
use std::{
fmt::Debug,
mem::MaybeUninit,
panic::{self, AssertUnwindSafe},
ptr::{addr_of_mut, NonNull},
};
@ -11,6 +12,8 @@ use bevy_utils::tracing::warn;
use crate::world::{Command, World};
use super::DeferredWorld;
struct CommandMeta {
/// SAFETY: The `value` must point to a value of type `T: Command`,
/// where `T` is some specific type that was used to produce this metadata.
@ -18,11 +21,8 @@ struct CommandMeta {
/// `world` is optional to allow this one function pointer to perform double-duty as a drop.
///
/// Advances `cursor` by the size of `T` in bytes.
consume_command_and_get_size: unsafe fn(
value: OwningPtr<Unaligned>,
world: Option<NonNull<World>>,
cursor: NonNull<usize>,
),
consume_command_and_get_size:
unsafe fn(value: OwningPtr<Unaligned>, world: Option<NonNull<World>>, cursor: &mut usize),
}
/// Densely and efficiently stores a queue of heterogenous types implementing [`Command`].
@ -41,6 +41,7 @@ pub struct CommandQueue {
// be passed to the corresponding `CommandMeta.apply_command_and_get_size` fn pointer.
pub(crate) bytes: Vec<MaybeUninit<u8>>,
pub(crate) cursor: usize,
pub(crate) panic_recovery: Vec<MaybeUninit<u8>>,
}
/// Wraps pointers to a [`CommandQueue`], used internally to avoid stacked borrow rules when
@ -49,6 +50,7 @@ pub struct CommandQueue {
pub(crate) struct RawCommandQueue {
pub(crate) bytes: NonNull<Vec<MaybeUninit<u8>>>,
pub(crate) cursor: NonNull<usize>,
pub(crate) panic_recovery: NonNull<Vec<MaybeUninit<u8>>>,
}
// CommandQueue needs to implement Debug manually, rather than deriving it, because the derived impl just prints
@ -117,6 +119,7 @@ impl CommandQueue {
RawCommandQueue {
bytes: NonNull::new_unchecked(addr_of_mut!(self.bytes)),
cursor: NonNull::new_unchecked(addr_of_mut!(self.cursor)),
panic_recovery: NonNull::new_unchecked(addr_of_mut!(self.panic_recovery)),
}
}
}
@ -130,6 +133,7 @@ impl RawCommandQueue {
Self {
bytes: NonNull::new_unchecked(Box::into_raw(Box::default())),
cursor: NonNull::new_unchecked(Box::into_raw(Box::new(0usize))),
panic_recovery: NonNull::new_unchecked(Box::into_raw(Box::default())),
}
}
}
@ -164,17 +168,23 @@ impl RawCommandQueue {
}
let meta = CommandMeta {
consume_command_and_get_size: |command, world, mut cursor| {
// SAFETY: Pointer is assured to be valid in `CommandQueue.apply_or_drop_queued`
unsafe { *cursor.as_mut() += std::mem::size_of::<C>() }
consume_command_and_get_size: |command, world, cursor| {
*cursor += std::mem::size_of::<C>();
// SAFETY: According to the invariants of `CommandMeta.consume_command_and_get_size`,
// `command` must point to a value of type `C`.
let command: C = unsafe { command.read_unaligned() };
match world {
// Apply command to the provided world...
// SAFETY: Calller ensures pointer is not null
Some(mut world) => command.apply(unsafe { world.as_mut() }),
Some(mut world) => {
// SAFETY: Caller ensures pointer is not null
let world = unsafe { world.as_mut() };
command.apply(world);
// The command may have queued up world commands, which we flush here to ensure they are also picked up.
// If the current command queue already the World Command queue, this will still behave appropriately because the global cursor
// is still at the current `stop`, ensuring only the newly queued Commands will be applied.
world.flush();
}
// ...or discard it.
None => drop(command),
}
@ -222,50 +232,79 @@ impl RawCommandQueue {
pub(crate) unsafe fn apply_or_drop_queued(&mut self, world: Option<NonNull<World>>) {
// SAFETY: If this is the command queue on world, world will not be dropped as we have a mutable reference
// If this is not the command queue on world we have exclusive ownership and self will not be mutated
while *self.cursor.as_ref() < self.bytes.as_ref().len() {
let start = *self.cursor.as_ref();
let stop = self.bytes.as_ref().len();
let mut local_cursor = start;
// SAFETY: we are setting the global cursor to the current length to prevent the executing commands from applying
// the remaining commands currently in this list. This is safe.
*self.cursor.as_mut() = stop;
while local_cursor < stop {
// SAFETY: The cursor is either at the start of the buffer, or just after the previous command.
// Since we know that the cursor is in bounds, it must point to the start of a new command.
let meta = unsafe {
self.bytes
.as_mut()
.as_mut_ptr()
.add(*self.cursor.as_ref())
.add(local_cursor)
.cast::<CommandMeta>()
.read_unaligned()
};
// Advance to the bytes just after `meta`, which represent a type-erased command.
// SAFETY: For most types of `Command`, the pointer immediately following the metadata
// is guaranteed to be in bounds. If the command is a zero-sized type (ZST), then the cursor
// might be 1 byte past the end of the buffer, which is safe.
unsafe { *self.cursor.as_mut() += std::mem::size_of::<CommandMeta>() };
local_cursor += std::mem::size_of::<CommandMeta>();
// Construct an owned pointer to the command.
// SAFETY: It is safe to transfer ownership out of `self.bytes`, since the increment of `cursor` above
// guarantees 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::<Unaligned>::new(std::ptr::NonNull::new_unchecked(
self.bytes
.as_mut()
.as_mut_ptr()
.add(*self.cursor.as_ref())
.cast(),
self.bytes.as_mut().as_mut_ptr().add(local_cursor).cast(),
))
};
// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
// since they were stored next to each other by `.push()`.
// For ZSTs, the type doesn't matter as long as the pointer is non-null.
// This also advances the cursor past the command. For ZSTs, the cursor will not move.
// At this point, it will either point to the next `CommandMeta`,
// or the cursor will be out of bounds and the loop will end.
unsafe { (meta.consume_command_and_get_size)(cmd, world, self.cursor) };
let result = panic::catch_unwind(AssertUnwindSafe(|| {
// SAFETY: The data underneath the cursor must correspond to the type erased in metadata,
// since they were stored next to each other by `.push()`.
// For ZSTs, the type doesn't matter as long as the pointer is non-null.
// This also advances the cursor past the command. For ZSTs, the cursor will not move.
// At this point, it will either point to the next `CommandMeta`,
// or the cursor will be out of bounds and the loop will end.
unsafe { (meta.consume_command_and_get_size)(cmd, world, &mut local_cursor) };
}));
if let Err(payload) = result {
// local_cursor now points to the location _after_ the panicked command.
// Add the remaining commands that _would have_ been applied to the
// panic_recovery queue.
//
// This uses `current_stop` instead of `stop` to account for any commands
// that were queued _during_ this panic.
//
// This is implemented in such a way that if apply_or_drop_queued() are nested recursively in,
// an applied Command, the correct command order will be retained.
let panic_recovery = self.panic_recovery.as_mut();
let bytes = self.bytes.as_mut();
let current_stop = bytes.len();
panic_recovery.extend_from_slice(&bytes[local_cursor..current_stop]);
bytes.set_len(start);
*self.cursor.as_mut() = start;
// This was the "top of the apply stack". If we are _not_ at the top of the apply stack,
// when we call`resume_unwind" the caller "closer to the top" will catch the unwind and do this check,
// until we reach the top.
if start == 0 {
bytes.append(panic_recovery);
}
panic::resume_unwind(payload);
}
}
// Reset the buffer, so it can be reused after this function ends.
// SAFETY: `set_len(0)` is always valid.
// Reset the buffer: all commands past the original `start` cursor have been applied.
// SAFETY: we are setting the length of bytes to the original length, minus the length of the original
// list of commands being considered. All bytes remaining in the Vec are still valid, unapplied commands.
unsafe {
self.bytes.as_mut().set_len(0);
*self.cursor.as_mut() = 0;
self.bytes.as_mut().set_len(start);
*self.cursor.as_mut() = start;
};
}
}
@ -287,11 +326,18 @@ impl SystemBuffer for CommandQueue {
let _span_guard = _system_meta.commands_span.enter();
self.apply(world);
}
#[inline]
fn queue(&mut self, _system_meta: &SystemMeta, mut world: DeferredWorld) {
world.commands().append(self);
}
}
#[cfg(test)]
mod test {
use super::*;
use crate as bevy_ecs;
use crate::system::Resource;
use std::{
panic::AssertUnwindSafe,
sync::{
@ -412,10 +458,6 @@ mod test {
queue.apply(&mut world);
}));
// even though the first command panicking.
// the cursor was incremented.
assert!(queue.cursor > 0);
// Even though the first command panicked, it's still ok to push
// more commands.
queue.push(SpawnCommand);
@ -424,6 +466,37 @@ mod test {
assert_eq!(world.entities().len(), 3);
}
#[test]
fn test_command_queue_inner_nested_panic_safe() {
std::panic::set_hook(Box::new(|_| {}));
#[derive(Resource, Default)]
struct Order(Vec<usize>);
let mut world = World::new();
world.init_resource::<Order>();
fn add_index(index: usize) -> impl Command {
move |world: &mut World| world.resource_mut::<Order>().0.push(index)
}
world.commands().add(add_index(1));
world.commands().add(|world: &mut World| {
world.commands().add(add_index(2));
world.commands().add(PanicCommand("I panic!".to_owned()));
world.commands().add(add_index(3));
world.flush_commands();
});
world.commands().add(add_index(4));
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
world.flush_commands();
}));
world.commands().add(add_index(5));
world.flush_commands();
assert_eq!(&world.resource::<Order>().0, &[1, 2, 3, 4, 5]);
}
// NOTE: `CommandQueue` is `Send` because `Command` is send.
// If the `Command` trait gets reworked to be non-send, `CommandQueue`
// should be reworked.

View File

@ -0,0 +1,28 @@
use super::*;
use crate::{self as bevy_ecs};
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::Reflect;
/// Internal components used by bevy with a fixed component id.
/// Constants are used to skip [`TypeId`] lookups in hot paths.
/// [`ComponentId`] for [`OnAdd`]
pub const ON_ADD: ComponentId = ComponentId::new(0);
/// [`ComponentId`] for [`OnInsert`]
pub const ON_INSERT: ComponentId = ComponentId::new(1);
/// [`ComponentId`] for [`OnRemove`]
pub const ON_REMOVE: ComponentId = ComponentId::new(2);
/// Trigger emitted when a component is added to an entity.
#[derive(Event)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct OnAdd;
/// Trigger emitted when a component is inserted on to to an entity.
#[derive(Event)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct OnInsert;
/// Trigger emitted when a component is removed from an entity.
#[derive(Event)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
pub struct OnRemove;

View File

@ -1,10 +1,12 @@
use std::ops::Deref;
use crate::{
archetype::Archetype,
change_detection::MutUntyped,
component::ComponentId,
entity::Entity,
event::{Event, EventId, Events, SendBatchIds},
observer::{Observers, TriggerTargets},
prelude::{Component, QueryState},
query::{QueryData, QueryFilter},
system::{Commands, Query, Resource},
@ -52,6 +54,12 @@ impl<'w> From<&'w mut World> for DeferredWorld<'w> {
}
impl<'w> DeferredWorld<'w> {
/// Reborrow self as a new instance of [`DeferredWorld`]
#[inline]
pub fn reborrow(&mut self) -> DeferredWorld {
DeferredWorld { world: self.world }
}
/// Creates a [`Commands`] instance that pushes to the world's command queue
#[inline]
pub fn commands(&mut self) -> Commands {
@ -65,37 +73,42 @@ impl<'w> DeferredWorld<'w> {
/// Returns `None` if the `entity` does not have a [`Component`] of the given type.
#[inline]
pub fn get_mut<T: Component>(&mut self, entity: Entity) -> Option<Mut<T>> {
// SAFETY: &mut self ensure that there are no outstanding accesses to the component
// SAFETY:
// - `as_unsafe_world_cell` is the only thing that is borrowing world
// - `as_unsafe_world_cell` provides mutable permission to everything
// - `&mut self` ensures no other borrows on world data
unsafe { self.world.get_entity(entity)?.get_mut() }
}
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
/// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want
/// to check for entity existence instead of implicitly panic-ing.
#[inline]
#[track_caller]
pub fn entity_mut(&mut self, entity: Entity) -> EntityMut {
#[inline(never)]
#[cold]
#[track_caller]
fn panic_no_entity(entity: Entity) -> ! {
panic!("Entity {entity:?} does not exist");
}
match self.get_entity_mut(entity) {
Some(entity) => entity,
None => panic_no_entity(entity),
}
}
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
/// Returns [`None`] if the `entity` does not exist.
/// Instead of unwrapping the value returned from this function, prefer [`Self::entity_mut`].
#[inline]
pub fn get_entity_mut(&mut self, entity: Entity) -> Option<EntityMut> {
let location = self.entities.get(entity)?;
// SAFETY: `entity` exists and `location` is that entity's location
Some(unsafe { EntityMut::new(UnsafeEntityCell::new(self.world, 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_cell = UnsafeEntityCell::new(self.as_unsafe_world_cell(), entity, location);
// SAFETY: The UnsafeEntityCell has read access to the entire world.
let entity_ref = unsafe { EntityMut::new(entity_cell) };
Some(entity_ref)
}
/// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`.
/// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want
/// to check for entity existence instead of implicitly panic-ing.
#[inline]
pub fn entity_mut(&mut self, entity: Entity) -> EntityMut {
#[inline(never)]
#[cold]
fn panic_no_entity(entity: Entity) -> ! {
panic!("Entity {entity:?} does not exist");
}
match self.get_entity_mut(entity) {
Some(entity) => entity,
None => panic_no_entity(entity),
}
}
/// Returns [`Query`] for the given [`QueryState`], which is used to efficiently
@ -266,14 +279,17 @@ impl<'w> DeferredWorld<'w> {
#[inline]
pub(crate) unsafe fn trigger_on_add(
&mut self,
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
) {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_add {
hook(DeferredWorld { world: self.world }, entity, component_id);
if archetype.has_add_hook() {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_add {
hook(DeferredWorld { world: self.world }, entity, component_id);
}
}
}
}
@ -285,14 +301,17 @@ impl<'w> DeferredWorld<'w> {
#[inline]
pub(crate) unsafe fn trigger_on_insert(
&mut self,
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
) {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_insert {
hook(DeferredWorld { world: self.world }, entity, component_id);
if archetype.has_insert_hook() {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_insert {
hook(DeferredWorld { world: self.world }, entity, component_id);
}
}
}
}
@ -304,15 +323,67 @@ impl<'w> DeferredWorld<'w> {
#[inline]
pub(crate) unsafe fn trigger_on_remove(
&mut self,
archetype: &Archetype,
entity: Entity,
targets: impl Iterator<Item = ComponentId>,
) {
for component_id in targets {
// SAFETY: Caller ensures that these components exist
let hooks = unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_remove {
hook(DeferredWorld { world: self.world }, entity, component_id);
if archetype.has_remove_hook() {
for component_id in targets {
let hooks =
// SAFETY: Caller ensures that these components exist
unsafe { self.world.components().get_info_unchecked(component_id) }.hooks();
if let Some(hook) = hooks.on_remove {
hook(DeferredWorld { world: self.world }, entity, component_id);
}
}
}
}
/// Triggers all event observers for [`ComponentId`] in target.
///
/// # Safety
/// Caller must ensure observers listening for `event` can accept ZST pointers
#[inline]
pub(crate) unsafe fn trigger_observers(
&mut self,
event: ComponentId,
entity: Entity,
components: impl Iterator<Item = ComponentId>,
) {
Observers::invoke(self.reborrow(), event, entity, components, &mut ());
}
/// Triggers all event observers for [`ComponentId`] in target.
///
/// # Safety
/// Caller must ensure `E` is accessible as the type represented by `event`
#[inline]
pub(crate) unsafe fn trigger_observers_with_data<E>(
&mut self,
event: ComponentId,
entity: Entity,
components: impl Iterator<Item = ComponentId>,
data: &mut E,
) {
Observers::invoke(self.reborrow(), event, entity, components, data);
}
/// Sends a "global" [`Trigger`] without any targets.
pub fn trigger<T: Event>(&mut self, trigger: impl Event) {
self.commands().trigger(trigger);
}
/// Sends a [`Trigger`] with the given `targets`.
pub fn trigger_targets(&mut self, trigger: impl Event, targets: impl TriggerTargets) {
self.commands().trigger_targets(trigger, targets);
}
/// Gets an [`UnsafeWorldCell`] containing the underlying world.
///
/// # Safety
/// - must only be used to to make non-structural ECS changes
#[inline]
pub(crate) fn as_unsafe_world_cell(&mut self) -> UnsafeWorldCell {
self.world
}
}

View File

@ -4,16 +4,19 @@ use crate::{
change_detection::MutUntyped,
component::{Component, ComponentId, ComponentTicks, Components, StorageType},
entity::{Entities, Entity, EntityLocation},
event::Event,
observer::{Observer, Observers},
query::Access,
removal_detection::RemovedComponentEvents,
storage::Storages,
world::{Mut, World},
system::IntoObserverSystem,
world::{DeferredWorld, Mut, World},
};
use bevy_ptr::{OwningPtr, Ptr};
use std::{any::TypeId, marker::PhantomData};
use thiserror::Error;
use super::{unsafe_world_cell::UnsafeEntityCell, Ref};
use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE};
/// A read-only reference to a particular [`Entity`] and all of its components.
///
@ -876,6 +879,7 @@ impl<'w> EntityWorldMut<'w> {
&mut world.archetypes,
storages,
components,
&world.observers,
old_location.archetype_id,
bundle_info,
false,
@ -898,11 +902,14 @@ impl<'w> EntityWorldMut<'w> {
)
};
if old_archetype.has_on_remove() {
// SAFETY: All components in the archetype exist in world
unsafe {
deferred_world.trigger_on_remove(entity, bundle_info.iter_components());
}
// SAFETY: all bundle components exist in World
unsafe {
trigger_on_remove_hooks_and_observers(
&mut deferred_world,
old_archetype,
entity,
bundle_info,
);
}
let archetypes = &mut world.archetypes;
@ -1053,6 +1060,7 @@ impl<'w> EntityWorldMut<'w> {
&mut world.archetypes,
&mut world.storages,
&world.components,
&world.observers,
location.archetype_id,
bundle_info,
// components from the bundle that are not present on the entity are ignored
@ -1075,11 +1083,14 @@ impl<'w> EntityWorldMut<'w> {
)
};
if old_archetype.has_on_remove() {
// SAFETY: All components in the archetype exist in world
unsafe {
deferred_world.trigger_on_remove(entity, bundle_info.iter_components());
}
// SAFETY: all bundle components exist in World
unsafe {
trigger_on_remove_hooks_and_observers(
&mut deferred_world,
old_archetype,
entity,
bundle_info,
);
}
let old_archetype = &world.archetypes[location.archetype_id];
@ -1209,10 +1220,11 @@ impl<'w> EntityWorldMut<'w> {
(&*archetype, world.into_deferred())
};
if archetype.has_on_remove() {
// SAFETY: All components in the archetype exist in world
unsafe {
deferred_world.trigger_on_remove(self.entity, archetype.components());
// SAFETY: All components in the archetype exist in world
unsafe {
deferred_world.trigger_on_remove(archetype, self.entity, archetype.components());
if archetype.has_remove_observer() {
deferred_world.trigger_observers(ON_REMOVE, self.entity, archetype.components());
}
}
@ -1276,12 +1288,12 @@ impl<'w> EntityWorldMut<'w> {
world.archetypes[moved_location.archetype_id]
.set_entity_table_row(moved_location.archetype_row, table_row);
}
world.flush_commands();
world.flush();
}
/// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush_commands`]
/// Ensures any commands triggered by the actions of Self are applied, equivalent to [`World::flush`]
pub fn flush(self) -> Entity {
self.world.flush_commands();
self.world.flush();
self.entity
}
@ -1397,6 +1409,30 @@ impl<'w> EntityWorldMut<'w> {
})
}
}
/// Creates an [`Observer`](crate::observer::Observer) listening for events of type `E` targeting this entity.
/// In order to trigger the callback the entity must also match the query when the event is fired.
pub fn observe<E: Event, B: Bundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
self.world
.spawn(Observer::new(observer).with_entity(self.entity));
self
}
}
/// SAFETY: all components in the archetype must exist in world
unsafe fn trigger_on_remove_hooks_and_observers(
deferred_world: &mut DeferredWorld,
archetype: &Archetype,
entity: Entity,
bundle_info: &BundleInfo,
) {
deferred_world.trigger_on_remove(archetype, entity, bundle_info.iter_components());
if archetype.has_remove_observer() {
deferred_world.trigger_observers(ON_REMOVE, entity, bundle_info.iter_components());
}
}
/// A view into a single entity and component in a world, which may either be vacant or occupied.
@ -2292,6 +2328,7 @@ unsafe fn remove_bundle_from_archetype(
archetypes: &mut Archetypes,
storages: &mut Storages,
components: &Components,
observers: &Observers,
archetype_id: ArchetypeId,
bundle_info: &BundleInfo,
intersection: bool,
@ -2362,6 +2399,7 @@ unsafe fn remove_bundle_from_archetype(
let new_archetype_id = archetypes.get_id_or_insert(
components,
observers,
next_table_id,
next_table_components,
next_sparse_set_components,

View File

@ -1,20 +1,25 @@
//! Defines the [`World`] and APIs for accessing it directly.
pub(crate) mod command_queue;
mod component_constants;
mod deferred_world;
mod entity_ref;
pub mod error;
mod identifier;
mod spawn_batch;
pub mod unsafe_world_cell;
pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD};
pub use crate::world::command_queue::CommandQueue;
use crate::{component::ComponentInitializer, entity::EntityHashSet};
pub use crate::{
change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD},
world::command_queue::CommandQueue,
};
pub use component_constants::*;
pub use deferred_world::DeferredWorld;
pub use entity_ref::{
EntityMut, EntityRef, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef,
OccupiedEntry, VacantEntry,
};
pub use identifier::WorldId;
pub use spawn_batch::*;
use crate::{
@ -25,8 +30,9 @@ use crate::{
Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentInfo, ComponentTicks,
Components, Tick,
},
entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation},
entity::{AllocAtWithoutReplacement, Entities, Entity, EntityHashSet, EntityLocation},
event::{Event, EventId, Events, SendBatchIds},
observer::Observers,
query::{DebugCheckedUnwrap, QueryData, QueryEntityError, QueryFilter, QueryState},
removal_detection::RemovedComponentEvents,
schedule::{Schedule, ScheduleLabel, Schedules},
@ -43,10 +49,7 @@ use std::{
mem::MaybeUninit,
sync::atomic::{AtomicU32, Ordering},
};
mod identifier;
use self::unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell};
pub use identifier::WorldId;
use unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell};
/// A [`World`] mutation.
///
@ -110,30 +113,36 @@ pub struct World {
pub(crate) archetypes: Archetypes,
pub(crate) storages: Storages,
pub(crate) bundles: Bundles,
pub(crate) observers: Observers,
pub(crate) removed_components: RemovedComponentEvents,
pub(crate) change_tick: AtomicU32,
pub(crate) last_change_tick: Tick,
pub(crate) last_check_tick: Tick,
pub(crate) last_trigger_id: u32,
pub(crate) command_queue: RawCommandQueue,
}
impl Default for World {
fn default() -> Self {
Self {
let mut world = Self {
id: WorldId::new().expect("More `bevy` `World`s have been created than is supported"),
entities: Entities::new(),
components: Default::default(),
archetypes: Archetypes::new(),
storages: Default::default(),
bundles: Default::default(),
observers: Observers::default(),
removed_components: 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: Tick::new(0),
last_check_tick: Tick::new(0),
last_trigger_id: 0,
command_queue: RawCommandQueue::new(),
}
};
world.bootstrap();
world
}
}
@ -149,6 +158,14 @@ impl Drop for World {
}
impl World {
/// This performs initialization that _must_ happen for every [`World`] immediately upon creation (such as claiming specific component ids).
/// This _must_ be run as part of constructing a [`World`], before it is returned to the caller.
#[inline]
fn bootstrap(&mut self) {
assert_eq!(ON_ADD, self.init_component::<OnAdd>());
assert_eq!(ON_INSERT, self.init_component::<OnInsert>());
assert_eq!(ON_REMOVE, self.init_component::<OnRemove>());
}
/// Creates a new empty [`World`].
///
/// # Panics
@ -219,15 +236,6 @@ impl World {
&self.bundles
}
/// Creates a [`ComponentInitializer`] for this world.
#[inline]
pub fn component_initializer(&mut self) -> ComponentInitializer {
ComponentInitializer {
components: &mut self.components,
storages: &mut self.storages,
}
}
/// Retrieves this world's [`RemovedComponentEvents`] collection
#[inline]
pub fn removed_components(&self) -> &RemovedComponentEvents {
@ -235,7 +243,7 @@ impl World {
}
/// Creates a new [`Commands`] instance that writes to the world's command queue
/// Use [`World::flush_commands`] to apply all queued commands
/// Use [`World::flush`] to apply all queued commands
#[inline]
pub fn commands(&mut self) -> Commands {
// SAFETY: command_queue is stored on world and always valid while the world exists
@ -502,7 +510,7 @@ impl World {
/// scheme worked out to share an ID space (which doesn't happen by default).
#[inline]
pub fn get_or_spawn(&mut self, entity: Entity) -> Option<EntityWorldMut> {
self.flush_entities();
self.flush();
match self.entities.alloc_at_without_replacement(entity) {
AllocAtWithoutReplacement::Exists(location) => {
// SAFETY: `entity` exists and `location` is that entity's location
@ -895,7 +903,7 @@ impl World {
/// assert_eq!(position.x, 0.0);
/// ```
pub fn spawn_empty(&mut self) -> EntityWorldMut {
self.flush_entities();
self.flush();
let entity = self.entities.alloc();
// SAFETY: entity was just allocated
unsafe { self.spawn_at_empty_internal(entity) }
@ -961,7 +969,7 @@ impl World {
/// assert_eq!(position.x, 2.0);
/// ```
pub fn spawn<B: Bundle>(&mut self, bundle: B) -> EntityWorldMut {
self.flush_entities();
self.flush();
let change_tick = self.change_tick();
let entity = self.entities.alloc();
let entity_location = {
@ -1092,6 +1100,7 @@ impl World {
/// ```
#[inline]
pub fn despawn(&mut self, entity: Entity) -> bool {
self.flush();
if let Some(entity) = self.get_entity_mut(entity) {
entity.despawn();
true
@ -1113,9 +1122,9 @@ impl World {
/// By clearing this internal state, the world "forgets" about those changes, allowing a new round
/// of detection to be recorded.
///
/// When using `bevy_ecs` as part of the full Bevy engine, this method is added as a system to the
/// main app, to run during `Last`, so you don't need to call it manually. When using `bevy_ecs`
/// as a separate standalone crate however, you need to call this manually.
/// When using `bevy_ecs` as part of the full Bevy engine, this method is called automatically
/// by `bevy_app::App::update` and `bevy_app::SubApp::update`, so you don't need to call it manually.
/// When using `bevy_ecs` as a separate standalone crate however, you do need to call this manually.
///
/// ```
/// # use bevy_ecs::prelude::*;
@ -1743,7 +1752,7 @@ impl World {
I::IntoIter: Iterator<Item = (Entity, B)>,
B: Bundle,
{
self.flush_entities();
self.flush();
let change_tick = self.change_tick();
@ -2029,9 +2038,15 @@ impl World {
}
}
/// Calls both [`World::flush_entities`] and [`World::flush_commands`].
#[inline]
pub fn flush(&mut self) {
self.flush_entities();
self.flush_commands();
}
/// Applies any commands in the world's internal [`CommandQueue`].
/// This does not apply commands from any systems, only those stored in the world.
#[inline]
pub fn flush_commands(&mut self) {
// SAFETY: `self.command_queue` is only de-allocated in `World`'s `Drop`
if !unsafe { self.command_queue.is_empty() } {
@ -2082,6 +2097,13 @@ impl World {
self.last_change_tick
}
/// Returns the id of the last ECS event that was fired.
/// Used internally to ensure observers don't trigger multiple times for the same event.
#[inline]
pub(crate) fn last_trigger_id(&self) -> u32 {
self.last_trigger_id
}
/// Sets [`World::last_change_tick()`] to the specified value during a scope.
/// When the scope terminates, it will return to its old value.
///

View File

@ -27,7 +27,7 @@ where
pub(crate) fn new(world: &'w mut World, iter: I) -> Self {
// Ensure all entity allocations are accounted for so `self.entities` can realloc if
// necessary
world.flush_entities();
world.flush();
let change_tick = world.change_tick();

View File

@ -9,6 +9,7 @@ use crate::{
change_detection::{MutUntyped, Ticks, TicksMut},
component::{ComponentId, ComponentTicks, Components, StorageType, Tick, TickCells},
entity::{Entities, Entity, EntityLocation},
observer::Observers,
prelude::Component,
removal_detection::RemovedComponentEvents,
storage::{Column, ComponentSparseSet, Storages},
@ -231,6 +232,13 @@ impl<'w> UnsafeWorldCell<'w> {
&unsafe { self.world_metadata() }.removed_components
}
/// Retrieves this world's [`Observers`] collection.
pub(crate) unsafe fn observers(self) -> &'w Observers {
// SAFETY:
// - we only access world metadata
&unsafe { self.world_metadata() }.observers
}
/// Retrieves this world's [`Bundles`] collection.
#[inline]
pub fn bundles(self) -> &'w Bundles {
@ -571,6 +579,14 @@ impl<'w> UnsafeWorldCell<'w> {
// - caller ensures that we have permission to access the queue
unsafe { (*self.0).command_queue.clone() }
}
/// # Safety
/// It is the callers responsibility to ensure that there are no outstanding
/// references to `last_trigger_id`.
pub(crate) unsafe fn increment_trigger_id(self) {
// SAFETY: Caller ensure there are no outstanding references
unsafe { (*self.0).last_trigger_id += 1 }
}
}
impl Debug for UnsafeWorldCell<'_> {

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_encase_derive"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Bevy derive macro for encase"
homepage = "https://bevyengine.org"
@ -12,7 +12,7 @@ keywords = ["bevy"]
proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.0-dev" }
bevy_macro_utils = { path = "../bevy_macro_utils", version = "0.14.2" }
encase_derive_impl = "0.8"
[lints]

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gilrs"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Gamepad system made using Gilrs for Bevy Engine"
homepage = "https://bevyengine.org"
@ -10,11 +10,11 @@ keywords = ["bevy"]
[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_input = { path = "../bevy_input", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_time = { path = "../bevy_time", version = "0.14.2" }
# other
gilrs = "0.10.1"

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gizmos"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Provides gizmos for Bevy Engine"
homepage = "https://bevyengine.org"
@ -14,20 +14,20 @@ webgpu = []
[dependencies]
# Bevy
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0-dev", optional = true }
bevy_sprite = { path = "../bevy_sprite", version = "0.14.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_gizmos_macros = { path = "macros", version = "0.14.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.14.0-dev" }
bevy_pbr = { path = "../bevy_pbr", version = "0.14.2", optional = true }
bevy_sprite = { path = "../bevy_sprite", version = "0.14.2", optional = true }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_color = { path = "../bevy_color", version = "0.14.3" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_render = { path = "../bevy_render", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.2" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_gizmos_macros = { path = "macros", version = "0.14.2" }
bevy_time = { path = "../bevy_time", version = "0.14.2" }
bytemuck = "1.0"

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gizmos_macros"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Derive implementations for bevy_gizmos"
homepage = "https://bevyengine.org"
@ -13,7 +13,7 @@ proc-macro = true
[dependencies]
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" }
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.2" }
syn = "2.0"
proc-macro2 = "1.0"

View File

@ -575,11 +575,16 @@ impl<P: PhaseItem> RenderCommand<P> for DrawLineGizmo {
}
let instances = if line_gizmo.strip {
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..));
let item_size = VertexFormat::Float32x3.size();
let buffer_size = line_gizmo.position_buffer.size() - item_size;
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..));
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..));
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size));
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(item_size..));
let item_size = VertexFormat::Float32x4.size();
let buffer_size = line_gizmo.color_buffer.size() - item_size;
pass.set_vertex_buffer(2, line_gizmo.color_buffer.slice(..buffer_size));
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..));
u32::max(line_gizmo.vertex_count, 1) - 1
} else {
@ -625,11 +630,24 @@ impl<P: PhaseItem> RenderCommand<P> for DrawLineJointGizmo {
};
let instances = {
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..));
pass.set_vertex_buffer(1, line_gizmo.position_buffer.slice(..));
pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(..));
let item_size = VertexFormat::Float32x3.size();
// position_a
let buffer_size_a = line_gizmo.position_buffer.size() - item_size * 2;
pass.set_vertex_buffer(0, line_gizmo.position_buffer.slice(..buffer_size_a));
// position_b
let buffer_size_b = line_gizmo.position_buffer.size() - item_size;
pass.set_vertex_buffer(
1,
line_gizmo.position_buffer.slice(item_size..buffer_size_b),
);
// position_c
pass.set_vertex_buffer(2, line_gizmo.position_buffer.slice(item_size * 2..));
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(..));
// color
let item_size = VertexFormat::Float32x4.size();
let buffer_size = line_gizmo.color_buffer.size() - item_size;
// This corresponds to the color of position_b, hence starts from `item_size`
pass.set_vertex_buffer(3, line_gizmo.color_buffer.slice(item_size..buffer_size));
u32::max(line_gizmo.vertex_count, 2) - 2
};
@ -674,13 +692,11 @@ fn line_gizmo_vertex_buffer_layouts(strip: bool) -> Vec<VertexBufferLayout> {
position_layout.clone(),
{
position_layout.attributes[0].shader_location = 1;
position_layout.attributes[0].offset = Float32x3.size();
position_layout
},
color_layout.clone(),
{
color_layout.attributes[0].shader_location = 3;
color_layout.attributes[0].offset = Float32x4.size();
color_layout
},
]
@ -720,7 +736,7 @@ fn line_joint_gizmo_vertex_buffer_layouts() -> Vec<VertexBufferLayout> {
step_mode: VertexStepMode::Instance,
attributes: vec![VertexAttribute {
format: Float32x4,
offset: Float32x4.size(),
offset: 0,
shader_location: 3,
}],
};
@ -729,12 +745,10 @@ fn line_joint_gizmo_vertex_buffer_layouts() -> Vec<VertexBufferLayout> {
position_layout.clone(),
{
position_layout.attributes[0].shader_location = 1;
position_layout.attributes[0].offset = Float32x3.size();
position_layout.clone()
},
{
position_layout.attributes[0].shader_location = 2;
position_layout.attributes[0].offset = 2 * Float32x3.size();
position_layout
},
color_layout.clone(),

View File

@ -74,8 +74,8 @@ fn vertex(vertex: VertexInput) -> VertexOutput {
let near_clipping_plane_height = length(pos0.xyz - pos1.xyz);
// We can't use vertex.position_X because we may have changed the clip positions with clip_near_plane
let position_a = view.inverse_clip_from_world * clip_a;
let position_b = view.inverse_clip_from_world * clip_b;
let position_a = view.world_from_clip * clip_a;
let position_b = view.world_from_clip * clip_b;
let world_distance = length(position_a.xyz - position_b.xyz);
// Offset to compensate for moved clip positions. If removed dots on lines will slide when position a is ofscreen.

View File

@ -54,7 +54,9 @@ impl Plugin for LineGizmo2dPlugin {
)
.add_systems(
Render,
// FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed
(queue_line_gizmos_2d, queue_line_joint_gizmos_2d)
.chain()
.in_set(GizmoRenderSystem::QueueLineGizmos2d)
.after(prepare_assets::<GpuLineGizmo>),
);

View File

@ -53,7 +53,9 @@ impl Plugin for LineGizmo3dPlugin {
)
.add_systems(
Render,
// FIXME: added `chain()` to workaround vertex buffer being not updated when sliced size changed
(queue_line_gizmos_3d, queue_line_joint_gizmos_3d)
.chain()
.in_set(GizmoRenderSystem::QueueLineGizmos3d)
.after(prepare_assets::<GpuLineGizmo>),
);

View File

@ -1,6 +1,6 @@
[package]
name = "bevy_gltf"
version = "0.14.0-dev"
version = "0.14.2"
edition = "2021"
description = "Bevy Engine GLTF loading"
homepage = "https://bevyengine.org"
@ -9,32 +9,35 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]
[features]
dds = ["bevy_render/dds"]
dds = ["bevy_render/dds", "bevy_core_pipeline/dds"]
pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"]
pbr_multi_layer_material_textures = []
pbr_multi_layer_material_textures = [
"bevy_pbr/pbr_multi_layer_material_textures",
]
pbr_anisotropy_texture = ["bevy_pbr/pbr_anisotropy_texture"]
[dependencies]
# bevy
bevy_animation = { path = "../bevy_animation", version = "0.14.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.14.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.14.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.14.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.14.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.14.0-dev" }
bevy_pbr = { path = "../bevy_pbr", version = "0.14.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", features = [
bevy_animation = { path = "../bevy_animation", version = "0.14.2", optional = true }
bevy_app = { path = "../bevy_app", version = "0.14.2" }
bevy_asset = { path = "../bevy_asset", version = "0.14.2" }
bevy_color = { path = "../bevy_color", version = "0.14.3" }
bevy_core = { path = "../bevy_core", version = "0.14.2" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.14.2" }
bevy_ecs = { path = "../bevy_ecs", version = "0.14.2" }
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.2" }
bevy_math = { path = "../bevy_math", version = "0.14.2" }
bevy_pbr = { path = "../bevy_pbr", version = "0.14.2" }
bevy_reflect = { path = "../bevy_reflect", version = "0.14.2", features = [
"bevy",
] }
bevy_render = { path = "../bevy_render", version = "0.14.0-dev" }
bevy_scene = { path = "../bevy_scene", version = "0.14.0-dev", features = [
bevy_render = { path = "../bevy_render", version = "0.14.2" }
bevy_scene = { path = "../bevy_scene", version = "0.14.2", features = [
"bevy_render",
] }
bevy_transform = { path = "../bevy_transform", version = "0.14.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.14.2" }
bevy_tasks = { path = "../bevy_tasks", version = "0.14.2" }
bevy_utils = { path = "../bevy_utils", version = "0.14.2" }
# other
gltf = { version = "1.4.0", default-features = false, features = [

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