Help with performances

I’m finishing my last game, Mark In DeDark and now I’m worrying about performance.

Premise

  • The game is a 2D top-down puzzle game
  • I’m compiliing everything in x86 with MonoGame.Framework.WindowsDX
  • It is composed with 4 2048x2048 spritesheets
  • The world is totally black and I render all the lights with a normal map shader (every sprite is pixel art)
  • I also use Penumbra, but only for the player light (applying it on every other light was too expensive in terms of performance)
  • I’m testing everything with 1422x800 resolution
  • I didn’t put any sounds yet
  • I set the framerate to 50 FPS: _game.TargetElapsedTime = TimeSpan.FromSeconds(1d / 50d); and _game.IsFixedTimeStep = true;

Why I’m not happy with performances
Because the framerate isn’t constant. Sometimes it drops to 30 FPS, sometimes to 40 FPS, but rarely it is constant to 50 FPS. I already tried to set it to 30 FPS, but it isn’t constant either.

What I did to solve the problem
I already did a lot of profiling, and I already fixed some CPU bound problems. Now I’m asking your help because you have more experience with me with MonoGame and may have some advice I’m missing. (Thanks!)

What I saw
To do my tests, I created a “perfomance test level” in my game to use always the have always the same variables with the same PC (a HP Probook 450 with integrated graphic card Intel HD Graphics 4000, what to me is “the minimum”)

  • CPU is never higher than 6%
  • GPU is always in the range 70%/90%
  • Framerate is in the range 25-50 FPS

Code profiling
I’m using Visual Studio 2019 Community

