Merge branch 'issue43_unstablefps' into develop

This commit is contained in:
Stefanos A 2014-01-14 14:21:19 +01:00
commit 962a9f7733
2 changed files with 203 additions and 77 deletions

View file

@ -25,9 +25,23 @@ namespace Examples.Tests
int texture; int texture;
bool mouse_in_window = false; bool mouse_in_window = false;
bool viewport_changed = true; bool viewport_changed = true;
// time drift
Stopwatch watch = new Stopwatch(); Stopwatch watch = new Stopwatch();
double update_time, render_time; double update_time, render_time;
// timing information
double timestamp;
int update_count;
int update_fps;
int render_count;
int render_fps;
// position of moving objects on screen
double variable_update_timestep_pos = -1;
double variable_refresh_timestep_pos = -1;
double fixed_update_timestep_pos = -1;
public GameWindowStates() public GameWindowStates()
: base(800, 600, GraphicsMode.Default) : base(800, 600, GraphicsMode.Default)
{ {
@ -35,10 +49,10 @@ namespace Examples.Tests
Keyboard.KeyRepeat = true; Keyboard.KeyRepeat = true;
KeyDown += KeyDownHandler; KeyDown += KeyDownHandler;
KeyPress += KeyPressHandler; KeyPress += KeyPressHandler;
MouseEnter += delegate { mouse_in_window = true; }; MouseEnter += delegate { mouse_in_window = true; };
MouseLeave += delegate { mouse_in_window = false; }; MouseLeave += delegate { mouse_in_window = false; };
Mouse.Move += MouseMoveHandler; Mouse.Move += MouseMoveHandler;
Mouse.ButtonDown += MouseButtonHandler; Mouse.ButtonDown += MouseButtonHandler;
Mouse.ButtonUp += MouseButtonHandler; Mouse.ButtonUp += MouseButtonHandler;
@ -110,7 +124,7 @@ namespace Examples.Tests
{ {
return val > max ? max : val < min ? min : val; return val > max ? max : val < min ? min : val;
} }
static float DrawString(Graphics gfx, string str, int line) static float DrawString(Graphics gfx, string str, int line)
{ {
return DrawString(gfx, str, line, 0); return DrawString(gfx, str, line, 0);
@ -217,6 +231,8 @@ namespace Examples.Tests
{ {
double clock_time = watch.Elapsed.TotalSeconds; double clock_time = watch.Elapsed.TotalSeconds;
update_time += e.Time; update_time += e.Time;
timestamp += e.Time;
update_count++;
using (Graphics gfx = Graphics.FromImage(TextBitmap)) using (Graphics gfx = Graphics.FromImage(TextBitmap))
{ {
@ -247,22 +263,47 @@ namespace Examples.Tests
// Timing information // Timing information
line++; line++;
DrawString(gfx, "Timing:", line++); DrawString(gfx, "Timing:", line++);
DrawString(gfx, String.Format("Frequency: update ({0:f2}/{1:f2}); render ({2:f2}/{3:f2})", DrawString(gfx,
UpdateFrequency, TargetUpdateFrequency, RenderFrequency, TargetRenderFrequency), line++); String.Format("Frequency: update {4} ({0:f2}/{1:f2}); render {5} ({2:f2}/{3:f2})",
DrawString(gfx, String.Format("Period: update ({0:f4}/{1:f4}); render ({2:f4}/{3:f4})", UpdateFrequency, TargetUpdateFrequency,
UpdatePeriod, TargetUpdatePeriod, RenderPeriod, TargetRenderPeriod), line++); RenderFrequency, TargetRenderFrequency,
update_fps, render_fps),
line++);
DrawString(gfx,
String.Format("Period: update {4:N4} ({0:f4}/{1:f4}); render {5:N4} ({2:f4}/{3:f4})",
UpdatePeriod, TargetUpdatePeriod,
RenderPeriod, TargetRenderPeriod,
1.0 / update_fps, 1.0 / render_fps),
line++);
DrawString(gfx, String.Format("Time: update {0:f4}; render {1:f4}", DrawString(gfx, String.Format("Time: update {0:f4}; render {1:f4}",
UpdateTime, RenderTime), line++); UpdateTime, RenderTime), line++);
DrawString(gfx, String.Format("Drift: clock {0:f4}; update {1:f4}; render {2:f4}", DrawString(gfx, String.Format("Drift: clock {0:f4}; update {1:f4}; render {2:f4}",
clock_time, clock_time - update_time, clock_time - render_time), line++); clock_time, clock_time - update_time, clock_time - render_time), line++);
DrawString(gfx, String.Format("Text: {0}", TypedText.ToString()), line++); DrawString(gfx, String.Format("Text: {0}", TypedText.ToString()), line++);
if (timestamp >= 1)
{
timestamp -= 1;
update_fps = update_count;
render_fps = render_count;
update_count = 0;
render_count = 0;
}
// Input information // Input information
line = DrawKeyboards(gfx, line); line = DrawKeyboards(gfx, line);
line = DrawMice(gfx, line); line = DrawMice(gfx, line);
line = DrawJoysticks(gfx, line); line = DrawJoysticks(gfx, line);
line = DrawLegacyJoysticks(gfx, Joysticks, line); line = DrawLegacyJoysticks(gfx, Joysticks, line);
} }
fixed_update_timestep_pos += TargetUpdatePeriod;
variable_update_timestep_pos += e.Time;
if (fixed_update_timestep_pos >= 1)
fixed_update_timestep_pos -= 2;
if (variable_update_timestep_pos >= 1)
variable_update_timestep_pos -= 2;
} }
int DrawJoysticks(Graphics gfx, int line) int DrawJoysticks(Graphics gfx, int line)
@ -323,7 +364,31 @@ namespace Examples.Tests
protected override void OnRenderFrame(FrameEventArgs e) protected override void OnRenderFrame(FrameEventArgs e)
{ {
render_time += e.Time; render_time += e.Time;
render_count++;
GL.Clear(ClearBufferMask.ColorBufferBit);
if (viewport_changed)
{
viewport_changed = false;
GL.Viewport(0, 0, Width, Height);
}
DrawText();
DrawMovingObjects();
variable_refresh_timestep_pos += e.Time;
if (variable_refresh_timestep_pos >= 1)
variable_refresh_timestep_pos -= 2;
SwapBuffers();
}
// Uploads our text Bitmap to an OpenGL texture
// and displays is to screen.
private void DrawText()
{
System.Drawing.Imaging.BitmapData data = TextBitmap.LockBits( System.Drawing.Imaging.BitmapData data = TextBitmap.LockBits(
new System.Drawing.Rectangle(0, 0, TextBitmap.Width, TextBitmap.Height), new System.Drawing.Rectangle(0, 0, TextBitmap.Width, TextBitmap.Height),
System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb); System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
@ -331,29 +396,63 @@ namespace Examples.Tests
PixelType.UnsignedByte, data.Scan0); PixelType.UnsignedByte, data.Scan0);
TextBitmap.UnlockBits(data); TextBitmap.UnlockBits(data);
if (viewport_changed) Matrix4 text_projection = Matrix4.CreateOrthographicOffCenter(0, Width, Height, 0, -1, 1);
{ GL.MatrixMode(MatrixMode.Projection);
viewport_changed = false; GL.LoadMatrix(ref text_projection);
GL.MatrixMode(MatrixMode.Modelview);
GL.Viewport(0, 0, Width, Height); GL.LoadIdentity();
Matrix4 ortho_projection = Matrix4.CreateOrthographicOffCenter(0, Width, Height, 0, -1, 1);
GL.MatrixMode(MatrixMode.Projection);
GL.LoadMatrix(ref ortho_projection);
}
GL.Clear(ClearBufferMask.ColorBufferBit);
GL.Color4(Color4.White);
GL.Enable(EnableCap.Texture2D);
GL.Begin(PrimitiveType.Quads); GL.Begin(PrimitiveType.Quads);
GL.TexCoord2(0, 0); GL.Vertex2(0, 0); GL.TexCoord2(0, 0); GL.Vertex2(0, 0);
GL.TexCoord2(1, 0); GL.Vertex2(TextBitmap.Width, 0); GL.TexCoord2(1, 0); GL.Vertex2(TextBitmap.Width, 0);
GL.TexCoord2(1, 1); GL.Vertex2(TextBitmap.Width, TextBitmap.Height); GL.TexCoord2(1, 1); GL.Vertex2(TextBitmap.Width, TextBitmap.Height);
GL.TexCoord2(0, 1); GL.Vertex2(0, TextBitmap.Height); GL.TexCoord2(0, 1); GL.Vertex2(0, TextBitmap.Height);
GL.End(); GL.End();
GL.Disable(EnableCap.Texture2D);
}
SwapBuffers(); // Draws three moving objects, using three different timing methods:
// 1. fixed framerate based on TargetUpdatePeriod
// 2. variable framerate based on UpdateFrame e.Time
// 3. variable framerate based on RenderFrame e.Time
// If the timing implementation is correct, all three objects
// should be moving at the same speed, regardless of the current
// UpdatePeriod and RenderPeriod.
void DrawMovingObjects()
{
Matrix4 thing_projection = Matrix4.CreateOrthographic(2, 2, -1, 1);
GL.MatrixMode(MatrixMode.Projection);
GL.LoadMatrix(ref thing_projection);
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadIdentity();
GL.Translate(fixed_update_timestep_pos, -0.2, 0);
GL.Color4(Color4.Red);
DrawRectangle();
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadIdentity();
GL.Translate(variable_update_timestep_pos, -0.4, 0);
GL.Color4(Color4.DarkGoldenrod);
DrawRectangle();
GL.MatrixMode(MatrixMode.Modelview);
GL.LoadIdentity();
GL.Translate(variable_refresh_timestep_pos, -0.8, 0);
GL.Color4(Color4.DarkGreen);
DrawRectangle();
}
private void DrawRectangle()
{
GL.Begin(PrimitiveType.Quads);
GL.Vertex2(-0.05, -0.05);
GL.Vertex2(+0.05, -0.05);
GL.Vertex2(+0.05, +0.05);
GL.Vertex2(-0.05, +0.05);
GL.End();
} }
public static void Main() public static void Main()

View file

@ -75,6 +75,8 @@ namespace OpenTK
{ {
#region --- Fields --- #region --- Fields ---
const double MaxFrequency = 500.0; // Frequency cap for Update/RenderFrame events
readonly Stopwatch watch = new Stopwatch(); readonly Stopwatch watch = new Stopwatch();
IGraphicsContext glContext; IGraphicsContext glContext;
@ -90,6 +92,10 @@ namespace OpenTK
double update_timestamp; // timestamp of last UpdateFrame event double update_timestamp; // timestamp of last UpdateFrame event
double render_timestamp; // timestamp of last RenderFrame event double render_timestamp; // timestamp of last RenderFrame event
double update_epsilon; // quantization error for UpdateFrame events
bool is_running_slowly; // true, when UpdatePeriod cannot reach TargetUpdatePeriod
VSyncMode vsync; VSyncMode vsync;
FrameEventArgs update_args = new FrameEventArgs(); FrameEventArgs update_args = new FrameEventArgs();
@ -431,63 +437,84 @@ namespace OpenTK
} }
} }
double ClampElapsed(double elapsed)
{
return MathHelper.Clamp(elapsed, 0.0, 1.0);
}
void DispatchUpdateAndRenderFrame(object sender, EventArgs e) void DispatchUpdateAndRenderFrame(object sender, EventArgs e)
{ {
const int max_frameskip = 10; int is_running_slowly_retries = 4;
int frameskip = 0;
double timestamp = watch.Elapsed.TotalSeconds; double timestamp = watch.Elapsed.TotalSeconds;
double elapsed = 0;
do elapsed = ClampElapsed(timestamp - update_timestamp);
while (elapsed > 0 && elapsed + update_epsilon >= TargetUpdatePeriod)
{ {
// Raise UpdateFrame events until we catch up with our target update rate. RaiseUpdateFrame(elapsed, ref timestamp);
double update_elapsed = MathHelper.Clamp(timestamp - update_timestamp, 0.0, 1.0);
if (RaiseUpdateFrame(update_elapsed)) // Calculate difference (positive or negative) between
// actual elapsed time and target elapsed time. We must
// compensate for this difference.
update_epsilon += elapsed - TargetUpdatePeriod;
// Prepare for next loop
elapsed = ClampElapsed(timestamp - update_timestamp);
if (TargetUpdatePeriod <= Double.Epsilon)
{ {
update_period = update_elapsed; // According to the TargetUpdatePeriod documentation,
update_timestamp = timestamp; // a TargetUpdatePeriod of zero means we will raise
timestamp = watch.Elapsed.TotalSeconds; // UpdateFrame events as fast as possible (one event
update_time = timestamp - update_timestamp; // per ProcessEvents() call)
}
else
{
// We have executed enough UpdateFrame events to catch up.
// Break and issue a RenderFrame event.
break; break;
} }
} while (++frameskip < max_frameskip);
is_running_slowly = update_epsilon >= TargetUpdatePeriod;
if (is_running_slowly && --is_running_slowly_retries == 0)
{
// If UpdateFrame consistently takes longer than TargetUpdateFrame
// stop raising events to avoid hanging inside the UpdateFrame loop.
break;
}
}
elapsed = ClampElapsed(timestamp - render_timestamp);
if (elapsed > 0 && elapsed >= TargetRenderPeriod)
{
RaiseRenderFrame(elapsed, ref timestamp);
}
}
void RaiseUpdateFrame(double elapsed, ref double timestamp)
{
// Raise UpdateFrame event
update_args.Time = elapsed;
OnUpdateFrameInternal(update_args);
// Update UpdatePeriod/UpdateFrequency properties
update_period = elapsed;
// Update UpdateTime property
update_timestamp = timestamp;
timestamp = watch.Elapsed.TotalSeconds; timestamp = watch.Elapsed.TotalSeconds;
double render_elapsed = MathHelper.Clamp(timestamp - render_timestamp, 0.0, 1.0); update_time = timestamp - update_timestamp;
if (RaiseRenderFrame(render_elapsed))
{
render_period = render_elapsed;
render_timestamp = timestamp;
timestamp = watch.Elapsed.TotalSeconds;
render_time = timestamp - render_timestamp;
}
}
bool RaiseUpdateFrame(double time)
{
if (time > 0 && time >= TargetUpdatePeriod)
{
update_args.Time = time;
OnUpdateFrameInternal(update_args);
return true;
}
return false;
} }
bool RaiseRenderFrame(double time) void RaiseRenderFrame(double elapsed, ref double timestamp)
{ {
if (time > 0 && time >= TargetRenderPeriod) // Raise RenderFrame event
{ render_args.Time = elapsed;
render_args.Time = time; OnRenderFrameInternal(render_args);
OnRenderFrameInternal(render_args);
return true; // Update RenderPeriod/UpdateFrequency properties
} render_period = elapsed;
return false;
// Update RenderTime property
render_timestamp = timestamp;
timestamp = watch.Elapsed.TotalSeconds;
render_time = timestamp - render_timestamp;
} }
#endregion #endregion
@ -656,7 +683,7 @@ namespace OpenTK
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>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).</para> /// <para>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).</para>
/// <para>Values lower than 1.0Hz are clamped to 1.0Hz. Values higher than 200.0Hz are clamped to 200.0Hz.</para> /// <para>Values lower than 1.0Hz are clamped to 0.0. Values higher than 500.0Hz are clamped to 200.0Hz.</para>
/// </remarks> /// </remarks>
public double TargetRenderFrequency public double TargetRenderFrequency
{ {
@ -674,11 +701,11 @@ namespace OpenTK
{ {
TargetRenderPeriod = 0.0; TargetRenderPeriod = 0.0;
} }
else if (value <= 200.0) else if (value <= MaxFrequency)
{ {
TargetRenderPeriod = 1.0 / value; TargetRenderPeriod = 1.0 / value;
} }
else Debug.Print("Target render frequency clamped to 200.0Hz."); // TODO: Where is it actually performed? else Debug.Print("Target render frequency clamped to {0}Hz.", MaxFrequency);
} }
} }
@ -691,7 +718,7 @@ namespace OpenTK
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>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).</para> /// <para>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).</para>
/// <para>Values lower than 0.005 seconds (200Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0.</para> /// <para>Values lower than 0.002 seconds (500Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0.</para>
/// </remarks> /// </remarks>
public double TargetRenderPeriod public double TargetRenderPeriod
{ {
@ -703,7 +730,7 @@ namespace OpenTK
set set
{ {
EnsureUndisposed(); EnsureUndisposed();
if (value <= 0.005) if (value <= 1 / MaxFrequency)
{ {
target_render_period = 0.0; target_render_period = 0.0;
} }
@ -724,7 +751,7 @@ namespace OpenTK
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>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).</para> /// <para>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).</para>
/// <para>Values lower than 1.0Hz are clamped to 1.0Hz. Values higher than 200.0Hz are clamped to 200.0Hz.</para> /// <para>Values lower than 1.0Hz are clamped to 0.0. Values higher than 500.0Hz are clamped to 500.0Hz.</para>
/// </remarks> /// </remarks>
public double TargetUpdateFrequency public double TargetUpdateFrequency
{ {
@ -742,11 +769,11 @@ namespace OpenTK
{ {
TargetUpdatePeriod = 0.0; TargetUpdatePeriod = 0.0;
} }
else if (value <= 200.0) else if (value <= MaxFrequency)
{ {
TargetUpdatePeriod = 1.0 / value; TargetUpdatePeriod = 1.0 / value;
} }
else Debug.Print("Target update frequency clamped to 200.0Hz."); // TODO: Where is it actually performed? else Debug.Print("Target render frequency clamped to {0}Hz.", MaxFrequency);
} }
} }
@ -759,7 +786,7 @@ namespace OpenTK
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// <para>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).</para> /// <para>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).</para>
/// <para>Values lower than 0.005 seconds (200Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0.</para> /// <para>Values lower than 0.002 seconds (500Hz) are clamped to 0.0. Values higher than 1.0 seconds (1Hz) are clamped to 1.0.</para>
/// </remarks> /// </remarks>
public double TargetUpdatePeriod public double TargetUpdatePeriod
{ {
@ -771,7 +798,7 @@ namespace OpenTK
set set
{ {
EnsureUndisposed(); EnsureUndisposed();
if (value <= 0.005) if (value <= 1 / MaxFrequency)
{ {
target_update_period = 0.0; target_update_period = 0.0;
} }
@ -779,7 +806,7 @@ namespace OpenTK
{ {
target_update_period = value; target_update_period = value;
} }
else Debug.Print("Target update period clamped to 1.0 seconds."); // TODO: Where is it actually performed? else Debug.Print("Target update period clamped to 1.0 seconds.");
} }
} }