Skip to content

Asset Promises

Pravus edited this page Jul 1, 2025 · 3 revisions

AssetPromise Explained

AssetPromise is a core architectural piece of the asset loading mechanism in the project's ECS (Entity Component System) framework. It provides a unified, trackable, and cancellable way to request and handle assets that may load asynchronously, such as textures, GLTFs, audio clips, or data from a web request.

At its heart, AssetPromise is a lightweight struct that acts as a handle to an underlying ECS Entity. This entity represents the loading operation itself and holds all its state in various components. This design keeps the promise handle small and cheap to pass around, while the actual loading state is managed by the ECS world and its systems.

Anatomy of an AssetPromise

An AssetPromise is a generic struct defined as AssetPromise<TAsset, TLoadingIntention>.

  • TAsset: The type of the asset you expect to receive once the promise is fulfilled (e.g., Texture2DData, GltfContainerAsset, AudioClipData).
  • TLoadingIntention: A component that describes what to load and how. This "intention" component holds all the necessary parameters for the loading systems, such as URLs, caching policies, and, crucially, a CancellationTokenSource for cancellation. Every intention must implement the IAssetIntention interface.

Type Aliases for Clarity

Given the descriptive but long generic types, it's a very common and recommended practice to use using aliases to create shorthand names for specific promise types. This dramatically improves code readability.

// Example from DCL.AvatarRendering.AvatarShape.Systems.AvatarLoaderSystem.cs
using WearablePromise = ECS.StreamableLoading.Common.AssetPromise<DCL.AvatarRendering.Wearables.Components.WearablesResolution, DCL.AvatarRendering.Wearables.Components.Intentions.GetWearablesByPointersIntention>;
using EmotePromise = ECS.StreamableLoading.Common.AssetPromise<DCL.AvatarRendering.Emotes.EmotesResolution, DCL.AvatarRendering.Emotes.GetEmotesByPointersIntention>;

The Lifecycle of an AssetPromise

The lifecycle of an AssetPromise involves creation, polling for the result, consuming it, and handling cleanup.

1. Creation

There are two main ways to create an AssetPromise:

A) Standard Creation: AssetPromise.Create(...)

This is the most common method. It creates a new entity in the ECS world to represent the loading process.

// Example from DCL/Infrastructure/ECS/SceneLifeCycle/Systems/LoadFixedPointersSystem.cs

var promise = AssetPromise<SceneEntityDefinition, GetSceneDefinition>
   .Create(World, new GetSceneDefinition(new CommonLoadingArguments(url), ipfsPath), PartitionComponent.TOP_PRIORITY);
  • World: The ECS world where the loading entity will be created.
  • new GetSceneDefinition(...): An instance of the loading intention, containing all necessary data.
  • PartitionComponent.TOP_PRIORITY: A component that helps prioritize the work of loading systems.

This call creates an entity with the GetSceneDefinition, PartitionComponent, and StreamableLoadingState components. Specialized systems will then query for these entities to execute the loading logic.

B) Finalized Creation: AssetPromise.CreateFinalized(...)

This method creates a promise that is already resolved. It does not create an entity in the world. This is useful for returning a cached asset or an immediate failure without engaging the whole loading system pipeline.

// A finalized promise is "born" consumed and has a result from the start.
var result = new StreamableLoadingResult<MyAsset>(new MyAsset());
AssetPromise<MyAsset, MyIntention> promise = AssetPromise<MyAsset, MyIntention>.CreateFinalized(intention, result);

// promise.IsConsumed is true right after creation.

2. Retrieving the Result

Once a promise is created, the system that performs the loading will eventually add a StreamableLoadingResult<TAsset> component to the promise's entity upon completion. You can then retrieve this result in a few ways.

A) Peeking at the Result: TryGetResult()

This method checks if the result is available without altering the promise or its underlying entity. It's safe to call multiple times. It's useful for systems that need to check the status of a dependency without taking ownership of it.

if (myPromise.TryGetResult(World, out StreamableLoadingResult<MyAsset> result))
{
    // The result is ready!
    // The promise entity still exists.
    if (result.Succeeded)
        DoSomethingWith(result.Asset);
}

B) Consuming the Result: TryConsume()

This is the primary method for getting the result and taking ownership of it. Upon retrieving the result, it destroys the underlying entity, cleaning up all associated loading state components.

Important: A promise can only be consumed once. Calling TryConsume on an already consumed promise will throw an Exception.

if (myPromise.TryConsume(World, out StreamableLoadingResult<MyAsset> result))
{
    // The result is ready and has been transferred to you.
    // The promise entity has been destroyed.
    if (result.Succeeded)
        TakeOwnershipOf(result.Asset);

    // The promise is now considered "consumed".
}

A helper extension SafeTryConsume exists to simplify cases where a promise might have already been consumed.

3. Asynchronous async/await Usage

For non-ECS systems or async-based logic, you can use UniTask extensions to await a promise.

  • ToUniTaskAsync(world): Awaits the promise and consumes it, returning the modified (and now consumed) promise struct.
  • ToUniTaskWithoutDestroyAsync(world): Awaits the promise but does not consume it, making it equivalent to waiting until TryGetResult returns true.
async UniTask LoadMyAssetAsync(World world, AssetPromise<MyAsset, MyIntention> promise)
{
    // Wait for the result and consume the promise
    var consumedPromise = await promise.ToUniTaskAsync(world);

    if (consumedPromise.Result.Value.Succeeded)
        Debug.Log("Asset loaded!");
}

4. Cancellation and Cleanup

There are two ways to interrupt or clean up a promise.

  • ForgetLoading(world): This method is used to explicitly cancel an in-flight loading operation. It triggers the CancellationTokenSource within the loading intention (which should cause the loading system to halt its work) and destroys the promise entity.
// If we no longer need the asset, we can cancel the request.
myPromise.ForgetLoading(World);
  • Consume(world): If you no longer care about the result but don't necessarily need to cancel the operation (or it may have already finished), you can simply call Consume. This ensures the promise entity is destroyed, preventing resource leaks. If the promise was already consumed, it does nothing.
// Clean up the loading entity, we don't care about the result anymore.
myPromise.Consume(World);

Clone this wiki locally