By profiling with “CPU usage”, i see:

  • In the whole session, the Update method takes 9,9% and the Draw method takes 17%. The other % are external methods (or parent methods that contains these two)

  • Inside the Draw method, Penumbra takes 5%.

  • My biggest spriteBatch.End(); takes 4%, and digging into the method it seems that sprites sorting takes a lot of work (spriteBatch.Begin(transformMatrix: viewMatrix, samplerState: SamplerState.PointClamp, sortMode: SpriteSortMode.FrontToBack);)

  • The other % is drawing my multipass lighting for each light, that is done like this:

      public void DrawGroundWithNormalMap(
      	SpriteBatch spriteBatch,
      	Matrix viewMatrix,
      	Effect normalMapEffect,
      	Effect specularMapEffect,
      	GraphicsDevice graphicsDevice,
      	RenderTarget2D baseRenderTarget,
      	RenderTarget2D normalMapRenderTarget,
      	RenderTarget2D specularMapRenderTarget,
      	RenderTarget2D specularMapLightmaskRenderTarget,
      	RenderTarget2D specularMapWithAppliedLightmaskRenderTarget,
      	RenderTarget2D appliedNormalMapRenderTarget,
      	RenderTarget2D resultRenderTarget,
      	Action<SpriteBatch> drawGlows,
      	List<MarkLight> lights)
      {
      	// Tile set background cells
      	graphicsDevice.SetRenderTarget(baseRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	spriteBatch.Begin(transformMatrix: viewMatrix, samplerState: SamplerState.PointClamp);
      	OnAllVisibleCells(c =>
      	{
      		c.DrawGround(spriteBatch);
      	});
      	spriteBatch.End();
    
      	// Normal map
      	graphicsDevice.SetRenderTarget(normalMapRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	spriteBatch.Begin(transformMatrix: viewMatrix, samplerState: SamplerState.PointClamp);
      	OnAllVisibleCells(c =>
      	{
      		c.DrawGroundNormalMap(spriteBatch);
      	});
      	spriteBatch.End();
    
      	// Specular map
      	graphicsDevice.SetRenderTarget(specularMapRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	spriteBatch.Begin(transformMatrix: viewMatrix, samplerState: SamplerState.PointClamp);
      	OnAllVisibleCells(c =>
      	{
      		c.DrawGroundSpecularMap(spriteBatch);
      	});
      	spriteBatch.End();
    
      	// Specular map lightmask
      	graphicsDevice.SetRenderTarget(specularMapLightmaskRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	spriteBatch.Begin(transformMatrix: viewMatrix, samplerState: SamplerState.PointClamp, blendState: BlendState.NonPremultiplied);
      	drawGlows(spriteBatch);
      	spriteBatch.End();
    
      	// Apply normal map
      	graphicsDevice.SetRenderTarget(appliedNormalMapRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	normalMapEffect.Parameters["NormalTexture"].SetValue(normalMapRenderTarget);
      	spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive, SamplerState.PointClamp, null, null, normalMapEffect);
      	foreach (var light in lights)
      	{
      		// Because the light is off-screen
      		if (!light.ShouldDraw)
      			continue;
      			
      		normalMapEffect.Parameters["LightPosition"].SetValue(light.PositionForGround);
      		normalMapEffect.Parameters["LightColor"].SetValue(light.ColorNormal);
      		normalMapEffect.Parameters["LightDistanceSquared"].SetValue(light.DistanceSquared);
      		normalMapEffect.Parameters["LightOpacity"].SetValue(light.Opacity);
      		spriteBatch.Draw(baseRenderTarget, Vector2.Zero, Color.White);
      	}
      	spriteBatch.End();
    
      	// Lightmask for the specular map
      	graphicsDevice.SetRenderTarget(specularMapWithAppliedLightmaskRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	specularMapEffect.Parameters["SpecularTexture"].SetValue(specularMapLightmaskRenderTarget);
      	specularMapEffect.CurrentTechnique.Passes[0].Apply();
      	spriteBatch.Begin(effect: specularMapEffect);
      	spriteBatch.Draw(specularMapRenderTarget, Vector2.Zero, Color.White);
      	spriteBatch.End();
    
      	// Final composition
      	graphicsDevice.SetRenderTarget(resultRenderTarget);
      	graphicsDevice.Clear(Color.Black);
      	spriteBatch.Begin(blendState: BlendState.Additive);
      	spriteBatch.Draw(appliedNormalMapRenderTarget, Vector2.Zero, Color.White);
      	spriteBatch.Draw(specularMapWithAppliedLightmaskRenderTarget, Vector2.Zero, Color.White);
      	spriteBatch.End();
      }
    

This is the point in which I think I have to work more. Because it is a little ugly.

  • I draw the ground that you see by the camera
  • I draw its normal map
  • I draw a sort of specular texture for each game ligth, and I apply a lightmask to it (to show only certain parts of it)
  • I apply the normal map - foreach game light - with a shader
  • I apply the specular lightmasked

Garbage Collector
Each red point is an automatic garbage collection

My questions

  1. In your opinion, should I try to have smaller spritesheets? Is it very important? I could reduce all my textures by 1/4, by then I would have to apply a scaling in each Draw call.
  2. In your opinion, can I do something better in my big Draw call? Did I do something very horrible with the shaders calls?
  3. Do you think the Garbage Collector comes too frequently?
  4. Do have any other advices?

Thanks a lot!

1 Like

Okay this sounds to me like two garbage collection is killing you.

From your own figures.

GPU 70-90%

Well that is less than 100%, so you should be running at the clock rate of your machine , probably 60Hz

CPU 6%

So not your code hogging the CPU

Garbage collection is evil. I have seen it lock all 7 cores of my machine for 100ms

I would do an audit and make sure you are not doing anything silly like creating samplerstates on the fly, or RasteriserStates , they can kill you.

1 Like

Thanks a lot, I’ll focus more on GC!

I doubt GC is the issue. CPU usage is 6%.

I would say to only draw what you need. If you don’t need all the textures loaded, don’t.

How often do you call Begin and End on the sprite batch? Minimize that as much as you can. In my game, I basically have a single call to Begin and End, however, I’m not using shaders right now.

1 Like

How many render target changes are you doing for lighting etc. I’ve noticed rendertarget switching really slows down fps on integrated graphics. As a test take the render target changes out and see what the difference is.

I would reckon if you ran in on a pc with a graphics card you would get tthousnds of fos.

1 Like

Thanks! If I comment all render target changes, it is smoother, but the GPU is always at 30%. Everyhing now is on the biggest spriteBatch.End method, the one that has to do the biggest sorting part!!

For better performance rather then use spritebatch to sort your sprites you can use spritesort mode deffered. This means you need to order your draw calls yourself.

As a test change the biggest spritebatch to sort mode deferred and see what increase there is.

But the biggest performance killer on integrated graphics cards is changing the render targets. From memory when i last did some tests i had the following fps on the same tests.

Integrated gpu no rendertarget switching 180fps
Integrated gpu 3 rendertarget changes 45fps
1070 no render target switching 4000fps
1070 3 render target changes 3700fps

1 Like

Thanks a lot, this is a great news. After some optimizations I could also raise up a little the minimum game requirements!

As for the SortMode: I’m simulating a 3D environment with 2D sprites, and it’s all based on layer depths, I’ll have to refactor 50% of the code to make it working with deferred mode.

Thanks a lot!

Don’t be too concerned about texture size - as long as they fit in the GFX memory, it’s doesn’t really matter for rendering. It’s more about loading time and memory usage.

I would recommend putting a “StopWatch” in your Draw call, measuring the actual time for a frame - just to see if that is constant or takes too much time.

As you use the fixed framerate (targetelapsedtime) what happens is, that monogame tries to maintain a fixed timestep to your update method. This involves waiting or calling Update more than once between the frames - this can lead to framedrop etc. So by measuring the Draw Call itself, you can get a clearer view about how long drawing actually takes and if that fits into your desired framerate.

If that is alright, you should inspect your Update Method as well, to see if that fits in that time window as well … the time window is ~16 ms for 60fps, as soon as you reach it, you will experience jitter or random frame drops.

1 Like

Since you use FixedTimeStep and you see FrameDrops then the CPU/GPU average you measure is not the full picture. MG is droping frames to keep the Update/Draw cycle within a 100% CPU threshold.
You need to turn IsFixedTimeStep off to get usable stats.

What I see is you trigger a GC every 2 seconds. That’s a lot and probably the spikes from GC is the cause of the drop frames. You need to run the memory profiler to see what types are allocated on each frame.

1 Like

Thanks a lot, I’m going to it with stop watch and see what I get.

I’m going to analyze the memory allocations too, and redo the analysys with FixedTimeStamp to false. Thanks!

TimeSpan does rounding internally, so this won’t be exactly 50 FPS. Ticks will get you a more accurate framerate:

_game.TargetElapsedTime = TimeSpan.FromTicks((long)(TimeSpan.TicksPerSecond * (1d / 50d)));

3 Likes

I didn’t know! Thanks! :slight_smile: