#region --- License --- /* Copyright (c) 2006, 2007 Stefanos Apostolopoulos * See license.txt for license info */ #endregion using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; using OpenTK.Platform; using OpenTK.Input; using System.Threading; using OpenTK.OpenGL; 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 : INativeGLWindow { #region --- Fields --- INativeGLWindow glWindow; DisplayMode mode; ResizeEventArgs resizeEventArgs = new ResizeEventArgs(); bool isExiting; bool disposed; double update_period, render_period; double target_update_period, target_render_period, target_render_period_doubled; // TODO: Implement these: double update_time, render_time, event_time; //bool allow_sleep = true; // If true, GameWindow will call Timer.Sleep() if there is enough time. int width, height; VSyncMode vsync; #endregion #region --- Internal Properties --- bool MustResize { get { return glWindow.Width != this.Width || glWindow.Height != this.Height; } } #endregion #region --- Contructors --- /// /// Constructs a new GameWindow. Call CreateWindow() to open a render window. /// public GameWindow() { switch (Environment.OSVersion.Platform) { case PlatformID.Win32NT: case PlatformID.Win32S: case PlatformID.Win32Windows: case PlatformID.WinCE: glWindow = new OpenTK.Platform.Windows.WinGLNative(); break; case PlatformID.Unix: case (PlatformID)128: glWindow = new OpenTK.Platform.X11.X11GLNative(); break; default: throw new PlatformNotSupportedException("Your platform is not supported currently. Please, refer to http://opentk.sourceforge.net for more information."); } //glWindow.Resize += new ResizeEvent(glWindow_Resize); glWindow.Destroy += new DestroyEvent(glWindow_Destroy); } /// /// Constructs a new GameWindow, and opens a render window with the specified DisplayMode. /// /// The DisplayMode of the GameWindow. public GameWindow(DisplayMode mode) : this() { CreateWindow(mode); } /// /// Constructs a new GameWindow with the specified title, and opens a render window with the specified DisplayMode. /// /// The DisplayMode of the GameWindow. /// The Title of the GameWindow. public GameWindow(DisplayMode mode, string title) : this() { CreateWindow(mode, title); } void glWindow_Destroy(object sender, EventArgs e) { Debug.Print("GameWindow destruction imminent."); this.isExiting = true; this.OnDestroy(EventArgs.Empty); glWindow.Destroy -= glWindow_Destroy; //this.Dispose(); } void glWindow_Resize(object sender, ResizeEventArgs e) { this.OnResizeInternal(e); } #endregion #region --- Functions --- #region public void CreateWindow(DisplayMode mode, string title) /// /// Creates a render window for the calling GameWindow, with the specified DisplayMode and Title. /// /// The DisplayMode of the render window. /// The Title of the render window. /// /// It is an error to call this function when a render window already exists. /// Call DestroyWindow to close the render window. /// /// Occurs when a render window already exists. public void CreateWindow(DisplayMode mode, string title) { if (!Exists) { try { glWindow.CreateWindow(mode); this.Title = title; } catch (ApplicationException expt) { Debug.Print(expt.ToString()); throw; } } else { throw new ApplicationException("A render window already exists for this GameWindow."); } } #endregion #endregion #region --- INativeGLWindow Members --- #region public void Exit() /// /// Gracefully exits the current GameWindow. /// Override if you want to provide yor own exit sequence. /// If you override this method, place a call to base.Exit(), to ensure /// proper OpenTK shutdown. /// public virtual void Exit() { isExiting = true; //glWindow.Exit(); //this.Dispose(); } #endregion #region public bool IsIdle /// /// Gets a value indicating whether the current GameWindow is idle. /// If true, the OnUpdateFrame and OnRenderFrame functions should be called. /// public bool IsIdle { get { return glWindow.IsIdle; } } #endregion #region public bool Fullscreen /// /// TODO: This property is not implemented. /// Gets or sets a value indicating whether the GameWindow is in fullscrren mode. /// public bool Fullscreen { get { return false;/* return glWindow.Fullscreen; */ } set { /* glWindow.Fullscreen = value; */} } #endregion #region public IGLContext Context /// /// Returns the opengl IGLontext associated with the current GameWindow. /// Forces window creation. /// public IGLContext Context { get { if (!this.Exists && !this.IsExiting) { Debug.WriteLine("WARNING: OpenGL Context accessed before creating a render window. This may indicate a programming error. Force-creating a render window."); mode = new DisplayMode(640, 480); this.CreateWindow(mode); } return glWindow.Context; } } #endregion #region public bool Exists /// /// Gets a value indicating whether a render window exists. /// public bool Exists { get { return glWindow == null ? false : glWindow.Exists; } } #endregion #region public string Text /// /// Gets or sets the GameWindow title. /// public string Title { get { return glWindow.Title; } set { glWindow.Title = value; } } #endregion #region public bool Visible /// /// TODO: This property is not implemented /// Gets or sets a value indicating whether the GameWindow is visible. /// public bool Visible { get { throw new NotImplementedException(); return glWindow.Visible; } set { throw new NotImplementedException(); glWindow.Visible = value; } } #endregion #region public IWindowInfo WindowInfo public IWindowInfo WindowInfo { get { return glWindow.WindowInfo; } } #endregion #region public IInputDriver InputDriver /// /// Gets an interface to the InputDriver used to obtain Keyboard, Mouse and Joystick input. /// public IInputDriver InputDriver { get { return glWindow.InputDriver; } } #endregion #region public void CreateWindow(DisplayMode mode) /// /// Creates a render window for the calling GameWindow. /// /// The DisplayMode of the render window. /// /// It is an error to call this function when a render window already exists. /// Call DestroyWindow to close the render window. /// /// Occurs when a render window already exists. public void CreateWindow(DisplayMode mode) { if (!Exists) { try { glWindow.CreateWindow(mode); } catch (ApplicationException expt) { Debug.Print(expt.ToString()); throw; } } else { throw new ApplicationException("A render window already exists for this GameWindow."); } } #endregion #region OnCreate [Obsolete("The Create event is obsolete and will be removed on later versions. Use the Load event instead.")] public event CreateEvent Create; /// /// Raises the Create event. Override in derived classes to initialize resources. /// /// [Obsolete("The OnCreate method is obsolete and will be removed on later versions. Use the OnLoad method instead.")] public virtual void OnCreate(EventArgs e) { Debug.WriteLine("Firing GameWindow.Create event"); if (this.Create != null) { this.Create(this, e); } } #endregion #region public void DestroyWindow() /// /// Destroys the GameWindow. The Destroy event is raised before destruction commences /// (while the opengl context still exists), to allow resource cleanup. /// public void DestroyWindow() { if (Exists) { glWindow.DestroyWindow(); } else { throw new ApplicationException("Tried to destroy inexistent window."); } } #endregion #region OnDestroy /// /// Raises the Destroy event. Override in derived classes, to modify the shutdown /// sequence (e.g. to release resources before shutdown). /// /// public virtual void OnDestroy(EventArgs e) { Debug.WriteLine("Firing GameWindow.Destroy event"); if (this.Destroy != null) { this.Destroy(this, e); } } public event DestroyEvent Destroy; #endregion #endregion #region --- GameWindow Methods --- #region void Run() /// /// Enters the game loop of the GameWindow updating and rendering at the maximum possible frequency. /// /// public void Run() { Run(0.0, 0.0); } /// /// Enters the game loop of the GameWindow updating the specified update frequency, while maintaining the /// maximum possible render frequency. /// /// public void Run(double updateFrequency) { Run(updateFrequency, 0.0); } /// /// Enters the game loop of the GameWindow updating and rendering at the specified frequency. /// /// The frequency of UpdateFrame events. /// The frequency of RenderFrame events. public void Run(double updates_per_second, double frames_per_second) { 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; Stopwatch update_watch = new Stopwatch(), render_watch = new Stopwatch(); double time, next_render = 0.0, next_update = 0.0, update_time_counter = 0.0; int num_updates = 0; UpdateFrameEventArgs update_args = new UpdateFrameEventArgs(); RenderFrameEventArgs render_args = new RenderFrameEventArgs(); double sleep_granularity; // In seconds. //GC.Collect(2); //GC.WaitForPendingFinalizers(); //GC.Collect(2); // Find the minimum granularity of the Thread.Sleep() function. // TODO: Disabled - see comment on Thread.Sleep() problems below. //update_watch.Start(); //const int test_times = 5; //for (int i = test_times; --i > 0; ) // Thread.Sleep(1); //update_watch.Stop(); //sleep_granularity = System.Math.Round(1000.0 * update_watch.Elapsed.TotalSeconds / test_times, MidpointRounding.AwayFromZero) / 1000.0; //update_watch.Reset(); // We don't want to affect the first UpdateFrame! OnLoadInternal(EventArgs.Empty); Thread.CurrentThread.Priority = ThreadPriority.AboveNormal; while (!isExiting) { // Process events ProcessEvents(); if (isExiting) break; // Raise UpdateFrame events time = update_watch.Elapsed.TotalSeconds; if (time > 1.0) time = 1.0; while (next_update - time <= 0.0) { next_update = next_update - time + TargetUpdatePeriod; update_time_counter += time; ++num_updates; update_watch.Reset(); update_watch.Start(); update_args.Time = time; OnUpdateFrameInternal(update_args); if (TargetUpdateFrequency == 0.0) break; time = update_watch.Elapsed.TotalSeconds; next_update -= time; update_time_counter += time; // Allow up to 10 frames to be dropped. // Prevent the application from hanging with very high update frequencies. if (num_updates >= 10) break; } if (num_updates > 0) { update_period = update_time_counter / (double)num_updates; num_updates = 0; update_time_counter = 0.0; } // Raise RenderFrame events if (isExiting) break; time = render_watch.Elapsed.TotalSeconds; if (time > 1.0) time = 1.0; if (next_render - time <= 0.0) { next_render = next_render - time + TargetRenderPeriod; render_watch.Reset(); render_watch.Start(); render_period = render_args.Time = time; render_args.ScaleFactor = RenderPeriod / UpdatePeriod; OnRenderFrameInternal(render_args); } // Yield CPU time, if the Thread.Sleep() granularity allows it. // TODO: Disabled because it does not work reliably enough on all systems. // Enable vsync as a workaround. //if (AllowSleep && next_render > sleep_granularity && next_update > sleep_granularity) //{ // Thread.Sleep((int)(1000.0 * System.Math.Min(next_render - sleep_granularity, next_update - sleep_granularity))); //} } Thread.CurrentThread.Priority = ThreadPriority.Normal; OnUnloadInternal(EventArgs.Empty); if (this.Exists) { glWindow.DestroyWindow(); while (this.Exists) { this.ProcessEvents(); } } } #endregion #region public void ProcessEvents() /// /// Processes operating system events until the GameWindow becomes idle. /// /// /// When overriding the default GameWindow game loop (provided by the Run() function) /// 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. /// /// public void ProcessEvents() { //if (!isExiting) // InputDriver.Poll(); glWindow.ProcessEvents(); } #endregion #region OnRenderFrame(RenderFrameEventArgs e) /// /// Raises the RenderFrame event, and calls the public function. /// /// private void OnRenderFrameInternal(RenderFrameEventArgs e) { if (!this.Exists && !this.IsExiting) { Debug.Print("WARNING: RenderFrame event raised, without a valid render window. This may indicate a programming error. Creating render window."); mode = new DisplayMode(640, 480); this.CreateWindow(mode); } if (RenderFrame != null) RenderFrame(this, e); // Call the user's override. OnRenderFrame(e); } /// /// Override in derived classes to render a frame. /// /// Contains information necessary for frame rendering. /// /// The base implementation (base.OnRenderFrame) is empty, there is no need to call it. /// public virtual void OnRenderFrame(RenderFrameEventArgs e) { } /// /// Occurs when it is time to render the next frame. /// public event RenderFrameEvent RenderFrame; #endregion #region OnUpdateFrame(UpdateFrameEventArgs e) private void OnUpdateFrameInternal(UpdateFrameEventArgs e) { if (!this.Exists && !this.IsExiting) { Debug.Print("WARNING: UpdateFrame event raised without a valid render window. This may indicate a programming error. Creating render window."); mode = new DisplayMode(640, 480); this.CreateWindow(mode); } if (MustResize) { resizeEventArgs.Width = glWindow.Width; resizeEventArgs.Height = glWindow.Height; OnResizeInternal(resizeEventArgs); } if (UpdateFrame != null) { UpdateFrame(this, e); } OnUpdateFrame(e); } /// /// Override in derived classes to update a frame. /// /// Contains information necessary for frame updating. /// /// The base implementation (base.OnUpdateFrame) is empty, there is no need to call it. /// public virtual void OnUpdateFrame(UpdateFrameEventArgs e) { } /// /// Occurs when it is time to update the next frame. /// public event UpdateFrameEvent UpdateFrame; #endregion #region OnLoad(EventArgs e) /// /// Occurs after establishing an OpenGL context, but before entering the main loop. /// public event LoadEvent Load; /// /// Raises the Load event, and calls the user's OnLoad override. /// /// private void OnLoadInternal(EventArgs e) { Debug.WriteLine(String.Format("OpenGL driver information: {0}, {1}, {2}", GL.GetString(GL.Enums.StringName.RENDERER), GL.GetString(GL.Enums.StringName.VENDOR), GL.GetString(GL.Enums.StringName.VERSION))); if (this.Load != null) { this.Load(this, e); } OnLoad(e); } /// /// Occurs after establishing an OpenGL context, but before entering the main loop. /// Override to load resources that should be maintained for the lifetime of the application. /// /// Not used. public virtual void OnLoad(EventArgs e) { } #endregion #region OnUnload(EventArgs e) /// /// Occurs after after calling GameWindow.Exit, but before destroying the OpenGL context. /// public event UnloadEvent Unload; /// /// Raises the Unload event, and calls the user's OnUnload override. /// /// private void OnUnloadInternal(EventArgs e) { if (this.Unload != null) { this.Unload(this, e); } OnUnload(e); } /// /// Occurs after after calling GameWindow.Exit, but before destroying the OpenGL context. /// Override to unload application resources. /// /// Not used. public virtual void OnUnload(EventArgs e) { } #endregion #region public bool 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.OpenGL functions or properties. /// public bool IsExiting { get { return isExiting; } } #endregion #region public Keyboard Keyboard /// /// Gets the primary Keyboard device, or null if no Keyboard exists. /// public KeyboardDevice Keyboard { get { if (InputDriver.Keyboard.Count > 0) return InputDriver.Keyboard[0]; else return null; } } #endregion #region public Mouse Mouse /// /// Gets the primary Mouse device, or null if no Mouse exists. /// public MouseDevice Mouse { get { if (InputDriver.Mouse.Count > 0) return InputDriver.Mouse[0]; else return null; } } #endregion #region public VSyncMode VSync /// /// Gets or sets the VSyncMode. /// public VSyncMode VSync { get { return vsync; } set { if (value == VSyncMode.Off) Context.VSync = false; else if (value == VSyncMode.On) Context.VSync = true; vsync = value; } } #endregion #region public void SwapBuffers() /// /// Swaps the front and back buffer, presenting the rendered scene to the user. /// Only useful in double- or triple-buffered formats. /// /// Calling this function is equivalent to calling Context.SwapBuffers() public void SwapBuffers() { Context.SwapBuffers(); } #endregion #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 public double TargetRenderPeriod /// /// Gets or sets 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 { return target_render_period; } set { if (value <= 0.005) { target_render_period = target_render_period_doubled = 0.0; } else if (value <= 1.0) { target_render_period = value; target_render_period_doubled = 2.0 * target_render_period; } else Debug.Print("Target render period clamped to 1.0 seconds."); } } #endregion #region public double TargetRenderFrequency /// /// Gets or sets the target render frequency in Herz. /// /// /// 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 { if (TargetRenderPeriod == 0.0) return 0.0; return 1.0 / TargetRenderPeriod; } set { 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."); } } #endregion #region public double TargetUpdatePeriod /// /// Gets or sets 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 { return target_update_period; } set { 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."); } } #endregion #region public double TargetUpdateFrequency /// /// Gets or sets the target update frequency in Herz. /// /// /// 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 { if (TargetUpdatePeriod == 0.0) return 0.0; return 1.0 / TargetUpdatePeriod; } set { 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."); } } #endregion #region public double RenderFrequency /// /// Gets the actual frequency of RenderFrame events in Herz (i.e. FPS or Frames Per Second). /// public double RenderFrequency { get { if (render_period == 0.0) return 1.0; return 1.0 / render_period; } } #endregion #region public double RenderPeriod /// /// Gets the period of RenderFrame events in seconds. /// public double RenderPeriod { get { return render_period; } } #endregion #region public double UpdateFrequency /// /// Gets the frequency of UpdateFrame events in Herz. /// public double UpdateFrequency { get { if (update_period == 0.0) return 1.0; return 1.0 / update_period; } } #endregion #region public double UpdatePeriod /// /// Gets the period of UpdateFrame events in seconds. /// public double UpdatePeriod { get { return update_period; } } #endregion #endregion #region --- IResizable Members --- #region public int Width, Height /// /// Gets or sets the Width of the GameWindow's rendering area, in pixels. /// public int Width { get { return width; } set { if (value == this.Width) { return; } else if (value > 0) { glWindow.Width = value; } else { throw new ArgumentOutOfRangeException( "Width", value, "Width must be greater than 0" ); } } } /// /// Gets or sets the Height of the GameWindow's rendering area, in pixels. /// public int Height { get { return height; } set { if (value == this.Height) { return; } else if (value > 0) { glWindow.Height = value; } else { throw new ArgumentOutOfRangeException( "Height", value, "Height must be greater than 0" ); } } } #endregion #region public event ResizeEvent Resize; /// /// Occurs when the GameWindow is resized. Derived classes should override the OnResize method for better performance. /// public event ResizeEvent Resize; /// /// Raises the Resize event. /// /// Contains information about the Resize event. private void OnResizeInternal(ResizeEventArgs e) { Debug.Print("Firing GameWindow.Resize event: {0}.", e.ToString()); this.width = e.Width; this.height = e.Height; if (this.Resize != null) this.Resize(this, e); OnResize(e); } /// /// Override in derived classes to respond to the Resize events. /// /// Contains information about the Resize event. protected virtual void OnResize(ResizeEventArgs e) { } #endregion /* /// /// Gets the Top coordinate of the GameWindow's rendering area, in pixel coordinates relative to the GameWindow's top left point. /// public int Top { get { return glWindow.Top; } } /// /// /// Gets the Bottom coordinate of the GameWindow's rendering area, in pixel coordinates relative to the GameWindow's top left point. /// public int Bottom { get { return glWindow.Bottom; } } /// /// Gets the Left coordinate of the GameWindow's rendering area, in pixel coordinates relative to the GameWindow's top left point. /// public int Left { get { return glWindow.Left; } } /// /// Gets the Right coordinate of the GameWindow's rendering area, in pixel coordinates relative to the GameWindow's top left point. /// public int Right { get { return glWindow.Right; } } */ #endregion #region --- IDisposable Members --- /// /// Not used yet. /// private void DisposeInternal() { Dispose(); // User overridable Dispose method. } /// /// Disposes of the GameWindow, releasing all resources consumed by it. /// public virtual void Dispose() { Dispose(true); // Real Dispose method. GC.SuppressFinalize(this); } private void Dispose(bool manual) { if (!disposed) { // Is this safe? Maybe 'Debug' has been disposed, too... //Debug.Print("{0} disposing GameWindow.", manual ? "Manually" : "Automatically"); if (manual) { if (glWindow != null) { glWindow.Dispose(); glWindow = null; } } disposed = true; } } ~GameWindow() { this.Dispose(false); } #endregion } #region public enum VSyncMode /// /// Enumerates the available VSync modes. /// public enum VSyncMode { /// /// Vsync disabled. /// Off = 0, /// /// VSync enabled. /// On, /// /// VSync enabled, but automatically disabled if framerate falls below a specified limit. /// Adaptive, } #endregion #region --- GameWindow Events --- public delegate void UpdateFrameEvent(GameWindow sender, UpdateFrameEventArgs e); public delegate void RenderFrameEvent(GameWindow sender, RenderFrameEventArgs e); public delegate void LoadEvent(GameWindow sender, EventArgs e); public delegate void UnloadEvent(GameWindow sender, EventArgs e); public class UpdateFrameEventArgs : EventArgs { private double time; /// /// Gets the Time elapsed between frame updates, in seconds. /// public double Time { get { return time; } internal set { time = value; } } } public class RenderFrameEventArgs : EventArgs { private double time; private double scale_factor; /// /// Gets the Time elapsed between frame updates, in seconds. /// public double Time { get { return time; } internal set { time = value; } } public double ScaleFactor { get { return scale_factor; } internal set { if (value != 0.0 && !Double.IsNaN(value)) scale_factor = value; else scale_factor = 1.0; } } } #endregion #region --- GameWindow Exceptions --- public class GameWindowExitException : ApplicationException { } #endregion }