From 10f57dea29b6e53c813de29cd9f05b99000fb477 Mon Sep 17 00:00:00 2001 From: MathiasDevelopes <25400788+MathiasDevelopes@users.noreply.github.com> Date: Thu, 8 Oct 2026 19:59:34 +0200 Subject: [PATCH] feat(libSceVideoOut): deliver pre-vblank start events sceVideoOutAddPreVblankStartEvent threw "physical pre-vblank timing is not implemented", so titles that pace on it stopped at startup. It now registers the event like the vblank event does, and each emulated vblank tick triggers the pre-vblank start events just before the vblank events, with the pre-vblank count as data. The console's lead time is unknown. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceVideoOut/src/Event.cpp | 5 +---- core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp | 4 ++++ core/libs/prx/libSceVideoOut/tests/Flip.cpp | 7 +++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/src/Event.cpp b/core/libs/prx/libSceVideoOut/src/Event.cpp index c70ba7557..3ce96e7b3 100644 --- a/core/libs/prx/libSceVideoOut/src/Event.cpp +++ b/core/libs/prx/libSceVideoOut/src/Event.cpp @@ -120,10 +120,7 @@ int APS5_VABI sceVideoOutAddVblankEvent(KernelEqueue eq, int handle, void* udata } int APS5_VABI sceVideoOutAddPreVblankStartEvent(KernelEqueue eq, int handle, void* udata) try { - static_cast(eq); - static_cast(udata); - VideoOutDriver::Get().GetConfig(handle); - throw std::runtime_error("VideoOut: physical pre-vblank timing is not implemented"); + return registerVideoOutEvent(handle, eq, VIDEO_OUT_EVENT_PRE_VBLANK_START, udata); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 8f185445a..bb6183ee8 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -406,6 +406,10 @@ void VideoOutDriver::vblankEnd() { std::lock_guard cfgLock(cfg->mutex); if (!cfg->opened || cfg->failure) continue; require(cfg->vblankStatus.count != std::numeric_limits::max(), "vblank counter overflow"); + ++cfg->preVblankStatus.count; + cfg->preVblankStatus.processTime = sceKernelGetProcessTime(); + cfg->preVblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); + triggerEvents(*cfg, VIDEO_OUT_EVENT_PRE_VBLANK_START, reinterpret_cast(cfg->preVblankStatus.count)); ++cfg->vblankStatus.count; cfg->vblankStatus.processTime = sceKernelGetProcessTime(); cfg->vblankStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index ed59a5177..553ab71eb 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -258,6 +258,13 @@ void testControls() { } check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); check(owner->GetTriggeredEvents(&event, 1) == 1 && event.udata == &settings && sceVideoOutGetEventId(&event) == VIDEO_OUT_EVENT_VBLANK, "vblank event or updated user data missing"); + check(sceVideoOutAddPreVblankStartEvent(queue, handle, &mode) == 0, "pre-vblank subscription failed"); + check(sceVideoOutWaitVblank(handle) == 0, "vblank wait failed"); + KernelEvent events[2]{}; + check(owner->GetTriggeredEvents(events, 2) == 2, "pre-vblank or vblank event missing"); + const auto& preVblank = sceVideoOutGetEventId(&events[0]) == VIDEO_OUT_EVENT_PRE_VBLANK_START ? events[0] : events[1]; + check(sceVideoOutGetEventId(&preVblank) == VIDEO_OUT_EVENT_PRE_VBLANK_START && preVblank.udata == &mode, "pre-vblank event missing"); + check(sceVideoOutDeletePreVblankStartEvent(queue, handle) == 0, "pre-vblank unsubscription failed"); std::vector allocation(65536 + 65535); const auto storage = alignedBuffer(allocation); VideoOutBuffers buffer{storage.data(), nullptr, {allocation.data(), storage.data()}}; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8f2178189..105e348b7 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -239,6 +239,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes - [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws +- [sceVideoOutAddPreVblankStartEvent](../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut) - how long before the vblank the console signals pre-vblank start is unknown. The event is triggered on each emulated vblank tick immediately before the vblank event, with its own count as the event data ### Functional