Legacy GM Tips For Rendering Optimization

Anixias

Member
Currently, I have an isometric-projected ds_grid containing tiles (this ds_grid is the terraindata grid, containing enumeration values such as tile.grass or tile.water, etc.)
I have another grid of a matching size called variant and every cell is set to random(1) on the grid's initialization. This is so that if I have multiple graphics for a single tile, when rendering, it multiplies sprite_get_number(spr) by the corresponding number in variant to get a subimage, to remove repeated textures. I made 2 different grass textures, and you can not tell it is repeated at all. It's pretty fantastic. Adding a third tile would erase virtually all repeats.

So my problem comes into the fact that 32x16 tiles are being rendered in 1080p without scaling, resulting in a large amount of tiles being rendered. I also applied min/max row/column when looping to reduce draw calls by only drawing what is on screen, and although this helped a lot, my beast computer gets 60-70 fps_real when in 1080p. Note, I have put no gameplay, and the only step event code is to convert mouse coordinates into isometric world coordinates.

My initial thought was to collapse like-tiles into 2x2 or even 3x3 or 4x4 tiles, to severely reduce call count. This would work with tiles that had only one texture, such as water, but with grass, I would either have to lose the variant grid or create many different subimages in the collapsed tile graphics (For example, 1x1 grass has 2 possible textures, 2x2 grass has 16 different possible images, and 3x3 grass has 512 possibilities, and 4x4 grass has 65,536 possibilities. Obviously, this is not feasible).

My question is, how could I improve my FPS if I require the variant grid? If not that, how can I optimize while also reducing noticeable tiling?
 

Tthecreator

Your Creator!
What happens to your fps if you put all the tiles in a single primitive? If you have a tileset texture then you can just easily specify and draw parts of that from the same texture.

If your world is just a flat plain, you just draw a gigantic square and use a shader to fill in the textures.
 

Anixias

Member
Use a shader to fill in the textures on a primitive?
Well, the terrain will either be flat or be artificial 3D (such as in an rpg with a cliff texture, your character just looks like they are rising), and the terrain will never change once created, so I could try this.

Now, how exactly would I make this shader? I barely know enough logic to make a black and white, invert, and sepia shader, much less applying multiple textures to a primitive.

EDIT:
The whole world is not one single tile type, there would also be water and maybe sand or rock. Maybe.
 
Last edited:

Anixias

Member
That did virtually nothing to my FPS.

Update: Now I get approx. 40-45 FPS. Hardly any changes, none should affect FPS.
 
Last edited:
T

TimothyAllen

Guest
I wouldn't think 512 draw calls would cause that much dip in fps (especially considering you called your pc a beast). What does your loop that draws proccess / draws the tiles look like.

The key is to organize your data in such a way that is efficient for the CPU to process. Such as making data accessed in a cache friendly way and making branches predictable or remove them all together. (Assuming the bottle neck is at the CPU, which I do).
 
M

MrFox

Guest
Anixias,

May I suggest you to inactivate your draw events, then only display your fps to see if you have not already an issue with another part than the draw?

TimothyAllen,

I guess we talk more about 4050 draw calls just for the terrain tiles, and maybe more if he have multiple layers. But even with this, I agree that there is no reason to have such a bottleneck, except if he have complexe branches in his code.
 
T

TimothyAllen

Guest
I guess we talk more about 4050 draw calls
I read it wrong. I thought he meant he was draw 32x16 tiles as in 32 columns and 16 rows of tiles... Not that his tiles were 32x16 lol. My guess is he must be branching quite a bit in his draw loop (check terrian properties)

The game I'm currently working on does something similar (draws a certain sprite based on the grid cells data). But I only process this data when needed (grid changes or view moves some distance) and put all the tile info into an array which I just loop through and draw without using any branches.
 

Anixias

