diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 6b9a38c8338..a6eebad1fb8 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -72,7 +72,7 @@ if(BUILD_TESTING) "SDL video driver for VideoOut flip tests") set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened flipmaster) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 0c946149c5b..267a832a3a3 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -7,15 +7,7 @@ extern "C" { -int APS5_VABI sceVideoOutSetFlipMaster() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceVideoOutClearFlipMaster() { - NotImplemented_nid_no_patch(__func__); - return 0; -} int APS5_VABI sceVideoOutGetOutputStatusWithVrr(int handle, void* status) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index 4ecf9134176..6b70c51bcd5 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -30,6 +30,8 @@ int APS5_VABI sceVideoOutColorSettingsSetGamma(VideoOutColorSettings* settings, int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* settings); int APS5_VABI sceVideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t arg2); int APS5_VABI sceVideoOutVrrUnpegFromFixedRate(int handle); +int APS5_VABI sceVideoOutSetFlipMaster(int slaveHandle, int masterHandle); +int APS5_VABI sceVideoOutClearFlipMaster(int slaveHandle); } diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 8cfcc7299c0..2610129028e 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -39,6 +39,7 @@ static constexpr int VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE = -2144796650; static constexpr int VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE = -2144796647; static constexpr int VIDEO_OUT_ERROR_INVALID_EVENT = -2144796659; static constexpr int VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE = -2144796642; +static constexpr int VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION = -2144796644; static constexpr int VIDEO_OUT_BUS_TYPE_MAIN = 0; static constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; @@ -134,6 +135,8 @@ struct VideoOutConfig { std::chrono::steady_clock::time_point lastTimingFlip{}; uint64_t outputMode = VIDEO_OUT_OUTPUT_MODE_DEFAULT; float gamma = 1.0f; + int flipMaster = 0; + std::uint32_t flipSlaves = 0; VideoOutFlipStatus flipStatus{}; VideoOutVblankStatus vblankStatus{}; @@ -217,6 +220,8 @@ class VideoOutDriver { bool HasConfig(int handle); bool SupportsHighFrameRate(); int GetDisplayRefreshRate(); + int SetFlipMaster(int slaveHandle, int masterHandle); + int ClearFlipMaster(int slaveHandle); // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 82b62b6c24f..6af52826756 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -190,6 +190,9 @@ int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flipMode, int64_t if (cfg->closing) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); } + if (cfg->flipMaster != 0 || cfg->flipSlaves != 0) { + throw std::runtime_error(std::string(__func__) + ": flips on a port tied by sceVideoOutSetFlipMaster are not modelled"); + } const bool special = (index == VIDEO_OUT_BUFFER_INDEX_BLANK || index == VIDEO_OUT_BUFFER_INDEX_BLACK); if (!special && !cfg->buffers[index].Occupied()) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_INDEX"); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index f47aac9f3ef..b55f8ee8b56 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -332,6 +332,18 @@ int APS5_VABI sceVideoOutVrrPegToFixedRate(int handle, uint64_t arg1, uint64_t a LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutSetFlipMaster(int slaveHandle, int masterHandle) try { + return VideoOutDriver::Get().SetFlipMaster(slaveHandle, masterHandle); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutClearFlipMaster(int slaveHandle) try { + return VideoOutDriver::Get().ClearFlipMaster(slaveHandle); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + int APS5_VABI sceVideoOutAddVrrStatusFlagsPrivilege() { return 0; } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 5b3dcdc23b9..3eb03efd330 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -147,6 +147,7 @@ class VideoOutput final : public AgcDriver::IVideoOutput { if (queue->stopping || cfg->shutdownToken.stop_requested()) throw ProcessShutdown{}; std::lock_guard lock(cfg->mutex); checkConfig(*cfg); + require(cfg->flipMaster == 0 && cfg->flipSlaves == 0, "flips on a port tied by sceVideoOutSetFlipMaster are not modelled"); request->generation = cfg->generation; request->flipRate = cfg->flipRate; if (info.index >= 0) request->reuseTicket = cfg->bufferReuse[info.index].Reserve(); @@ -357,6 +358,10 @@ int VideoOutDriver::Open(int busType) { bool VideoOutDriver::Close(int handle) { std::lock_guard lock(mutex); if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; + if (handle > 0 && handle < VIDEO_OUT_NUM_MAX && contexts[handle] != nullptr) { + std::lock_guard cfgLock(contexts[handle]->mutex); + require(contexts[handle]->flipSlaves == 0, "closing a flip master that still has slave ports is not modelled"); + } return close(handle); } @@ -365,6 +370,12 @@ bool VideoOutDriver::close(int handle) { AgcDriverUnregisterVideoOutput_nid_postfix(static_cast(handle), outputs[handle]); outputs[handle].reset(); auto cfg = contexts[handle]; + if (cfg->flipMaster != 0) { + auto& master = *contexts[cfg->flipMaster]; + std::scoped_lock tieLock(cfg->mutex, master.mutex); + cfg->flipMaster = 0; + --master.flipSlaves; + } std::lock_guard cfgLock(cfg->mutex); cfg->opened = false; cfg->closing = true; @@ -418,6 +429,55 @@ bool VideoOutDriver::HasConfig(int handle) { return cfg->opened && !cfg->closing; } +int VideoOutDriver::SetFlipMaster(int slaveHandle, int masterHandle) { + std::lock_guard lock(mutex); + { + std::lock_guard queueLock(flipQueue->mutex); + if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); + if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; + } + if (slaveHandle <= 0 || slaveHandle >= VIDEO_OUT_NUM_MAX || contexts[slaveHandle] == nullptr) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (masterHandle <= 0 || masterHandle >= VIDEO_OUT_NUM_MAX || contexts[masterHandle] == nullptr) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto& slave = *contexts[slaveHandle]; + auto& master = *contexts[masterHandle]; + if (slaveHandle == masterHandle) { + std::lock_guard cfgLock(slave.mutex); + slave.CheckAlive(); + return slave.Closed() ? VIDEO_OUT_ERROR_INVALID_HANDLE : VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION; + } + std::scoped_lock cfgLock(slave.mutex, master.mutex); + slave.CheckAlive(); + master.CheckAlive(); + if (slave.Closed() || master.Closed()) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (slave.flipMaster != 0 || slave.flipSlaves != 0 || master.flipMaster != 0) return VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION; + if (slave.flipStatus.flipPendingNum != 0 || master.flipStatus.flipPendingNum != 0) return VIDEO_OUT_ERROR_RESOURCE_BUSY; + slave.flipMaster = masterHandle; + ++master.flipSlaves; + return 0; +} + +int VideoOutDriver::ClearFlipMaster(int slaveHandle) { + std::lock_guard lock(mutex); + { + std::lock_guard queueLock(flipQueue->mutex); + if (flipQueue->failure) std::rethrow_exception(flipQueue->failure); + if (flipQueue->stopping || LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; + } + if (slaveHandle <= 0 || slaveHandle >= VIDEO_OUT_NUM_MAX || contexts[slaveHandle] == nullptr) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto& slave = *contexts[slaveHandle]; + { + std::lock_guard cfgLock(slave.mutex); + slave.CheckAlive(); + if (slave.Closed()) return VIDEO_OUT_ERROR_INVALID_HANDLE; + require(slave.flipMaster != 0, "sceVideoOutClearFlipMaster on a port without a flip master: the console's answer is unknown"); + } + auto& master = *contexts[slave.flipMaster]; + std::scoped_lock cfgLock(slave.mutex, master.mutex); + slave.flipMaster = 0; + --master.flipSlaves; + return 0; +} + int VideoOutDriver::SubmitFlip(int handle, int index, int flipMode, int64_t flipArg) { if (LibcShutdownToken_nid_postfix().stop_requested()) throw ProcessShutdown{}; // A title that does not pace on flipPendingNum can run ahead of the presenter now that the queue diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b90bbc768ad..30ba9a55ee8 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -480,6 +480,68 @@ void testOverlay() { LibcRunShutdown_nid_postfix(); } +void testFlipMaster() { + const int mainHandle = sceVideoOutOpen(255, 0, 0, nullptr); + const int overlayHandle = sceVideoOutOpen(255, 1, 0, nullptr); + auto mainCfg = VideoOutDriver::Get().GetConfig(mainHandle); + auto overlayCfg = VideoOutDriver::Get().GetConfig(overlayHandle); + std::vector allocation(65536 + 65535); + const auto storage = alignedBuffer(allocation); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 64, 64, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(mainHandle, 0, 0, &buffer, 1, &attribute, 0, nullptr) == 0, "buffer registration failed"); + const auto waitFlips = [&](std::uint64_t count) { + std::unique_lock lock(mainCfg->mutex); + const bool done = mainCfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return mainCfg->failure || mainCfg->flipStatus.count == count; }); + if (mainCfg->failure) std::rethrow_exception(mainCfg->failure); + check(done && mainCfg->flipStatus.flipPendingNum == 0, "the flip did not complete"); + }; + const auto tied = [](const std::shared_ptr& cfg, int master, std::uint32_t slaves) { + std::lock_guard lock(cfg->mutex); + return cfg->flipMaster == master && cfg->flipSlaves == slaves; + }; + for (int invalid : {-1, 0, 3, VIDEO_OUT_NUM_MAX, 0x7fffffff}) { + check(sceVideoOutSetFlipMaster(invalid, mainHandle) == VIDEO_OUT_ERROR_INVALID_HANDLE, "an invalid slave handle was tied"); + check(sceVideoOutSetFlipMaster(overlayHandle, invalid) == VIDEO_OUT_ERROR_INVALID_HANDLE, "an invalid master handle was tied"); + check(sceVideoOutClearFlipMaster(invalid) == VIDEO_OUT_ERROR_INVALID_HANDLE, "an invalid handle was untied"); + } + check(sceVideoOutSetFlipMaster(mainHandle, mainHandle) == VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION, "a port became its own flip master"); + check(expectFailure([&] { sceVideoOutClearFlipMaster(mainHandle); }).find("without a flip master") != std::string::npos, "a port without a flip master was untied"); + check(tied(mainCfg, 0, 0) && tied(overlayCfg, 0, 0), "a rejected call changed the tie"); + + auto gate = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, gate); + std::array held{0xc004105c, 7, 0xfffffffeu, 1, 0, 0}; + Packet heldPacket{held.data(), 6, 0, {}}; + check(sceAgcDriverSubmitDcb(&heldPacket) == 0, "the gated flip was not accepted"); + check(sceVideoOutSubmitFlip(mainHandle, 0, 1, 1) == 0, "the flip was not accepted"); + check(sceVideoOutSetFlipMaster(overlayHandle, mainHandle) == VIDEO_OUT_ERROR_RESOURCE_BUSY, "a master with a pending flip was tied"); + gate->Release(); + waitFlips(1); + AgcDriverUnregisterVideoOutput_nid_postfix(7, gate); + + check(sceVideoOutSetFlipMaster(overlayHandle, mainHandle) == 0, "the overlay port was not tied to the main port"); + check(tied(overlayCfg, mainHandle, 0) && tied(mainCfg, 0, 1), "the tie was not recorded"); + check(sceVideoOutSetFlipMaster(overlayHandle, mainHandle) == VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION, "a slave port was tied twice"); + check(sceVideoOutSetFlipMaster(mainHandle, overlayHandle) == VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION, "a master port became a slave"); + check(expectFailure([&] { sceVideoOutSubmitFlip(mainHandle, 0, 1, 2); }).find("sceVideoOutSetFlipMaster") != std::string::npos, "a flip on a tied port was accepted"); + check(expectFailure([&] { sceVideoOutClose(mainHandle); }).find("flip master") != std::string::npos, "a master with a slave port was closed"); + check(sceVideoOutClearFlipMaster(overlayHandle) == 0, "the tie was not cleared"); + check(tied(overlayCfg, 0, 0) && tied(mainCfg, 0, 0), "the tie was not removed"); + check(sceVideoOutSubmitFlip(mainHandle, 0, 1, 3) == 0, "the flip after clearing the tie was not accepted"); + waitFlips(2); + + check(sceVideoOutSetFlipMaster(overlayHandle, mainHandle) == 0, "the overlay port was not tied again"); + check(sceVideoOutClose(overlayHandle) == 0, "the slave port was not closed"); + check(tied(mainCfg, 0, 0), "closing the slave port kept the tie"); + check(sceVideoOutSubmitFlip(mainHandle, 0, 1, 4) == 0, "the flip after closing the slave port was not accepted"); + waitFlips(3); + sceVideoOutUnregisterBuffers(mainHandle, 0); + check(sceVideoOutClose(mainHandle) == 0, "the master port was not closed"); + LibcRunShutdown_nid_postfix(); +} + void testUnavailable() { const auto message = expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); }); check(expectFailure([] { sceVideoOutOpen(255, 0, 0, nullptr); }) == message, "a second open lost the presentation failure"); @@ -725,6 +787,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); + else if (argc == 2 && std::string(argv[1]) == "flipmaster") testFlipMaster(); else if (argc == 2 && std::string(argv[1]) == "deferred") testDeferredBuffer(); else if (argc == 2 && std::string(argv[1]) == "unregistered") testUnregisteredBuffer(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index aab10875a0a..7c3c01dcb1c 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -6,7 +6,7 @@ ### Unknown function info -- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2), [sceVideoOutSetFlipMaster and sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut) +- PPSA29866 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero - PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index a20f0876afb..66ac8745a68 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -23,3 +23,4 @@ - [Overlay output bus](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [Mouse capture and delta scaling](../../../core/libs/prx/libSceVideoOut/src/MouseInput.cpp) (libSceVideoOut) - the console's mouse reports relative motion only, so a host cursor can never coincide with the title's cursor; while a title has a mouse open and the window is focused the host cursor is hidden and captured (SDL relative mode, released on focus loss or while the pad's mouse-to-stick toggle is on). Motion deltas are scaled from the presented image's size in window units to the title's display resolution with sub-pixel carry; the console's deltas are counts, so a title that applies its own sensitivity moves its cursor that much faster than the host pointer would have. The scale takes SDL's relative motion in logical window units, the units of `SDL_GetWindowSize`; under Windows DPI scaling SDL may report it in physical pixels, which is unverified (no Windows run yet), and a drawable larger than the window does not change the scale. +- [sceVideoOutSetFlipMaster, sceVideoOutClearFlipMaster](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - grouped master/slave flips (modes 8 and 9) are not modelled, so a flip on a tied port throws, from `sceVideoOutSubmitFlip` and from a command-buffer flip