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
Assetsresource 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 ofAssetsstorage. 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.RenderAssetUsageis 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 theAssetsresource. 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
ArcsWe can't modify it easily, and we'd end up needing to clone it anyway for mutation. In other words,Archelps with data you read many times and rarely write to (ex: Meshes, Images, etc.), butArcis 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
Handlestores theEntitythat holds it's actual storage (more on that in a bit). If theHandle::storageisNone, 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_aliveis just a per-asset arc to track if the asset is used or not. The handle also stores theHandle::assetitself, which is anArc<AssetRef>.AssetRefstores 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_128stuff).AssetRefalso stores anupdatedwhich 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 theOnceLockon 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'sOnceLockand replace theHandle::assetwith the new one if it exists.I'm just scratching the surface here. The big deal is that
Handlenever 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:
AssetStorageis the component that lives on theHandle::storageEntity. 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::assetandAssetStorageMode?AssetStorage::assetis 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).AssetStorageModeis the most contentious part of this. The simplest one isUpdatedRarely. That means thisAssetStorage::assetisNoneby 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::assetNoneagain.UpdatedOftenis similar, except thatAssetStorage::assetis alwaysSome, and when it is updated, it is cloned into theHandle. Finally,DontArckeeps the asset only inAssetStorage::assetasSome. It never clones into theHandle, instead keeping itAssetHolder::Missinguntil the mode is changed. (We can change the enum to help with invariants; I'm trying to be brief.)UpdatedRarelyis great for static items likeImage, and it only stores one copy of the asset;UpdatedOftenis great for assets that change frequently like a proceduralMesh.DontArcis great for assets that change so much or are referenced so rarely that arcing isn't worth it (ex: save data).Each
AssetStorageModehas 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 apostmethod, 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_128shader. Include the shader in the binary and create a staticLazyLock<Handle<Shader>>which creates theHandleby giving it aHandle::storageofNoneand aAssetHolder::Presentof the parsedShader. Now clone and pass around theHandleas you like. If you want theShaderto be discoverable, you can spawn aAssetStorageentity 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 aHandlewith that entity andAssetHolder::missing, and starts loading the asset. The caller can check on the loading process by seeing if itsHandle'sAssetRef::updatedhas 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 aHandlewith that entity andAssetHolder::present. Spawn thatEntitywith aAssetStorageconfigured 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 isNoneor the entity doesn't exist, theHandleis not meant to be changed (ex: for static assets in the binary). On that&mut AssetStorage<Mesh>, request an&mut Meshand change it. A change detection system will post the updated mesh to allHandles.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 theHandle` 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
Assetsand mapping ids all the time. The built-inArcs 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