#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2009 the Open Toolkit library. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // #endregion using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.Threading; using OpenTK.Graphics; using OpenTK.Input; using OpenTK.Platform; namespace OpenTK { /// /// The GameWindow class contains cross-platform methods to create and render on an OpenGL /// window, handle input and load resources. /// /// /// GameWindow contains several events you can hook or override to add your custom logic: /// /// /// OnLoad: Occurs after creating the OpenGL context, but before entering the main loop. /// Override to load resources. /// /// /// OnUnload: Occurs after exiting the main loop, but before deleting the OpenGL context. /// Override to unload resources. /// /// /// OnResize: Occurs whenever GameWindow is resized. You should update the OpenGL Viewport /// and Projection Matrix here. /// /// /// OnUpdateFrame: Occurs at the specified logic update rate. Override to add your game /// logic. /// /// /// OnRenderFrame: Occurs at the specified frame render rate. Override to add your /// rendering code. /// /// /// Call the Run() method to start the application's main loop. Run(double, double) takes two /// parameters that /// specify the logic update rate, and the render update rate. /// public class GameWindow : NativeWindow, IGameWindow, IDisposable { #region --- Fields --- object exit_lock = new object(); IGraphicsContext glContext; bool isExiting = false; double update_period, render_period; double target_update_period, target_render_period; // TODO: Implement these: double update_time, render_time; VSyncMode vsync; Stopwatch update_watch = new Stopwatch(), render_watch = new Stopwatch(); double next_render = 0.0, next_update = 0.0; FrameEventArgs update_args = new FrameEventArgs(); FrameEventArgs render_args = new FrameEventArgs(); #endregion #region --- Contructors --- #region public GameWindow() /// Constructs a new GameWindow with sensible default attributes. public GameWindow() : this(640, 480, GraphicsMode.Default, "OpenTK Game Window", 0, DisplayDevice.Default) { } #endregion #region public GameWindow(int width, int height) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. public GameWindow(int width, int height) : this(width, height, GraphicsMode.Default, "OpenTK Game Window", 0, DisplayDevice.Default) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. public GameWindow(int width, int height, GraphicsMode mode) : this(width, height, mode, "OpenTK Game Window", 0, DisplayDevice.Default) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode, string title) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. /// The title of the GameWindow. public GameWindow(int width, int height, GraphicsMode mode, string title) : this(width, height, mode, title, 0, DisplayDevice.Default) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. /// The title of the GameWindow. /// GameWindow options regarding window appearance and behavior. public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options) : this(width, height, mode, title, options, DisplayDevice.Default) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. /// The title of the GameWindow. /// GameWindow options regarding window appearance and behavior. /// The OpenTK.Graphics.DisplayDevice to construct the GameWindow in. public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device) : this(width, height, mode, title, options, device, 1, 0, GraphicsContextFlags.Default) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device, int major, int minor, GraphicsContextFlags flags) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. /// The title of the GameWindow. /// GameWindow options regarding window appearance and behavior. /// The OpenTK.Graphics.DisplayDevice to construct the GameWindow in. /// The major version for the OpenGL GraphicsContext. /// The minor version for the OpenGL GraphicsContext. /// The GraphicsContextFlags version for the OpenGL GraphicsContext. public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device, int major, int minor, GraphicsContextFlags flags) : this(width, height, mode, title, options, device, major, minor, flags, null) { } #endregion #region public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device, int major, int minor, GraphicsContextFlags flags, IGraphicsContext sharedContext) /// Constructs a new GameWindow with the specified attributes. /// The width of the GameWindow in pixels. /// The height of the GameWindow in pixels. /// The OpenTK.Graphics.GraphicsMode of the GameWindow. /// The title of the GameWindow. /// GameWindow options regarding window appearance and behavior. /// The OpenTK.Graphics.DisplayDevice to construct the GameWindow in. /// The major version for the OpenGL GraphicsContext. /// The minor version for the OpenGL GraphicsContext. /// The GraphicsContextFlags version for the OpenGL GraphicsContext. /// An IGraphicsContext to share resources with. public GameWindow(int width, int height, GraphicsMode mode, string title, GameWindowFlags options, DisplayDevice device, int major, int minor, GraphicsContextFlags flags, IGraphicsContext sharedContext) : base(width, height, title, options, mode == null ? GraphicsMode.Default : mode, device == null ? DisplayDevice.Default : device) { try { glContext = new GraphicsContext(mode == null ? GraphicsMode.Default : mode, WindowInfo, major, minor, flags); glContext.MakeCurrent(WindowInfo); (glContext as IGraphicsContextInternal).LoadAll(); VSync = VSyncMode.On; //glWindow.WindowInfoChanged += delegate(object sender, EventArgs e) { OnWindowInfoChangedInternal(e); }; } catch (Exception e) { Debug.Print(e.ToString()); base.Dispose(); throw; } } #endregion #endregion #region --- Public Members --- #region Methods #region Dispose /// /// Disposes of the GameWindow, releasing all resources consumed by it. /// public override void Dispose() { try { Dispose(true); } finally { try { if (glContext != null) { glContext.Dispose(); glContext = null; } } finally { base.Dispose(); } } GC.SuppressFinalize(this); } #endregion #region Exit /// /// Closes the GameWindow. Equivalent to method. /// /// /// Override if you are not using . /// If you override this method, place a call to base.Exit(), to ensure proper OpenTK shutdown. /// public virtual void Exit() { Close(); } #endregion #region MakeCurrent /// /// Makes the GraphicsContext current on the calling thread. /// public void MakeCurrent() { EnsureUndisposed(); Context.MakeCurrent(WindowInfo); } #endregion #region OnClose /// /// Called when the NativeWindow is about to close. /// /// /// The for this event. /// Set e.Cancel to true in order to stop the GameWindow from closing. protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { base.OnClosing(e); if (!e.Cancel) { isExiting = true; OnUnloadInternal(EventArgs.Empty); } } #endregion #region OnLoad /// /// Called after an OpenGL context has been established, but before entering the main loop. /// /// Not used. protected virtual void OnLoad(EventArgs e) { Load(this, e); } #endregion #region OnUnload /// /// Called after GameWindow.Exit was called, but before destroying the OpenGL context. /// /// Not used. protected virtual void OnUnload(EventArgs e) { Unload(this, e); } #endregion #region public void Run() /// /// Enters the game loop of the GameWindow using the maximum update rate. /// /// public void Run() { Run(0.0, 0.0); } #endregion #region public void Run(double updateFrequency) /// /// Enters the game loop of the GameWindow using the specified update rate. /// maximum possible render frequency. /// public void Run(double updateRate) { Run(updateRate, 0.0); } #endregion #region public void Run(double updates_per_second, double frames_per_second) /// /// Enters the game loop of the GameWindow updating and rendering at the specified frequency. /// /// /// When overriding the default game loop you should call ProcessEvents() /// to ensure that your GameWindow responds to operating system events. /// /// Once ProcessEvents() returns, it is time to call update and render the next frame. /// /// /// The frequency of UpdateFrame events. /// The frequency of RenderFrame events. public void Run(double updates_per_second, double frames_per_second) { EnsureUndisposed(); try { if (updates_per_second < 0.0 || updates_per_second > 200.0) throw new ArgumentOutOfRangeException("updates_per_second", updates_per_second, "Parameter should be inside the range [0.0, 200.0]"); if (frames_per_second < 0.0 || frames_per_second > 200.0) throw new ArgumentOutOfRangeException("frames_per_second", frames_per_second, "Parameter should be inside the range [0.0, 200.0]"); TargetUpdateFrequency = updates_per_second; TargetRenderFrequency = frames_per_second; Visible = true; // Make sure the GameWindow is visible. OnLoadInternal(EventArgs.Empty); OnResize(EventArgs.Empty); // On some platforms, ProcessEvents() does not return while the user is resizing or moving // the window. We can avoid this issue by raising UpdateFrame and RenderFrame events // whenever we encounter a size or move event. // Note: hack disabled. Threaded rendering isprovides a better solution to this issue. //Move += DispatchUpdateAndRenderFrame; //Resize += DispatchUpdateAndRenderFrame; Debug.Print("Entering main loop."); update_watch.Start(); render_watch.Start(); while (true) { ProcessEvents(); if (Exists && !IsExiting) DispatchUpdateAndRenderFrame(this, EventArgs.Empty); else return; } } finally { Move -= DispatchUpdateAndRenderFrame; Resize -= DispatchUpdateAndRenderFrame; if (Exists) { // TODO: Should similar behaviour be retained, possibly on native window level? //while (this.Exists) // ProcessEvents(false); } } } void DispatchUpdateAndRenderFrame(object sender, EventArgs e) { RaiseUpdateFrame(update_watch, ref next_update, update_args); RaiseRenderFrame(render_watch, ref next_render, render_args); } void RaiseUpdateFrame(Stopwatch update_watch, ref double next_update, FrameEventArgs update_args) { int num_updates = 0; double total_update_time = 0; // Cap the maximum time drift to 1 second (e.g. when the process is suspended). double time = update_watch.Elapsed.TotalSeconds; if (time <= 0) { // Protect against negative Stopwatch.Elapsed values. // See http://connect.microsoft.com/VisualStudio/feedback/details/94083/stopwatch-returns-negative-elapsed-time update_watch.Reset(); update_watch.Start(); return; } if (time > 1.0) time = 1.0; // Raise UpdateFrame events until we catch up with our target update rate. while (next_update - time <= 0 && time > 0) { next_update -= time; update_args.Time = time; OnUpdateFrameInternal(update_args); time = update_time = Math.Max(update_watch.Elapsed.TotalSeconds, 0) - time; // Stopwatches are not accurate over long time periods. // We accumulate the total elapsed time into the time variable // while reseting the Stopwatch frequently. update_watch.Reset(); update_watch.Start(); // Don't schedule a new update more than 1 second in the future. // Sometimes the hardware cannot keep up with updates // (e.g. when the update rate is too high, or the UpdateFrame processing // is too costly). This cap ensures we can catch up in a reasonable time // once the load becomes lighter. next_update += TargetUpdatePeriod; next_update = Math.Max(next_update, -1.0); total_update_time += update_time; // Allow up to 10 consecutive UpdateFrame events to prevent the // application from "hanging" when the hardware cannot keep up // with the requested update rate. if (++num_updates >= 10 || TargetUpdateFrequency == 0.0) break; } // Calculate statistics if (num_updates > 0) { update_period = total_update_time / (double)num_updates; } } void RaiseRenderFrame(Stopwatch render_watch, ref double next_render, FrameEventArgs render_args) { // Cap the maximum time drift to 1 second (e.g. when the process is suspended). double time = render_watch.Elapsed.TotalSeconds; if (time <= 0) { // Protect against negative Stopwatch.Elapsed values. // See http://connect.microsoft.com/VisualStudio/feedback/details/94083/stopwatch-returns-negative-elapsed-time render_watch.Reset(); render_watch.Start(); return; } if (time > 1.0) time = 1.0; double time_left = next_render - time; if (time_left <= 0.0 && time > 0) { // Schedule next render event. The 1 second cap ensures // the process does not appear to hang. next_render = time_left + TargetRenderPeriod; if (next_render < -1.0) next_render = -1.0; render_watch.Reset(); render_watch.Start(); if (time > 0) { // Todo: revisit this code. Maybe check average framerate instead? // Note: VSyncMode.Adaptive enables vsync by default. The code below // is supposed to disable vsync if framerate becomes too low (half of target // framerate in the current approach) and reenable once the framerate // rises again. // Note 2: calling Context.VSync = true repeatedly seems to cause jitter on // some configurations. If possible, we should avoid repeated calls. // Note 3: we may not read/write the VSync property without a current context. // This may come to pass if the user has moved rendering to his own thread. if (Context.IsCurrent && VSync == VSyncMode.Adaptive && TargetRenderPeriod != 0) { // Check if we have enough time for a vsync if (RenderTime > 2.0 * TargetRenderPeriod) Context.SwapInterval = 0; else Context.SwapInterval = 1; } render_period = render_args.Time = time; OnRenderFrameInternal(render_args); render_time = render_watch.Elapsed.TotalSeconds; } } } #endregion #region SwapBuffers /// /// Swaps the front and back buffer, presenting the rendered scene to the user. /// public void SwapBuffers() { EnsureUndisposed(); this.Context.SwapBuffers(); } #endregion #endregion #region Properties #region Context /// /// Returns the opengl IGraphicsContext associated with the current GameWindow. /// public IGraphicsContext Context { get { EnsureUndisposed(); return glContext; } } #endregion #region IsExiting /// /// Gets a value indicating whether the shutdown sequence has been initiated /// for this window, by calling GameWindow.Exit() or hitting the 'close' button. /// If this property is true, it is no longer safe to use any OpenTK.Input or /// OpenTK.Graphics.OpenGL functions or properties. /// public bool IsExiting { get { EnsureUndisposed(); return isExiting; } } #endregion #region Joysticks /// /// Gets a readonly IList containing all available OpenTK.Input.JoystickDevices. /// public IList Joysticks { get { return InputDriver.Joysticks; } } #endregion #region Keyboard /// /// Gets the primary Keyboard device, or null if no Keyboard exists. /// public KeyboardDevice Keyboard { get { return InputDriver.Keyboard.Count > 0 ? InputDriver.Keyboard[0] : null; } } #endregion #region Mouse /// /// Gets the primary Mouse device, or null if no Mouse exists. /// public MouseDevice Mouse { get { return InputDriver.Mouse.Count > 0 ? InputDriver.Mouse[0] : null; } } #endregion #region --- GameWindow Timing --- // TODO: Disabled because it is not reliable enough. Use vsync as a workaround. //#region public bool AllowSleep //public bool AllowSleep //{ // get { return allow_sleep; } // set { allow_sleep = value; } //} //#endregion #region RenderFrequency /// /// Gets a double representing the actual frequency of RenderFrame events, in hertz (i.e. fps or frames per second). /// public double RenderFrequency { get { EnsureUndisposed(); if (render_period == 0.0) return 1.0; return 1.0 / render_period; } } #endregion #region RenderPeriod /// /// Gets a double representing the period of RenderFrame events, in seconds. /// public double RenderPeriod { get { EnsureUndisposed(); return render_period; } } #endregion #region RenderTime /// /// Gets a double representing the time spent in the RenderFrame function, in seconds. /// public double RenderTime { get { EnsureUndisposed(); return render_time; } protected set { EnsureUndisposed(); render_time = value; } } #endregion #region TargetRenderFrequency /// /// Gets or sets a double representing the target render frequency, in hertz. /// /// /// A value of 0.0 indicates that RenderFrame events are generated at the maximum possible frequency (i.e. only limited by the hardware's capabilities). /// Values lower than 1.0Hz are clamped to 1.0Hz. Values higher than 200.0Hz are clamped to 200.0Hz. /// public double TargetRenderFrequency { get { EnsureUndisposed(); if (TargetRenderPeriod == 0.0) return 0.0; return 1.0 / TargetRenderPeriod; } set { EnsureUndisposed(); if (value < 1.0) { TargetRenderPeriod = 0.0; } else if (value <= 200.0) { TargetRenderPeriod = 1.0 / value; } else Debug.Print("Target render frequency clamped to 200.0Hz."); // TODO: Where is it actually performed? } } #endregion #region TargetRenderPeriod /// /// Gets or sets a double representing the target render period, in seconds. /// /// /// A value of 0.0 indicates that RenderFrame events are generated at the maximum possible frequency (i.e. only limited by the hardware's capabilities). /// Values lower than 0.005 seconds (200Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0. /// public double TargetRenderPeriod { get { EnsureUndisposed(); return target_render_period; } set { EnsureUndisposed(); if (value <= 0.005) { target_render_period = 0.0; } else if (value <= 1.0) { target_render_period = value; } else Debug.Print("Target render period clamped to 1.0 seconds."); } } #endregion #region TargetUpdateFrequency /// /// Gets or sets a double representing the target update frequency, in hertz. /// /// /// A value of 0.0 indicates that UpdateFrame events are generated at the maximum possible frequency (i.e. only limited by the hardware's capabilities). /// Values lower than 1.0Hz are clamped to 1.0Hz. Values higher than 200.0Hz are clamped to 200.0Hz. /// public double TargetUpdateFrequency { get { EnsureUndisposed(); if (TargetUpdatePeriod == 0.0) return 0.0; return 1.0 / TargetUpdatePeriod; } set { EnsureUndisposed(); if (value < 1.0) { TargetUpdatePeriod = 0.0; } else if (value <= 200.0) { TargetUpdatePeriod = 1.0 / value; } else Debug.Print("Target update frequency clamped to 200.0Hz."); // TODO: Where is it actually performed? } } #endregion #region TargetUpdatePeriod /// /// Gets or sets a double representing the target update period, in seconds. /// /// /// A value of 0.0 indicates that UpdateFrame events are generated at the maximum possible frequency (i.e. only limited by the hardware's capabilities). /// Values lower than 0.005 seconds (200Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0. /// public double TargetUpdatePeriod { get { EnsureUndisposed(); return target_update_period; } set { EnsureUndisposed(); if (value <= 0.005) { target_update_period = 0.0; } else if (value <= 1.0) { target_update_period = value; } else Debug.Print("Target update period clamped to 1.0 seconds."); // TODO: Where is it actually performed? } } #endregion #region UpdateFrequency /// /// Gets a double representing the frequency of UpdateFrame events, in hertz. /// public double UpdateFrequency { get { EnsureUndisposed(); if (update_period == 0.0) return 1.0; return 1.0 / update_period; } } #endregion #region UpdatePeriod /// /// Gets a double representing the period of UpdateFrame events, in seconds. /// public double UpdatePeriod { get { EnsureUndisposed(); return update_period; } } #endregion #region UpdateTime /// /// Gets a double representing the time spent in the UpdateFrame function, in seconds. /// public double UpdateTime { get { EnsureUndisposed(); return update_time; } } #endregion #endregion #region VSync /// /// Gets or sets the VSyncMode. /// public VSyncMode VSync { get { EnsureUndisposed(); GraphicsContext.Assert(); return vsync; } set { EnsureUndisposed(); GraphicsContext.Assert(); Context.VSync = (vsync = value) != VSyncMode.Off; } } #endregion #region WindowState /// /// Gets or states the state of the NativeWindow. /// public override WindowState WindowState { get { return base.WindowState; } set { base.WindowState = value; Debug.Print("Updating Context after setting WindowState to {0}", value); if (Context != null) Context.Update(WindowInfo); } } #endregion #endregion #region Events /// /// Occurs before the window is displayed for the first time. /// public event EventHandler Load = delegate { }; /// /// Occurs when it is time to render a frame. /// public event EventHandler RenderFrame = delegate { }; /// /// Occurs before the window is destroyed. /// public event EventHandler Unload = delegate { }; /// /// Occurs when it is time to update a frame. /// public event EventHandler UpdateFrame = delegate { }; #endregion #endregion #region --- Protected Members --- #region Dispose /// /// Override to add custom cleanup logic. /// /// True, if this method was called by the application; false if this was called by the finalizer thread. protected virtual void Dispose(bool manual) { } #endregion #region OnRenderFrame /// /// Called when the frame is rendered. /// /// Contains information necessary for frame rendering. /// /// Subscribe to the event instead of overriding this method. /// protected virtual void OnRenderFrame(FrameEventArgs e) { RenderFrame(this, e); } #endregion #region OnUpdateFrame /// /// Called when the frame is updated. /// /// Contains information necessary for frame updating. /// /// Subscribe to the event instead of overriding this method. /// protected virtual void OnUpdateFrame(FrameEventArgs e) { UpdateFrame(this, e); } #endregion #region OnWindowInfoChanged /// /// Called when the WindowInfo for this GameWindow has changed. /// /// Not used. protected virtual void OnWindowInfoChanged(EventArgs e) { } #endregion #region OnResize /// /// Called when this window is resized. /// /// Not used. /// /// You will typically wish to update your viewport whenever /// the window is resized. See the /// method. /// protected override void OnResize(EventArgs e) { base.OnResize(e); glContext.Update(base.WindowInfo); } #endregion #endregion #region --- Private Members --- #region OnLoadInternal private void OnLoadInternal(EventArgs e) { OnLoad(e); } #endregion #region OnRenderFrameInternal private void OnRenderFrameInternal(FrameEventArgs e) { if (Exists && !isExiting) OnRenderFrame(e); } #endregion #region OnUnloadInternal private void OnUnloadInternal(EventArgs e) { OnUnload(e); } #endregion #region OnUpdateFrameInternal private void OnUpdateFrameInternal(FrameEventArgs e) { if (Exists && !isExiting) OnUpdateFrame(e); } #endregion #region OnWindowInfoChangedInternal private void OnWindowInfoChangedInternal(EventArgs e) { glContext.MakeCurrent(WindowInfo); OnWindowInfoChanged(e); } #endregion #endregion } #region public enum VSyncMode /// /// Enumerates available VSync modes. /// public enum VSyncMode { /// /// Vsync disabled. /// Off = 0, /// /// VSync enabled. /// On, /// /// VSync enabled, unless framerate falls below one half of target framerate. /// If no target framerate is specified, this behaves exactly like . /// Adaptive, } #endregion }