Files
iptvnator/spikes/mpv-frame-copy/RESULTS.md
T
4grayandClaude Fable 5 7d75d989e8 feat(embedded-mpv): Linux port of the frame-copy rendering engine (headless EGL) (#1171)
* 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>
2026-07-15 20:42:50 +02:00

7.7 KiB
Raw Blame History

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 --loop restarts 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 → -g2 at 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).