From 02b2b7b6997b1c5f1dd0b3170b40ee585bdafd54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 29 Sep 2026 13:28:57 -0600 Subject: [PATCH] sceDisplay: Release vblank waiters ~50us after the vblank On hardware a thread waiting for vblank returns ~53us after it, where we had it back in ~5us, and the first of four waiters runs after ~85us: each waiter beyond the first adds ~9us (pspautotests threads/scheduling/ vblankwake). The waiters are now released by a separate event 48us + 9us per extra waiter after the vblank. Which vblank a wait is for is still decided at the vblank, so a thread that starts waiting in between still waits a whole frame (sceDisplay section version 8). Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/HLE/sceDisplay.cpp | 55 ++++++++++++++++++++++++++++++++--------- test.py | 1 + 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Core/HLE/sceDisplay.cpp b/Core/HLE/sceDisplay.cpp index 49a78a14b6..fbdc423bb2 100644 --- a/Core/HLE/sceDisplay.cpp +++ b/Core/HLE/sceDisplay.cpp @@ -93,6 +93,9 @@ static bool framebufIsLatched; static int enterVblankEvent = -1; static int leaveVblankEvent = -1; +// Threads whose vblank wait is over, released a little after the vblank (see hleEnterVblank). +static int vblankWakeEvent = -1; +static std::vector vblankWakePending; static int afterFlipEvent = -1; static int lagSyncEvent = -1; @@ -156,6 +159,7 @@ static u64 nextFlipCycles = 0; void hleEnterVblank(u64 userdata, int cyclesLate); void hleLeaveVblank(u64 userdata, int cyclesLate); +static void hleVblankWake(u64 userdata, int cyclesLate); void hleAfterFlip(u64 userdata, int cyclesLate); void hleLagSync(u64 userdata, int cyclesLate); @@ -210,6 +214,8 @@ void __DisplayInit() { enterVblankEvent = CoreTiming::RegisterEvent("EnterVBlank", &hleEnterVblank); leaveVblankEvent = CoreTiming::RegisterEvent("LeaveVBlank", &hleLeaveVblank); + vblankWakeEvent = CoreTiming::RegisterEvent("VBlankWake", &hleVblankWake); + vblankWakePending.clear(); afterFlipEvent = CoreTiming::RegisterEvent("AfterFlip", &hleAfterFlip); lagSyncEvent = CoreTiming::RegisterEvent("LagSync", &hleLagSync); @@ -231,7 +237,7 @@ struct GPUStatistics_v0 { }; void __DisplayDoState(PointerWrap &p) { - auto s = p.Section("sceDisplay", 1, 7); + auto s = p.Section("sceDisplay", 1, 8); if (!s) return; @@ -258,6 +264,14 @@ void __DisplayDoState(PointerWrap &p) { CoreTiming::RestoreRegisterEvent(leaveVblankEvent, "LeaveVBlank", &hleLeaveVblank); Do(p, afterFlipEvent); CoreTiming::RestoreRegisterEvent(afterFlipEvent, "AfterFlip", &hleAfterFlip); + if (s >= 8) { + Do(p, vblankWakeEvent); + Do(p, vblankWakePending); + } else { + vblankWakeEvent = -1; + vblankWakePending.clear(); + } + CoreTiming::RestoreRegisterEvent(vblankWakeEvent, "VBlankWake", &hleVblankWake); if (s >= 5) { Do(p, lagSyncEvent); @@ -316,6 +330,7 @@ void __DisplayDoState(PointerWrap &p) { void __DisplayShutdown() { vblankWaitingThreads.clear(); + vblankWakePending.clear(); } void __DisplayVblankBeginCallback(SceUID threadID, SceUID prevCallbackId) { @@ -545,22 +560,21 @@ void hleEnterVblank(u64 userdata, int cyclesLate) { // Trigger VBlank interrupt handlers. __TriggerInterrupt(PSP_INTR_IMMEDIATE | PSP_INTR_ONLY_IF_ENABLED | PSP_INTR_ALWAYS_RESCHED, PSP_VBLANK_INTR, PSP_INTR_SUB_ALL); - // Wake up threads waiting for VBlank - u32 error; - bool wokeThreads = false; + // Threads waiting for this vblank are released about 48us after it, plus ~9us for each one + // beyond the first: on hardware a lone waiter returns ~53us after the vblank (~58us with a + // vblank handler registered), and with four the first to run does so after ~85us, where we'd + // otherwise have it back in ~5us (pspautotests threads/scheduling/vblankwake). Which vblank a + // wait is for is still decided here, so a thread that starts waiting in between waits for the + // next one. for (size_t i = 0; i < vblankWaitingThreads.size(); i++) { if (--vblankWaitingThreads[i].vcountUnblock == 0) { - // Only wake it if it wasn't already released by someone else. - SceUID waitID = __KernelGetWaitID(vblankWaitingThreads[i].threadID, WAITTYPE_VBLANK, error); - if (waitID == 1) { - __KernelResumeThreadFromWait(vblankWaitingThreads[i].threadID, 0); - wokeThreads = true; - } + vblankWakePending.push_back(vblankWaitingThreads[i].threadID); vblankWaitingThreads.erase(vblankWaitingThreads.begin() + i--); } } - if (wokeThreads) { - __KernelReSchedule("entered vblank"); + if (!vblankWakePending.empty()) { + const int releaseUs = 48 + 9 * ((int)vblankWakePending.size() - 1); + CoreTiming::ScheduleEvent(usToCycles(releaseUs) - cyclesLate, vblankWakeEvent, 0); } // We use the emulation timebase here, for auto movements to be smooth as seen from the game. @@ -756,6 +770,23 @@ void hleAfterFlip(u64 userdata, int cyclesLate) { } } +static void hleVblankWake(u64 userdata, int cyclesLate) { + u32 error; + bool wokeThreads = false; + for (SceUID threadID : vblankWakePending) { + // Only wake it if it wasn't already released by someone else. + SceUID waitID = __KernelGetWaitID(threadID, WAITTYPE_VBLANK, error); + if (waitID == 1) { + __KernelResumeThreadFromWait(threadID, 0); + wokeThreads = true; + } + } + vblankWakePending.clear(); + if (wokeThreads) { + __KernelReSchedule("vblank waiters released"); + } +} + void hleLeaveVblank(u64 userdata, int cyclesLate) { flippedThisFrame = false; VERBOSE_LOG(Log::sceDisplay,"Leave VBlank %i", (int)userdata - 1); diff --git a/test.py b/test.py index 78e41f170d..97534a33fa 100755 --- a/test.py +++ b/test.py @@ -396,6 +396,7 @@ tests_good = [ "threads/scheduling/delayzero", "threads/scheduling/dispatchwake", "threads/scheduling/mutexhandoff", + "threads/scheduling/vblankwake", "threads/scheduling/readyqueue", "threads/scheduling/waittimeouts", "threads/semaphores/semaphores",