Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Starting a new thread from Update for the first time makes SDL.GL.SwapWindow take a long time to complete in the following frame #8090

Open
2 tasks done
martinmaxk opened this issue Nov 6, 2023 · 4 comments

Comments

@martinmaxk
Copy link

martinmaxk commented Nov 6, 2023

Prerequisites

  • I have verified this issue is not present in the develop branch
  • I have searched open and closed issues to ensure it has not already been reported.

MonoGame Version

MonoGame 3.8.1.303

Which MonoGame platform are you using?

MonoGame Cross-Platform Desktop Application (mgdesktopgl)

Operating System

Windows

Description

Even in a blank MonoGame project, if you start a new thread from Update then it will skip a few frames (lag) due to SDL.GL.SwapWindow taking much longer to complete than usual (~70-100 milliseconds on my machine). If you just create a new thread and do not start it, then there is no lag. Starting more threads in the same Update does not increase the lag compared to just one. It also seems like after the first thread started from Update, starting threads in Update later does not trigger the lag. If you start a new thread in e.g. Initialize and then later in Update it will still trigger the lag.

I ran develop and the problem is present in the latest develop branch as well. The problem is only present when compiling the source in Release mode, it will not lag when compiling in Debug mode. Compiling the test project in Debug/Release mode does not matter.

I verified that it was indeed SDL.GL.SwapWindow that was slow by benchmarking it and if you try to override Game.EndDraw() and leave it empty, there is no lag because then it will not call SDL.GL.SwapWindow (neither is anything drawn to the screen).

Vsync does not matter, IsFixedTimeStep does not matter (if set to false, then Update has the large ElapsedGameTime).

Steps to Reproduce

  1. Create an empty MonoGame Cross-Platform Desktop Application using MonoGame 3.8.1.303
  2. Replace the code in Game1.cs with the following:
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;


namespace TestThreadFrameDrop
{
    public class Game1 : Game
    {
        private GraphicsDeviceManager _graphics;

        public Game1()
        {
            _graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            IsMouseVisible = true;

            TargetElapsedTime = TimeSpan.FromSeconds(1f / 144f);
            IsFixedTimeStep = true;

            _graphics.SynchronizeWithVerticalRetrace = false;
        }

        private bool hasStarted;
        protected override void Update(GameTime gameTime)
        {
            Debug.WriteLine("Update - " + gameTime.ElapsedGameTime.TotalMilliseconds);
            if (gameTime.TotalGameTime.TotalMilliseconds > 100 && !hasStarted)
            {
                var thread = new Thread(() => { });
                thread.Start();
                hasStarted = true;
            }
            base.Update(gameTime);
        }


        protected override void Draw(GameTime gameTime)
        {
            Debug.WriteLine("Draw - " + gameTime.ElapsedGameTime.TotalMilliseconds);
            if (hasStarted && gameTime.ElapsedGameTime.TotalMilliseconds > 20)
                throw new Exception(gameTime.ElapsedGameTime.TotalMilliseconds.ToString());

            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }

        protected override void EndDraw()
        {
            base.EndDraw(); // Comment out this line to remove the lag
        }
    }
}
  1. Run the game (either from VS or outside of VS, either debug or release, doesn't matter)
  2. The game throws an exception in the following frame because the delay between Draw calls is too long.

Minimal Example Repo

No response

Expected Behavior

SDL.GL.SwapWindow should not take significantly longer than usual and there should not be this much lag (Exception should not be triggered).

Resulting Behavior

When you start a new thread for the first time in Update there is significant lag due to SDL.GL.SwapWindow taking much longer than usual (Exception is triggered in the code).

Files

No response

@martinmaxk martinmaxk changed the title Starting a new thread makes SDL.GL.SwapWindow take a long time to complete in the following frame Starting a new thread from Update for the first time makes SDL.GL.SwapWindow take a long time to complete in the following frame Nov 6, 2023
@stromkos
Copy link
Contributor

The creation of a thread can take some time(2-100ms depending on the computer) and should not be done in Update(), since it blocks the program from continuing until completed.

Either use thread-pools and/or start a control thread once in Initialize() or the constructor to create and control the other threads.


I verified that it was indeed SDL.GL.SwapWindow that was slow by benchmarking it and if you try to override Game.EndDraw() and leave it empty, there is no lag because then it will not call SDL.GL.SwapWindow (neither is anything drawn to the screen).

By removing the EndDraw() call you sped up the elapsed time by batching, but not performing any of the the draw functions to the graphics device.

To verify this statement, take a blank project and set:

 IsFixedTimeStep = false;
 _graphics.SynchronizeWithVerticalRetrace = false;

And compare the elapsed times to an overridden EndDraw() . Timer resolution should be an issue.

@martinmaxk
Copy link
Author

martinmaxk commented Nov 14, 2023

I can run the program at 144 FPS no problem (it is an empty project) without creating threads so obviously EndDraw usually takes less than 7 ms and therefore cannot yield a 70-100ms speedup by removing it. thread.Start time is definitely less than 1 ms. Okay so maybe creation of the thread blocks the main thread with some delay which coincides with EndDraw and it is only slow the first time due to JIT. But then how come creating threads from Initialize does not prevent the first thread creation in Update from being very slow? And why does the slowdown disappear when removing EndDraw? Using thread pools with Task.Run from Update can give the same behaviour and I suspect the slowdown happens when the thread pool decides to start a new thread.

But I will try to create a control thread in Initialize.

Edit: Creating threads from a separate control thread produces the same issue.
If I create a busy loop that runs for 5 milliseconds in the update loop then that piece of code will be slow (70-100ms) instead, so it seems it is just a coincidence after all. Not sure why removing EndDraw fixed the slowdown before though. I also do not understand why starting threads from Initialize does not prevent the first thread creation in Update from being very slow.

@mrhelmut
Copy link
Contributor

Not calling EndDraw basically skips a frame. Even though vsync is disabled by code, it might still be forced by your graphics card driver. My guess is that there's a combination of factors with the fixed timer, and possibly vsync, which makes creating a thread introducing timer jitter and EndDraw blocks until it catches up.

If you need to create threads at runtime, one of the best practice is to pool them (e.g. creating them ahead of time, and grabbing whichever one is available when we need to have some work done). System.Threading.Task does that under the hood. I would assume that starting a task instead of a thread would not block the rendering.

@martinmaxk
Copy link
Author

martinmaxk commented Jan 19, 2024

I initially encountered this issue when creating tasks with Task.Run which made the game lag sometimes. Replacing new Thread(() => { }); with Task.Run(() => { }); gives similar issues. Probably related to when Task.Run is starting new threads under the hood, but not sure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants