Asset Streaming

This example shows how assets streamed in by Umbra are loaded into your own renderer. This sample is intended to clarify the basic concepts, and a more involved example is available in samples/runtime directory of the SDK.

First of all, you must define your own loading functions that will translate a texture, material, or a mesh from Umbra's format into an object you can use in your own renderer. For example textures and models could be uploaded to the GPU and a pointer to a custom texture object returned. Here we settle with just declaring the functions and assume implementations are available somewhere else.

UserPointer createTexture(const TextureData* src);
UserPointer createMaterial(const MaterialData* src);
UserPointer createMesh(const MeshData* src);

Umbra's runtime hands out so called AssetJobs to send new assets to the user application, or to request unloading of existing ones. The application can decide how many jobs it will process per frame. In this example we take advantage of this to put an upper limit on how many milliseconds processing can take. So we define a function that fetches new jobs until a time limit has been reached, or until no jobs are available:

void singleThreadedAssetLoad(Runtime* runtime, int timeLimitMs)
{
    int start = getCurrentTimeMs();

    while (getCurrentTimeMs() < start + timeLimitMs)
    {
        // Get next asset to load. Note that runtime should not be accessed
        // from multiple threads simultaneously
        AssetJob* job = runtime->getNextAssetJob();

        // Break the loop if all data available has been loaded.
        if (!job) break;

Then depending on the types of the asset and the job a different loading function gets called. The call to finish() notifies the runtime that the asset in question is ready to be used for rendering.

        switch (job->getAssetType())
        {
        case AssetJob::AssetType::Texture:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createTexture(job->getTextureData());
                job->finish(id);
            }
            else
                destroyTexture(job->getUserPointer());
            break;

        case AssetJob::AssetType::Material:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createMaterial(job->getMaterialData());
                job->finish(id);
            }
            else
                destroyMaterial(job->getUserPointer());
            break;

        case AssetJob::AssetType::Mesh:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createMesh(job->getMeshData());
                job->finish(id);
            }
            else
                destroyMesh(job->getUserPointer());
            break;
        }
    }
}

For optimal performance, the user defined loading functions (createTexture etc.) should not be run on the main thread. Instead, one or more worker threads should process the jobs in parallel. In the code above everything is done in a single thread for simplicity.

This concludes the asset streaming example. The complete source code is reproduced below.

UserPointer createTexture(const TextureData* src);
UserPointer createMaterial(const MaterialData* src);
UserPointer createMesh(const MeshData* src);

void singleThreadedAssetLoad(Runtime* runtime, int timeLimitMs)
{
    int start = getCurrentTimeMs();

    while(getCurrentTimeMs() < start + timeLimitMs)
    {
        // Get next asset to load. Note that runtime should not be accessed
        // from multiple threads simultaneously
        AssetJob* job = runtime->getNextAssetJob();

        // Break the loop if all data available has been loaded.
        if (!job) break;

        // Find job details
        switch (job->getAssetType())
        {
        case AssetJob::AssetType::Texture:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createTexture(job->getTextureData());
                job->finish(id);
            }
            else
                destroyTexture(job->getUserPointer());
            break;

        case AssetJob::AssetType::Material:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createMaterial(job->getMaterialData());
                job->finish(id);
            }
            else
                destroyMaterial(job->getUserPointer());
            break;

        case AssetJob::AssetType::Mesh:
            if (job->getJobType() == AssetJob::StreamIn)
            {
                auto id = createMesh(job->getMeshData());
                job->finish(id);
            }
            else
                destroyMesh(job->getUserPointer());
            break;
        }
    }
}