mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(embedded-mpv): Linux frame-copy helper via headless EGL
Port the frame-copy engine's native layer to Linux (PORTING.md items 1-4): - frame_helper_gl.h: platform GlContext abstraction. macOS keeps the CGL path (moved verbatim); Linux acquires an EGL display in order surfaceless-Mesa -> default display -> GBM render node, binds a 3.2 core desktop-GL context surfaceless (1x1 pbuffer fallback), and hands mpv eglGetProcAddress. The helper's own GL calls link against glvnd libOpenGL, so no display server is required. - frame_shm.h: portable frame_shm_now_ns() (CLOCK_MONOTONIC) shared by the helper and the reader addon, replacing the macOS-only clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); producer and consumer stay on the same clock. - embedded_mpv_frame_reader.c: real implementation now also on __linux__ (the code was already POSIX apart from the clock call). - binding.gyp: OS==linux executable branch for iptvnator_mpv_helper linking system libmpv (-lmpv) + EGL/OpenGL/gbm, with rpaths for $ORIGIN/lib and the build-time library dir. The in-process addon still does not link libmpv - the ban only binds in-process, the helper is out of process. - build-embedded-mpv.js: system-dev fallback on Linux (LIBMPV_INCLUDE_DIR or /usr/include) so a distro libmpv-dev install builds without staging a vendored runtime; a pre-set LINUX_NATIVE_LIBRARY_DIR now wins over the vendored lib dir. Verified on Ubuntu 25.04 / i7-1165G7 (Iris Xe): lavfi smoke per PORTING.md (idle->loading->playing snapshots at 4 Hz, aspect-fit generation bump g1 1280x720 -> g2 960x720 for a 4:3 source), reader probe 60 fps at 1080p60 with 0 torn reads, clean quit with no leaked processes or shm. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26271fc076
commit
1a0c2306a9
6 files changed
+378
-62
No files matched your search
@@ -223,6 +223,28 @@ function resolveRuntime() {
|
||||
};
|
||||
}
|
||||
|
||||
if (targetPlatform === 'linux') {
|
||||
// Dev-first Linux flow (frame-copy helper links system libmpv): a
|
||||
// distro libmpv-dev install is a full runtime — no staging needed.
|
||||
// LIBMPV_INCLUDE_DIR / LINUX_NATIVE_LIBRARY_DIR override the system
|
||||
// paths for machines with a local (non-root) libmpv prefix.
|
||||
const systemIncludeDir =
|
||||
process.env.LIBMPV_INCLUDE_DIR || '/usr/include';
|
||||
if (fs.existsSync(path.join(systemIncludeDir, 'mpv', 'client.h'))) {
|
||||
return {
|
||||
origin: 'system-dev',
|
||||
includeDir: systemIncludeDir,
|
||||
libDir: process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib',
|
||||
binDir: undefined,
|
||||
manifest: {
|
||||
warning:
|
||||
'Development-only system libmpv toolchain. Release packaging keeps the external-mpv-process contract.',
|
||||
},
|
||||
windowsImportLib: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (targetPlatform === 'darwin' && homebrewFallbackEnabled) {
|
||||
const homebrewLibMpv = findLibMpv(homebrewLibDir);
|
||||
const homebrewHeader = path.join(homebrewIncludeDir, 'mpv', 'client.h');
|
||||
@@ -454,7 +476,10 @@ function main() {
|
||||
npm_config_update_binary: 'false',
|
||||
LIBMPV_INCLUDE_DIR: runtime.includeDir,
|
||||
...(targetPlatform === 'linux'
|
||||
? { LINUX_NATIVE_LIBRARY_DIR: runtime.libDir }
|
||||
? {
|
||||
LINUX_NATIVE_LIBRARY_DIR:
|
||||
process.env.LINUX_NATIVE_LIBRARY_DIR || runtime.libDir,
|
||||
}
|
||||
: { LIBMPV_LIBRARY_DIR: outputLibDir }),
|
||||
...(runtime.windowsImportLib
|
||||
? {
|
||||
|
||||
@@ -117,6 +117,41 @@
|
||||
"<!(node -e \"const path = require('path'); const dir = process.env.LIBMPV_LIBRARY_DIR || '/opt/homebrew/lib'; process.stdout.write(path.join(dir, 'libmpv.2.dylib'))\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
"OS==\"linux\"",
|
||||
{
|
||||
"type": "executable",
|
||||
"sources": [
|
||||
"helper/mpv_frame_helper.cpp"
|
||||
],
|
||||
"include_dirs": [
|
||||
"<!(node -p \"process.env.LIBMPV_INCLUDE_DIR || '/usr/include'\")",
|
||||
"helper"
|
||||
],
|
||||
"cflags_cc!": [
|
||||
"-fno-exceptions"
|
||||
],
|
||||
"cflags_cc": [
|
||||
"-std=c++17",
|
||||
"-fexceptions"
|
||||
],
|
||||
"cflags": [
|
||||
"-pthread"
|
||||
],
|
||||
"ldflags": [
|
||||
"-pthread",
|
||||
"-Wl,-rpath,'$$ORIGIN/lib'",
|
||||
"-Wl,-rpath,<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")"
|
||||
],
|
||||
"libraries": [
|
||||
"-L<!(node -p \"process.env.LINUX_NATIVE_LIBRARY_DIR || '/usr/lib'\")",
|
||||
"-lmpv",
|
||||
"-lEGL",
|
||||
"-lOpenGL",
|
||||
"-lgbm"
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* 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 picked in order — Mesa's surfaceless platform (no
|
||||
* display server needed), the session's default display, then a GBM render
|
||||
* node — with 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
|
||||
* its entry points via eglGetProcAddress.
|
||||
*
|
||||
* 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 <cstdio>
|
||||
|
||||
#ifndef EGL_PLATFORM_SURFACELESS_MESA
|
||||
#define EGL_PLATFORM_SURFACELESS_MESA 0x31DD
|
||||
#endif
|
||||
#ifndef EGL_PLATFORM_GBM_KHR
|
||||
#define EGL_PLATFORM_GBM_KHR 0x31D7
|
||||
#endif
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
void makeCurrent() { CGLSetCurrentContext(cgl_); }
|
||||
|
||||
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) {
|
||||
return reinterpret_cast<void*>(eglGetProcAddress(name));
|
||||
}
|
||||
|
||||
class GlContext {
|
||||
public:
|
||||
bool create(std::string& errorOut) {
|
||||
if (!acquireDisplay()) {
|
||||
errorOut = "no usable EGL display (surfaceless/default/GBM)";
|
||||
return false;
|
||||
}
|
||||
if (eglBindAPI(EGL_OPENGL_API) != EGL_TRUE) {
|
||||
errorOut = "EGL implementation lacks desktop OpenGL support";
|
||||
return false;
|
||||
}
|
||||
EGLConfig config = nullptr;
|
||||
if (!chooseConfig(EGL_PBUFFER_BIT, &config) &&
|
||||
!chooseConfig(0, &config)) {
|
||||
errorOut = "no usable EGLConfig for desktop OpenGL";
|
||||
return false;
|
||||
}
|
||||
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,
|
||||
};
|
||||
context_ =
|
||||
eglCreateContext(display_, config, EGL_NO_CONTEXT, contextAttrs);
|
||||
if (context_ == EGL_NO_CONTEXT) {
|
||||
errorOut = "failed to create a 3.2 core EGL context";
|
||||
return false;
|
||||
}
|
||||
/* Probe surfaceless binding here (main thread) so the render thread
|
||||
* can just makeCurrent(); fall back to a 1x1 pbuffer. Unbind before
|
||||
* returning — see the threading contract above. */
|
||||
if (eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
context_) != EGL_TRUE) {
|
||||
const EGLint pbufferAttrs[] = {EGL_WIDTH, 1, EGL_HEIGHT, 1,
|
||||
EGL_NONE};
|
||||
surface_ = eglCreatePbufferSurface(display_, config, pbufferAttrs);
|
||||
if (surface_ == EGL_NO_SURFACE ||
|
||||
eglMakeCurrent(display_, surface_, surface_, context_) !=
|
||||
EGL_TRUE) {
|
||||
errorOut =
|
||||
"eglMakeCurrent failed (no surfaceless context or "
|
||||
"pbuffer)";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT);
|
||||
return true;
|
||||
}
|
||||
|
||||
void makeCurrent() {
|
||||
eglMakeCurrent(display_, surface_, surface_, context_);
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (display_ != EGL_NO_DISPLAY) {
|
||||
eglMakeCurrent(display_, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT);
|
||||
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:
|
||||
bool acquireDisplay() {
|
||||
display_ = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA,
|
||||
nullptr, nullptr);
|
||||
if (initDisplay(display_)) return true;
|
||||
|
||||
display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if (initDisplay(display_)) return true;
|
||||
|
||||
display_ = EGL_NO_DISPLAY;
|
||||
if (openGbmDevice()) {
|
||||
display_ = eglGetPlatformDisplay(EGL_PLATFORM_GBM_KHR, gbmDevice_,
|
||||
nullptr);
|
||||
if (initDisplay(display_)) return true;
|
||||
}
|
||||
display_ = EGL_NO_DISPLAY;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool initDisplay(EGLDisplay display) {
|
||||
if (display == EGL_NO_DISPLAY) return false;
|
||||
EGLint major = 0;
|
||||
EGLint minor = 0;
|
||||
return eglInitialize(display, &major, &minor) == EGL_TRUE;
|
||||
}
|
||||
|
||||
bool openGbmDevice() {
|
||||
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;
|
||||
}
|
||||
gbmFd_ = fd;
|
||||
gbmDevice_ = device;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool chooseConfig(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;
|
||||
}
|
||||
|
||||
EGLDisplay display_ = EGL_NO_DISPLAY;
|
||||
EGLContext context_ = EGL_NO_CONTEXT;
|
||||
EGLSurface surface_ = EGL_NO_SURFACE;
|
||||
struct gbm_device* gbmDevice_ = nullptr;
|
||||
int gbmFd_ = -1;
|
||||
};
|
||||
|
||||
#else
|
||||
#error "frame_helper_gl.h has no GL context implementation for this platform"
|
||||
#endif
|
||||
|
||||
} // namespace frame_helper
|
||||
@@ -1,23 +1,19 @@
|
||||
/*
|
||||
* Offscreen render + readback pipeline for the frame-copy helper (macOS).
|
||||
* Offscreen render + readback pipeline for the frame-copy helper.
|
||||
*
|
||||
* Headless CGL context, mpv render API into an FBO sized to the viewport,
|
||||
* async PBO readback ring, BGRA frames published into the FrameShm ring.
|
||||
* Validated in spikes/mpv-frame-copy (4K60 sustained on M1 Pro).
|
||||
* Headless GL context (per-platform, see frame_helper_gl.h), mpv render API
|
||||
* into an FBO sized to the viewport, async PBO readback ring, BGRA frames
|
||||
* published into the FrameShm ring. Validated in spikes/mpv-frame-copy
|
||||
* (4K60 sustained on M1 Pro).
|
||||
*
|
||||
* Threading: everything here runs on the render thread except
|
||||
* requestResize()/stop(), which only touch atomics/mutex-guarded state.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#define GL_SILENCE_DEPRECATION
|
||||
#include <OpenGL/OpenGL.h>
|
||||
#include <OpenGL/gl3.h>
|
||||
|
||||
#include <mpv/client.h>
|
||||
#include <mpv/render_gl.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
@@ -30,12 +26,13 @@
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "frame_helper_gl.h"
|
||||
#include "frame_helper_io.h"
|
||||
#include "frame_shm.h"
|
||||
|
||||
namespace frame_helper {
|
||||
|
||||
inline uint64_t nowNs() { return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); }
|
||||
inline uint64_t nowNs() { return frame_shm_now_ns(); }
|
||||
|
||||
struct ShmRing {
|
||||
FrameShmHeader* header = nullptr;
|
||||
@@ -129,9 +126,8 @@ private:
|
||||
void renderFrame();
|
||||
|
||||
mpv_handle* mpv_ = nullptr;
|
||||
CGLContextObj cgl_ = nullptr;
|
||||
GlContext gl_;
|
||||
mpv_render_context* renderContext_ = nullptr;
|
||||
void* glDylib_ = nullptr;
|
||||
|
||||
std::string shmBaseName_;
|
||||
ShmRing ring_;
|
||||
@@ -155,10 +151,6 @@ private:
|
||||
std::atomic<int> initState_{0};
|
||||
};
|
||||
|
||||
inline void* frameHelperGetProcAddress(void* ctx, const char* name) {
|
||||
return dlsym(ctx, name);
|
||||
}
|
||||
|
||||
inline bool RenderPipeline::start(mpv_handle* mpv,
|
||||
const std::string& shmBaseName, int width,
|
||||
int height, std::string& errorOut) {
|
||||
@@ -166,48 +158,18 @@ inline bool RenderPipeline::start(mpv_handle* mpv,
|
||||
shmBaseName_ = shmBaseName;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
|
||||
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;
|
||||
return gl_.create(errorOut);
|
||||
}
|
||||
|
||||
inline bool RenderPipeline::setupGl(std::string& errorOut) {
|
||||
CGLSetCurrentContext(cgl_);
|
||||
gl_.makeCurrent();
|
||||
|
||||
if (!rebuildTargets(width_, height_)) {
|
||||
errorOut = "framebuffer setup failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
mpv_opengl_init_params glInit = {frameHelperGetProcAddress, glDylib_};
|
||||
mpv_opengl_init_params glInit = {gl_.procLoader(), gl_.procLoaderCtx()};
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_API_TYPE,
|
||||
const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
||||
@@ -399,8 +361,7 @@ inline void RenderPipeline::runLoop() {
|
||||
if (texture_) glDeleteTextures(1, &texture_);
|
||||
if (fbo_) glDeleteFramebuffers(1, &fbo_);
|
||||
if (pbos_[0]) glDeleteBuffers(FRAME_SHM_RING_SLOTS, pbos_);
|
||||
CGLSetCurrentContext(nullptr);
|
||||
if (cgl_) CGLDestroyContext(cgl_);
|
||||
gl_.destroy();
|
||||
ring_.destroy();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <time.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <atomic>
|
||||
@@ -20,6 +21,16 @@ typedef std::atomic<uint64_t> frame_shm_atomic_u64;
|
||||
typedef _Atomic uint64_t frame_shm_atomic_u64;
|
||||
#endif
|
||||
|
||||
/* Monotonic clock for produce_time_ns/heartbeat_ns. Producer (helper) and
|
||||
* consumer (reader addon) MUST use this same clock so age math stays valid.
|
||||
* CLOCK_MONOTONIC is portable across macOS (10.12+) and Linux; the Windows
|
||||
* port will need a QueryPerformanceCounter shim here. */
|
||||
static inline uint64_t frame_shm_now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
|
||||
}
|
||||
|
||||
#define FRAME_SHM_MAGIC 0x564d5046u /* 'FPMV' */
|
||||
#define FRAME_SHM_VERSION 1u
|
||||
#define FRAME_SHM_RING_SLOTS 3u
|
||||
@@ -27,7 +38,7 @@ typedef _Atomic uint64_t frame_shm_atomic_u64;
|
||||
|
||||
typedef struct {
|
||||
frame_shm_atomic_u64 seq; /* 0 while the slot is being (re)written */
|
||||
uint64_t produce_time_ns; /* CLOCK_MONOTONIC_RAW after copy completes */
|
||||
uint64_t produce_time_ns; /* frame_shm_now_ns() after copy completes */
|
||||
} FrameShmSlot;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
* memory cage forbids external ArrayBuffers over foreign memory.
|
||||
*
|
||||
* Plain C N-API (no node-addon-api) so it stays ABI-stable and trivial.
|
||||
* macOS-only for now — other platforms export an empty object; the
|
||||
* TypeScript side gates on platform before requiring it.
|
||||
* macOS and Linux (POSIX shm) — other platforms export an empty object;
|
||||
* the TypeScript side gates on platform before requiring it.
|
||||
*/
|
||||
#define NAPI_VERSION 8
|
||||
#include <node_api.h>
|
||||
|
||||
#ifdef __APPLE__
|
||||
#if defined(__APPLE__) || defined(__linux__)
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdatomic.h>
|
||||
@@ -30,9 +30,8 @@ static FrameShmHeader* g_header = NULL;
|
||||
static uint8_t* g_base = NULL;
|
||||
static size_t g_size = 0;
|
||||
|
||||
static uint64_t now_ns(void) {
|
||||
return clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW);
|
||||
}
|
||||
/* Same clock as the helper's produce_time_ns/heartbeat_ns writers. */
|
||||
static uint64_t now_ns(void) { return frame_shm_now_ns(); }
|
||||
|
||||
static void unmap_current(void) {
|
||||
if (g_base) {
|
||||
@@ -205,11 +204,11 @@ static napi_value Init(napi_env env, napi_value exports) {
|
||||
return exports;
|
||||
}
|
||||
|
||||
#else /* !__APPLE__ */
|
||||
#else /* neither __APPLE__ nor __linux__ */
|
||||
|
||||
static napi_value Init(napi_env env, napi_value exports) {
|
||||
/* Frame-copy is macOS-only for now; loading this module elsewhere
|
||||
* yields an empty exports object and the TS side treats it as
|
||||
/* Frame-copy is not ported to this platform yet; loading this module
|
||||
* here yields an empty exports object and the TS side treats it as
|
||||
* unsupported. */
|
||||
(void)env;
|
||||
return exports;
|
||||
|
||||
Reference in new issue
Block a user