Loading Win2D resources from outside the CreateResources event


Last month Damyan wrote a series of articles about async resource loading in Win2D.  OmariO posted a question:

“What if I don’t want to wait until all the images loaded and prefer to draw each image as soon as it is ready?”

Hmm, thought the Win2D team.  What a great question!  But we weren’t entirely sure what the right answer was 🙁  So Damyan replied:

“There’s nothing built-in to Win2D to support this directly, however, the building blocks are there. You do raise a good point though and I’ve filed a backlog item for us to address this scenario in depth. Expect either a blog post or new feature with a more complete answer.”

Can you guess where this post is going yet?  After some thinking, discussing and experimenting, I’m back as promised to provide a more complete answer…

We start off using the CreateResources event as previously described:


   
void CanvasControl_CreateResources(CanvasControl sender,
                                      
CanvasCreateResourcesEventArgs args)
    {
   
   
// Synchronous resource creation goes here:
   
    foo =
new CanvasRenderTarget(sender, …);
   
    bar =
new CanvasRadialGradientBrush(sender, …);
   
   
// etc.

   
    args.TrackAsyncAction(CreateResourcesAsync(sender).AsAsyncAction());
   
}
 

   
async Task CreateResourcesAsync(CanvasControl sender)
   
{
   
   
// Asynchronous resource loading goes here:
   
    baz =
await CanvasBitmap.LoadAsync(sender, …);
   
    qux =
await CanvasBitmap.LoadAsync(sender, …);
   
   
// etc.
   
}


But let’s say we are writing a game, and this game contains several different levels.  We don’t want to load all the resources for every level in one go at startup, as that would take too long and might run out of memory.  So we use CreateResources to load only global things that are shared across all levels, and create a separate per-level load method:


   
async Task LoadResourcesForLevelAsync(CanvasControl resourceCreator, int level)
   
{
   
    levelBackground =
await CanvasBitmap.LoadAsync(resourceCreator, …);
   
    levelThingie =
await CanvasBitmap.LoadAsync(resourceCreator, …);
   
   
// etc.
   
}


WinRT and .NET have rich support for async programming, so we can call this whenever we like to load new levels without blocking our CanvasControl from displaying other previously loaded graphics.

This simple code works, but it’s not very robust.  Remember lost graphics devices?  Win2D knows nothing about your custom LoadResourcesForLevelAsync method, so it can’t automatically handle errors like it does for the built-in CreateResources event.

To handle lost devices, you must do four things:

1)      Track when a custom async load task is in progress.

2)      If a custom load task throws an exception due to lost device, re-throw this from somewhere Win2D will be able to see it (your Draw method, or CanvasAnimatedControl.Update).

3)      If Win2D raises the CreateResources event to recover from a lost device while a custom load task is in progress, your CreateResources handler should cancel that custom task.

4)      If Win2D raises CreateResources to recover from a lost device after you have finished loading data using a custom task, your CreateResources handler must reload that custom data as well as its usual global resources.


Keeping the CanvasControl_CreateResources and LoadResourcesForLevelAsync
methods as shown above, here is a complete implementation that handles all four requirements:


   
int? currentLevel, wantedLevel;

   
// This implements requirement #1.
   
Task levelLoadTask;
 

   
public void LoadNewLevel(int newLevel)
   
{
       
Debug.Assert(levelLoadTask == null);
        wantedLevel = newLevel;
       
levelLoadTask = LoadResourcesForLevelAsync(canvasControl, newLevel);
   
}
 

   
async Task CreateResourcesAsync(CanvasControl sender)
   
{
        // If there is a previous load in progress, stop it, and
   
   
// swallow any stale errors. This implements requirement #3.
       
if (levelLoadTask != null)
       
{
           
levelLoadTask.AsAsyncAction().Cancel();
           
try { await levelLoadTask; } catch { }
           
levelLoadTask =
null;
       
}

        // Unload resources used by the previous level here.

        // Asynchronous resource loading goes here:
        baz = await CanvasBitmap.LoadAsync(sender, …);
        qux = await CanvasBitmap.LoadAsync(sender, …);
        // etc.

        // If we are already in a level, reload its per-level resources.
   
   
// This implements requirement #4.
       
if (wantedLevel.HasValue)
       
{
           
LoadNewLevel(wantedLevel.Value);
       
}
   
}

    bool IsLoadInProgress()
   
