Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use async-fn-in-trait (RPIT) for various Asset traits. #11362

Closed
wants to merge 296 commits into from

Conversation

ArthurBrussee
Copy link
Contributor

@ArthurBrussee ArthurBrussee commented Jan 15, 2024

(First OSS contribution, so apologies if I mess up the format, roll-ups, or err the rest! Picked a random issue to see what it's like)

Objective

Simplify implementing some asset traits without Box::pin(async move{}) shenanigans.

Fixes (in part) #11308

Solution

Use async-fn in traits when possible in all traits. Traits with return position impl trait are not object safe however, and as AssetReader and AssetWriter are both used with dynamic dispatch, you need a Boxed version of these futures anyway.

In the future, Rust is adding proc macros to generate these traits automatically, and at some point in the future dyn traits should 'just work'. Until then.... this seemed liked the right approach given more ErasedXXX already exist, but, no clue if there's plans here! Especially since these are public now, it's a bit of an unfortunate API, and means this is a breaking change.

In theory this saves some performance when these traits are used with static dispatch, but, seems like most code paths go through dynamic dispatch which boxes anyway.


Changelog

  • AssetReader, AssetWriter, AssetLoader, AssetSaver and Process now use async-fn in traits, so they're easier to implement.

Migration Guide

  • Custom implementations of AssetReader, AssetWriter, AssetLoader, AssetSaver and Process should switch to async fn rather than returning bevy_utils::BoxedFuture. Simultaniously, to use dynamic dispatch on these traits you should instead use dyn ErasedXXX.

Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@JMS55
Copy link
Contributor

JMS55 commented Jan 15, 2024

Note for reviewers: Much easier to review if you hide the whitespace diff.

@JMS55 JMS55 added A-Assets Load files from disk to use for things like images, models, and sounds C-Code-Quality A section of code that is hard to understand or change labels Jan 15, 2024
Copy link
Contributor

Your PR increases Bevy Minimum Supported Rust Version. Please update the rust-version field in the root Cargo.toml file.

@ArthurBrussee
Copy link
Contributor Author

Ah didn't realise Bevy MSRV isn't 1.75 yet, that makes this a bit pointless.

Also WASM doesn't build - trait fns need to be optionally Send, only on non-wasm platforms. That's... tricky. Maybe too early for async-fn-in-trait after all, but, will do some reading on what the state of things is there currently.

crates/bevy_asset/src/io/mod.rs Outdated Show resolved Hide resolved
@JMS55
Copy link
Contributor

JMS55 commented Jan 15, 2024

Ah didn't realise Bevy MSRV isn't 1.75 yet, that makes this a bit pointless.

Shouldn't be a blocker, I think we can bump it.

Copy link
Contributor

Your PR increases Bevy Minimum Supported Rust Version. Please update the rust-version field in the root Cargo.toml file.

1 similar comment
Copy link
Contributor

Your PR increases Bevy Minimum Supported Rust Version. Please update the rust-version field in the root Cargo.toml file.

@ArthurBrussee
Copy link
Contributor Author

Ah didn't realise Bevy MSRV isn't 1.75 yet, that makes this a bit pointless.

Shouldn't be a blocker, I think we can bump it.

Oh ok great! Would that be a seperate PR or included in this? Given the insistent bot I've added it in here.

That leaves the WASM problem. The recap, mainly for myself:

  • impl Future<> functions can (usually) be Send fine, as by design RPIT allows trait bounds to be inferred
  • However, for dynamic dispatch you need to KNOW the future is Send,. Hence the boxed future has an explicit Send bound.
  • On WASM however, wasm_bindgen uses JsPromise objects for futures(?) which are NOT send.
  • Bevy's boxed future is only Send on non-wasm platforms.
  • For impl Future<> however, we now need a way to specify MaybeSend for each function....

The potential solutions as far as I can see:

So yeah, not sure, might have hit a dead end for wasm!

@JMS55
Copy link
Contributor

JMS55 commented Jan 17, 2024

This PR is fine for bumping MSRV.

I don't have time to think about WASM atm, will let someone else respond. Feel free to join the -dev channels in discord (engine-dev/asset-dev) and brainstorm solutions with other people though :)

@mockersf
Copy link
Member

No issues to update the MSRV, this is usually done in the first PR that needs the new version of Rust.

I would love to have this, but I also think this is blocked for Wasm for now 🙁

@ArthurBrussee
Copy link
Contributor Author

So it seems there is a way to make conditional Send bounds, and more people do it this way. Have made an attempt with a "FutureSend" (could be MaybeSend? ConditionalSend?) that seems to work for wasm!

Also tried to understand where this problem really comes from, and it basically comes down to: wasm_bindgen uses JS promises which aren't Send for futures for web APIs, including Fetch, which is why the HttpWasmAssetReader can't have Send futures. That really seems to be the only reason wasm needs this special treatment, though of course that propogates all the way through.

Let me know if this seems workable, still happy to say this is getting too weird and it should wait for rust to stabilise more async features, or idk use #[async_trait] instead and call it a day haha.

Thanks!

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on board for these changes! Love the simplified AssetLoader interface.

The FutureSend pattern doesn't bother me at all. It feels like a natural extension of the BoxedFuture approach we're using. It is also just an "internal" change to the trait declaration. I like that users don't need to think about it.

The only place this feels like a potential downgrade to me is ErasedAssetReader/Writer, and the need for them to show up in a few public apis in place of AssetReader. The trait split allows for async syntax when implementing AssetReaders/Writers. This is an ergonomics win, but implementing the traits is largely something that we do internally. It is a very uncommon thing for a user to do. Given that the split comes at the cost of additional complexity for consumers of the traits (which feels like it could be more common), that does feel like a high price to pay. However in practice, these traits are largely abstracted out from consumers. In general I feel neutral on this change, so I'm happy to accept it as it is.

basis_universal::ColorSpace::Linear

let compressed_basis_data = {
// PERF: this should live inside the future, but CompressorParams and Compressor are not Send / can't be owned by the BoxedFuture (which _is_ Send)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove this as this does live "inside" the future now. Intersting that this pattern works!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opps nice catch! And yeah annoying that the extra scope is needed (known problem appearantly) but is cool that it works at all! Maybe future borrow checker one day :)

@ArthurBrussee
Copy link
Contributor Author

Thanks a lot for that! Really nice to hear your design thoughts :) Great that the Wasm solution sounds workable! I've renamed FutureSend to WasmNotSend as per giooschi's suggestion on Discord to match WGPU.

And yeah ErasedX is the main actual visible usability downside... Good point that that's maybe more what is exposed to the user even :/ My hope was that since a bunch of the other Asset traits already have these it's not a whole other concep to lear.

Really annoying that #[async-trait] CAN do without this but 'native' RPITIT can't! From yosh & boats blog seems the async-WG thinks dyn + async traits might happen in 2024, and could get rid of all these wrappers.

@cart
Copy link
Member

cart commented Jan 23, 2024

WasmNotSend

Not a huge deal, but I'd prefer it if we made the naming a bit more generic to avoid having "wasm" in the function signature name. I think the trait name should make sense with or without the wasm context.

The module name seems like a good choice: ConditionalSend

@ArthurBrussee
Copy link
Contributor Author

Oh neat! Yeah prefer that too, made it ConditionalSend now. There could even be a generic feature for bevy tasks to support a work-stealing scheduler or not (which then just isn't supported on wasm).

@ArthurBrussee
Copy link
Contributor Author

Have updated to latest main in case this is still the way to go :) Some weird CI failures atm that I think are unrelated, other PRs seem to have the same.

Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice changes! The ConditionalSend trait is also a nice trick.

crates/bevy_utils/src/lib.rs Outdated Show resolved Hide resolved
andristarr and others added 9 commits February 6, 2024 18:04
# Objective

- The exported hashtypes are just re-exports from hashbrown, we want to
drop that dependency and (in the future) let the user import their own
choice.
- Fixes bevyengine#11717

## Solution

- Adding a deprecated tag on the re-exports, so in future releases these
can be safely removed.
# Objective

Split up from bevyengine#11007, fixing most of the remaining work for bevyengine#10569.

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`. This covers all shapes that Bevy has mesh
structs for in `bevy_render::mesh::shapes`.

`Cone` and `ConicalFrustum` are new shapes, so I can add them in a
follow-up, or I could just add them here directly if that's preferrable.

## Solution

Implement `Meshable` for `Cuboid`, `Sphere`, `Cylinder`, `Capsule`,
`Torus`, and `Plane3d`.

The logic is mostly just a copy of the the existing `bevy_render`
shapes, but `Plane3d` has a configurable surface normal that affects the
orientation. Some property names have also been changed to be more
consistent.

The default values differ from the old shapes to make them a bit more
logical:

- Spheres now have a radius of 0.5 instead of 1.0. The default capsule
is equivalent to the default cylinder with the sphere's halves glued on.
- The inner and outer radius of the torus are now 0.5 and 1.0 instead of
0.5 and 1.5 (i.e. the new minor and major radii are 0.25 and 0.75). It's
double the width of the default cuboid, half of its height, and the
default sphere matches the size of the hole.
- `Cuboid` is 1x1x1 by default unlike the dreaded `Box` which is 2x1x1.

Before, with "old" shapes:


![old](https://github.com/bevyengine/bevy/assets/57632562/733f3dda-258c-4491-8152-9829e056a1a3)

Now, with primitive meshing:


![new](https://github.com/bevyengine/bevy/assets/57632562/5a1af14f-bb98-401d-82cf-de8072fea4ec)

I only changed the `3d_shapes` example to use primitives for now. I can
change them all in this PR or a follow-up though, whichever way is
preferrable.

### Sphere API

Spheres have had separate `Icosphere` and `UVSphere` structs, but with
primitives we only have one `Sphere`.

We need to handle this with builders:

```rust
// Existing structs
let ico = Mesh::try_from(Icophere::default()).unwrap();
let uv = Mesh::from(UVSphere::default());

// Primitives
let ico = Sphere::default().mesh().ico(5).unwrap();
let uv = Sphere::default().mesh().uv(32, 18);
```

We could add methods on `Sphere` directly to skip calling `.mesh()`.

I also added a `SphereKind` enum that can be used with the `kind`
method:

```rust
let ico = Sphere::default()
    .mesh()
    .kind(SphereKind::Ico { subdivisions: 8 })
    .build();
```

The default mesh for a `Sphere` is an icosphere with 5 subdivisions
(like the default `Icosphere`).

---

## Changelog

- Implement `Meshable` and `Default` for `Cuboid`, `Sphere`, `Cylinder`,
`Capsule`, `Torus`, and `Plane3d`
- Use primitives in `3d_shapes` example

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
# Objective

Bevy could benefit from *irradiance volumes*, also known as *voxel
global illumination* or simply as light probes (though this term is not
preferred, as multiple techniques can be called light probes).
Irradiance volumes are a form of baked global illumination; they work by
sampling the light at the centers of each voxel within a cuboid. At
runtime, the voxels surrounding the fragment center are sampled and
interpolated to produce indirect diffuse illumination.

## Solution

This is divided into two sections. The first is copied and pasted from
the irradiance volume module documentation and describes the technique.
The second part consists of notes on the implementation.

### Overview

An *irradiance volume* is a cuboid voxel region consisting of
regularly-spaced precomputed samples of diffuse indirect light. They're
ideal if you have a dynamic object such as a character that can move
about
static non-moving geometry such as a level in a game, and you want that
dynamic object to be affected by the light bouncing off that static
geometry.

To use irradiance volumes, you need to precompute, or *bake*, the
indirect
light in your scene. Bevy doesn't currently come with a way to do this.
Fortunately, [Blender] provides a [baking tool] as part of the Eevee
renderer, and its irradiance volumes are compatible with those used by
Bevy.
The [`bevy-baked-gi`] project provides a tool, `export-blender-gi`, that
can
extract the baked irradiance volumes from the Blender `.blend` file and
package them up into a `.ktx2` texture for use by the engine. See the
documentation in the `bevy-baked-gi` project for more details as to this
workflow.

Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes that
can
be arbitrarily scaled, rotated, and positioned in a scene with the
[`bevy_transform::components::Transform`] component. The 3D voxel grid
will
be stretched to fill the interior of the cube, and the illumination from
the
irradiance volume will apply to all fragments within that bounding
region.

Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used
in
*Half-Life 2* ([Mitchell 2006], slide 27). These encode a single color
of
light from the six 3D cardinal directions and blend the sides together
according to the surface normal.

The primary reason for choosing ambient cubes is to match Blender, so
that
its Eevee renderer can be used for baking. However, they also have some
advantages over the common second-order spherical harmonics approach:
ambient cubes don't suffer from ringing artifacts, they are smaller (6
colors for ambient cubes as opposed to 9 for spherical harmonics), and
evaluation is faster. A smaller basis allows for a denser grid of voxels
with the same storage requirements.

If you wish to use a tool other than `export-blender-gi` to produce the
irradiance volumes, you'll need to pack the irradiance volumes in the
following format. The irradiance volume of resolution *(Rx, Ry, Rz)* is
expected to be a 3D texture of dimensions *(Rx, 2Ry, 3Rz)*. The
unnormalized
texture coordinate *(s, t, p)* of the voxel at coordinate *(x, y, z)*
with
side *S* ∈ *{-X, +X, -Y, +Y, -Z, +Z}* is as follows:

```text
s = x

t = y + ⎰  0 if S ∈ {-X, -Y, -Z}
        ⎱ Ry if S ∈ {+X, +Y, +Z}

        ⎧   0 if S ∈ {-X, +X}
p = z + ⎨  Rz if S ∈ {-Y, +Y}
        ⎩ 2Rz if S ∈ {-Z, +Z}
```

Visually, in a left-handed coordinate system with Y up, viewed from the
right, the 3D texture looks like a stacked series of voxel grids, one
for
each cube side, in this order:

| **+X** | **+Y** | **+Z** |
| ------ | ------ | ------ |
| **-X** | **-Y** | **-Z** |

A terminology note: Other engines may refer to irradiance volumes as
*voxel
global illumination*, *VXGI*, or simply as *light probes*. Sometimes
*light
probe* refers to what Bevy calls a reflection probe. In Bevy, *light
probe*
is a generic term that encompasses all cuboid bounding regions that
capture
indirect illumination, whether based on voxels or not.

Note that, if binding arrays aren't supported (e.g. on WebGPU or WebGL
2),
then only the closest irradiance volume to the view will be taken into
account during rendering.

[*ambient cubes*]:
https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf

[Mitchell 2006]:
https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf

[Blender]: http://blender.org/

[baking tool]:
https://docs.blender.org/manual/en/latest/render/eevee/render_settings/indirect_lighting.html

[`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi

### Implementation notes

This patch generalizes light probes so as to reuse as much code as
possible between irradiance volumes and the existing reflection probes.
This approach was chosen because both techniques share numerous
similarities:

1. Both irradiance volumes and reflection probes are cuboid bounding
regions.
2. Both are responsible for providing baked indirect light.
3. Both techniques involve presenting a variable number of textures to
the shader from which indirect light is sampled. (In the current
implementation, this uses binding arrays.)
4. Both irradiance volumes and reflection probes require gathering and
sorting probes by distance on CPU.
5. Both techniques require the GPU to search through a list of bounding
regions.
6. Both will eventually want to have falloff so that we can smoothly
blend as objects enter and exit the probes' influence ranges. (This is
not implemented yet to keep this patch relatively small and reviewable.)