Member
Here is my rendering code:
Code:
///World Map
if world_map
{
    var minx = floor(clamp(0,iso_cart_x(view_xview[0],view_yview[0])/tile_size,ds_grid_width(terraindata)-1));
    var maxx = ceil(clamp(0,iso_cart_x(view_xview[0]+view_wview[0],view_yview[0]+view_hview[0])/tile_size+1,ds_grid_width(terraindata)));
    var miny = floor(clamp(0,iso_cart_y(view_xview[0]+view_wview[0],view_yview[0])/tile_size-4,ds_grid_height(terraindata)-1));
    var maxy = ceil(clamp(0,iso_cart_y(view_xview[0],view_yview[0]+view_hview[0])/tile_size+1,ds_grid_height(terraindata)));
 
    //Terrain
    for(var i = minx; i < maxx; i++)
    {
        for(var j = miny; j < maxy; j++)
        {
            var color = c_white;
            var spr = -1;
            if system.user_id != landownerdata[# i,j] color = c_gray;
            switch(terraindata[# i,j])
            {
                default:
                case tile.grass:
                    spr = tile_grass;
                    break;
                case tile.water:
                    spr = tile_water;
                    break;
            }
            draw_sprite_ext(spr,variant[# i,j]*sprite_get_number(spr),cart_iso_x(i*tile_size,j*tile_size),cart_iso_y(i*tile_size,j*tile_size),1,1,0,color,1);
            if xOver == i and yOver == j
            {
                //Handle clicking tiles here
                //Go into a tile
                if mouse_check_button_pressed(mb_left) //For now. Soon, a popup of info will appear. Click on, and release on, the load button.
                {
                    //Also, make sure it isn't a water tile. No sea-cities.
                    if terraindata[# i,j] != tile.water
                    {
                        //And, make sure this player owns this tile
                        if system.user_id == landownerdata[# i,j]
                        {
                            load_into = true;
                            block_x = i;
                            block_y = j;
                        }
                    }
                }
                if mouse_check_button_pressed(mb_right)
                {
                    //Also, make sure it isn't a water tile. No sea-cities.
                    if terraindata[# i,j] != tile.water
                    {
                        //And, make sure no player owns this tile
                        if landownerdata[# i,j] == -1
                        {
                            var uid = system.user_id;
                            var pos = -1;
                            for(var u = 0; u < ds_grid_width(playerdata); u++)
                            {
                                if playerdata[# u,0] == uid
                                {
                                    pos = u;
                                    break;
                                }
                            }
                            if pos >= 0
                            {
                                if playerdata[# pos,3] >= 30000
                                {
                                    playerdata[# pos,3] -= 30000;
                                    landownerdata[# i,j] = uid;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    //Objects
    for(var i = minx; i < maxx; i++)
    {
        for(var j = miny; j < maxy; j++)
        {
            var color = c_white;
            //if xOver == i and yOver == j color = c_red;
            var spr = -1;
            switch(objectdata[# i,j])
            {
                default:
                    break;
                case object.city:
                    spr = spr_city;
                    break;
            }
            if sprite_exists(spr) draw_sprite_ext(spr,0,cart_iso_x(i*tile_size,j*tile_size),cart_iso_y(i*tile_size,j*tile_size),1,1,0,color,1);
        }
    }
    //Mouse Grid
    if xOver == clamp(xOver,0,ds_grid_width(terraindata)-1) and yOver == clamp(yOver,0,ds_grid_height(terraindata)-1)
    {
        draw_set_color(c_white);
        var xx = cart_iso_x(xOver*tile_size,yOver*tile_size);
        var yy = cart_iso_y(xOver*tile_size,yOver*tile_size);
        draw_sprite(spr_tile,0,xx,yy);
        if yOver == ds_grid_height(terraindata)-1 draw_sprite(spr_tile,1,xx,yy);
        if xOver == ds_grid_width (terraindata)-1 draw_sprite(spr_tile,2,xx,yy);
    }
}
Mouse Grid section should have no effect. It just draws a tile graphic (in parts, due to depth) where the mouse is.
The cart/iso functions convert coordinates to screen projections and vice versa.

EDIT:
Disabling this script increased my average FPS to 2000.

EDIT2:
All of the FPS loss comes from rendering the terrain, the first nested for-loops, near the top.

btw, this is actually up to 7,917 draw calls...

I simply set var count = 0 before the loop, called count++ in the most nested loop, and then printed count to the debug console. The highest I could get was 7,917. Not a great number...
 
Last edited:
T

TimothyAllen

Guest
Have you removed all logic and just draw an abritrary tile to see what kind of impact all those condition branches are having (Im predicting a lot). If you get a significant performance boost, then you need to streamline your data and only run the logic portions when needed (if the grid changes / view has moved some distance).
 

Anixias

Member
There is already virtually no logic. This is the vast majority of the code in the entire game. Commenting out this one script sets my FPS to average around 2000.
The grid only changes on map change, and the view will almost always be moving.

It definitely is only that terrain loop, because reducing the map size significantly improves FPS, as does moving the view away from the terrain, to stop tiles from rendering. If my view is outside the map, my FPS averages 2000.
 
T

TimothyAllen

Guest
There is already virtually no logic. This is the vast majority of the code in the entire game. Commenting out this one script sets my FPS to average around 2000.
The grid only changes on map change, and the view will almost always be moving.

It definitely is only that terrain loop, because reducing the map size significantly improves FPS, as does moving the view away from the terrain, to stop tiles from rendering. If my view is outside the map, my FPS averages 2000.
I'm talking about that loop... You have a crap ton of branching going on there. REMOVE THE BRANCHES to see his much they are impacting performance. As in only draw some arbitrary tile (so all the draw calls are still there) but with absolutely no branching in the loop.
 

Anixias

Member
After rendering only tiles, I get 80-90 FPS. (in 720p)
60-70FPS in 1080p

I'm pretty certain my problem is just under 8000 draw calls per frame.
 
Last edited:
T

TimothyAllen

Guest
So no change? You simply did:
Code:
for (i blah...)
{
    for (j blah...)
    {
        draw_sprite_ext(blah);
    }
}
And the fps was no different then running that huge code above?

Edit: obviously that's not real code.
 
T

TimothyAllen

Guest
8000??? How many texture swaps? Those two combined are your problem right there.

My project has about 5 - 10 draw calls in comparison (with 1 to 2 Million vertices) and able to maintain 1000 FPS at 2560 x 1080.
8k is quite a lot but I find it hard to believe the branches have no impact.

Edit: you do bring up a good point about optimizing his texture pages.
 
Last edited by a moderator:

Anixias

Member
I have 3 textures in the game.....
This game is extremely early in development, I just made a world-generation script and projection scripts and now I'm rendering 3 textures.
The texture swaps shouldn't be a current issue.
The branches did have 0FPS average impact.
Did you read the branches? They only perform on mouse clicks.

I just noticed that since my world map is 100x100 tiles, I've got 10,000 draw calls....

Scared of the in-game 200x200 map.

EDIT:
I've been thinking about saving around 4 surfaces that I render nearby tiles to, and then just render those surfaces, so I only have to actually loop when moving the view far enough to change chunks. Should I try this?
 
T

TimothyAllen

Guest
If you don't plan on exporting to mobile. Mobile doesnt like large surfaces. (including the application surface)
 

Anixias

Member
Mobile controls would be annoying for a game like this. Think of Sid Meier's Civilization V and then port it to mobile, mentally. Too many buttons for a small screen.

So, I need to figure out how I'm going to go about this chunk-rendering idea.
 

sp202

Member
No idea if this is a good approach but it would greatly reduce the number of draw calls: draw all the tiles onto a surface in the create event and then draw only the surface. Obviously you'd need to redraw the tiles if the surface gets destroyed or you could create a sprite out of the surface and not worry about volatility.
 
A

anomalous

Guest
GMS1.4 is not built for speed or optimization when it comes to tiles, or large rooms in general. However, based on what you wrote, your design is likely the issue.

Room sized, or even view sized loops that run every step are not typically good.
Drawing sprites instead of tiles, is typically not good.
If you want to randomly mix your grass tiles do it up front, why would that need to be done over and over at run-time? Maybe I misunderstand what you are doing.
Why not use tiles?

Suggestions:
1. use tiles
2. test the largest room size and tile count in a test room and carefully review in debugger average time, engine time, fps. Maybe include panning with arrow keys so you can see it dynamically.
3. If issues, consider adding tile rows/columns to the room in a buffer around view as needed, instead of looping the entire screen grid and drawing sprites and doing calculations (?) at run time.
Precalculate everything that can be. Any loops should be as optimized as possible, a few ds_grid gets, macros/enums/local vars, and only run when needed.

In my testing, filling a large room with 4 layers of tiles, brought the ENGINE to a crawl (this is not a draw issue, it only draws in view) This necessitated actually adding and removing tiles in a buffer around the current view, at run-time. However, in doing that, only the necessary tile row and column added, was looped, and only when it changed. Same with deleting tiles. It would only run the entire view loop of tiles if you "teleported" or at room start, where a slight pause was fine or masked with a transition effect (but it was still quite fast) And the ds_grids that held the tiles for the room was not a single grid. It was 3 grids that held the background, left, and top coordinates, so that each could be pulled with no significant calculation.

But do you have to do that? I don't know, that's why you test it, otherwise info on the forums will be outdated and based on mysticism. I can tell you that with GMS1.4 I had no issues generating 4 layers of tiles on 1920x1200 view dynamically adding/removing a buffer around view. I don't recall how fast it ran, but it was maybe 20% of a 60fps budget.
 

Anixias

Member
I'm not sure if tiles would be the best approach, but I can surely test them. So, every time the map changes, I should delete all current tiles and then convert the ds_grid to tiles? If that doesn't work, I will attempt some form of surface method, to just render sprites to a surface once when needed, and just draw surfaces.
 
A

anomalous

Guest
You said you had a ds_grid with tile data, I'm just worried that if you don't think tiles are a good approach, perhaps we're missing something?

Yes, at room start you loop through your tile grid, and add all those tiles to the room. deleting all tiles in a room is a function, use it instead of looping yourself. If you change rooms they may auto delete anyway (not sure I'd trust it?)

Notice the testing i mention above. If you do rooms too large, you will need instead to dynamically add/remove tiles just outside the view, ensuring the view is always populated with tiles, but the rest of the room is empty. This is moderately difficult, but runs quite fast, no issues. The only additionally big need is I think another ds_grid that you record which tiles are "placed in the room" vs those that are not.

Surface is only good if you have a room the size of an appropriate surface size. Otherwise I'd like someone to explain how they think that would help. Creating the surface at view size is slow, its only useful when you can do it once at room load in my experience.
 
T

TimothyAllen

Guest
I really think you should look into using a vertex buffer. I just did a quick test and i was able to draw 10k tiles using a vertex buffer and maintain over 1k fps vs draw 10k tiles using draw_sprite in a loop and only getting 100 fps
 
M

MishMash

Guest
This is a post I made a while ago regarding a few common optimisations that you can make: https://forum.yoyogames.com/index.p...downscaling-a-full-hd-game.17264/#post-111272

As others have suggested, vertex buffers are very useful, they also avoid you having to constantly run through this loop, as you only need to perform all of those calculations the one time. You have the right idea, you can also use surfaces to reduce draw calls. I do this in my game with a process I can "strip" rendering. As our rendering is rather complex, it adds alot of variation etc; we only update the draw surface as the player moves, rendering a strip of blocks at a time. This is done by keeping a surface which is bigger than the size of the screen, and shifting everything along as each strip is rendered. This massively improved performance.

One quick thing you may be able to do is ensure all your tiles are on the same texture page. Any swap in textures across different texture pages will result in a vertex batch break. Under the hood, GM batches lots of simultaneous draw calls into one by building dynamic vertex buffers on the fly. If however the rendering state changes, it needs to break the batch, submit the draw call and start a new batch. You can monitor whether this is happening using software like PIX.

You said you had a ds_grid with tile data, I'm just worried that if you don't think tiles are a good approach, perhaps we're missing something?
Tiles actually get quite slow if you aren't careful, you are far better off using a vertex-buffer based solution. This is because in many cases, it is better to have full control over your own rendering system, than let GM do what it does. Tiles are a simple and fast solution, however they do not cater to more niche scenarios.
 

Anixias

Member
I have never worked with vertex buffers so I'm not sure how to do that. I do know how buffers work, though, it's just that I've never learned how to use vertex buffers. The manual is useful but I would still like to figure it out. Anywhere I could find a vertex buffer tutorial?
 
M

MishMash

Guest
GameMaker Shader Overview - Part 1
GameMaker Shader Overview - Part 2
GameMaker Shader Overview - Part 3
GameMaker Shader Overview - Part 4

These tutorials should give you a strong insight into the render process as a whole. Part 4 is the only section which targets vertex buffers specifically, however the other 3 sections should give you a good amount of primer information to make sure you understand what you are actually doing when constructing geometry. If you have any additional queries with them after this, you can always post more specific questions on the forums as well :)
 
Top