2D Tiled World - Performance

If you are a new Irrlicht Engine user, and have a newbie-question, this is the forum for you. You may also post general programming questions here.
Post Reply
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

2D Tiled World - Performance

Post by LunaRebirth »

Hi,

I've created a 2D MMO where the map infinitely loads as you move.

The map is a 5x5 grid of 30x30 tiles, where each tile is a 16px by 16px piece of a 512px by 512px tileset image. Each grid may contain up to 5 layers of tiles (but don't need to focus on this. performance is an issue with only 1 layer even right now).


When the player moves, we check if their new position has any differences in where their grid should be located. If the grid doesn't match, we remove the grid that the player no longer contains, and load the new grids that the player should have, keeping a consistent 5x5 grid at all times, no matter where the player has moved to.

Thus, when a new grid loads the default tiles (like grass), 30x30 tiles must load per grid (on a 5x5 grid, if the player moves up to the next grid, that would be 5 grids of 30x30 tiles that need to load, so 4,500 tiles).

The tiles take a very long time to be removed (each grid removed delays the player), and takes another very long tile to add a new grid.
Even when all grids are finished loading, my FPS drops extremely low and is unplayable.

I tried to solve the loading issues by loading only parts of a grid every so many milliseconds, and then continuing on the next frame. This prevents the window from saying "The application has stopped responding" while loading grids, but again, even after grids have finished loading I can barely walk because of the low FPS.

Does anyone have any ideas on solving this performance issue? I'm up for any suggestions.

In case you want to see the code, it is using Lua which is available here: https://github.com/treytencarey/Classic ... /world.lua (see the loadGrid function)

Lua has its own performance issues when storing values in memory (like how I'm storing every tile). Note that even if I don't store every tile, I'm still having performance issues on Irrlicht's end.
CuteAlien
Admin
Posts: 9734
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: 2D Tiled World - Performance

Post by CuteAlien »

Use a profiler annd check where exactly you lose the time. So you know if it's memory allocation, or texture loading, or...
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

A lot of it seems to be draw calls. (I use listboxes as the parent of tiles so I can move the listbox rather than each individual tile while the player is moving.)

Image
MartinVee
Posts: 139
Joined: Tue Aug 02, 2016 3:38 pm
Location: Québec, Canada

Re: 2D Tiled World - Performance

Post by MartinVee »

I don't want to go all Captain Obvious over you, but that really seems to be your problem. Have you try not using a List Box?
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

I tried making the first tile the parent of all other tiles so that I could set the first tile's position to move the world instead. The problem then, obviously, becomes irr::gui::CGUIImage::draw() rather than irr::gui::CGUIListBox::draw().
Performance is about the same, if not worse, if I iterate over each tile IGUIImage element setting it's position as the player moves.
CuteAlien
Admin
Posts: 9734
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: 2D Tiled World - Performance

Post by CuteAlien »

Hm, that seems to be the time which is mainly used in drawing. But your troubles where when adding/removing tiles? Maybe write a test which enforces that to happen a lot more (maybe just disable drawing completly for this test so it won't show up).
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

Adding/removing is not a big deal since I can do them in batches instead of all at once.

The biggest problem is that when my window is at a size of 640x480 and I draw tiles accordingly (so 40x30 tiles), my FPS is at about 40 (which looks pretty good).
But if I go fullscreen, making the window size 1600x900, and now drawing 100x57 tiles, my FPS drops to about 15, which looks extremely choppy. I'm also porting it to Web using Emscripten, dropping the FPS to significantly less, so I need the drawing to be as fast as possible.


Perhaps there's nothing I can do about it other than work on speeding up my own code, though a majority of it is in Irrlicht
CuteAlien
Admin
Posts: 9734
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: 2D Tiled World - Performance

Post by CuteAlien »

Draw calls are expensive. Also material-changes. So the trick is to reduce those. If you need textures on tiles to be completely independent it's actually going to be tricky to do with Irrlicht (my suspicion is that without adding instancing to Irrlicht that might be very hard, thought sorting tiles by material first might help a little). It's better if you can put all textures which belong on those tiles into a single huge textures. Often that is done by putting all textures used per game-level into one texture. Then you can create a single polygon where you just set the uv-coordinates correctly to hit the tile you need inside the huge texture. You have to be slightly careful when doing that (especially in combination with mip-mapping) to avoid those textures to bleed into each other.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
devsh
Competition winner
Posts: 2057
Joined: Tue Dec 09, 2008 6:00 pm
Location: UK
Contact:

Re: 2D Tiled World - Performance

Post by devsh »

How many GUI elements have you got total?

I think the way you create and upload the textures might be a problem, 16 pix tiles are really small... actually much smaller than most GPU texture page-sizes (AMD has something like 128x64) which means that:
A) 16x16 isn't actually saving you any GPU or even CPU memory on the API side
B) you're choking the OpenGL (ES) texture transfer API

I'd advise to make your tiles > 64 pix, this will reduce your draw calls and objects by a factor of n^2 (factor of 16 for 64 pix tiles)


As for the grid stuff, have you thought about modulo (%) addressing and texture sampling wrap around?

You can keep a cache texture as-big or slightly larger than your screen, lets take your example 1600x900 screen and a 16pix tile.
Example
A new tile comes into the range of the screen that hasn't been visible before.
You calculate tile local position in the texture from modulo of the cache texture size
Tile has position in the world = (4096,2048)
Local Address in cache texture = (4096%1600,2048%900) = (896,248)
You then paste your 16 pix tile into the cache texture (use Render To Texture or OpenGL glTexSubImage2D) at position (896,248)

The screen can draw the cache texture using the true world-space coordinates of the screen coordinates as UV coords (divided by cache texture size, naturally), then the built-in default texture wrapping mode will actually "perform the scrolling" .

There's no need to move/copy/translate the contents of the per-screen cache texture due to view-movement, the tiles stay in the same place until they get overwritten by newly visible tiles long after they leave the view.

That's what Nvidia used but in 3D for the VXGI middleware for a cascaded voxel field around the camera.
However here you have no shaders, to texture arrays and no atlasing... just plain old uv-wraparound hack.

The only minor artefact will be a bit of bleeding on the X and Y lines in the world-space which are multiples of the screen size, but that will only happen if the tiles were displayed at lower resolution than the screen (i.e. 16pix on a tile would take up more than 16pix on the screen).

You have 5700 draw-calls, thats the entire draw call budget of Build A World... if you follow the above advice you will get 1 draw call plus the occasional texture transfer (that you'd have to do anyway) == If you don't get 32x better FPS (with no Vsync) after implementing the above I will personally apologize to you.


Lastly

The Irrlicht GUI system is not high-performance, so for the tiles I would merge all tiles into one logically singular GUI element (or not have it in the GUI layer at all, could be a scene node with a top-down ortho camera view).
Having separate GUI elements or scene-nodes (any entity) per-tile causes a bunch of overheads such as recalculating absolute positions, visibility, updating positions, OnPostRender etc.
Drawing a single GUI image per tile will cause render-states to be set very frequently, unlike when drawing a collection of meshes with a 2D projection using the same material.

P.S. The amount of times floorf is called in your code causes me to question whether you compiled with optimization flags for that profile.
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

16 pix tiles are standard, from what I have played, with tiled games and tiled level editors. Because it's an online level editor and MMO, > 64 pix would not be ideal.


Your example makes sense, but I'm not sure I'm 100% understanding how to implement it.
Here's what I've grasped from it, or at least a version that sounds like it does the same:

Code: Select all

 
void test(IAnimatedMeshSceneNode* gridMesh)
{
    // Grab the tileset image and convert to an IImage
    ITexture* tilesetTex = Game::driver->getTexture("Tilesets/tileset2.png");
    IImage* tilesetImg = Game::driver->createImageFromData (
        tilesetTex->getColorFormat(),
        tilesetTex->getSize(),
        tilesetTex->lock(),
        false
    );
 
    // Create a blank grid image
    const dimension2d<unsigned int> d(640, 480); // Width and Height of a grid
    IImage* gridImg = Game::driver->createImage (
        tilesetTex->getColorFormat(),
        d
    );
 
    // Loop through the amount of tiles needed to fill the new grid image
    for (int x = 0; x < 640/16; x++)
    {
        for (int y = 0; y < 480/16; y++)
        {
            // Copy a single tile to the new grid image
            tilesetImg->copyTo(gridImg, vector2d<int>(x*16,y*16), rect<s32>(0,0,16,16));
        }
    }
    tilesetImg->drop();
 
    // Now I could grab this texture without re-creating it
    video::ITexture* newTexture = Game::driver->addTexture("newGrid.png",gridImg);
 
    gridMesh->setMaterialTexture(0, newTexture);
}
 
Doing the above increased my FPS to > 300.


Thanks for your help, I think I don't need to worry about this anymore at the moment, but it's here if anyone wants to see how I did it.
devsh
Competition winner
Posts: 2057
Joined: Tue Dec 09, 2008 6:00 pm
Location: UK
Contact:

Re: 2D Tiled World - Performance

Post by devsh »

Even 32bit pix would be an improvement.

I don't see how your pseudocode achieves scrolling (it seems to do a 1:1 copy filling the entire screen with one tile-type), but that would kind-of be the start.

For scrolling as you move away from the (0,0,0) position you need the modulo trick :)
[and your width/height needs to be 1 or 2 tiles larger in each dimension than the screen]

P.S. I don't like the fact that you're not using glTexSubImage2D, re-creating textures is very expensive as opposed to updating regions.
I understand stock-irrlicht doesn't make it easy, but its possible to fish out the GLuint native texture object handle through a static/dynamic cast.
Alternatively do lock/unlock with manual filling, its still faster than creating and destroying textures.
Unless you actually plan on accelerating the tile copying with the GPU via drawing them to a render-target, you don't actually need your tileset texture as an ITexture, it could be an IImage.
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

The scrolling has been implemented in my GitHub link under source file "player.lua" in function updateGridPosition() (used with the IGUIImages version, but still works the same for IBillboardSceneNode)

I haven't really tried it yet but theoretically I could grab newTileset.png without re-creating textures since I called driver->addTexture() with the new texture, right?
devsh
Competition winner
Posts: 2057
Joined: Tue Dec 09, 2008 6:00 pm
Location: UK
Contact:

Re: 2D Tiled World - Performance

Post by devsh »

The code in your link is hard to read. Let me show with some ascii art what you should see if you implemented scrolling properly.

Assume a world with 16 pix tiles 4x4 tiles and a screen that has 2x2 tiles (which needs at least a 1 tile border, so 3x3), and assume the upper left corner of the screen is at position (18,18) which is inside tile (1,1) our of the 4x4.
{Y goes down, upper left is origin}

So world tiles:
ABCD
EFGH
JKLM
NOPR

Screen tiles would be:
DBC
HFG
MOP

Notice how the tiles do not change position after first coming into view, adressing is module 3.
I haven't really tried it yet but theoretically I could grab newTileset.png without re-creating textures since I called driver->addTexture() with the new texture, right?
Elaborate?

From what I understand you are talking about the tileset image/texture, that is completely orthogonal to what I proposed. In-fact the tileset does not need an ITexture at all, and using createImage instead of addTexture will save you VRAM and stutters.

The texture I was talking about was the gridImage, instead of being recreated with `Game::driver->addTexture("newGrid.png",gridImg);` you could update via irrlicht's lock/unlock or even better glTexSubImage2D with explicit region (lock/unlock will be n^2, TexSubImage2D will be n)

the most important thing is to make sure that glTexImage2D is never being called mid-frame, but that glTexSubImage2D is being called instead.
LunaRebirth
Posts: 386
Joined: Sun May 11, 2014 12:13 am

Re: 2D Tiled World - Performance

Post by LunaRebirth »

This is solved. The code in my link shouldn't be hard to read, it's just in Lua instead of C++. Scrolling, as stated, is implemented properly.

That is, we get the player's position in the world divided by the size of a grid and store it into memory.
When this is done, we take this grid position and create a 2D array around it (E.G if the grid is at {0,0}, we will have all adjacent grids like { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, … } then create/load a grid at the position multiplied by the grid width/height. When the grid position changes, we compare the new grid array with the old grid array. This gives us the new grids that are being loaded/created. Doing it the opposite way, comparing the new grid to the old grid, we get the grids that are being destroyed, and remove them accordingly.

That is how the scrolling works, and the code is viewable in the github link I've provided.
devsh wrote:Elaborate?
I only need to create the first ever loaded grid. Using the C++ code I provided above, I called driver->addTexture("newGrid.png", tex). Now I don't ever need to create a new grid, instead I can simply grab it by calling driver->getTexture("newGrid.png").

It must be an IImage so that I can copy part of one IImage (the tileset) to the new IImage (a grid, which is just a bunch of grass tiles). Then it must be added as a texture so that I can use it in an IBillboardSceneNode.
CuteAlien
Admin
Posts: 9734
Joined: Mon Mar 06, 2006 2:25 pm
Location: Tübingen, Germany
Contact:

Re: 2D Tiled World - Performance

Post by CuteAlien »

You can also copy into textures. Use lock() to get the memory and then just write the bytes into it. You just have to be a little careful with texture memory because it might have some padding on the right side of the image. So instead of getWidth you have to work with getPitch () which is the real size in bytes per line. It doesn't matter what you write into the padding bytes, it's only important that you start your next line at oldLineStart + getPitch() or it will mess up on graphic cards which do the padding bytes.
IRC: #irrlicht on irc.libera.chat
Code snippet repository: https://github.com/mzeilfelder/irr-playground-micha
Free racer made with Irrlicht: http://www.irrgheist.com/hcraftsource.htm
Post Reply