-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
bevy_reflect: Reflection-based cloning #13432
base: main
Are you sure you want to change the base?
Conversation
fn parse_clone(&mut self, input: ParseStream) -> syn::Result<()> { | ||
let ident = input.parse::<kw::Clone>()?; | ||
|
||
if input.peek(token::Paren) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we should use darling
as a helper crate to parse attributes?
It seems easier than what you're doing, especially if the attributes become more complex later on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could utilize something like darling
in the future (not this PR though). I'm not sure our parsing logic is so complex we really need it, but it's certainly worth considering.
@@ -509,6 +533,24 @@ impl ContainerAttributes { | |||
} | |||
} | |||
|
|||
pub fn get_clone_impl(&self, bevy_reflect_path: &Path) -> Option<proc_macro2::TokenStream> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is this used? I couldn't understand this part.
I get the clone_impl
on struct,tuple,enum,value,etc. but not this.
Or is this the top-level impl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used by derive_data.rs
. I could probably move this logic into that file directly, since it's the only place where it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, I like the change!
I think the naming will be clearer when the PartialReflect/UniqueReflect PR gets merged as well.
Had some small comments, and i'll probably wait for the blocking PR to be merged before approving
0a05f93
to
51f9b2d
Compare
51f9b2d
to
ad1146e
Compare
# Objective Fix #10284. ## Solution When `DynamicSceneBuilder` extracts entities, they are cloned via `PartialReflect::clone_value`, making them into dynamic versions of the original components. This loses any custom `ReflectSerialize` type data. Dynamic scenes are deserialized with the original types, not the dynamic versions, and so any component with a custom serialize may fail. In this case `Rect` and `Vec2`. The dynamic version includes the field names 'x' and 'y' but the `Serialize` impl doesn't, hence the "expect float" error. The solution here: Instead of using `clone_value` to clone the components, `FromReflect` clones and retains the original information needed to serialize with any custom `Serialize` impls. I think using something like `reflect_clone` from (#13432) might make this more efficient. I also did the same when deserializing dynamic scenes to appease some of the round-trip tests which use `ReflectPartialEq`, which requires the types be the same and not a unique/proxy pair. I'm not sure it's otherwise necessary. Maybe this would also be more efficient when spawning dynamic scenes with `reflect_clone` instead of `FromReflect` again? An alternative solution would be to fall back to the dynamic version when deserializing `DynamicScene`s if the custom version fails. I think that's possible. Or maybe simply always deserializing via the dynamic route for dynamic scenes? ## Testing This example is similar to the original test case in #10284: ``` rust #![allow(missing_docs)] use bevy::{prelude::*, scene::SceneInstanceReady}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, (save, load).chain()) .observe(check) .run(); } static SAVEGAME_SAVE_PATH: &str = "savegame.scn.ron"; fn save(world: &mut World) { let entity = world.spawn(OrthographicProjection::default()).id(); let scene = DynamicSceneBuilder::from_world(world) .extract_entity(entity) .build(); if let Some(registry) = world.get_resource::<AppTypeRegistry>() { let registry = registry.read(); let serialized_scene = scene.serialize(®istry).unwrap(); // println!("{}", serialized_scene); std::fs::write(format!("assets/{SAVEGAME_SAVE_PATH}"), serialized_scene).unwrap(); } world.entity_mut(entity).despawn_recursive(); } fn load(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(DynamicSceneBundle { scene: asset_server.load(SAVEGAME_SAVE_PATH), ..default() }); } fn check(_trigger: Trigger<SceneInstanceReady>, query: Query<&OrthographicProjection>) { dbg!(query.single()); } ``` ## Migration Guide The `DynamicScene` format is changed to use custom serialize impls so old scene files will need updating: Old: ```ron ( resources: {}, entities: { 4294967299: ( components: { "bevy_render::camera::projection::OrthographicProjection": ( near: 0.0, far: 1000.0, viewport_origin: ( x: 0.5, y: 0.5, ), scaling_mode: WindowSize(1.0), scale: 1.0, area: ( min: ( x: -1.0, y: -1.0, ), max: ( x: 1.0, y: 1.0, ), ), ), }, ), }, ) ``` New: ```ron ( resources: {}, entities: { 4294967299: ( components: { "bevy_render::camera::projection::OrthographicProjection": ( near: 0.0, far: 1000.0, viewport_origin: (0.5, 0.5), scaling_mode: WindowSize(1.0), scale: 1.0, area: ( min: (-1.0, -1.0), max: (1.0, 1.0), ), ), }, ), }, ) ``` --------- Co-authored-by: Gino Valente <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very good PR, I just have some small questions :D
return #FQResult::Err( | ||
#bevy_reflect_path::ReflectCloneError::FieldNotClonable { | ||
field: #field_id, | ||
variant: #FQOption::Some(::std::borrow::Cow::Borrowed(#variant_name)), | ||
container_type_path: ::std::borrow::Cow::Borrowed(<Self as #bevy_reflect_path::TypePath>::type_path()) | ||
} | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood it right, if we have an ignored field, we can only fallback to Derive impl, right?
@@ -4,15 +4,15 @@ use bevy_reflect_derive::{impl_reflect, impl_reflect_value}; | |||
use glam::*; | |||
|
|||
impl_reflect!( | |||
#[reflect(Debug, Hash, PartialEq, Default)] | |||
#[reflect(Clone, Debug, Hash, PartialEq, Default)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Should we use #[reflect(Clone)] Whenever possible?
/// Unlike [`Reflect::clone_value`], which often returns a dynamic representation of `Self`, | ||
/// this method attempts create a clone of `Self` directly, if possible. | ||
/// | ||
/// If the clone cannot be performed, `None` is returned. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we must warn the user which cases can return None
From what I read in this PR, this returns None if Clone isn't implemented for all types, but when I first read the PR description, this left me very confused: How can Clone fail?
# Objective #16132 introduced entity cloning functionality, and while it works and is useful, it can be made faster. This is the promised follow-up to improve performance. ## Solution **PREFACE**: This is my first time writing `unsafe` in rust and I have only vague idea about what I'm doing. I would encourage reviewers to scrutinize `unsafe` parts in particular. The solution is to clone component data to an intermediate buffer and use `EntityWorldMut::insert_by_ids` to insert components without additional archetype moves. To facilitate this, `EntityCloner::clone_entity` now reads all components of the source entity and provides clone handlers with the ability to read component data straight from component storage using `read_source_component` and write to an intermediate buffer using `write_target_component`. `ComponentId` is used to check that requested type corresponds to the type available on source entity. Reflect-based handler is a little trickier to pull of: we only have `&dyn Reflect` and no direct access to the underlying data. `ReflectFromPtr` can be used to get `&dyn Reflect` from concrete component data, but to write it we need to create a clone of the underlying data using `Reflect`. For this reason only components that have `ReflectDefault` or `ReflectFromReflect` or `ReflectFromWorld` can be cloned, all other components will be skipped. The good news is that this is actually only a temporary limitation: once #13432 lands we will be able to clone component without requiring one of these `type data`s. This PR also introduces `entity_cloning` benchmark to better compare changes between the PR and main, you can see the results in the **showcase** section. ## Testing - All previous tests passing - Added test for fast reflect clone path (temporary, will be removed after reflection-based cloning lands) - Ran miri ## Showcase Here's a table demonstrating the improvement: | **benchmark** | **main, avg** | **PR, avg** | **change, avg** | | ----------------------- | ------------- | ----------- | --------------- | | many components reflect | 18.505 µs | 2.1351 µs | -89.095% | | hierarchy wide reflect* | 22.778 ms | 4.1875 ms | -81.616% | | hierarchy tall reflect* | 107.24 µs | 26.322 µs | -77.141% | | hierarchy many reflect | 78.533 ms | 9.7415 ms | -87.596% | | many components clone | 1.3633 µs | 758.17 ns | -45.937% | | hierarchy wide clone* | 2.7716 ms | 3.3411 ms | +20.546% | | hierarchy tall clone* | 17.646 µs | 20.190 µs | +17.379% | | hierarchy many clone | 5.8779 ms | 4.2650 ms | -27.439% | *: these benchmarks have entities with only 1 component ## Considerations Once #10154 is resolved a large part of the functionality in this PR will probably become obsolete. It might still be a little bit faster than using command batching, but the complexity might not be worth it. ## Migration Guide - `&EntityCloner` in component clone handlers is changed to `&mut ComponentCloneCtx` to better separate data. - Changed `EntityCloneHandler` from enum to struct and added convenience functions to add default clone and reflect handler more easily. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Chris Russell <[email protected]>
Objective
Using
Reflect::clone_value
can be somewhat confusing to those unfamiliar with how Bevy's reflection crate works. For example take the following code:What can we expect to be the underlying type of
clone
? If you guessedusize
, then you're correct! Let's try another:What about this code? What is the underlying type of
clone
? If you guessedFoo
, unfortunately you'd be wrong. It's actuallyDynamicStruct
.It's not obvious that the generated
Reflect
impl actually callsStruct::clone_dynamic
under the hood, which always returnsDynamicStruct
.There are already some efforts to make this a bit more apparent to the end-user: #7207 changes the signature of
Reflect::clone_value
to instead returnBox<dyn PartialReflect>
, signaling that we're potentially returning a dynamic type.But why can't we return
Foo
?Foo
can obviously be cloned— in fact, we already derivedClone
on it. But even without the derive, this seems like somethingReflect
should be able to handle. Almost all types that implementReflect
either contain no data (trivially clonable), they contain a#[reflect_value]
type (which, by definition, must implementClone
), or they contain anotherReflect
type (which recursively fall into one of these three categories).This PR aims to enable true reflection-based cloning where you get back exactly the type that you think you do.
Solution
Add a
Reflect::reflect_clone
method which returnsResult<Box<dyn Reflect>, ReflectCloneError>
, where theBox<dyn Reflect>
is guaranteed to be the same type asSelf
.Notice that we didn't even need to derive
Clone
for this to work: it's entirely powered via reflection!Under the hood, the macro generates something like this:
If we did derive
Clone
, we can tellReflect
to rely on that instead:Generated Code
Or, we can specify our own cloning function:
Generated Code
Similarly, we can specify how fields should be cloned. This is important for fields that are
#[reflect(ignore)]
'd as we otherwise have no way to know how they should be cloned.Generated Code
If we don't supply a
clone
attribute for an ignored field, then the method will automatically returnErr(ReflectCloneError::FieldNotClonable {/* ... */})
.Err
values "bubble up" to the caller. So ifFoo
containsBar
and thereflect_clone
method forBar
returnsErr
, then thereflect_clone
method forFoo
also returnsErr
.Attribute Syntax
You might have noticed the differing syntax between the container attribute and the field attribute.
This was purely done for consistency with the current attributes. There are PRs aimed at improving this. #7317 aims at making the "special-cased" attributes more in line with the field attributes syntactically. And #9323 aims at moving away from the stringified paths in favor of just raw function paths.
Compatibility with Unique Reflect
This PR was designed with Unique Reflect (#7207) in mind. This method actually wouldn't change that much (if at all) under Unique Reflect. It would still exist on
Reflect
and it would stillOption<Box<dyn Reflect>>
. In fact, Unique Reflect would only improve the user's understanding of what this method returns.We may consider moving what's currently
Reflect::clone_value
toPartialReflect
and possibly renaming it topartial_reflect_clone
orclone_dynamic
to better indicate how it differs fromreflect_clone
.Testing
You can test locally by running the following command:
Changelog
Reflect::reflect_clone
methodReflectCloneError
error enum#[reflect(Clone)]
container attribute#[reflect(clone)]
field attribute