diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs index 7bcea93..67201de 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageDescription.cs @@ -457,7 +457,7 @@ public sealed record DepthFormatRule(GuestDepthFormat DepthFormat, GuestPixelFor public readonly record struct DisplayPixelFormat(Format HostFormat, GuestPixelFormat GuestFormat, uint BytesPerElement, bool Bgra16); -// The six display surface pixel formats the store accepts. +// Display formats retain their encoded values; presentation applies the transfer function. public static class DisplayFormatRule { public static readonly (ulong PixelFormat, DisplayPixelFormat Info)[] Table = @@ -466,6 +466,10 @@ public static class DisplayFormatRule (0x8000000000000000, new DisplayPixelFormat(Format.B8G8R8A8Srgb, GuestPixelFormat.Bits8_8_8_8Srgb, 4, false)), (0x8100000022000000, new DisplayPixelFormat(Format.A2B10G10R10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), (0x8100000000000000, new DisplayPixelFormat(Format.A2R10G10B10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), + (0x8100000622000000, new DisplayPixelFormat(Format.A2B10G10R10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), + (0x8100000600000000, new DisplayPixelFormat(Format.A2R10G10B10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), + (0x8100070422000000, new DisplayPixelFormat(Format.A2B10G10R10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), + (0x8100070400000000, new DisplayPixelFormat(Format.A2R10G10B10UnormPack32, GuestPixelFormat.Bits10_10_10_2UNorm, 4, false)), (0xc001000622000000, new DisplayPixelFormat(Format.R16G16B16A16Sfloat, GuestPixelFormat.Bits16_16_16_16Float, 8, false)), (0xc001000600000000, new DisplayPixelFormat(Format.R16G16B16A16Sfloat, GuestPixelFormat.Bits16_16_16_16Float, 8, true)), ]; diff --git a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs index 05eab54..c0b7dc3 100644 --- a/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs +++ b/src/SharpEmu.Libs/Gpu/Images/ImageRequestBuilders.DepthTarget.cs @@ -231,10 +231,10 @@ public static partial class ImageRequestBuilders // Builds the request for a video-out surface registered by the guest. public static ImageRequest DisplaySurface(in DisplaySurfaceWords surface) { - const ulong StrictColorimetryOption = 1; + const ulong SupportedOptions = (1UL << 3) | (1UL << 5); // Strict colorimetry and premultiplied alpha. var compression = ImageDescription.ClassifyDisplayCompression(surface.Compressed, surface.MetadataAddress, surface.DccControl, surface.DccClearColor); if (surface.Width == 0 || surface.Height == 0 || surface.Width > 16384 || surface.Height > 16384 || - (surface.Option != 0 && surface.Option != StrictColorimetryOption) || surface.TilingMode != 0 || surface.DataAddress == 0 || + (surface.Option & ~SupportedOptions) != 0 || surface.TilingMode != 0 || surface.DataAddress == 0 || compression == DisplayCompression.Unsupported) { throw SubmissionScheduler.Fatal( diff --git a/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs b/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs index 6ef7fb0..9e84110 100644 --- a/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs +++ b/src/SharpEmu.Libs/VideoOut/HostVideoOptions.cs @@ -4,6 +4,7 @@ namespace SharpEmu.Libs.VideoOut; using SharpEmu.Libs.Gpu.Metal; +using SharpEmu.Libs.Gpu; public enum HostWindowMode { @@ -66,6 +67,9 @@ public sealed record HostVideoOptions public PerformanceOverlayCorner OverlayCorner { get; init; } = PerformanceOverlayCorner.TopRight; public PerformanceOverlayMode OverlayMode { get; init; } = PerformanceOverlayMode.TitleBar; + internal bool CanUseHdr(bool displayHdrEnabled, bool surfaceSupportsHdr) => + surfaceSupportsHdr && (HdrMode == HostHdrMode.On || HdrMode == HostHdrMode.Auto && displayHdrEnabled); + public HostVideoOptions Normalize() => this with { Width = Math.Clamp(Width, 640, 16384), @@ -81,14 +85,22 @@ public sealed record HostVideoOptions public static class HostVideoHost { private static HostVideoOptions _currentOptions = HostVideoOptions.Default; + private static bool _videoConfigured; public static HostVideoOptions CurrentOptions => Volatile.Read(ref _currentOptions); + internal static bool IsHdrOutputSupported => + Volatile.Read(ref _videoConfigured) && CurrentOptions.HdrMode != HostHdrMode.Off && + GuestGpu.Current.BackendName == "Vulkan" && VulkanVideoPresenter.QueryHdrOutputSupport(); + public static bool TryConfigureVideo(HostVideoOptions options) { var normalized = options.Normalize(); Volatile.Write(ref _currentOptions, normalized); - return VulkanVideoPresenter.TryConfigureVideo(normalized) & - MetalVideoPresenter.TryConfigureVideo(normalized); + var configured = VulkanVideoPresenter.TryConfigureVideo(normalized) & + MetalVideoPresenter.TryConfigureVideo(normalized); + if (configured) + Volatile.Write(ref _videoConfigured, true); + return configured; } } diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 1a852fc..9656d52 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -7,6 +7,7 @@ using SharpEmu.Logging; using SharpEmu.HLE.Host; using SharpEmu.Libs.Diagnostics; using SharpEmu.Libs.Gpu; +using SharpEmu.Libs.Gpu.Images; using SharpEmu.Libs.Audio; using SharpEmu.Libs.Kernel; using System.Buffers; @@ -297,7 +298,8 @@ public static partial class VideoOutExports uint TilingMode, uint Width, uint Height, - uint PitchInPixel); + uint PitchInPixel, + ulong Option = 0); [SysAbiExport( Nid = "Up36PTk687E", @@ -514,20 +516,28 @@ public static partial class VideoOutExports } Span status = stackalloc byte[VideoOutOutputStatusSize]; - status.Clear(); - var resolutionClass = port.OutputWidth >= 3840 || port.OutputHeight >= 2160 ? 2 : 1; - BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass); - BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], 1); - // The status uses a refresh-rate code, not the frequency used for pacing. - var refreshRateCode = port.RefreshRate >= 119 - ? SceVideoOutRefreshRate119_88Hz - : SceVideoOutRefreshRate59_94Hz; - BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], refreshRateCode); + WriteOutputStatus(status, port.OutputWidth, port.OutputHeight, port.RefreshRate, + HostVideoHost.IsHdrOutputSupported); return ctx.Memory.TryWrite(statusAddress, status) ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; } + internal static void WriteOutputStatus(Span status, uint width, uint height, uint refreshRate, + bool hdrOutputSupported) + { + status.Clear(); + var resolutionClass = width >= 3840 || height >= 2160 ? 2 : 1; + BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass); + BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], hdrOutputSupported ? 2 : 1); + // The status uses a refresh-rate code, not the frequency used for pacing. + var refreshRateCode = refreshRate >= 119 + ? SceVideoOutRefreshRate119_88Hz + : SceVideoOutRefreshRate59_94Hz; + BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], refreshRateCode); + BinaryPrimitives.WriteUInt64LittleEndian(status[0x10..0x18], hdrOutputSupported ? 1UL : 0UL); + } + [SysAbiExport( Nid = "DYhhWbJSeRg", ExportName = "sceVideoOutColorSettingsSetGamma_", @@ -1199,7 +1209,8 @@ public static partial class VideoOutExports attribute.TilingMode, attribute.Width, attribute.Height, - attribute.PitchInPixel); + attribute.PitchInPixel, + attribute.Option); return true; } } @@ -1348,6 +1359,50 @@ public static partial class VideoOutExports return (int)OrbisGen2Result.ORBIS_GEN2_OK; } + [SysAbiExport( + Nid = "HuViW4HnrOw", + ExportName = "sceVideoOutSubmitChangeBufferAttribute2", + Target = Generation.Gen5, + LibraryName = "libSceVideoOut")] + public static int VideoOutSubmitChangeBufferAttribute2(CpuContext ctx) + { + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var setIndex = unchecked((int)ctx[CpuRegister.Rsi]); + var attributeAddress = ctx[CpuRegister.Rdx]; + var option = ctx[CpuRegister.Rcx]; + lock (_stateGate) + { + if (!_ports.TryGetValue(handle, out var port)) return OrbisVideoOutErrorInvalidHandle; + if (attributeAddress == 0) return OrbisVideoOutErrorInvalidOption; + if ((uint)setIndex >= MaxDisplayBufferGroups) return OrbisVideoOutErrorInvalidIndex; + if (option != 0) return OrbisVideoOutErrorInvalidOption; + if (port.Groups[setIndex] is null) return OrbisVideoOutErrorInvalidIndex; + if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute)) + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + if (attribute.Width is 0 or > 16384 || attribute.Height is 0 or > 16384 || + attribute.TilingMode != 0 || (attribute.Option & ~((1UL << 3) | (1UL << 5))) != 0 || + !DisplayFormatRule.TryDecode(attribute.PixelFormat, out _)) + return OrbisVideoOutErrorInvalidValue; + + // Keep buffer addresses and membership when the guest switches SDR/HDR formats. + port.Groups[setIndex] = new VideoOutBufferGroup { Index = setIndex, Attribute = attribute }; + port.OutputWidth = attribute.Width; + port.OutputHeight = attribute.Height; + var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat); + foreach (var slot in port.BufferSlots) + { + if (slot.GroupIndex != setIndex) continue; + GuestGpu.Current.RegisterKnownDisplayBuffer(slot.AddressLeft, guestFormat); + if (slot.AddressRight != 0) + GuestGpu.Current.RegisterKnownDisplayBuffer(slot.AddressRight, guestFormat); + } + + TraceVideoOut($"videoout.change_buffer_attribute2 handle={handle} set={setIndex} " + + $"fmt=0x{attribute.PixelFormat:X16} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height}"); + return 0; + } + } + [SysAbiExport( Nid = "w3BY+tAEiQY", ExportName = "sceVideoOutRegisterBuffers", diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 1b69988..7022620 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -549,7 +549,7 @@ internal static unsafe partial class VulkanVideoPresenter try { var surface = new DisplaySurfaceWords( - displayBuffer.Address, 0, displayBuffer.PixelFormat, displayBuffer.Width, displayBuffer.Height, displayBuffer.TilingMode, 0, 0, 0, false); + displayBuffer.Address, 0, displayBuffer.PixelFormat, displayBuffer.Width, displayBuffer.Height, displayBuffer.TilingMode, displayBuffer.Option, 0, 0, false); var request = ImageRequestBuilders.DisplaySurface(surface); _ = BeginBatchedGuestCommands(); var imageIdentifier = _imageCache.FindImage(ref request); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Swapchain.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Swapchain.cs index 34cc45e..7569039 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Swapchain.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.Swapchain.cs @@ -80,6 +80,8 @@ internal static unsafe partial class VulkanVideoPresenter } var hdrState = _window.HdrState; + _ = ChooseSurfaceFormat(formats, requestHdr: true, out var surfaceSupportsHdr); + var hdrOutputSupported = _videoOptions.CanUseHdr(hdrState.Enabled, surfaceSupportsHdr); var guestHdrRequested = VideoOutExports.IsHdrOutputRequested; var requestHdr = _videoOptions.HdrMode switch { @@ -140,7 +142,8 @@ internal static unsafe partial class VulkanVideoPresenter _imageInitialized = new bool[swapchainImageCount]; Console.Error.WriteLine( $"[LOADER][INFO] Vulkan output color: requested={_videoOptions.HdrMode} " + - $"display_hdr={hdrState.Enabled} guest_hdr={guestHdrRequested} active={_hdrOutputActive} " + + $"display_hdr={hdrState.Enabled} guest_hdr_supported={hdrOutputSupported} " + + $"guest_hdr={guestHdrRequested} active={_hdrOutputActive} " + $"format={_swapchainFormat} colorspace={_swapchainColorSpace} " + $"sdr_white={_hdrSdrWhiteLevel:F3} headroom={_hdrHeadroom:F3}"); if (requestHdr && !_hdrOutputActive) @@ -148,6 +151,13 @@ internal static unsafe partial class VulkanVideoPresenter Console.Error.WriteLine( "[LOADER][WARN] HDR output requested but the Vulkan surface exposes no scRGB format; using SDR."); } + + lock (_gate) + { + _hdrOutputSupported = hdrOutputSupported; + _hdrCapabilitiesReady = true; + System.Threading.Monitor.PulseAll(_gate); + } } private void CreateGuestDrawResources() diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs index 385d0ab..34d429a 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs @@ -37,6 +37,8 @@ internal static unsafe partial class VulkanVideoPresenter private static uint _windowHeight; private static bool _closed; private static bool _presenterCloseRequested; + private static bool _hdrCapabilitiesReady; + private static bool _hdrOutputSupported; private static bool _splashHidden; @@ -121,10 +123,30 @@ internal static unsafe partial class VulkanVideoPresenter } _videoOptions = options.Normalize(); + _hdrCapabilitiesReady = false; + _hdrOutputSupported = false; return true; } } + internal static bool QueryHdrOutputSupport() + { + // The guest can query HDR before registering its first display buffer. + EnsureStarted((uint)_videoOptions.Width, (uint)_videoOptions.Height); + lock (_gate) + { + while (!_hdrCapabilitiesReady && !_closed && _presenterStartupFailure is null && + !HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _presenterCloseRequested)) + { + System.Threading.Monitor.Wait(_gate); + } + + return _hdrCapabilitiesReady && !_closed && _presenterStartupFailure is null && + !HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _presenterCloseRequested) && + _hdrOutputSupported; + } + } + private static void ResetHostSessionStateLocked() { _latestPresentation = null; diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs index 619a640..141c722 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageDescriptionTests.cs @@ -112,7 +112,7 @@ public sealed class ImageDescriptionTests } [Fact] - public void DisplayFormats_DecodeTheSixPixelFormats() + public void DisplayFormats_DecodeSupportedPixelFormats() { Assert.True(DisplayFormatRule.TryDecode(0x8000000022000000, out var rgba)); Assert.Equal((Format.R8G8B8A8Srgb, GuestPixelFormat.Bits8_8_8_8Srgb, 4u, false), (rgba.HostFormat, rgba.GuestFormat, rgba.BytesPerElement, rgba.Bgra16)); diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs index 3541db0..47fe5e3 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Images/ImageRequestBuildersTests.cs @@ -361,6 +361,22 @@ public sealed class ImageRequestBuildersTests : IClassFixture(() => ImageRequestBuilders.DisplaySurface(new DisplaySurfaceWords(Base, 0, 0x8000000022000000, 1920, 1080, 1, 0, 0, 0, false))); Assert.Throws(() => ImageRequestBuilders.DisplaySurface(new DisplaySurfaceWords(Base, 0, 0x1234, 1920, 1080, 0, 0, 0, 0, false))); Assert.Throws(() => ImageRequestBuilders.DisplaySurface(new DisplaySurfaceWords(Base, 0, 0x8000000022000000, 0, 1080, 0, 0, 0, 0, false))); - Assert.Equal(3, fatal.Messages.Count); + Assert.Throws(() => ImageRequestBuilders.DisplaySurface(new DisplaySurfaceWords(Base, 0, 0x8100070422000000, 1920, 1080, 0, 1, 0, 0, false))); + Assert.Equal(4, fatal.Messages.Count); } private readonly record struct GuestSpanCheck(ulong Address, ulong Size); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs index debb298..3cb5b8a 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutFlipRequestTests.cs @@ -40,6 +40,62 @@ public sealed class VideoOutFlipRequestTests : IDisposable _ = VideoOutExports.VideoOutClose(_context); } + [Fact] + public void ChangeBufferAttribute2UpdatesTheRegisteredGroupAndValidatesArguments() + { + var manager = new ModuleManager(); + manager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + Assert.True(manager.TryGetExport("HuViW4HnrOw", out var export)); + Assert.Equal("sceVideoOutSubmitChangeBufferAttribute2", export.Name); + + var ports = (System.Collections.IDictionary)typeof(VideoOutExports) + .GetField("_ports", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!.GetValue(null)!; + var port = ports[_handle]!; + // Seed registration without starting a native presenter in this state test. + var groups = (Array)port.GetType().GetProperty("Groups")!.GetValue(port)!; + groups.SetValue(Activator.CreateInstance(groups.GetType().GetElementType()!, nonPublic: true), 0); + var slots = (Array)port.GetType().GetProperty("BufferSlots")!.GetValue(port)!; + var slot = slots.GetValue(0)!; + slot.GetType().GetProperty("GroupIndex")!.SetValue(slot, 0); + slot.GetType().GetProperty("AddressLeft")!.SetValue(slot, MemoryBase); + + const ulong pqFormat = 0x8100070422000000; + var attribute = new byte[0x50]; + BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x0C), 3840); + BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x10), 2160); + BinaryPrimitives.WriteUInt64LittleEndian(attribute.AsSpan(0x18), 8); + BinaryPrimitives.WriteUInt64LittleEndian(attribute.AsSpan(0x20), pqFormat); + Assert.True(_memory.TryWrite(StatusAddress, attribute)); + + ulong Change(int setIndex = 0, ulong address = StatusAddress, ulong option = 0, int? handle = null) + { + _context[CpuRegister.Rdi] = unchecked((ulong)(handle ?? _handle)); + _context[CpuRegister.Rsi] = unchecked((ulong)setIndex); + _context[CpuRegister.Rdx] = address; + _context[CpuRegister.Rcx] = option; + Assert.True(manager.TryDispatch("HuViW4HnrOw", _context, out _)); + return _context[CpuRegister.Rax]; + } + + Assert.Equal(0UL, Change()); + Assert.True(VideoOutExports.TryGetDisplayBufferInfo(_handle, 0, out var info)); + Assert.Equal(new VideoOutExports.DisplayBufferInfo(MemoryBase, pqFormat, 0, 3840, 2160, 3840, 8), info); + Assert.Equal(3840u, port.GetType().GetProperty("OutputWidth")!.GetValue(port)); + Assert.Equal(2160u, port.GetType().GetProperty("OutputHeight")!.GetValue(port)); + Assert.Equal(unchecked((ulong)(int)0x8029000B), Change(handle: -1)); + Assert.Equal(unchecked((ulong)(int)0x8029001A), Change(address: 0)); + Assert.Equal(unchecked((ulong)(int)0x8029001A), Change(option: 1)); + Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: -1)); + Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: 4)); + Assert.Equal(unchecked((ulong)(int)0x8029000A), Change(setIndex: 1)); + Assert.Equal(unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT), Change(address: MemoryBase + 0x1000)); + BinaryPrimitives.WriteUInt32LittleEndian(attribute.AsSpan(0x0C), 0); + Assert.True(_memory.TryWrite(StatusAddress, attribute)); + Assert.Equal(unchecked((ulong)(int)0x80290001), Change()); + Assert.True(VideoOutExports.TryGetDisplayBufferInfo(_handle, 0, out var afterFailure)); + Assert.Equal(info, afterFailure); + } + [Fact] public void CleanupPreservesAnotherPortsPendingFlip() { diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs index 9b9ce58..8f93a81 100644 --- a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using SharpEmu.HLE; +using SharpEmu.Libs.VideoOut; using Xunit; namespace SharpEmu.Libs.Tests.VideoOut; @@ -23,6 +24,33 @@ public sealed class VideoOutOutputSupportTests private static readonly ulong MemoryFault = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + [Theory] + [InlineData(HostHdrMode.Auto, false, true, false)] + [InlineData(HostHdrMode.Auto, true, false, false)] + [InlineData(HostHdrMode.Auto, true, true, true)] + [InlineData(HostHdrMode.On, false, true, true)] + [InlineData(HostHdrMode.On, true, true, true)] + [InlineData(HostHdrMode.On, true, false, false)] + [InlineData(HostHdrMode.Off, true, true, false)] + public void OutputStatusReportsHdrOnlyWhenAllowedAndSupported( + HostHdrMode mode, bool displayHdrEnabled, bool surfaceSupportsHdr, bool expectedHdr) + { + var supported = new HostVideoOptions { HdrMode = mode }.CanUseHdr(displayHdrEnabled, surfaceSupportsHdr); + Assert.Equal(expectedHdr, supported); + + var bytes = Enumerable.Repeat((byte)0xA5, 0x32).ToArray(); + VideoOutExports.WriteOutputStatus(bytes.AsSpan(1, 0x30), 3840, 2160, 120, supported); + + Assert.Equal(0xA5, bytes[0]); + Assert.Equal(0xA5, bytes[^1]); + var status = bytes.AsSpan(1, 0x30); + Assert.Equal(2U, BinaryPrimitives.ReadUInt32LittleEndian(status)); + Assert.Equal(expectedHdr ? 2U : 1U, BinaryPrimitives.ReadUInt32LittleEndian(status[4..])); + Assert.Equal(13UL, BinaryPrimitives.ReadUInt64LittleEndian(status[8..])); + Assert.Equal(expectedHdr ? 1UL : 0UL, BinaryPrimitives.ReadUInt64LittleEndian(status[0x10..])); + Assert.All(status[0x18..].ToArray(), value => Assert.Equal(0, value)); + } + [Theory] [InlineData(Generation.Gen4)] [InlineData(Generation.Gen5)]