diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1035417..e06377dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -83,6 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add toggle to hide/show disabled channels in channel list - Add disabled text color and `(D)` and `(H)` labels for disabled and hidden channels in channel list - Graphics engine: fix subtitle path escaping and font loading +- Fix corrupt output (green artifacts) when decoding 10-bit content using AMD Polaris GPUs ### Changed - Classic schedules: `Refresh` classic playouts from playout list; do not `Reset` them diff --git a/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs b/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs index 5128a8008..b004ae1ed 100644 --- a/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs +++ b/ErsatzTV.FFmpeg.Tests/Capabilities/Vaapi/VaapiCapabilityParserTests.cs @@ -9,6 +9,16 @@ namespace ErsatzTV.FFmpeg.Tests.Capabilities.Vaapi; [TestFixture] public class VaapiCapabilityParserTests { + private const string GenerationPolarisOutput = @"Trying display: drm +vainfo: VA-API version: 1.22 (libva 2.22.0) +vainfo: Driver version: Mesa Gallium driver 25.2.7-arch1.1 for AMD Radeon RX 550 / 550 Series (radeonsi, polaris12, ACO, DRM 3.64, 6.17.8-arch1-1) +vainfo: Supported config attributes per profile/entrypoint pair +VAProfileMPEG2Simple/VAEntrypointVLD + VAConfigAttribRTFormat : VA_RT_FORMAT_YUV420 + VAConfigAttribMaxPictureWidth : 1920 + VAConfigAttribMaxPictureHeight : 1088 +"; + private const string BriefOutput = @"Trying display: wayland vainfo: VA-API version: 1.18 (libva 2.18.2) vainfo: Driver version: Mesa Gallium driver 23.1.2 for AMD Radeon RX 6750 XT (navi22, LLVM 15.0.7, DRM 3.52, 6.3.8-arch1-1) @@ -201,4 +211,18 @@ VAProfileNone/VAEntrypointVideoProc entrypoint.RateControlModes.Count.ShouldBeGreaterThan(0); } } + + [Test] + public void Full_ShouldParseGeneration() + { + string generation = VaapiCapabilityParser.ParseGeneration(FullOutput); + generation.ShouldBe("navi22"); + } + + [Test] + public void Polaris_ShouldParseGeneration() + { + string generation = VaapiCapabilityParser.ParseGeneration(GenerationPolarisOutput); + generation.ShouldBe("polaris12"); + } } diff --git a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs index c0b4e4d51..fce8e934a 100644 --- a/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs +++ b/ErsatzTV.FFmpeg/Capabilities/HardwareCapabilitiesFactory.cs @@ -29,6 +29,9 @@ public partial class HardwareCapabilitiesFactory( private static readonly CompositeFormat VaapiCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.vaapi.{0}.{1}.{2}"); + private static readonly CompositeFormat VaapiGenerationCacheKeyFormat = + CompositeFormat.Parse("ffmpeg.hardware.vaapi.generation.{0}.{1}.{2}"); + private static readonly CompositeFormat QsvCacheKeyFormat = CompositeFormat.Parse("ffmpeg.hardware.qsv.{0}"); private static readonly CompositeFormat FFmpegCapabilitiesCacheKeyFormat = CompositeFormat.Parse("ffmpeg.{0}"); @@ -467,12 +470,25 @@ public partial class HardwareCapabilitiesFactory( string display = vaapiDisplay.IfNone("drm"); string driver = vaapiDriver.IfNone(string.Empty); string device = vaapiDevice.IfNone(string.Empty); + string generation = string.Empty; var cacheKey = string.Format(CultureInfo.InvariantCulture, VaapiCacheKeyFormat, display, driver, device); + var generationCacheKey = string.Format( + CultureInfo.InvariantCulture, + VaapiGenerationCacheKeyFormat, + display, + driver, + device); if (memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && profileEntrypoints is not null) { - return new VaapiHardwareCapabilities(profileEntrypoints, logger); + if (memoryCache.TryGetValue(generationCacheKey, out string? cachedGeneration) && + cachedGeneration is not null) + { + generation = cachedGeneration; + } + + return new VaapiHardwareCapabilities(profileEntrypoints, generation, logger); } Option output = await GetVaapiOutput(display, vaapiDriver, device); @@ -485,6 +501,7 @@ public partial class HardwareCapabilitiesFactory( foreach (string o in output) { profileEntrypoints = VaapiCapabilityParser.ParseFull(o); + generation = VaapiCapabilityParser.ParseGeneration(o); } if (profileEntrypoints is not null && profileEntrypoints.Count != 0) @@ -507,7 +524,8 @@ public partial class HardwareCapabilitiesFactory( } memoryCache.Set(cacheKey, profileEntrypoints); - return new VaapiHardwareCapabilities(profileEntrypoints, logger); + memoryCache.Set(generationCacheKey, generation); + return new VaapiHardwareCapabilities(profileEntrypoints, generation, logger); } } catch (Exception ex) @@ -541,7 +559,7 @@ public partial class HardwareCapabilitiesFactory( if (memoryCache.TryGetValue(cacheKey, out List? profileEntrypoints) && profileEntrypoints is not null) { - return new VaapiHardwareCapabilities(profileEntrypoints, logger); + return new VaapiHardwareCapabilities(profileEntrypoints, string.Empty, logger); } QsvOutput output = await GetQsvOutput(ffmpegPath, qsvDevice); @@ -583,7 +601,7 @@ public partial class HardwareCapabilitiesFactory( device); memoryCache.Set(cacheKey, profileEntrypoints); - return new VaapiHardwareCapabilities(profileEntrypoints, logger); + return new VaapiHardwareCapabilities(profileEntrypoints, string.Empty, logger); } } } diff --git a/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs index b015e5f5b..6f68bc4e2 100644 --- a/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs +++ b/ErsatzTV.FFmpeg/Capabilities/Vaapi/VaapiCapabilityParser.cs @@ -2,7 +2,7 @@ using System.Text.RegularExpressions; namespace ErsatzTV.FFmpeg.Capabilities.Vaapi; -public static class VaapiCapabilityParser +public static partial class VaapiCapabilityParser { public static List Parse(string output) { @@ -10,8 +10,7 @@ public static class VaapiCapabilityParser foreach (string line in string.Join("", output).Split("\n")) { - const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)"; - Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN); + Match match = ProfileEntrypointRegex().Match(line); if (match.Success) { profileEntrypoints.Add( @@ -33,9 +32,7 @@ public static class VaapiCapabilityParser for (var i = 0; i < allLines.Length; i++) { string line = allLines[i]; - const string PROFILE_ENTRYPOINT_PATTERN = @"(VAProfile\w*).*(VAEntrypoint\w*)"; - const string PROFILE_RATE_CONTROL_PATTERN = @".*VA_RC_(\w*).*"; - Match match = Regex.Match(line, PROFILE_ENTRYPOINT_PATTERN); + Match match = ProfileEntrypointRegex().Match(line); if (match.Success) { profile = new VaapiProfileEntrypoint(match.Groups[1].Value.Trim(), match.Groups[2].Value.Trim()); @@ -44,7 +41,7 @@ public static class VaapiCapabilityParser else { // check for rate control - match = Regex.Match(line, PROFILE_RATE_CONTROL_PATTERN); + match = ProfileRateControlRegex().Match(line); if (match.Success) { switch (match.Groups[1].Value.Trim().ToLowerInvariant()) @@ -65,4 +62,36 @@ public static class VaapiCapabilityParser return profileEntrypoints; } + + public static string ParseGeneration(string output) + { + string generation = string.Empty; + Match match = MiscGenerationRegex().Match(output); + if (match.Success) + { + generation = match.Groups[1].Value.Trim().ToLowerInvariant(); + if (generation is "radeonsi") + { + match = RadeonSiGenerationRegex().Match(output); + if (match.Success) + { + generation = match.Groups[1].Value.Trim().ToLowerInvariant(); + } + } + } + + return generation; + } + + [GeneratedRegex(@"(VAProfile\w*).*(VAEntrypoint\w*)")] + private static partial Regex ProfileEntrypointRegex(); + + [GeneratedRegex(@".*VA_RC_(\w*).*")] + private static partial Regex ProfileRateControlRegex(); + + [GeneratedRegex(@"Driver version:.*\(radeonsi, (\w+)")] + private static partial Regex RadeonSiGenerationRegex(); + + [GeneratedRegex(@"Driver version:.*\((\w+),")] + private static partial Regex MiscGenerationRegex(); } diff --git a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs index 5b98730a8..4a4c9957a 100644 --- a/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs +++ b/ErsatzTV.FFmpeg/Capabilities/VaapiHardwareCapabilities.cs @@ -4,18 +4,13 @@ using Microsoft.Extensions.Logging; namespace ErsatzTV.FFmpeg.Capabilities; -public class VaapiHardwareCapabilities : IHardwareCapabilities +public class VaapiHardwareCapabilities( + List profileEntrypoints, + string generation, + ILogger logger) + : IHardwareCapabilities { - private readonly ILogger _logger; - private readonly List _profileEntrypoints; - - public VaapiHardwareCapabilities(List profileEntrypoints, ILogger logger) - { - _profileEntrypoints = profileEntrypoints; - _logger = logger; - } - - public int EntrypointCount => _profileEntrypoints.Count; + public int EntrypointCount => profileEntrypoints.Count; public FFmpegCapability CanDecode( string videoFormat, @@ -25,121 +20,126 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities { int bitDepth = maybePixelFormat.Map(pf => pf.BitDepth).IfNone(8); + bool isPolaris = generation.Contains("polaris", StringComparison.OrdinalIgnoreCase); + bool isHardware = (videoFormat, videoProfile.IfNone(string.Empty).ToLowerInvariant()) switch { // no hardware decoding of 10-bit h264 (VideoFormat.H264, _) when bitDepth == 10 => false, + // skip polaris hardware decoding 10-bit + (_, _) when bitDepth == 10 && isPolaris => false, + // no hardware decoding of h264 baseline profile (VideoFormat.H264, "baseline" or "66") => false, (VideoFormat.H264, "main" or "77") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.H264Main, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.H264, "high" or "100") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.H264High, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.H264, "high 10" or "110") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.H264High, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.H264, "baseline constrained" or "constrained baseline" or "578") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.H264ConstrainedBaseline, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Mpeg2Video, "main" or "4") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Mpeg2Main, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Mpeg2Video, "simple" or "5") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Mpeg2Simple, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vc1, "simple" or "0") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vc1Simple, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vc1, "main" or "1") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vc1Main, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vc1, "advanced" or "3") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vc1Advanced, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Hevc, "main" or "1") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.HevcMain, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Hevc, "main 10" or "2") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.HevcMain10, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vp9, "profile 0" or "0") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vp9Profile0, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vp9, "profile 1" or "1") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vp9Profile1, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vp9, "profile 2" or "2") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vp9Profile2, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Vp9, "profile 3" or "3") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Vp9Profile3, VaapiEntrypoint: VaapiEntrypoint.Decode }), (VideoFormat.Av1, "main" or "0") => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Av1Profile0, VaapiEntrypoint: VaapiEntrypoint.Decode @@ -151,7 +151,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities if (!isHardware) { - _logger.LogDebug( + logger.LogDebug( "VAAPI does not support decoding {Format}/{Profile}, will use software decoder", videoFormat, videoProfile); @@ -173,35 +173,35 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities VideoFormat.H264 when bitDepth == 10 => false, VideoFormat.H264 => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.H264Main, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower }), VideoFormat.Hevc when bitDepth == 10 => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.HevcMain10, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower }), VideoFormat.Hevc => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.HevcMain, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower }), VideoFormat.Av1 => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Av1Profile0, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower }), VideoFormat.Mpeg2Video => - _profileEntrypoints.Any(e => e is + profileEntrypoints.Any(e => e is { VaapiProfile: VaapiProfile.Mpeg2Main, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower @@ -212,7 +212,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities if (!isHardware) { - _logger.LogDebug( + logger.LogDebug( "VAAPI does not support encoding {Format} with bit depth {BitDepth}, will use software encoder", videoFormat, bitDepth); @@ -230,7 +230,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities VideoFormat.H264 when bitDepth == 10 => None, VideoFormat.H264 => - _profileEntrypoints.Where(e => e is + profileEntrypoints.Where(e => e is { VaapiProfile: VaapiProfile.H264Main, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower @@ -238,7 +238,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities .HeadOrNone(), VideoFormat.Hevc when bitDepth == 10 => - _profileEntrypoints.Where(e => e is + profileEntrypoints.Where(e => e is { VaapiProfile: VaapiProfile.HevcMain10, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower @@ -246,7 +246,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities .HeadOrNone(), VideoFormat.Hevc => - _profileEntrypoints.Where(e => e is + profileEntrypoints.Where(e => e is { VaapiProfile: VaapiProfile.HevcMain, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower @@ -254,7 +254,7 @@ public class VaapiHardwareCapabilities : IHardwareCapabilities .HeadOrNone(), VideoFormat.Mpeg2Video => - _profileEntrypoints.Where(e => e is + profileEntrypoints.Where(e => e is { VaapiProfile: VaapiProfile.Mpeg2Main, VaapiEntrypoint: VaapiEntrypoint.Encode or VaapiEntrypoint.EncodeLowPower diff --git a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs index 4a68f4935..16b5bb5e4 100644 --- a/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs +++ b/ErsatzTV.FFmpeg/Pipeline/VaapiPipelineBuilder.cs @@ -355,7 +355,7 @@ public class VaapiPipelineBuilder : SoftwarePipelineBuilder if (currentState.FrameDataLocation == FrameDataLocation.Hardware) { - result.Add(new VaapiFormatFilter(format)); + result.Add(new VaapiFormatFilter(format)); } else {