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..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).