/* * Headless GL context for the frame-copy helper, one implementation per * platform behind the same tiny GlContext surface. * * macOS: accelerated CGL 3.2 core context; GL entry points for mpv come * from the OpenGL framework via dlsym. * * Linux: EGL display candidates are fully probed in order — Mesa's * surfaceless platform (no display server needed), the session's default * display, then a GBM render node. The first hardware renderer wins; an * earlier software renderer is recreated only if no hardware tier works. * Each tier uses a desktop-GL 3.2 core context bound surfaceless (1x1 pbuffer * fallback for drivers without EGL_KHR_surfaceless_context). The helper's * own GL calls resolve at link time through libGL (glvnd); mpv resolves * core symbols from that linked library and falls back to eglGetProcAddress * for extensions. * * Windows: WGL against a hidden 1x1 window (never shown, never pumped — * offscreen FBO rendering only, no SwapBuffers). A legacy context * bootstraps wglCreateContextAttribsARB for a 3.2 core context. opengl32 * only exports GL 1.1, so the post-1.1 entry points the render pipeline * needs are declared here as same-named function pointers and resolved via * wglGetProcAddress; mpv resolves its own through the same loader. * * Threading contract: create() runs on the main thread and must leave the * context unbound; the render thread calls makeCurrent() once and owns the * context until destroy(). */ #pragma once #include #if defined(__APPLE__) #define GL_SILENCE_DEPRECATION #include #include #include #elif defined(__linux__) /* Keep eglplatform.h away from Xlib types: the helper never uses native * window/display handles, so X11 headers are an unnecessary build dep. */ #define EGL_NO_X11 1 #define MESA_EGL_NO_X11_HEADERS 1 #define GL_GLEXT_PROTOTYPES 1 #include #include #include #include #include #include #include #include #include #include #include #ifndef EGL_PLATFORM_SURFACELESS_MESA #define EGL_PLATFORM_SURFACELESS_MESA 0x31DD #endif #ifndef EGL_PLATFORM_GBM_KHR #define EGL_PLATFORM_GBM_KHR 0x31D7 #endif #elif defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #include #include #include /* The Windows SDK ships GL 1.1 headers only; everything newer that the * render pipeline touches is declared below and resolved at runtime. */ #ifndef GL_BGRA #define GL_BGRA 0x80E1 #endif #ifndef GL_UNSIGNED_INT_8_8_8_8_REV #define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367 #endif #ifndef GL_FRAMEBUFFER #define GL_FRAMEBUFFER 0x8D40 #endif #ifndef GL_READ_FRAMEBUFFER #define GL_READ_FRAMEBUFFER 0x8CA8 #endif #ifndef GL_COLOR_ATTACHMENT0 #define GL_COLOR_ATTACHMENT0 0x8CE0 #endif #ifndef GL_FRAMEBUFFER_COMPLETE #define GL_FRAMEBUFFER_COMPLETE 0x8CD5 #endif #ifndef GL_PIXEL_PACK_BUFFER #define GL_PIXEL_PACK_BUFFER 0x88EB #endif #ifndef GL_STREAM_READ #define GL_STREAM_READ 0x88E1 #endif #ifndef GL_MAP_READ_BIT #define GL_MAP_READ_BIT 0x0001 #endif typedef ptrdiff_t GLsizeiptr; typedef ptrdiff_t GLintptr; #define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091 #define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092 #define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126 #define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 #endif namespace frame_helper { /* Matches mpv_opengl_init_params.get_proc_address. */ using GlGetProcAddressFn = void* (*)(void* ctx, const char* name); #if defined(__APPLE__) inline void* glDlsymGetProcAddress(void* ctx, const char* name) { return dlsym(ctx, name); } class GlContext { public: bool create(std::string& errorOut) { glDylib_ = dlopen( "/System/Library/Frameworks/OpenGL.framework/Versions/Current/" "OpenGL", RTLD_LAZY | RTLD_LOCAL); if (!glDylib_) { errorOut = "failed to open the OpenGL framework"; return false; } CGLPixelFormatAttribute attrs[] = { kCGLPFAOpenGLProfile, (CGLPixelFormatAttribute)kCGLOGLPVersion_3_2_Core, kCGLPFAAccelerated, kCGLPFAColorSize, (CGLPixelFormatAttribute)24, kCGLPFAAlphaSize, (CGLPixelFormatAttribute)8, (CGLPixelFormatAttribute)0, }; CGLPixelFormatObj pixelFormat = nullptr; GLint matched = 0; if (CGLChoosePixelFormat(attrs, &pixelFormat, &matched) != kCGLNoError || !pixelFormat) { errorOut = "no accelerated CGL pixel format"; return false; } const CGLError contextError = CGLCreateContext(pixelFormat, nullptr, &cgl_); CGLDestroyPixelFormat(pixelFormat); if (contextError != kCGLNoError || !cgl_) { errorOut = "failed to create a headless CGL context"; return false; } return true; } bool makeCurrent(std::string& errorOut) { if (CGLSetCurrentContext(cgl_) != kCGLNoError) { errorOut = "failed to bind the CGL render context"; return false; } return true; } void destroy() { CGLSetCurrentContext(nullptr); if (cgl_) CGLDestroyContext(cgl_); cgl_ = nullptr; /* glDylib_ stays mapped: mpv may still resolve symbols during its * own teardown, and the process exits right after anyway. */ } GlGetProcAddressFn procLoader() const { return glDlsymGetProcAddress; } void* procLoaderCtx() const { return glDylib_; } const char* renderApiName() const { return "cgl"; } private: CGLContextObj cgl_ = nullptr; void* glDylib_ = nullptr; }; #elif defined(__linux__) inline void* eglWrapGetProcAddress(void* /*ctx*/, const char* name) { if (void* linkedSymbol = dlsym(RTLD_DEFAULT, name)) return linkedSymbol; return reinterpret_cast(eglGetProcAddress(name)); } class GlContext { public: bool create(std::string& errorOut) { const DisplayTier tiers[] = { DisplayTier::SurfacelessMesa, DisplayTier::Default, DisplayTier::Gbm, }; bool hasSoftwareFallback = false; DisplayTier softwareFallback = DisplayTier::SurfacelessMesa; std::string failures; for (DisplayTier tier : tiers) { Candidate candidate; std::string candidateError; if (!tryCandidate(tier, candidate, candidateError)) { if (candidateOwnsResources(candidate)) { errorOut = candidateError + "; failed to safely tear down rejected EGL " "candidate"; return false; } appendFailure(failures, candidateError); continue; } if (!candidate.softwareRenderer) { adoptCandidate(candidate); std::fprintf(stderr, "egl display: %s\n", tierName(tier)); return true; } std::fprintf(stderr, "egl candidate %s uses software renderer %s; " "trying the next tier\n", tierName(tier), candidate.renderer.c_str()); if (!hasSoftwareFallback) { softwareFallback = tier; hasSoftwareFallback = true; } destroyCandidate(candidate); if (candidateOwnsResources(candidate)) { errorOut = std::string(tierName(tier)) + ": failed to safely release software EGL " "candidate"; return false; } } /* Recreate rather than retain the software context while probing the * remaining tiers: EGL implementations may alias platform displays, * and terminating one alias can invalidate the retained context. */ if (hasSoftwareFallback) { Candidate candidate; std::string candidateError; if (tryCandidate(softwareFallback, candidate, candidateError)) { adoptCandidate(candidate); std::fprintf(stderr, "egl display: %s (software fallback: %s)\n", tierName(softwareFallback), renderer_.c_str()); return true; } if (candidateOwnsResources(candidate)) { errorOut = candidateError + "; failed to safely tear down software fallback"; return false; } appendFailure(failures, candidateError); } errorOut = "no usable EGL display (surfaceless/default/GBM)"; if (!failures.empty()) errorOut += ": " + failures; return false; } bool makeCurrent(std::string& errorOut) { /* eglBindAPI is thread-local. create() runs on the main thread, so the * render thread must select desktop GL for both bind and teardown. */ if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) { errorOut = "failed to bind the desktop OpenGL EGL API"; return false; } if (eglMakeCurrent(display_, surface_, surface_, context_) != EGL_TRUE) { errorOut = "failed to bind the EGL render context"; eglReleaseThread(); return false; } return true; } void destroy() { if (display_ != EGL_NO_DISPLAY) { bool released = true; if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) { std::fprintf(stderr, "egl teardown: failed to bind desktop OpenGL\n"); released = false; } else if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) != EGL_TRUE) { std::fprintf(stderr, "egl teardown: failed to release current " "context\n"); released = false; } if (eglReleaseThread() != EGL_TRUE) { std::fprintf(stderr, "egl teardown: eglReleaseThread failed\n"); released = false; } /* Never tear down the display/GBM device under a context that EGL * may still consider current. The process is already exiting, so * leaking on this exceptional path is safer than use-after-free. */ if (!released) return; if (surface_ != EGL_NO_SURFACE) eglDestroySurface(display_, surface_); if (context_ != EGL_NO_CONTEXT) eglDestroyContext(display_, context_); eglTerminate(display_); } surface_ = EGL_NO_SURFACE; context_ = EGL_NO_CONTEXT; display_ = EGL_NO_DISPLAY; if (gbmDevice_) { gbm_device_destroy(gbmDevice_); gbmDevice_ = nullptr; } if (gbmFd_ >= 0) { close(gbmFd_); gbmFd_ = -1; } } GlGetProcAddressFn procLoader() const { return eglWrapGetProcAddress; } void* procLoaderCtx() const { return nullptr; } const char* renderApiName() const { return "egl"; } private: enum class DisplayTier { SurfacelessMesa, Default, Gbm }; struct Candidate { EGLDisplay display = EGL_NO_DISPLAY; EGLContext context = EGL_NO_CONTEXT; EGLSurface surface = EGL_NO_SURFACE; struct gbm_device* gbmDevice = nullptr; int gbmFd = -1; bool initialized = false; bool current = false; bool threadReleased = false; bool softwareRenderer = false; std::string renderer; }; static const char* tierName(DisplayTier tier) { switch (tier) { case DisplayTier::SurfacelessMesa: return "surfaceless-mesa"; case DisplayTier::Default: return "default"; case DisplayTier::Gbm: return "gbm render node"; } return "unknown"; } static void appendFailure(std::string& failures, const std::string& failure) { if (failure.empty()) return; if (!failures.empty()) failures += "; "; failures += failure; } static bool candidateOwnsResources(const Candidate& candidate) { return candidate.display != EGL_NO_DISPLAY || candidate.context != EGL_NO_CONTEXT || candidate.surface != EGL_NO_SURFACE || candidate.gbmDevice || candidate.gbmFd >= 0 || candidate.current; } static bool isSoftwareRenderer(const std::string& renderer) { std::string normalized = renderer; std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char value) { return static_cast(std::tolower(value)); }); for (const char* marker : {"llvmpipe", "softpipe", "swrast", "software rasterizer", "lavapipe"}) { if (normalized.find(marker) != std::string::npos) return true; } return false; } static bool openGbmDevice(Candidate& candidate) { for (int node = 128; node <= 131; node++) { char devicePath[32]; std::snprintf(devicePath, sizeof(devicePath), "/dev/dri/renderD%d", node); const int fd = open(devicePath, O_RDWR | O_CLOEXEC); if (fd < 0) continue; struct gbm_device* device = gbm_create_device(fd); if (!device) { close(fd); continue; } candidate.gbmFd = fd; candidate.gbmDevice = device; return true; } return false; } static bool chooseConfig(EGLDisplay display, EGLint surfaceType, EGLConfig* out) { const EGLint attrs[] = { EGL_SURFACE_TYPE, surfaceType, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE, }; EGLint matched = 0; return eglChooseConfig(display, attrs, out, 1, &matched) == EGL_TRUE && matched > 0; } bool tryCandidate(DisplayTier tier, Candidate& candidate, std::string& errorOut) { const auto fail = [&](const std::string& detail) { errorOut = std::string(tierName(tier)) + ": " + detail; destroyCandidate(candidate); return false; }; switch (tier) { case DisplayTier::SurfacelessMesa: candidate.display = eglGetPlatformDisplay( EGL_PLATFORM_SURFACELESS_MESA, nullptr, nullptr); break; case DisplayTier::Default: candidate.display = eglGetDisplay(EGL_DEFAULT_DISPLAY); break; case DisplayTier::Gbm: if (!openGbmDevice(candidate)) { return fail("no accessible DRM render node"); } candidate.display = eglGetPlatformDisplay( EGL_PLATFORM_GBM_KHR, candidate.gbmDevice, nullptr); break; } if (candidate.display == EGL_NO_DISPLAY) { return fail("eglGetDisplay returned EGL_NO_DISPLAY"); } EGLint major = 0; EGLint minor = 0; if (eglInitialize(candidate.display, &major, &minor) != EGL_TRUE) { return fail("eglInitialize failed"); } candidate.initialized = true; if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) { return fail("desktop OpenGL API unavailable"); } EGLConfig config = nullptr; if (!chooseConfig(candidate.display, EGL_PBUFFER_BIT, &config) && !chooseConfig(candidate.display, 0, &config)) { return fail("no desktop-OpenGL EGLConfig"); } const EGLint contextAttrs[] = { EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 2, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE, }; candidate.context = eglCreateContext(candidate.display, config, EGL_NO_CONTEXT, contextAttrs); if (candidate.context == EGL_NO_CONTEXT) { return fail("failed to create a 3.2 core context"); } if (eglMakeCurrent(candidate.display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate.context) != EGL_TRUE) { const EGLint pbufferAttrs[] = { EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE, }; candidate.surface = eglCreatePbufferSurface( candidate.display, config, pbufferAttrs); if (candidate.surface == EGL_NO_SURFACE || eglMakeCurrent(candidate.display, candidate.surface, candidate.surface, candidate.context) != EGL_TRUE) { return fail( "eglMakeCurrent failed (surfaceless and pbuffer)"); } } candidate.current = true; const GLubyte* renderer = glGetString(GL_RENDERER); if (!renderer) return fail("GL_RENDERER unavailable"); candidate.renderer = reinterpret_cast(renderer); candidate.softwareRenderer = isSoftwareRenderer(candidate.renderer); if (eglMakeCurrent(candidate.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) != EGL_TRUE) { return fail("failed to unbind the probe context"); } candidate.current = false; if (eglReleaseThread() != EGL_TRUE) { return fail("eglReleaseThread failed after probing"); } candidate.threadReleased = true; return true; } static bool releaseCandidateThread(Candidate& candidate) { if (candidate.threadReleased) return true; if (candidate.current) { if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE || eglMakeCurrent(candidate.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) != EGL_TRUE) { return false; } candidate.current = false; } if (eglReleaseThread() != EGL_TRUE) return false; candidate.threadReleased = true; return true; } static void destroyCandidate(Candidate& candidate) { if (!releaseCandidateThread(candidate)) { std::fprintf(stderr, "egl candidate teardown skipped: context release " "failed\n"); return; } if (candidate.display != EGL_NO_DISPLAY) { if (candidate.surface != EGL_NO_SURFACE) eglDestroySurface(candidate.display, candidate.surface); if (candidate.context != EGL_NO_CONTEXT) eglDestroyContext(candidate.display, candidate.context); if (candidate.initialized) eglTerminate(candidate.display); } if (candidate.gbmDevice) gbm_device_destroy(candidate.gbmDevice); if (candidate.gbmFd >= 0) close(candidate.gbmFd); candidate = Candidate{}; } void adoptCandidate(Candidate& candidate) { display_ = candidate.display; context_ = candidate.context; surface_ = candidate.surface; gbmDevice_ = candidate.gbmDevice; gbmFd_ = candidate.gbmFd; renderer_ = candidate.renderer; candidate = Candidate{}; } EGLDisplay display_ = EGL_NO_DISPLAY; EGLContext context_ = EGL_NO_CONTEXT; EGLSurface surface_ = EGL_NO_SURFACE; struct gbm_device* gbmDevice_ = nullptr; int gbmFd_ = -1; std::string renderer_; }; #elif defined(_WIN32) /* Post-1.1 GL entry points used by frame_helper_render.h, with their real * names so the shared render code compiles unchanged. APIENTRY (stdcall) * must match the driver exports. Single-TU helper; inline for safety. */ #define FRAME_HELPER_WGL_ENTRY_POINTS(X) \ X(void, glGenFramebuffers, (GLsizei n, GLuint* out)) \ X(void, glDeleteFramebuffers, (GLsizei n, const GLuint* ids)) \ X(void, glBindFramebuffer, (GLenum target, GLuint fbo)) \ X(void, glFramebufferTexture2D, \ (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, \ GLint level)) \ X(GLenum, glCheckFramebufferStatus, (GLenum target)) \ X(void, glGenBuffers, (GLsizei n, GLuint* out)) \ X(void, glDeleteBuffers, (GLsizei n, const GLuint* ids)) \ X(void, glBindBuffer, (GLenum target, GLuint buffer)) \ X(void, glBufferData, \ (GLenum target, GLsizeiptr size, const void* data, GLenum usage)) \ X(void*, glMapBufferRange, \ (GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access)) \ X(GLboolean, glUnmapBuffer, (GLenum target)) #define FRAME_HELPER_WGL_DECLARE(ret, name, args) \ inline ret(APIENTRY* name) args = nullptr; FRAME_HELPER_WGL_ENTRY_POINTS(FRAME_HELPER_WGL_DECLARE) #undef FRAME_HELPER_WGL_DECLARE typedef HGLRC(WINAPI* FrameHelperWglCreateContextAttribsFn)(HDC, HGLRC, const int*); /* wglGetProcAddress covers post-1.1 symbols but returns sentinel values for * the 1.1 core set, which lives in opengl32.dll instead. */ inline void* wglLoaderGetProcAddress(void* ctx, const char* name) { PROC proc = wglGetProcAddress(name); const intptr_t sentinel = reinterpret_cast(proc); if (proc && sentinel != 1 && sentinel != 2 && sentinel != 3 && sentinel != -1) { return reinterpret_cast(proc); } return reinterpret_cast( GetProcAddress(static_cast(ctx), name)); } class GlContext { public: bool create(std::string& errorOut) { opengl32_ = GetModuleHandleA("opengl32.dll"); if (!opengl32_) { errorOut = "opengl32.dll is not loaded"; return false; } WNDCLASSA windowClass = {}; windowClass.style = CS_OWNDC; windowClass.lpfnWndProc = DefWindowProcA; windowClass.hInstance = GetModuleHandleA(nullptr); windowClass.lpszClassName = "iptvnator_mpv_helper_gl"; /* Re-registration fails harmlessly if a previous context leaked the * class; CreateWindow below is the real gate. */ RegisterClassA(&windowClass); window_ = CreateWindowExA(0, windowClass.lpszClassName, "", WS_POPUP, 0, 0, 1, 1, nullptr, nullptr, windowClass.hInstance, nullptr); if (!window_) { errorOut = "failed to create the hidden GL window"; return false; } dc_ = GetDC(window_); if (!dc_) { errorOut = "failed to get a DC for the hidden GL window"; return false; } PIXELFORMATDESCRIPTOR descriptor = {}; descriptor.nSize = sizeof(descriptor); descriptor.nVersion = 1; descriptor.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW; descriptor.iPixelType = PFD_TYPE_RGBA; descriptor.cColorBits = 32; descriptor.cAlphaBits = 8; descriptor.iLayerType = PFD_MAIN_PLANE; const int pixelFormat = ChoosePixelFormat(dc_, &descriptor); if (pixelFormat == 0 || SetPixelFormat(dc_, pixelFormat, &descriptor) != TRUE) { errorOut = "no usable pixel format for the hidden GL window"; return false; } HGLRC legacy = wglCreateContext(dc_); if (!legacy) { errorOut = "failed to create a legacy WGL context"; return false; } if (wglMakeCurrent(dc_, legacy) != TRUE) { if (wglDeleteContext(legacy) != TRUE) { std::fprintf(stderr, "wgl create: failed to delete unbound legacy " "context\n"); } errorOut = "failed to bind the legacy WGL context"; return false; } const auto createContextAttribs = reinterpret_cast( wglLoaderGetProcAddress(opengl32_, "wglCreateContextAttribsARB")); if (createContextAttribs) { const int attribs[] = { WGL_CONTEXT_MAJOR_VERSION_ARB, 3, WGL_CONTEXT_MINOR_VERSION_ARB, 2, WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB, 0, }; HGLRC core = createContextAttribs(dc_, nullptr, attribs); if (core) { if (wglMakeCurrent(dc_, core) != TRUE) { if (wglDeleteContext(core) != TRUE) { std::fprintf( stderr, "wgl create: failed to delete unbound core " "context\n"); } if (wglGetCurrentContext() != legacy && wglMakeCurrent(dc_, legacy) != TRUE) { errorOut = "failed to restore the legacy WGL context"; return false; } context_ = legacy; } else { context_ = core; if (wglDeleteContext(legacy) != TRUE) { std::fprintf( stderr, "wgl create: failed to delete superseded legacy " "context\n"); } } } else { /* Pre-3.2 driver: keep the legacy (compatibility) context; * the entry-point loading below decides adequacy. */ context_ = legacy; } } else { context_ = legacy; } bool loaded = true; const char* missing = nullptr; #define FRAME_HELPER_WGL_LOAD(ret, name, args) \ name = reinterpret_cast( \ wglLoaderGetProcAddress(opengl32_, #name)); \ if (!name && loaded) { \ loaded = false; \ missing = #name; \ } FRAME_HELPER_WGL_ENTRY_POINTS(FRAME_HELPER_WGL_LOAD) #undef FRAME_HELPER_WGL_LOAD if (!loaded) { errorOut = std::string("missing GL entry point: ") + missing; if (wglMakeCurrent(nullptr, nullptr) != TRUE) { errorOut += "; failed to release the WGL context"; } return false; } /* Unbind before returning — see the threading contract above. */ if (wglMakeCurrent(nullptr, nullptr) != TRUE) { errorOut = "failed to release the WGL context for render-thread handoff"; return false; } return true; } bool makeCurrent(std::string& errorOut) { if (!dc_ || !context_) { errorOut = "WGL render context is unavailable"; return false; } if (wglMakeCurrent(dc_, context_) != TRUE) { errorOut = "failed to bind the WGL render context"; return false; } return true; } void destroy() { if (context_ && wglMakeCurrent(nullptr, nullptr) != TRUE) { std::fprintf(stderr, "wgl teardown: failed to release current context\n"); return; } if (context_ && wglDeleteContext(context_) != TRUE) { std::fprintf(stderr, "wgl teardown: failed to delete context\n"); return; } context_ = nullptr; /* The hidden window/class belong to the main thread (DestroyWindow * is thread-affine and this runs on the render thread); the process * exits right after, so they are left to OS teardown. */ dc_ = nullptr; window_ = nullptr; } GlGetProcAddressFn procLoader() const { return wglLoaderGetProcAddress; } void* procLoaderCtx() const { return opengl32_; } const char* renderApiName() const { return "wgl"; } private: HWND window_ = nullptr; HDC dc_ = nullptr; HGLRC context_ = nullptr; HMODULE opengl32_ = nullptr; }; #else #error "frame_helper_gl.h has no GL context implementation for this platform" #endif } // namespace frame_helper