From 85d1eb2e606ab2ba9e4785f90ff5f4c271a25f31 Mon Sep 17 00:00:00 2001 From: MathiasDevelopes <25400788+MathiasDevelopes@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:47:24 +0200 Subject: [PATCH 1/2] fix(libSceVideoOut): overlay-bus flips no longer replace the main output A title that opens a main and an overlay port flips both, and every flip was presented as a whole frame, so the window alternated between the two buses: PPSA01736 shows its intro on the main bus and an empty overlay, and the intro blinked black on every other frame. Overlay flips now complete and signal their events without being shown. Compositing the overlay over the main output is not implemented. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 2 +- .../libSceVideoOut/include/VideoOutDriver.hpp | 1 + .../prx/libSceVideoOut/src/VideoOutDriver.cpp | 50 ++++++++++------- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 53 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 8525d4e8e1d..cf28079199b 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -33,7 +33,7 @@ set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flipMode IN ITEMS lifetime reopen decode controls) + foreach(flipMode IN ITEMS lifetime reopen decode controls overlay) if(flipMode STREQUAL "lifetime") add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests) else() diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index a1acea03e1d..493482c0ca7 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -123,6 +123,7 @@ struct VideoOutConfig { uint32_t width = VIDEO_OUT_DEFAULT_WIDTH; uint32_t height = VIDEO_OUT_DEFAULT_HEIGHT; uint64_t generation = 0; + int busType = VIDEO_OUT_BUS_TYPE_MAIN; bool opened = false; bool closing = false; std::exception_ptr failure; diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 83a27e73edf..9b07a020b60 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -315,6 +315,7 @@ int VideoOutDriver::Open(int busType) { } auto cfg = std::make_shared(); cfg->generation = generation; + cfg->busType = busType; cfg->opened = true; cfg->flipStatus.flipArg = -1; cfg->flipStatus.currentBuffer = -1; @@ -464,28 +465,37 @@ void VideoOutDriver::processFlip(FlipRequest& req) { 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); - const auto extensions = windowExtensions(window.Handle()); - const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; - timing.Mark("window_prepare"); - const auto gpuReady = [](void* context) { - auto& request = *static_cast(context); - std::lock_guard lock(request.cfg->mutex); - request.cfg->CheckAlive(); - if (request.cfg->Closed()) return; - require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition"); - request.gpuComplete = true; - request.cfg->vblankCond.notify_all(); - }; - if (req.index >= 0) { - const auto display = DescribeVideoOutBuffer(req.buffer, req.group); - AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); + if (req.cfg->busType == VIDEO_OUT_BUS_TYPE_OVERLAY) { + std::lock_guard lock(req.cfg->mutex); + req.cfg->CheckAlive(); + if (!req.cfg->Closed()) { + require(!req.terminal && !req.gpuComplete, "invalid GPU completion transition"); + req.gpuComplete = true; + } } else { - AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + window.Ensure(req.width, req.height); + const auto extensions = windowExtensions(window.Handle()); + const AgcDriver::PresentationWindow target{window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, req.width, req.height, req.timing}; + timing.Mark("window_prepare"); + const auto gpuReady = [](void* context) { + auto& request = *static_cast(context); + std::lock_guard lock(request.cfg->mutex); + request.cfg->CheckAlive(); + if (request.cfg->Closed()) return; + require(!request.terminal && !request.gpuComplete, "invalid GPU completion transition"); + request.gpuComplete = true; + request.cfg->vblankCond.notify_all(); + }; + if (req.index >= 0) { + const auto display = DescribeVideoOutBuffer(req.buffer, req.group); + AgcDriverPresentBuffer_nid_postfix(target, display, gpuReady, &req); + } else { + AgcDriverPresentClear_nid_postfix(target, req.index == VIDEO_OUT_BUFFER_INDEX_BLACK, gpuReady, &req); + } + timing.Mark("present"); + window.UpdateTitle(); + timing.Mark("window_title"); } - timing.Mark("present"); - window.UpdateTitle(); - timing.Mark("window_title"); std::lock_guard lock(req.cfg->mutex); timing.Mark("completion_mutex_wait"); req.cfg->CheckAlive(); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 768193f8c4c..6add71dd3e8 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -329,6 +329,58 @@ void testPresentation() { LibcRunShutdown_nid_postfix(); } +void testOverlay() { + const int mainHandle = sceVideoOutOpen(255, 0, 0, nullptr); + const int overlayHandle = sceVideoOutOpen(255, 1, 0, nullptr); + check(mainHandle != overlayHandle, "the overlay bus shares the main port"); + auto mainCfg = VideoOutDriver::Get().GetConfig(mainHandle); + auto overlayCfg = VideoOutDriver::Get().GetConfig(overlayHandle); + std::vector mainAllocation(6 * 65536 + 65535); + std::vector overlayAllocation(6 * 65536 + 65535); + const auto mainStorage = alignedBuffer(mainAllocation); + const auto overlayStorage = alignedBuffer(overlayAllocation); + fillBuffer(mainStorage, 259, 137); + fillBuffer(overlayStorage, 259, 137); + VideoOutBuffers mainBuffer{mainStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBuffers overlayBuffer{overlayStorage.data(), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); + check(sceVideoOutRegisterBuffers2(mainHandle, 0, 0, &mainBuffer, 1, &attribute, 0, nullptr) == 0, "main buffer registration failed"); + check(sceVideoOutRegisterBuffers2(overlayHandle, 0, 0, &overlayBuffer, 1, &attribute, 0, nullptr) == 0, "overlay buffer registration failed"); + const auto flip = [](int handle, const std::shared_ptr& cfg, std::int64_t argument) { + std::uint64_t target; + { + std::lock_guard lock(cfg->mutex); + target = cfg->flipStatus.count + 1; + } + check(sceVideoOutSubmitFlip(handle, 0, 1, argument) == 0, "the flip was not accepted"); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return (cfg->failure && cfg->flipStatus.flipPendingNum == 0) || cfg->flipStatus.count == target; }), "the flip did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == 0 && cfg->flipStatus.flipPendingNum == 0, "flip status is wrong"); + }; + const auto presentsAfter = [](std::uint64_t previous) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (AgcDriver::VulkanDevice::PresentCounts().presents == previous && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + return AgcDriver::VulkanDevice::PresentCounts().presents; + }; + const auto start = AgcDriver::VulkanDevice::PresentCounts().presents; + flip(mainHandle, mainCfg, 1); + const auto afterMain = presentsAfter(start); + check(afterMain > start, "a main-bus flip was not presented"); + flip(overlayHandle, overlayCfg, 2); + flip(overlayHandle, overlayCfg, 3); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + check(AgcDriver::VulkanDevice::PresentCounts().presents == afterMain, "an overlay-bus flip replaced the main output"); + flip(mainHandle, mainCfg, 4); + check(presentsAfter(afterMain) > afterMain, "a main-bus flip after overlay flips was not presented"); + sceVideoOutUnregisterBuffers(overlayHandle, 0); + sceVideoOutUnregisterBuffers(mainHandle, 0); + sceVideoOutClose(overlayHandle); + sceVideoOutClose(mainHandle); + 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"); @@ -560,6 +612,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testUnavailable(); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); + else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); return 0; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 96682d45e44..09866da54d4 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -418,3 +418,4 @@ Throughout the project, every function at every stage either **does exactly what - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers and other signals (`SIGBUS`, `SIGHUP`, ...) are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`; `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` of the other signals still returns `EINVAL` - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. +- [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 From 988394447a8dd43faec5d9390580db5d6b8a605a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sat, 10 Oct 2026 08:47:50 +0200 Subject: [PATCH 2/2] docs(TechnicalDebt): move the overlay bus line off the end of the section --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 09866da54d4..e2b30d102e0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -338,6 +338,7 @@ Throughout the project, every function at every stage either **does exactly what - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. - [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign; the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected before rounding: a product whose exact value is below 2^-126 gives zero even when it rounds to 2^-126, computed exactly from the significands for `v_mul_f32`, `v_mul_legacy_f32`, and for `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. `v_fma_mix_f32` was measured with f32 sources only; its f16 sources are not flushed. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` flushes its inputs, its product and its result in every mode, see its entry. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_mac_legacy_f32`, `v_dot2_f32_f16`, `v_div_scale_f32` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and LDS float atomics keep the previous lowering. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested, and `v_ldexp_f32` flushes in every mode. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. +- [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 - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). @@ -418,4 +419,3 @@ Throughout the project, every function at every stage either **does exactly what - [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers and other signals (`SIGBUS`, `SIGHUP`, ...) are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`; `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` of the other signals still returns `EINVAL` - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. -- [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