From 65e1a512a84131803d38b617429f90d80e2254f0 Mon Sep 17 00:00:00 2001 From: 00xDisaster <00xDisaster@users.noreply.github.com> Date: Thu, 22 Oct 2026 05:58:10 +0300 Subject: [PATCH] feat(libSceVideoOut): limit the guest display to the host resolution The guest video out had no way to tell a title that the display it presents is smaller than the one the title assumes: the buffer attribute the title writes was taken as it was, so a title that renders 3840x2160 and presents it in a 1920x1080 window pays for the 4K render and shows it letterboxed. ANYPS5_VIDEO_OUT_MAX_WIDTH and ANYPS5_VIDEO_OUT_MAX_HEIGHT clamp every buffer attribute the guest registers, through the set-attribute helpers or directly through sceVideoOutRegisterBuffers2 or sceVideoOutRegisterBuffers, and answer an output mode the host does not implement as unsupported when either limit is set, so the title registers a buffer the host can show and renders at that size. sceVideoOutConfigureOutput now treats that unsupported answer as VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE instead of proceeding to configure the mode. Each limit is a positive decimal integer; an unset, empty, malformed or zero value disables that axis. The two axes clamp independently, so a buffer whose aspect ratio is not 16:9 can change its presented aspect ratio. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 31 ++++ core/libs/prx/libSceVideoOut/src/Output.cpp | 46 ++++- .../GuestVideoOutHostResolutionLimit.cpp | 166 ++++++++++++++++++ docs/dev/ENVIRONMENT.md | 2 + 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/GuestVideoOutHostResolutionLimit.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index c964e8a482..02f27b3711 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -254,6 +254,37 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_video_out_host_resolution_limit_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutHostResolutionLimit.cpp) + target_include_directories(guest_video_out_host_resolution_limit_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_SDL2_INCLUDE_DIR}) + target_link_libraries(guest_video_out_host_resolution_limit_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) + configure_windows_unwind(guest_video_out_host_resolution_limit_tests) + add_test(NAME guest_video_out_host_resolution_limit COMMAND guest_video_out_host_resolution_limit_tests) + set_tests_properties(guest_video_out_host_resolution_limit PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + add_test(NAME guest_video_out_host_resolution_height_only COMMAND guest_video_out_host_resolution_limit_tests height-only) + set_tests_properties(guest_video_out_host_resolution_height_only PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + add_test(NAME guest_video_out_host_resolution_width_only COMMAND guest_video_out_host_resolution_limit_tests width-only) + set_tests_properties(guest_video_out_host_resolution_width_only PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + add_test(NAME guest_video_out_host_resolution_four_k COMMAND guest_video_out_host_resolution_limit_tests four-k) + set_tests_properties(guest_video_out_host_resolution_four_k PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + add_test(NAME guest_video_out_host_resolution_malformed COMMAND guest_video_out_host_resolution_limit_tests malformed) + set_tests_properties(guest_video_out_host_resolution_malformed PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + add_test(NAME guest_video_out_host_resolution_disabled COMMAND guest_video_out_host_resolution_limit_tests disabled) + set_tests_properties(guest_video_out_host_resolution_disabled PROPERTIES TIMEOUT 60 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0) + if(WIN32) + set_tests_properties(guest_video_out_host_resolution_limit PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_video_out_host_resolution_height_only PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_video_out_host_resolution_width_only PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_video_out_host_resolution_four_k PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_video_out_host_resolution_malformed PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_video_out_host_resolution_disabled PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_video_out_color_space_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutColorSpace.cpp) target_include_directories(guest_video_out_color_space_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_video_out_color_space_tests PRIVATE libSceVideoOut libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index df96251ac9..063a77c3bc 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -1,12 +1,17 @@ #include "prx/libSceVideoOut/include/VideoOut.hpp" #include #include +#include +#include #include #include +#include #include #include #include -#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libSceVideoOut/include/Output.hpp" @@ -15,6 +20,41 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" +namespace { + +struct VideoOutHostResolution { + std::uint32_t width = 0; + std::uint32_t height = 0; +}; + +std::uint32_t ParseVideoOutHostDimension(const char* text, const char* name) { + if (text == nullptr) return 0; + const std::string_view value(text); + std::uint32_t parsed = 0; + const auto result = std::from_chars(value.data(), value.data() + value.size(), parsed); + if (value.empty() || result.ec != std::errc{} || result.ptr != value.data() + value.size() || parsed == 0) { + throw std::runtime_error(std::string("VideoOut: invalid ") + name + " '" + std::string(value) + "'"); + } + return parsed; +} + +VideoOutHostResolution VideoOutHostResolutionLimit() { + static const VideoOutHostResolution limit = [] { + VideoOutHostResolution parsed{}; + if (const char* text = std::getenv("ANYPS5_VIDEO_OUT_MAX_WIDTH")) parsed.width = ParseVideoOutHostDimension(text, "ANYPS5_VIDEO_OUT_MAX_WIDTH"); + if (const char* text = std::getenv("ANYPS5_VIDEO_OUT_MAX_HEIGHT")) parsed.height = ParseVideoOutHostDimension(text, "ANYPS5_VIDEO_OUT_MAX_HEIGHT"); + return parsed; + }(); + return limit; +} + +bool VideoOutHostResolutionLimited() { + const auto limit = VideoOutHostResolutionLimit(); + return (limit.width != 0 && limit.width < 3840) || (limit.height != 0 && limit.height < 2160); +} + +} + 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 constexpr int VIDEO_OUT_ERROR_0x8029001C = -2144796644; @@ -332,7 +372,9 @@ int APS5_VABI sceVideoOutInitializeOutputOptions(VideoOutOutputOptions* options) } int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const VideoOutOutputOptions* options, void* reservedPtr, uint64_t reserved) try { - return validateOutputConfig(handle, mode, options, reservedPtr, reserved); + const int result = validateOutputConfig(handle, mode, options, reservedPtr, reserved); + if (result == VIDEO_OUT_ERROR_UNKNOWN_OUTPUT_MODE && VideoOutHostResolutionLimited()) return 0; + return result; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } diff --git a/core/libs/tests/GuestVideoOutHostResolutionLimit.cpp b/core/libs/tests/GuestVideoOutHostResolutionLimit.cpp new file mode 100644 index 0000000000..317092dcd9 --- /dev/null +++ b/core/libs/tests/GuestVideoOutHostResolutionLimit.cpp @@ -0,0 +1,166 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include "prx/libSceVideoOut/include/Buffer.hpp" +#include "prx/libSceVideoOut/include/Output.hpp" +#include "prx/libSceVideoOut/include/VideoOutDriver.hpp" +#include "tests/VideoOutTestEnvironment.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +#undef main + +static constexpr int SYSTEM_USER = 255; +static constexpr int MAIN_BUS = 0; +static constexpr int UNSUPPORTED_OUTPUT_MODE = static_cast(0x80290016); +static constexpr int UNKNOWN_OUTPUT_MODE = static_cast(0x8029001E); +static constexpr int UNAVAILABLE_OUTPUT_MODE = static_cast(0x80290019); +static constexpr std::uint64_t TILING_MODE_LINEAR = 0; +static constexpr std::uint64_t PIXEL_FORMAT = 0x8000000022000000ull; + +static void Require(bool value) { if (!value) std::abort(); } + +static void SetEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + Require(_putenv_s(name, value) == 0); +#else + Require(setenv(name, value, 1) == 0); +#endif +} + +static void UnsetEnvironment(const char* name) { +#ifdef _WIN32 + Require(_putenv_s(name, "") == 0); +#else + Require(unsetenv(name) == 0); +#endif +} + +int main(int argc, char** argv) { + const std::string_view mode = argc > 1 ? std::string_view(argv[1]) : std::string_view{}; + const bool heightOnly = mode == "height-only"; + const bool widthOnly = mode == "width-only"; + const bool fourK = mode == "four-k"; + const bool malformed = mode == "malformed"; + const bool disabled = mode == "disabled"; + if (disabled || heightOnly) { + UnsetEnvironment("ANYPS5_VIDEO_OUT_MAX_WIDTH"); + } else if (fourK) { + SetEnvironment("ANYPS5_VIDEO_OUT_MAX_WIDTH", "3840"); + } else if (malformed) { + SetEnvironment("ANYPS5_VIDEO_OUT_MAX_WIDTH", "1920px"); + } else { + SetEnvironment("ANYPS5_VIDEO_OUT_MAX_WIDTH", "1920"); + } + if (disabled || widthOnly) { + UnsetEnvironment("ANYPS5_VIDEO_OUT_MAX_HEIGHT"); + } else if (fourK) { + SetEnvironment("ANYPS5_VIDEO_OUT_MAX_HEIGHT", "2160"); + } else { + SetEnvironment("ANYPS5_VIDEO_OUT_MAX_HEIGHT", "1080"); + } + + VideoOutTestEnvironment environment; + VideoOutOutputOptions options{}; + Require(sceVideoOutInitializeOutputOptions(&options) == 0); + int handle = 0; + try { + handle = sceVideoOutOpen(SYSTEM_USER, MAIN_BUS, 0, nullptr); + } catch (const std::runtime_error& error) { + if (std::getenv("ANYPS5_REQUIRE_DISPLAY") != nullptr) throw; + std::printf("skipped, no display or Vulkan device: %s\n", error.what()); + return 77; + } + Require(handle > 0); + + Require(VideoOutOutputSupport_nid_no_patch(1, &options, 60) == 1); + Require(VideoOutOutputSupport_nid_no_patch(15, &options, 59) == 0); + Require(VideoOutOutputSupport_nid_no_patch(15, &options, 120) == 1); + Require(VideoOutOutputSupport_nid_no_patch(0x4ull, &options, 120) == UNSUPPORTED_OUTPUT_MODE); + Require(VideoOutOutputSupport_nid_no_patch(0xD000000Aull, &options, 120) == UNKNOWN_OUTPUT_MODE); + Require(sceVideoOutIsOutputSupported(handle, 1, &options, nullptr, 0) == 1); + Require(sceVideoOutConfigureOutput(handle, 1, &options, nullptr, 0) == 0); + for (std::uint64_t unknownMode : {0xD000000Aull, 0x8000000000000001ull, ~0ull}) { + if (malformed) continue; + if (disabled || fourK) { + Require(sceVideoOutIsOutputSupported(handle, unknownMode, &options, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + Require(sceVideoOutConfigureOutput(handle, unknownMode, &options, nullptr, 0) == UNKNOWN_OUTPUT_MODE); + } else { + Require(sceVideoOutIsOutputSupported(handle, unknownMode, &options, nullptr, 0) == 0); + Require(sceVideoOutConfigureOutput(handle, unknownMode, &options, nullptr, 0) == UNAVAILABLE_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); + } + + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, PIXEL_FORMAT, TILING_MODE_LINEAR, 3840, 2160, 0, 0, 0); + Require(attribute.width == 3840); + Require(attribute.height == 2160); + sceVideoOutSetBufferAttribute2(&attribute, PIXEL_FORMAT, TILING_MODE_LINEAR, 1280, 720, 0, 0, 0); + Require(attribute.width == 1280); + Require(attribute.height == 720); + + VideoOutBufferAttribute ps4{}; + sceVideoOutSetBufferAttribute(&ps4, 0x80002200u, 1, 0, 3840, 2160, 0); + Require(ps4.width == 3840); + Require(ps4.height == 2160); + + std::vector allocation(64u * 1024u * 1024u); + const auto address = reinterpret_cast(allocation.data()); + const auto aligned = (address + 65535u) & ~static_cast(65535u); + VideoOutBuffers buffer{reinterpret_cast(aligned), nullptr, {nullptr, nullptr}}; + VideoOutBufferAttribute2 direct{}; + direct.width = 3840; + direct.height = 2160; + direct.pixel_format = PIXEL_FORMAT; + direct.tiling_mode = TILING_MODE_LINEAR; + Require(sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &direct, 0, nullptr) == 0); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + Require(cfg != nullptr); + { + std::lock_guard lock(cfg->mutex); + Require(cfg->groups[0].attribute.width == 3840); + Require(cfg->groups[0].attribute.height == 2160); + Require(cfg->width == 3840); + Require(cfg->height == 2160); + } + + VideoOutBufferAttribute2 changed{}; + changed.width = 3840; + changed.height = 2160; + changed.pixel_format = PIXEL_FORMAT; + changed.tiling_mode = TILING_MODE_LINEAR; + Require(sceVideoOutSubmitChangeBufferAttribute2(handle, 0, &changed, nullptr) == 0); + { + std::lock_guard lock(cfg->mutex); + Require(cfg->groups[0].attribute.width == 3840); + Require(cfg->groups[0].attribute.height == 2160); + } + + if (malformed) { + bool supportedThrew = false; + try { + (void)sceVideoOutIsOutputSupported(handle, 0xD000000Aull, &options, nullptr, 0); + } catch (const std::runtime_error& error) { + supportedThrew = std::string_view(error.what()).find("ANYPS5_VIDEO_OUT_MAX_WIDTH") != std::string_view::npos; + } + Require(supportedThrew); + bool configureThrew = false; + try { + (void)sceVideoOutConfigureOutput(handle, 0xD000000Aull, &options, nullptr, 0); + } catch (const std::runtime_error& error) { + configureThrew = std::string_view(error.what()).find("ANYPS5_VIDEO_OUT_MAX_WIDTH") != std::string_view::npos; + } + Require(configureThrew); + } + + Require(sceVideoOutClose(handle) == 0); + LibcRunShutdown_nid_postfix(); +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index c6338fa3be..25512fb4df 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -578,6 +578,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `ANYPS5_FULLSCREEN` | presence | `core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp` | | opens the display window fullscreen at the desktop resolution when set to a non-empty value other than 0; F11 still switches back | | `ANYPS5_INPUT_CONFIG` | string | `core/libs/prx/libSceVideoOut/src/InputMapping.cpp` | | path of the input mapping file that binds host keys and mouse to guest pad actions | | `ANYPS5_REQUIRE_DISPLAY` | presence | `core/libs/prx/libSceVideoOut/tests/Flip.cpp` | | makes video-out tests fail instead of skipping when no display or Vulkan device is available | +| `ANYPS5_VIDEO_OUT_MAX_HEIGHT` | integer | `core/libs/prx/libSceVideoOut/src/Output.cpp` | | host display height hint: when set to a positive integer below 2160, an unknown output mode is reported as unsupported so the title can fall back to the host resolution; a malformed or zero value makes output-mode queries throw; buffer attributes are left untouched | +| `ANYPS5_VIDEO_OUT_MAX_WIDTH` | integer | `core/libs/prx/libSceVideoOut/src/Output.cpp` | | host display width hint: when set to a positive integer below 3840, an unknown output mode is reported as unsupported so the title can fall back to the host resolution; a malformed or zero value makes output-mode queries throw; buffer attributes are left untouched | | `APS5_NO_PAD_INPUT` | presence | `core/libs/prx/libSceVideoOut/src/PadInput.cpp` | | ignores keyboard and mouse input so a measurement run is not advanced by a stray press | | `APS5_NO_WGI` | presence | `core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp` | | turns off SDL's Windows.Gaming.Input joystick backend at default hint priority, on Windows hosts where it faults a few seconds into every run | | `APS5_OUTPUT_RESOLUTION` | string | `core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp` | | `x` in pixels that replaces the host display's size when `sceVideoOutGetOutputStatus` and `sceVideoOutGetOutputStatusWithVrr` report the output resolution: 1 for a size below 3840 wide and 2160 high, 2 otherwise, as for the display; unset or empty follows the display, and any other value makes those calls throw |