* 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> * feat(embedded-mpv): enable the frame-copy engine gates on Linux Flip the TypeScript side of the Linux port (PORTING.md item 5). A shared dependency-free predicate, isFrameCopyPlatformSupported() (linux any-arch, darwin arm64-only), now backs all four gates so they cannot drift: - main.ts: the persisted Settings toggle promotes to the env flag on Linux too (this runs before window creation and controls the sandbox relax). - EmbeddedMpvNativeService.isFrameCopyEngineActive/isFrameCopyAvailable. - EmbeddedMpvFrameCopyAdapter.isSupported. getSupport() ordering: the frame-copy branch moves above the Linux-only native-engine prerequisites - the X11/Xwayland display-server check and the system-mpv-on-PATH probe only bind the --wid native engine, while the frame-copy helper renders offscreen (headless EGL) and links libmpv itself. createSession() also skips resolving the native window handle for frame-copy sessions, which the adapter ignores anyway, so native-Wayland sessions no longer trip the window-handle assertion. Settings copy: the i18n frame-copy description now says macOS (Apple Silicon) and Linux in all 18 languages; stale macOS-only doc comments in the settings/support interfaces updated alongside. Tests: platform-gate matrix for the adapter (darwin arm64/x64, linux x64/arm64, win32) and service specs covering Linux activation under native Wayland, macOS arm64 staying active, macOS x64 staying native, and the skipped window handle for frame-copy sessions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(packaging): CI + package guards for the Linux frame-copy helper - build-and-make.yaml: install libegl-dev/libgl-dev/libgbm-dev on the Linux runner (the helper's EGL backend needs them now that the helper target builds on Linux), and verify the built helper exists and DOES link libmpv - the inverse of the addon's no-libmpv rule, which still holds and stays validated. - electron-after-pack.cjs: strip iptvnator_mpv_helper from packaged Linux apps. It links the build host's system libmpv, which end-user systems cannot be assumed to have; the support probe treats the missing helper as frame-copy-unavailable (dev-build-only engine until the bundled-runtime staging milestone). - frame_helper_gl.h: log the chosen EGL display tier to stderr (the adapter mirrors helper stderr), so bring-up problems on exotic setups are diagnosable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(embedded-mpv): document the Linux frame-copy port - architecture doc: frame-copy section covers Linux (EGL display tiers, build deps, package strip), Linux support matrix notes the frame-copy exception to the X11 + system-mpv requirements, Linux measured baseline. - RESULTS.md: Ubuntu 25.04 / i7-1165G7 (Iris Xe) measurement rows via the production helper + reader probe; viewport-size claim reproduced. - PORTING.md: Linux marked done with pointers to what changed; Windows remains the open port and its perf gate the open decision. - CLAUDE.md + tools/embedded-mpv/README.md: platform scope, Linux dev build requirements, system-headers fallback, helper strip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(embedded-mpv): commit the Linux frame-copy measurement probe linux-frame-probe.mjs reproduces the RESULTS.md Linux rows: spawns the production helper, attaches the frame-reader addon to the announced shm generation, and reports new-frame fps, copy wall time, produce->copy age, torn reads and pixel spread. Usage documented in RESULTS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address multi-agent review findings on the Linux port Confirmed findings (each verified by 3 adversarial reviewers): - CI would fail to link the helper: -lOpenGL needs the unversioned glvnd libOpenGL.so, shipped only by libopengl-dev, which neither the runner images nor the previous apt line provide. Added to the workflow and to every documented Linux build-dep list. - The new 'test -x' dist guard could never pass: webpack's dist asset copy drops file modes (helper arrives as 0644). The guard is now 'test -f'; electron-after-pack.cjs restores the execute bit on packaged helpers (also fixes packaged-macOS spawns); the support probe now requires X_OK, so a mode-stripped helper reads as frame-copy-unavailable and falls back to native instead of failing spawn with EACCES. - The Settings frame-copy toggle was unreachable in exactly the Linux states the port targets: the native-Wayland and missing-system-mpv unsupported payloads omitted frameCopyAvailable, and toggle visibility derives solely from it. Both returns now advertise availability. Also from review: - build-embedded-mpv.js keeps the old graceful-skip contract when the new system-dev fallback finds libmpv-dev but the GL/EGL/gbm dev stack is missing (previously such machines skipped; a hard electron-build failure was a regression). - createSession derives the window-handle skip from the dispatched addon instead of re-evaluating the engine gate, so the two cannot disagree. - The render thread logs the GL renderer string (surfaceless Mesa can silently pick llvmpipe on non-Mesa-primary systems; now diagnosable — verified 'Mesa Intel Iris Xe' on this machine). - Specs pin the new semantics: frameCopyAvailable advertised while native is unsupported (Wayland / no mpv), frame-copy supported without a system mpv, and the handle-skip test disposes its session through the owning adapter. - Docs: PORTING.md file map reflects the frame_helper_gl.h seam for the Windows porter; helper-strip removal correctly gated on milestone 4 (bundled libmpv), not milestone 3; RESULTS.md preamble notes the RAW->MONOTONIC clock change; stale '(macOS)' scope comments updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): address Greptile/Codex review comments - Sandbox gate requires a usable helper (Greptile P1, security): the main.ts env promotion now also probes for an executable iptvnator_mpv_helper before relaxing the window sandbox — a stale opt-in on packaged Linux (helper deliberately stripped) or after a cleaned native build no longer costs a sandboxless launch for an engine that cannot activate. Helper discovery (addon candidate paths + X_OK probe) moved into embedded-mpv-frame-copy-platform.util.ts, shared by main.ts and the service; the service keeps thin instance wrappers so tests can stub per scenario. New util spec pins the platform matrix, candidate resolution, and the execute-bit semantics. - Stale frame-copy artifacts on skipped builds (Codex P2): cleanOutput() now also removes iptvnator_mpv_helper and embedded_mpv_frame_reader.node, so a failed/skipped rebuild cannot leave a previous helper advertising frame-copy support against a runtime the build just declared unavailable. - Multiarch default lib dir (Greptile P1, partially refuted): -l resolution never depended on our -L (the compiler's built-in search paths include the Debian/Ubuntu multiarch dir — proven by the green CI run linking with a nonexistent -L dir), but the system-dev fallback now defaults to /usr/lib/<multiarch-triple> when present so the -L flag and the helper's baked rpath point somewhere real. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(embedded-mpv): harden Linux frame-copy port --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
7.7 KiB
Frame-copy spike — measurement log
One section per machine. Append new runs here; do not overwrite old ones —
this file is the cross-hardware comparison baseline for the go/no-go
decision (gates in README.md and
.plans/2026-07-10-embedded-mpv-frame-copy-unification.md).
How to reproduce a row:
# synthetic (no decode load):
./run.sh 'av://lavfi:testsrc2=size=3840x2160:rate=60' 3840x2160
# real 4K60 HEVC with hw decode (generate once):
ffmpeg -y -f lavfi -i "testsrc2=size=3840x2160:rate=60" -t 12 \
-c:v hevc_videotoolbox -b:v 25M -tag:v hvc1 -pix_fmt yuv420p /tmp/spike-4k-hevc.mp4
HELPER_ARGS='--hwdec videotoolbox --no-audio --loop' ./run.sh /tmp/spike-4k-hevc.mp4 3840x2160
Read STATS lines from viewer stdout after they stabilize (skip the first
window); helper stats are on its stderr. CPU: ps -o %cpu -p <pid> for the
helper and the Electron Helper (Renderer) process during playback.
Column meanings: new fps — frames actually reaching the canvas; copy —
shm→ArrayBuffer memcpy in the addon; upload — texSubImage2D wall time;
age — produce→uploaded latency (helper memcpy done → texture updated,
same monotonic clock on both sides — CLOCK_MONOTONIC_RAW in the original
spike harness used for the M1 rows; the production engine uses
CLOCK_MONOTONIC via frame_shm_now_ns() since the Linux port).
MacBook Pro M1 Pro (arm64), macOS, 120 Hz internal display — 2026-07-10
Source: commit 7e39d2e5, libmpv 2.3.0 (Homebrew mpv 0.39.0), Electron 41.7.2.
| Scenario | Producer fps | New fps | copy ms avg/p95 | upload ms avg/p95 | age ms avg/p95 | torn | CPU helper / renderer |
|---|---|---|---|---|---|---|---|
| 1080p60 testsrc2, sw decode | 60.0 | 59.8 | 0.28 / 0.35 | 0.29 / 0.40 | 4.2 / 6.8 | 0 | — |
| 4K60 testsrc2, sw decode | 60.0 | 60.0 | 1.17 / 1.35 | 3.8 / 4.5 | 11.0 / 12.7 | 0 | — |
| 4K60 HEVC 25 Mbit, hwdec=videotoolbox | 60.0 | 59.9 | 1.2 / 1.6 | 3.3 / 4.1 | 9.8 / 11.7 | 0 | ~18 % / ~24 % |
Helper-side PBO map+copy at 4K: 1.0–1.8 ms avg. Only fps dip observed was at
the --loop file restart (decoder reinit), not in the copy path.
Pacing / judder and HDR (2026-07-10, same machine, viewer with interval instrumentation)
Metrics: present iv sd — stddev of intervals between texture uploads (viewer clock, rAF-quantized); src iv sd — stddev of intervals between produced frames (helper clock); late1.5x — present intervals > 1.5× the producer's median interval (a missed beat).
| Scenario | New fps | present iv sd | src iv sd | late (steady state) | Notes |
|---|---|---|---|---|---|
| 4K60 HEVC hwdec (steady state) | 60.0 | 0.5–1.4 ms | 0.7–1.2 ms | 0–1 per 2 s | worst intervals only at --loop restart |
| 1080p50 testsrc2 (50→120 Hz cadence) | 50.0 | ~4.1 ms | ~1.9 ms | 0; LONGRUN 30 s: 0.13 % (startup only) | sd is 120 Hz rAF grid quantization (16.7/25 ms alternation around 20 ms), not lost frames |
| 1080p25 testsrc2 | 25.0 | ~4.1 ms | ~2.7 ms | 0 | same grid effect |
| 4K25 HDR10 PQ/BT.2020 HEVC hwdec | 25.0 | ~5.5 ms | ~4.3 ms | 0 | mpv tonemaps to SDR before readback (PIXELPROBE spread 255); copy/upload costs unchanged |
Viewport-size scaling check (2026-07-10)
Design claim "render at viewport size, pay viewport price" confirmed: the same 4K60 HEVC clip rendered into a 1280×720 FBO costs helper map+copy 0.17 ms (vs 1.5 ms at 4K), viewer copy 0.16 ms (vs 1.2), upload 0.17 ms (vs 3.5), steady 60 fps. Full 4K price is only paid in 4K-sized viewports (i.e. fullscreen on a 4K display).
10-minute long run — 4K60 HEVC hwdec, --loop (2026-07-10)
Final cumulative line at t=574 s: 33 501 frames, avg 58.32 fps, late1.5x 0.475 %, late2.5x 0.20 %, worst interval 2306 ms, dropped 261, torn 0.
Reading the anomalies before quoting the headline numbers:
- All 261 dropped frames and the single 2.3 s worst-interval stall happened in the first ~60 s (startup/warmup); from t=93 s to the end — zero drops over ~8.5 minutes.
- The steady-state late frames (~102 × late1.5x, ~43 × late2.5x after
warmup ≈ 0.36 % / 0.15 %) track the
--looprestarts of the 12 s test clip (~48 restarts, each a decoder reinit hiccup) — a test-clip artifact, not a pipeline property. A real long-form stream should be cleaner. - torn=0 across the whole run: the seqlock ring never produced a torn read.
Cadence verdict on this hardware: the producer keeps a clean source cadence (mpv's own pacing survives); the only jitter is display-grid quantization in the rAF presenter, bounded by one 120 Hz tick (~8.3 ms). A future refinement could reduce it with display-rate matching, but nothing here blocks the gate.
HDR clip generation for repro:
ffmpeg -y -f lavfi -i "testsrc2=size=3840x2160:rate=25" -t 10 \
-c:v hevc_videotoolbox -profile:v main10 -pix_fmt p010le -b:v 30M -tag:v hvc1 /tmp/spike-4k-hdr.mp4
ffmpeg -y -i /tmp/spike-4k-hdr.mp4 -c:v copy \
-bsf:v "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9" \
/tmp/spike-4k-hdr10.mp4 # videotoolbox omits VUI color tags; inject HDR10 ones
Reference worst-case budget from the analysis doc, for orientation: 33 MB 4K memcpy est. 6–8 ms (measured ≈1.2 ms); end-to-end added latency est. 40–60 ms (measured ≈10 ms produce→uploaded, before compositor).
Intel Mac — SKIPPED by decision (2026-07-10)
Owner decision: the frame-copy engine targets Apple Silicon (M1+) only on macOS; support detection must gate on arm64. Rationale: Intel Macs able to run the app at all (Electron 41 ⇒ macOS 10.15+) are a shrinking 2015–2020 cohort and keep the existing docked/external/web player paths; the only Intel device sourced (iMac mid-2011, High Sierra) cannot run the app and would not have been representative. The macOS hardware gate is therefore closed by the M1 Pro numbers above; the remaining risk hardware is Windows/Linux.
Linux mid-range laptop (iGPU) — Ubuntu 25.04, i7-1165G7 / Iris Xe, x64 — 2026-07-11
Source: Linux port branch (headless-EGL frame_helper_gl.h backend), system
libmpv 2.5.0 (mpv 0.40), Mesa 25.0 iris. Measured with the production helper
binary + embedded_mpv_frame_reader.node in a Node probe loop
(linux-frame-probe.mjs in this directory — the spike viewer harness is
macOS-only), so age here is produce→reader-copy and excludes the renderer
texture upload. hwdec was NOT active — this machine
has no VAAPI driver installed (intel-media-va-driver), so HEVC rows are
software decode; treat them as a decode-limited floor, not a pipeline
ceiling.
| Scenario | New fps | copy ms avg/p95 | age ms avg/p95 | torn |
|---|---|---|---|---|
| 1080p60 testsrc2, sw | 60.1 | 1.16 / 1.37 | 2.26 / 3.21 | 0 |
| 4K60 testsrc2, sw | 50.0 | 5.75 / 6.92 | 7.22 / 8.00 | 0 |
| 4K60 HEVC 25 Mbit, sw decode | 39.9 | 7.62 / 14.6 | 9.06 / 17.0 | 0 |
| 4K60 HEVC 25 Mbit in a 1280×720 viewport | 53.1 | 0.94 / 2.57 | 2.42 / 5.28 | 0 |
Readings:
- 1080p60 — the realistic viewport class for this laptop's 1920×1200 screen — holds a clean 60 fps with ~1 ms copies.
- The 4K rows are stress rows: producers are limited by software decode/source generation on 4 cores, not by the copy path (copy stays well under one 60 Hz frame budget even at full 4K).
- The viewport-size claim reproduces on Linux: the same 4K60 HEVC clip in a 720p viewport drops the copy from 7.6 ms to 0.94 ms and lifts fps from ~40 to ~53 (remaining gap = software decode).
- torn=0 across every run; the aspect-fit generation bump was verified
separately (4:3 source in a 16:9 viewport →
-g2at 960×720). - EGL display tier used: Mesa surfaceless platform (first tier; no display server needed).
Windows mid-range laptop (iGPU) — PENDING
Blocked on the Windows helper port (WGL or D3D11 readback path).