{
       
// No loading task?
       
if (levelLoadTask == null)
           
return false;

       
// Loading task is still running?
       
if (!levelLoadTask.IsCompleted)
           
return true;

       
// Query the load task results and re-throw any exceptions
   
   
// so Win2D can see them. This implements requirement #2.
       
try
       
{
           
levelLoadTask.Wait();
       
}
       
catch (AggregateException aggregateException)
       
{

           
// .NET async tasks wrap all errors in an AggregateException.
           
// We unpack this so Win2D can directly see any lost device errors.

           
aggregateException.Handle(exception => {
throw exception; });
       
}
       
finally
       
{
           
levelLoadTask =
null;
       
}

       
currentLevel = wantedLevel;
       
return false;
   
}
 

   
void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
   
{
       
if (IsLoadInProgress())
       
{
           
DrawLoadingScreen();
       
}
       
else
       
{
           
DrawCurrentLevel(currentLevel);
       
}
   
}


Comments (4)

  1. Imperial Dynamics says:

    Very nice post.

    We have begun porting our Silverlight WP game Outlander to Win2D.

  2. drd says:

    I just found myself needing to simply load up a large png, split it to several different png's, but only if parts of the image are not identical (I don't want to save what would end up being identical images as the original png may contain some duplicates). Didn't take long to get this working with Drawing (GDI+) but graphics.DrawImage is really slow. Paint.NET guy says he interoped to use GDI instead of GDI+ to do this kind of things. Surely there is a nicer way?

    I wonder if Win2D can be used for this kind of thing or what is the recommended way. I will later also need to perform OCR. And all this in a console/service type of app.

  3. yufeih says:

    This still feels too much for such a simple requirement.

    – Letting the app handle device lost makes me worry about it every time a resource is used.

    – Force all resources to be created in a single entry point (CreateResources) might not suite well for larger scale applications.

    What if we are still doing virtualization on bitmaps, but instead of keeping a whole copy of the image in CPU, just keep a function that can reload the content whenever the device is lost?

    For CanvasBitmap, it can be constructed from a function:

    var cat = new CanvasBitmap(device, () => myContentManager.LoadAsync("yourCat.png"));

    It is still possible to use the async construction API to ensure the CanvasBitmap is ready:

    var cat = await CanvasBitmap.LoadAsync(device, () => myContentManager.LoadAsync("yourCat.png"));

    Creating CanvasBitmap from files can be inlined as:

    var cat = new CanvasBitmap(device, "yourCat.png");

    Pros:

    – I can now create an instance and sharing instances of CanvasBitmap class anywhere I want in my codebase.

    – I don't have to worry about device lost in my app code any more.

    – I can have a custom content management system sit on top of Win2D and contents are requested on device lost automatically

    – It is extremely easy to draw each image as soon as it is loaded

    Cons:

    – CanvasBitmap now has an additional loading state, which means operations like GetPixelColors and SaveAsync are not valid when the bitmap is reloading itself.

    – When the CanvasBitmap is not loaded, nothing will be rendered

    I'd like to know how you guys think about this approach.

  4. sjb says:

    This solution does not feel “finished”. My scenario is that I want to tile the background of a control in UWP with a bitmap selected by the user — since tiling a bitmap is no longer built into UWP the way it was in WinRT/Silverlight, it would seem that Win2d is the expected way to do this. The proposed solution is too complex for that use case. The whole thing should be wrapped into a general-purpose class.

    More generally, in a typical UWP application the request to change resources is likely to be fired from a property callback, and in the proposed solution there is no entry point that can be called from the property callback to trigger the load of resources. Undocumented is the fact that LoadResourcesForLevelAsync has to be called from within a Win2d callback such as Draw. If you call it from outside that context you get an exception saying that the device is not there.

    I feel that more should be done on the integration of Win2d and .NET / UWP. For example, the memory leak problem also needs to be eliminated or at least made more convenient to deal with from .NET / UWP. This is a more challenging question. Why are we even working in a system that is not compatible with Microsoft’s primary programming platform, in the first place?

Skip to main content