Skip to content

Instantly share code, notes, and snippets.

@stillwwater
Created October 24, 2024 04:33
Show Gist options
  • Save stillwwater/c0d2679fef8aa9d8f4a7d01819ab1c7c to your computer and use it in GitHub Desktop.
Save stillwwater/c0d2679fef8aa9d8f4a7d01819ab1c7c to your computer and use it in GitHub Desktop.
OpenGL on DXGI swapchain
#define COBJMACROS
#include <windows.h>
#include <d3d11.h>
#include <dxgi1_3.h>
cvar_t vid_dxgi = {"vid_dxgi", 0, CVAR_BOOL | CVAR_ALLOW | CVAR_INIT}; // DXGI layer for OpenGL
cvar_t vid_waitsync = {"vid_waitsync", 0, CVAR_BOOL | CVAR_ALLOW}; // lower latency, lower framerate
cvar_t vid_display = {"vid_display", -1, CVAR_INT | CVAR_ALLOW}; // -1 = primary
cvar_t vid_fullscreen = {"vid_fullscreen", 1, CVAR_BOOL | CVAR_ALLOW};
HANDLE vid_hwnd;
HANDLE vid_hdc;
// D3D state
ID3D11Device *vdx_device;
ID3D11DeviceContext *vdx_dc;
IDXGISwapChain *vdx_swap_chain;
ID3D11Texture2D *vdx_ds_texture;
ID3D11Texture2D *vdx_color_texture;
ID3D11Texture2D *vdx_gl_color_texture; // texture mapped to GL fbo
ID3D11RenderTargetView *vdx_rtv;
ID3D11DepthStencilView *vdx_dsv;
HANDLE *vdx_frame_sync_object;
// GL/DX interop state
HANDLE vdx_gl_device;
HANDLE vdx_gl_dsv; // handler to D3D view
HANDLE vdx_gl_rtv;
GLuint vdx_gl_dsv_name; // handle to GL texture
GLuint vdx_gl_rtv_name;
GLuint vdx_gl_fbo;
#define vdx_disable_if_error(x) \
if (FAILED(hr)) { print("[dxgi] dxgi disabled 0x%08X\n", (int)hr); vid_dxgi.value = 0; return; }
#define vdx_disable_if_zero(x) \
if (!(x)) { print("[dxgi] disabled (failed to create %s)\n", #x); vid_dxgi.value = 0; return; }
// Windows GL extensions
typedef HGLRC wglCreateContextAttribsARB_t(HDC hDC, HGLRC hShareContext, const int *attribList);
typedef BOOL wglChoosePixelFormatARB_t(HDC hdc, const int *piAttribIList,
const FLOAT *pfAttribFList, UINT nMaxFormats, int *piFormats, UINT *nNumFormats);
HMODULE opengl32_dll;
wglCreateContextAttribsARB_t *wglCreateContextAttribsARB;
wglChoosePixelFormatARB_t *wglChoosePixelFormatARB;
// Initializes an OpenGL device context for a given window. Failing to create a
// device context results in a fatal error.
void
vgl_init(void *window)
{
HDC hdc;
HGLRC hglrc;
int pfd_index;
PIXELFORMATDESCRIPTOR pfd, device_pfd;
profile("vgl_init");
HWND hwnd = window;
// in order to load a modern opengl context we first need to load an old
// opengl using a dummy window and use it to query the extension to load a
// modern context.
WNDCLASSA wndclass = {0};
wndclass.lpfnWndProc = DefWindowProcA;
wndclass.hInstance = GetModuleHandleA(NULL);
wndclass.lpszClassName = "MV_WNDWGLLOADER";
RegisterClassA(&wndclass);
HWND glwnd = CreateWindowA("MV_WNDWGLLOADER", "MOONVEIL",
CS_OWNDC, 0, 0, 0, 0, NULL, NULL, wndclass.hInstance, NULL);
if (!glwnd) {
fatal_error("Failed to create dummy GL window (0x%lX)", GetLastError());
}
ZeroMemory(&pfd, sizeof(pfd));
// we're doing this again later, for now all we care about is a PFD that
// supports OpenGL
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.cAlphaBits = 8;
pfd.cDepthBits = 24;
pfd.cStencilBits = 8;
pfd.iLayerType = PFD_MAIN_PLANE;
hdc = GetDC(glwnd);
pfd_index = ChoosePixelFormat(hdc, &pfd);
DescribePixelFormat(hdc, pfd_index, sizeof(device_pfd), &device_pfd);
if (!SetPixelFormat(hdc, pfd_index, &device_pfd)) {
fatal_error("Failed to find a suitable pixel format for the graphics context (0x%lX).", GetLastError());
}
// create opengl device context for the current window
hglrc = wglCreateContext(hdc);
if (!wglMakeCurrent(hdc, hglrc)) {
fatal_error("Failed to create an opengl context (0x%lX).", GetLastError());
}
// load the extensions we need in order to load the real context
wglCreateContextAttribsARB = (wglCreateContextAttribsARB_t *)wglGetProcAddress("wglCreateContextAttribsARB");
wglChoosePixelFormatARB = (wglChoosePixelFormatARB_t *)wglGetProcAddress("wglChoosePixelFormatARB");
if (!wglCreateContextAttribsARB || !wglChoosePixelFormatARB) {
fatal_error("Failed to load opengl extensions");
}
wglMakeCurrent(hdc, 0);
wglDeleteContext(hglrc);
ReleaseDC(glwnd, hdc);
DestroyWindow(glwnd);
opengl32_dll = LoadLibraryA("opengl32.dll");
if (!opengl32_dll) {
fatal_error("Missing opengl32.dll (0x%lX)", GetLastError());
}
// do it for real this time
hdc = GetDC(hwnd);
ZeroMemory(&pfd, sizeof pfd);
int pf_attribs[] = {
WGL_DRAW_TO_WINDOW_ARB, GL_TRUE,
WGL_SUPPORT_OPENGL_ARB, GL_TRUE,
WGL_DOUBLE_BUFFER_ARB, GL_TRUE,
WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
WGL_COLOR_BITS_ARB, 32,
WGL_DEPTH_BITS_ARB, 24,
WGL_STENCIL_BITS_ARB, 8,
WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB, GL_TRUE,
0,
};
// note that on intel drivers passing GL_FALSE to
// WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB also sets the default buffer as
// sRGB. Remove the property entirely for a linear buffer.
int context_attribs[] = {
WGL_CONTEXT_MAJOR_VERSION_ARB, MV_MIN_GL_MAJOR_VERSION,
WGL_CONTEXT_MINOR_VERSION_ARB, MV_MIN_GL_MINOR_VERSION,
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
0,
};
UINT n_formats;
wglChoosePixelFormatARB(hdc, pf_attribs, NULL, 1, &pfd_index, &n_formats);
if (n_formats == 0) {
fatal_error("No suitable pixel format returned by wglChoosePixelFormatARB.");
}
DescribePixelFormat(hdc, pfd_index, sizeof(pfd), &pfd);
if (!SetPixelFormat(hdc, pfd_index, &pfd)) {
fatal_error("Failed to find a suitable pixel format for the graphics context (0x%lX).", GetLastError());
}
hglrc = wglCreateContextAttribsARB(hdc, 0, context_attribs);
if (!wglMakeCurrent(hdc, hglrc)) {
fatal_error("Failed to create an opengl context (0x%lX).", GetLastError());
}
ReleaseDC(hwnd, hdc);
}
void *
vgl_proc(char *name)
{
PROC proc;
// try getting it from the driver
proc = wglGetProcAddress(name);
if (!proc) {
// try getting it from the OS
proc = GetProcAddress(opengl32_dll, name);
}
return (void *)proc; // ok on architectures we care about
}
void
vgl_present(void)
{
wglSwapIntervalEXT(vid_vsync.value);
SwapBuffers(vid_hdc);
}
D3D11_TEXTURE2D_DESC
vdx_texture_desc(DXGI_FORMAT format, UINT bind_flags)
{
D3D11_TEXTURE2D_DESC dsdesc = {0};
dsdesc.Format = format;
dsdesc.Width = vid_width.value;
dsdesc.Height = vid_height.value;
dsdesc.MipLevels = 1;
dsdesc.ArraySize = 1;
dsdesc.SampleDesc.Count = 1;
dsdesc.Usage = D3D11_USAGE_DEFAULT;
dsdesc.BindFlags = bind_flags;
return dsdesc;
}
void
vdx_init(void *window)
{
HRESULT hr;
D3D11_TEXTURE2D_DESC tdesc;
print("vdx_init (DXGI layer for OpenGL)\n");
vid_hwnd = window;
// Create D3D device and swap chain
DXGI_SWAP_CHAIN_DESC sdesc = {0};
sdesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sdesc.SampleDesc.Count = 1;
sdesc.BufferCount = 2; // double buffer
sdesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sdesc.OutputWindow = vid_hwnd;
sdesc.Windowed = true;
sdesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL;
sdesc.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT
| DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING
| DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
int flags = 0;
#ifndef NDEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
hr = D3D11CreateDeviceAndSwapChain(
NULL, // pAdapter
D3D_DRIVER_TYPE_HARDWARE, // DriverType
NULL, // Software
flags,
NULL, // pFeatureLevels
0, // FeatureLevels
D3D11_SDK_VERSION,
&sdesc, // pSwapChainDesc
&vdx_swap_chain,
&vdx_device,
NULL, // pFeatureLevel
&vdx_dc);
vdx_disable_if_error(hr);
// get swap chain sync object
IDXGISwapChain2 *swap_chain2;
hr = IDXGISwapChain_QueryInterface(vdx_swap_chain, &IID_IDXGISwapChain2, (void **)&swap_chain2);
vdx_disable_if_error(hr);
vdx_frame_sync_object = IDXGISwapChain2_GetFrameLatencyWaitableObject(swap_chain2);
// create depth stencil target texture and view
tdesc = vdx_texture_desc(DXGI_FORMAT_D24_UNORM_S8_UINT, D3D11_BIND_DEPTH_STENCIL);
hr = ID3D11Device_CreateTexture2D(vdx_device, &tdesc, NULL, &vdx_ds_texture);
vdx_disable_if_error(hr);
// create color target texture for OpenGL, view is created every frame
tdesc = vdx_texture_desc(DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, D3D11_BIND_RENDER_TARGET);
hr = ID3D11Device_CreateTexture2D(vdx_device, &tdesc, NULL, &vdx_gl_color_texture);
vdx_disable_if_error(hr);
}
void
vdx_lock_frame(void)
{
// wait for an available back buffer
// this reduces the average framerate since it prevents the GPU from
// keeping multiple frames in flight, but significantly improves latency
// (especially with vsync)
WaitForSingleObject(vdx_frame_sync_object, INFINITE);
// get the backbuffer from the FLIP swap chain
IDXGISwapChain_GetBuffer(vdx_swap_chain, 0, &IID_ID3D11Texture2D, (void *)&vdx_color_texture);
// RTV for the current swap chain backbuffer
D3D11_RENDER_TARGET_VIEW_DESC rtvdesc = {DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, D3D11_RTV_DIMENSION_TEXTURE2D};
ID3D11Device_CreateRenderTargetView(vdx_device, (void *)vdx_color_texture, &rtvdesc, &vdx_rtv);
ID3D11DeviceContext_OMSetRenderTargets(vdx_dc, 1, &vdx_rtv, /* vdx_dsv */ NULL);
// lock resources for GL access
wglDXLockObjectsNV(vdx_gl_device, 1, &vdx_gl_rtv);
}
void
vdx_present(void)
{
wglDXUnlockObjectsNV(vdx_gl_device, 1, &vdx_gl_rtv);
// copy shared DX/GL framebuffer to swap chain
//
// ideally the swap chain buffer could be used directly by OpenGL to avoid
// a copy, but in practice calling wglDXRegisterObjectNV every frame with
// the swap chain buffer leads to highly inconsistent frame times
ID3D11DeviceContext_CopyResource(vdx_dc, (void *)vdx_color_texture, (void *)vdx_gl_color_texture);
int flags = 0;
int swap = vid_vsync.value;
if (!vid_vsync.value)
flags |= DXGI_PRESENT_ALLOW_TEARING;
if (vid_vsync.value < 0)
swap = 1;
IDXGISwapChain_Present(vdx_swap_chain, swap, flags);
// release backbuffer back to the swap chain
ID3D11RenderTargetView_Release(vdx_rtv);
}
void
vid_init(void *window)
{
profile("vid_init");
print("vid_init\n");
vid_hdc = GetDC(window);
// init OpenGL
vgl_init(window);
gl_init();
if (vid_dxgi.value) {
// if anything goes wrong while setting up DXGI, disable it and use the
// normal GDI path
vdx_disable_if_zero(wglDXRegisterObjectNV);
vdx_disable_if_zero(wglDXUnregisterObjectNV);
vdx_disable_if_zero(wglDXOpenDeviceNV);
vdx_disable_if_zero(wglDXCloseDeviceNV);
vdx_disable_if_zero(wglDXLockObjectsNV);
vdx_disable_if_zero(wglDXUnlockObjectsNV);
// init D3D state
vdx_init(window);
vdx_gl_device = wglDXOpenDeviceNV(vdx_device);
vdx_disable_if_zero(vdx_gl_device);
// wrap D3D textures in GL objects
glGenTextures(1, &vdx_gl_rtv_name);
vdx_gl_rtv = wglDXRegisterObjectNV(vdx_gl_device, vdx_gl_color_texture, vdx_gl_rtv_name,
GL_TEXTURE_2D, WGL_ACCESS_READ_WRITE_NV);
vdx_disable_if_zero(vdx_gl_rtv);
// lock resources before attaching to the framebuffer
// this is needed so glCheckFramebufferStatus reports the correct value
wglDXLockObjectsNV(vdx_gl_device, 1, &vdx_gl_rtv);
// layer gl framebuffer
glGenFramebuffers(1, &vdx_gl_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, vdx_gl_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, vdx_gl_rtv_name, 0);
vdx_disable_if_zero((!glGetError()));
// double check if the driver thinks we can render to this target
GLenum fbstatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
vdx_disable_if_zero((fbstatus == GL_FRAMEBUFFER_COMPLETE));
// unlock so it can be used by D3D
wglDXUnlockObjectsNV(vdx_gl_device, 1, &vdx_gl_rtv);
}
// delay showing window until everything is setup. Some drivers (intel)
// have problems with fullscreen otherwise.
ShowWindow(window, SW_SHOW);
}
uint32
vid_framebuffer(void)
{
if (vid_dxgi.value)
return vdx_gl_fbo;
return 0;
}
void
vid_lock_frame(void)
{
if (vid_dxgi.value)
vdx_lock_frame();
}
void
vid_present(void)
{
if (vid_dxgi.value)
vdx_present();
else
vgl_present();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment