From 151fec7fd67b54cdeaf133cd38eca9a595e68490 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Fri, 2 Oct 2026 11:50:15 +1300 Subject: [PATCH] fix(videoout): read the open param as the service thread's priority and affinity and throws for any non-zero word after it. Its layout matches shadPS4's ServiceThreadParams instead: an unknown first word (16), a set-priority flag and a priority (256..767), a set-affinity flag and a 64-bit CPU mask at offset 16. PPSA21564 passes 16, 1, 0x100, 1 and 0x1FFF, so it stops at startup on main. sceVideoOutOpen now validates each field and reads the mask only when its flag is set, so a 16-byte param with both flags clear still works. The priority and affinity are not applied, since the present and vblank threads are host threads; this and the unknown first word are listed in the technical debt. --- .../libSceVideoOut/include/VideoOutDriver.hpp | 16 ++++++++- core/libs/prx/libSceVideoOut/src/Output.cpp | 33 +++++++++++------ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 36 +++++++++++++++---- docs/dev/TechnicalDebt.md | 2 ++ 4 files changed, 69 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index c79e4169b..ea75f8d80 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -37,10 +38,23 @@ static constexpr int VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE = -2144796647; static constexpr int VIDEO_OUT_ERROR_INVALID_EVENT = -2144796659; static constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0; -static constexpr std::uint32_t VIDEO_OUT_OPEN_PARAM_SIZE = 16; static constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; static constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2; +static constexpr std::uint32_t VIDEO_OUT_OPEN_PARAM_FIRST_WORD = 16; +static constexpr std::int32_t VIDEO_OUT_SERVICE_THREAD_PRIORITY_HIGHEST = 256; +static constexpr std::int32_t VIDEO_OUT_SERVICE_THREAD_PRIORITY_LOWEST = 767; +static constexpr std::uint64_t VIDEO_OUT_SERVICE_THREAD_AFFINITY_ALL = 0x1FFF; + +struct VideoOutOpenParam { + std::uint32_t firstWord; + std::uint32_t setPriority; + std::int32_t priority; + std::uint32_t setAffinity; + std::uint64_t affinity; +}; +static_assert(offsetof(VideoOutOpenParam, affinity) == 16); + static constexpr int VIDEO_OUT_BUFFER_NUM_MAX = 16; static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX = 4; static constexpr int VIDEO_OUT_NUM_MAX = 4; diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 598dc08c3..c595e3022 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -30,19 +31,31 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO return 0; } +static void validateOpenParam(const void* param) { + if (param == nullptr) return; + VideoOutOpenParam openParam{}; + std::memcpy(&openParam, param, offsetof(VideoOutOpenParam, affinity)); + if (openParam.firstWord != VIDEO_OUT_OPEN_PARAM_FIRST_WORD) { + throw std::runtime_error(std::string(__func__) + ": unsupported first word"); + } + if (openParam.setPriority > 1 || openParam.setAffinity > 1) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); + } + if (openParam.setPriority == 1 && (openParam.priority < VIDEO_OUT_SERVICE_THREAD_PRIORITY_HIGHEST || openParam.priority > VIDEO_OUT_SERVICE_THREAD_PRIORITY_LOWEST)) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); + } + if (openParam.setAffinity == 1) { + std::memcpy(&openParam.affinity, static_cast(param) + offsetof(VideoOutOpenParam, affinity), sizeof(openParam.affinity)); + if (openParam.affinity == 0 || (openParam.affinity & ~VIDEO_OUT_SERVICE_THREAD_AFFINITY_ALL) != 0) { + throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); + } + } +} + extern "C" { int APS5_VABI sceVideoOutOpen(int userId, int busType, int index, const void* param) try { - if (param != nullptr) { - std::array words{}; - std::memcpy(words.data(), param, sizeof(words)); - if (words[0] != VIDEO_OUT_OPEN_PARAM_SIZE) { - throw std::runtime_error(std::string(__func__) + ": unsupported param size"); - } - if (std::any_of(words.begin() + 1, words.end(), [](std::uint32_t word) { return word != 0; })) { - throw std::runtime_error(std::string(__func__) + ": param options not implemented"); - } - } + validateOpenParam(param); if (userId != 255 && userId != 0) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_VALUE"); } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 3949f3575..59c0b9d9e 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -159,15 +159,37 @@ void testDecode() { expectFailure([&] { AgcDriver::DisplayBufferSize(buffer); }); } +void testOpenParam() { + using Words = std::array; + constexpr auto first = VIDEO_OUT_OPEN_PARAM_FIRST_WORD; + for (const Words& rejected : { + Words{24, 0, 0, 0, 0, 0}, + Words{first, 2, 0, 0, 0, 0}, + Words{first, 0, 0, 0x101, 0, 0}, + Words{first, 1, 255, 0, 0, 0}, + Words{first, 1, 768, 0, 0, 0}, + Words{first, 0, 0, 1, 0, 0}, + Words{first, 0, 0, 1, 0x2000, 0}, + Words{first, 0, 0, 1, 0, 1}, + }) { + expectFailure([&] { sceVideoOutOpen(255, 0, 0, rejected.data()); }); + } + std::array zeroed{first, 0, 0, 0}; + const auto zeroedHandle = sceVideoOutOpen(255, 0, 0, zeroed.data()); + check(zeroedHandle >= 0, "open with a zeroed param failed"); + sceVideoOutClose(zeroedHandle); + for (const Words& accepted : {Words{first, 1, 767, 0, 0, 0}, Words{first, 0, 0, 1, 1, 0}}) { + const auto handle = sceVideoOutOpen(255, 0, 0, accepted.data()); + check(handle >= 0, "open with a valid service thread setting failed"); + sceVideoOutClose(handle); + } +} + void testControls() { - std::array openParam{VIDEO_OUT_OPEN_PARAM_SIZE, 0, 0, 0}; + testOpenParam(); + const std::array openParam{VIDEO_OUT_OPEN_PARAM_FIRST_WORD, 1, 0x100, 1, 0x1FFF}; const auto handle = sceVideoOutOpen(255, 0, 0, openParam.data()); - check(handle >= 0, "open with a zeroed param failed"); - openParam[0] = 24; - expectFailure([&] { sceVideoOutOpen(255, 0, 0, openParam.data()); }); - openParam[0] = VIDEO_OUT_OPEN_PARAM_SIZE; - openParam[2] = 1; - expectFailure([&] { sceVideoOutOpen(255, 0, 0, openParam.data()); }); + check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); for (int rate = 0; rate <= 2; ++rate) { check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7edcb5578..fc40f9508 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -15,10 +15,12 @@ Throughout the project, every function at every stage either **does exactly what - [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls do nothing +- [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling - The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). ### Unknown function info +- [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. Whether the param continues past offset 24 is also unknown - [AMPR WaitOnAddress / WaitOnCounter](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - compare encoding assumed to be the WAIT_REG_MEM one (0 always, 1 <, 2 <=, 3 ==, 4 !=, 5 >=, 6 >) - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature