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:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user