Wait, the Handle
is the Asset
?
#18949
Replies: 1 comment 3 replies
-
Ok after giving this a more careful read I see some of the merits. I still don't like it, but it is more understandable. Just to summarize my understanding: to mutate an asset, you now go to There's a couple things that I don't like here. The first is just the complexity here. The indirection goes crazy. Mutates are actually clones that don't apply immediately, and you have to wait for a frame for it to be applied. The second is that every write (for One thing I wasn't clear on is if you have a handle, does accessing the asset through it access the currently held Arc? Or does it chain up to the most recent instance? If it's the latter then A) that can be a lot of indirection if you don't have a mutable reference to the handle to collapse the chain, B) a mutation can change the handle in the middle of the read resulting in different data between each deref. Alternatively, if it's the former, then what are we even winning here? Now users need to regularly get a mutable reference to their handle and "upgrade" it to the newest version? That also doesn't seem ideal, from a UX experienece or a memory-usage perspective (if you forget a handle, you may accidentally be holding many different versions of an asset in memory). Please let me know if I've misunderstood something here! |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
With assets as entities on the horizon, I've been thinking more about the asset system and how we could redesign it with assets as entities to improve performance and user experience in a number of ways.
What is an asset? I mean really, what is the definition of an asset in bevy? The best I can come up with is an asset is "some data referenced in many places but only stored in one." Not all assets are loadable; not all are savable, and not all of them are ever on the file system. It's just some data stored in one place (soon on an entity) and referenced in many places (currently with a handle instead of a pointer).
Existing Problems
Currently, assets live in a standard storage in an
Assets
resource and are referenced by asking that resource for the asset with a particularHandle
. There's a few issues with that. For one, assets can't be used in async code without first cloning the asset out ofAssets
storage. That's especially painful for saving an asset. Another issue is that they have to be cloned between worlds. That puts one copy on the render world and one on the main world for things like meshes, images, etc.RenderAssetUsage
is a clunky way around that, but ideally, we have a general solution to this. Third, for immutable assets (like most images) it's really painful (and slow) to need to look up an asset in theAssets
resource. The image isn't changing, so why jump through those maps?#15813 (putting every asset in arcs) is one solution to this. But while it's better than present, it still has many drawbacks. For one, not all assets benefit from being arced. Take player save data for example. That is common data stored in one placed and referenced from many. (It's an asset.) Ideally, we want to modify it as the player progresses and occasionally clone it for a save process. With
Arcs
We can't modify it easily, and we'd end up needing to clone it anyway for mutation. In other words,Arc
helps with data you read many times and rarely write to (ex: Meshes, Images, etc.), butArc
is really painful for assets that are rarely sent between threads, world, etc but are updated often (ex: save data, procedural meshes, player preferences, etc.).A New Solution
What if the handle was the asset?
Consider this layout:
There's a lot going on here, so let me explain as best I can.
The
Handle
stores theEntity
that holds it's actual storage (more on that in a bit). If theHandle::storage
isNone
, the asset has no storage entity yet. This is to replace the currentHandle::weak_from_128
; instead of being weak, the storage can just beNone
.Handle::keep_alive
is just a per-asset arc to track if the asset is used or not. The handle also stores theHandle::asset
itself, which is anArc<AssetRef>
.AssetRef
stores the current value of the asset inAssetRef::current
, anAssetHolder
. This is eitherMissing
(ex: the asset is loading),Present
(ex: the asset is loaded) orStatic
(ex: to facilitateHandle::weak_from_128
stuff).AssetRef
also stores anupdated
which is aOnceLock<Arc<AssetRef<A>>>
. This tracks, the next version of the asset. For example, if the asset is modified, anew Arc<AssetRef<A>>
is created, and theOnceLock
on the old one is set.To get an asset from
&Handle
, just grab the current,AssetRef
'sAssetHolder
, deref it and return the&'handle A
. If it isMissing
, returnNone
(just likeAssets::get
). To try to update an asset from&mut Handle
, check theAssetRef::updated
'sOnceLock
and replace theHandle::asset
with the new one if it exists.I'm just scratching the surface here. The big deal is that
Handle
never have to guess when their asset has changed, they can see it (and update it) immediately. This is lossless, inexpensive, granular change detection. Fast for rarely changed assets and usable for frequently changed.Now the storage:
AssetStorage
is the component that lives on theHandle::storage
Entity
. It holds a rootHandle
. This is how it can updateAssetRef::updated
'sOnceLock
. It also allows users to get handles from aQuery<AssetStorage<MyAsset>>
. It also stores anOption<AssetPath>
in case it was loaded from a file. This is pretty standard, but what aboutAssetStorage::asset
andAssetStorageMode
?AssetStorage::asset
is the most up to date asset version. Any changes to the asset will flow through this value, ex:Query<&mut AssetStorage<MyAsset>>
. Systems can then run onQuery<&mut AssetStorage<MyAsset>, Changed<AssetStorage<MyAsset>>>
(or something) to post those updates to other handles. This is another way to do asset change detection! If a system want's the most up-to-date version, it can just pull fromQuery<&AssetStorage<MyAsset>>
(just like the currentAssets
).AssetStorageMode
is the most contentious part of this. The simplest one isUpdatedRarely
. That means thisAssetStorage::asset
isNone
by default. Then, when something tries to change the asset, it clones fromAssetStorage::handle
's asset, putting the cloned value intoAssetStorage::asset
, the allowing that to be modified. This change is later picked up by a different system to post the change to theHandle
, leavingAssetStorage::asset
None
again.UpdatedOften
is similar, except thatAssetStorage::asset
is alwaysSome
, and when it is updated, it is cloned into theHandle
. Finally,DontArc
keeps the asset only inAssetStorage::asset
asSome
. It never clones into theHandle
, instead keeping itAssetHolder::Missing
until the mode is changed. (We can change the enum to help with invariants; I'm trying to be brief.)UpdatedRarely
is great for static items likeImage
, and it only stores one copy of the asset;UpdatedOften
is great for assets that change frequently like a proceduralMesh
.DontArc
is great for assets that change so much or are referenced so rarely that arcing isn't worth it (ex: save data).Each
AssetStorageMode
has a purpose, and a user can change it as needed. Users also don't need to wait for a system to post any changes; we can provide apost
method, etc. There's lots of room for expansion here; we could add other handle types for different modes that could skip some checks, etc. But I'm trying to be brief.UX
Example: A
Handle::weak_from_128
shader. Include the shader in the binary and create a staticLazyLock<Handle<Shader>>
which creates theHandle
by giving it aHandle::storage
ofNone
and aAssetHolder::Present
of the parsedShader
. Now clone and pass around theHandle
as you like. If you want theShader
to be discoverable, you can spawn aAssetStorage
entity for it. If you want to respond to mutations from it being discoverable, you'll need to either clone and update the static handle before using it, or make it aRwLock<Option<Handle<Shader>>>
or something.Example: An image loaded from a file. Ask the asset server to load the asset, giving it a
AssetStorageMode
. (We can provide a default here too.) The asset server reserves an entity remotely, creates aHandle
with that entity andAssetHolder::missing
, and starts loading the asset. The caller can check on the loading process by seeing if itsHandle
'sAssetRef::updated
has changed yet.Example: Adding an asset manually. (We'd probably make a command for this, but this is what it would do.) Reserve an
Entity
. Create aHandle
with that entity andAssetHolder::present
. Spawn thatEntity
with aAssetStorage
configured according to the desiredAssetStorageMode
.Example: Changing a mesh. Make a
Query<&mut AssetStorage<Mesh>>
and get the entity assocated with the desiredHandle::storage
. If that isNone
or the entity doesn't exist, theHandle
is not meant to be changed (ex: for static assets in the binary). On that&mut AssetStorage<Mesh>
, request an&mut Mesh
and change it. A change detection system will post the updated mesh to allHandle
s.Example: Cleaning unused assets. Make a
Query<(Entity, &mut AssetStorage<Mesh>)>
. If anyAssetStorage
's``Handle::keep_aliveis the only strong arc, the entity can be despawned, dropping the
Handle` and all copies of the asset.There's a ton of flexibility here but in the interest of time, I'll leave the examples here. The performance and UX of this is much better than needing to go though
Assets
and mapping ids all the time. The built-inArc
s make sharing between threads, worlds, and async trivial. Change detection is fast, easy, and hard to accidentally miss. It works well with assets as entities, etc.That said, I'm no asset expert, so there could be things I'm missing. Your thoughts are welcome.
Beta Was this translation helpful? Give feedback.
All reactions