Reputation: 199
What is an efficient and accurate way of syncing the frames per second in an OpenGL window using C++. I have tried just putting Sleep(17);
in my main game loop and that gets it down to 59 frames per second but it is not accurate and efficient. Here is my OpenGL window code without the Sleep(17);
in my main loop:
#include <windows.h>
#include <gl\gl.h>
HDC hDC = NULL;
HGLRC hRC = NULL;
HWND hWnd = NULL;
HINSTANCE hInstance;
bool keys[256];
bool active = true;
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height == 0)
{
height = 1;
}
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(0, 800, 0, 600, 1, -1);
glMatrixMode(GL_MODELVIEW);
}
int InitGL(GLvoid)
{
glShadeModel(GL_SMOOTH);
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
glClearDepth(1.0f);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
return 1;
}
int DrawGLScene(GLvoid)
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
return 1;
}
GLvoid KillGLWindow(GLvoid)
{
if (hRC)
{
if (!wglMakeCurrent(NULL,NULL))
{
MessageBox(NULL, "Release Of DC And RC Failed." ,"SHUTDOWN ERROR" ,MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC))
{
MessageBox(NULL, "Release Rendering Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION);
}
hRC = NULL;
}
if (hDC && !ReleaseDC(hWnd, hDC))
{
MessageBox(NULL, "Release Device Context Failed.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION);
hDC = NULL;
}
if (hWnd && !DestroyWindow(hWnd))
{
MessageBox(NULL, "Could Not Release hWnd.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION);
hWnd = NULL;
}
if (!UnregisterClass("Project2DClass",hInstance))
{
MessageBox(NULL, "Could Not Unregister Class.", "SHUTDOWN ERROR", MB_OK | MB_ICONINFORMATION);
hInstance = NULL;
}
}
BOOL CreateGLWindow(char* title, int width, int height, int bits)
{
GLuint PixelFormat;
WNDCLASS wc;
DWORD dwExStyle;
DWORD dwStyle;
RECT WindowRect;
WindowRect.left = (long)0;
WindowRect.right = (long)width;
WindowRect.top = (long)0;
WindowRect.bottom = (long)height;
hInstance = GetModuleHandle(NULL);
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_WINLOGO);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = "Project2DClass";
if (!RegisterClass(&wc))
{
MessageBox(NULL, "Failed To Register The Window Class.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
dwStyle = WS_OVERLAPPEDWINDOW;
AdjustWindowRectEx(&WindowRect, dwStyle, false, dwExStyle);
if (!(hWnd = CreateWindowEx(
dwExStyle,
"Project2DClass",
title,
dwStyle | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
0,
0,
WindowRect.right-WindowRect.left,
WindowRect.bottom-WindowRect.top,
NULL,
NULL,
hInstance,
NULL))
)
{
KillGLWindow();
MessageBox(NULL, "Window Creation Error.", "ERROR", MB_OK | MB_ICONEXCLAMATION);
return 0;
}
static PIXELFORMATDESCRIPTOR pfd =
{
sizeof(PIXELFORMATDESCRIPTOR),
1,
PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
PFD_TYPE_RGBA,
bits,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
32,
0,
0,
PFD_MAIN_PLANE,
0,
0,
0,
0
};
if (!(hDC = GetDC(hWnd)))
{
KillGLWindow();
MessageBox(NULL, "Can't Create A GL Device Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
if (!(PixelFormat=ChoosePixelFormat(hDC, &pfd)))
{
KillGLWindow();
MessageBox(NULL, "Can't Find A Suitable PixelFormat.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
if(!SetPixelFormat(hDC, PixelFormat, &pfd))
{
KillGLWindow();
MessageBox(NULL,"Can't Set The PixelFormat.","ERROR",MB_OK|MB_ICONEXCLAMATION);
return 0;
}
if (!(hRC = wglCreateContext(hDC)))
{
KillGLWindow();
MessageBox(NULL, "Can't Create A GL Rendering Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
if(!wglMakeCurrent(hDC, hRC))
{
KillGLWindow();
MessageBox(NULL,"Can't Activate The GL Rendering Context.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
SetFocus(hWnd);
ReSizeGLScene(width, height);
if (!InitGL())
{
KillGLWindow();
MessageBox(NULL, "Initialization Failed.", "ERROR", MB_OK|MB_ICONEXCLAMATION);
return 0;
}
return 1;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_ACTIVATE:
{
if (!HIWORD(wParam))
{
active = true;
}
else
{
active = false;
}
return 0;
}
case WM_SYSCOMMAND:
{
switch (wParam)
{
case SC_SCREENSAVE:
case SC_MONITORPOWER:
return 0;
}
break;
}
case WM_CLOSE:
{
PostQuitMessage(0);
return 0;
}
case WM_KEYDOWN:
{
keys[wParam] = true;
return 0;
}
case WM_KEYUP:
{
keys[wParam] = false;
return 0;
}
case WM_SIZE:
{
ReSizeGLScene(LOWORD(lParam), HIWORD(lParam));
return 0;
}
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg;
BOOL running = true;
if (!CreateGLWindow("Project 2D", 800, 600, 32))
{
return 0;
}
while(running)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
running = false;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
if (active)
{
if (keys[VK_ESCAPE])
{
running = false;
}
else
{
DrawGLScene();
SwapBuffers(hDC);
}
}
}
}
KillGLWindow();
return (msg.wParam);
}
And also, how can I get my window to launch in the middle of my screen.
EDIT:
Here is a java equivalent off the LWJGL framework:
/*
* Copyright (c) 2002-2012 LWJGL Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'LWJGL' nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.lwjgl.opengl;
import org.lwjgl.Sys;
/**
* A highly accurate sync method that continually adapts to the system
* it runs on to provide reliable results.
*
* @author Riven
* @author kappaOne
*/
class Sync {
/** number of nano seconds in a second */
private static final long NANOS_IN_SECOND = 1000L * 1000L * 1000L;
/** The time to sleep/yield until the next frame */
private static long nextFrame = 0;
/** whether the initialisation code has run */
private static boolean initialised = false;
/** for calculating the averages the previous sleep/yield times are stored */
private static RunningAvg sleepDurations = new RunningAvg(10);
private static RunningAvg yieldDurations = new RunningAvg(10);
/**
* An accurate sync method that will attempt to run at a constant frame rate.
* It should be called once every frame.
*
* @param fps - the desired frame rate, in frames per second
*/
public static void sync(int fps) {
if (fps <= 0) return;
if (!initialised) initialise();
try {
// sleep until the average sleep time is greater than the time remaining till nextFrame
for (long t0 = getTime(), t1; (nextFrame - t0) > sleepDurations.avg(); t0 = t1) {
Thread.sleep(1);
sleepDurations.add((t1 = getTime()) - t0); // update average sleep time
}
// slowly dampen sleep average if too high to avoid yielding too much
sleepDurations.dampenForLowResTicker();
// yield until the average yield time is greater than the time remaining till nextFrame
for (long t0 = getTime(), t1; (nextFrame - t0) > yieldDurations.avg(); t0 = t1) {
Thread.yield();
yieldDurations.add((t1 = getTime()) - t0); // update average yield time
}
} catch (InterruptedException e) {
}
// schedule next frame, drop frame(s) if already too late for next frame
nextFrame = Math.max(nextFrame + NANOS_IN_SECOND / fps, getTime());
}
/**
* This method will initialise the sync method by setting initial
* values for sleepDurations/yieldDurations and nextFrame.
*
* If running on windows it will start the sleep timer fix.
*/
private static void initialise() {
initialised = true;
sleepDurations.init(1000 * 1000);
yieldDurations.init((int) (-(getTime() - getTime()) * 1.333));
nextFrame = getTime();
String osName = System.getProperty("os.name");
if (osName.startsWith("Win")) {
// On windows the sleep functions can be highly inaccurate by
// over 10ms making in unusable. However it can be forced to
// be a bit more accurate by running a separate sleeping daemon
// thread.
Thread timerAccuracyThread = new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (Exception e) {}
}
});
timerAccuracyThread.setName("LWJGL Timer");
timerAccuracyThread.setDaemon(true);
timerAccuracyThread.start();
}
}
/**
* Get the system time in nano seconds
*
* @return will return the current time in nano's
*/
private static long getTime() {
return (Sys.getTime() * NANOS_IN_SECOND) / Sys.getTimerResolution();
}
private static class RunningAvg {
private final long[] slots;
private int offset;
private static final long DAMPEN_THRESHOLD = 10 * 1000L * 1000L; // 10ms
private static final float DAMPEN_FACTOR = 0.9f; // don't change: 0.9f is exactly right!
public RunningAvg(int slotCount) {
this.slots = new long[slotCount];
this.offset = 0;
}
public void init(long value) {
while (this.offset < this.slots.length) {
this.slots[this.offset++] = value;
}
}
public void add(long value) {
this.slots[this.offset++ % this.slots.length] = value;
this.offset %= this.slots.length;
}
public long avg() {
long sum = 0;
for (int i = 0; i < this.slots.length; i++) {
sum += this.slots[i];
}
return sum / this.slots.length;
}
public void dampenForLowResTicker() {
if (this.avg() > DAMPEN_THRESHOLD) {
for (int i = 0; i < this.slots.length; i++) {
this.slots[i] *= DAMPEN_FACTOR;
}
}
}
}
}
Upvotes: 1
Views: 962
Reputation: 162164
SwapBuffers
with Vertical Retrace Synchronization (V-Sync) enabled. Unless you disabled it in the graphics driver it should be enabled by default. You can also use the Swap Interval extension to fine tune the ratio between SwapBuffers
timing and display vertical retrace.
Also, since Windows does it's CPU time consumption calculation wrongly, add a Sleep(0)
after SwapBuffers
, which fixes the issue of too high indicated CPU load.
Upvotes: 5