Generating tilesheets with masking cause noticable drop of FPS

Hello,

I want to generate multiple tilesheets with different terrain textures at game initialize.

Terrains
Terrain

Master tilesheet
Master Tile

Border tilesheet

Using masking I blend each terrain with the master tile to create 3 different terrains. After that a border with masking is also added and the tilesheet is saved using RenderTarget.

Okay, the issue is that the code works, but running it creates a drop in FPS. (Note that the tilesheet saving is only called once)

I don’t know if monogame is having trouble to cache a few tilesheets or if I’m allocating huge amounts of unused memory. Any help or suggestions will be appreciated.

The TileCache Static Class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;

namespace BuildTest
{
    static class TileCache
    {
        private static GraphicsDevice graphicsDevice = Asset.GraphicsDevice;

        private static Texture2D[] terrains;
        private static Texture2D texture;
        private static Texture2D textureMask;

        private static Texture2D textureShaderColor;
        private static Texture2D textureShader;

        private static Vector2[] sourcePos;

        private static RenderTarget2D renderTarget;
        private static Matrix projection;

        private static AlphaTestEffect alphaEffect1;
        private static AlphaTestEffect alphaEffect2;

        public static Texture2D Terrain(int id)
        {
            return terrains[id];
        }

        private static Dictionary<int, Texture2D> terrainShaders;

        public static bool IsCached { get; set; }

        public static void Initialize(Vector2[] _sourcePos, Texture2D _texture, Texture2D _textureMask)
        {
            //Shaders
            terrainShaders = new Dictionary<int, Texture2D>
            {
                [0] = new SolidColorTexture(graphicsDevice, Color.Transparent),
                [1] = new SolidColorTexture(graphicsDevice, Color.White),
                [2] = new SolidColorTexture(graphicsDevice, new Color(113, 89, 60, 255)),
                [3] = new SolidColorTexture(graphicsDevice, new Color(157, 234, 128, 255)),
            };

            //Setting the different terrains
            sourcePos = _sourcePos;

            //Setting the textures
            texture = _texture;
            textureMask = _textureMask;

            textureShader = Asset.GetTexture("textures/tilesheet/maskshader_01");
            textureShaderColor = new SolidColorTexture(graphicsDevice, Color.White);

            //setting the terrain array size
            terrains = new Texture2D[sourcePos.Length];

            //Projection
            projection = Matrix.CreateOrthographicOffCenter(0, textureMask.Width, textureMask.Height, 0, 0, 1);

            //Render
            renderTarget = new RenderTarget2D(graphicsDevice, textureMask.Width, textureMask.Height, false, graphicsDevice.PresentationParameters.BackBufferFormat, DepthFormat.Depth24Stencil8);

            //Stencils
            alphaEffect1 = new AlphaTestEffect(graphicsDevice)
            {
                AlphaFunction = CompareFunction.Equal,
                ReferenceAlpha = 127
            };

            alphaEffect1.Projection = projection;

            alphaEffect2 = new AlphaTestEffect(graphicsDevice)
            {
                AlphaFunction = CompareFunction.Greater,
                ReferenceAlpha = 130
            };

            alphaEffect2.Projection = projection;
        }

        private static void Shader(GameTime gameTime, SpriteBatch sb)
        {
            sb.Draw(textureShader, new Vector2(0, 0), Color.White);
        }

        private static void ShaderColor(SpriteBatch sb, int key)
        {
            sb.Draw(terrainShaders[key], new Rectangle(0, 0, textureShader.Width, textureShader.Height), Color.White);
        }

        private static void Mask(GameTime gameTime, SpriteBatch sb)
        {
            sb.Draw(textureMask, new Vector2(0, 0), Color.White);
        }

        private static void DrawSurface(SpriteBatch sb, int key)
        {
            for (int x = 0; x < textureMask.Width / TileMap.tileWidth; x++)
            {
                for (int y = 0; y < textureMask.Height / TileMap.tileHeight; y++)
                {
                    sb.Draw(texture, new Vector2(x * TileMap.tileWidth, y * TileMap.tileHeight), new Rectangle((int)sourcePos[key].X, (int)sourcePos[key].Y, TileMap.tileWidth, TileMap.tileHeight), Color.White);
                }
            }
        }

        private static void ResetDraw()
        {
            graphicsDevice.SetRenderTarget(null);

            graphicsDevice.DepthStencilState = new DepthStencilState() { DepthBufferEnable = true };

            graphicsDevice.SetRenderTarget(renderTarget);

            graphicsDevice.Clear(Color.Transparent);
        }

        public static void Cache(GameTime gameTime, SpriteBatch spriteBatch)
        {
            for (int i = 0; i < sourcePos.Length; i++)
            {
                ResetDraw();

                //First masking
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.AlwaysStencilState, null, alphaEffect1);
                Mask(gameTime, spriteBatch);
                spriteBatch.End();

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, alphaEffect2);
                Mask(gameTime, spriteBatch);
                spriteBatch.End();

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.EqualStencilState, null, null);
                DrawSurface(spriteBatch, i);
                spriteBatch.End();

                Texture2D saveMasking = new Texture2D(graphicsDevice, textureMask.Width, textureMask.Height);

                Color[] Maskdata = new Color[textureMask.Width * textureMask.Height];
                renderTarget.GetData(Maskdata);
                saveMasking.SetData(Maskdata);

                ResetDraw();

