Jake Poznanski - Speeding up XNA Content Load

Main

Index
About Me
Resume

Projects

RepRap
Mobot 2010
Mobot 2009
MK2 Torpedo Gyro
The Humway
Thingiverse Profile

Articles

Debugging Behind the Iron Curtain
Why do Google ads point to adware?

Software

Crazy Casino
TriPeaks
Paper Dash
Mars Runner
Spades
Hearts
FreeCell
Solitaire
Word Search
Ice Ball
Word Chief
Pirates Ahoy
RepRap Firmware
RepDev
RobotWinAPI
Vocoders

Tutorials

XNA Content Load
XNA Background Load

Media

Photography

Introduction

            If you’ve been doing any XNA development for Windows Phone 7, you may have noticed that the load times for your game can be much higher than what you expect when you actually test on a real device. Your game may run just fine on the emulator, but launching it on the device can be quite painful. This is especially true if your game doesn’t present its first screen to the user within about 5 seconds: in that case, it gets terminated by the operating system.

            I will cover two specific tricks to speed up the loading time of your game. First, I will show you how to keep your Texture files compressed in their native format within your XAP file, greatly lowering the amount of data that must be transferred from flash storage to load your game. In Part 2, I will show you how to load content in the background without using a background thread. Background threads on Windows Phone 7 are not recommended with XNA games, as they will compete for CPU time with the main thread, and will create a very large slowdown for any loading you are doing. With my method, you will still be able to show a loading bar or do simple animations while your assets are loading, without creating background threads.

            By using these techniques in a game project I was working on for WP7, I reduced the load time of a game from 30 seconds to 6 seconds, as measured on a Samsung test phone.

Attached Project: code/XNA Tutorial.zip

Texture2D Explanation

In my simple example, we will load five 800x480 full screen textures, and a smaller logo image 289x50 pixels. By default, the XNA content pipeline converts these images to raw uncompressed format which is stored on your phone’s storage to be loaded. The size for just one texture like this is 1.5MB, while the plain PNG was only 50KB. Using the example images I've provided, we go from loading 7.37 MB to 287 KB by simply loading the small PNGs from flash instead of the large textures.

(Side note: The XNA pipeline supports compressing Texture objects, but only if they are sized as a power of two. In this case, resizing a 480x800 image to the appropriate scale, you would get about 256KB per compressed image. However, this forces your textures to be larger, and the scaling does not look nice. We can achieve even greater compression by keeping the images in their native format.)

For benchmarking purposes, I will create a new empty XNA solution with 3 projects. Each one will load the same five large images and one small one, then display the loading time on the screen.

Version 1: Plain loading of images using XNA default settings.

Version 2: Scaling to powers of two sizes and using XNA compression.

Version 3: Loading PNGs directly from our application.

Version 1

To create Version 1, I just made a new XNA project, and added our 6 PNG files to the Content Solution with default settings. In the LoadContent method, I load my PNGs as Texture 2Ds, and calculate how long this takes using the DateTime class.

            LoadContent

        SpriteFont debugFont;

        Dictionary<string, Texture2D> assets;

        long loadTime;

 

        protected override void LoadContent()

        {

            long start = DateTime.Now.Ticks;

 

            assets["sea1"] = Content.Load<Texture2D>("sea1");

            assets["sea2"] = Content.Load<Texture2D>("sea2");

            assets["sea3"] = Content.Load<Texture2D>("sea3");

            assets["sea4"] = Content.Load<Texture2D>("sea4");

            assets["background"] = Content.Load<Texture2D>("background");

            assets["logo"] = Content.Load<Texture2D>("logo");

 

            loadTime = (DateTime.Now.Ticks - start) / 10000;

 

            // Create a new SpriteBatch, which can be used to draw textures.

            spriteBatch = new SpriteBatch(GraphicsDevice);

            debugFont = Content.Load<SpriteFont>("DebugFont");

        }

 

            Draw

The draw method just draws one of the images we have loaded, as well as the small logo image. It displays the time it took to load the content as well. This draw method will not change between versions.

        protected override void Draw(GameTime gameTime)

        {

            GraphicsDevice.Clear(Color.CornflowerBlue);

 

            spriteBatch.Begin();

            Texture2D background = assets["background"], logo = assets["logo"];

            spriteBatch.Draw(background, new Rectangle(0, 0, background.Width, background.Height), Color.White);

            spriteBatch.Draw(logo, new Rectangle(0, GraphicsDevice.Viewport.Height - logo.Height, logo.Width, logo.Height), Color.White);

            spriteBatch.DrawString(debugFont, "Load time 1: " + loadTime, new Vector2(0, 0), Color.White);

            spriteBatch.End();

 

            base.Draw(gameTime);

        }

 

Results

On average, the reported load time is ~3000 milliseconds. This is not very good, 3 seconds just to load a couple of images!

Version 2

In an effort to decrease loading times in Version 2, I changed my textures to be sized as powers of two, and used the XNA compression option to compress them. To do this, I went to my Content Project, and for each image adjusted the properties under “Content Processor”. I set “Resize to Power of Two” to be “True”, and “Texture Form” to be “DxtCompressed”. On the bright side, the code remained exactly the same.

            Results

            On average, load time has greatly improved:  I’m seeing ~450 ms to load the game. However, my images now look wrong because they have been resized by the XNA pipeline.

