Merge pull request #3886 from PrintOrb/fix/videoout-high-frame-rate
fix(videoout): support permitted high-frame-rate output for Quake II
This commit is contained in:
commit
44ab0c39ee
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:$<TARGET_FILE_DIR:libc>")
|
||||
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:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING
|
||||
"SDL video driver for VideoOut flip tests")
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -215,6 +215,8 @@ public:
|
|||
std::shared_ptr<VideoOutConfig> 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<void>& 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> flipQueue = std::make_shared<FlipQueue>();
|
||||
|
||||
DisplayWindow window;
|
||||
std::atomic<int> displayRefreshRate{0};
|
||||
|
||||
std::jthread presentThread;
|
||||
std::jthread vblankThread;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<uint64_t>::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<uint64_t>(req.flipRate + 1);
|
||||
require(req.cfg->lastFlipVblank <= std::numeric_limits<uint64_t>::max() - interval, "flip interval overflow");
|
||||
|
|
@ -555,6 +573,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) {
|
|||
void VideoOutDriver::presentLoop(std::stop_token token, std::promise<void>& 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<void>& 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<void>& star
|
|||
}
|
||||
|
||||
void VideoOutDriver::vblankLoop(std::stop_token token) {
|
||||
using Frame = std::chrono::duration<int64_t, std::ratio<1001, 60000>>;
|
||||
using Frame = std::chrono::duration<int64_t, std::ratio<1001, 120000>>;
|
||||
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<std::uint64_t>(frame));
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "[videoout] vblank failed: %s\n", error.what());
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
#include "prx/libSceVideoOut/include/Output.hpp"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
|
||||
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<int>(0x80290016);
|
||||
constexpr int invalidValue = static_cast<int>(0x80290001);
|
||||
constexpr int unknownMode = static_cast<int>(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");
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] = {};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<std::uint8_t> 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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "tests/VideoOutTestEnvironment.hpp"
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
|
||||
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<int>(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<double>(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<bool>(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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<const char*, const char*> 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';
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue