diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 42c5517f9..b378dc7c9 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -31,10 +31,13 @@ namespace MediaBrowser.Controller.MediaEncoding private const string VideotoolboxAlias = "vt"; private const string OpenclAlias = "ocl"; private const string CudaAlias = "cu"; + private const string DrmAlias = "dr"; + private const string VulkanAlias = "vk"; private readonly IApplicationPaths _appPaths; private readonly IMediaEncoder _mediaEncoder; private readonly ISubtitleEncoder _subtitleEncoder; private readonly IConfiguration _config; + private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15); private readonly Version _minKernelVersioni915Hang = new Version(5, 18); private static readonly string[] _videoProfilesH264 = new[] @@ -149,6 +152,14 @@ namespace MediaBrowser.Controller.MediaEncoding && _mediaEncoder.SupportsFilter("hwupload_cuda"); } + private bool IsVulkanFullSupported() + { + return _mediaEncoder.SupportsHwaccel("vulkan") + && _mediaEncoder.SupportsFilter("libplacebo") + && _mediaEncoder.SupportsFilter("scale_vulkan") + && _mediaEncoder.SupportsFilterWithOption(FilterOptionType.OverlayVulkanFrameSync); + } + private bool IsHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream == null @@ -176,6 +187,19 @@ namespace MediaBrowser.Controller.MediaEncoding || string.Equals(state.VideoStream.VideoRangeType, "HLG", StringComparison.OrdinalIgnoreCase)); } + private bool IsVulkanHwTonemapAvailable(EncodingJobInfo state, EncodingOptions options) + { + if (state.VideoStream == null) + { + return false; + } + + // libplacebo has partial Dolby Vision to SDR tonemapping support. + return options.EnableTonemapping + && string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase) + && GetVideoColorBitDepth(state) == 10; + } + private bool IsVaapiVppTonemapAvailable(EncodingJobInfo state, EncodingOptions options) { if (state.VideoStream == null @@ -756,8 +780,13 @@ namespace MediaBrowser.Controller.MediaEncoding } else if (_mediaEncoder.IsVaapiDeviceAmd) { - args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); - filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + if (!IsVulkanFullSupported() + || !_mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier + || Environment.OSVersion.Version < _minKernelVersionAmdVkFmtModifier) + { + args.Append(GetOpenclDeviceArgs(0, "Advanced Micro Devices", null, OpenclAlias)); + filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias); + } } else { @@ -2774,22 +2803,41 @@ namespace MediaBrowser.Controller.MediaEncoding return string.Empty; } - var args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709"; + var args = string.Empty; + var algorithm = options.TonemappingAlgorithm; - if (hwTonemapSuffix.Contains("vaapi", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(hwTonemapSuffix, "vaapi", StringComparison.OrdinalIgnoreCase)) { - args += ",procamp_vaapi=b={2}:c={3}:extra_hw_frames=16"; + args = "tonemap_vaapi=format={0}:p=bt709:t=bt709:m=bt709,procamp_vaapi=b={1}:c={2}:extra_hw_frames=16"; return string.Format( CultureInfo.InvariantCulture, args, - hwTonemapSuffix, videoFormat ?? "nv12", options.VppTonemappingBrightness, options.VppTonemappingContrast); } + else if (string.Equals(hwTonemapSuffix, "vulkan", StringComparison.OrdinalIgnoreCase)) + { + args = "libplacebo=format={1}:tonemapping={2}:color_primaries=bt709:color_trc=bt709:colorspace=bt709:peak_detect=0:upscaler=none:downscaler=none"; + + if (!string.Equals(options.TonemappingRange, "auto", StringComparison.OrdinalIgnoreCase)) + { + args += ":range={6}"; + } + + if (string.Equals(options.TonemappingAlgorithm, "bt2390", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "bt.2390"; + } + + else if (string.Equals(options.TonemappingAlgorithm, "none", StringComparison.OrdinalIgnoreCase)) + { + algorithm = "clip"; + } + } else { - args += ":tonemap={2}:peak={3}:desat={4}"; + args = "tonemap_{0}=format={1}:p=bt709:t=bt709:m=bt709:tonemap={2}:peak={3}:desat={4}"; if (options.TonemappingParam != 0) { @@ -2807,7 +2855,7 @@ namespace MediaBrowser.Controller.MediaEncoding args, hwTonemapSuffix, videoFormat ?? "nv12", - options.TonemappingAlgorithm, + algorithm, options.TonemappingPeak, options.TonemappingDesat, options.TonemappingParam, @@ -3770,7 +3818,9 @@ namespace MediaBrowser.Controller.MediaEncoding var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty; var isSwDecoder = string.IsNullOrEmpty(vidDecoder); var isSwEncoder = !vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); - var isVaapiOclSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported() && IsOpenclFullSupported(); + var isVaapiFullSupported = isLinux && IsVaapiSupported(state) && IsVaapiFullSupported(); + var isVaapiOclSupported = isVaapiFullSupported && IsOpenclFullSupported(); + var isVaapiVkSupported = isVaapiFullSupported && IsVulkanFullSupported(); // legacy vaapi pipeline(copy-back) if ((isSwDecoder && isSwEncoder) @@ -3798,14 +3848,24 @@ namespace MediaBrowser.Controller.MediaEncoding if (_mediaEncoder.IsVaapiDeviceInteliHD) { // Intel iHD path, with extra vpp tonemap and overlay support. - return GetVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); + return GetIntelVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - // Intel i965 and Amd radeonsi/r600 path, only featuring scale and deinterlace support. + // prefered vaapi + vulkan filters pipeline + if (_mediaEncoder.IsVaapiDeviceAmd + && isVaapiVkSupported + && _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier + && Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier) + { + // AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support. + return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder); + } + + // Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support. return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder); } - public (List MainFilters, List SubFilters, List OverlayFilters) GetVaapiFullVidFiltersPrefered( + public (List MainFilters, List SubFilters, List OverlayFilters) GetIntelVaapiFullVidFiltersPrefered( EncodingJobInfo state, EncodingOptions options, string vidDecoder, @@ -4003,6 +4063,203 @@ namespace MediaBrowser.Controller.MediaEncoding return (mainFilters, subFilters, overlayFilters); } + public (List MainFilters, List SubFilters, List OverlayFilters) GetAmdVaapiFullVidFiltersPrefered( + EncodingJobInfo state, + EncodingOptions options, + string vidDecoder, + string vidEncoder) + { + var inW = state.VideoStream?.Width; + var inH = state.VideoStream?.Height; + var reqW = state.BaseRequest.Width; + var reqH = state.BaseRequest.Height; + var reqMaxW = state.BaseRequest.MaxWidth; + var reqMaxH = state.BaseRequest.MaxHeight; + var threeDFormat = state.MediaSource.Video3DFormat; + + var isVaapiDecoder = vidDecoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var isVaapiEncoder = vidEncoder.Contains("vaapi", StringComparison.OrdinalIgnoreCase); + var isSwDecoder = string.IsNullOrEmpty(vidDecoder); + var isSwEncoder = !isVaapiEncoder; + var isVaInVaOut = isVaapiDecoder && isVaapiEncoder; + + var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); + var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); + var doVkTonemap = IsVulkanHwTonemapAvailable(state, options); + var doDeintH2645 = doDeintH264 || doDeintHevc; + + var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream; + var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream; + var hasAssSubs = hasSubs + && (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase) + || string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)); + + /* Make main filters for video stream */ + var mainFilters = new List(); + + mainFilters.Add(GetOverwriteColorPropertiesParam(state, doVkTonemap)); + + if (isSwDecoder) + { + // INPUT sw surface(memory) + // sw deint + if (doDeintH2645) + { + var swDeintFilter = GetSwDeinterlaceFilter(state, options); + mainFilters.Add(swDeintFilter); + } + + var outFormat = doVkTonemap ? "yuv420p10le" : "nv12"; + var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH); + // sw scale + mainFilters.Add(swScaleFilter); + mainFilters.Add("format=" + outFormat); + + // keep video at memory except vk tonemap, + // since the overhead caused by hwupload >>> using sw filter. + // sw => hw + if (doVkTonemap) + { + mainFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + } + } + else if (isVaapiDecoder) + { + // INPUT vaapi surface(vram) + // hw deint + if (doDeintH2645) + { + var deintFilter = GetHwDeinterlaceFilter(state, options, "vaapi"); + mainFilters.Add(deintFilter); + } + + var outFormat = doVkTonemap ? string.Empty : (hasSubs && isVaInVaOut ? "bgra" : "nv12"); + var hwScaleFilter = GetHwScaleFilter("vaapi", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH); + + // allocate extra pool sizes for overlay_vulkan + if (!string.IsNullOrEmpty(hwScaleFilter) && isVaInVaOut && hasSubs) + { + hwScaleFilter += ":extra_hw_frames=32"; + } + + // hw scale + mainFilters.Add(hwScaleFilter); + } + + if ((isVaapiDecoder && doVkTonemap) || (isVaInVaOut && (doVkTonemap || hasSubs))) + { + // map from vaapi to vulkan via vaapi-vulkan interop (Vega/gfx9+). + mainFilters.Add("hwmap=derive_device=vulkan"); + } + + // vk tonemap + if (doVkTonemap) + { + var outFormat = isVaInVaOut && hasSubs ? "bgra" : "nv12"; + var tonemapFilter = GetHwTonemapFilter(options, "vulkan", outFormat); + mainFilters.Add(tonemapFilter); + } + + if (doVkTonemap && isVaInVaOut && !hasSubs) + { + // OUTPUT vaapi(nv12/bgra) surface(vram) + // reverse-mapping via vaapi-vulkan interop. + mainFilters.Add("hwmap=derive_device=vaapi:reverse=1"); + mainFilters.Add("format=vaapi"); + } + + var memoryOutput = false; + var isUploadForVkTonemap = isSwDecoder && doVkTonemap; + if ((isVaapiDecoder && isSwEncoder) || isUploadForVkTonemap) + { + memoryOutput = true; + + // OUTPUT nv12 surface(memory) + mainFilters.Add("hwdownload"); + mainFilters.Add("format=nv12"); + } + + // OUTPUT nv12 surface(memory) + if (isSwDecoder && isVaapiEncoder) + { + memoryOutput = true; + } + + if (memoryOutput) + { + // text subtitles + if (hasTextSubs) + { + var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false); + mainFilters.Add(textSubtitlesFilter); + } + } + + if (memoryOutput && isVaapiEncoder) + { + if (!hasGraphicalSubs) + { + mainFilters.Add("hwupload_vaapi"); + } + } + + /* Make sub and overlay filters for subtitle stream */ + var subFilters = new List(); + var overlayFilters = new List(); + if (isVaInVaOut) + { + if (hasSubs) + { + if (hasGraphicalSubs) + { + // scale=s=1280x720,format=bgra,hwupload + var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + subFilters.Add(subSwScaleFilter); + subFilters.Add("format=bgra"); + } + else if (hasTextSubs) + { + var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5); + var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true); + subFilters.Add(alphaSrcFilter); + subFilters.Add("format=bgra"); + subFilters.Add(subTextSubtitlesFilter); + } + + subFilters.Add("hwupload=derive_device=vulkan:extra_hw_frames=16"); + + overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + + // explicitly sync using libplacebo. + overlayFilters.Add("libplacebo=format=nv12:upscaler=none:downscaler=none"); + + // OUTPUT vaapi(nv12/bgra) surface(vram) + // reverse-mapping via vaapi-vulkan interop. + overlayFilters.Add("hwmap=derive_device=vaapi:reverse=1"); + overlayFilters.Add("format=vaapi"); + } + } + else if (memoryOutput) + { + if (hasGraphicalSubs) + { + var subSwScaleFilter = isSwDecoder + ? GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH) + : GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); + subFilters.Add(subSwScaleFilter); + overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + + if (isVaapiEncoder) + { + overlayFilters.Add("hwupload_vaapi"); + } + } + } + + return (mainFilters, subFilters, overlayFilters); + } + public (List MainFilters, List SubFilters, List OverlayFilters) GetVaapiLimitedVidFiltersPrefered( EncodingJobInfo state, EncodingOptions options, diff --git a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs index a4869cb67..b1d319d21 100644 --- a/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs +++ b/MediaBrowser.Controller/MediaEncoding/FilterOptionType.cs @@ -28,6 +28,11 @@ namespace MediaBrowser.Controller.MediaEncoding /// /// The overlay_vaapi_framesync. /// - OverlayVaapiFrameSync = 4 + OverlayVaapiFrameSync = 4, + + /// + /// The overlay_vulkan_framesync. + /// + OverlayVulkanFrameSync = 5 } } diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs index 69d0bf45c..52c57b906 100644 --- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs +++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs @@ -61,6 +61,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// true if the Vaapi device is an Intel(legacy i965 driver) GPU, false otherwise. bool IsVaapiDeviceInteli965 { get; } + /// + /// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier. + /// + /// true if the Vaapi device supports vulkan drm format modifier, false otherwise. + bool IsVaapiDeviceSupportVulkanFmtModifier { get; } + /// /// Whether given encoder codec is supported. /// diff --git a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs index 9b4b1db94..8c8fc6b0f 100644 --- a/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs +++ b/MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs @@ -102,7 +102,11 @@ namespace MediaBrowser.MediaEncoding.Encoder "tonemap_vaapi", "procamp_vaapi", "overlay_vaapi", - "hwupload_vaapi" + "hwupload_vaapi", + // vulkan + "libplacebo", + "scale_vulkan", + "overlay_vulkan" }; private static readonly IReadOnlyDictionary _filterOptionsDict = new Dictionary @@ -111,7 +115,8 @@ namespace MediaBrowser.MediaEncoding.Encoder { 1, new string[] { "tonemap_cuda", "GPU accelerated HDR to SDR tonemapping" } }, { 2, new string[] { "tonemap_opencl", "bt2390" } }, { 3, new string[] { "overlay_opencl", "Action to take when encountering EOF from secondary input" } }, - { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } } + { 4, new string[] { "overlay_vaapi", "Action to take when encountering EOF from secondary input" } }, + { 5, new string[] { "overlay_vulkan", "Action to take when encountering EOF from secondary input" } } }; // These are the library versions that corresponds to our minimum ffmpeg version 4.x according to the version table below @@ -351,6 +356,39 @@ namespace MediaBrowser.MediaEncoding.Encoder } } + public bool CheckVulkanDrmDeviceByExtensionName(string renderNodePath, string[] vulkanExtensions) + { + if (!OperatingSystem.IsLinux()) + { + return false; + } + + if (string.IsNullOrEmpty(renderNodePath)) + { + return false; + } + + try + { + var command = "-v verbose -hide_banner -init_hw_device drm=dr:" + renderNodePath + " -init_hw_device vulkan=vk@dr"; + var output = GetProcessOutput(_encoderPath, command, true, null); + foreach (string ext in vulkanExtensions) + { + if (!output.Contains(ext, StringComparison.Ordinal)) + { + return false; + } + } + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error detecting the given drm render node path"); + return false; + } + } + private IEnumerable GetHwaccelTypes() { string? output = null; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index 757a01715..ec3412f90 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -72,6 +72,16 @@ namespace MediaBrowser.MediaEncoding.Encoder private bool _isVaapiDeviceAmd = false; private bool _isVaapiDeviceInteliHD = false; private bool _isVaapiDeviceInteli965 = false; + private bool _isVaapiDeviceSupportVulkanFmtModifier = false; + + private static string[] _vulkanFmtModifierExts = { + "VK_KHR_sampler_ycbcr_conversion", + "VK_EXT_image_drm_format_modifier", + "VK_KHR_external_memory_fd", + "VK_EXT_external_memory_dma_buf", + "VK_KHR_external_semaphore_fd", + "VK_EXT_external_memory_host" + }; private Version _ffmpegVersion = null; private string _ffmpegPath = string.Empty; @@ -110,6 +120,8 @@ namespace MediaBrowser.MediaEncoding.Encoder public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965; + public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier; + /// /// Run at startup or if the user removes a Custom path from transcode page. /// Sets global variables FFmpegPath. @@ -169,6 +181,8 @@ namespace MediaBrowser.MediaEncoding.Encoder _isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice); _isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice); _isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice); + _isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts); + if (_isVaapiDeviceAmd) { _logger.LogInformation("VAAPI device {RenderNodePath} is AMD GPU", options.VaapiDevice); @@ -181,6 +195,11 @@ namespace MediaBrowser.MediaEncoding.Encoder { _logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice); } + + if (_isVaapiDeviceSupportVulkanFmtModifier) + { + _logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice); + } } }