Files
iptvnator/spikes/mpv-frame-copy/PORTING.md
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

233 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frame-copy engine — Windows/Linux porting handoff
> Historical handoff and maintenance notes for the completed Linux and
> Windows ports. The canonical runtime contract now lives in
> `docs/architecture/embedded-mpv-native.md`.
## State as of 2026-07-15
- The macOS base shipped through PR #1169 and is now merged into `master`.
- The engine works end-to-end on macOS Apple Silicon: Settings toggle →
restart → helper process renders mpv offscreen → shm ring → preload pump
→ WebGL canvas. Verified live with real IPTV + Stalker VOD.
- Scope decision: macOS = arm64 only (Intel Macs keep the native engine).
- **The Linux port below is DONE in PR #1171** (rebased directly onto the
merged #1169 result) — headless
EGL helper, portable clock, reader on `__linux__`, TS gates, i18n,
measurements in RESULTS.md. Verified end-to-end in-app on Ubuntu 25.04
(Wayland session) with the xtream mock portal. Dev-build-only on Linux:
the helper links system libmpv and `electron-after-pack.cjs` strips it
from packages until milestone 4 (Linux bundled-libmpv runtime) — remove
that strip when milestone 4 lands.
- **The Windows port below landed through PR #1175** — WGL GlContext twin,
QPC clock + `Local\` named-file-mapping
shm twins (the protocol keeps POSIX-style `/impv-*` names; the native
sides derive the mapping name), reader compiled as C++ on `_WIN32`
(MSVC has no C11 `<stdatomic.h>`), helper `.exe` binding.gyp target
linking the vendored import lib + opengl32 (mpv DLL resolved from the
exe's own directory), TS gates + `.exe` helper discovery, i18n,
packaging/CI guards (win32 packages must ship helper + reader; MSVC
intermediates and import libs excluded from dist). **The open iGPU perf
gate is CLOSED**: RESULTS.md rows on the same i7-1165G7/Iris Xe laptop
as the Linux section (dual boot) — 1080p60 sustained (clean 60 s run),
the viewport-price claim reproduces, d3d11va hwdec active (5.5× CPU
drop), torn=0 everywhere. Machine gotchas for future sessions: Windows
11 Smart App Control must be OFF to run locally-built unsigned helpers,
and a fresh Windows install can leave the iGPU on the Basic Display
Adapter — bind the real Intel driver (`pnputil /remove-device` +
`/scan-devices` once the driver is in the store) or WGL has no 3.2 core
context and no d3d11va.
- PR #1169 credits larsemig's idea (#1154 comment 4932807350). Shared-player
controls remain a separate integration after this platform stack; do not
conflate that UI layer with the frame-copy transport ports.
## What "porting" means
Only the helper (and a small reader-addon branch) is platform-specific.
The stdio protocol, shm layout, TS adapter, main-process service, preload
pump, and Angular UI are shared and already shipped.
```
apps/electron-backend/native/helper/ # state after all three ports:
├── mpv_frame_helper.cpp # portable: protocol, mpv session, snapshots
├── frame_helper_io.h # portable: TSV-in/JSON-out, percent-encoding
├── frame_shm.h # portable layout + shared clock (POSIX
│ # CLOCK_MONOTONIC / Windows QPC) + the Windows
│ # Local\ mapping-name derivation
├── frame_helper_render.h # portable render + ShmRing (POSIX shm_open /
│ # Windows CreateFileMapping twins)
└── frame_helper_gl.h # PLATFORM SEAM: GlContext — CGL (macOS),
# EGL (Linux) and WGL (Windows)
apps/electron-backend/native/src/embedded_mpv_frame_reader.c
# real impl on __APPLE__ + __linux__ + _WIN32
# (compiled as C++ there), stub elsewhere
```
All three ports have landed; new platforms follow the same seams: a
GlContext twin in `frame_helper_gl.h`, shm create/open + `frame_shm_now_ns`
twins, the TS gate in `embedded-mpv-frame-copy-platform.util.ts`, packaging.
## Branching & merge strategy
- The platform stack landed in order #1169 → #1171 → #1175.
- Each child was rewritten onto the latest squash-merged `master` using only
its platform-specific commit range; the old parent history was not replayed.
- New frame-copy work branches directly from `master`. Keep future stacks at
most one unmerged level deep and land platform measurements with their port.
## Per-platform task lists
### Linux (DONE 2026-07-11 — see the update in "State" above)
1. **Render backend**: headless EGL (`EGL_PLATFORM_SURFACELESS_MESA` /
`eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA)` with fallback to
default-display and GBM candidates) + the same FBO/PBO/readback code.
Each candidate is validated through context bind and `GL_RENDERER`; a
hardware renderer wins over an earlier software tier. mpv resolves linked
core GL symbols through `dlsym(RTLD_DEFAULT)` and falls back to
`eglGetProcAddress` for extensions.
2. **shm**: POSIX `shm_open` works as-is. The ONLY blocker in shared code:
`clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW)` is **macOS-only** — replace
with a portable `now_ns()` (`clock_gettime(CLOCK_MONOTONIC, ...)`) in
`frame_helper_render.h`, `frame_shm` users, and the reader addon. Keep
producer/consumer on the SAME clock.
3. **Reader addon**: change `#ifdef __APPLE__` to also cover `__linux__`
(code is already POSIX apart from the clock call).
4. **binding.gyp**: helper target gets a linux branch — link system libmpv
for dev first (`-lmpv`); bundled-libmpv runtime is a later packaging
task. NOTE: the LINUX ADDON must still not link libmpv (that rule is for
in-process only — the helper is a separate process, linking is the whole
point and is legal there).
5. **TS gates**: `isFrameCopyEngineActive`/`isFrameCopyAvailable` in
`embedded-mpv-native.service.ts` (drop the darwin/arm64-only condition
for linux), `EmbeddedMpvFrameCopyAdapter.isSupported`, Settings copy +
i18n descriptions currently say "macOS Apple Silicon only".
6. **Big prize**: no window embedding → native Wayland just works; later
bundle libmpv → no system-mpv-on-PATH requirement → Flatpak/Snap. Update
the Linux support matrix in `docs/architecture/embedded-mpv-native.md`
when this lands.
### Windows (DONE 2026-07-12 — see the update in "State" above)
One deviation from the checklist below: the WGL context uses a hidden
regular window, not a message-only one — SetPixelFormat needs a
display-capable DC.
1. **Render backend**: WGL headless — create a hidden message-only window +
dummy pixel format, `wglCreateContextAttribsARB` 3.2 core, then the same
FBO/PBO path. `MPV_RENDER_API_TYPE_SW` is the fallback bring-up if WGL
fights (CPU render, still proves the pipeline).
2. **shm**: `CreateFileMapping(INVALID_HANDLE_VALUE, ...)` +
`MapViewOfFile` behind the same `FrameShmHeader` layout. Name mapping:
`Local\\impv-...` (session-local namespace). Reader addon gets the
`#ifdef _WIN32` twin. Atomics: `std::atomic<uint64_t>` fine with MSVC;
the C reader can use `InterlockedCompareExchange`-free plain
`_Atomic`-equivalent via C11 `<stdatomic.h>` (clang-cl) or volatile+
`MemoryBarrier` — simplest is compiling the reader as C++ on Windows.
3. **Process control**: `child.kill('SIGTERM')` on Windows is
TerminateProcess (no graceful signal) — the quit command + stdin-EOF
paths (already implemented) are the graceful route; keep the kill as the
hard fallback. stdio pipes work unchanged.
4. **binding.gyp**: helper `.exe` target under `OS=="win"` linking the
existing import lib (`LIBMPV_IMPORT_LIB` env — see build-embedded-mpv.js
Windows path). DLL resolution: the helper exe sits next to `lib/` with
the mpv DLL — either copy the DLL beside the exe at build time or call
`SetDllDirectory`/`AddDllDirectory` at startup. Watch the documented
import-library-vs-DLL-basename gotcha (embedded-mpv-native.md).
5. **TS gates + audio**: same switches as Linux. WASAPI audio comes from
mpv directly — nothing to do.
6. **This is the open PERFORMANCE gate**: mid-range iGPU laptop numbers
decide go/no-go (RESULTS.md has the methodology + reference M1 numbers:
4K60 sustained, ~10 ms produce→upload, zero torn frames).
### Both platforms — shared chores
- `validatePackagedEmbeddedMpv` in `tools/packaging/embedded-mpv-packaging.cjs`
currently requires frame-copy artifacts **on darwin only** — extend per
platform when artifacts ship. Keep tests host-agnostic (CI runs them on
a Linux runner; asserting an empty error list for a darwin dir fails
there with "link validation must run on a macOS host" — already fixed
once, don't regress).
- `getMainWindowScaleFactor` (Electron `screen`) is cross-platform — no
work needed; the helper receives device pixels.
- Sandbox story: the flag relaxes the BrowserWindow sandbox for the preload
reader require. Same trade-off applies on Win/Linux. Revisit-before-
default-on candidates are in the architecture doc.
## Hard-won gotchas (do not rediscover these)
1. **Preload + tslib**: repo tsconfig has `target: es2015`; ANY construct
that emits TS helpers in preload code (async/await, object spread in
downlevel positions) with `importHelpers: true` makes webpack
externalize `tslib` → the sandboxed preload dies with
`module not found: tslib` → `window.electron` disappears app-wide.
`apps/electron-backend/tsconfig.app.json` now sets
`importHelpers: false` — NEVER revert it. Symptom to recognize:
"Unable to load preload script" in renderer console.
2. **V8 memory cage**: `napi_create_external_arraybuffer` over shm aborts
in Electron. The reader MUST memcpy into a V8 buffer. Budgeted (~1.2 ms
at 4K).
3. **Frame orientation**: helper renders with `MPV_RENDER_PARAM_FLIP_Y=1`
and `glReadPixels` reads rows bottom-up → the shm buffer is already in
texture order. The pump shader samples with UN-flipped uv. Adding a
second flip shows upside-down video (bug already made and fixed once).
4. **BGRA fast path**: readback as `GL_BGRA`/`GL_UNSIGNED_INT_8_8_8_8_REV`,
upload as RGBA, swizzle `.bgr` in the fragment shader. On Windows check
whether BGRA readback stays the fast path per driver; measure, don't
assume.
5. **Aspect**: mpv reports unset `video-aspect-override` as `"-1.000000"`
→ normalize to `"no"`. The helper aspect-fits the FBO to
`dwidth`/`dheight` inside the viewport (no baked letterbox bars) and
bumps a shm generation (`<base>-g<N>`) on every size change; the pump
re-attaches via the FRAME_SOURCE_CHANGED event.
6. **Stale attach race**: attach/detach bump a shared epoch in the pump;
every await re-checks it. Keep that invariant if touching the pump.
7. **Lifecycle**: dispose escalation is quit-command → stdin.end() (helper
exits on EOF) → SIGTERM(500 ms) → SIGKILL(2 s). The SERVICE also reaps
all sessions on `render-process-gone`/`did-navigate` (renderer crash or
hard reload never runs Angular teardown — without this, helpers leak).
Watch `ps | grep iptvnator_mpv_helper` during any manual test session.
8. **Stale opt-in**: `isFrameCopyEngineActive()` requires the helper binary
on disk; missing helper = silent fallback to native, and the Settings
checkbox stays visible while the saved value is true so it can always
be cleared.
9. **node-gyp naming**: module targets emit `<target_name>.node` (no
`product_name` needed); the helper uses the `"type": "none"` default +
per-OS `"type": "executable"` override trick in binding.gyp.
10. **snapshot protocol**: helper's `snapshot` JSON mirrors
`NativeEmbeddedMpvSessionSnapshot` verbatim (volume 0..1, `null`able
duration/track ids, `videoWidth/videoHeight` when known). Status
semantics are ported from `embedded_mpv.mm` — END_FILE reason mapping,
`eof-reached` ⇒ `ended` (keep-open), pause gated on loadedPath,
only fatal/load errors flip status. Don't invent new mappings.
## Testing recipes
- **Helper standalone** (no Electron):
`(printf 'load\turl=av://lavfi:testsrc2=size=640x360:rate=30\n'; sleep 5; printf 'quit\n') | ./iptvnator_mpv_helper --shm-base /impv-t --width 1280 --height 720`
→ expect `shm` generations, `snapshot` events at 4 Hz, aspect-fit
generation after video loads.
- **Reader probe** (any Node ≥18):
`node -e "const r=require('.../embedded_mpv_frame_reader.node'); const i=r.open('/impv-t-g2'); ..."`
→ `latestSeq()` advancing + pixel min/max spread.
- **In-app**: `IPTVNATOR_ENABLE_EMBEDDED_MPV_FRAME_COPY=1 pnpm run
serve:backend:embedded-mpv` or the Settings toggle (+restart). Second
parallel instance for CDP testing: build, then run
`electron dist/apps/electron-backend/main.js --remote-debugging-port=9223
--user-data-dir=/tmp/x` with `ELECTRON_IS_DEV=0` for the file:// renderer
(dist package.json has no `main` field — point at main.js explicitly;
a separate user-data-dir avoids the Chromium profile singleton).
- **Perf gate**: follow `RESULTS.md` methodology (STATS/LONGRUN lines,
present-interval sd/p99/late counters). Reference: M1 Pro tables therein.
The spike harness in this directory is macOS-only; for Windows/Linux
measure through the real app + helper stderr or port collect-results.sh.
## Suggested milestone order
1. **Completed in #1171:** Linux helper bring-up (EGL + portable clock) →
lavfi smoke → in-app behind flag → measure.
2. Windows helper bring-up (WGL, named shm, reader twin) → same ladder →
**iGPU laptop numbers = the decisive open gate**.
3. Packaging: per-platform artifact validation + runtime staging.
4. Only then: revisit Linux bundled-libmpv + Flatpak/Snap story.