To do this, we generalize most of the methods in the reflection probes
patch bevyengine#11366 to be generic over a trait, `LightProbeComponent`. This
trait is implemented by both `EnvironmentMapLight` (for reflection
probes) and `IrradianceVolume` (for irradiance volumes). Using a trait
will allow us to add more types of light probes in the future. In
particular, I highly suspect we will want real-time reflection planes
for mirrors in the future, which can be easily slotted into this
framework.

## Changelog

> This section is optional. If this was a trivial fix, or has no
externally-visible impact, you can delete this section.

### Added
* A new `IrradianceVolume` asset type is available for baked voxelized
light probes. You can bake the global illumination using Blender or
another tool of your choice and use it in Bevy to apply indirect
illumination to dynamic objects.
# Objective

- System `create_surfaces` needs to happen before `prepare_windows` or
we lose one frame at startup

## Solution

- Specify the ordering, remove the set as it doesn't mean anything there
# Objective

- Fixes bevyengine#11740 

## Solution

- Turned `Mesh::set_indices` into `Mesh::insert_indices` and added
related methods for completeness.

---

## Changelog

- Replaced `Mesh::set_indices(indices: Option<Indices>)` with
`Mesh::insert_indices(indices: Indices)`
- Replaced `Mesh::with_indices(indices: Option<Indices>)` with
`Mesh::with_inserted_indices(indices: Indices)` and
`Mesh::with_removed_indices()` mirroring the API for inserting /
removing attributes.
- Updated the examples and internal uses of the APIs described above.

