Hello everybody,
I am experiencing some rendering performance issue with cocos2d-x v3.13 on a simple static scene.
Note that I didn’t test any other cocos2d-x version, so I don’t know if this si the case for previous versions.
Basically, here is what I did:
I create a new Helloworl project, and remove everything on screen (ie the default sprite, the text and the close button), except the cocos2d-x statistics (to know the FPS and number of draw calls).
I created a 20x15 tile map using sprites (ie not using the TMX maps). Each tile has about 2 sprites (one on top of another, i.e. the ground and some object, like a bush). So in total I have at most 600 sprites on screen (a little less since sometimes a tile has only one sprite). All the sprites were added to the same layer.
The scene is static, i.e. I only add the sprites once in the init() function of the HelloWorld layer, and there is no update or anything else.
I tried 4 different solutions:
- solution 1: put all the sprites in the same spritesheet, and use setZOrder() to affect the render order of each object. Thus, rendering is done in only 1 draw call. On my device, I get about 29 fps, which is very low considering that my game is actually doing nothing but displaying static sprites, all drawn in a single draw call !
Here is a screenshot so that you can have a better idea (note: it is a screenshot when running the win32 app, not the one on the device)
- solution 2: using sprites from 2 different spritesheet, still with setZOrder(). I know that my game won’t be able to handle all sprites within the same spritesheet, so I had to test if sprites come from two different spritesheets. As expected, the number of draw calls hugely increased to about 114 draw calls per frame, since sprites with same z-order could use different spritesheets. However, I got about 28 fps on my device, so almost identical as the previous one, while there are 100 times more draw calls.
Here is a screenshot from the win32 app. The sprites with inverted colors are those of the second spritesheet:
- solution 3: put all sprites in the same spritesheet, and use setZPosition() with depth-buffer enabled. So the rendering is still done in a single draw call, and i get a slight improvement with about 34 fps, probably because I didn’t use any alpha test for transparent objects, thus resulting in less pixels being drawn. However, this is still far below 60 fps.
Here is a screenshot (you can see black areas since I am using depth buffer with no alpha testing):
- solution 4: using sprites from 2 different spritesheet, with setZPosition() and depth-buffer enabled. This drastically increased the number of draw calls to 233, but had no impact of the fps, which stayed arround 35.
And here is a screenshot for the last one:
Moreover, I saw important fluctuation of the fps. It is quite periodic, like each second the fps drops down then comes back up, then drops down again, etc. This is very strange since the app is always doing the same thing (i.e. just rendering this static scene).
So my questions are: how can I achieve 60 fps with such a simple scene? Or why can’t I achieve it? There is no game logic yet, so I guess the fps will drop considerably if I had some. Why do I get the same fps with 1 draw wall and 100 or 200 draw calls whie accoring to everywhere on the internet, batching is the main solution for iptimizing the rendering? And why are there these fps fluctuations ? Is the engine doing some computation each second, even if nothing changed ?
Note: I am not asking why I get black areas when using the depth-buffer, I know this is normal since I didn’t do any alpha testing.
My device is a SAMSUNG phone (GT-S7390G), running android v4.1.2. No other app was running in my app while performing the tests, this is a phone I only use for developping. Compilation was done in debug mode.
And here is the code I added to the HelloWorld init() method to build the map when using only one sprite sheet:
//Create map
int map_size_x = 20, map_size_y = 15;
float offset_x = 103.0f;
float offset_y = 83.0f;
float scale = 0.5f;
int z_order_offset = 5;
for (int y = 0; y < map_size_y; y++)
{
for (int x = 0; x < map_size_x; x++)
{
cocos2d::Vec2 pos(50.0f + x * offset_x * scale, 100.0f + y * offset_y * scale);
int z_order = -y * z_order_offset;
cocos2d::Sprite* spr = 0;
switch (rand() % 4)
{
case 0:
spr = cocos2d::Sprite::createWithSpriteFrameName("ground_01.png");
break;
case 1:
spr = cocos2d::Sprite::createWithSpriteFrameName("water_01.png");
break;
case 2:
spr = cocos2d::Sprite::createWithSpriteFrameName("grass_01.png");
break;
case 3:
spr = cocos2d::Sprite::createWithSpriteFrameName("dirt_01.png");
break;
}
spr->setPosition(pos);
spr->setScale(scale);
//if using z-order:
//addChild(spr, z_order + 0);
//if using depth-buffer:
spr->setPositionZ(z_order * 0.1f);
addChild(spr);
spr = 0;
switch (rand() % 12)
{
case 0:
spr = cocos2d::Sprite::createWithSpriteFrameName("bush_01.png");
break;
case 1:
spr = cocos2d::Sprite::createWithSpriteFrameName("bush_02.png");
break;
case 2:
spr = cocos2d::Sprite::createWithSpriteFrameName("bush_03.png");
break;
case 3:
spr = cocos2d::Sprite::createWithSpriteFrameName("chest_01.png");
break;
case 4:
spr = cocos2d::Sprite::createWithSpriteFrameName("cristal_01.png");
break;
case 5:
spr = cocos2d::Sprite::createWithSpriteFrameName("rock_01.png");
break;
case 6:
spr = cocos2d::Sprite::createWithSpriteFrameName("bloc_02.png");
break;
case 7:
spr = cocos2d::Sprite::createWithSpriteFrameName("key_01.png");
break;
case 8:
spr = cocos2d::Sprite::createWithSpriteFrameName("star_01.png");
break;
default:
break;
}
if (spr)
{
spr->setPosition(pos);
spr->setScale(scale);
//if using z-order:
//addChild(spr, z_order + 1);
//if using depth-buffer:
spr->setPositionZ((z_order + 1) * 0.1f);
addChild(spr);
}
}
}
I forgot to mention that each spritesheet is in .png format, 1024x1024, and sprite frames are about 100x80px.
Any help is apreciated!
Edit:
Ok, I did some further testing:
-
First, it seems that my spriteFrames were too big (I am not talking about the spritesheet size, but the size of each sprite in the spritesheet). By dividing the size by two, I could manage 60 fps. Is there any way to do that automatically, for instance using some kind of mipmap ? Moreover, can I reasonably expect that a device with a resolution twice bigger will have similar performance if my images are as well twice bigger?
I don’t really get this performance difference. I tried using nearest interpolation for texture but it didn’t change much the fps. Lets consider that I have two spritesheet of 1024x1024px. In the first one, I have tile sprites of 100x100px and in the second spritesheet, tiles of 50x50px. If I create two sprites, with the same visible size on screen (for instance 63x63px after scaling them, etc.), the first one with a sprite frame from the first spritesheet and the second one with a sprite frame from the second spritesheet (thus smaller than the first one). If I use no texture anti-aliasing, why would the first sprite take more time to render than the second one, while the number of drawn pixels is the same, as well as the whole size of each spritesheet they belong too ? -
Finally, it seems that when using the depth buffer, automatic sprite batching is disabled (if you look at solution 4, when using z-position and depth-buffer with 2 spritesheets, I got more than 200 draw calls, so clearly sprites are not batched, or not well batched). I manually assigned sprites to two SpriteBatchNode (one for each spritesheet), and it went down to only 3 draw calls. Why is it not bathed automatically, I am missing something ?