Version 3

            How can you combine the great load times of compressed textures without rescaling your images? The answer is to load the PNGs directly.

To do this, you must go into your XNA project and set Build Action to "None", and Copy To Output Directory to "Copy if newer" on all your texture assets. This will bypass the XNA content pipeline, and just include the PNGs in your XAP file directly.

Now, we will write a helper method that loads a PNG from the Content directory in your project and returns a Texture2D object.

LoadTextureStream

 

This method opens a stream of your PNG file using the TItleContainer.OpenStream method provided by XNA and creates a texture2D object using Texture2D.FromStream. The trick here is pre-multiplying the alpha channel. As it turns out, you can do this using the phone’s GPU extremely quickly! This step is needed because the XNA content pipeline would do this to any textures that you import, but since we are bypassing the content processor, we have to do it ourselves. If you don’t do this step, you will notice that the borders on all sprites you have in your game look jagged and incorrect.

Important: BlendStates are GPU objects, and should not be created once per Texture. Instead, you should create the two needed BlendStates once and store them in a static variable in your loader class.  Please see the Background Loading in XNA tutorial for a more complete solution.

 

      private Texture2D LoadTextureStream(string loc)

        {

            Texture2D file = null;

            RenderTarget2D result = null;

 

            using (Stream titleStream = TitleContainer.OpenStream("Content\\" + loc + ".png"))

            {

                file = Texture2D.FromStream(GraphicsDevice, titleStream); 

            }

 

            //Setup a render target to hold our final texture which will have premulitplied alpha values

            result = new RenderTarget2D(GraphicsDevice, file.Width, file.Height);

 

            GraphicsDevice.SetRenderTarget(result);

            GraphicsDevice.Clear(Color.Black);

 

            //Multiply each color by the source alpha, and write in just the color values into the final texture

            BlendState blendColor = new BlendState();

            blendColor.ColorWriteChannels = ColorWriteChannels.Red | ColorWriteChannels.Green | ColorWriteChannels.Blue;

    

            blendColor.AlphaDestinationBlend = Blend.Zero;

            blendColor.ColorDestinationBlend = Blend.Zero;

     

            blendColor.AlphaSourceBlend = Blend.SourceAlpha;

            blendColor.ColorSourceBlend = Blend.SourceAlpha;

 

            SpriteBatch spriteBatch = new SpriteBatch(GraphicsDevice);

            spriteBatch.Begin(SpriteSortMode.Immediate, blendColor);

            spriteBatch.Draw(file, file.Bounds, Color.White);

            spriteBatch.End();

 

            //Now copy over the alpha values from the PNG source texture to the final one, without multiplying them

            BlendState blendAlpha = new BlendState();

            blendAlpha.ColorWriteChannels = ColorWriteChannels.Alpha;

 

            blendAlpha.AlphaDestinationBlend = Blend.Zero;

            blendAlpha.ColorDestinationBlend = Blend.Zero;

 

            blendAlpha.AlphaSourceBlend = Blend.One;

            blendAlpha.ColorSourceBlend = Blend.One;

 

            spriteBatch.Begin(SpriteSortMode.Immediate, blendAlpha);

            spriteBatch.Draw(file, file.Bounds, Color.White);

            spriteBatch.End();

 

            //Release the GPU back to drawing to the screen

            GraphicsDevice.SetRenderTarget(null);

 

            return result as Texture2D;

        }

 

LoadContent

            We also need to slightly modify LoadContent to use our new method instead of Content.Load.

      protected override void LoadContent()

        {

            long start = DateTime.Now.Ticks;

 

            assets["sea1"] = LoadTextureStream("sea1");

            assets["sea2"] = LoadTextureStream("sea2");

            assets["sea3"] = LoadTextureStream("sea3");

            assets["sea4"] = LoadTextureStream("sea4");

            assets["background"] = LoadTextureStream("background");

            assets["logo"] = LoadTextureStream("logo");

 

            loadTime = (DateTime.Now.Ticks - start) / 10000;

 

            // Create a new SpriteBatch, which can be used to draw textures.

            spriteBatch = new SpriteBatch(GraphicsDevice);

            debugFont = Content.Load<SpriteFont>("DebugFont");

        }

 

Draw

Our Draw method doesn’t require any change! We have already pre-multiplied the alpha channel in our loaded textures, just like the regular pipeline would have done.

Results

On average, my load time is ~550ms, 100ms slower to load the test set than using DXTCompressed images in the XNA framework, but I get to maintain the original image size, and the solution is very short and simple.

 

Conclusion

Everything looks great! If you want your graphics to look right and load in reasonable time, you can just use my LoadTextureStream method.

If you are doing some more advanced manipulation of the alpha channels, you should be able to easily modify it to fit your needs. Unless you are explicitly using BlendState.Nonpremulitplied, this should work as-is.

For more reading on premultiplied alpha, check out this article by Shawn Hargreaves.