Files
home-network-k3s-impl/decisions/2026-10-02-plex-quicksync-verification.md
jakman85andClaude Opus 5 f0994c11a0
gate / gate (pull_request) Failing after 1m44s
gate / conformance (pull_request) Has been skipped
docs(decisions): record the PR's measured CI result — gitea-settings has drifted
Read PR #39's own gate log rather than the tick (CLAUDE.md §2b). Every stage
this branch could affect is green: decisions-log, lint, l0, render-golden, l1,
and approval-policy (which evaluated for real on the pull_request event and
passed — no sensitive path touched). The one failure is `gitea-settings`:

    required_approvals: DECLARED 1 but LIVE is 0
    Gitea has DRIFTED from infra/gitea/expected-branch-protection.json

Every other declared setting matched, and one runner is online. So live `main`
currently requires ZERO approvals while the declaration says 1, and because
.gitea/workflows/gate.yml sets REQUIRE_GITEA_SETTINGS=true, that drift blocks
EVERY PR in the repo, not only this one. The check that would notice approvals
are unenforced is the check that is failing.

Not touched here: infra/gitea/** is a sensitive path (non-author approval
required) and outside this branch's documentation-only scope. Re-asserting the
protection or changing the declaration is an operator decision in its own PR;
local branch fix/admin-approval-override (616a497) looks like that work in
flight.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-10-02 01:01:13 -05:00

150 lines
11 KiB
Markdown

---
date: 2026-10-02
branch: fix/plex-quicksync-verification
title: Hardware transcoding works — and `runbooks/plex-quicksync.md`'s verification section was wrong
---
## What changed
**Hardware transcoding is confirmed working on prod.** Measured 2026-10-02 from
`Plex Media Server.log` in the running pod: `vaapi` on `/dev/dri/renderD128`
(`Intel CoffeeLake-S GT2 [UHD Graphics 630]`), `using hardware decode accelerator vaapi`,
`zero-copy support present`, `using zero-copy transcoding`, and
`final decoder: vaapi, final encoder: vaapi`. VAAPI for **both** directions, zero-copy, so frames
never round-trip through the CPU. The device-plugin mechanism from PR #37 works end to end.
**And `runbooks/plex-quicksync.md`'s verification section was wrong, which is the correction this
branch exists to publish.** The page said the Plex dashboard showing `Transcode (hw)` was "the *only*
proof available, and nothing else will ever tell you". The operator had hardware transcoding fully
working while **Dashboard → Activity showed nothing of the sort** — so the page's authoritative check
was silent on a working system, and sent someone looking for a fault that did not exist. Three
inference errors are now corrected on the page, two of them recorded as named false leads so the next
reader does not re-walk them:
1. **The dashboard is not proof.** Replaced as the primary check by Plex's own log — the grep is in
the page, in its **redacting** form, because `Plex Media Server.log` carries API URLs containing
`X-Plex-Token` (CLAUDE.md §6; a credential has been leaked into a transcript here before). The
page also now says explicitly that **`Plex Transcoder Statistics.log` is the wrong file** — it
holds XML `<Variant .../>` records, not ffmpeg output, and the name invites the mistake — and why
`grep -a` is required. The dashboard is **kept but demoted** to a secondary, best-effort signal,
with its absence stated to prove nothing.
2. **An fd snapshot is not evidence.** A `/proc/*/fd` sweep inside the pod found no holder of
`renderD128` while hardware transcoding was active; transcode sessions are short and the scan
landed in a gap. Recorded as a documented false lead, with the reliable process-level check in its
place: the transcoder's **argv** (`-hwaccel`, `-hwaccel_device`, `vaapi`, `renderD128`), found via
`ps -eo pid,user,comm | grep -i transcoder`.
3. **A missing preference key does not mean the setting is off.** The page told readers to treat an
absent `HardwareAcceleratedCodecs` in `Preferences.xml` as "never enabled". With hardware
acceleration demonstrably on and working, that key was **absent from all 27 attributes** while
`HardwareDevicePath` **was** written and the log said `hardware transcoding: enabled`. First-command
#4 now reads both keys and states plainly that `HardwareDevicePath` present is a positive signal,
absence of `HardwareAcceleratedCodecs` means nothing either way, only `="0"` is conclusive, and the
log settles it.
Also on the page:
- **GID `992` is no longer presented as a number to expect.** Under the plugin the injected device
arrives `root:root`, so the image's `cont-init.d` hook resolves GID **`0`** and adds `plex` to the
**root group** — measured `Groups: 0 100 1000`, `id plex` → `groups=1000(plex),0(root),100(users)`.
992 was the hostPath-era host `render` GID. The page now says so, marks the root-group membership as
an inherent property of the plugin's `DeviceSpec` rather than a defect, and repeats that it is
**not** a reason to reintroduce `supplementalGroups` (still a no-op: `s6-setuidgid` rebuilds the
group list from `/etc/group`). The stale `Groups: 100 1000` claim in the escalation section is gone.
- **A new troubleshooting entry for legitimate per-title fallbacks**, marked as a **hypothesis**.
Between two fully hardware sessions, one logged `enabled, but no hardware decode accelerator found`
and empty final decoder/encoder. UHD 630 decodes H.264, HEVC 8/10-bit and VP9 but **not AV1** and
not 10-bit 4:2:2 HEVC, so the likely cause is the source file, not the platform — explicitly
written as a lead to check first, **not** a confirmed diagnosis of that item.
- The status is stated at the top of the page and in a rewritten provenance footer that separates the
2026-10-02 confirmations from the superseded 2026-09-30/2026-10-01 hostPath-era readings.
- `runbooks/README.md`'s index entry for the page was stale in the same two ways (it still described a
`/dev/dri` hostPath and the dashboard-only proof) and is corrected to match.
Unchanged on purpose: the "STOP HERE IF PLEX IS `Pending`" section and the escape hatch (delete the
`hardware:` block from `catalog/plex.yaml`, re-render) keep exactly their existing prominence — they
are correct and they are the 2am material. The "no alert is possible" reasoning is also kept verbatim
in substance: Plex declares no `metricsEndpoint`, renders no ServiceMonitor, and a fallback alert
would be a rule evaluating against nothing.
**Documentation only.** No change to `catalog/`, `schemas/`, `apps/` or `platform/`.
## What I verified
- **Gate, in the pinned toolbox from this worktree** (`DEV_KUBECONFIG=/dev/null`, `ENGINE` shim
bind-mounting the main checkout's `.git`): **rc 3 UNDETERMINED**, diff base
`origin/main @ 948875aad37d`, with exactly the three expected skips and **no failures**. Green:
`decisions-log`, `lint`, `l0`, `render-golden`, `l1`, `approval-policy`. Skipped:
`spec-immutability` (spec repo unavailable → R-TEST-06 not verified), `gitea-settings`
(`SETTINGS_API_TOKEN` unset), `conformance` (`CLUSTER_AVAILABLE!=true` → L2/L3 not run).
Touched requirements: **R-OBS-04**, reported `UNVERIFIED` for lack of a cluster. `approval-policy`
passed as `N/A` locally (`event='none'`, not a `pull_request`) — it evaluates for real on the PR.
**rc 3 is not green**, it is the calibrated worktree result, and it is reported here as rc 3.
- **The gate was run three times, with the same rc 3 / three-skip result on each committed tip** —
including the final one. Worth recording from the first attempt: run *before* committing, with
`HEAD == origin/main`, the touched-requirement set is `NOT DETERMINABLE (on-base)` and the
R-TEST-03 coverage gate cannot be applied at all. Commit before gating from a fresh worktree branch.
- **`runbooks/kustomization.yaml` still carries `- plex-quicksync=plex-quicksync.md`** in the
`configMapGenerator` `files:` list (line 110) — checked explicitly, because without it the page
merges and still 404s. `README=README.md` is likewise present, which is what serves the corrected
index entry.
- **This change touches no `sensitive_paths`.** `infra/gitea/approval-policy.json` lists `ci/**`,
`.gitea/**`, `conformance/**`, `clusters/**`, `bootstrap/**`, `infra/gitea/**`,
`platform/kyverno/**`, `platform-config/kustomization.yaml`, `.sops.yaml`, `**/*.sops.yaml`,
`**/rbac.yaml`, `CLAUDE.md`. `runbooks/**` and `decisions/**` are not among them, and the
`approval-policy` stage passed.
- **No YAML was added or edited**, so `indent-sequences: consistent` (which blocked a sibling branch
today) is not in play here; `lint` passed over the full tracked YAML set regardless.
- **`DECISIONS-LOG.md` is untouched** — this handoff is its own file, per CLAUDE.md §7.
- **The PR's own CI run was read, not assumed** (PR #39, run 208, task 261, head `1677dff5688b`).
**It is RED, and not because of this branch.** Every stage this change could affect is green —
`decisions-log`, `lint`, `l0`, `render-golden`, `l1`, `approval-policy` (`approval-policy` evaluated
for real on the `pull_request` event and passed: no sensitive path touched). The single failure is
**`gitea-settings`**, quoting the measured field:
[fail] required_approvals: DECLARED 1 but LIVE is 0
[fail] Gitea has DRIFTED from infra/gitea/expected-branch-protection.json.
[fail] MERGE GATE: BLOCKED
Every other declared setting matched (`block_admin_merge_override`, `block_on_rejected_reviews`,
`dismiss_stale_approvals`, `enable_status_check`, `enable_push=false`,
`status_check_contexts = ["gate / gate (pull_request)"]`, `runners online: 1`). So **live `main`
currently requires ZERO approvals**, the declaration still says 1, and with
`REQUIRE_GITEA_SETTINGS: "true"` in `.gitea/workflows/gate.yml` that drift **blocks every PR in
the repo**, not just this one. Note the shape of it: the check that would notice approvals are
unenforced is the check that is failing. Local branch `fix/admin-approval-override` (`616a497`,
"fix(ci): let an administrator merge their own green PR") looks like the in-flight work that means
to change the declaration. **I did not touch it** — `infra/gitea/**` is a sensitive path, needs a
non-author approval, and is outside this branch's documentation-only scope. It is an operator
decision: re-assert the protection, or change the declaration deliberately in its own PR.
- The repository-wide grep for `992` to find what else still asserts it: remaining occurrences are in
`catalog/plex.yaml`, `catalog/render.py`, `schemas/renderer-contract.md`,
`platform/intel-gpu-plugin/README.md`, `platform/intel-gpu-plugin/kustomization.yaml`, and three
older `decisions/` entries.
## What I did NOT verify
- **I did not re-measure anything on the live cluster in this session.** Every platform number on the
page — the VAAPI/zero-copy log lines, `RW-OPEN-OK` as uid 1000, the `gpu.intel.com/i915` limits and
requests on the pod spec, `Groups: 0 100 1000`, `id plex`, the 27 `Preferences.xml` attributes, the
empty `/proc/*/fd` sweep — came to me as measurements taken earlier on 2026-10-02 and was
transcribed, not re-run. Per CLAUDE.md §2b, treat the page as reporting those readings rather than
as proof of current state; re-run the primary check before relying on it.
- **The per-title fallback cause is unverified and labelled as such.** The source codec of the title
that logged `no hardware decode accelerator found` was never read. The AV1 / 10-bit-4:2:2-HEVC
explanation is inferred from UHD 630's capability set alone.
- **Whether Plex's dashboard *ever* shows `Transcode (hw)` on this deployment is unknown.** It was
observed silent during working hardware transcoding; whether that is a client/version quirk, a
reporting bug, or something about this setup was not investigated. The page therefore demotes it
rather than explaining it.
- **The commands as written in the page were not executed from the page.** They are the forms that
were run, re-wrapped for `dev.sh`; in particular the `exec -it deploy/plex -- sh` route for the argv
check is noted as possibly lacking a TTY under `dev.sh run` and was not exercised in that shape.
- **Stale `992` copies outside `runbooks/` were left alone**, deliberately — scope was documentation
only, and `catalog/plex.yaml`, `catalog/render.py`, `schemas/renderer-contract.md` and
`platform/intel-gpu-plugin/` all still carry the hostPath-era `Groups: 44 100 992 1000` reading as
if current. `render.py --selftest` asserts around that comment, so correcting it is a real change
with gate consequences, not a doc edit. **That is open work.** Older `decisions/` entries also cite
992; those are historical records and should stay as written.
- **CI on the PR.** The branch is pushed and the PR opened; I did not watch the Gitea run report, so
nothing here should be read as "CI green". The only gate result I can quote is the local rc 3 above.