From f51180973ea6d8d8d48735682647876ecadfaf2f Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:22:39 +0200 Subject: [PATCH] feat(libSceAudioOut): implement sceAudioOutSysGetHdmiMonitorInfo Surround Sound Studio (PPSA99051), catalog build on main a11c8f7f with the flat-interpolation fix, stops 16 s in, before its first frame (orishamir, #3581 comment 6109839314): sceAudioOutSysGetHdmiMonitorInfo not implemented The call takes (type, out, size); out must be non-null, size exactly 0x180 and type 1 (HDMI) or 0x10 (sainsaji/PS5-Audio-Passthrough-Research c3be9c2a, docs/FINDINGS.md section 3 and AGENTS.md:25, firmware 12.70). Titles read the monitor name at +0x0B, the descriptor count at +0x94 and 8-byte descriptors from +0x98 (same, FINDINGS.md hardware log item 1 and sample-app/src/platform/ps5/bitstream_out.cpp:92-128). Audio reaches the host through SDL as PCM and there is no EDID, so type 1 with a 0x180-byte buffer reports a sink without a name or descriptors: the buffer is zeroed and 0 is returned. Surround Sound Studio's Passthrough page then lists no formats; EVO Player's code treats an empty list like a negative return. A null buffer returns 0x80260004, as the other AudioOut calls do. Type 0x10, other types and other sizes throw: their results and error codes are not known. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 +++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 16 ++++- .../tests/AudioOutSysHdmiMonitorInfo.cpp | 71 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 +- 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 968a17129..bc11e8e5b 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -57,6 +57,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out_sys_hdmi_monitor_info_tests tests/AudioOutSysHdmiMonitorInfo.cpp) + target_include_directories(audio_out_sys_hdmi_monitor_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_sys_hdmi_monitor_info_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_sys_hdmi_monitor_info_tests) + add_test(NAME audio_out_sys_hdmi_monitor_info COMMAND audio_out_sys_hdmi_monitor_info_tests) + if(WIN32) + set_tests_properties(audio_out_sys_hdmi_monitor_info PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(audio_out2_latency_tests tests/AudioOut2Latency.cpp) target_include_directories(audio_out2_latency_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_latency_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index ddddb77fb..c8adb77b2 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,8 @@ static constexpr std::uint32_t FORMAT_MASK = 0xFFu; static constexpr std::uint64_t TARGET_LATENCY_US = 40000; static constexpr std::uint64_t DRAIN_TIMEOUT_US = 200000; static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; +static constexpr int MONITOR_TYPE_HDMI = 1; +static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; enum class Format { Unknown, @@ -506,8 +509,17 @@ int APS5_VABI sceAudioOutSysConfigureOutput() { return 0; } -int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int type, void* info, std::uint32_t size) { + if (type != MONITOR_TYPE_HDMI) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: monitor type " + std::to_string(type) + " not supported"); + } + if (size != HDMI_MONITOR_INFO_SIZE) { + throw std::runtime_error("sceAudioOutSysGetHdmiMonitorInfo: size " + std::to_string(size) + " not supported"); + } + if (info == nullptr) { + return -2144993276; + } + std::memset(info, 0, HDMI_MONITOR_INFO_SIZE); return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp new file mode 100644 index 000000000..ce23a664d --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSysHdmiMonitorInfo.cpp @@ -0,0 +1,71 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutSysGetHdmiMonitorInfo(int, void*, std::uint32_t); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int monitorTypeHdmi = 1; +constexpr std::uint32_t infoSize = 0x180; +constexpr std::size_t descriptorCountOffset = 0x94; +constexpr std::size_t guardSize = 16; +constexpr std::uint8_t untouched = 0xA5; +constexpr int invalidPointer = static_cast(0x80260004); + +bool Throws(int type, std::uint32_t size, const std::string& expected) { + std::array info{}; + try { + sceAudioOutSysGetHdmiMonitorInfo(type, info.data(), size); + } catch (const std::runtime_error& error) { + if (std::string(error.what()) == expected) return true; + std::fprintf(stderr, "threw: %s\n", error.what()); + return false; + } + return false; +} + +} + +int main() { + std::array buffer{}; + buffer.fill(untouched); + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, buffer.data(), infoSize) == 0, + "an HDMI query with a 0x180-byte buffer must succeed"); + for (std::uint32_t i = 0; i < infoSize; i++) { + Require(buffer[i] == 0, "the monitor info must describe a sink without a name or audio descriptors"); + } + std::uint32_t descriptors = 0xFFFFFFFFu; + std::memcpy(&descriptors, buffer.data() + descriptorCountOffset, sizeof(descriptors)); + Require(descriptors == 0, "the audio descriptor count must be 0"); + for (std::size_t i = infoSize; i < buffer.size(); i++) { + Require(buffer[i] == untouched, "nothing past 0x180 bytes may be written"); + } + + Require(sceAudioOutSysGetHdmiMonitorInfo(monitorTypeHdmi, nullptr, infoSize) == invalidPointer, + "a null buffer must report an invalid pointer"); + + Require(Throws(monitorTypeHdmi, infoSize - 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 383 not supported"), "a short buffer must throw"); + Require(Throws(monitorTypeHdmi, infoSize + 1, + "sceAudioOutSysGetHdmiMonitorInfo: size 385 not supported"), "a long buffer must throw"); + Require(Throws(0x10, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 16 not supported"), "monitor type 0x10 must throw"); + Require(Throws(0, infoSize, + "sceAudioOutSysGetHdmiMonitorInfo: monitor type 0 not supported"), "monitor type 0 must throw"); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..a6f817c92 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -208,7 +208,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysGetHdmiMonitorInfo, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysGetHdmiMonitorInfo` to read the formats the TV or receiver lists ([bitstream.cpp](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/bitstream.cpp#L71)) and `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI sink to describe or switch +- [sceAudioOutSysClose, sceAudioOutSysConfigureOutput, sceAudioOutSysOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; added without an implementation and declared without parameters. [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L166-L240), whose audio code both titles share, calls `sceAudioOutSysOpen`, `sceAudioOutSysConfigureOutput` and `sceAudioOutSysClose` only to open a port for HDMI bitstream passthrough. Its declared arguments are not confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no HDMI output to switch - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order - [sceAudioOut2PortCreate](../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped @@ -518,6 +518,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [AudioOut2 contexts](../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [sceAudioOutSysGetHdmiMonitorInfo](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - [ATRAC9 gapless segments](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - a run job stops after the gapless total of output samples counted from the start of the segment, leaving the rest of its input unconsumed, and the next run starts the next segment with the decoder as it is and without a second skip. FMOD in PPSA29141 sets a total shorter than a sound's frames and reads the frames after it on later runs; with the output held at the total its stream reader waited for them forever and the last buffer repeated. The console's handling of the frames after the total is not documented; a reset (`SetGaplessDecode` with the reset flag, `ClearContext`, initialization) starts a new segment. An ATRAC9 run job without `RUN_MULTIPLE_FRAMES` decodes its whole input as one with it: FMOD in PPSA29141 puts two run jobs in one batch with the second's input right after the whole of the first's. Whether the console stops after one frame there is not verified; MP3 and Opus run jobs still stop after one frame