## Migration Guide

- Use `Mesh::insert_indices` or `Mesh::with_inserted_indices` instead of
`Mesh::set_indices` / `Mesh::with_indices`.
- If you have passed `None` to `Mesh::set_indices` or
`Mesh::with_indices` you should use `Mesh::remove_indices` or
`Mesh::with_removed_indices` instead.

---------

Co-authored-by: François <mockersf@gmail.com>
# Objective

Fix bevyengine#11657

## Solution

Add a `ReflectKind` enum, add `Reflect::reflect_kind` which returns a
`ReflectKind`, and add `kind` method implementions to `ReflectRef`,
`ReflectMut`, and `ReflectOwned`, which returns a `ReflectKind`.

I also changed `AccessError` to use this new struct instead of it's own
`TypeKind` struct.

---

## Changelog

- Added `ReflectKind`, an enumeration over the kinds of a reflected type
without its data.
- Added `Reflect::reflect_kind` (with default implementation)
- Added implementation for the `kind` method on `ReflectRef`,
`ReflectMut`, and `ReflectOwned` which gives their kind without any
information, as a `ReflectKind`
… views. (bevyengine#11751)

Don't try to create a uniform buffer for light probes if there are no
views.

Fixes the panic on examples that have no views, such as
`touch_input_events`.
…1758)

# Objective

- This aims to fix bevyengine#11755
- After bevyengine#10812 some pipeline compilation can take more time than before
and all call to `get_render_pipeline` should check the result.

## Solution

- Check `get_render_pipeline` call result for msaa_writeback
- I checked that no other call to `get_render_pipeline` in bevy code
base is missng the checking on the result.
[`ScheduleLabel`] derive macro uses "ScheduleName" as the trait name by
mistake. This only affects the error message when a user tries to use
the derive macro on a union type. No other code is affected.
@ArthurBrussee
Copy link
Contributor Author

Wellll seems like I fucked the merge pretty bad :') I'll redo this PR at head, it was getting a bit messy anyway.

@ArthurBrussee ArthurBrussee deleted the async-post-RPIT branch March 2, 2024 23:26
github-merge-queue bot pushed a commit that referenced this pull request Mar 18, 2024
# Objective

Simplify implementing some asset traits without Box::pin(async move{})
shenanigans.
Fixes (in part) #11308

## Solution
Use async-fn in traits when possible in all traits. Traits with return
position impl trait are not object safe however, and as AssetReader and
AssetWriter are both used with dynamic dispatch, you need a Boxed
version of these futures anyway.

In the future, Rust is [adding
](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html)proc
macros to generate these traits automatically, and at some point in the
future dyn traits should 'just work'. Until then.... this seemed liked
the right approach given more ErasedXXX already exist, but, no clue if
there's plans here! Especially since these are public now, it's a bit of
an unfortunate API, and means this is a breaking change.

