From 33c89d30dc967d4764114d1bca859d497115080d Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 3 Oct 2026 11:15:47 +1300 Subject: [PATCH] fix(videoout): pace the next flip from the vblank a flip was released at processFlip set lastFlipVblank to the vblank count after the host present returned. When the acquire, the wait for an earlier presentation or the present itself crossed the next vblank, the next flip's target moved out by one vblank even when that flip was already queued: a frame ready in 17 ms was shown for 33 ms, and a title that advances its logic by a fixed step per presented frame ran in slow motion. A flip takes effect at the vblank it is released at, so that vblank is now the one the next flip's interval counts from, recorded when the flip is released. Flip events, flipStatus and flipPendingNum are unchanged. Test: video_out_flip_tests pacing (needs a GPU and a display) submits the first flip at flip rate 2, whose presentation opens the device and spans several vblanks, and checks that lastFlipVblank is 3. --- .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 2 +- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 19adde40e..ee5fa36a2 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -394,6 +394,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.cfg->vblankCond.wait(lock, req.cfg->shutdownToken, [&] { return req.cfg->vblankStatus.count >= target || req.cfg->failure || req.cfg->closing; }); timing.Mark("vblank_wait"); checkConfig(*req.cfg); + req.cfg->lastFlipVblank = req.cfg->vblankStatus.count; } require(req.width != 0 && req.height != 0 && req.width <= static_cast(std::numeric_limits::max()) && req.height <= static_cast(std::numeric_limits::max()), "invalid window dimensions"); window.Ensure(req.width, req.height); @@ -444,7 +445,6 @@ void VideoOutDriver::processFlip(FlipRequest& req) { require(req.cfg->flipStatus.count != std::numeric_limits::max(), "flip counter overflow"); triggerEvents(*req.cfg, VIDEO_OUT_EVENT_FLIP, reinterpret_cast(req.flipArg)); ++req.cfg->flipStatus.count; - req.cfg->lastFlipVblank = req.cfg->vblankStatus.count; req.cfg->flipStatus.processTime = sceKernelGetProcessTime(); req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); req.cfg->flipStatus.flipArg = req.flipArg; diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 3949f3575..ba4c375fa 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -273,6 +273,31 @@ void testBackToBack() { LibcRunShutdown_nid_postfix(); } +void testReleaseVblank() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(6 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + fillBuffer(storage, 259, 137); + VideoOutBuffers buffer{storage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); + sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, 0, nullptr); + check(sceVideoOutSetFlipRate(handle, 2) == 0, "flip rate was not applied"); + sceVideoOutSubmitFlip(handle, 0, 1, 1); + { + std::unique_lock lock(cfg->mutex); + const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(done, "the flip did not complete"); + check(cfg->vblankStatus.count > 3, "the first presentation did not outlast the vblank it was released at"); + check(cfg->lastFlipVblank == 3, "the next flip is not paced from the vblank this flip was released at"); + } + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + } int main(int argc, char** argv) { @@ -281,6 +306,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "controls") testControls(); else if (argc == 2 && std::string(argv[1]) == "present") testPresentation(false); else if (argc == 2 && std::string(argv[1]) == "backtoback") testBackToBack(); + else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testPresentation(true); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed");