Preloading Bitmap Decompression
Since they were introduced way back in Flash Player 8, bitmaps have become a core feature in almost all Flash apps. The way you handle them—creation, operations, and destroying—is one of the most important factors determining your app’s performance. Today’s article shows one little-known trick to help out the performance of loading and using bitmaps.
First of all, let’s review the steps that occur when your Flash app loads and uses a bitmap:
- The bytes of the bitmap file (e.g. PNG, JPEG, GIF) are loaded
- The file is decompressed into a block of ARGB pixels and stored in a
BitmapData
- A
Bitmap
is created to use theBitmapData
- The
Bitmap
and/orBitmapData
are used (e.g. put on theStage
, uploaded to aTexture
)
Let’s look at some simple AS3 code that loads and uses a Bitmap
to illustrate when these steps occur:
// This will (eventually) start step #1- loading the bytes of the bitmap file var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded);{ loader.load(new URLRequest("myImage.png")); private function onLoaded(ev:Event): void { // At some point before this function is called, #3 (Bitmap creation) occurs var loaderInfo:LoaderInfo = ev.target as LoaderInfo; var bitmap:Bitmap = loaderInfo.content as Bitmap; var bmd:BitmapData = bitmap.bitmapData; // This will (eventually) start step #2 (image decompression) and #4 (displaying it) addChild(bitmap); }
This isn’t a very detailed description of when the steps are occurring. Flash Player will decide to start the load at some point and will only dispatch the event at a given point in the next frame or later. All of our handling of the Bitmap
and BitmapData
didn’t manage to trigger steps #2 or #4. Only adding the Bitmap
to the Stage
will eventually trigger them, but when?
To find out, let’s get a very large image file and see if the addChild
function immediately does the expensive work of #2- decompressing the image from PNG to a block of ARGB pixels. Here’s a test app:
package { import flash.display.Bitmap; import flash.display.Loader; import flash.display.LoaderInfo; import flash.display.Sprite; import flash.events.Event; import flash.net.URLRequest; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.utils.getTimer; public class PreloadingBitmaps extends Sprite { public function PreloadingBitmaps() { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded); loader.load(new URLRequest("Adobe_Flash_Professional_CS5_icon.png")); } private function onLoaded(ev:Event): void { var logger:TextField = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); var beforeTime:int; var afterTime:int; beforeTime = getTimer(); addChild((ev.target as LoaderInfo).content as Bitmap); afterTime = getTimer(); logger.appendText("Time: " + (afterTime-beforeTime) + "\n"); } } }
I ran this test in the following environment:
- Flex SDK (MXMLC) 4.6.0.23201, compiling in release mode (no debugging or verbose stack traces)
- Release version of Flash Player 11.5.31.5
- 2.3 Ghz Intel Core i7
- Mac OS X 10.8.2
And here are the results that were printed:
Time: 0
Well, decompressing a 1052×1063 image does not take zero milliseconds. This means that addChild
isn’t doing the work, so it’s deferred to some unknown time later on for it to be done. When? If this work is expensive, as in the case of a lot of bitmaps being used or just a giant one like this, this could represent a framerate “skip” or “jitter” when the decompression actually happens.
It’d be a lot nicer if we could control exactly when the image decompression happens. This way we could make sure it happens at an advantageous time such as a loading screen. Well, it turns out that we can actually force Flash Player to decompress the image on-demand. One simple way of doing this is to simply try to get the color value of one of its pixels with getPixel
. Here’s a test app to demonstrate that it works:
package { import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Loader; import flash.display.LoaderInfo; import flash.display.Sprite; import flash.events.Event; import flash.net.URLRequest; import flash.text.TextField; import flash.text.TextFieldAutoSize; import flash.utils.getTimer; public class PreloadingBitmaps extends Sprite { public function PreloadingBitmaps() { var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoaded); loader.load(new URLRequest("Adobe_Flash_Professional_CS5_icon.png")); } private function onLoaded(ev:Event): void { var logger:TextField = new TextField(); logger.autoSize = TextFieldAutoSize.LEFT; addChild(logger); var bmd:BitmapData = ((ev.target as LoaderInfo).content as Bitmap).bitmapData; var beforeTime:int; var afterTime:int; beforeTime = getTimer(); bmd.getPixel(0, 0); afterTime = getTimer(); logger.appendText("First access: " + (afterTime-beforeTime) + "\n"); beforeTime = getTimer(); bmd.getPixel(0, 0); afterTime = getTimer(); logger.appendText("Second access: " + (afterTime-beforeTime) + "\n"); } } }
In the same testing environment as above, this prints:
First access: 49 Second access: 0
The 49 milliseconds of the first access is definitely indicating that step #2 (image decompression) is occurring during the first getPixel
call. Since the image decompression is already done by the first getPixel
call, the second call has no image decompression to do and completes in under one millisecond.
With this simple tool in hand we can now force the image decompression to occur when we want it to. It’s now possible to move the decompression so that it happens during a loading screen or some other convenient time instead of delaying it to the critical first frames of the game, simulation, movie or whatever the app is displaying for the user.
Spot any bugs? Have any questions? Post a comment!
#1 by Smily on December 24th, 2012 ·
I remember quite a while ago that Bitmaps that didn’t have an attached decoded BitmapData performed almost an order of magnitude faster. I don’t know if that’s still the case, but in any case, it’s probably worth noting that not decompressing forcibly is probably better (if not in terms of CPU, in terms of memory usage) if you don’t actually use the data afterwards (which seems obvious, but might not be).
#2 by Gil Amran on December 24th, 2012 ·
Hi,
Did you try loaderContext.imageDecodingPolicy = ImageDecodingPolicy.ON_LOAD ?
This means that Flash will do the decompression in the background in the loading process…
I must say that when I used the ON_LOAD I got double memory… (Reported a bug)
Thanks
Gil Amran
#3 by jackson on December 24th, 2012 ·
Hi Gil. I hadn’t tried this, but it too seems like a good option for controlling the load process as long as that memory usage issue gets cleared up. Thanks for the tip!
#4 by Nicolas on January 1st, 2013 ·
Thanks!
On an unrelated note, I’d love to see some good use of Scout shown. It sounds right up your alley…
#5 by Luke on January 8th, 2013 ·
Hi,
i have made the game, where from to to time i need to read pixels from a large BitmapData object (some terrain infomation). When i do not read pixels for over a dozen seconds, Garbage Collector removes decompressed image from memory and after that calling bmd.getPixel(0, 0) runs decompression again.
#6 by jackson on January 8th, 2013 ·
Wow, that’s a really interesting find! I’ll put together a test to check it out and get back to you. Thanks for the tip!
#7 by T on January 11th, 2013 ·
Would love to know what you find out! Dealing with a similar issue now – trying to preload some large images into memory as the stage loads, so when they are needed later there is no wait for decompression. Not sure if there is a “fix” out there, but this extra clue certainly helps. Very little of the GC process is accessible via code still.
#8 by T on January 12th, 2013 ·
After a bit of testing, a workaround for this may be to set a timer to run bmd.getPixel(0,0) at a regular interval to ensure the decompressed bytes aren’t thrown out by the Garbage Collector. For my use-case, I want to preload (and pre decompress) a collection of large images, then show them after a few minutes of interaction, without any noticeable delay. So far, this has been the best workaround I’ve found. Still tweaking what the actual interval should be for the timer, or of there is another way to achieve the same thing.
#9 by Nicolas on January 15th, 2013 ·
I have the same issue (on iOS for me but guess it can happen anywhere) so I posted a thread about this here : http://forums.adobe.com/message/4995817#4995817
Didn’t have any solution yet, so it’s Just for information.
#10 by Nicolas on January 15th, 2013 ·
Last comment should have been a replay to #5 Luke, misclicked