In theory this saves some performance when these traits are used with
static dispatch, but, seems like most code paths go through dynamic
dispatch, which boxes anyway.

I also suspect a bunch of the lifetime annotations on these function
could be simplified now as the BoxedFuture was often the only thing
returned which needed a lifetime annotation, but I'm not touching that
for now as traits + lifetimes can be so tricky.

This is a revival of
[pull/11362](#11362) after a
spectacular merge f*ckup, with updates to the latest Bevy. Just to recap
some discussion:
- Overall this seems like a win for code quality, especially when
implementing these traits, but a loss for having to deal with ErasedXXX
variants.
- `ConditionalSend` was the preferred name for the trait that might be
Send, to deal with wasm platforms.
- When reviewing be sure to disable whitespace difference, as that's 95%
of the PR.


## Changelog
- AssetReader, AssetWriter, AssetLoader, AssetSaver and Process now use
async-fn in traits rather than boxed futures.

## Migration Guide
- Custom implementations of AssetReader, AssetWriter, AssetLoader,
AssetSaver and Process should switch to async fn rather than returning a
bevy_utils::BoxedFuture.
- Simultaniously, to use dynamic dispatch on these traits you should
instead use dyn ErasedXXX.
dekirisu pushed a commit to dekirisu/bevy_gltf_trait that referenced this pull request Jul 7, 2024
# Objective

Simplify implementing some asset traits without Box::pin(async move{})
shenanigans.
Fixes (in part) bevyengine/bevy#11308

## Solution
Use async-fn in traits when possible in all traits. Traits with return
position impl trait are not object safe however, and as AssetReader and
AssetWriter are both used with dynamic dispatch, you need a Boxed
version of these futures anyway.

In the future, Rust is [adding
](https://blog.rust-lang.org/2023/12/21/async-fn-rpit-in-traits.html)proc
macros to generate these traits automatically, and at some point in the
future dyn traits should 'just work'. Until then.... this seemed liked
the right approach given more ErasedXXX already exist, but, no clue if
there's plans here! Especially since these are public now, it's a bit of
an unfortunate API, and means this is a breaking change.

In theory this saves some performance when these traits are used with
static dispatch, but, seems like most code paths go through dynamic
dispatch, which boxes anyway.

I also suspect a bunch of the lifetime annotations on these function
could be simplified now as the BoxedFuture was often the only thing
returned which needed a lifetime annotation, but I'm not touching that
for now as traits + lifetimes can be so tricky.

This is a revival of
[pull/11362](bevyengine/bevy#11362) after a
spectacular merge f*ckup, with updates to the latest Bevy. Just to recap
some discussion:
- Overall this seems like a win for code quality, especially when
implementing these traits, but a loss for having to deal with ErasedXXX
variants.
- `ConditionalSend` was the preferred name for the trait that might be
Send, to deal with wasm platforms.
- When reviewing be sure to disable whitespace difference, as that's 95%
of the PR.


## Changelog
- AssetReader, AssetWriter, AssetLoader, AssetSaver and Process now use
async-fn in traits rather than boxed futures.

## Migration Guide
- Custom implementations of AssetReader, AssetWriter, AssetLoader,
AssetSaver and Process should switch to async fn rather than returning a
bevy_utils::BoxedFuture.
- Simultaniously, to use dynamic dispatch on these traits you should
instead use dyn ErasedXXX.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Assets Load files from disk to use for things like images, models, and sounds C-Code-Quality A section of code that is hard to understand or change
Projects
None yet
Development

Successfully merging this pull request may close these issues.