mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* test(e2e): explain black canvases in the packaged frame-copy smoke When the smoke's canvas stays black, print and attach the session snapshot (with mpv's drop counter), every slot of the helper's shared-memory frame rings read from /dev/shm, and the session's verbose mpv log (log-file). Together they separate these cases: the helper never published a frame, mpv rendered black, or the preload pump did not draw a real frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): stop random black frames on software GL in frame-copy mpv's LUT scalers (the default lanczos/spline family) build their weight texture from an uninitialized talloc_array: mp_compute_lut fills only kernel->size of every stride floats, so each row's padding is heap garbage (reinit_scaler in video/out/gpu/video.c, mpv 0.41 and master). The LUT is an rgba16f texture sampled with linear filtering. When the padding holds NaN, or a value large enough to become Inf as half-float, Mesa's llvmpipe carries it through the zero-weight neighbour texel, the weights become NaN and the frame clamps to pure black. Whether it happens depends on heap contents, so the packaged smoke's paused frame was black in most attempts on the CI runner. Evidence from CI (8 repeats each, no retries): baseline 2/8 passed; LUT-free bilinear scalers 8/8; gpu-dumb-mode 8/8; LP_NUM_THREADS=1 6/8. A synchronous glReadPixels right after mpv_render_context_render() showed the black frame already in the helper's FBO. The readback, the shared-memory ring and the preload pump were correct. On a CPU rasterizer the helper now selects mpv's LUT-free bilinear scalers and disables sigmoid upscaling. Software GL cannot afford the LUT scalers anyway. A session option for the same key still wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): report software scaler results and scope smoke ring diagnostics The software-renderer fallback now reports which scaler options mpv accepted and which it rejected (with mpv's error), instead of always printing success. The smoke's black-canvas diagnostics read only the failed session's rings (<sessionId>-g<N>), not every impv-fc ring in /dev/shm. Addresses Greptile review feedback. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
802 lines
29 KiB
C++
802 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 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 <algorithm>
|
|
#include <cctype>
|
|
#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);
|
|
|
|
/* GL_RENDERER strings of CPU rasterizers (Mesa llvmpipe/softpipe/swrast). */
|
|
inline bool isSoftwareGlRenderer(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;
|
|
}
|
|
|
|
#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<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; }
|
|
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 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 = isSoftwareGlRenderer(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_; }
|
|
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
|