Huge FPS drop when drawing two textures from the same instance, no drop if two instances make one draw each. (3.7/Desktop/OpenGL)

Hi!

Iā€™m new to MG/XNA and Iā€™m just trying stuff out; this is an inventory slot, itā€™s supposed to be drawing the slot texture and the item icon.

The class has a method like this (pseudocode because itā€™s on another machine, but this is actually the whole code anyway):

void Draw() //Called by another class within the sb.Begin/sb.End pair.
{
spriteBatch.Draw(texture, position, color)

spriteBatch.Draw(texture, position, sourceRectangle)

// Or any other variant of it, it makes no difference.
}

Thereā€™s no difference if I use a single icon for every slot, or different ones, or an atlas.

If the slot draws two textures, the FPS get halved, or worse (no fixed FPS so itā€™s in the thousands, on the machine Iā€™m testing this it drops from 5700 to 2200). However, if I instantiate another set of slots (same position and everything) and make each set draw only one texture, thereā€™s no FPS drop. The code that manages the draws is the same, I just added an if/else to make each set only call one of the draws.

Couldnā€™t find anything about it, any idea what could be causing this?

Thank you!

does this line throw a warning or something during build?

Yes, it gives the warning about it being an obsolete/deprecated method.But nothing changes if that one isnā€™t used, and also thereā€™s no drop if that one is used with the ā€œtwin instancesā€.

Iā€™ve made a project with just that code in a single class to test it on different machines and to post it here. The more powerful the PC is, the bigger the drop. On a very old PC it only goes from ~2200 to ~1800 FPS. Hereā€™s the code, just change the value of ā€˜useTwoInstancesā€™ (the textures Iā€™ve used are random 48x48 px images, tried with transparent background, ā€œtransparentā€ (255, 0, 255), no transparency, everything):

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Game4
{
public class Game1 : Game
{
GraphicsDeviceManager graphics;
public static SpriteBatch spriteBatch;

    public static Texture2D slotTexture;
    public static Texture2D itemIcon;
    int textureSize = 48;

    ItemSlot[] testItemSlots1 = new ItemSlot[10];
    ItemSlot[] testItemSlots2 = new ItemSlot[10];

    public static bool useTwoInstances = false;

    public Game1()
    {
        graphics = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";
    }

    protected override void Initialize()
    {
        base.Initialize();
        graphics.SynchronizeWithVerticalRetrace = false;
        IsFixedTimeStep = false;
        graphics.ApplyChanges();
    }

    protected override void LoadContent()
    {
        spriteBatch = new SpriteBatch(GraphicsDevice);
        slotTexture = Content.Load<Texture2D>(@"slotTexture");
        itemIcon = Content.Load<Texture2D>(@"itemIcon");

        for (int i = 0; i < testItemSlots1.Length; i++)
        {
            testItemSlots1[i] = new ItemSlot
            {
                position = new Vector2(8 + i * (textureSize + 4), 8)
            };

            if (useTwoInstances)
            {
                testItemSlots2[i] = new ItemSlot
                {
                    position = new Vector2(8 + i * (textureSize + 4), 8),
                    isTwo = true
                };
            }
        }
    }

    protected override void UnloadContent()
    {

    }

    protected override void Update(GameTime gameTime)
    {
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.CornflowerBlue);

        spriteBatch.Begin();

        foreach (ItemSlot _ItemSlot in testItemSlots1)
        {
            _ItemSlot.Draw();
        }

        if (useTwoInstances)
        {
            foreach (ItemSlot _ItemSlot in testItemSlots2)
            {
                _ItemSlot.Draw();
            }
        }

        spriteBatch.End();

        base.Draw(gameTime);
    }
}

public class ItemSlot
{
    public Texture2D slotTexture = Game1.slotTexture;
    public Texture2D itemIcon = Game1.itemIcon;
    public Vector2 position;
    public bool isTwo;

    public void Draw()
    {
        if (Game1.useTwoInstances)
        {
            if (isTwo)
            {
                Game1.spriteBatch.Draw(slotTexture, position, Color.White);
            }
            else
            {
                Game1.spriteBatch.Draw(itemIcon, position, Color.White);
            }
        }
        else
        {
            Game1.spriteBatch.Draw(itemIcon, position, Color.White);
            Game1.spriteBatch.Draw(slotTexture, position, Color.White);
        }
    }
}

}

FPS isnā€™t a valid measurement. Use the hi-res timer and measure actual time.


The way youā€™re drawing in the not two-instances case will texture thrash resulting in a lot of draw calls if youā€™re using the default SpriteSortMode.Deferred setting of SpriteBatch.Begin.

// this is not a good idea on default SpriteBatch.Begin
spriteBatch.Draw(emptySlotTexture, ....);
spriteBatch.Draw(myItemTexture, ....);

// ==================
// This is a better option
// ==================
// draw all of the slots first
spriteBatch.Begin(SpriteSortMode.Deferred, ...);
for (int i = 0; i < numSlots; ++i)
{
     // they're all the same texture, SpriteBatch will make 1 draw call for all of them (if they fit)
     spriteBatch.Draw(emptySlotTexture, ... );
}
spriteBatch.End();

// next draw all of the slot content images

// now use texture sort mode to avoid thrashing if multiple of same item in different slots
// texture sort mode is not required, in which case a seperate set of Begin/End
// is not necessary
spriteBatch.Begin(SpriteSortMode.Texture ...);
for (int i = 0; i < numSlots; ++i)
{
    if (slot[i].HasItem)
        spriteBatch.Draw(slot[i].itemTexture, .... );
}
spriteBatch.End();

Iā€™d assume your two-instance case behaves better because youā€™re probably drawing all slot-images first, and then later drawing the item images - instead of alternating between them.


Are you sure you were doing texture atlasing correcting when you say you tried it?

Thank you, that fixed it!

Yes, timers are consistent with the FPS (from Fraps) in this case.

I actually thought SpriteBatch was supposed to take care of thatā€¦ I was so wrong!
Thank you even more because you gave me enough info to make better searches about this topic, now I understand it a bit more.

Is there any documentation that mentions this kind of caveats, or theyā€™re just implied because everybody is supposed to know about them?

Yes, just using two different methods to draw each texture and calling them in the same Begin/End pair fixed it, even without Texture sorting when using the sprite atlas and repeating textures. I had tried with the two instances way because I thought the drop could be because of multiple textures with transparency rendered on top of each other, so I made the second instance to have it draw its texture at a different position, then moved it to the same position to see if that made any difference. Iā€™ve kept it as example because, if anything, it should have been slower than having half of the instances, but all it was doing was accidentally sorting the drawings the right way. :grin:

It probably isnā€™t right, Iā€™m just doing it the way Iā€™ve posted in OP, with ā€˜textureā€™, ā€˜positionā€™, and ā€˜sourceRectangleā€™ being the only arguments passed, I just made it up quickly to test if it made any difference. For the sake of this testing it seems to work fine enough and doesnā€™t cause any performance loss.
I just started using MG after having used a few game engines, so what Iā€™m doing now is just testing out in a very basic way all the stuff that relies on the framework to make sure itā€™s the right tool before porting the actual game.

Thank you both for your help! :slight_smile:

Some rules of thumb.
(SpriteBatch has a Texture sort mode, even with that the below applies. Which is the difference in your bool switch.)
*Switching textures is not free.
*Each at best will be in a batch per texture which equals a blit to the gpu, there is a batch per texture at least.
*The more drawn batches called the more expensive.
*There is no batching in immediate mode and basically that means the above texture thrashing blit t1 t2 t1 t2 and the gpu will also switch textures per each ect.
*Scaling youā€™re textures down to draw them smaller is almost always more costly then scaling them up.
*Scaling them up is still more costly then drawing at a 1 to 1 ratio.
*Switchin states has a cost.
*Creating a new state in the running game loop is not only expensive it creates huge amounts of garbage.
*Create declare and define your states in class scope or define them in load never in update or draw only assign premade ones to the graphics device there.

Apologies for the late reply, thank you for those infos! :slight_smile: