Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Objective
Closes #5563.
Note
Much of this code was actually written almost two years ago. I did my best to revive the work, but there may be subtle inconsistencies with the rest of the codebase or reduced quality in some areas (I was young okay!). So please do feel free to point those out so I can correct them!
Sometimes you need more information than just "are these two values equal?" Sometimes you need something more like "why aren't these two values equal?" This is where diffing comes in.
With diffing, we can pinpoint exactly why two values differ from each other. This can be really helpful for handling specific differences, which would otherwise require lots of specific if-statement spaghetti.
Additionally, because diffing is solely concerned with the changes, it means they can also be more efficient in memory usage. Rather than storing a copy of the entire changed value, we can simply store a copy of the change itself. This can be a massive win for large structs where only a single field is changed.
Because of the smaller memory usage, it can also have huge gains on networking. Instead of sending entire objects to the server, you can just send the changes.
This PR is an attempt to add some basic support for diffing within
bevy_reflect
.Solution
Adds reflection-based diffing.
This adds two methods to
Reflect
:Reflect::diff
andReflect::apply_diff
.Reflect::diff
When
Reflect::diff
is called on two values, it will return aDiff
enum.This enum contains variants for:
NoChange
,Replaced
, andModified
.NoChange
is pretty self-explanatory. There is no perceived change1 between the two values.Replaced
usually means that the two values are not of the same type. There's no good way to assess differences between two different types, so we just say that it was replaced.Modified
is where the fun begins. This contains aDiffType
which is another enum that holds kind-specific diffs, such asStructDiff
,MapDiff
, andTupleDiff
. These values are recursive in that they may contain otherDiff
values.Reflect::apply_diff
With a
Diff
created, you can then apply the changes to the old value to get the new one.By default,
Diff
will attempt to store references (hence theBorrowed
in the output above) rather than owned data. The full type is actuallyDiff<'old, 'new>
. However, note that you can convert this to an owned instance usingDiff::clone_diff
, which returns aDiff<'static, 'static>
.We'll need to convert this to an owned diff if we wish to modify the original value (see Open Questions for details).
And with this, we have successfully applied our changes to the original value. Note that
Reflect::apply
is strictly additive, so callingold.apply(&new)
would not have removed the3
frombar
. We can consider addressing this in the future (perhaps by internally using diffing).Mini-Example
As a fun sample of how this can be used, here's a proof-of-concept undo/redo system:
Undo/Redo Example
Open Questions
Currently,
Diff
is required to contain the'old
lifetime due to theEntryDiff::Deleted
variant:bevy/crates/bevy_reflect/src/diff/map_diff.rs
Lines 11 to 12 in c4be479
We could consider storing an owned value instead of a borrowed one, but this raises two problems. First, it eagerly clones data we might not want (some applications may just want to examine a diff rather than apply it). Second, there are currently some issues regarding dynamic types and hashing (see #6601). By eagerly cloning this data, we risk users hitting that issue more often. Although, this hashing problem will already be experienced if users call
clone_diff
before applying the diff.Should we go ahead and convert it to an owned value to get rid of the
'old
requirement?Future Work
This PR sets the groundwork for more diffing work.
Specifically, I want to make these diffs serializable. This will make it much easier to compactly serialize and send these diffs over the network or even between processes.
I originally had that work done, but probably due to a git stash malfunction, I can't seem to find it. If this PR gets general consensus among the community, I'll restart work on that front as well.
Testing
To test, you can run the tests for the
bevy_reflect
crate:Changelog
TL;DR
Added
Reflect::diff
trait methodReflect::apply_diff
trait method (with default implementation)Array::apply_array_diff
trait methodEnum::apply_enum_diff
trait methodList::apply_list_diff
trait methodMap::apply_map_diff
trait methodStruct::apply_struct_diff
trait methodTuple::apply_tuple_diff
trait methodTupleStruct::apply_tuple_struct_diff
trait methoddiff
moduleArrayDiff
structDiffApplyError
enumDiffApplyResult
type aliasDiffError
enumDiffResult
type aliasDiffType
enumDiff
enumElementDiff
enumEntryDiff
enumEnumDiff
enumListDiff
structMapDiff
structStructDiff
structTupleDiff
structTupleStructDiff
structValueDiff
enumdiff_array
functiondiff_enum
functiondiff_list
functiondiff_map
functiondiff_struct
functiondiff_tuple_struct
functiondiff_tuple
functiondiff_value
functionMigration Guide
Manual implementors of
Reflect
will need to add an implementation for the newReflect::diff
method.Additionally, they may need to add an implementation for the relevant subtrait method:
Array::apply_array_diff
Enum::apply_enum_diff
List::apply_list_diff
Map::apply_map_diff
Struct::apply_struct_diff
Tuple::apply_tuple_diff
TupleStruct::apply_tuple_struct_diff
Footnotes
Recall that reflection is not perfect at detecting differences. Fields could be ignored which might make it seem like two values are the same when in actuality they are not. This is something users of reflection should expect, but it's important as a reminder nonetheless. ↩