Reputation: 944
I'm trying to build an OpenGL application that is responsive even while the main window is being resized or moved. The most logical solution that I have found is to create a child window and a Message Pump in separate thread which renders the OpenGL. It can resize itself in between frames as necessary. The primary message pump and window frame runs in the main process.
It works great to a point. The window can be moved, menus used and resized without affecting the frame rate of the child window. SwapBuffers() is where it all falls apart.
SwapBuffers() seems to be running in software mode when it is run in this manner. It no longer holds at 60 FPS to match my monitor's VSync, it jumps into the hundreds when the window is around 100x100 and drops to 20 FPS when maximized to 1920x1080. When running in a single thread, everything seems normal.
There were a few issues that I resolved. Like when messages need to pass between parent and child it stalls the entire application. Overriding WM_SETCURSOR and setting WS_EX_NOPARENTNOTIFY resolved those. It still occasionally stutters when I click.
I'm hoping that I'm just not doing something properly and that someone experienced with OpenGL can help me out. Something to do with my initialization or cleanup may be off since this interferes with other OpenGL applications running on my PC even after I close it.
Here is a simplified test case that exhibits the issues that I'm experiencing. It should compile in about any modern Visual Studio.
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <wchar.h>
#pragma comment(lib, "opengl32.lib")
typedef signed int s32;
typedef unsigned int u32;
typedef unsigned long long u64;
typedef float f32;
typedef double f64;
bool run = true;
// Window procedure for the main application window
LRESULT CALLBACK AppWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_DESTROY && (GetWindowLong(hWnd, GWL_EXSTYLE) & WS_EX_APPWINDOW))
PostQuitMessage(0);
return DefWindowProc(hWnd, msg, wParam, lParam);
}
// Window procedure for the OpenGL rendering window
LRESULT CALLBACK RenderWindowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_SETCURSOR)
{
SetCursor(LoadCursor(NULL, IDC_CROSS));
return TRUE;
}
if (msg == WM_SIZE)
glViewport(0, 0, LOWORD(lParam)-2, HIWORD(lParam)-2);
return AppWindowProc(hWnd, msg, wParam, lParam);
}
int WINAPI ThreadMain(HWND parent)
{
HINSTANCE hInstance = GetModuleHandle(0);
// Depending on if this is running as a child or a overlap window, set up the window styles
UINT ClassStyle, Style, ExStyle;
if (parent)
{
ClassStyle = 0;
Style = WS_CHILD;
ExStyle = WS_EX_NOPARENTNOTIFY;
} else {
ClassStyle = 0 | CS_VREDRAW | CS_HREDRAW;
Style = WS_OVERLAPPEDWINDOW;
ExStyle = WS_EX_APPWINDOW;
}
// Create the child window class
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = ClassStyle;
wc.hInstance = hInstance;
wc.lpfnWndProc = RenderWindowProc;
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.hIcon = LoadIcon(0, IDI_APPLICATION);
wc.hCursor = LoadCursor(0, IDC_ARROW);
wc.lpszClassName = L"OGLChild";
ATOM ClassAtom = RegisterClassEx(&wc);
// Create the child window
RECT r = {0, 0, 640, 480};
if (parent)
GetClientRect(parent, &r);
HWND WindowHandle = CreateWindowExW(ExStyle, (LPCTSTR)MAKELONG(ClassAtom, 0), 0, Style,
0, 0, r.right, r.bottom, parent, 0, hInstance, 0);
// Initialize OpenGL render context
PIXELFORMATDESCRIPTOR pfd = {0};
pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
pfd.nVersion = 1;
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
pfd.iPixelType = PFD_TYPE_RGBA;
pfd.cColorBits = 32;
pfd.cDepthBits = 16;
pfd.iLayerType = PFD_MAIN_PLANE;
HDC DeviceContext = GetDC(WindowHandle);
int format = ChoosePixelFormat(DeviceContext, &pfd);
SetPixelFormat(DeviceContext, format, &pfd);
HGLRC RenderContext = wglCreateContext(DeviceContext);
wglMakeCurrent(DeviceContext, RenderContext);
ShowWindow(WindowHandle, SW_SHOW);
GetClientRect(WindowHandle, &r);
glViewport(0, 0, r.right, r.bottom);
// Set up an accurate clock
u64 start, now, last, frequency;
QueryPerformanceFrequency((LARGE_INTEGER*)&frequency);
QueryPerformanceCounter((LARGE_INTEGER*)&now);
start = last = now;
u32 frames = 0; // total frames this second
f64 nextFrameCount = 0; // next FPS update
f32 left = 0; // line position
MSG msg;
while (run)
{
while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
if (msg.message == WM_QUIT || msg.message == WM_DESTROY)
run = false;
}
// Update the clock
QueryPerformanceCounter((LARGE_INTEGER*)&now);
f64 clock = (f64)(now - start) / frequency;
f64 delta = (f64)(now - last) / frequency;
last = now;
// Render a line moving
glOrtho(0, 640, 480, 0, -1, 1);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glColor3f(1.0f, 1.0f, 0.0f);
left += (f32)(delta * 320.0f);
if (left > 640.0f)
left = 0;
glBegin(GL_LINES);
glVertex2f(0.0f, 0.0f);
glVertex2f(left, 480.0f);
glEnd();
SwapBuffers(DeviceContext);
// Resize as necessary
if (parent)
{
RECT pr, cr;
GetClientRect(parent, &pr);
GetClientRect(WindowHandle, &cr);
if (pr.right != cr.right || pr.bottom != cr.bottom)
MoveWindow(WindowHandle, 0, 0, pr.right, pr.bottom, FALSE);
}
// Update FPS counter
frames++;
if (clock > nextFrameCount)
{
WCHAR title[16] = {0};
_snwprintf_s(title, 16, 16, L"FPS: %u", frames);
SetWindowText(parent ? parent : WindowHandle, title);
nextFrameCount = clock + 1;
frames = 0;
}
Sleep(1);
}
// Cleanup OpenGL context
wglDeleteContext(RenderContext);
wglMakeCurrent(0,0);
ReleaseDC(WindowHandle, DeviceContext);
DestroyWindow(WindowHandle);
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
int result = MessageBox(0, L"Would you like to run in threaded child mode?",
L"Threaded OpenGL Demo", MB_YESNOCANCEL | MB_ICONQUESTION);
if (result == IDNO)
return ThreadMain(0);
else if (result != IDYES)
return 0;
// Create the parent window class
WNDCLASSEX wc = {0};
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.hInstance = hInstance;
wc.lpfnWndProc = (WNDPROC)AppWindowProc;
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wc.hIcon = LoadIcon(0, IDI_APPLICATION);
wc.hCursor = LoadCursor(0, IDC_ARROW);
wc.lpszClassName = L"OGLFrame";
ATOM ClassAtom = RegisterClassEx(&wc);
// Create the parent window
HWND WindowHandle = CreateWindowExW(WS_EX_APPWINDOW, (LPCTSTR)MAKELONG(ClassAtom, 0),
0, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
0, 0, 640, 480, 0, 0, hInstance, 0);
ShowWindow(WindowHandle, SW_SHOW);
// Start the child thread
HANDLE thread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&ThreadMain, (LPVOID)WindowHandle, 0, 0);
MSG msg;
while (run)
{
while (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
if (msg.message == WM_QUIT)
run = false;
}
Sleep(100);
}
DestroyWindow(WindowHandle);
// Wait for the child thread to finish
WaitForSingleObject(thread, INFINITE);
ExitProcess(0);
return 0;
}
I've been running this on a NVIDIA GeForce 8800 GTX, but I'd hope that any solution will work equally on any modern card.
I have tried other methods, such as a thread rendering into a window in the main process but during resizing I've gotten artifacting and even a couple blue screens. My application will be cross platform so DirectX isn't an option.
Upvotes: 3
Views: 2586
Reputation: 944
The issue turned out to be that prolonged debugging of OpenGL applications in Visual Studio can cause OpenGL to start failing to create contexts. Since I didn't trap for any errors, I never realized that it was running without hardware acceleration. It required a full reboot to recover and now works fine.
Also, replacing the pump WinMain with this fixes any issues with resizing:
MSG msg;
while (GetMessage(&msg, 0, 0, 0) != 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
run = false;
Upvotes: 2