diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 3932b5dc581..19d8c92645a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -239,5 +239,6 @@ std::size_t CaptureCallerOffsets(std::span frames, unsigned } extern "C" void AgcDriverCheckGuestMemory_nid_postfix(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable = false); +extern "C" void AgcDriverStoreGuestLabel_nid_postfix(std::uint64_t* pointer, std::uint64_t value); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f19c67b2843..6e26b5491f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1986,3 +1986,10 @@ void Write(std::uint64_t address, std::span source, std::size_t extern "C" void AgcDriverCheckGuestMemory_nid_postfix(const void* pointer, std::size_t bytes, std::size_t alignment, bool writable) { AgcDriver::GuestMemory::CheckRange(pointer, bytes, alignment, writable); } + +extern "C" void AgcDriverStoreGuestLabel_nid_postfix(std::uint64_t* pointer, std::uint64_t value) { + AgcDriver::GuestMemory::CheckRange(pointer, sizeof(*pointer), std::atomic_ref::required_alignment, true); + AgcDriver::GuestMemory::StoreOwnBytes(reinterpret_cast(pointer), sizeof(*pointer), [&] { + std::atomic_ref(*pointer).store(value, std::memory_order_release); + }); +} diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 6b9a38c8338..8de55cc8bff 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -71,6 +71,15 @@ if(BUILD_TESTING) set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") + foreach(flipMode IN ITEMS labels labels_main) + add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=${ANYPS5_VIDEO_OUT_TEST_DRIVER}") + if(WIN32) + set_tests_properties(video_out_flip_${flipMode} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flipMode IN ITEMS lifetime reopen decode controls overlay unopened) if(flipMode STREQUAL "lifetime") diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 41a36673a72..90bd1cabaf2 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -14,6 +14,7 @@ void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* att int APS5_VABI sceVideoOutSubmitChangeBufferAttribute2(int handle, int set_index, const VideoOutBufferAttribute2* attribute, void* option); int APS5_VABI sceVideoOutSubmitFlip(int handle, int index, int flip_mode, int64_t flip_arg); int APS5_VABI sceVideoOutUnregisterBuffers(int handle, int set_index); +int APS5_VABI sceVideoOutGetBufferLabelAddress(int handle, uintptr_t* address); #endif diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 8cfcc7299c0..85d086c9650 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -140,6 +140,7 @@ struct VideoOutConfig { VideoOutVblankStatus preVblankStatus{}; std::array buffers{}; + alignas(std::atomic_ref::required_alignment) std::array bufferLabels{}; std::array bufferPending{}; std::array bufferReuse; std::array groups{}; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 82b62b6c24f..898f378abe7 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -31,6 +31,21 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, co extern "C" { +int APS5_VABI sceVideoOutGetBufferLabelAddress(int handle, uintptr_t* address) try { + if (address == nullptr) return VIDEO_OUT_ERROR_INVALID_ADDRESS; + auto& driver = VideoOutDriver::Get(); + if (!driver.HasConfig(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + auto cfg = driver.GetConfig(handle); + std::lock_guard lock(cfg->mutex); + cfg->CheckAlive(); + if (cfg->Closed()) return VIDEO_OUT_ERROR_INVALID_HANDLE; + const auto labels = reinterpret_cast(cfg->bufferLabels.data()); + std::memcpy(address, &labels, sizeof(labels)); + return VIDEO_OUT_BUFFER_NUM_MAX; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixelFormat, uint32_t tilingMode, uint32_t width, uint32_t height, uint64_t option, uint32_t dccControl, uint64_t dccCbRegisterClearColor) { if (attribute == nullptr) { throw std::runtime_error("sceVideoOutSetBufferAttribute2: null attribute"); @@ -104,6 +119,8 @@ int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIn cfg->height = attribute->height; for (int i = 0; i < bufferNum; i++) { cfg->buffers[bufferIndexStart + i] = registered[i]; + auto& label = cfg->bufferLabels[bufferIndexStart + i]; + AgcDriverStoreGuestLabel_nid_postfix(&label, 0); } return 0; } catch (const ProcessShutdown&) { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 5b3dcdc23b9..7c898b9957e 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -24,6 +24,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/Shutdown.hpp" @@ -557,6 +558,11 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.cfg->flipStatus.processTime = sceKernelGetProcessTime(); req.cfg->flipStatus.processTimeCounter = sceKernelGetProcessTimeCounter(); req.cfg->flipStatus.flipArg = req.flipArg; + const auto previous = req.cfg->flipStatus.currentBuffer; + if (previous >= 0) { + auto& label = req.cfg->bufferLabels[previous]; + AgcDriverStoreGuestLabel_nid_postfix(&label, 0); + } req.cfg->flipStatus.currentBuffer = req.index; req.cfg->width = req.width; req.cfg->height = req.height; diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index b90bbc768ad..6df2306f9fc 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -665,6 +666,75 @@ void testFlipAfterGpuWork() { LibcRunShutdown_nid_postfix(); } +void testLabels(bool overlay) { + std::uintptr_t address = 0x1234; + for (const int invalid : {-1, 0, VIDEO_OUT_NUM_MAX, std::numeric_limits::max()}) { + check(sceVideoOutGetBufferLabelAddress(invalid, &address) == VIDEO_OUT_ERROR_INVALID_HANDLE && address == 0x1234, "invalid label query changed its output"); + } + check(sceVideoOutGetBufferLabelAddress(1, nullptr) == VIDEO_OUT_ERROR_INVALID_ADDRESS, "null label output was accepted"); + check(sceVideoOutGetBufferLabelAddress(2, &address) == VIDEO_OUT_ERROR_INVALID_HANDLE && address == 0x1234, "unopened label query changed its output"); + const int bus = overlay ? VIDEO_OUT_BUS_TYPE_OVERLAY : VIDEO_OUT_BUS_TYPE_MAIN; + const int handle = sceVideoOutOpen(255, bus, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutGetBufferLabelAddress(handle, &address) == VIDEO_OUT_BUFFER_NUM_MAX, "label query returned the wrong count"); + check(address != 0 && address % std::atomic_ref::required_alignment == 0, "label array is absent or unaligned"); + auto* labels = reinterpret_cast(address); + for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) check(std::atomic_ref(labels[i]).load() == 0, "new port has a nonzero label"); + alignas(std::uintptr_t) std::array unaligned; + unaligned.fill(std::byte{0x5a}); + check(sceVideoOutGetBufferLabelAddress(handle, reinterpret_cast(unaligned.data() + 1)) == VIDEO_OUT_BUFFER_NUM_MAX, "unaligned label-address output failed"); + std::uintptr_t repeated = 0; + std::memcpy(&repeated, unaligned.data() + 1, sizeof(repeated)); + check(repeated == address && unaligned.front() == std::byte{0x5a} && unaligned.back() == std::byte{0x5a}, "label query changed its address or output canaries"); + for (int i : {0, 1, 15}) std::atomic_ref(labels[i]).store(55); + std::vector allocation(2 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + std::array buffers{}; + buffers[0].data = storage.data(); + buffers[1].data = storage.data() + 65536; + VideoOutBufferAttribute2 attribute{}; + attribute.width = 64; + attribute.height = 64; + attribute.pixel_format = 0x8000000000000000ull; + check(sceVideoOutRegisterBuffers2(handle, 0, 0, buffers.data(), 2, &attribute, 0, nullptr) == 0, "label-test buffers failed to register"); + check(std::atomic_ref(labels[0]).load() == 0 && std::atomic_ref(labels[1]).load() == 0 && std::atomic_ref(labels[15]).load() == 55, "registration reset the wrong labels"); + std::atomic_ref(labels[0]).store(1); + std::atomic_ref(labels[1]).store(1); + const auto flip = [&](int index, std::uint64_t count) { + check(sceVideoOutSubmitFlip(handle, index, VIDEO_OUT_FLIP_MODE_VSYNC, index) == 0, "label-test flip failed to submit"); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + VideoOutFlipStatus status{}; + do { + check(sceVideoOutGetFlipStatus(handle, &status) == 0, "label-test flip status failed"); + if (status.count >= count) return; + std::this_thread::yield(); + } while (std::chrono::steady_clock::now() < deadline); + throw std::runtime_error("label-test flip timed out"); + }; + flip(0, 1); + check(std::atomic_ref(labels[0]).load() == 1 && std::atomic_ref(labels[1]).load() == 1, "first flip released a label before replacement"); + flip(1, 2); + check(std::atomic_ref(labels[0]).load() == 0 && std::atomic_ref(labels[1]).load() == 1, "replacement flip released the wrong label"); + flip(VIDEO_OUT_BUFFER_INDEX_BLACK, 3); + check(std::atomic_ref(labels[1]).load() == 0 && std::atomic_ref(labels[15]).load() == 55, "black flip did not release only the previous buffer"); + flip(VIDEO_OUT_BUFFER_INDEX_BLANK, 4); + check(std::atomic_ref(labels[15]).load() == 55, "blank flip accessed a negative label index"); + const int otherHandle = sceVideoOutOpen(255, overlay ? VIDEO_OUT_BUS_TYPE_MAIN : VIDEO_OUT_BUS_TYPE_OVERLAY, 0, nullptr); + std::uintptr_t other = 0; + check(sceVideoOutGetBufferLabelAddress(otherHandle, &other) == VIDEO_OUT_BUFFER_NUM_MAX && other != address, "ports share their labels"); + sceVideoOutClose(otherHandle); + sceVideoOutClose(handle); + repeated = 0x1234; + check(sceVideoOutGetBufferLabelAddress(handle, &repeated) == VIDEO_OUT_ERROR_INVALID_HANDLE && repeated == 0x1234, "closed label query changed its output"); + const int reopened = sceVideoOutOpen(255, bus, 0, nullptr); + check(sceVideoOutGetBufferLabelAddress(reopened, &repeated) == VIDEO_OUT_BUFFER_NUM_MAX && repeated != address, "reopened port shares live old-generation labels"); + auto* fresh = reinterpret_cast(repeated); + for (int i = 0; i < VIDEO_OUT_BUFFER_NUM_MAX; ++i) check(std::atomic_ref(fresh[i]).load() == 0, "reopened port retained an old label"); + check(std::atomic_ref(labels[15]).load() == 55, "reopening changed old-generation labels"); + sceVideoOutClose(reopened); + LibcRunShutdown_nid_postfix(); +} + void testUnopenedEventRegistration() { KernelEqueue eq = 0; check(sceKernelCreateEqueue(&eq, "unopened") == 0, "the event queue was not created"); @@ -722,6 +792,8 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank(); 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]) == "labels") testLabels(true); + else if (argc == 2 && std::string(argv[1]) == "labels_main") testLabels(false); else if (argc == 2 && std::string(argv[1]) == "onedevice") testOneDevice(); else if (argc == 2 && std::string(argv[1]) == "unopened") testUnopenedEventRegistration(); else if (argc == 2 && std::string(argv[1]) == "overlay") testOverlay(); diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index a20f0876afb..242745fbf14 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -16,6 +16,7 @@ - [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - mode 1 is supported; mode 15 is supported when the current host display runs at 120 Hz or above. Configuring mode 15 requires attribute3 bit 0x40; this single-bit mask is inferred from the measured full value 0x80040, not isolated by a console measurement. Vblanks run at 119.88 Hz until mode 1 restores 59.94 Hz. Display modes are read through SDL; a move to a slower display rejects further high-frame-rate presentation. Selecting another host display mode and VRR are not implemented. The console answers per display and per title: with a 120 Hz TV it also answers 1 for mode 15 (119.88 Hz), and [PS5_Vulkan](https://github.com/mihawk-99/PS5_Vulkan/blob/5b5e4fc2d80fdb67a4f61ba9e6a8a424026bb8a5/docs/HARDWARE_FINDINGS.md) measured that `sceVideoOutConfigureOutput` accepts 15 only from a title whose param.json declares high-frame-rate output (attribute3 0x80040). Modes 4, 7, 8, 12-14, 16-17 and 19 return `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` here, as they did on the measured display. Mode 18 throws, because one console answered 0x80290016 and another 0x8029001E. The name `VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE` for 0x8029001E is ours. Word 3 of the output options is accepted with any value, as on the console, and its meaning is unknown. PPSA12544 probes `0xd000000a`, which the console answers with 0x8029001E, so the title selects 3840 pixels - [sceVideoOutGetResolutionStatus](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the 48-byte layout follows the declarations in ps5-opengl and EVO-PLAYER-PS5 (#1578), which agree; not confirmed on a console. `screenSizeInInch` is 0, the value EDID uses for an undefined size, since the host display's size is not read; `flags` is 0, its bits are unknown. The pane equals the full size because `sceVideoOutSetWindowModeMargins` applies no margins - [sceVideoOutOpen](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown +- [sceVideoOutGetBufferLabelAddress](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) exposes a port-owned contiguous array of 16 aligned 64-bit labels and returns its count. Astro Bot PPSA21564's bundled AGC driver passes a pointer output and addresses each label at `base + index * 8`. The count, zero initialization on registration, and release of the previous displayed buffer when its replacement completes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/23c41333822fe8eca4c609b43119180af63ae4ac/src/core/libraries/videoout/video_out.cpp) and its driver; PS5 console equivalence has not been measured. Labels live until the port closes, and reopening creates a new array. Error precedence follows the reference for null output and the existing open-port policy for handles. Exact scanout release timing, repeated same-buffer flips and concurrent guest label stores remain unverified. EOP-flip registration and interrupt tokens remain unimplemented; exposing these labels does not implement that separate API. - [GPU flips](../../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - a command-buffer flip of a buffer index with no registered buffer when the GPU reaches it presents black and completes like any flip (flip event, count, `flipPendingNum`). The console's behaviour is unknown; PPSA05684 requires that it completes: its renderer submits a flip of buffer 0 while another thread waits for `sceVideoOutIsFlipPending` to reach 0 before registering the buffers, and `flipPendingNum` counts that flip from its submission as in shadPS4 - [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