Skip to content

Do not unload subassets or the root asset unless all subassets and root asset handles have been dropped. #21878

@andriyDev

Description

@andriyDev

The problem

If you do not use subassets, assets work quite intuitively. You call AssetServer::load, you get a handle, and as long as that handle is held, the asset system will keep your asset loaded, and will not reload the asset even if you call load again (barring hot reloading).

In contrast, if you do use subassets, it's chaos. The problem is that the lifetimes of subassets are all separate. This means one subasset may have been dropped, while others are still being used. This results in weird situations like an asset being reloaded just because another of its (expired) subassets got loaded. These distinct lifetimes makes solving problems like #12756 extremely difficult - we can't know whether the subasset hasn't loaded yet, or its handle has just expired since no-one was holding it.

A solution

Instead of dropping subassets as soon as that subasset's handle has been dropped, only drop a subasset or the root asset if all subassets and root asset handles are gone. In other words, extend make the lifetime of all subassets and root assets for a path be the same.

Example 1: glTF

glTF is a format specifically designed for easy transmission. Unfortunately, Bevy struggles with it quite a bit. One nasty issue is that you can easily incur multiple loads. For example, you have a character with mesh asset_server.load("character.gltf#Mesh0/Primitive0"). You make it play animation asset_server.load("character.gltf#Animation0") - an idle animation, then you set its animation to asset_server.load("character.gltf#Animation1") - a walk animation, and finally back to asset_server.load("character.gltf#Animation0"). You've incurred up to 4 loads of all the glTF data here, since you didn't store all those handles.

In this particular case, the answer is somewhat obvious in hindsight - just hold the handles for all the assets you want to use ahead of time. But this is not intuitive behavior to say the least - the user is using an asset that they've loaded, yet the asset keeps reloading in the background because they keep missing one of the subassets.

By applying the proposed solution, this would incur one load, and then the glTF data would just remain in memory. Since you're using some parts of the asset, the whole asset stays alive.

Example 2: #21222

This issue is just about our documentation, but the real problem is the fact that this logic is confusing to begin with! We shouldn't need to guess which asset loader an asset came from. On one hand, maybe we should be storing

An immediate benefit we get is we can avoid loads for subassets that definitely don't exist. If the root asset is loaded, but the subasset isn't, then that subasset label is definitely not a subasset - we can skip the load here entirely!

Risks/Concerns

  1. Since a file is either loaded or not, some parts of an asset that you aren't using may continue to be loaded. This incurs a memory cost.
    1. This is certainly a concern, but how often are users taking significant advantage of this? Probably not that often. The common use case is likely loading all or nothing - loading an entire glTF scene, or an entire sound file. The "easy answer" to this is to encourage users to make more granular assets. So if you only want a small part of your glTF file, make that its own file. We already encourage this by the nature that we load the whole asset and then just discard everything we don't use - users likely don't want to pay the cost of loading all the unused data anyway, so the recommendation remains the same. For glTF specifically, we should invest in one-to-many asset processing (mentioned here Bevy Asset V2 Tracking Issue #9714), and provide a processor for splitting glTF files into nicer units.
  2. Users may be using the current behavior to only load small parts of their files, like a ZIP file loader for example.
    1. This is a bad solution, but it works. Users pay the cost of loading the whole ZIP, but they get out their one file in the end. Instead, we should resolve Improve support for "random-access" assets (e.g., ZIP files) #21641. I think at least solving that issue partially is necessary before we commit to this change, since otherwise we leave users with no recourse (though maybe we can just tell users to use nested immediate asset loading and then take the assets they want - this will leave them in the same state as before, loading the whole asset just for one subasset, but at least they won't keep the rest of the asset in memory).

Two discussions on this:
https://discord.com/channels/691052431525675048/749332104487108618/1430629996438749184
https://discord.com/channels/691052431525675048/749332104487108618/1439999328566644746

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-AssetsLoad files from disk to use for things like images, models, and soundsD-ComplexQuite challenging from either a design or technical perspective. Ask for help!S-Needs-SMEDecision or review from an SME is requiredX-ControversialThere is active debate or serious implications around merging this PR

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions