How to use OpenGL with a device chosen by you

Here is how:

First you need to create a device context (HDC) which represents the device of your choice. This can be done with CreateDC. You can specify the device by string name e.g. “\\.\DISPLAY1”. These names can be found by EnumDisplayDevices.

The next part is a bit hacky.
If you attempt to use this DC as is, it wont work. opengl32.dll uses the user32.dll function WindowFromDC to get the associated window. For DCs get by CreateDC it returns NULL which causes opengl32.dll to return error.
We can do the following: create some dummy window and hook WindowFromDC to return the dummy instead of NULL for our DC.
How to hook WindowFromDC? There are many ways. For example we can plainly get it’s address (in user32) and write a jmp instruction to our code to return the dummy window. This is most likely a read-only location so we would need to change the memory protection mode with VirtualProtect. Be sure to set the protection to copy-on-write and NOT to read-write or otherwise you will crash the entire windows. There are other ways to do the hooking too - for example change the import-adress-table entry for this function in the opengl32.dll module.
However you choose to do it, you must hook this function for opengl32 AND the ICD driver module (nvoglnt.dll for NVIDIA, atioglxx.dll for ATI and iglicd32.dll for intel).

Now you can SetPixelFormat/wglCreateContext with this DC.

Of course, don’t try to render to a window with this context. That certainly wont work and may result in a crash (tho for me it did not).
Instead create a pbuffer or use the framebuffer_object extension to setup a drawable for the opengl where to draw to.

I tested this on a machine equipped with discrete NVIDIA GPU plus integrated intel GPU and windows XP and it works great, including that the so-created context really runs on the specified device and on none else. Haven’t tried it on ATI yet, but most probably will work there too.

Happy hacking!
Lucho.

also tested this on ATI and on windows 7 32-bit exe on 64-bit os with disabled aero.
All tested situations so far work fine.
Later will test 64-bit exe and with enabled aero.

To clarify, the motivation for this is because there is no official way to choose device with opengl on windows.

Possible use would be if you have several opengl-capable devices installed on a machine and you want to use them all for fast offscreen rendering/image processing

How did you verify that your approach actually works? By ‘works’ I mean not only that GL renders something, but that it actually causes the driver to render and upload data to only the chosen GPU.
What about NV_gpu_affinity and AMD_gpu_association?

First, as i mantioned my machine has 2 GPUS:

  • integrated Intel GPU - GMA 3100, whih only supports OpenGL 1.3, but has pbuffers.
  • nvidia GeForce 9400 which supports opengl 3.3.

When i create/makecurrent a DC/GLRC for some of them i check glGetString(GL_RENDERER) and it is really the right one. Then i setup a FBO object for the nvidia gpu or pbuffer for the intel gpu (intel doesn’t support FBO) and used glClearColor/glClear to fill it with something and then used glReadPixels and check that it was really filled with my color, which it was.

I used some “strange” clear color (not black or white) to be unlikely to coincide with the initial state (im not sure if the initial content of the pbuffer/FBO is defined), so i may be sure that the clear actually worked.

Those are vendor specific and can not be used for inter-vendor device choosing as in my case.
You can only use them to pick among several NVIDIAs(NV_gpu_affinity) or ATIs (AMD_gpu_association) you have.

If, for example, you have 2 ATIs and 2 NVIDIAs (very unlikely but still…) then you may only access the NVIDIAs or the ATIs, depending on what the initial DC will happen to be - you are at the windows mercy. I don’t know for you but for me this feels VERY unsatisfactory.

checked windows 7, 64-bit exe, enabled aero - works great.

but i discovered a little limitation of this method - you can freely choose a device, but only once per process. If you attempt to create opengl context for a second (different) device in the same process, the second context will be again for the first device.
It seems that opengl32 loads and initializes an ICD driver only once per-process. Then you are stick with it for the rest of the process lifetime. If you need to use more devices at the same time, you will have to create separate process for each device (i checked this too and it works).

here is some test source (the function patch_window_from_dc is for 64-bit mode)


#include <string.h>
#include <windows.h>
#include <gl/gl.h>
#include "glext.h"
#include "wglext.h"


HWND WINAPI window_from_dc_replacement(HDC dc)
{
	static HWND wnd = NULL;

	if (dc == NULL)
		return NULL;

	if (wnd == NULL) {
		WNDCLASSA wc;
		memset(&wc, 0, sizeof(wc));
		wc.lpfnWndProc = DefWindowProc;
		wc.hInstance = GetModuleHandleA(NULL);
		wc.lpszClassName = "_dummy_window_class_";
		RegisterClassA(&wc);
		wnd = CreateWindowA(wc.lpszClassName, NULL, WS_POPUP, 0, 0, 32, 32, NULL, NULL, wc.hInstance, NULL);
	}

	return wnd;
}

void patch_window_from_dc()
{
	DWORD old_prot;
	unsigned __int64 wfdc = (unsigned __int64)GetProcAddress(GetModuleHandleA("user32.dll"), "WindowFromDC");

	VirtualProtect((void *)wfdc, 14, PAGE_EXECUTE_WRITECOPY, &old_prot);

	// jmp [eip + 0]
	*(char *)(wfdc + 0) = 0xFF;
	*(char *)(wfdc + 1) = 0x25;
	*(unsigned *)(wfdc + 2) = 0x00000000;
	*(unsigned __int64 *)(wfdc + 6) = (unsigned __int64)&window_from_dc_replacement;
}


unsigned char buf[4*256*256];

void test_pbuffer()
{
	PIXELFORMATDESCRIPTOR pfd;
	int pfi;

	PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB;
	PFNWGLCREATEPBUFFERARBPROC wglCreatePbufferARB;
	PFNWGLGETPBUFFERDCARBPROC wglGetPbufferDCARB;
	PFNWGLRELEASEPBUFFERDCARBPROC wglReleasePbufferDCARB;
	PFNWGLDESTROYPBUFFERARBPROC wglDestroyPbufferARB;

	int attribs[] = {
		WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
		WGL_DRAW_TO_PBUFFER_ARB, 1,
		WGL_COLOR_BITS_ARB, 24,
		0 };
	int pbf, n;
	HPBUFFERARB pb;
	HDC pbdc;

	const char *vendor;
	HGLRC glrc;
	HDC dc = CreateDCA("\\\\.\\DISPLAY3", "\\\\.\\DISPLAY3", NULL, NULL);
	memset(&pfd, 0, sizeof(pfd));
	pfd.nSize = sizeof(pfd);
	pfd.nVersion = 1;
	pfd.dwFlags = PFD_SUPPORT_OPENGL|PFD_DRAW_TO_WINDOW|PFD_DOUBLEBUFFER_DONTCARE|PFD_STEREO_DONTCARE|PFD_DEPTH_DONTCARE;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfi = ChoosePixelFormat(dc, &pfd);
	SetPixelFormat(dc, pfi, &pfd);
	glrc = wglCreateContext(dc);
	wglMakeCurrent(dc, glrc);

	vendor = glGetString(GL_VENDOR);
	wglChoosePixelFormatARB = (PFNWGLCHOOSEPIXELFORMATARBPROC)wglGetProcAddress("wglChoosePixelFormatARB");
	wglCreatePbufferARB = (PFNWGLCREATEPBUFFERARBPROC)wglGetProcAddress("wglCreatePbufferARB");
	wglGetPbufferDCARB = (PFNWGLGETPBUFFERDCARBPROC)wglGetProcAddress("wglGetPbufferDCARB");
	wglReleasePbufferDCARB = (PFNWGLRELEASEPBUFFERDCARBPROC)wglGetProcAddress("wglReleasePbufferDCARB");
	wglDestroyPbufferARB = (PFNWGLDESTROYPBUFFERARBPROC)wglGetProcAddress("wglDestroyPbufferARB");

	wglChoosePixelFormatARB(dc, attribs, NULL, 1, &pbf, &n);
	pb = wglCreatePbufferARB(dc, pbf, 256, 256, NULL);
	pbdc = wglGetPbufferDCARB(pb);
	wglMakeCurrent(pbdc, glrc);
	
	glClearColor(0.5f, 0, 1, 0);		
	glClear(GL_COLOR_BUFFER_BIT);
	glScissor(64, 64, 128, 128);
	glEnable(GL_SCISSOR_TEST);		
	glClearColor(0, 1, 0.5f, 0);		
	glClear(GL_COLOR_BUFFER_BIT);
	glReadPixels(0, 0, 256,256, GL_RGBA, GL_UNSIGNED_BYTE, buf);
}


void test_fbo()
{
	PIXELFORMATDESCRIPTOR pfd;
	int pfi;

	void (_stdcall *glGenRenderbuffers)(GLsizei n, GLuint *renderbuffers);
	void (_stdcall *glBindRenderbuffer)(GLenum target, GLuint renderbuffer);
	void (_stdcall *glRenderbufferStorage)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height);
	void (_stdcall *glGenFramebuffers)(GLsizei n, GLuint *framebuffers);
	void (_stdcall *glBindFramebuffer)(GLenum target, GLuint framebuffer);
	void (_stdcall *glFramebufferRenderbuffer)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer);
	GLenum (_stdcall *glCheckFramebufferStatus)(GLenum target);

	const char *vendor;
	GLuint cb, fb;
	GLenum res;

	HGLRC glrc;
	HDC dc = CreateDCA("\\\\.\\DISPLAY1", "\\\\.\\DISPLAY1", NULL, NULL);
	memset(&pfd, 0, sizeof(pfd));
	pfd.nSize = sizeof(pfd);
	pfd.nVersion = 1;
	pfd.dwFlags = PFD_SUPPORT_OPENGL|PFD_DRAW_TO_WINDOW|PFD_DOUBLEBUFFER_DONTCARE|PFD_STEREO_DONTCARE|PFD_DEPTH_DONTCARE;
	pfd.iPixelType = PFD_TYPE_RGBA;
	pfi = ChoosePixelFormat(dc, &pfd);
	SetPixelFormat(dc, pfi, &pfd);
	glrc = wglCreateContext(dc);
	wglMakeCurrent(dc, glrc);

	vendor = glGetString(GL_VENDOR);
	*(PROC *)&glGenRenderbuffers = wglGetProcAddress("glGenRenderbuffers");
	*(PROC *)&glBindRenderbuffer = wglGetProcAddress("glBindRenderbuffer");
	*(PROC *)&glRenderbufferStorage = wglGetProcAddress("glRenderbufferStorage");
	*(PROC *)&glGenFramebuffers = wglGetProcAddress("glGenFramebuffers");
	*(PROC *)&glBindFramebuffer = wglGetProcAddress("glBindFramebuffer");
	*(PROC *)&glFramebufferRenderbuffer = wglGetProcAddress("glFramebufferRenderbuffer");
	*(PROC *)&glCheckFramebufferStatus = wglGetProcAddress("glCheckFramebufferStatus");

	glGenRenderbuffers(1, &cb);
	glBindRenderbuffer(GL_RENDERBUFFER, cb);
	glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, 256, 256);
	
	glGenFramebuffers(1, &fb);
	glBindFramebuffer(GL_FRAMEBUFFER, fb);
	glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cb);
	
	res = glCheckFramebufferStatus(GL_FRAMEBUFFER);
//	if (res != GL_FRAMEBUFFER_COMPLETE) _asm int 3
	
	glClearColor(0.5f, 0, 1, 0);
	glClear(GL_COLOR_BUFFER_BIT);
	glReadPixels(0, 0, 256,256, GL_RGBA, GL_UNSIGNED_BYTE, buf);
}


int WINAPI WinMain(HINSTANCE i, HINSTANCE pi, LPSTR cl, int s)
{
	patch_window_from_dc();

	if (0) {
		DISPLAY_DEVICEA dev;
		int k = 0;
		dev.cb = sizeof(dev);
		while (EnumDisplayDevicesA(NULL, k, &dev, 0))
			k += 1;
	}

	test_fbo();
	//test_pbuffer();

	return 0;
}

The mentioned limitation has a useful consequence - you can do the WindowFromDC trick once to force opengl32 to load particular ICD driver and then you can remove the hacked DC/GLRC and revert WindowFromDC to it’s original state and work normally (with no more hacks) - any subsequently created GL contexts will use the already loaded ICD (and it’s device(s)).

Also, i havent tried this, but i believe the ICD can be changed during the process lifetime if you don’t link statically with opengl32 but use LoadLibrary - then you can reload it (FreeLibrary+LoadLibrary) which should also unload the ICD. Still you can’t use more than one opengl vendor at the same time.

I am also looking for a way to choose the device

I have tried your code on Vista 64, I have two nvidia a Quadro NVS 285 and a Geforce 8800 with one monitor plugged on each.

So the quadro is on DISPLAY1 and the geforce on DISPLAY3.

So whenever I call DISPLAY1 or DISPLAY3 in CreateDCA glGetString(GL_RENDERER) always return the card linked to the display set as primary.

My method actually lets you to force the opengl32.dll to load particular ICD, but all devices by single vendor seem to be serviced by single ICD.
Apparently the NVIDIA ICD doesn’t care which device is represented by the DC you give.
So, it seems, my method only lets you choose between different vendors (if you have more than one on your system).
To choose a device within single vendor, you can use the vendor specific extensions - NV_gpu_affinity and AMD_gpu_association.

In the general case (more than one vendor and/or more than one device per vendor) the following algorithm may work:

  1. decide which device you need
  2. use my method which will cause the ICD for the selected device’s vendor to be loaded.
  3. which is the vendor:
    a) ATI - use AMD_gpu_association to select among the several ATIs
    b) NVIDIA - use NV_gpu_affinity
    c) other (intel) - if there is only one device by this vendor on your system - you already got it from my method, otherwise you are out of luck (unless that vendor’s ICD actually honors your device choice by the the DC you give)

you get the idea :slight_smile:

Interesting…

With main Radeon 6850 and secondary Intel 630 I get an AV exception on ChoosePixelFormat

This topic was automatically closed 183 days after the last reply. New replies are no longer allowed.