Game Window- allow configuring separate update thread
authorTzach Shabtay <tzachshabtay@gmail.com>
Sat, 23 Dec 2017 06:58:07 +0000 (01:58 -0500)
committerTzach Shabtay <tzachshabtay@gmail.com>
Sat, 23 Dec 2017 06:58:07 +0000 (01:58 -0500)
src/OpenTK/GameWindow.cs

index 4a0a581..c49fc82 100644 (file)
@@ -25,6 +25,7 @@
 
 using System;
 using System.Diagnostics;
+using System.Threading;
 using OpenTK.Graphics;
 using OpenTK.Platform;
 
@@ -66,7 +67,11 @@ namespace OpenTK
     {
         private const double MaxFrequency = 500.0; // Frequency cap for Update/RenderFrame events
 
-        private readonly Stopwatch watch = new Stopwatch();
+        private readonly Stopwatch watchRender = new Stopwatch();
+        private readonly Stopwatch watchUpdate = new Stopwatch();
+        private Thread updateThread;
+
+        private readonly bool isSingleThreaded;
 
         private IGraphicsContext glContext;
 
@@ -161,12 +166,30 @@ namespace OpenTK
         /// <param name="sharedContext">An IGraphicsContext to share resources with.</param>
         public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device,
                           int major, int minor, GraphicsContextFlags flags, IGraphicsContext sharedContext)
+            : this(width, height, mode, title, options, device, major, minor, flags, sharedContext, true)
+        {}
+
+        /// <summary>Constructs a new GameWindow with the specified attributes.</summary>
+        /// <param name="width">The width of the GameWindow in pixels.</param>
+        /// <param name="height">The height of the GameWindow in pixels.</param>
+        /// <param name="mode">The OpenTK.Graphics.GraphicsMode of the GameWindow.</param>
+        /// <param name="title">The title of the GameWindow.</param>
+        /// <param name="options">GameWindow options regarding window appearance and behavior.</param>
+        /// <param name="device">The OpenTK.Graphics.DisplayDevice to construct the GameWindow in.</param>
+        /// <param name="major">The major version for the OpenGL GraphicsContext.</param>
+        /// <param name="minor">The minor version for the OpenGL GraphicsContext.</param>
+        /// <param name="flags">The GraphicsContextFlags version for the OpenGL GraphicsContext.</param>
+        /// <param name="sharedContext">An IGraphicsContext to share resources with.</param>
+        /// <param name="isSingleThreaded">Should the update and render frames be fired on the same thread? If false, render and update events will be fired from separate threads.</param>
+        public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device,
+                          int major, int minor, GraphicsContextFlags flags, IGraphicsContext sharedContext, bool isSingleThreaded)
             : base(width, height, title, options,
                    mode == null ? GraphicsMode.Default : mode,
                    device == null ? DisplayDevice.Default : device)
         {
             try
             {
+                this.isSingleThreaded = isSingleThreaded;
                 glContext = new GraphicsContext(mode == null ? GraphicsMode.Default : mode, WindowInfo, major, minor, flags);
                 glContext.MakeCurrent(WindowInfo);
                 (glContext as IGraphicsContextInternal).LoadAll();
@@ -334,13 +357,22 @@ namespace OpenTK
                 //Resize += DispatchUpdateAndRenderFrame;
 
                 Debug.Print("Entering main loop.");
-                watch.Start();
+                if (!isSingleThreaded)
+                {
+                    updateThread = new Thread(UpdateThread);
+                    updateThread.Start();
+                }
+                watchRender.Start();
                 while (true)
                 {
                     ProcessEvents();
                     if (Exists && !IsExiting)
                     {
-                        DispatchUpdateAndRenderFrame(this, EventArgs.Empty);
+                        if (isSingleThreaded)
+                        {
+                            DispatchUpdateFrame(watchRender);
+                        }
+                        DispatchRenderFrame();
                     }
                     else
                     {
@@ -350,9 +382,6 @@ namespace OpenTK
             }
             finally
             {
-                Move -= DispatchUpdateAndRenderFrame;
-                Resize -= DispatchUpdateAndRenderFrame;
-
                 if (Exists)
                 {
                     // TODO: Should similar behaviour be retained, possibly on native window level?
@@ -362,21 +391,30 @@ namespace OpenTK
             }
         }
 
+        private void UpdateThread()
+        {
+            OnUpdateThreadStarted(this, new EventArgs());
+            watchUpdate.Start();
+            while (Exists && !IsExiting)
+            {
+                DispatchUpdateFrame(watchUpdate);
+            }
+        }
+
         private double ClampElapsed(double elapsed)
         {
             return MathHelper.Clamp(elapsed, 0.0, 1.0);
         }
 
-        private void DispatchUpdateAndRenderFrame(object sender, EventArgs e)
+        private void DispatchUpdateFrame(Stopwatch watch)
         {
             int is_running_slowly_retries = 4;
             double timestamp = watch.Elapsed.TotalSeconds;
-            double elapsed = 0;
+            double elapsed = ClampElapsed(timestamp - update_timestamp);
 
-            elapsed = ClampElapsed(timestamp - update_timestamp);
             while (elapsed > 0 && elapsed + update_epsilon >= TargetUpdatePeriod)
             {
-                RaiseUpdateFrame(elapsed, ref timestamp);
+                RaiseUpdateFrame(watch, elapsed, ref timestamp);
 
                 // Calculate difference (positive or negative) between
                 // actual elapsed time and target elapsed time. We must
@@ -403,15 +441,19 @@ namespace OpenTK
                     break;
                 }
             }
+        }
 
-            elapsed = ClampElapsed(timestamp - render_timestamp);
+        private void DispatchRenderFrame()
+        {
+            double timestamp = watchRender.Elapsed.TotalSeconds;
+            double elapsed = ClampElapsed(timestamp - render_timestamp);
             if (elapsed > 0 && elapsed >= TargetRenderPeriod)
             {
                 RaiseRenderFrame(elapsed, ref timestamp);
             }
         }
 
-        private void RaiseUpdateFrame(double elapsed, ref double timestamp)
+        private void RaiseUpdateFrame(Stopwatch watch, double elapsed, ref double timestamp)
         {
             // Raise UpdateFrame event
             update_args.Time = elapsed;
@@ -438,7 +480,7 @@ namespace OpenTK
 
             // Update RenderTime property
             render_timestamp = timestamp;
-            timestamp = watch.Elapsed.TotalSeconds;
+            timestamp = watchRender.Elapsed.TotalSeconds;
             render_time = timestamp - render_timestamp;
         }
 
@@ -791,6 +833,12 @@ namespace OpenTK
         public event EventHandler<FrameEventArgs> UpdateFrame = delegate { };
 
         /// <summary>
+        /// If game window is configured to run with a dedicated update thread (by passing isSingleThreaded = false in the constructor),
+        /// occurs when the update thread has started. This would be a good place to initialize thread specific stuff (like setting a synchronization context).
+        /// </summary>
+        public event EventHandler OnUpdateThreadStarted = delegate { };
+
+        /// <summary>
         /// Override to add custom cleanup logic.
         /// </summary>
         /// <param name="manual">True, if this method was called by the application; false if this was called by the finalizer thread.</param>