// this bitmap asset should be defined in project.xml
// <assets path="assets/hero.png"/>
var bitmapData:BitmapData = Assets.getBitmapData("assets/hero.png");
var texture:Texture = Texture.fromBitmapData(bitmap);
Context Loss
All Stage3D rendering happens through a so called "render context" (an instance of the Context3D class). It stores all current settings of the GPU, like the list of active textures, pointers to the vertex data, etc. The render context is your connection to the GPU — without it, you can’t do any Stage3D rendering.
And here comes the problem: that context can sometimes get lost. This means that you lose references to all data that was stored in graphics memory; most notably: textures.
Such a context loss doesn’t happen equally frequently on all systems; it’s rare on iOS and macOS, happens from time to time on Windows and very often on Android (rotating the screen? Bam!). So there’s no way around it: we need to expect the worst and prepare for a context loss.
How to trigger a context loss
There is an easy way to check if your application can handle a context loss: simply dispose the current context via |
Default Behavior
When Starling recognizes that the current render context has been lost, it initiates the following procedures:
-
Starling will automatically create a new context and initialize it with the same settings as before.
-
All vertex- and index buffers will be restored.
-
All vertex- and fragment programs (shaders) will be recompiled.
-
Textures will be restored by whatever means possible (from memory/disk/etc.)
Restoring buffers and programs is not problematic; Starling has all data that’s required and it doesn’t take much time. Textures, however, are a headache. To illustrate that, let’s look at the worst case example: a texture created from a bitmap asset.
The moment you call Texture.fromBitmapData
, the bitmap is uploaded to GPU memory, which means it’s now part of the context.
If we could rely on the context staying alive forever, we’d be done now.
However, we cannot rely on that: the texture data could be lost anytime. That’s why Starling will keep a copy of the original bitmap. When the worst happens, it will use it to recreate the texture. All of that happens behind the scenes.
Lo and behold! That means that the texture is in memory twice.
-
The bitmap data (conventional memory)
-
The texture (graphics memory)
Given the tight memory constraints we’re facing on mobile, this is a catastrophe. You don’t want this to happen!
Ideally, though, we want to have the texture in memory only once. For this to happen, you must not preload the bitmap asset; instead, you need to load it from an URL that points to a local or remote file. That way, only the URL needs to be stored; the actual data can then be reloaded from the original location.
There are two ways to make this happen:
-
Use the AssetManager to load your textures.
-
Restore the texture manually.
My recommendation is to use the AssetManager whenever possible. It will handle a context loss without wasting any memory; you don’t have to add any special restoration logic whatsoever.
Nevertheless, it’s good to know what’s happening behind the scenes. Who knows — you might run into a situation where a manual restoration is your only choice.
Manual Restoration
Let’s look at a possible implementation of a method that creates a texture from a BitmapData asset:
public static function textureFromAsset(assetID:String):Texture
{
var bitmapData:BitmapData = Assets.getBitmapData(assetID);
var texture:Texture = Texture.empty(bitmapData.width, bitmapData.height);
texture.root.uploadBitmapData(bitmapData);
texture.root.onRestore = function():Void
{
var bitmapData:BitmapData = Assets.getBitmapData(assetID);
texture.root.uploadFromBitmap(bitmapData);
};
return texture;
}
You can see that the magic is happening in the root.onRestore
callback.
Wait a minute: what’s root
?
You might not know it, but when you’ve got a Texture instance, that’s actually often not a concrete texture at all.
In reality, it might be just a pointer to a part of another texture (a SubTexture).
Even a fromBitmap
call could return such a texture!
(Explaining the reasoning behind that would be beyond the scope of this chapter, though.)
In any case, texture.root
will always return the ConcreteTexture object, and that’s where the onRestore
callback is found.
This callback will be executed directly after a context loss, and it gives you the chance of recreating your texture.
In our case, that callback simply uploads the bitmap again to the root texture. Voilà, the texture is restored!
The devil lies in the details, though.
You should construct a custom onRestore
-callback very carefully to be sure not to store another bitmap copy without knowing it.
Here’s one innocent looking example that’s actually totally useless:
public static function textureFromAsset(assetID:String):Texture
{
// DO NOT use this code! BAD example.
var bitmapData:BitmapData = Assets.getBitmapData(assetID);
var texture:Texture = Texture.empty(bitmapData.width, bitmapData.height);
texture.root.uploadBitmapData(bitmapData);
texture.root.onRestore = function():Void
{
texture.root.uploadFromBitmap(bitmapData);
};
return texture;
}
Can you spot the error?
The problem is that the method creates a BitmapData object and uses it in the callback.
That callback is actually a so-called closure; that’s an inline function that will be stored together with some of the variables that accompany it.
In other words, you’ve got a function object that stays in memory, ready to be called when the context is lost.
And the bitmap instance is stored inside it, even though you never explicitly said so.
(Well, in fact you did, by using bitmapData
inside the callback.)
In the original code, the bitmap is not referenced, but created inside the callback.
Thus, there is no bitmapData
instance to be stored with the closure.
Only the assetID
string is referenced in the callback — and that is in memory, anyway.
That technique works in all kinds of scenarios:
-
If your texture originates from an URL, you pass only that URL to the callback and reload it from there.
-
For ATF textures, the process is just the same, except that you need to upload the data with
root.uploadATFData
instead. -
For a bitmap containing a rendering of a conventional display object, just reference that display object and draw it into a new bitmap in the callback. (That’s just what Starling’s TextField class does.)
Let me emphasize: the AssetManager does all this for you, so that’s the way to go. I just wanted to show you how that is achieved. |
Render Textures
Another area where a context loss is especially nasty: render textures. Just like other textures, they will lose all their contents — but there’s no easy way to restore them. After all, their content is the result of any number of dynamic draw operations.
If the RenderTexture is just used for eye candy (say, footprints in the snow), you might be able to just live with it getting cleared. If its content is crucial, on the other hand, you need a solution for this problem.
There’s no way around it: you will need to manually redraw the texture’s complete contents.
Again, the onRestore
callback could come to the rescue:
renderTexture.root.onRestore = function():Void
{
var contents:Sprite = getContents();
renderTexture.clear(); // required on texture restoration
renderTexture.draw(contents);
});
I hear you: it’s probably more than just one object, but a bunch of draw calls executed over a longer period. For example, a drawing app with a RenderTexture-canvas, containing dozens of brush strokes.
In such a case, you need to store sufficient information about all draw commands to be able to reproduce them.
If we stick with the drawing app scenario, you might want to add support for an undo/redo system, anyway. Such a system is typically implemented by storing a list of objects that encapsulate individual commands. You can re-use that system in case of a context loss to restore all draw operations.
Now, before you start implementing this system, there is one more gotcha you need to be aware of.
When the root.onRestore
callback is executed, it’s very likely that not all of your textures are already available.
After all, they need to be restored, too, and that might take a while!
If you loaded your textures with the AssetManager, however, it has got you covered.
In that case, you can listen to its TEXTURES_RESTORED
event instead.
Also, make sure to use drawBundled
for optimal performance.
assetManager.addEventListener(Event.TEXTURES_RESTORED, function():Void
{
renderTexture.drawBundled(function():Void
{
for (command in listOfCommands)
command.redraw(); // executes `renderTexture.draw()`
});
});
This time, there is no need to call clear, because that’s the default behavior of onRestore , anyway — and we did not modify that.
Remember, we are in a different callback here (Event.TEXTURES_RESTORED ), and onRestore has not been modified from its default implementation.
|