poor tile render performance with overlaid sprites?

I’ve been looking at efficient ways to render a large number of tiles. I adapted my code from here: capitalgstudios.wordpress.com/2019/01/03/spritebatch-vs-vertexbuffer/.

This can render 250k tiles at 60fps. You can zoom out and have all tiles on the screen at once with no impact on the performance.
However, if I render the same number of tiles in a tighter formation, sprites overlaid on top of each other, then render performance decreases a lot, FPS went down to ~14. You can try out and toggle with F1 to see the difference.

Why is this happening?

screenshots:

graphic used:
earth

font used:

https://github.com/bo3b/3Dmigoto/blob/master/arial.spritefont

test code:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System;
using System.Collections.Generic;

namespace SampleTemplate
{
    public class Examples : Game
    {
        public static SpriteBatch SpriteBatch = null;
        public static SpriteFont SpriteFont = null;

        // Constants
        private const int QuadPixelSize = 32;
        private const int QuadTotalVertices = 6;

        // Quad Data
        private int m_iWidth = 500;
        private int m_iHeight = 500;
        private int m_iQuadTotal = 0; // number of quads IE =  m_iWidth * m_iHeight

        // misc
        public Random Random = new Random();
        private GraphicsDeviceManager m_oGraphicDevice = null;
        private string sMode;
        public bool tileLayout_grid = true;

        // Textures
        private Texture2D m_oTextureQuad = null;

        // Effects
        private BasicEffect m_oBasicEffect = null;

        // Vertex Buffers
        private VertexBuffer m_oVertexBuffer = null;

        // Input
        private KeyboardState m_oPreviousKeyboardState = Keyboard.GetState();

        // Components
        private FrameRateCounter m_oFrameRateCounter = null;
        private Camera m_oCamera = null;


        public Examples()
        {
            m_oGraphicDevice = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            m_oFrameRateCounter = new FrameRateCounter(this); // add basic FPS counter
            m_oCamera = new Camera(this);
            Components.Add(m_oFrameRateCounter);
            Components.Add(m_oCamera);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            SpriteBatch = new SpriteBatch(GraphicsDevice);

            // Load Textures
            m_oTextureQuad = Content.Load<Texture2D>("earth");

            // Load fonts
            SpriteFont = Content.Load<SpriteFont>("arial");

            // Effects
            m_oBasicEffect = new BasicEffect(GraphicsDevice);
            m_oBasicEffect.TextureEnabled = true;
            m_oBasicEffect.Texture = m_oTextureQuad;
            GraphicsDevice.BlendState = BlendState.NonPremultiplied;
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            m_oGraphicDevice.ApplyChanges();
            //m_oBasicEffect.VertexColorEnabled = true; // if enabled I won't be able to use VertexPositionTexture

            Vector3 oCameraPosition = new Vector3(0.0f, 0.0f, 1); // 100000.0f
            Vector3 oCameraTarget = new Vector3(0.0f, 0.0f, 0.0f); // Look back at the origin

            float fFovAngle = MathHelper.ToRadians(45);  // convert 45 degrees to radians
            float fAspectRatio = GraphicsDevice.Viewport.Width / GraphicsDevice.Viewport.Height;
            float fNear = 0.01f; // the near clipping plane distance
            float fFar = 1500000f; // the far clipping plane distance

            Matrix oWorld = Matrix.CreateTranslation(0.0f, 0.0f, 0.0f);
            Matrix oView = Matrix.CreateLookAt(oCameraPosition, oCameraTarget, Vector3.Up);
            Matrix oProjection = Matrix.CreatePerspectiveFieldOfView(fFovAngle, fAspectRatio, fNear, fFar);
            m_oBasicEffect.World = oWorld;
            m_oBasicEffect.View = oView;
            m_oBasicEffect.Projection = oProjection;

            CreateTiles();
        }


