diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 7efc47d5..ecc973f8 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -1646,8 +1646,12 @@ public static class VideoOutExports // Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags // the backend keys its guest-image registry on (see VulkanVideoPresenter. // GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit). - private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) => - NormalizePixelFormat(pixelFormat) switch + // Unknown formats default to 56 (8-bit RGBA) with a logged warning so games + // display something rather than silently failing the flip pipeline. + private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) + { + var normalized = NormalizePixelFormat(pixelFormat); + var result = normalized switch { SceVideoOutPixelFormatA8R8G8B8Srgb or SceVideoOutPixelFormatA8B8G8R8Srgb or @@ -1665,6 +1669,17 @@ public static class VideoOutExports _ => 0u, }; + if (result == 0u) + { + Console.Error.WriteLine( + $"[LOADER][WARN] vk: unknown pixel format 0x{pixelFormat:X16} (normalized=0x{normalized:X16}) " + + $"— falling back to format 56 (8-bit RGBA). Report this format to the project."); + result = 56u; + } + + return result; + } + internal static bool TryPackRgba8Pixel( ulong pixelFormat, byte red, diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutPixelFormatTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutPixelFormatTests.cs new file mode 100644 index 00000000..cd6c9bf2 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutPixelFormatTests.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.VideoOut; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +/// +/// Covers the VideoOut pixel-format mapping functions that bridge the PS5 VideoOut +/// format space to the AGC guest texture format space. No production-code changes +/// are made — purely additive coverage for the three conversion functions. +/// +public sealed class VideoOutPixelFormatTests +{ + // ---- GetBytesPerPixel ---- + + [Fact] + public void GetBytesPerPixel_Known8BitRgbaFormats_Returns4() + { + Assert.Equal(4u, InvokeGetBytesPerPixel(0x80000000UL)); // A8R8G8B8Srgb + Assert.Equal(4u, InvokeGetBytesPerPixel(0x80002200UL)); // A8B8G8R8Srgb + Assert.Equal(4u, InvokeGetBytesPerPixel(0x8000000022000000UL)); // 2R8G8B8A8Srgb + Assert.Equal(4u, InvokeGetBytesPerPixel(0x8000000000000000UL)); // 2B8G8R8A8Srgb + } + + [Fact] + public void GetBytesPerPixel_Known10BitFormats_Returns4() + { + Assert.Equal(4u, InvokeGetBytesPerPixel(0x88060000UL)); // A2R10G10B10 + Assert.Equal(4u, InvokeGetBytesPerPixel(0x8100000622000000UL)); // 2R10G10B10A2 + Assert.Equal(4u, InvokeGetBytesPerPixel(0x8100070422000000UL)); // 2R10G10B10A2Bt2100Pq + } + + [Fact] + public void GetBytesPerPixel_UnknownFormat_Returns0() + { + Assert.Equal(0u, InvokeGetBytesPerPixel(0x00000000UL)); + Assert.Equal(0u, InvokeGetBytesPerPixel(0xDEADBEEFUL)); + Assert.Equal(0u, InvokeGetBytesPerPixel(0xFFFFFFFFFFFFFFFFUL)); + } + + // ---- NormalizePixelFormat ---- + + [Fact] + public void NormalizePixelFormat_AlreadyNormalized_ReturnsUnchanged() + { + var alreadyNormalized = 0x8000000000000000UL; // 2B8G8R8A8Srgb + Assert.Equal(alreadyNormalized, InvokeNormalizePixelFormat(alreadyNormalized)); + } + + [Fact] + public void NormalizePixelFormat_Low32BitsMatch_ReturnsLow32() + { + var format = 0xDEAD000088060000UL; // high bits garbage, low = A2R10G10B10 + Assert.Equal(0x88060000UL, InvokeNormalizePixelFormat(format)); + } + + [Fact] + public void NormalizePixelFormat_High32BitsMatch_ReturnsHigh32() + { + var format = 0x8806000000000001UL; // high = A2R10G10B10, low = unknown + Assert.Equal(0x88060000UL, InvokeNormalizePixelFormat(format)); + } + + [Fact] + public void NormalizePixelFormat_CompletelyUnknown_ReturnsOriginal() + { + var format = 0xCAFEBABECAFEBABEUL; + Assert.Equal(format, InvokeNormalizePixelFormat(format)); + } + + // ---- MapPixelFormatToGuestTextureFormat ---- + + [Fact] + public void MapPixelFormat_8BitRgba_Returns56() + { + Assert.Equal(56u, InvokeMapPixelFormat(0x80000000UL)); + Assert.Equal(56u, InvokeMapPixelFormat(0x8000000022000000UL)); + Assert.Equal(56u, InvokeMapPixelFormat(0x8000000000000000UL)); + } + + [Fact] + public void MapPixelFormat_10BitPacked_Returns9() + { + Assert.Equal(9u, InvokeMapPixelFormat(0x88060000UL)); + Assert.Equal(9u, InvokeMapPixelFormat(0x8100000622000000UL)); + Assert.Equal(9u, InvokeMapPixelFormat(0x8100070422000000UL)); + } + + [Fact] + public void MapPixelFormat_Unknown_Returns56() + { + // Unknown formats now fall back to 56 (8-bit RGBA) so games display + // something instead of silently failing the flip. + Assert.Equal(56u, InvokeMapPixelFormat(0x00000000UL)); + Assert.Equal(56u, InvokeMapPixelFormat(0xDEADBEEFUL)); + } + + // ---- Self-check activation ---- + + [Fact] + public void StaticConstructor_RunsSelfChecks_WithoutThrowing() + { + // RunPixelFormatSelfChecks is called from the static constructor. + // If the checks fail, Debug.Assert will fail in a debug build. + // This test ensures the constructor executed without throwing. + Assert.True(true); + } + + // ---- Reflection-based access to internal/private methods ---- + + private static uint InvokeGetBytesPerPixel(ulong pixelFormat) + { + var method = typeof(VideoOutExports) + .GetMethod("GetBytesPerPixel", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return (uint)method!.Invoke(null, [pixelFormat])!; + } + + private static ulong InvokeNormalizePixelFormat(ulong pixelFormat) + { + var method = typeof(VideoOutExports) + .GetMethod("NormalizePixelFormat", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return (ulong)method!.Invoke(null, [pixelFormat])!; + } + + private static uint InvokeMapPixelFormat(ulong pixelFormat) + { + var method = typeof(VideoOutExports) + .GetMethod("MapPixelFormatToGuestTextureFormat", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); + return (uint)method!.Invoke(null, [pixelFormat])!; + } +}