                //Shaders
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.AlwaysStencilState, null, alphaEffect1);
                Shader(gameTime, spriteBatch);
                spriteBatch.End();

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, alphaEffect2);
                Shader(gameTime, spriteBatch);
                spriteBatch.End();

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.EqualStencilState, null, null);
                ShaderColor(spriteBatch, i);
                spriteBatch.End();

                Texture2D saveShader = new Texture2D(graphicsDevice, textureMask.Width, textureMask.Height);
                Color[] shaderData = new Color[textureMask.Width * textureMask.Height];
                renderTarget.GetData(shaderData);
                saveShader.SetData(shaderData);

                ResetDraw();

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, null);
                spriteBatch.Draw(saveMasking, new Vector2(0, 0), Color.White);
                spriteBatch.Draw(saveShader, new Vector2(0, 0), Color.White);
                spriteBatch.End();

                terrains[i] = new Texture2D(graphicsDevice, textureMask.Width, textureMask.Height);

                Color[] data = new Color[textureMask.Width * textureMask.Height];
                renderTarget.GetData(data);
                terrains[i].SetData(data);

                graphicsDevice.SetRenderTarget(null);
            }

            renderTarget.Dispose();
            renderTarget = null;
        }
    }
}

Okay, the issue is that the code works, but running it creates a drop in FPS. (Note that the tilesheet saving is only called once)

As you in you set a breakpoint and know for certain that it is only being called once?

That unused and unassigned IsCached looks suspicious. Are you setting that elsewhere, or not doing such and checking it before you call to build your cache?

Otherwise, everything there looks moot in the big scheme of things (misc bad things: double-for-loop new nesting, not disposing your junk SolidColorTextures created when initializing the dictionary, etc) as long as it really is only running the caching once.


If the cache really is only built once then you’ll want to look at the drawing you do with the cache. If you’re constantly switching back and forth between different terrain types than you’re going to slaughter SpriteBatch with constant flushing.

If you previously were using one big tilesheet than that’s likely the case.

  • In your ResetDraw function you should not create a new DepthStencilState every frame and if you do create a new one you should dispose of the old one. In this case you can just use DepthStencilState.Default. I also do not understand the point of switching the render target inside it.
  • Instead of doing GetData followed by SetData, just render the first texture to the second. That’s faster because it can do the operations in full on the GPU.

You can get performance hits if you fill up VRAM too much. Just disposing of the textures you create could fix your issue.

Is the caching operation itself not really slow as well? It looks like it can be optimized a lot. Could you explain in some more detail what each draw call in the loop in Cache is doing? That will make it easier to understand and help optimize it :slight_smile: Intuitively it seems like you don’t really need to loop over the source positions.

@AcidFaucent, @Jjagg The IsCached is being called in a MapManager class that I have. I am running the cache method, at draw. I think that the method should be optimized aswell, since it doesn’t seems smart to have to run the if statement at each draw.

I’m sure that the cache draw runs once. I have used a Console.WriteLine() for that.


        public static void Cache(GameTime gameTime, SpriteBatch spriteBatch)
        {
            if (!TileCache.IsCached)
            {
                TileCache.Cache(gameTime, spriteBatch);

                TileCache.IsCached = true;
            }
        }

```

I will optimize the things you mentioned. When I load a map, then I create an `array[mapWidth*mapHeight] . The array has on each coordinate assigned a Tile class. The Tile class has properties, that allows me to set pos, rect, and texture. In the cache (that runs at game start) generates 3 tilesheets out of the three terrains (water, dirt, grass). I assign the specific tilesheet to each specific Tile object texture. The MapManager runs through the array and runs the Tile objects draw method, which is a simple spriteBatch.draw and spriteBatch.Begin and End() is not called on each Tile object.
 
You're completely right in your two bulletpoints. I will probably try to rewrite the code and make it more understandable. The sourcePos loop, is to go through the 3 terrains, so that I can blend them with the mastertile and create 3 tiles with each terrain texture. (water, dirt, grass).

It just hit me that I'm masking the mask to achieve transparency, but I can just use the AlphaFunction = CompareFunction.Equal with ReferenceAlpha = 127 without having to mask the mask to achieve transparency that is already there. Its the first time I have used stencils, so mistakes have to be done.

A quick explanation of one part.

```

// Drawing the mastertile using the alphaeffect 1 with the mastertile using the alphaeffect 2 to achieve trasnparency and only the red part to mask.
                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.AlwaysStencilState, null, alphaEffect1);
                Mask(gameTime, spriteBatch);
                spriteBatch.End();

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null, alphaEffect2);
                Mask(gameTime, spriteBatch);
                spriteBatch.End();

// I draw a 6*8 times a single terrain to then blend the master tilesheet and then get a tilesheet of a terrain.

                spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, TileMap.EqualStencilState, null, null);
                DrawSurface(spriteBatch, i);
                spriteBatch.End();

// I save the generated tilesheet of a terrain and save it. So that I can thereafter add colored borders using again masking.
                Texture2D saveMasking = new Texture2D(graphicsDevice, textureMask.Width, textureMask.Height);

                Color[] Maskdata = new Color[textureMask.Width * textureMask.Height];
                renderTarget.GetData(Maskdata);
                saveMasking.SetData(Maskdata);

```

I got the masking inspiration from [link](https://cdn-images-1.medium.com/max/1200/1*zCwXK83QR4Hw_QKH_4HMfg.png)