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) <[email protected]>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-09-30 09:06:30 -06:00
1 parent f26ca37dad
commit 02b2b7b699
2 files changed
+44 -12

No files matched your search

+43 -12
View File
@@ -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<SceUID> 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);
+1
View File
@@ -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",