Add CRT-Lottes shader via FFmpeg program_opencl

Implements Timothy Lottes' CRT shader (port of mpv-retro-shaders) as an
optional server-side post-processing filter activated per playback session
via streamOptions[crtShader]=true.

Server-side changes:
- Add crt_lottes.cl OpenCL kernel (scanlines, bloom, curvature, shadow mask
  variants 0-4, sRGB linearisation) deployed alongside the server binary
- Add IsCrtShaderEnabled / GetCrtShaderOclFilters / GetCrtShaderFilter
  helpers to EncodingHelper
- SW pipeline: format=rgba → hwupload → program_opencl → hwdownload
- Intel VAAPI + OCL tonemap: inline scale_opencl round-trip (zero PCIe)
- Intel VAAPI, VAAPI encoder, no tonemap: hwmap→opencl→CRT→hwmap (zero PCIe)
- Intel VAAPI, SW encoder, no tonemap: hwmap→opencl→CRT→hwdownload (1× PCIe)
- AMD VAAPI + VK tonemap: hwmap→opencl→CRT→hwmap after scale_vaapi (zero PCIe)
- AMD VAAPI, SW encoder: hwmap→opencl→CRT→hwdownload (1× PCIe)

Shadow mask variant is configurable via streamOptions[crtShadowMask]=0..4.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mani
2026-02-24 14:19:31 +01:00
parent 0bc8b92b6a
commit 6fbc0f87df
3 changed files with 436 additions and 2 deletions

View File

@@ -3708,6 +3708,76 @@ namespace MediaBrowser.Controller.MediaEncoding
};
}
/// <summary>
/// Returns true when the CRT-Lottes shader is requested via streamOptions and
/// the compiled kernel file exists next to the server binary.
/// </summary>
public bool IsCrtShaderEnabled(EncodingJobInfo state)
{
return state.BaseRequest.StreamOptions.TryGetValue("crtShader", out var val)
&& string.Equals(val, "true", StringComparison.OrdinalIgnoreCase)
&& File.Exists(Path.Combine(AppContext.BaseDirectory, "Resources", "Shaders", "crt_lottes.cl"));
}
private string GetCrtEscapedShaderPath()
{
return Path.Combine(AppContext.BaseDirectory, "Resources", "Shaders", "crt_lottes.cl")
.Replace("\\", "/", StringComparison.Ordinal)
.Replace(":", "\\:", StringComparison.Ordinal);
}
private string GetCrtBuildOpts(EncodingJobInfo state)
{
state.BaseRequest.StreamOptions.TryGetValue("crtShadowMask", out var maskVal);
var shadowMask = int.TryParse(maskVal, out var m) && m >= 0 && m <= 4 ? m : 2;
return FormattableString.Invariant($"-DSHADOW_MASK={shadowMask}");
}
/// <summary>
/// Returns three OpenCL-native filters (scale_opencl → program_opencl → scale_opencl)
/// that apply the CRT shader while the frame stays in GPU VRAM.
/// Use this when the pipeline is already in an OpenCL hardware context.
/// Returns an empty list when the shader is disabled.
/// </summary>
public List<string> GetCrtShaderOclFilters(EncodingJobInfo state)
{
if (!IsCrtShaderEnabled(state))
{
return [];
}
var escapedPath = GetCrtEscapedShaderPath();
var buildOpts = GetCrtBuildOpts(state);
return
[
"scale_opencl=w=iw:h=ih:format=rgba",
FormattableString.Invariant(
$"program_opencl=source={escapedPath}:kernel=crt_lottes:build_opts='{buildOpts}'"),
"scale_opencl=w=iw:h=ih:format=nv12"
];
}
/// <summary>
/// Gets the FFmpeg filter chain that applies the CRT-Lottes OpenCL shader
/// for the software (CPU-memory) pipeline path.
/// Includes hwupload/hwdownload around the OpenCL kernel.
/// Returns an empty string when the shader is disabled.
/// </summary>
public string GetCrtShaderFilter(EncodingJobInfo state)
{
if (!IsCrtShaderEnabled(state))
{
return string.Empty;
}
var escapedPath = GetCrtEscapedShaderPath();
var buildOpts = GetCrtBuildOpts(state);
return FormattableString.Invariant(
$"format=rgba,hwupload=derive_device=opencl,program_opencl=source={escapedPath}:kernel=crt_lottes:build_opts='{buildOpts}',hwdownload,format=yuv420p");
}
/// <summary>
/// Gets the parameter of software filter chain.
/// </summary>
@@ -3829,6 +3899,14 @@ namespace MediaBrowser.Controller.MediaEncoding
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
}
// CRT-Lottes OpenCL post-processing (applied after subtitle burn-in,
// only when explicitly requested via streamOptions[crtShader]=true).
var crtFilter = GetCrtShaderFilter(state);
if (!string.IsNullOrEmpty(crtFilter))
{
mainFilters.Add(crtFilter);
}
return (mainFilters, subFilters, overlayFilters);
}
@@ -5084,6 +5162,10 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12", isMjpegEncoder);
mainFilters.Add(tonemapFilter);
// CRT shader inline — frame is already in OpenCL NV12, zero PCIe transfer.
// scale_opencl converts NV12→RGBA for the kernel, then back to NV12.
mainFilters.AddRange(GetCrtShaderOclFilters(state));
}
if (doOclTonemap && isVaInVaOut)
@@ -5093,6 +5175,15 @@ namespace MediaBrowser.Controller.MediaEncoding
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
mainFilters.Add("format=vaapi");
}
else if (!doOclTonemap && isVaInVaOut && IsCrtShaderEnabled(state))
{
// CRT shader for pure-VAAPI path (no OCL tonemap already done).
// VAAPI → OpenCL (GPU-internal via hwmap, no PCIe) → CRT → back to VAAPI.
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
mainFilters.AddRange(GetCrtShaderOclFilters(state));
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
mainFilters.Add("format=vaapi");
}
var memoryOutput = false;
var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
@@ -5103,7 +5194,19 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
// prefer hwmap to hwdownload on opencl/vaapi.
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
if (isVaapiDecoder && isSwEncoder && !doOclTonemap && IsCrtShaderEnabled(state))
{
// Frame still in VAAPI (no prior OCL step). Route through OpenCL for CRT,
// then download to CPU in one step (avoids double PCIe vs. SW-path CRT).
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
mainFilters.AddRange(GetCrtShaderOclFilters(state));
mainFilters.Add("hwdownload");
}
else
{
mainFilters.Add(isHwmapNotUsable ? "hwdownload" : "hwmap=mode=read");
}
mainFilters.Add("format=nv12");
}
@@ -5343,6 +5446,15 @@ namespace MediaBrowser.Controller.MediaEncoding
// clear the surf->meta_offset and output nv12
mainFilters.Add("scale_vaapi=format=nv12");
// CRT shader via VAAPI→OpenCL→VAAPI round-trip (all in GPU VRAM).
if (IsCrtShaderEnabled(state))
{
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
mainFilters.AddRange(GetCrtShaderOclFilters(state));
mainFilters.Add("hwmap=derive_device=vaapi:mode=write:reverse=1");
mainFilters.Add("format=vaapi");
}
// hw deint
if (doDeintH2645)
{
@@ -5356,7 +5468,19 @@ namespace MediaBrowser.Controller.MediaEncoding
// OUTPUT nv12 surface(memory)
if (isSwEncoder && (doVkTonemap || isVaapiDecoder))
{
mainFilters.Add("hwdownload");
if (IsCrtShaderEnabled(state) && !doVkTonemap)
{
// Frame in VAAPI (no Vulkan tonemap done). Route through OpenCL for CRT
// then download to CPU in one step.
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
mainFilters.AddRange(GetCrtShaderOclFilters(state));
mainFilters.Add("hwdownload");
}
else
{
mainFilters.Add("hwdownload");
}
mainFilters.Add("format=nv12");
}