Is this pattern correct?

Many doubts about the addressable release operations. The more I read the documentation and see the examples, the more I get confused.

I have a service class that wraps the addressable operations. Let’s just discuss this method (which is wrong, I came to realise):

public async UniTask<GameObject> InstantiateAssetAsync(string assetName, Transform parent = null,
        bool resetScale = false)
    {
        //Addressables.InstantiateAsync has a considerable overhead.
        //So far we know: thanks to the content load service preloading, bundles are preloaded in memory
        //Both LoadAssetAsync and InstantiateAsync need to do a heavy lookup to find in which bundle the prefab
        //we are looking for is. Fortunately this look up is cached and as far as I know, done once per asset key
        //At the moment we are not tracking or unloading bundles dependencies, this will be necessary when we will move to a multi realm setup
        try
        {
            if (_resourceCache.TryGetValue(assetName, out var prefab) == false)
            {
                AsyncOperationHandle<GameObject> op = Addressables.LoadAssetAsync<GameObject>(assetName);
                prefab = await op.Task;
                _resourceCache[assetName] = prefab;
                Addressables.Release(op);
            }

            GameObject instance = default;
            if (prefab != null)
            {
                instance = GameObject.Instantiate(prefab, parent);
                instance.name = assetName;
        
                if (resetScale)
                {
                    instance.transform.localScale = Vector3.one;
                }
            }

            return instance;
        }
        catch (Exception e)
        {
            // Don't blow up over a missing asset
            LmLogSystem.Exception(e);
            return null;
        }
    }

Questions:

I understand now, after a ton of pain, that I cannot release the AsyncOperationHandle immediately. This made me realise that the release of the operation is not just about releasing stuff that the operation itself creates. Since I am just loading and not instantiating:

  1. what is the system actually releasing once the ref count reaches 0?
  2. why is the game sort of working most of the time anyway and it’s causing issues only rarely?
  3. if I cannot release immediately, when am I supposed to release the load operation? Do I have to hold an array of operations and release them when I unload the level?

And bonus questions:

why does the Addressables.InstantiateAsync operation exist if I can do what I have done above? Is it because in this way I can save an array of prefabs and then release the operation through the prefab instead of holding the operation itself? InstantiateAsync has a overhead for the look up, but after checking the code it seems that the look up is cached, so more or less it shouldn’t be that different to what I have done here?

1 Like

Having examined the source code it only seems to Destroy() the operation itself by default, when the reference counter reaches 0. Because the default allocator (DefaultAllocationStrategy) relies on the garbage collector to free the actual asset.

There is also LRUCacheAllocationStrategy, which doesn’t appear to free anything at all but instead keeps a reference to the asset around in case it’s needed again later.

yeah but it is not just the operation. If I leave the code as I showed before, the game actually breaks. It’s some random behaviour of data not showing properly. Precisely the following code would break. I understand it’s different to what I have shown before, but that actually adds just more confusion:

    public async UniTask<T> LoadAssetAsync<T>(string assetName)
            where T : class
    {
        try
        {
            AsyncOperationHandle<T> op = Addressables.LoadAssetAsync<T>(assetName);
            var payload = await op.Task;
             Addressables.Release(op);

            if (payload == null)
            {
                LmLogSystem.Error(LmLogChannel.UI, $"Failed to load asset {assetName}");
            }
          
            return payload;
        }
        catch (Exception e)
        {
            LmLogSystem.Exception(e);

            // Don't blow up over a missing asset
            return null;
        }
    }

What breaks exactly? Is the payload null or does it throw the exception?

that’s the other point. IT’s a random behavior. Payload is never null, but then you see things start to glitch randomly.

The payload would never be null, that’s just a Task.

Each LoadAsset call adds a count to the ref count for the Asset (and dependencies [bundles]). Release drops that ref count. It will then invalidate the handle that was used to release (not null, just flags it as being used).

Should just break immediately, well if you are using Use AssetBundles mode. If you are in use Asset Database, then the assets will not be unloaded, as the editor handles that.

Yes, when ever you are done with it.

Best not to use it. I am not sure as for the original purpose, maybe just as a convenience. Though deprecating it would cause quite a few people to have to rewrite a lot of their code.

AsyncOperationHandle<T> op = Addressables.LoadAssetAsync<T>(assetName);
            var payload = await op.Task;
             //Addressables.Release(op); don't do this, will immediately set it for unloading.
            if (op.Status == Failed)
            {
                LmLogSystem.Error(LmLogChannel.UI, $"Failed to load asset {assetName}");
            }
          
            return op; // the task doesn't contain the operation to allow it for release
1 Like

thanks for the reply @andymilsom , I appreciate it since I realised also that my initial post was quite badly written (fixed it a bit to make it more readable :))

From your answer, I am still not sure what a load operation release actually releases when the count reaches 0. If the asset is a Gameobject (prefab) will the prefab be released and therefore not available? What happens if I hold the prefab reference myself? will this reference be invalidated?

The Addressables asset management is different to the base Unity system. Or rather built on top of it.

Where the standard ResourceManager doesn’t unload assets until all direct references have be remove/nulled. Addressables unloads the AssetBundles when all assets in those bundles ref counts reach 0. Unloading the bundle and any Assets that where loaded from the bundle. Addressables does not directly unload the assets individually, but all at once when the bundle is unloaded. Though you can still use Resources.UnloadUnusedAssets to sweep the memory and remove anything that no longer has a direct reference (this is slow). Once Addressables ref count on an asset reaches 0 it will drop a direct reference.

Yes, the unloading of the AssetBundle and assets is irrelevant of any direct references.
If you have a instance of the Prefab in the scene for example. The GameObjects are now scene objects, not shared assets. But any direct references like Materials Meshes etc will become lost. There is no sweep to see if anything holds a reference to it, as it assumes that you are done with everything because you released it.

1 Like