From b479dc04666182d715c3d6e6e7e4f227d3911c89 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 18 Jul 2026 07:44:23 +0800 Subject: [PATCH] videoout: implement output support query (#269) Co-authored-by: Chris Cheng --- src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 47 ++++++-- .../VideoOut/VideoOutOutputSupportTests.cs | 103 ++++++++++++++++++ 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index 09733828..4a6eade7 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -23,6 +23,7 @@ public static class VideoOutExports private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B); private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C); private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D); + private const int OrbisVideoOutErrorUnsupportedOutputMode = unchecked((int)0x80290016); private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A); private const int SceVideoOutBusTypeMain = 0; private const int SceVideoOutBufferAttributeOptionNone = 0; @@ -34,8 +35,11 @@ public static class VideoOutExports private const int VideoOutBufferAttributeSize = 0x28; private const int VideoOutBufferAttribute2Size = 0x50; private const int VideoOutBuffersEntrySize = 0x20; + private const int VideoOutOutputOptionsSize = 0x40; private const int VideoOutOutputStatusSize = 0x30; private const int VideoOutVblankStatusSize = 0x28; + private const ulong SceVideoOutOutputModeDefault = 1; + private const ulong SceVideoOutOutputMode119_88Hz = 0xF; private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000; @@ -276,17 +280,46 @@ public static class VideoOutExports [SysAbiExport( Nid = "Nv8c-Kb+DUM", ExportName = "sceVideoOutIsOutputSupported", - Target = Generation.Gen4 | Generation.Gen5, + Target = Generation.Gen5, LibraryName = "libSceVideoOut")] public static int VideoOutIsOutputSupported(CpuContext ctx) { - var busType = unchecked((int)ctx[CpuRegister.Rdi]); - _ = ctx[CpuRegister.Rsi]; // pixelFormat - _ = ctx[CpuRegister.Rdx]; // aspectRatio + var handle = unchecked((int)ctx[CpuRegister.Rdi]); + var mode = ctx[CpuRegister.Rsi]; + var optionsAddress = ctx[CpuRegister.Rdx]; + var reservedPointer = ctx[CpuRegister.Rcx]; + var reserved = ctx[CpuRegister.R8]; - // The emulator supports any output configuration on the main bus. - // Return 1 (supported) for SceVideoOutBusTypeMain, 0 otherwise. - return busType == SceVideoOutBusTypeMain ? 1 : 0; + if (!TryGetPort(handle, out var port)) + { + return OrbisVideoOutErrorInvalidHandle; + } + + if (reservedPointer != 0 || reserved != 0) + { + return OrbisVideoOutErrorInvalidValue; + } + + if (optionsAddress != 0) + { + Span options = stackalloc byte[VideoOutOutputOptionsSize]; + if (!ctx.Memory.TryRead(optionsAddress, options)) + { + return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; + } + + if (options.ContainsAnyExcept((byte)0)) + { + return OrbisVideoOutErrorInvalidOption; + } + } + + if (mode != SceVideoOutOutputModeDefault && mode != SceVideoOutOutputMode119_88Hz) + { + return OrbisVideoOutErrorUnsupportedOutputMode; + } + + return mode == SceVideoOutOutputModeDefault || port.RefreshRate >= 119 ? 1 : 0; } [SysAbiExport( diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs new file mode 100644 index 00000000..0092c9a6 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VideoOutOutputSupportTests.cs @@ -0,0 +1,103 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed class VideoOutOutputSupportTests +{ + private const string OpenNid = "Up36PTk687E"; + private const string CloseNid = "uquVH4-Du78"; + private const string OutputSupportNid = "Nv8c-Kb+DUM"; + private const ulong MemoryBase = 0x1_0000_0000; + private const ulong OptionsAddress = MemoryBase + 0x100; + private static readonly ulong InvalidValue = unchecked((ulong)(int)0x80290001); + private static readonly ulong InvalidHandle = unchecked((ulong)(int)0x8029000B); + private static readonly ulong UnsupportedOutputMode = unchecked((ulong)(int)0x80290016); + private static readonly ulong InvalidOption = unchecked((ulong)(int)0x8029001A); + private static readonly ulong MemoryFault = + unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); + + [Fact] + public void Gen5QueryReportsCapabilitiesAndValidatesArguments() + { + var gen4Manager = new ModuleManager(); + gen4Manager.RegisterExports( + SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4)); + Assert.False(gen4Manager.TryGetExport(OutputSupportNid, out _)); + + var manager = new ModuleManager(); + manager.RegisterExports( + SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen5)); + Assert.True(manager.TryGetExport(OutputSupportNid, out var export)); + Assert.Equal("sceVideoOutIsOutputSupported", export.Name); + Assert.Equal("libSceVideoOut", export.LibraryName); + Assert.Equal(Generation.Gen5, export.Target); + + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0; + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0; + context[CpuRegister.Rcx] = 0; + Assert.True(manager.TryDispatch(OpenNid, context, out _)); + var handle = context[CpuRegister.Rax]; + Assert.NotEqual(0UL, handle); + + try + { + Assert.Equal(1UL, DispatchOutputSupport(manager, context, handle, 1)); + Assert.Equal(0UL, DispatchOutputSupport(manager, context, handle, 15)); + Assert.Equal( + InvalidHandle, + DispatchOutputSupport(manager, context, ulong.MaxValue, 1)); + Assert.Equal( + InvalidValue, + DispatchOutputSupport(manager, context, handle, 1, reservedPointer: 1)); + Assert.Equal( + InvalidValue, + DispatchOutputSupport(manager, context, handle, 1, reserved: 1)); + Assert.Equal( + 1UL, + DispatchOutputSupport(manager, context, handle, 1, OptionsAddress)); + Assert.Equal( + MemoryFault, + DispatchOutputSupport(manager, context, handle, 1, MemoryBase + 0x1000)); + + Assert.True(memory.TryWrite(OptionsAddress, new byte[] { 1 })); + Assert.Equal( + InvalidOption, + DispatchOutputSupport(manager, context, handle, 1, OptionsAddress)); + Assert.Equal( + UnsupportedOutputMode, + DispatchOutputSupport(manager, context, handle, 2)); + } + finally + { + context[CpuRegister.Rdi] = handle; + _ = manager.TryDispatch(CloseNid, context, out _); + } + } + + private static ulong DispatchOutputSupport( + ModuleManager manager, + CpuContext context, + ulong handle, + ulong mode, + ulong optionsAddress = 0, + ulong reservedPointer = 0, + ulong reserved = 0) + { + context[CpuRegister.Rdi] = handle; + context[CpuRegister.Rsi] = mode; + context[CpuRegister.Rdx] = optionsAddress; + context[CpuRegister.Rcx] = reservedPointer; + context[CpuRegister.R8] = reserved; + context[CpuRegister.R9] = 0x1FC; + + Assert.True(manager.TryDispatch(OutputSupportNid, context, out _)); + return context[CpuRegister.Rax]; + } +}