I'm very pleased to announce that we dropped the first release of multi-texturing support into the Phaser dev branch today. I cannot stress enough just how important this is for performance, both on desktop and mobile.
To explain why, it's first worth explaining how a render pass works.
The Pixi Render Pass
Phaser uses a custom build of Pixi v2 internally. Pixi v2 supports Sprite batching under WebGL in order to render lots of sprites, really fast. The way batching works is this:
Every frame, Pixi starts at the root of the display list, which is the Phaser.Stage
object. It then dives into this, and keeps iterating, until it finds a Display Object. With the object in hand it looks at its BaseTexture
. From this it takes the image the texture uses, and binds it to the GPU. A new sprite batch is then started.
After uploading vertices data it moves to the next Sprite in the display list. If it's using the same BaseTexture
then Happy Days. There's no need to bind a new texture, so it just adds the sprite data to the current batch.
This whole process continues until one of two things happens:
- It encounters a Sprite that has a different BaseTexture, or
- It hits 2000 objects in the batch
In each case the batch is now flushed, sending all of the data to WebGL, causing a draw call to be made. The new texture is bound to the GPU. The empty batch is populated again. And so it carries on, until the end of the display list is reached, and the render pass is over.
This whole process happens every frame, so on modern browsers that's 60 times per second.
Using Texture Atlases
Obviously you want to keep the number of draw calls, and WebGL operations, to an absolute minimum. This is why using texture atlases is so important. You can have loads of sprites all using different sections of the same image. And it doesn't cause the batch to flush, because the underlying source image hasn't changed. No texture bind had to take place, so they all get bundled into a single draw call. Perfect.
However there are limits on the maximum texture sizes that GPUs support. For example an iPhone 6S with an A9 GPU has a maximum texture size of 4096 x 4096 pixels. Gaming level desktop GPUs can support much more, but if you want the widest number of players possible for your game, you can't go there.
While 4096 x 4096 is a quite decent texture size, it's highly unlikely you'd fit all of your game assets into it. The larger, and more complex the game, the more assets you need. With character animations, game back drops, UI, particles, etc you often need to use multiple texture atlases just to render a single scene.
And in some cases using an atlas isn't even desirable. For example a photographic style game backdrop may be a far smaller download as a JPG, rather than a PNG, which is what a texture atlas really needs to be when you factor in frame padding.
Breaking the Batch
It's actually incredible easy to code a highly complex scene, from a WebGL performance point of view, with just a handful of sprites. For example take the following code:
function create() {
var keys = ['mushroom', 'clown', 'beball', 'coke', 'asuna',
'bikkuriman', 'bsquad1', 'bsquad2', 'bsquad3', 'car',
'carrot', 'duck', 'diamond', 'eggplant', 'firstaid'];
var group = game.add.group();
for (var i = 0; i < 210; i++)
{
group.create(0, 0, keys[i % 15]);
}
group.align(16, -1, 50, 44, Phaser.CENTER);
}
Here you can see that it's creating 210 sprites in a single Group. As the for
loop runs it assigns each sprite one of 15 different textures. These are just simple PNGs that were loaded in the preloader.
The end result is a relatively tiny number of sprites, well under our batch limit, but organized in the display list in such a way that the WebGL Sprite Batch is utterly unable to benefit from what it does. After every sprite the batch is flushed, a new texture is bound, and it starts all over again.
If you look at the results in a WebGL frame debugger (here FireFox Dev Tools) it's shocking:
Because of the way we structured our display list, we generated 212 draw operations, and a staggering 1911 WebGL calls. Just from 210 tiny sprites.
Now I fully appreciate this is a contrived example, and you could easily fit all of the sprites in our test into a single texture atlas. Yet I don't believe we're straying far from reality here. Very often developers group the assets in their texture atlases based on the type of game element it is, rather than where it appears in the display list.
Once you've got a nicely animated main character, animated baddies, some explosions going on, maybe some bitmap text floating up, particles, UI buttons, and game scenery, I believe most games are already heavily mixing different images in their display list, causing constant batch flushes.
The effect of Multi-Texturing
So what can we do about it?
If the device supports it, WebGL is capable of using more than one texture at a time in a shader. This is entirely GPU dependent, but thankfully very easy to determine in advance. An iPhone 5S with its A7 GPU can support up to 16 texture image units at once, in a single shader. Even a lowly iPhone 4 can support up to 8. The difference it makes is remarkable:
This is the exact same example, with multi-texturing enabled in Phaser. From 212 draw ops down to just 2, and one of those was clearing the screen. 1911 WebGL calls, down to 19.
I'll leave you to figure out which one is better for overall performance.
setTexturePriority
So how do you enable it? There are a couple of ways, but in true Phaser fashion we've made it as simple as a single call renderer.setTexturePriority(keys)
Adding the line above to our example is literally all we did. The new method setTexturePriority
takes an array of string-based keys, which are pulled from the Phaser Cache, and then binds each one of them as an active texture to the GPU.
If your keys
array has 12 elements in it, but the GPU only supports 8 texture units, then Phaser will just bind the first 7, and ignore the rest. On more powerful devices, if the GPU supports 16 textures, it will bind them all, and your entire scene will render in one single draw call. Phaser reserves 1 texture as a 'swap texture', the rest are entirely free for use though.
What is more, you can change the texture priority at any time. Although we'd never suggest doing it in the main loop of your game, you could easily do it at a less vital time, such as when the game changes to a new level. This gives you the control to instruct Phaser which of the textures are the most important, depending on what your game needs at that moment in time.
You don't even have to use the setTexturePriority
method, as the BaseTexture
class now has a new textureIndex
property. This gives you fine-grained control over exactly what is bound, and when.
Coming in Phaser 2.7.0
Multi-Texture support will be arriving in Phaser 2.7.0. If you'd like to get a head-start, and help test it, then you'll find it in the dev branch today. This feature was added thanks to the funds received from the Phaser Patreon. The money directly allowed us to pay for the development, and R&D time needed to implement it.
Hopefully this article has helped explain both the important of texture atlases, display list structure, and what multi-texture support will do to help your games.