        protected override void Update(GameTime gameTime)
        {
            KeyboardState oCurrentKeyboardState = Keyboard.GetState();

            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            {
                Exit();
            }

            if (m_oPreviousKeyboardState.IsKeyDown(Keys.F1) && oCurrentKeyboardState.IsKeyUp(Keys.F1))
            {
                tileLayout_grid = !tileLayout_grid;
                CreateTiles();
            }

            if (m_oPreviousKeyboardState.IsKeyDown(Keys.Up) && oCurrentKeyboardState.IsKeyUp(Keys.Up))
            {
                m_iWidth += 5;
                m_iHeight += 5;

                CreateTiles();
            }

            if (m_oPreviousKeyboardState.IsKeyDown(Keys.Down) && oCurrentKeyboardState.IsKeyUp(Keys.Down))
            {
                if (m_iWidth - 5 > 0)
                {
                    m_iWidth -= 5;
                }

                if (m_iHeight - 5 > 0)
                {
                    m_iHeight -= 5;
                }

                CreateTiles();
            }

            m_oPreviousKeyboardState = oCurrentKeyboardState;

            base.Update(gameTime);
        }



        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            m_oBasicEffect.View =  m_oCamera.View;

            GraphicsDevice.SamplerStates[0] = SamplerState.PointClamp;
            GraphicsDevice.SetVertexBuffer(m_oVertexBuffer);
            foreach (EffectPass oEffectPass in m_oBasicEffect.CurrentTechnique.Passes)
            {
                oEffectPass.Apply();
                GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, m_iQuadTotal * 2); // 2* as it is two triangles
            }
        
            // text output
            SpriteBatch.Begin();
            SpriteBatch.DrawString(SpriteFont, $"Rendering {m_iQuadTotal} tiles, layout: {sMode} (Cam X: {m_oCamera.m_oPosition.X}, Y: {m_oCamera.m_oPosition.Y}, Z: {m_oCamera.m_oPosition.Z})", new Vector2(100, 5), Color.Black); //  drop shadow
            SpriteBatch.DrawString(SpriteFont, $"Rendering {m_iQuadTotal} tiles, layout: {sMode} (Cam X: {m_oCamera.m_oPosition.X}, Y: {m_oCamera.m_oPosition.Y}, Z: {m_oCamera.m_oPosition.Z})", new Vector2(99, 4), Color.White);
            SpriteBatch.End();
            
