diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c32d15923..bcc95b201 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -239,8 +239,8 @@ jobs: key: linux save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} evict-old-files: job - - name: Install lavapipe for the GPU execution tests and the audio development files - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev + - name: Install lavapipe and the SDL audio and XRandR development files + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev - run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON - name: Check that every test executable is registered with ctest run: python3 tools/check_test_registration.py build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f54726afa..cbab5a8f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: save: false - name: Install lavapipe for the GPU execution tests and the audio development files if: matrix.platform == 'linux' - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev - name: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE diff --git a/CMakeLists.txt b/CMakeLists.txt index 724e7d910..1ac82b9a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,12 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_directory_property(ANYPS5_SDL_X11_XRANDR DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_X11_XRANDR) + if(NOT ANYPS5_SDL_X11_XRANDR) + message(FATAL_ERROR "SDL has no XRandR support, so X11 display refresh rates cannot be queried. " + "Install libxrandr-dev (Debian, Ubuntu) or libXrandr-devel (Fedora), " + "enable SDL_X11_XRANDR, then rerun configuration.") + endif() get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE) get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO) get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA) diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index faef0b458..042deeed6 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -33,6 +33,27 @@ target_link_libraries(video_out_flip_tests PRIVATE libSceVideoOut libSceAgcDrive set_target_properties(video_out_flip_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(video_out_flip_tests) if(BUILD_TESTING) + add_test_executable(video_out_output_mode_policy_tests tests/OutputModePolicy.cpp) + target_include_directories(video_out_output_mode_policy_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(video_out_output_mode_policy_tests PRIVATE libSceVideoOut libc libkernel) + configure_windows_unwind(video_out_output_mode_policy_tests) + add_test(NAME video_out_output_mode_policy COMMAND video_out_output_mode_policy_tests) + set_tests_properties(video_out_output_mode_policy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(video_out_output_mode_policy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_video_out_high_frame_rate_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHighFrameRate.cpp) + target_include_directories(guest_video_out_high_frame_rate_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_high_frame_rate_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_high_frame_rate_tests) + add_test(NAME guest_video_out_high_frame_rate COMMAND guest_video_out_high_frame_rate_tests) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") diff --git a/core/libs/prx/libSceVideoOut/include/Output.hpp b/core/libs/prx/libSceVideoOut/include/Output.hpp index a188ee600..4ecf91341 100644 --- a/core/libs/prx/libSceVideoOut/include/Output.hpp +++ b/core/libs/prx/libSceVideoOut/include/Output.hpp @@ -5,6 +5,8 @@ #include "prx/libc/include/General.hpp" #include "SceTypes.hpp" +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate); + extern "C" { int APS5_VABI sceVideoOutOpen(int user_id, int bus_type, int index, const void* param); diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index 11fa5eb89..8cfcc7299 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -215,6 +215,8 @@ public: std::shared_ptr GetConfig(int handle); bool IsOpen(int handle); bool HasConfig(int handle); + bool SupportsHighFrameRate(); + int GetDisplayRefreshRate(); // 0, or VIDEO_OUT_ERROR_FLIP_QUEUE_FULL when the title has VIDEO_OUT_FLIP_QUEUE_CAPACITY flips pending. int SubmitFlip(int handle, int index, int flipMode, int64_t flipArg); @@ -223,7 +225,8 @@ private: bool close(int handle); void presentLoop(std::stop_token token, std::promise& started); void vblankLoop(std::stop_token token); - void vblankEnd(); + void vblankEnd(std::uint64_t tick); + void updateDisplayRefreshRate(); void processFlip(FlipRequest& req); void triggerEvents(VideoOutConfig& cfg, int eventKind, void* triggerData); @@ -235,6 +238,7 @@ private: std::shared_ptr flipQueue = std::make_shared(); DisplayWindow window; + std::atomic displayRefreshRate{0}; std::jthread presentThread; std::jthread vblankThread; diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index 067e01908..f47aac9f3 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -11,17 +11,19 @@ #include "prx/libSceVideoOut/include/Output.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" static constexpr std::uint32_t VIDEO_OUT_OUTPUT_OPTIONS_FIRST_WORD = 0x00FF0000; static constexpr std::size_t VIDEO_OUT_OUTPUT_OPTIONS_FREE_WORD = 3; -static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { - if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; - if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; +int ValidateVideoOutOutputMode_nid_no_patch(uint64_t mode, const VideoOutOutputOptions* options, int displayRefreshRate) { switch (mode) { case VIDEO_OUT_OUTPUT_MODE_DEFAULT: break; - case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case VIDEO_OUT_OUTPUT_MODE_119_88HZ: case 0x10: case 0x11: case 0x13: + case VIDEO_OUT_OUTPUT_MODE_119_88HZ: + if (displayRefreshRate < 120) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; + break; + case 0x4: case 0x7: case 0x8: case 0xC: case 0xD: case 0xE: case 0x10: case 0x11: case 0x13: return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; case 0x12: throw std::runtime_error(std::string(__func__) + ": output mode 0x12 answers differently on two consoles"); @@ -37,6 +39,12 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO return 0; } +static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) { + if (!VideoOutDriver::Get().HasConfig(handle) || !VideoOutDriver::Get().IsOpen(handle)) return VIDEO_OUT_ERROR_INVALID_HANDLE; + if (reservedPtr != nullptr || reserved != 0) return VIDEO_OUT_ERROR_INVALID_VALUE; + return ValidateVideoOutOutputMode_nid_no_patch(mode, options, VideoOutDriver::Get().GetDisplayRefreshRate()); +} + static void validateOpenParam(const void* param) { if (param == nullptr) return; VideoOutOpenParam openParam{}; @@ -235,6 +243,7 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO if (supported < 0) { return supported; } + if (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ && (GetAppAttribute3_nid_no_patch() & 0x40u) == 0) return VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE; auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_INVALID_HANDLE"); diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index fbe9da37b..f605f9dbb 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -447,12 +447,29 @@ void VideoOutDriver::triggerEvents(VideoOutConfig& cfg, int eventKind, void* tri } } -void VideoOutDriver::vblankEnd() { +bool VideoOutDriver::SupportsHighFrameRate() { + return GetDisplayRefreshRate() >= 120; +} + +int VideoOutDriver::GetDisplayRefreshRate() { + return displayRefreshRate.load(); +} + +void VideoOutDriver::updateDisplayRefreshRate() { + const int display = SDL_GetWindowDisplayIndex(window.Handle()); + if (display < 0) throw std::runtime_error(std::string("SDL_GetWindowDisplayIndex failed: ") + SDL_GetError()); + SDL_DisplayMode mode{}; + if (SDL_GetCurrentDisplayMode(display, &mode) != 0) throw std::runtime_error(std::string("SDL_GetCurrentDisplayMode failed: ") + SDL_GetError()); + displayRefreshRate.store(mode.refresh_rate); +} + +void VideoOutDriver::vblankEnd(std::uint64_t tick) { std::lock_guard lock(mutex); for (const auto& cfg : contexts) { if (!cfg) continue; std::lock_guard cfgLock(cfg->mutex); if (!cfg->opened || cfg->failure) continue; + if (cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ && tick % 2 != 0) continue; require(cfg->vblankStatus.count != std::numeric_limits::max(), "vblank counter overflow"); ++cfg->preVblankStatus.count; cfg->preVblankStatus.processTime = sceKernelGetProcessTime(); @@ -477,6 +494,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { req.ReleaseLocked(); return; } + require(req.cfg->outputMode != VIDEO_OUT_OUTPUT_MODE_119_88HZ || SupportsHighFrameRate(), "display no longer supports 119.88 Hz output"); require(req.ready && !req.terminal && req.generation == req.cfg->generation, "stale or incomplete flip request"); const auto interval = static_cast(req.flipRate + 1); require(req.cfg->lastFlipVblank <= std::numeric_limits::max() - interval, "flip interval overflow"); @@ -555,6 +573,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { void VideoOutDriver::presentLoop(std::stop_token token, std::promise& started) { try { window.Ensure(VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT); + updateDisplayRefreshRate(); const auto extensions = windowExtensions(window.Handle()); AgcDriverAttachWindow_nid_postfix({window.Handle(), extensions, &createWindowSurface, &windowDrawableSize, VIDEO_OUT_DEFAULT_WIDTH, VIDEO_OUT_DEFAULT_HEIGHT, nullptr}); } catch (...) { @@ -599,6 +618,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } } padInput.Update(); + updateDisplayRefreshRate(); if (current) { require(current->timing != nullptr, "missing presentation timing"); const auto dequeued = AgcDriver::FrameTiming::Clock::now(); @@ -658,7 +678,7 @@ void VideoOutDriver::presentLoop(std::stop_token token, std::promise& star } void VideoOutDriver::vblankLoop(std::stop_token token) { - using Frame = std::chrono::duration>; + using Frame = std::chrono::duration>; const auto start = std::chrono::steady_clock::now(); try { for (int64_t frame = 1; !token.stop_requested(); ++frame) { @@ -669,7 +689,7 @@ void VideoOutDriver::vblankLoop(std::stop_token token) { std::lock_guard lock(flipQueue->mutex); if (token.stop_requested() || flipQueue->failure) return; } - vblankEnd(); + vblankEnd(static_cast(frame)); } } catch (const std::exception& error) { std::fprintf(stderr, "[videoout] vblank failed: %s\n", error.what()); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 2be38015f..0e25c5fd1 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -306,7 +306,8 @@ void testControls() { check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); - check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode is supported"); + const int highFrameRate = sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0); + check(highFrameRate == 1 || highFrameRate == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unexpected high-frame-rate support result"); check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_119_88HZ, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE, "unavailable output mode was configured"); check(sceVideoOutIsOutputSupported(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode is supported"); check(sceVideoOutConfigureOutput(handle, 0xd000000aull, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE, "unknown output mode was configured"); diff --git a/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp new file mode 100644 index 000000000..5a4a60094 --- /dev/null +++ b/core/libs/prx/libSceVideoOut/tests/OutputModePolicy.cpp @@ -0,0 +1,43 @@ +#include "prx/libSceVideoOut/include/Output.hpp" +#include +#include +#include + +static void Require(bool value, const char* reason) { + if (!value) { + std::fprintf(stderr, "%s\n", reason); + std::abort(); + } +} + +int main() { + constexpr int unsupported = static_cast(0x80290016); + constexpr int invalidValue = static_cast(0x80290001); + constexpr int unknownMode = static_cast(0x8029001e); + VideoOutOutputOptions options{}; + Require(sceVideoOutInitializeOutputOptions(&options) == 0, "output options initialization failed"); + for (int refresh : {-1, 0, 30, 59, 60, 119, 120, 144, 240}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &options, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, nullptr, refresh) == (refresh < 120 ? unsupported : 0), "mode 15 without options did not follow the display refresh threshold"); + Require(ValidateVideoOutOutputMode_nid_no_patch(1, &options, refresh) == 0, "default mode depended on the display refresh rate"); + } + for (std::uint64_t mode : {4, 7, 8, 12, 13, 14, 16, 17, 19}) { + Require(ValidateVideoOutOutputMode_nid_no_patch(mode, &options, 240) == unsupported, "unimplemented output mode was accepted on a fast display"); + } + Require(ValidateVideoOutOutputMode_nid_no_patch(0xd000000a, &options, 240) == unknownMode, "unknown output mode was accepted"); + VideoOutOutputOptions changed = options; + changed.internalData[0] = 0; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == invalidValue, "mode 15 accepted invalid output options"); + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 60) == unsupported, "invalid options changed unsupported-mode error precedence"); + changed = options; + changed.internalData[3] = 0xffffffff; + Require(ValidateVideoOutOutputMode_nid_no_patch(15, &changed, 120) == 0, "mode 15 rejected the free options word"); + bool thrown = false; + try { + ValidateVideoOutOutputMode_nid_no_patch(18, &options, 120); + } catch (const std::runtime_error&) { + thrown = true; + } + Require(thrown, "unsettled mode 18 did not throw"); + +} diff --git a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp index d18f2e520..0cceb906f 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/AppMetadata.hpp @@ -23,6 +23,7 @@ AppTitleId GetAppTitleId_nid_postfix(); bool HasAppIcon_nid_postfix(); AppIconData GetAppIconData_nid_postfix(); std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix(); +std::uint32_t GetAppAttribute3_nid_no_patch(); std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index); } diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp index 1737a427d..be912c6e7 100644 --- a/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamJsonParser.hpp @@ -9,6 +9,7 @@ struct ParsedParamJson { std::string title; std::string titleId; std::uint64_t downloadDataSizeMiB = 0; + std::uint32_t attribute3 = 0; std::int32_t userDefinedParams[4] = {}; }; diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index 79df79827..4065ef5af 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -18,6 +18,7 @@ bool g_iconLoaded = false; char g_title[128] = {}; char g_titleId[12] = {}; std::uint64_t g_downloadDataSizeMiB = 0; +std::uint32_t g_attribute3 = 0; std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; @@ -38,6 +39,7 @@ void ensureTitleLoaded() { copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; + g_attribute3 = parsed.attribute3; std::memcpy(g_userDefinedParams, parsed.userDefinedParams, sizeof(g_userDefinedParams)); g_titleLoaded = true; } @@ -89,6 +91,11 @@ std::uint64_t GetAppDownloadDataSizeMiB_nid_postfix() { return g_downloadDataSizeMiB; } +std::uint32_t GetAppAttribute3_nid_no_patch() { + ensureTitleLoaded(); + return g_attribute3; +} + std::int32_t GetAppUserDefinedParam_nid_postfix(std::uint32_t index) { if (index >= 4) throw std::out_of_range("user defined param index out of range"); ensureTitleLoaded(); diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 927674d8b..769ff1efc 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -305,6 +305,12 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { ParsedParamJson result; result.title = title; result.titleId = titleId; + if (const JsonValue* attribute = findObjectMember(root, "attribute3")) { + const auto& number = attribute->numberText; + if (attribute->type != JsonType::Number || number.find_first_of("-.eE") != std::string::npos) throw std::runtime_error("param.json attribute3 is not an unsigned integer"); + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), result.attribute3); + if (parsed.ec != std::errc{} || parsed.ptr != number.data() + number.size()) throw std::runtime_error("param.json attribute3 exceeds uint32 range"); + } if (const JsonValue* downloadData = findObjectMember(root, "downloadDataSize")) { if (downloadData->type != JsonType::Number || downloadData->numberValue < 0) throw std::runtime_error("param.json downloadDataSize is not a size"); if (downloadData->numberText.find_first_of("-.eE") == std::string::npos) { diff --git a/core/libs/tests/GuestVideoOutHighFrameRate.cpp b/core/libs/tests/GuestVideoOutHighFrameRate.cpp new file mode 100644 index 000000000..3e4f4c443 --- /dev/null +++ b/core/libs/tests/GuestVideoOutHighFrameRate.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutOpen(int, int, int, const void*); +int APS5_VABI sceVideoOutClose(int); +int APS5_VABI sceVideoOutIsOutputSupported(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutConfigureOutput(int, std::uint64_t, const VideoOutOutputOptions*, void*, std::uint64_t); +int APS5_VABI sceVideoOutWaitVblank(int); +int APS5_VABI sceVideoOutGetOutputStatus(int, VideoOutOutputStatus*); +int APS5_VABI sceVideoOutGetResolutionStatus(int, VideoOutResolutionStatus*); +} + +static constexpr int Unsupported = static_cast(0x80290016u); + +static void Require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +static double Measure(int handle) { + Require(sceVideoOutWaitVblank(handle) == 0, "initial vblank failed"); + const auto start = std::chrono::steady_clock::now(); + for (int frame = 0; frame < 60; ++frame) Require(sceVideoOutWaitVblank(handle) == 0, "vblank failed"); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +int main(int argc, char** argv) { + const bool requireDisplay = argc > 1 && std::strcmp(argv[1], "--require-high-frame-rate") == 0; + VideoOutTestEnvironment environment; + { + std::ofstream param("app0/sce_sys/param.json", std::ios::binary); + param << R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"High frame rate test"}},"attribute3":64})"; + Require(static_cast(param), "cannot write high-frame-rate fixture"); + } + int handle; + try { + handle = sceVideoOutOpen(255, 0, 0, nullptr); + } catch (const std::runtime_error& error) { + if (requireDisplay) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + const int supported = sceVideoOutIsOutputSupported(handle, 15, nullptr, nullptr, 0); + if (supported == Unsupported && !requireDisplay) { + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == Unsupported, "unsupported display accepted high-frame-rate output"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); + std::puts("skipped, current display does not provide 120 Hz"); + return 77; + } + Require(supported == 1, "120 Hz output not reported as supported"); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode failed"); + const double normal = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 15, nullptr, nullptr, 0) == 0, "permitted high-frame-rate mode failed"); + VideoOutOutputStatus output{}; + VideoOutResolutionStatus resolution{}; + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 13, "wrong output refresh rate"); + Require(sceVideoOutGetResolutionStatus(handle, &resolution) == 0 && resolution.refreshRate == 13, "wrong resolution refresh rate"); + const double fast = Measure(handle); + Require(sceVideoOutConfigureOutput(handle, 1, nullptr, nullptr, 0) == 0, "default mode restore failed"); + Require(sceVideoOutGetOutputStatus(handle, &output) == 0 && output.refreshRate == 3, "default output refresh rate not restored"); + const double restored = Measure(handle); + std::printf("60 vblanks: default=%.3f s high=%.3f s restored=%.3f s\n", normal, fast, restored); + Require(normal > 0.8 && normal < 1.3 && fast > 0.4 && fast < 0.7 && restored > 0.8 && restored < 1.3, "incorrect vblank periods"); + Require(normal / fast > 1.7 && restored / fast > 1.7, "high-frame-rate mode did not double vblank frequency"); + Require(sceVideoOutClose(handle) == 0, "close failed"); + LibcRunShutdown_nid_postfix(); +} diff --git a/core/libs/tests/GuestVideoOutOutputMode.cpp b/core/libs/tests/GuestVideoOutOutputMode.cpp index 01b657eac..da708754d 100644 --- a/core/libs/tests/GuestVideoOutOutputMode.cpp +++ b/core/libs/tests/GuestVideoOutOutputMode.cpp @@ -52,7 +52,10 @@ int main() { Require(handle > 0); Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); Require(sceVideoOutIsOutputSupported(handle, 1, nullptr, nullptr, 0) == 1); - for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0xFull, 0x10ull, 0x11ull, 0x13ull}) { + const int highFrameRate = sceVideoOutIsOutputSupported(handle, 15, &options, nullptr, 0); + Require(highFrameRate == 1 || highFrameRate == UNSUPPORTED_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, 15, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); + for (std::uint64_t mode : {0x4ull, 0x7ull, 0x8ull, 0xCull, 0xDull, 0xEull, 0x10ull, 0x11ull, 0x13ull}) { Require(sceVideoOutIsOutputSupported(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); Require(sceVideoOutConfigureOutput(handle, mode, &options, nullptr, 0) == UNSUPPORTED_OUTPUT_MODE); } diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index cfe365afb..1606b7f87 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -88,6 +88,19 @@ static void CheckLanguages(const std::filesystem::path& path) { Require(rejected, "Accepted localizedParameters without a language entry"); } +static void CheckOutputPermission(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA99007","localizedParameters":{"en-US":{"titleName":"Example"}})"; + Require(ParseText(path, prefix + "}").attribute3 == 0, "Absent output permission is not zero"); + for (const auto& [text, expected] : {std::pair{"0", 0u}, {"524352", 0x80040u}, {"4294967295", 0xFFFFFFFFu}}) { + Require(ParseText(path, prefix + ",\"attribute3\":" + text + "}").attribute3 == expected, "Incorrect output permission"); + } + for (const auto* text : {"-1", "0.5", "1e3", "4294967296", "true", "null", "\"524352\""}) { + bool rejected = false; + try { ParseText(path, prefix + ",\"attribute3\":" + text + "}"); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted invalid attribute3"); + } +} + static void CheckNumberSyntax(const std::filesystem::path& path) { const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":"Example"}})"; const std::pair fields[] = { @@ -156,6 +169,7 @@ int main() { } CheckLanguages(path); CheckUserDefinedParams(path); + CheckOutputPermission(path); CheckNesting(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8825151d9..1b4dae68c 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -27,6 +27,7 @@ git submodule update --init --recursive ## Requirements - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. +- Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). Audio output needs SDL's PipeWire, PulseAudio or ALSA backend, built from their development files (`libpipewire-0.3-dev`, `libpulse-dev` and `libasound2-dev` on Debian/Ubuntu; `pipewire-devel`, `pulseaudio-libs-devel` and `alsa-lib-devel` on Fedora). SDL loads them at run time. Without any of them SDL keeps only its silent dummy and disk drivers, so configuration fails. - Fedora: if CMake reports `Could NOT find Threads`, install `glibc-static` and `libstdc++-static` with `sudo dnf install glibc-static libstdc++-static`, then rerun configuration. The project enables static executable linking, and FFmpeg's thread check needs these libraries. - Windows: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release `15.2.0posix-14.0.0-ucrt-r7`) is currently supported. Add its `mingw64/bin` directory to `PATH` before configuring. diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index e2de5493e..3a7fc1e81 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -12,7 +12,7 @@ - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented -- [sceVideoOutIsOutputSupported](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - only the default mode (1) is reported as supported. 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-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 +- [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 - [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