From cce9311f34c312ac6d7e8abd1002e0733a122586 Mon Sep 17 00:00:00 2001 From: p3nicillin Date: Mon, 26 Oct 2026 11:09:57 +0000 Subject: [PATCH] fix(libSceVideoOut): answer 0 and UNAVAILABLE_OUTPUT_MODE when the output has no 120 Hz PS5_Vulkan measured on a base PS5 set to 1080p output that sceVideoOutIsOutputSupported(15) answers 0, neither 1 nor an error, and that sceVideoOutConfigureOutput(15) then returns 0x80290019, in a title declaring high-frame-rate output. A host display below 120 Hz answered 0x80290016 to both instead. Titles that branch on the answer being 1 are unchanged; the undeclared-title answer stays 0x80290016. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceVideoOut/include/Output.hpp | 1 + core/libs/prx/libSceVideoOut/src/Output.cpp | 15 +++++++++------ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 2 +- .../prx/libSceVideoOut/tests/OutputModePolicy.cpp | 6 ++++++ core/libs/tests/GuestVideoOutHighFrameRate.cpp | 6 +++--- core/libs/tests/GuestVideoOutOutputMode.cpp | 2 +- docs/dev/debt/libSceVideoOut.md | 2 +- 7 files changed, 22 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index caebc2514a50..1742392ee82d 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -5,6 +5,7 @@ #include "SceTypes.hpp" int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate); +int VideoOutOutputSupport_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate); extern "C" { diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index f80da8452d52..d849980f0254 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -51,10 +51,16 @@ int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputO return 0; } +int VideoOutOutputSupport_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { + const int result = ValidateVideoOutOutputMode_nid_no_patch(mode, options, displayRefreshRate); + if (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && result == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE) return 0; + return result != 0 ? result : 1; +} + static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; - return ValidateVideoOutOutputMode_nid_no_patch(mode, options, VideoOutDriver::Get().GetDisplayRefreshRate()); + return VideoOutOutputSupport_nid_no_patch(mode, options, VideoOutDriver::Get().GetDisplayRefreshRate()); } static void validateOpenParam(const void* param) { @@ -295,11 +301,7 @@ int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) } int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { - const int result = validateOutputConfig(handle, mode, options, reservedPtr, reserved); - if (result != 0) { - return result; - } - return 1; + return validateOutputConfig(handle, mode, options, reservedPtr, reserved); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } @@ -310,6 +312,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO return supported; } if (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && (GetAppAttribute3_nid_no_patch() & 0x40u) == 0) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; + if (supported == 0) return VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index ca0ae05f8aa4..2b4571df617a 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -432,7 +432,7 @@ void testControls() { const auto cfg = VideoOutDriver::Get().GetConfig(handle); check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); const int highFrameRate = sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0); - check(highFrameRate == 1 || highFrameRate == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unexpected high-frame-rate support result"); + check(highFrameRate == 1 || highFrameRate == 0, "unexpected high-frame-rate support result"); check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode was configured"); check(sceVideoOutIsOutputSupported(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode is supported"); check(sceVideoOutConfigureOutput(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode was configured"); diff --git a/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp index 59fafc523575..7f999c654b0b 100644 --- a/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp +++ b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp @@ -26,6 +26,12 @@ int main() { Require(ValidateVideoOutOutputMode_nid_no_patch(mode, &options, 240) == unsupported, "unimplemented output mode was accepted on a fast display"); } Require(ValidateVideoOutOutputMode_nid_no_patch(0xd000000a, &options, 240) == unknownMode, "unknown output mode was accepted"); + for (int refresh : {59, 60, 119, 120, 144}) { + Require(VideoOutOutputSupport_nid_no_patch(15, &options, refresh) == (refresh < 120 ? 0 : 1), "mode 15 support did not answer 0 or 1 for the display"); + Require(VideoOutOutputSupport_nid_no_patch(1, &options, refresh) == 1, "default mode was not supported"); + Require(VideoOutOutputSupport_nid_no_patch(4, &options, refresh) == unsupported, "mode 4 did not stay unsupported"); + Require(VideoOutOutputSupport_nid_no_patch(0xd000000a, &options, refresh) == unknownMode, "unknown mode did not stay unknown"); + } VideoOutOutputOptions changed = options; changed.internalData[0] = 0; Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == invalidValue, "mode 15 accepted invalid output options"); diff --git a/core/libs/tests/GuestVideoOutHighFrameRate.cpp b/core/libs/tests/GuestVideoOutHighFrameRate.cpp index 541436b2364a..82efcf6eebce 100644 --- a/core/libs/tests/GuestVideoOutHighFrameRate.cpp +++ b/core/libs/tests/GuestVideoOutHighFrameRate.cpp @@ -8,7 +8,7 @@ #include #include -static constexpr int Unsupported = static_cast(0x80290016u); +static constexpr int Unavailable = static_cast(0x80290019u); static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); @@ -38,8 +38,8 @@ int main(int argc, char** argv) { return 77; } const int supported = sceVideoOutIsOutputSupported(handle, 15, nullptr, nullptr, 0); - if (supported == Unsupported && !requireDisplay) { - Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == Unsupported, "unsupported display accepted high-frame-rate output"); + if (supported == 0 && !requireDisplay) { + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == Unavailable, "an output without 120 Hz accepted high-frame-rate output"); Require(sceVideoOutClose(handle) == 0, "close failed"); LibcRunShutdown_nid_postfix(); std::puts("skipped, current display does not provide 120 Hz"); diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 7c86474ea989..ccbe2e587940 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -54,7 +54,7 @@ int main() { Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); - Require(highFrameRate == 1 || highFrameRate == UNSUPPORTED_OUTPUT_MODE); + Require(highFrameRate == 1 || highFrameRate == 0); Require(sceVideoOutConfigureOutput(handle, 15, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0x10ull, 0x11ull, 0x13ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index d57e8243d19a..22f6d48df582 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -17,7 +17,7 @@ - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside a retail title), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 +- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above, and otherwise answers 0 while `sceVideoOutConfigureOutput(15)` returns `UNAVAILABLE_OUTPUT_MODE` (0x80290019): [PS5_Vulkan measured](https://github.com/mihawk-99/PS5_Vulkan/blob/f410e67a0241b3e8edcdbcd2a9b285dfc4a412ed/docs/HARDWARE_FINDINGS.md#L3793-L3835) both on a base PS5 whose output was set to 1080p, in a title declaring high-frame-rate output, and 1 and 0 at 1440p. Which code a title without the declaration gets when both apply is not measured; the declaration is checked first here. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display falls back the configured output mode to mode 1, fires the output-mode event and continues at 59.94 Hz. The console's exact fallback behaviour when a 120 Hz display is lost has not been measured; this fallback avoids the abort in #7877 and matches the HDMI output mode renegotiation that a display change requires. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and two others 0x8029001E, the last a PS5 on 13.40 with a 120 Hz TV (measured 2026-10-19 inside a retail title), whose answers for every other mode match the ones here. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels; PPSA26344 at `0xCD0946` passes `(handle, 0xf, nullptr, nullptr, 0)` and branches on the result equalling 1 - [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; a PS5 (firmware 13.40, measured 2026-10-19 inside a retail title) reported its TV's 60.0f. The 48-byte layout, from the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), is confirmed there: full size and pane 3840 x 2160, `refreshRate` 3, `flags` 0 and the rest 0 on the title's handle, 0x8029000B for handles 0 to 8. `flags` is 0 here too; its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutGetOutputStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the status after a buffer is registered, a 1440p output and a display below 4K with the console's resolution on Automatic were not measured. A payload process on a PS5 (firmware 12.70) with no buffer registered reported `resolution` 2 right after `sceVideoOutOpen(0xff, 0, 0, NULL)` with the console's resolution on Automatic on a 4K display and 1 with it set to 1080p, and `dynamicRange` 2 with `flags` 0 on an HDR display with HDR on, while this code sets flag bit 0 whenever it reports dynamic range 2. `resolution` is 2 when the current display mode SDL reports for the window's display is at least 3840 pixels wide or 2160 high and 1 otherwise, read with the refresh rate and independent of the registered buffers; on Wayland SDL's mode is the output's logical size (from SDL's source, not run), so a scaled 3840x2160 output reports 1. `APS5_OUTPUT_RESOLUTION` replaces that size - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. PPSA26344's only call at `0xCCF5EA` passes `(0xFF, 0, 0, nullptr)`. Whether a non-null param continues past offset 24 is also unknown. Seven titles disassembled for this entry: PPSA18194 01.004.003 (0xABA44D) and PPSA21564 01.007.000 (0x2A0E65) pass a stack param with words 16, 0, 0, 1 and affinity 0x3E (PPSA18194) or 16, 1, 256, 1 and affinity 0x1FFF (PPSA21564), and zero its bytes 24 to 88 first. The other nine calls pass no param, as `(0xFF, 0, 0, nullptr)` (PPSA01494 01.100.002 0x26C661A, PPSA03984 01.020.000 0x4FAD792, PPSA21564 0x464783, PPSA30839 01.003.000 0x4D60AB, PPSA31162 02.000.001 0x54D3CB, PPSA34502 01.010.000 0x62AECA) or with a bus type read from a field (PPSA01494 0x2197A25, PPSA30839 0x54C6845, PPSA31162 0x69310F5)