            base.Draw(gameTime);
        }


        #region TILE MAP
        private void CreateTiles()
        {
            if (tileLayout_grid) // depending on the tile layout mode, we recreate the tiles and move the camera there
			{
                CreateTiles_grid();
                sMode = "Grid";
                m_oCamera.m_oPosition = new Vector3(-8000, 8100, 19701);
            } 
            else
			{
                CreateTiles_tight();
                sMode = "Tight";
                m_oCamera.m_oPosition = new Vector3(-1300, 800, 1601);
            }
        }


        private void CreateTiles_tight()
        {
            m_iQuadTotal = m_iWidth * m_iHeight;

            // Generate vertex buffer
            VertexPositionTexture[] oVertices = new VertexPositionTexture[m_iQuadTotal * QuadTotalVertices];
            m_oVertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), oVertices.Length, BufferUsage.WriteOnly);

            int iX = 0;
            int iY = 0;
            Random random = new Random();

            
            for (int iCell = 0; iCell < m_iQuadTotal; iCell++)
            {
                Vector2 randomPos = new Vector2((float)(random.NextDouble() * GraphicsDevice.Viewport.Width/10), (float)(random.NextDouble() * GraphicsDevice.Viewport.Height / 10));

                int iCellIndex = iCell * 6;

                // Vertex Position
                // - Triangle 1
                oVertices[iCellIndex + 0].Position = new Vector3((randomPos.X * 32), (randomPos.Y * 32), 0);
                oVertices[iCellIndex + 1].Position = new Vector3((randomPos.X * 32), 32 + (randomPos.Y * 32), 0);
                oVertices[iCellIndex + 2].Position = new Vector3(32 + (randomPos.X * 32), (randomPos.Y * 32), 0);
                // - Triangle 2
                oVertices[iCellIndex + 3].Position = oVertices[iCellIndex + 1].Position;
                oVertices[iCellIndex + 4].Position = new Vector3(32 + (randomPos.X * 32), 32 + (randomPos.Y * 32), 0);
                oVertices[iCellIndex + 5].Position = oVertices[iCellIndex + 2].Position;

                // Vertex Texture
                // - Triangle 1
                oVertices[iCellIndex + 0].TextureCoordinate = new Vector2(0, 1); // bottom left
                oVertices[iCellIndex + 1].TextureCoordinate = new Vector2(0, 0); // Top Left
                oVertices[iCellIndex + 2].TextureCoordinate = new Vector2(1, 1); // Bottom Right
                // - Triangle 2
                oVertices[iCellIndex + 3].TextureCoordinate = oVertices[iCellIndex + 1].TextureCoordinate; // Top Left again
                oVertices[iCellIndex + 4].TextureCoordinate = new Vector2(1, 0); // Top Right
                oVertices[iCellIndex + 5].TextureCoordinate = oVertices[iCellIndex + 2].TextureCoordinate; // Bottom Right again

                if (iX < (m_iWidth - 1))
                {
                    iX++;
                }
                else
                {
                    iX = 0;
                    iY++;
                }
            }
            m_oVertexBuffer.SetData(oVertices);
        }

        private void CreateTiles_grid()
        {
            m_iQuadTotal = m_iWidth * m_iHeight;

            // Generate vertex buffer
            VertexPositionTexture[] oVertices = new VertexPositionTexture[m_iQuadTotal * QuadTotalVertices];
            m_oVertexBuffer = new VertexBuffer(GraphicsDevice, typeof(VertexPositionTexture), oVertices.Length, BufferUsage.WriteOnly);

            int iX = 0;
            int iY = 0;
            for (int iCell = 0; iCell < m_iQuadTotal; iCell++)
            {
                int iCellIndex = iCell * 6;
                //Color oColor = new Color(Random.Next(0, 255), Random.Next(0, 255), Random.Next(0, 255));

                // Vertex Position
                // - Triangle 1
                oVertices[iCellIndex + 0].Position = new Vector3((iX * 32), (iY * 32), 0);
                oVertices[iCellIndex + 1].Position = new Vector3((iX * 32), 32 + (iY * 32), 0);
                oVertices[iCellIndex + 2].Position = new Vector3(32 + (iX * 32), (iY * 32), 0);
                // - Triangle 2
                oVertices[iCellIndex + 3].Position = oVertices[iCellIndex + 1].Position;
                oVertices[iCellIndex + 4].Position = new Vector3(32 + (iX * 32), 32 + (iY * 32), 0);
                oVertices[iCellIndex + 5].Position = oVertices[iCellIndex + 2].Position;

                // Vertex Texture
                // - Triangle 1
                oVertices[iCellIndex + 0].TextureCoordinate = new Vector2(0, 1); // bottom left
                oVertices[iCellIndex + 1].TextureCoordinate = new Vector2(0, 0); // Top Left
                oVertices[iCellIndex + 2].TextureCoordinate = new Vector2(1, 1); // Bottom Right
                // - Triangle 2
                oVertices[iCellIndex + 3].TextureCoordinate = oVertices[iCellIndex + 1].TextureCoordinate; // Top Left again
                oVertices[iCellIndex + 4].TextureCoordinate = new Vector2(1, 0); // Top Right
                oVertices[iCellIndex + 5].TextureCoordinate = oVertices[iCellIndex + 2].TextureCoordinate; // Bottom Right again

                if (iX < (m_iWidth - 1))
                {
                    iX++;
                }
                else
                {
                    iX = 0;
                    iY++;
                }
            }
            m_oVertexBuffer.SetData(oVertices);
        }
        #endregion
        

        #region CAMERA / FRAME RATE COUNTER
        public class Camera : Microsoft.Xna.Framework.GameComponent
        {
            public Vector3 m_oPosition = new Vector3(0, 0, 1.0f);
            private float m_fZoom = 1.0f;

            // Input
            private KeyboardState m_oPreviousKeyboardState = Keyboard.GetState();
            private KeyboardState m_oCurrentKeyboardState = Keyboard.GetState();
            private MouseState m_oPreviousMouseState = Mouse.GetState();
            private MouseState m_oCurrentMouseState = Mouse.GetState();

            public Camera(Game oGame) : base(oGame)
            {

            }

            #region MonoGame Pipeline
            public override void Initialize()
            {
                base.Initialize();
            }

            public override void Update(GameTime oGameTime)
            {
                m_oCurrentKeyboardState = Keyboard.GetState();
                m_oCurrentMouseState = Mouse.GetState();

                // Horizontal Panning
                if (m_oCurrentKeyboardState.IsKeyDown(Keys.D))
                {
                    m_oPosition.X -= 100;
                }

                if (m_oCurrentKeyboardState.IsKeyDown(Keys.A))
                {
                    m_oPosition.X += 100;
                }

                // Vertical Panning
                if (m_oCurrentKeyboardState.IsKeyDown(Keys.W))
                {
                    m_oPosition.Y += 100;
                }

                if (m_oCurrentKeyboardState.IsKeyDown(Keys.S))
                {
                    m_oPosition.Y -= 100;
                }

                // Zooming
                if (m_oCurrentKeyboardState.IsKeyDown(Keys.Q))
                {
                    m_oPosition.Z += 1000;
                }

                if (m_oCurrentKeyboardState.IsKeyDown(Keys.E))
                {
                    m_oPosition.Z -= 100;
                }

                m_oPreviousKeyboardState = m_oCurrentKeyboardState;
                m_oPreviousMouseState = m_oCurrentMouseState;

                base.Update(oGameTime);
            }
            #endregion

            #region Public Properties
            public Matrix View
            {
                get { return Matrix.CreateLookAt(new Vector3(-m_oPosition.X, m_oPosition.Y, m_oPosition.Z), new Vector3(-m_oPosition.X, m_oPosition.Y, 0), Vector3.Up) * Matrix.CreateScale(m_fZoom); }
            }
            #endregion
        }

        public class FrameRateCounter : DrawableGameComponent
        {
            private int m_iFrameRate = 0;
            private int m_iFrameCounter = 0;
            private string m_sMessage = string.Empty;
            private TimeSpan m_oElapsedTime = TimeSpan.Zero;

            public FrameRateCounter(Game oGame) : base(oGame)
            {

            }

            protected override void LoadContent()
            {

            }

            public override void Update(GameTime oGameTime)
            {
                m_oElapsedTime += oGameTime.ElapsedGameTime;

                if (m_oElapsedTime > TimeSpan.FromSeconds(1))
                {
                    m_oElapsedTime -= TimeSpan.FromSeconds(1);
                    m_iFrameRate = m_iFrameCounter;
                    m_iFrameCounter = 0;
                }

                m_sMessage = $"FPS: {m_iFrameRate}";
            }

            public override void Draw(GameTime oGameTime)
            {
                m_iFrameCounter++;

                Examples.SpriteBatch.Begin();
                Examples.SpriteBatch.DrawString(Examples.SpriteFont, m_sMessage, new Vector2(6, 5), Color.Black);
                Examples.SpriteBatch.DrawString(Examples.SpriteFont, m_sMessage, new Vector2(5, 4), Color.White);
                Examples.SpriteBatch.End();
            }
        }
        #endregion
    }
}

I’ve never drawn tiles by quads, but are you changing the texture sheet for each different layer? If so try draw all of layer 1, then all of layer 2 etc

Edit: unlock your trimester and see how much fps your actually getting with 1 layer

Thanks for taking a look!

There is only 1 texture, 1 layer and a single DrawPrimitives() call that renders all tiles at once. If you press F1, it toggles how all those tiles get rendered, either into a grid where they are all evenly spaced, or alternatively tightly all over each other.

There are the same number of tiles, using the same texture in both cases. And my question is, why does performance take a massive hit when they are rendered on top of each other?

PS: what is a trimester?

Fillrate is killing ye. When you zoom out, you draw much smaller tiles - probably less than one pixel in size, so they are faster to render than bigger tiles.