From 5e5a719f27f81a68c3cfb2b05e94f8c072f8f507 Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 3 Jun 2026 10:42:16 +0200 Subject: [PATCH] feat(stream): enable GPU libplacebo in prod image + gate to real GPU MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make libplacebo actually reachable in the shipped agent image, and refuse it where it would be a regression. Dockerfile (so a Vulkan-capable host can use the GPU tonemap path): - install libvulkan1 (the Vulkan loader libplacebo links at runtime; ~150 KB) - add 'graphics' to NVIDIA_DRIVER_CAPABILITIES so the nvidia container runtime mounts the Vulkan ICD (nvidia_icd.json + GLX libs) under --gpus all Both are inert without a working Vulkan GPU — the functional probe gates use. hls.go: gate libplacebo on a real HW encoder (HWAccel != none). A software-only host with mesa would expose lavapipe (CPU Vulkan); the functional probe accepts it but its tonemap is SLOWER than the zscale CPU chain, so libplacebo there is a regression. No HW encoder -> stay on zscale. Verified on the GPU dev box: nvenc session still picks libplacebo (-c:v h264_nvenc -vf ...,libplacebo=...:tonemapping=bt.2390); new unit test locks the software-encoder path onto zscale. --- Dockerfile | 19 ++++++++++++++----- internal/engine/hls.go | 9 ++++++++- internal/engine/tonemap_test.go | 28 +++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7bb1416..f86dbb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,9 +35,15 @@ FROM debian:bookworm-slim # 7z → archive extractor for RAR/7z-packed downloads (p7zip-full also reads # RAR5, so unrar — unavailable as a free Debian package — isn't needed). # tzdata/ca-certificates → TLS + correct local time for schedules/logs. +# libvulkan1 → the Vulkan loader (libvulkan.so.1). ffmpeg's libplacebo filter +# (GPU HDR→SDR tonemap) loads Vulkan dynamically through it; without the +# loader the filter can't reach a GPU even when the NVIDIA driver mounts +# its ICD. ~150 KB. The agent only USES libplacebo after a functional +# probe (FFmpegSupportsLibplacebo) succeeds AND a real HW encoder is +# present, so this is inert on hosts without a working Vulkan GPU. RUN apt-get update && \ apt-get install -y --no-install-recommends \ - ca-certificates tzdata wget xz-utils par2 p7zip-full && \ + ca-certificates tzdata wget xz-utils par2 p7zip-full libvulkan1 && \ rm -rf /var/lib/apt/lists/* # TARGETARCH is set automatically by Docker buildx during cross-builds. @@ -88,11 +94,14 @@ ENV UNARR_DOWNLOAD_DIR=/downloads ENV XDG_DATA_HOME=/data # NVIDIA passthrough defaults. `--gpus all` alone only grants the "utility" + -# "compute" capabilities; nvenc needs "video". Baking these here means a plain -# `docker run --gpus all` (or the compose device reservation) lights up HW -# transcode with zero extra flags. Harmless when no GPU is attached. +# "compute" capabilities; nvenc needs "video", and "graphics" makes the runtime +# mount the NVIDIA Vulkan ICD (nvidia_icd.json + GLX libs) so ffmpeg's libplacebo +# filter (GPU HDR tonemap, paired with libvulkan1 above) can create a Vulkan +# device. Baking these here means a plain `docker run --gpus all` (or the compose +# device reservation) lights up HW transcode + GPU tonemap with zero extra flags. +# Harmless when no GPU is attached. ENV NVIDIA_VISIBLE_DEVICES=all -ENV NVIDIA_DRIVER_CAPABILITIES=video,compute,utility +ENV NVIDIA_DRIVER_CAPABILITIES=video,compute,utility,graphics VOLUME ["/config", "/downloads", "/data"] diff --git a/internal/engine/hls.go b/internal/engine/hls.go index deb54c2..bb5009b 100644 --- a/internal/engine/hls.go +++ b/internal/engine/hls.go @@ -1359,7 +1359,14 @@ func buildHLSFFmpegArgsAt(cfg HLSSessionConfig, probe *StreamProbe, tmpDir strin // CPU chain; else play untonemapped (desaturated, last resort). Skip // libplacebo on VAAPI: its Vulkan surface flow doesn't compose with our // nv12+hwupload path, so VAAPI keeps the zscale-or-none behaviour. - useLibplacebo := probe.HDR != "" && cfg.Transcode.HasLibplacebo && codec != "h264_vaapi" + // + // Gate on a real HW encoder (HWAccel != none): only then is the Vulkan + // device a genuine GPU. A software-only host with mesa would expose lavapipe + // (CPU Vulkan), which the functional probe accepts but whose tonemap is + // SLOWER than the zscale CPU chain — so on those hosts libplacebo would be a + // regression. No HW encoder ⇒ stay on zscale. + useLibplacebo := probe.HDR != "" && cfg.Transcode.HasLibplacebo && + codec != "h264_vaapi" && cfg.Transcode.HWAccel != HWAccelNone tonemap := "" if probe.HDR != "" && cfg.Transcode.TonemapHDR && !useLibplacebo { tonemap = hdrTonemapChain diff --git a/internal/engine/tonemap_test.go b/internal/engine/tonemap_test.go index 2cc0d5b..ce5f3ff 100644 --- a/internal/engine/tonemap_test.go +++ b/internal/engine/tonemap_test.go @@ -68,13 +68,15 @@ func TestTonemap_AppliedInNoDownscaleBranch(t *testing.T) { } func TestTonemap_LibplaceboPreferredOverZscale(t *testing.T) { - // HDR source + an ffmpeg with libplacebo → the single GPU filter replaces - // the whole CPU zscale chain (and the trailing format=/setparams it folds in). + // HDR source + an ffmpeg with libplacebo on a REAL HW encoder (NVENC) → the + // single GPU filter replaces the whole CPU zscale chain (and the trailing + // format=/setparams it folds in). NVENC (not None) because libplacebo is + // gated on a real GPU — a software encoder stays on zscale. cfg := HLSSessionConfig{ SessionID: "test", SourcePath: "/movies/x.mkv", Quality: "720p", - Transcode: TranscodeRuntime{FFmpegPath: "/usr/bin/ffmpeg", HWAccel: HWAccelNone, TonemapHDR: true, HasLibplacebo: true}, + Transcode: TranscodeRuntime{FFmpegPath: "/usr/bin/ffmpeg", HWAccel: HWAccelNVENC, TonemapHDR: true, HasLibplacebo: true}, } probe := &StreamProbe{Width: 3840, Height: 2160, BitDepth: 10, HDR: "HDR10", DurationSec: 100} vf := vfChain(strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/t", 0, 0), " ")) @@ -86,6 +88,26 @@ func TestTonemap_LibplaceboPreferredOverZscale(t *testing.T) { } } +func TestTonemap_LibplaceboSkippedOnSoftwareEncoder(t *testing.T) { + // libplacebo present but no HW encoder (software libx264) → must NOT use + // libplacebo: a software host's only Vulkan would be lavapipe (CPU), slower + // than zscale. Falls back to the zscale chain. + cfg := HLSSessionConfig{ + SessionID: "test", + SourcePath: "/movies/x.mkv", + Quality: "720p", + Transcode: TranscodeRuntime{FFmpegPath: "/usr/bin/ffmpeg", HWAccel: HWAccelNone, TonemapHDR: true, HasLibplacebo: true}, + } + probe := &StreamProbe{Width: 3840, Height: 2160, BitDepth: 10, HDR: "HDR10", DurationSec: 100} + vf := vfChain(strings.Join(buildHLSFFmpegArgsAt(cfg, probe, "/tmp/t", 0, 0), " ")) + if strings.Contains(vf, "libplacebo") { + t.Errorf("software encoder must not use libplacebo (lavapipe trap), got %q", vf) + } + if !strings.Contains(vf, "zscale=t=linear") { + t.Errorf("software encoder with HDR + zscale should fall back to the zscale chain, got %q", vf) + } +} + func TestTonemap_SkippedWhenFFmpegLacksZscale(t *testing.T) { vf := vfChain(hlsArgsFor("HDR10", false, HWAccelNone)) if strings.Contains(vf, "zscale") || strings.Contains(vf, "tonemap") {