Files
iptvnator/apps/electron-backend/native/helper/frame_helper_gl.h
T
4gray 59e08fd2d6 feat(embedded-mpv): add Windows frame-copy support (#1175)
Port the embedded mpv frame-copy pipeline to Windows with WGL rendering and named shared memory. Includes packaging validation, platform gates, tests, and architecture documentation.
2026-07-15 21:27:56 +02:00

796 lines
29 KiB
C++

/*
* 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 libOpenGL (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 <string>
#if defined(__APPLE__)
#define GL_SILENCE_DEPRECATION
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <dlfcn.h>
#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 <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/gl.h>
#include <GL/glext.h>
#include <fcntl.h>
#include <gbm.h>
#include <unistd.h>
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <dlfcn.h>
#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 <windows.h>
#include <GL/gl.h>
#include <cstdint>
#include <cstdio>
/* 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_; }
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<void*>(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; }
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<char>(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<const char*>(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<intptr_t>(proc);
if (proc && sentinel != 1 && sentinel != 2 && sentinel != 3 &&
sentinel != -1) {
return reinterpret_cast<void*>(proc);
}
return reinterpret_cast<void*>(
GetProcAddress(static_cast<HMODULE>(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<FrameHelperWglCreateContextAttribsFn>(
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<ret(APIENTRY*) args>( \
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_; }
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