diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3e9c238d..724a3cfe0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -215,8 +215,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 - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers + - 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 - 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 544be3ea4..f54726afa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,9 +60,9 @@ jobs: with: key: ${{ matrix.platform }} save: false - - name: Install lavapipe for the GPU execution tests + - 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 + 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: Configure Linux if: matrix.platform == 'linux' run: cmake -S . -B build $CONFIGURE diff --git a/CMakeLists.txt b/CMakeLists.txt index 5cb4fb85c..78afe2fa4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ if(BUILD_TESTING) endif() option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF) +option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF) option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF) option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF) add_compile_definitions(APS5_ENABLE_TIMING_LOG=$) @@ -32,11 +33,27 @@ include(cmake/SystemDeps.cmake) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(NOT APPLE) +if(ANYPS5_ENABLE_ASAN) + if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux") + endif() + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$") + message(FATAL_ERROR "ANYPS5_ENABLE_ASAN requires GCC or Clang") + endif() + add_compile_options(-fsanitize=address -fno-omit-frame-pointer) + add_link_options(-fsanitize=address) + add_compile_definitions(ANYPS5_ENABLE_ASAN=1) +elseif(NOT APPLE) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++") endif() if(BUILD_TESTING) + add_test_executable(shader_cache_file_read_tests + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp) + target_include_directories(shader_cache_file_read_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler) + add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests) + set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} @@ -65,6 +82,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py) add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py) + add_test(NAME release_package COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py) endif() endif() @@ -93,6 +112,16 @@ if(NOT ANYPS5_USE_SYSTEM_DEPS) set(CMAKE_EXE_LINKER_FLAGS "") add_subdirectory(3rdparty/SDL2) set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}") + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + 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) + if(NOT ANYPS5_SDL_PIPEWIRE AND NOT ANYPS5_SDL_PULSEAUDIO AND NOT ANYPS5_SDL_ALSA) + message(FATAL_ERROR "SDL found no PipeWire, PulseAudio or ALSA development files, so audio output would be silent. " + "Install libpipewire-0.3-dev, libpulse-dev and libasound2-dev (Debian, Ubuntu) " + "or pipewire-devel, pulseaudio-libs-devel and alsa-lib-devel (Fedora), then rerun configuration.") + endif() + endif() endif() set(ARCHITECTURE x86_64) add_subdirectory(3rdparty/ffmpeg-core) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2da76ed8e..23a160828 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -55,6 +55,7 @@ target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) configure_windows_unwind(libc) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(libc PRIVATE LINKER:-z,now) + target_link_options(libc INTERFACE LINKER:--no-as-needed) endif() set(UNPATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs/unpatched) @@ -101,9 +102,9 @@ foreach(LIB_DIR ${LIBS_DIRS}) endif() if(LIB_NAME STREQUAL "libkernel") - target_link_libraries(${LIB_NAME} PRIVATE libc) + target_link_libraries(${LIB_NAME} PUBLIC libc) else() - target_link_libraries(${LIB_NAME} PRIVATE libkernel libc) + target_link_libraries(${LIB_NAME} PRIVATE libkernel PUBLIC libc) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Linux") target_link_options(${LIB_NAME} PRIVATE LINKER:-z,now) @@ -188,6 +189,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp) + target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc) + configure_windows_unwind(guest_signin_dialog_tests) + add_test(NAME guest_signin_dialog COMMAND guest_signin_dialog_tests) + if(WIN32) + set_tests_properties(guest_signin_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp) target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc) @@ -208,6 +219,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_rudp_pump_tests tests/GuestRudpPump.cpp) + target_include_directories(guest_rudp_pump_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_rudp_pump_tests PRIVATE libSceRudp libc) + configure_windows_unwind(guest_rudp_pump_tests) + add_test(NAME guest_rudp_pump COMMAND guest_rudp_pump_tests) + if(WIN32) + set_tests_properties(guest_rudp_pump PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp) target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc) @@ -329,4 +350,14 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_acm_tests tests/GuestAcm.cpp) + target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc) + configure_windows_unwind(guest_acm_tests) + add_test(NAME guest_acm COMMAND guest_acm_tests) + if(WIN32) + set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index bd63ff766..4269cee09 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -95,6 +95,14 @@ struct KernelAioRwRequest { std::int32_t fd; }; +struct KernelAioSchedulingParam { + std::int32_t schedulingWindowSize; + std::int32_t delayedCountLimit; + std::uint32_t enableSplit; + std::uint32_t splitSize; + std::uint32_t splitChunkSize; +}; + struct KernelBatchMapEntry { void* start; std::uint64_t offset; @@ -1253,7 +1261,7 @@ union NetCtlInfo { std::uint8_t opaque[256]; }; using NetCtlCallback = void (*)(int, void*); -struct HttpEpoll {}; +struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*); using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*); @@ -1563,6 +1571,13 @@ struct SaveDataBackup { std::uint8_t reserved[32]; }; +struct SaveDataConvert { + std::int32_t user_id; + std::int32_t pad; + const SceSaveDataDirName* dir_name; + std::uint8_t unknown[48]; +}; + struct AppContentInitParam { char reserved[32]; }; struct AppContentBootParam { @@ -1582,6 +1597,19 @@ struct ContentExportInitParam2 { std::int64_t reserved1; }; +struct ContentExportParam { + char title[257]; + char comment[257]; + char content_type[65]; +}; +static_assert(sizeof(ContentExportParam) == 579); + +struct ContentExportCallbackParam { + std::size_t content_length; + void* func; + void* user_data; +}; + struct ContentSearchInitParam { std::size_t memory_size; }; struct PngEncCreateParam { diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index e62e5934d..fd80c9e1a 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -53,13 +53,11 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) { return 0; } -int APS5_VABI sceAcm_FFT() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAcm_FFT(void) { return 0; } -int APS5_VABI sceAcm_Panner() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAcm_Panner(void) { return 0; } diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index de20362b0..a4692c619 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -11,10 +11,15 @@ extern "C" { -// unknown signature -void* APS5_VABI sceAgcAcbAtomicGds_0900(void) { - NotImplemented_nid_no_patch(__func__); - return nullptr; +std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { + const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) | + ((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) | + ((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u); + return Agc::Command::Emit(buf, 0x1du, { + operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index, + mask & 0x00ff00ffu, data, static_cast(compareData), + static_cast(compareData >> 32u), static_cast(extraData), + static_cast(extraData >> 32u)}, __func__); } std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) { diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp index 8977432c4..eefbb3120 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -13,8 +13,7 @@ extern "C" { // unknown signature -APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY); -void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(void) { +void* APS5_VABI sceAgcDcbAtomicGds_0900(void) { NotImplemented_nid_no_patch(__func__); return nullptr; } diff --git a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp index 14c803b8f..4248c29fd 100644 --- a/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp +++ b/core/libs/prx/libSceAgc/Shader/include/ShaderConstants.hpp @@ -19,6 +19,7 @@ constexpr std::uint32_t VGT_SHADER_STAGES_EN = 0x2D5u; constexpr std::uint32_t VGT_PRIMITIVE_TYPE = 0x242u; constexpr std::uint32_t GE_CNTL = 0x25Bu; +constexpr std::uint32_t GE_USER_VGPR1 = 0x25Cu; constexpr std::uint32_t GE_USER_VGPR_EN = 0x262u; constexpr std::uint32_t GE_PC_ALLOC = 0x260u; diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index b3b0da8f4..608c859ad 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -53,10 +53,21 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS : CreateInterpolantValue(psWord, gsSemantic); } -int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) { +std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) { + const std::uint32_t f16 = (psWord >> 20u) & 0x3u; + if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) { + throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown"); + } + return CreateInterpolantValueSplitF16(psWord, gsSemantic); +} + +int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) { if (regs == nullptr) { throw std::runtime_error(std::string(fn) + ": regs is null"); } + if (ps == nullptr && !fillIdentityTail) { + throw std::runtime_error(std::string(fn) + ": ps is null"); + } if (ps != nullptr && ps->num_input_semantics > 32) { throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } @@ -66,13 +77,14 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* auto* output = regs; regs = values.data(); if (ps == nullptr || ps->num_input_semantics == 0) { - FillIdentityInterpolants(regs, 0); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0; + if (fillIdentityTail) FillIdentityInterpolants(regs, 0); if (ps != nullptr) { - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); } transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -98,11 +110,12 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic)); } - FillIdentityInterpolants(regs, ps->num_input_semantics); - AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {}); + const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : ps->num_input_semantics; + if (fillIdentityTail) FillIdentityInterpolants(regs, ps->num_input_semantics); + AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {}); AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0); transaction.Commit(); - std::copy(values.begin(), values.end(), output); + std::copy_n(values.begin(), count, output); return 0; } @@ -111,12 +124,17 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* extern "C" { int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true); } APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping); int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) { - return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16); + return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true); +} + +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) { + return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false); } } diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 9963ae2db..587ae15a3 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -11,6 +11,18 @@ #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" +namespace { + +void RequireSpecialOffset(const ShaderRegister& reg, std::uint32_t expected, const char* name) { + if (reg.offset != expected) { + char message[160]; + std::snprintf(message, sizeof(message), "sceAgcCreatePrimState: %s special register at offset 0x%x, expected 0x%x", name, static_cast(reg.offset), static_cast(expected)); + throw std::runtime_error(message); + } +} + +} + extern "C" { int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_regs, const Shader* hs, const Shader* gs, std::uint32_t prim_type) { @@ -25,21 +37,17 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers"); } (void)GraphicsPrimTypeToGsOut(prim_type); - const auto validStages = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN; }; - if (!validStages(gs) || (hs != nullptr && !validStages(hs))) { - throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); - } - if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) { - throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets"); - } + RequireSpecialOffset(gs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + if (hs != nullptr) RequireSpecialOffset(hs->specials->vgt_shader_stages_en, ShaderRegs::VGT_SHADER_STAGES_EN, "VGT_SHADER_STAGES_EN"); + RequireSpecialOffset(gs->specials->ge_cntl, ShaderRegs::GE_CNTL, "GE_CNTL"); std::array contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type}; if (hs != nullptr) { contextValues[0].value |= hs->specials->vgt_shader_stages_en.value; } if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) { - if (gs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(gs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); } else if (hs != nullptr) { - if (hs->specials->vgt_gs_out_prim_type.offset != ShaderRegs::VGT_GS_OUT_PRIM_TYPE) throw std::runtime_error(std::string(__func__) + ": invalid context register offsets"); + RequireSpecialOffset(hs->specials->vgt_gs_out_prim_type, ShaderRegs::VGT_GS_OUT_PRIM_TYPE, "VGT_GS_OUT_PRIM_TYPE"); contextValues[1] = hs->specials->vgt_gs_out_prim_type; } else { contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)}; diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 860242c9d..0b85ef7ff 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -90,10 +90,4 @@ int APS5_VABI sceAgcGetShaderInstrumentation() { return 0; } -APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); -int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index fd59ffa94..d3d9c7f83 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -37,6 +37,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr); extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw); extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); namespace { @@ -404,6 +405,101 @@ void testMemory() { check(storage.words == beforeMask, "invalid mask patch modified packet memory"); } +struct AtomicGdsAllocation { + Storage storage; + std::uint32_t capacity = 15; + std::uint32_t calls = 0; + bool accepted = true; +}; + +bool APS5_VABI growAtomicGds(CommandBuffer* buffer, std::uint32_t count, void* userData) { + auto& allocation = *static_cast(userData); + check(count == 15, "atomic GDS allocation omitted packet or reserved words"); + ++allocation.calls; + if (!allocation.accepted) return false; + buffer->bottom = allocation.storage.words.data(); + buffer->top = buffer->bottom + allocation.capacity; + buffer->cursor_up = buffer->bottom; + buffer->cursor_down = buffer->top; + return true; +} + +void testAtomicGds() { + Storage storage; + storage.words.fill(0xdeadbeefu); + storage.buffer.cursor_up += 2; + auto* packet = sceAgcAcbAtomicGds_0900(&storage.buffer, 0xffffffffu, 1, 0xffffffffu, 0xffffffffu, + 0xabcd, 0xfedc, 0x12345678u, 0x87654321u, 0xfedcba9876543210ull, 0x0123456789abcdefull); + const std::array expected{ + 0xc0091d00u, 0x001500ffu, 0x13fu, 0xabcdu, 0xfedcu, 0x00340078u, + 0x87654321u, 0x76543210u, 0xfedcba98u, 0x89abcdefu, 0x01234567u}; + check(packet == storage.words.data() + 2, "atomic GDS returned the wrong packet address"); + check(std::equal(expected.begin(), expected.end(), packet), "atomic GDS packet or guest ABI mismatch"); + check(storage.buffer.cursor_up == packet + expected.size(), "atomic GDS did not advance by eleven words"); + check(storage.words[0] == 0xdeadbeefu && storage.words[1] == 0xdeadbeefu && + std::all_of(storage.words.begin() + 13, storage.words.end(), [](auto word) { return word == 0xdeadbeefu; }), + "atomic GDS wrote outside its packet"); + + const std::array, 9> controls{{ + {0, 0, 0}, {0, 1, 0x00020000u}, {0, 2, 0}, {0, 0xffffffffu, 0}, + {0x20u, 1, 0x00040020u}, {0x40u, 1, 0x00120040u}, + {0x200u, 1, 0x00030000u}, {0xfffffd00u, 0, 0}, {0xffu, 1, 0x001400ffu}}}; + for (const auto& entry : controls) { + Storage controlStorage; + auto* controlPacket = sceAgcAcbAtomicGds_0900(&controlStorage.buffer, entry[0], entry[1], + 0xfffffffeu, 0xffffffc0u, 0, 0, 0xff00ff00u, 0, 0, 0); + std::array controlExpected{}; + controlExpected[0] = 0xc0091d00u; + controlExpected[1] = entry[2]; + check(std::equal(controlExpected.begin(), controlExpected.end(), controlPacket), "atomic GDS control or field masking mismatch"); + } + + auto emit = [](CommandBuffer* buffer) { + return sceAgcAcbAtomicGds_0900(buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + }; + Storage exact; + exact.buffer.cursor_down = exact.words.data() + 15; + exact.buffer.reserved_dw = 4; + check(emit(&exact.buffer) == exact.words.data() && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS rejected an exact fit with reserved space"); + const auto exactBefore = exact.words; + expectFailure([&] { emit(&exact.buffer); }); + check(exact.words == exactBefore && exact.buffer.cursor_up == exact.words.data() + 11, + "atomic GDS overwrote reserved space"); + Storage exhausted; + exhausted.buffer.cursor_down = exhausted.words.data() + 10; + const auto before = exhausted.words; + expectFailure([&] { emit(&exhausted.buffer); }); + check(exhausted.words == before && exhausted.buffer.cursor_up == exhausted.words.data(), + "failed atomic GDS allocation modified the buffer"); + expectFailure([&] { emit(nullptr); }); + + Storage noDown; + noDown.buffer.top = noDown.words.data() + 11; + noDown.buffer.cursor_down = nullptr; + check(emit(&noDown.buffer) == noDown.words.data() && noDown.buffer.cursor_up == noDown.buffer.top, + "atomic GDS did not honor a buffer with no down cursor"); + + AtomicGdsAllocation allocation; + CommandBuffer growing{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &allocation, 4}; + check(emit(&growing) == allocation.storage.words.data() && allocation.calls == 1 && + growing.cursor_up == allocation.storage.words.data() + 11, "atomic GDS did not use the guest allocation callback"); + const std::array zeroPacket{0xc0091d00u}; + check(std::equal(zeroPacket.begin(), zeroPacket.end(), allocation.storage.words.begin()), + "atomic GDS callback allocation produced the wrong packet"); + for (const bool accepted : {false, true}) { + AtomicGdsAllocation failure; + failure.accepted = accepted; + failure.capacity = 14; + const auto failureBefore = failure.storage.words; + CommandBuffer failed{nullptr, nullptr, nullptr, nullptr, growAtomicGds, &failure, 4}; + expectFailure([&] { emit(&failed); }); + check(failure.calls == 1 && failure.storage.words == failureBefore && + failed.cursor_up == (accepted ? failure.storage.words.data() : nullptr), + "atomic GDS wrote a packet after failed or insufficient allocation"); + } +} + void testDebugRaiseException() { int result = -1; try { @@ -459,6 +555,7 @@ int main(int argc, char** argv) { testRegisterRange(); testPacketPayloadAddress(); testMemory(); + testAtomicGds(); testDebugRaiseException(); testDefaults(); LibcRunShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp index 1ea1d6ca2..b176809a4 100644 --- a/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/tests/InterpolantMapping.cpp @@ -13,12 +13,14 @@ extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps); +extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps); namespace { using Registers = std::array; const Shader* mappedPixel = nullptr; Registers preparedMapping{}; +std::size_t preparedCount = 0; unsigned mappings = 0; unsigned links = 0; @@ -65,6 +67,11 @@ void testIdentity() { regs = filled(); check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed"); checkIdentity(regs, 0, "mapping without inputs is not the identity"); + regs = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed"); + check(preparedCount == 0u, "update without inputs prepared registers"); + const auto untouched = filled(); + check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers"); } void testMapping() { @@ -99,6 +106,15 @@ void testMapping() { check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed"); checkIdentity(split, static_cast(inputs.size()), "unused interpolants are not the identity"); + auto update = filled(); + const auto untouched = filled(); + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(preparedCount == inputs.size(), "update prepared registers past its inputs"); + check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers"); + check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping"); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + auto regular = filled(); check(sceAgcCreateInterpolantMapping_0100(regular.data(), &gs, &ps) == 0, "mapping failed"); check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers"); @@ -106,6 +122,51 @@ void testMapping() { checkIdentity(regular, static_cast(inputs.size()), "unused interpolants are not the identity"); } +void testUpdateMapping() { + std::array inputs{semantic(1, 0, 0), semantic(2, 0, 1), semantic(3, 0, 2), semantic(4, 0, 3)}; + inputs[0].is_custom = 1; + inputs[0].default_value = 2; + inputs[1].default_value = 1; + inputs[1].default_value_hi = 1; + inputs[2].default_value_hi = 2; + inputs[3].is_custom = 1; + inputs[3].default_value = 3; + inputs[3].default_value_hi = 3; + std::array outputs{semantic(1, 5, 0), semantic(2, 7, 1), semantic(4, 9, 3)}; + Shader gs{}; + gs.output_semantics = outputs.data(); + gs.num_output_semantics = static_cast(outputs.size()); + Shader ps{}; + ps.input_semantics = inputs.data(); + ps.num_input_semantics = static_cast(inputs.size()); + + auto update = filled(); + const auto untouched = filled(); + check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed"); + check(update[0].value == 0x00000625u, "custom interpolant update changed"); + check(update[1].value == 0x01380107u, "low f16 half update changed"); + check(update[2].value == 0x02480220u, "unmatched high f16 half update changed"); + check(update[3].value == 0x03680709u, "full f16 interpolant update changed"); + for (std::uint32_t i = 0; i < inputs.size(); ++i) { + check(update[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "updated register offset changed"); + } + const std::size_t written = inputs.size() * sizeof(ShaderRegister); + check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs"); + + auto create = filled(); + check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed"); + check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping"); + checkIdentity(create, static_cast(inputs.size()), "unused interpolants are not the identity"); + + for (const std::uint32_t index : {1u, 3u}) { + inputs[index].default_value = 0; + update = filled(); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); }); + check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers"); + inputs[index].default_value = inputs[index].default_value_hi; + } +} + void testRejections() { std::array inputs{}; Shader gs{}; @@ -117,18 +178,23 @@ void testRejections() { const auto saved = regs; expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); }); check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers"); ps.num_input_semantics = 1; expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); }); expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); }); + expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); }); } } extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { - check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); + check(shader != nullptr && context.size() <= preparedMapping.size() && primitive.empty(), "invalid mapping preparation call"); mappedPixel = shader; std::copy(context.begin(), context.end(), preparedMapping.begin()); + preparedCount = context.size(); ++mappings; } @@ -141,8 +207,9 @@ int main() { try { testIdentity(); testMapping(); + testUpdateMapping(); testRejections(); - check(mappings == 3u && links == 2u, "mapping preparation call count changed"); + check(mappings == 7u && links == 5u, "mapping preparation call count changed"); LibcRunShutdown_nid_postfix(); std::puts("AGC interpolant mapping tests passed"); return 0; diff --git a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp index c02780707..ad88c7a41 100644 --- a/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp +++ b/core/libs/prx/libSceAgc/tests/ShaderHelpers.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t); @@ -48,6 +49,30 @@ void OptionalGeometryOutputRegister(ShaderSpecialRegs& special, const Shader& ve special.vgt_shader_stages_en.value = 0; special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2}; } + +void HandBuiltUserVgprRegister() { + using namespace ShaderRegs; + ShaderSpecialRegs handBuilt{}; + handBuilt.ge_cntl = {GE_CNTL, 0x100u}; + handBuilt.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0x2412010u}; + handBuilt.vgt_gs_out_prim_type = {0x2ceu, 2}; + handBuilt.ge_user_vgpr_en = {GE_USER_VGPR1, 0}; + Shader preRaster{}; + preRaster.type = static_cast(ShaderBinaryType::Gs); + preRaster.specials = &handBuilt; + std::array context{}; + std::array primitive{}; + preparations = 0; + Require(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4) == 0); + Require(context[0].offset == VGT_SHADER_STAGES_EN && context[0].value == 0x2412010u && context[1].offset == VGT_GS_OUT_PRIM_TYPE && context[1].value == static_cast(GsOutputPrimitiveType::Triangles)); + Require(primitive[0].offset == GE_CNTL && primitive[0].value == 0x100u && primitive[1].offset == GE_USER_VGPR1 && primitive[1].value == 0 && primitive[2].offset == VGT_PRIMITIVE_TYPE && primitive[2].value == 4); + Require(preparations == 1 && stageCount == 1 && preparedPrimitive[1].offset == GE_USER_VGPR1 && preparedPrimitive[1].value == 0); + handBuilt.ge_cntl.offset = 0x25cu; + std::string message; + try { static_cast(sceAgcCreatePrimState(context.data(), primitive.data(), nullptr, &preRaster, 4)); } + catch (const std::runtime_error& error) { message = error.what(); } + Require(message.find("GE_CNTL") != std::string::npos && message.find("0x25c") != std::string::npos); +} } extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { @@ -168,6 +193,7 @@ int main() { Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, nullptr) == 0); Require(preparations == 0 && mappings == 0 && links == 0); OptionalGeometryOutputRegister(special, vertex, hull); + HandBuiltUserVgprRegister(); std::cout << "shader helper preparation tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index bef499a10..20c42fc92 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -111,6 +111,7 @@ add_custom_command( set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ComputePipelineCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayBuffer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/DisplayFormat.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/DeviceAccess.cpp @@ -173,6 +174,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -313,8 +315,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_wait_shutdown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_GPU_WAIT_TIMEOUT_MS=20" TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_driver_resolution_shutdown COMMAND agc_driver_wait_shutdown_tests --resolution-shutdown) + set_tests_properties(agc_driver_resolution_shutdown PROPERTIES TIMEOUT 10 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_driver_wait_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_wait_shutdown agc_driver_resolution_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -485,6 +489,11 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_masked_select_tests) +add_test_executable(agc_vector_window_tests EXCLUDE_FROM_ALL tests/VectorWindow.cpp) +target_link_libraries(agc_vector_window_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vector_window_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vector_window_tests) + add_test_executable(agc_constant_bit_test_tests EXCLUDE_FROM_ALL tests/ConstantBitTest.cpp) target_link_libraries(agc_constant_bit_test_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_constant_bit_test_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -539,6 +548,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_vop3_interp_decode_tests EXCLUDE_FROM_ALL tests/Vop3InterpDecode.cpp) +target_link_libraries(agc_vop3_interp_decode_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vop3_interp_decode_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vop3_interp_decode_tests) +if(BUILD_TESTING) + set_target_properties(agc_vop3_interp_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_vop3_interp_decode COMMAND agc_vop3_interp_decode_tests) + if(WIN32) + set_tests_properties(agc_vop3_interp_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp) target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) target_link_libraries(agc_pass_hazards_tests PRIVATE libc) @@ -775,6 +797,13 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only) + add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only) + set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_storage_baseline_settled PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) set_tests_properties(agc_storage_baseline_settled PROPERTIES ENVIRONMENT_MODIFICATION @@ -786,8 +815,12 @@ if(BUILD_TESTING) set_tests_properties(agc_unit_shadow PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000") add_test(NAME agc_descriptor_pool_recycling COMMAND agc_driver_recorder_tests --descriptor-pool-only) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) + set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) + set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -837,6 +870,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/DepthSurfaceReuse.cpp Graphics/src/Draw.cpp Graphics/src/DepthSurface.cpp + Graphics/src/MultisampleTarget.cpp Graphics/src/Pipeline.cpp Graphics/src/PipelineLibrary.cpp Graphics/src/State.cpp @@ -994,15 +1028,17 @@ if(BUILD_TESTING) endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) + set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_frontend_pair COMMAND agc_shader_memory_tests --frontend-pair) add_test(NAME agc_shader_frontend_pair_off COMMAND agc_shader_memory_tests --frontend-pair) set_tests_properties(agc_shader_frontend_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") set_tests_properties(agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_FRONTEND_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests") + foreach(agcTest IN ITEMS agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vector_window_tests agc_constant_bit_test_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests ${agcExecutionTests}) + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") @@ -1014,6 +1050,39 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests) + set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + set_tests_properties(agc_driver_emulated_color_compare agc_driver_emulated_color_compare_srgb_decode PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) + set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + add_test(NAME agc_shared_write_partial_collect_two_pass COMMAND agc_shared_write_tests --partial-collect) + set_tests_properties(agc_shared_write_partial_collect_two_pass PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_SINGLE_PASS_COLLECT=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + set_tests_properties(agc_shared_write_partial_collect agc_shared_write_partial_collect_two_pass PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + set_target_properties(agc_driver_pipeline_optimized_readback_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + foreach(pipelineReadbackCase IN ITEMS d32 d16 color32 color64) + foreach(pipelineReadbackMode IN ITEMS default fast) + set(pipelineReadbackTest agc_driver_pipeline_optimized_readback_${pipelineReadbackCase}) + set(pipelineReadbackEnvironment "ANYPS5_NO_SHADER_CACHE=1") + if(pipelineReadbackMode STREQUAL "fast") + string(APPEND pipelineReadbackTest "_fast") + list(APPEND pipelineReadbackEnvironment "APS5_NO_PIPELINE_LTO=1") + endif() + add_test(NAME ${pipelineReadbackTest} COMMAND agc_driver_pipeline_optimized_readback_tests ${pipelineReadbackCase}) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT "${pipelineReadbackEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(${pipelineReadbackTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endforeach() add_test(NAME agc_driver_f64_denorm_preserve_spirv COMMAND agc_driver_f64_fma_subnormals_tests --verify-spv-denorm-mode) set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) @@ -1021,8 +1090,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_structurizer_intersection_off COMMAND agc_structurizer_tests) set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_DOM_INTERSECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_structurizer_iteration_selection_off COMMAND agc_structurizer_tests) + set_tests_properties(agc_structurizer_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_control_flow_iteration_selection_off COMMAND agc_shader_control_flow_tests) + set_tests_properties(agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_ITERATION_SELECTION=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_structurizer_intersection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_structurizer_intersection_off agc_structurizer_iteration_selection_off agc_shader_control_flow_iteration_selection_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_gated_dispatch_capture_short_timeout COMMAND agc_driver_gated_dispatch_capture_tests) set_tests_properties(agc_driver_gated_dispatch_capture_short_timeout PROPERTIES @@ -1035,8 +1108,8 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_provoking_vertex PROPERTIES TIMEOUT 30) set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE) set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF) - foreach(flatAperturesPart RANGE 5) - add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/6) + foreach(flatAperturesPart RANGE 13) + add_test(NAME agc_driver_flat_apertures_${flatAperturesPart} COMMAND agc_driver_flat_apertures_tests --part ${flatAperturesPart}/14) set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) set_tests_properties(agc_driver_flat_apertures_${flatAperturesPart} PROPERTIES ENVIRONMENT_MODIFICATION @@ -1046,8 +1119,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) + add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) + add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure) @@ -1105,7 +1182,7 @@ if(BUILD_TESTING) endif() endforeach() endforeach() - foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable) + foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync) set(preparedTest "agc_driver_prepared_shaders_${preparedMode}") if(preparedMode STREQUAL "indirect") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect) @@ -1113,6 +1190,8 @@ if(BUILD_TESTING) add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred) elseif(preparedMode STREQUAL "deferred_undecodable") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable) + elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync") + add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup) elseif(preparedMode STREQUAL "debug_failure") add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration) set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30) @@ -1122,6 +1201,8 @@ if(BUILD_TESTING) set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(preparedMode STREQUAL "uncached") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1") + elseif(preparedMode STREQUAL "registered_workgroup_sync") + set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1") elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure") set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1") endif() @@ -1241,6 +1322,60 @@ endif() set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shader_replay) +add_executable(agc_compute_pipeline_replay EXCLUDE_FROM_ALL tools/ComputePipelineReplay.cpp) +target_include_directories(agc_compute_pipeline_replay PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_compute_pipeline_replay PRIVATE libSceAgcDriver libc) +set_target_properties(agc_compute_pipeline_replay PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_compute_pipeline_replay) + +if(BUILD_TESTING) + add_test_executable(agc_compute_pipeline_capture_tests tests/ComputePipelineCapture.cpp Execution/src/ComputePipelineCapture.cpp) + target_include_directories(agc_compute_pipeline_capture_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) + target_link_libraries(agc_compute_pipeline_capture_tests PRIVATE libc) + set_target_properties(agc_compute_pipeline_capture_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_capture_tests) + add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags) + set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) + set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_compute_pipeline_capture PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + +if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS) + add_test_executable(agc_compute_pipeline_replay_tests tests/ComputePipelineReplay.cpp) + target_include_directories(agc_compute_pipeline_replay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} + ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR} + ) + target_link_libraries(agc_compute_pipeline_replay_tests PRIVATE libSceAgcDriver libc) + set_target_properties(agc_compute_pipeline_replay_tests PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(agc_compute_pipeline_replay_tests) + add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests) + set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests) + add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND} + -DFIXTURE=$ + -DREPLAY=$ + -DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake) + set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device") + if(WIN32) + set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(BUILD_TESTING) add_test_executable(texture_format_lookup_tests ${LIBS_INCLUDE_DIR}/tests/TextureFormatLookup.cpp) target_include_directories(texture_format_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp new file mode 100644 index 000000000..270c29c79 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp @@ -0,0 +1,77 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct ComputeDescriptorBinding { + std::uint32_t binding = 0; + VkDescriptorType type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + std::uint32_t count = 1; + VkShaderStageFlags stages = VK_SHADER_STAGE_COMPUTE_BIT; + VkDescriptorBindingFlags flags = 0; +}; + +struct ComputeDescriptorSetLayout { + VkDescriptorSetLayoutCreateFlags flags = 0; + std::vector bindings; +}; + +struct ComputeSpecialization { + std::uint32_t id = 0; + std::uint32_t value = 0; +}; + +struct ComputePipelineCapture { + static constexpr std::uint32_t Version = 1; + static constexpr std::uint32_t MaxSpirvWords = 8u * 1024u * 1024u; + static constexpr std::size_t MaxFileBytes = 36u * 1024u * 1024u; + std::vector spirv; + std::vector sets; + std::vector pushConstants; + std::vector specialization; + std::uint32_t requiredSubgroupSize = 0; + VkPipelineCreateFlags flags = 0; + VkPipelineShaderStageCreateFlags stageFlags = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + + void Validate() const; + std::vector Encode() const; + static ComputePipelineCapture Decode(std::span bytes); + static ComputePipelineCapture Read(const std::filesystem::path& path); + std::filesystem::path Write(const std::filesystem::path& directory) const; + std::string Identity() const; +}; + +struct ComputePipelineReplayResult { + double moduleMilliseconds = 0; + double layoutMilliseconds = 0; + double pipelineMilliseconds = 0; + double cacheMilliseconds = 0; + double totalMilliseconds = 0; + std::string deviceName; + std::uint32_t vendorId = 0; + std::uint32_t deviceId = 0; + std::uint32_t driverVersion = 0; + std::uint32_t apiVersion = 0; + std::array pipelineCacheUuid{}; + std::size_t initialCacheBytes = 0; + std::vector cache; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index cc387b5e7..b5659313b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -343,6 +343,7 @@ private: std::uint64_t queue0Executing = 0; std::atomic orderHolders{0}; std::atomic runningWorkers{0}; + std::atomic idleEpoch{0}; std::atomic queue0Awaited{0}; std::atomic flipsAhead{0}; std::atomic flipHolders{0}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 7ccb5d690..f0ee74af4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -92,6 +92,7 @@ std::shared_ptr ProgramSnapshot(const ShaderRegistry& shad void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); +bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index ba201ca15..5321424ce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -183,6 +183,7 @@ unsigned long long CodeOffset(const void* address); // every read and store through this interface calls first. Code that reads guest memory directly // calls FlushGpuWrites itself. void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes)); +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)); void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd // while the call stores its page states, moved past it after, so a reader that loads an even diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp index 57ce88164..98fd3cbad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp @@ -16,6 +16,7 @@ public: ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete; PreparedShaderState& Edit(const ShaderSnapshot& snapshot); const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const; + void Hold(); void Commit(); private: diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 37a54bcdd..fe9ea5117 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" #include "prx/libSceAgcDriver/Execution/include/Recipe.hpp" +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" @@ -31,6 +32,9 @@ struct RecordedDispatch; class VulkanDevice { public: explicit VulkanDevice(const PresentationWindow* window = nullptr); + struct CompileOnly {}; + explicit VulkanDevice(CompileOnly); + ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache = {}); ~VulkanDevice(); VulkanDevice(const VulkanDevice&) = delete; VulkanDevice& operator=(const VulkanDevice&) = delete; @@ -57,6 +61,7 @@ public: // mutex through the flush hook, so a thread submitting on another queue's behalf (a WAIT_REG_MEM // poller, a compute worker's between-packet flush) passes false: it must only submit, never wait. void SubmitRecorded(bool reapFirst = true); + void StoreIdleResults(bool reapFirst = true); // Unlocked drain: SubmitAndEpoch (under the mutex) returns the serial covering every recorded // batch (0: nothing in flight); WaitRecorded(serial) waits for it WITHOUT the mutex (the caller // holds a shared_ptr to this device); ReapRecorded(serial) (under the mutex) runs the completions @@ -296,6 +301,8 @@ public: void EnqueueDraw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}); private: + VulkanDevice(const PresentationWindow* window, bool compileOnly); + friend struct VulkanDeviceTestAccess; // The device's Graphics::Context: a copy of the one built at setup (its instance functions // resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew. Graphics::Context graphicsContext() const; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp new file mode 100644 index 000000000..6137a82dc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ComputePipelineCapture.cpp @@ -0,0 +1,265 @@ +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +void RequireCapture(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string("compute pipeline capture: ") + message); +} + +std::string Hash(std::span bytes) { + std::uint64_t hash = 14695981039346656037ull; + for (const auto value : bytes) { + hash ^= std::to_integer(value); + hash *= 1099511628211ull; + } + std::ostringstream result; + result << std::hex << std::setfill('0') << std::setw(16) << hash; + return result.str(); +} + +class Encoder { +public: + void Word(std::uint32_t value) { + for (unsigned shift = 0; shift < 32; shift += 8) bytes.push_back(static_cast((value >> shift) & 0xffu)); + } + void String(const std::string& value) { + Word(static_cast(value.size())); + for (const auto character : value) bytes.push_back(static_cast(static_cast(character))); + } + std::vector bytes; +}; + +class Decoder { +public: + explicit Decoder(std::span bytes) : bytes(bytes) {} + std::uint32_t Word() { + RequireCapture(bytes.size() - cursor >= 4, "truncated integer"); + std::uint32_t value = 0; + for (unsigned shift = 0; shift < 32; shift += 8) value |= std::to_integer(bytes[cursor++]) << shift; + return value; + } + std::uint32_t Count(std::uint32_t maximum, std::size_t minimumBytes) { + const auto count = Word(); + RequireCapture(count <= maximum && count <= (bytes.size() - cursor) / minimumBytes, "count exceeds record bounds"); + return count; + } + std::string String() { + const auto count = Count(VK_MAX_PHYSICAL_DEVICE_NAME_SIZE - 1, 1); + std::string result; + for (std::uint32_t index = 0; index < count; ++index) result.push_back(static_cast(std::to_integer(bytes[cursor++]))); + return result; + } + bool Finished() const { return cursor == bytes.size(); } +private: + std::span bytes; + std::size_t cursor = 0; +}; + +std::vector ReadBytes(const std::filesystem::path& path) { + const auto size = std::filesystem::file_size(path); + RequireCapture(size <= ComputePipelineCapture::MaxFileBytes, "file exceeds 36 MiB"); + std::ifstream input(path, std::ios::binary); + RequireCapture(input.good(), "cannot open capture"); + std::vector bytes(static_cast(size)); + input.read(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + RequireCapture(input.good() && input.peek() == std::char_traits::eof(), "capture changed while reading"); + return bytes; +} + +} + +void ComputePipelineCapture::Validate() const { + RequireCapture(spirv.size() >= 5 && spirv.size() <= MaxSpirvWords, "SPIR-V word count out of bounds"); + RequireCapture(spirv[0] == 0x07230203u && spirv[1] >= 0x00010000u && spirv[1] <= 0x00010600u && (spirv[1] & 0xffu) == 0 && spirv[3] != 0 && spirv[3] <= 0x003fffffu && spirv[4] == 0, "invalid SPIR-V header"); + bool mainEntry = false; + for (std::size_t cursor = 5; cursor < spirv.size();) { + const auto count = spirv[cursor] >> 16u; + RequireCapture(count != 0 && count <= spirv.size() - cursor, "truncated SPIR-V instruction"); + if ((spirv[cursor] & 0xffffu) == 15u) { + RequireCapture(count >= 5 && spirv[cursor + 1] == 5u && spirv[cursor + 2] != 0 && spirv[cursor + 2] < spirv[3] && spirv[cursor + 3] == 0x6e69616du && spirv[cursor + 4] == 0, "only a compute main entry point is supported"); + RequireCapture(!mainEntry, "multiple entry points are unsupported"); + mainEntry = true; + } + cursor += count; + } + RequireCapture(mainEntry, "compute main entry point missing"); + RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name"); + RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags"); + RequireCapture(stageFlags == 0, "unsupported shader stage flags"); + RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size"); + RequireCapture(sets.size() <= 8, "too many descriptor sets"); + std::size_t totalBindings = 0; + for (const auto& layout : sets) { + RequireCapture((layout.flags & ~VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) == 0, "unsupported descriptor layout flags"); + RequireCapture(layout.bindings.size() <= 4096, "too many descriptor bindings"); + totalBindings += layout.bindings.size(); + std::set used; + const auto highest = layout.bindings.empty() ? 0u : std::ranges::max(layout.bindings, {}, &ComputeDescriptorBinding::binding).binding; + for (const auto& binding : layout.bindings) { + RequireCapture(used.insert(binding.binding).second, "duplicate descriptor binding"); + RequireCapture(binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER || binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE || binding.type == VK_DESCRIPTOR_TYPE_SAMPLER, "unsupported descriptor type"); + RequireCapture(binding.count != 0 && binding.count <= 1048576, "invalid descriptor count"); + RequireCapture(binding.stages == VK_SHADER_STAGE_COMPUTE_BIT, "unsupported descriptor stage flags"); + constexpr VkDescriptorBindingFlags supported = VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT | VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + RequireCapture((binding.flags & ~supported) == 0, "unsupported descriptor binding flags"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT) == 0 || (binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER && (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0), "unsupported update-after-bind descriptor"); + RequireCapture((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || binding.binding == highest, "variable descriptor count must use the highest binding"); + } + } + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + RequireCapture(pushConstants.size() <= 1, "too many compute push constant ranges"); + for (const auto& range : pushConstants) RequireCapture(range.stageFlags == VK_SHADER_STAGE_COMPUTE_BIT && range.size != 0 && range.size <= 4096 && range.offset <= 4096 - range.size && (range.offset % 4) == 0 && (range.size % 4) == 0, "invalid push constant range"); + RequireCapture(specialization.size() <= 4096, "too many specialization values"); + std::set specializationIds; + for (const auto& value : specialization) RequireCapture(specializationIds.insert(value.id).second, "duplicate specialization ID"); +} + +std::vector ComputePipelineCapture::Encode() const { + Validate(); + Encoder output; + output.Word(0x35535041u); + output.Word(0x504d4f43u); + output.Word(Version); + output.Word(flags); + output.Word(stageFlags); + output.Word(requiredSubgroupSize); + output.Word(vendorId); + output.Word(deviceId); + output.Word(driverVersion); + output.Word(apiVersion); + for (const auto value : pipelineCacheUuid) output.Word(value); + output.String(deviceName); + output.Word(static_cast(sets.size())); + for (const auto& layout : sets) { + output.Word(layout.flags); + output.Word(static_cast(layout.bindings.size())); + for (const auto& binding : layout.bindings) { + output.Word(binding.binding); + output.Word(static_cast(binding.type)); + output.Word(binding.count); + output.Word(binding.stages); + output.Word(binding.flags); + } + } + output.Word(static_cast(pushConstants.size())); + for (const auto& range : pushConstants) { + output.Word(range.stageFlags); + output.Word(range.offset); + output.Word(range.size); + } + output.Word(static_cast(specialization.size())); + for (const auto& value : specialization) { + output.Word(value.id); + output.Word(value.value); + } + output.Word(static_cast(spirv.size())); + for (const auto word : spirv) output.Word(word); + RequireCapture(output.bytes.size() <= MaxFileBytes, "encoded capture exceeds size bound"); + return std::move(output.bytes); +} + +ComputePipelineCapture ComputePipelineCapture::Decode(std::span bytes) { + RequireCapture(bytes.size() <= MaxFileBytes, "record exceeds 36 MiB"); + Decoder input(bytes); + RequireCapture(input.Word() == 0x35535041u && input.Word() == 0x504d4f43u, "invalid file signature"); + RequireCapture(input.Word() == Version, "unsupported file version"); + ComputePipelineCapture result; + result.flags = input.Word(); + result.stageFlags = input.Word(); + result.requiredSubgroupSize = input.Word(); + result.vendorId = input.Word(); + result.deviceId = input.Word(); + result.driverVersion = input.Word(); + result.apiVersion = input.Word(); + for (auto& value : result.pipelineCacheUuid) { + const auto word = input.Word(); + RequireCapture(word <= 255, "invalid cache UUID byte"); + value = static_cast(word); + } + result.deviceName = input.String(); + result.sets.resize(input.Count(8, 8)); + std::size_t totalBindings = 0; + for (auto& layout : result.sets) { + layout.flags = input.Word(); + const auto count = input.Count(4096, 20); + totalBindings += count; + RequireCapture(totalBindings <= 4096, "too many total descriptor bindings"); + layout.bindings.resize(count); + for (auto& binding : layout.bindings) { + binding.binding = input.Word(); + binding.type = static_cast(input.Word()); + binding.count = input.Word(); + binding.stages = input.Word(); + binding.flags = input.Word(); + } + } + result.pushConstants.resize(input.Count(1, 12)); + for (auto& range : result.pushConstants) { + range.stageFlags = input.Word(); + range.offset = input.Word(); + range.size = input.Word(); + } + result.specialization.resize(input.Count(4096, 8)); + for (auto& value : result.specialization) { + value.id = input.Word(); + value.value = input.Word(); + } + result.spirv.resize(input.Count(MaxSpirvWords, 4)); + for (auto& word : result.spirv) word = input.Word(); + RequireCapture(input.Finished(), "trailing bytes after capture"); + result.Validate(); + return result; +} + +ComputePipelineCapture ComputePipelineCapture::Read(const std::filesystem::path& path) { + return Decode(ReadBytes(path)); +} + +std::string ComputePipelineCapture::Identity() const { + return Hash(Encode()); +} + +std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path& directory) const { + const auto bytes = Encode(); + const auto recordDirectory = directory / ("compute-" + Hash(bytes)); + const auto path = recordDirectory / "capture.aps5cp"; + static std::mutex mutex; + std::lock_guard lock(mutex); + std::filesystem::create_directories(directory); + RequireCapture(!std::filesystem::is_symlink(std::filesystem::symlink_status(directory)), "capture directory is a symbolic link"); + if (std::filesystem::exists(recordDirectory)) { + RequireCapture(std::filesystem::is_directory(recordDirectory) && !std::filesystem::is_symlink(std::filesystem::symlink_status(recordDirectory)) && !std::filesystem::is_symlink(std::filesystem::symlink_status(path)), "existing capture path is not a regular capture"); + RequireCapture(ReadBytes(path) == bytes, "existing capture differs or is incomplete"); + return path; + } + std::uintmax_t totalBytes = 0; + std::size_t count = 0; + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + RequireCapture(++count < 128, "capture directory reached 128 entries"); + RequireCapture(entry.is_directory() && !entry.is_symlink(), "capture directory contains an unexpected entry"); + const auto capture = entry.path() / "capture.aps5cp"; + RequireCapture(std::filesystem::is_regular_file(capture) && !std::filesystem::is_symlink(std::filesystem::symlink_status(capture)), "capture directory contains an incomplete record"); + const auto size = std::filesystem::file_size(capture); + RequireCapture(size <= MaxFileBytes, "existing capture exceeds size bound"); + totalBytes += size; + } + RequireCapture(totalBytes + bytes.size() <= 256u * 1024u * 1024u, "capture directory exceeds 256 MiB"); + RequireCapture(std::filesystem::create_directory(recordDirectory), "capture directory was concurrently created"); + std::ofstream output(path, std::ios::binary); + RequireCapture(output.good(), "cannot create capture"); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + RequireCapture(output.good(), "cannot finish capture"); + return path; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index fe033b378..e8ecb282d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -92,7 +92,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe resolveDrawDecode(queue, submission, decode, registerKey, drawKey, profile); const auto& graphics = decode->state; - const auto& pixel = decode->pixel; + auto pixel = decode->pixel; std::vector programs = decode->programs; const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) { if (!graphics.stages.mesh) return; @@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& program = programs[i]; if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); - vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i]); + vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); @@ -204,6 +204,13 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end()); } else { resultIndex[i] = results.size(); + if (program.binary.stage == ShaderRecompiler::ShaderStage::Fragment) { + for (auto previous = i; previous-- > 0;) { + if (programResults[previous] == nullptr) continue; + pixel = Graphics::DefaultUnexportedInputs(decode->pixel, programResults[previous]->parameterExports); + break; + } + } results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected)); if (!rejected.empty()) return DrawVerdict::Rejected; programResults[i] = &results.back(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index ba5510217..2af5a114d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -109,16 +109,7 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission) { auto product = std::make_shared(); - bool vertexWorkgroup = false; - const auto stages = queue.context.find(0x2d5); - const auto low = queue.shader.find(0xc8); - const auto high = queue.shader.find(0xc9); - if (stages != queue.context.end() && (stages->second & 0x02002024u) == 0x2000u && low != queue.shader.end() && high != queue.shader.end()) { - const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); - const auto front = ProgramSnapshot(*submission.shaders, address, {2}); - vertexWorkgroup = front->header.empty() && ProgramUsesWorkgroup(*front, static_cast((address - front->codeAddress) / sizeof(std::uint32_t))); - } - product->state = Graphics::DecodeState(queue, vertexWorkgroup); + product->state = Graphics::DecodeState(queue, VertexWorkgroupRequired(queue, *submission.shaders)); DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true); product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 277f9673d..54f524103 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -123,13 +123,19 @@ extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) { AgcDriver::ReportFailure(error); } -extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) { +extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } -extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) { +extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span stages, std::span context, std::span primitive) try { AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive); +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index 5e7f9a53c..b8e7adb77 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -265,7 +266,10 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(block).first(std::min(chunk, bytes - done)), 16); phase(FillCpu); } - if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) { + Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); + } if (profile) { const auto now = std::chrono::steady_clock::now(); const auto cover = static_cast(coverage.cover); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index c7b1198a1..a2ba8abae 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -60,7 +61,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (opcode == 0x50 && packet.size() >= 7) { - if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) Graphics::NoteDepthMetadataFill(packet[4] | (static_cast(packet[5]) << 32u), packet[6] & 0x3ffffffu, packet[2]); + if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy"); + if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + Graphics::NoteColorMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + } } if (!drainAll && !endOfPipeInterrupt && (opcode == 0x49 || opcode == 0x37)) { if (const auto label = Pm4::DecodeLabelWrite(packet)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 9fa87efc4..d568f7b81 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include #include namespace AgcDriver::DriverDetail { @@ -80,6 +81,24 @@ void Driver::run(std::uint32_t id) noexcept { try { for (;;) { submission = Submission{}; + struct Running { + std::atomic& count; + std::atomic& idleEpoch; + std::condition_variable& changed; + bool active = false; + Running(std::atomic& count, std::atomic& idleEpoch, std::condition_variable& changed) + : count(count), idleEpoch(idleEpoch), changed(changed) {} + void Start() { + count.fetch_add(1, std::memory_order_acq_rel); + active = true; + } + ~Running() { + if (active && count.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } + } running{runningWorkers, idleEpoch, changed}; static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; { PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async()); @@ -91,6 +110,10 @@ void Driver::run(std::uint32_t id) noexcept { workerQueued() = &worker.queued; if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id); const auto ready = [&] { return failure || stopping || !pending.empty(); }; + const auto allQueuesIdle = [&] { + return runningWorkers.load(std::memory_order_acquire) == 0 + && std::all_of(workers.begin(), workers.end(), [](const auto& item) { return item.second.pending.empty(); }); + }; const auto poll = [&](const auto& done) { #ifdef _WIN32 lock.unlock(); @@ -104,9 +127,21 @@ void Driver::run(std::uint32_t id) noexcept { while (!ready()) { if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) { + const auto idleEpochAtCheck = idleEpoch.load(std::memory_order_acquire); + const bool storeIdleResults = id == 0 && allQueuesIdle() && !completionsPending(); + lock.unlock(); + if (storeIdleResults) { + if (const auto localDevice = device.Load()) localDevice->StoreIdleResults(); + } + lock.lock(); + if (ready()) break; + if (id == 0 && allQueuesIdle() && !completionsPending() + && (!storeIdleResults || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue; if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed); - changed.wait(lock, ready); + const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire); + changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); }); if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed); + if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue; break; } if (poll(ready)) break; @@ -133,16 +168,10 @@ void Driver::run(std::uint32_t id) noexcept { if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now(); timing.Mark("dequeue_order"); worker.queued.fetch_sub(1, std::memory_order_acq_rel); + running.Start(); if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - submission.enqueuedAt).count()); } - { - struct Running { - std::atomic& count; - explicit Running(std::atomic& count) : count(count) { count.fetch_add(1, std::memory_order_acq_rel); } - ~Running() { count.fetch_sub(1, std::memory_order_acq_rel); } - } running{runningWorkers}; - execute(submission); - } + execute(submission); if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast(submission.serial), id); const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index a4fbb6e4a..a0db6f839 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -332,8 +332,12 @@ bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const { } void Driver::noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked) { - if (blocked) runningWorkers.fetch_sub(1, std::memory_order_acq_rel); - else runningWorkers.fetch_add(1, std::memory_order_acq_rel); + if (blocked) { + if (runningWorkers.fetch_sub(1, std::memory_order_acq_rel) == 1) { + idleEpoch.fetch_add(1, std::memory_order_release); + changed.notify_all(); + } + } else runningWorkers.fetch_add(1, std::memory_order_acq_rel); if (queue == 0) queue0Awaited.store(blocked ? awaited : 0, std::memory_order_release); if (queue == 0 && !blocked) queue0AwaitsTitle.store(false, std::memory_order_release); if (blocked && orderHolders.load(std::memory_order_acquire) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 9b46369ce..580d119af 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -130,6 +130,12 @@ void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lock key) { + std::unique_lock lock(snapshot.prepared->mutex); + AwaitRegisteredPreparation(*snapshot.prepared, lock); + return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); }); +} + } bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) { @@ -147,6 +153,22 @@ bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset } +bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry) { + Graphics::NoteRegisterRead(Graphics::RegisterBank::Context, 0x2d5); + const auto routing = queue.context.find(0x2d5); + if (routing == queue.context.end() || (routing->second & 0x02002024u) != 0x2000u) return false; + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc8); + Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, 0xc9); + const auto low = queue.shader.find(0xc8); + const auto high = queue.shader.find(0xc9); + if (low == queue.shader.end() || high == queue.shader.end()) return false; + require((high->second & ~0xffu) == 0, "reserved graphics program address bits are set"); + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second) << 40u); + const auto snapshot = ProgramSnapshot(registry, address, {2}); + require((address - snapshot->codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned"); + return (snapshot->header.empty() || snapshot->type == 2) && ProgramUsesWorkgroup(*snapshot, (address - snapshot->codeAddress) / sizeof(std::uint32_t)); +} + struct ShaderPreparationTransaction::State { struct Change { std::shared_ptr destination; @@ -157,9 +179,13 @@ struct ShaderPreparationTransaction::State { }; static std::mutex writers; static thread_local State* active; - std::unique_lock lock{writers}; + std::unique_lock lock{writers, std::defer_lock}; std::map changes; bool failed = false; + + void Hold() { + if (!lock.owns_lock()) lock.lock(); + } }; std::mutex ShaderPreparationTransaction::State::writers; @@ -180,6 +206,7 @@ ShaderPreparationTransaction::~ShaderPreparationTransaction() { PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) { require(!committed, "shader preparation transaction is already committed"); + root->Hold(); auto& changes = root->changes; const auto found = changes.find(snapshot.prepared.get()); if (found != changes.end()) { @@ -193,6 +220,7 @@ PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& sn const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const { require(!committed, "shader preparation transaction is already committed"); + root->Hold(); auto& changes = root->changes; const auto found = changes.find(snapshot.prepared.get()); if (found != changes.end()) return found->second.prepared; @@ -201,6 +229,11 @@ const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapsh return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; } +void ShaderPreparationTransaction::Hold() { + require(!committed, "shader preparation transaction is already committed"); + root->Hold(); +} + void ShaderPreparationTransaction::Commit() { require(!committed && !root->failed, "shader preparation transaction was aborted"); if (state != nullptr) { @@ -294,6 +327,7 @@ std::shared_ptr RegisteredProgram(const ShaderRegistry& re void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot) { ShaderPreparationTransaction transaction; + transaction.Hold(); auto published = snapshot; if (registry != nullptr) { const auto found = registry->find(snapshot->codeAddress); @@ -606,6 +640,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } else { const auto routing = RegisterValue(state.context, 0x2d5); wave = (routing & 0x00400000u) != 0 ? 32u : 64u; + if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr; if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) { if (registration) return {}; auto stageState = state; @@ -618,7 +653,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap if (firstUser == 0) userCount += 8; } plan->userData.resize(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { @@ -805,7 +840,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment; const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; std::optional vertex; - if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; @@ -883,7 +918,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std return; } DrawDecode decoded{}; - decoded.state.stages = Graphics::DecodeShaderStages(state); + decoded.state.stages = Graphics::DecodeShaderStages(state, VertexWorkgroupRequired(state, *registry)); DecodeGraphicsPrograms(decoded, state, *registry, true, false); auto prepared = PrepareGraphicsStages(decoded, localDevice->Target()); for (auto& stage : prepared) { @@ -935,12 +970,14 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + const bool committedAbi = CommittedAbi(*snapshot, key); + auto entries = committedAbi ? std::vector{} : PrepareRegistered(*snapshot, *localDevice, state, false); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { transaction.Commit(); return; } - auto entries = PrepareRegistered(*snapshot, *localDevice, state, false); + if (committedAbi) entries = PrepareRegistered(*snapshot, *localDevice, state, false); auto& prepared = transaction.Edit(*snapshot); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; }); @@ -1094,6 +1131,7 @@ void Driver::RegisterShader(const Shader* shader) { snapshot.header.resize(fields.header_size); std::memcpy(snapshot.header.data(), static_cast(shader), fields.header_size); + transaction.Hold(); { std::lock_guard lock(mutex); rethrowFailure(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 36d7b2817..b764c499d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1058,6 +1058,10 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop, } std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoized) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return 0; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return 0; +#endif auto& tracker = Tracker(); // Whole pages, so a page shared with the next range is collected with either. constexpr std::uint64_t page = 4096; @@ -1185,6 +1189,10 @@ bool WriteWatched() { } bool Watched(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1192,6 +1200,10 @@ bool Watched(std::uint64_t address, std::size_t bytes) { } bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation) { + if (bytes == 0 || bytes > std::numeric_limits::max() - address) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif auto& tracker = Tracker(); const auto lock = lockTracker(tracker); tracker.initialize(); @@ -1208,6 +1220,9 @@ bool UnchangedSinceAll(std::span queries) { if (!tracker.watched) return false; for (const auto& [address, bytes, generation] : queries) { if (generation == 0 || bytes == 0 || !tracker.covers(address, bytes)) return false; +#ifdef _WIN32 + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(address, bytes)) return false; +#endif const auto first = tracker.blockOf(address); const auto last = tracker.blockOf(address + bytes - 1); if (!tracker.unchanged(first, last, generation)) return false; @@ -1736,12 +1751,19 @@ void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) { flushHook.store(hook, std::memory_order_release); } +std::atomic readGuard{nullptr}; + +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) { + readGuard.store(guard, std::memory_order_release); +} + void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes); } void Read(std::uint64_t address, std::span destination, std::size_t alignment) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -1758,6 +1780,7 @@ void Read(std::uint64_t address, std::span destination, std::size_t a void ReadCommitted(std::uint64_t address, std::span destination) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index f3b0dea8f..9b7b06e0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -453,7 +453,7 @@ void Validate(std::span packet, std::uint32_t queue) { require(packet.size() >= 5, "WRITE_DATA has no data"); require((packet[1] & ~(0x40110f00u | WriteDataCachePolicy)) == 0, "WRITE_DATA engine or reserved fields are not implemented"); const auto destination = (packet[1] >> 8u) & 0xfu; - require(destination == 1 || destination == 2 || destination == 5, "WRITE_DATA register or GDS destination is not implemented"); + require(destination == 1 || destination == 2 || destination == 3 || destination == 5, "WRITE_DATA register destination is not implemented"); require((packet[2] & 3u) == 0, "misaligned WRITE_DATA destination"); break; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87d211a49..8a1600be3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2,6 +2,9 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "BdaAbi.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" @@ -15,6 +18,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -70,6 +74,50 @@ void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("Vulkan presentation: ") + reason); } +bool CoreVulkanCapability(std::uint32_t capability) { + switch (capability) { + case spv::CapabilityMatrix: + case spv::CapabilityShader: + case spv::CapabilityInputAttachment: + case spv::CapabilitySampled1D: + case spv::CapabilityImage1D: + case spv::CapabilitySampledBuffer: + case spv::CapabilityImageBuffer: + case spv::CapabilityImageQuery: + case spv::CapabilityDerivativeControl: + case spv::CapabilityStorageImageExtendedFormats: + return true; + default: + return false; + } +} + +void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, VkPipelineCache cache, VkShaderModule module, VkPipelineLayout layout, VkPipelineCreateFlags flags, VkPipelineShaderStageCreateFlags stageFlags, const VkSpecializationInfo* specialization, std::uint32_t subgroupSize, VkPipeline* pipeline) { + VkComputePipelineCreateInfo info{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; + info.flags = flags; + info.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; + info.stage.flags = stageFlags; + info.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; + info.stage.module = module; + info.stage.pName = "main"; + info.stage.pSpecializationInfo = specialization; + VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT subgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; + subgroup.requiredSubgroupSize = subgroupSize; + if (subgroupSize != 0) info.stage.pNext = &subgroup; + info.layout = layout; + check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); +} + +bool CaptureComputePipeline(std::size_t words) { + const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS"); + if (minimum == nullptr) return true; + std::uint32_t threshold = 0; + const auto end = minimum + std::strlen(minimum); + const auto parsed = std::from_chars(minimum, end, threshold); + require(parsed.ec == std::errc{} && parsed.ptr == end && threshold <= ComputePipelineCapture::MaxSpirvWords, "APS5_DUMP_COMPUTE_MIN_WORDS must be an integer from 0 through 8388608"); + return words >= threshold; +} + // The ShaderResources content cache dispatches share with recorded draws: Graphics::ResourceCache, // one process-wide instance (see SharedResourceCache) that the State references so this file keeps // its Find/Insert/Remove/Clear calls; the device clears it at teardown before its descriptor caches @@ -178,6 +226,7 @@ struct WorkgroupMemory { }; struct VulkanDevice::State { + bool compileOnly = false; void* library = nullptr; PFN_vkGetInstanceProcAddr instanceProc = nullptr; PFN_vkGetDeviceProcAddr deviceProc = nullptr; @@ -220,6 +269,9 @@ struct VulkanDevice::State { // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; bool storageBufferUpdateAfterBind = false; + bool descriptorBindingPartiallyBound = false; + bool descriptorBindingVariableDescriptorCount = false; + bool runtimeDescriptorArray = false; VkPhysicalDeviceDescriptorIndexingPropertiesEXT descriptorIndexingProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_PROPERTIES_EXT}; bool imageInt64Atomics = false; bool bufferInt64Atomics = false; @@ -567,6 +619,10 @@ struct VulkanDevice::State { ~State() { contextReady = false; + if (compileOnly && device != VK_NULL_HANDLE) { + reinterpret_cast(deviceProc(device, "vkDestroyDevice"))(device, nullptr); + device = VK_NULL_HANDLE; + } if (device != VK_NULL_HANDLE) { const auto idle = reinterpret_cast(deviceProc(device, "vkDeviceWaitIdle"))(device); if (idle != VK_SUCCESS && idle != VK_ERROR_DEVICE_LOST) std::terminate(); @@ -598,6 +654,7 @@ struct VulkanDevice::State { // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); Graphics::ClearDepthSurfaces(device); + Graphics::ClearMultisampleTargets(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); @@ -656,7 +713,165 @@ struct VulkanDevice::State { } }; -VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { +ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePipelineCapture& capture, std::span initialCache) { + require(state->compileOnly, "pipeline replay requires a compile-only device"); + const auto start = std::chrono::steady_clock::now(); + const auto elapsed = [](auto since) { return std::chrono::duration(std::chrono::steady_clock::now() - since).count(); }; + capture.Validate(); + require(initialCache.size() <= 64u * 1024u * 1024u, "replay pipeline cache exceeds 64 MiB"); + if (!initialCache.empty()) { + require(initialCache.size() >= 32, "replay pipeline cache header is truncated"); + const auto word = [&](std::size_t offset) { + std::uint32_t result = 0; + for (unsigned shift = 0; shift < 32; shift += 8) result |= std::to_integer(initialCache[offset++]) << shift; + return result; + }; + require(word(0) >= 32 && word(0) <= initialCache.size() && word(4) == VK_PIPELINE_CACHE_HEADER_VERSION_ONE, "unsupported replay pipeline cache header"); + require(word(8) == state->properties.vendorID && word(12) == state->properties.deviceID, "replay pipeline cache belongs to another device"); + for (std::size_t index = 0; index < VK_UUID_SIZE; ++index) require(std::to_integer(initialCache[16 + index]) == state->properties.pipelineCacheUUID[index], "replay pipeline cache UUID differs from the driver"); + } + require((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) == 0 || state->pipelineExecutableInfo, "captured pipeline statistics feature is not enabled"); + if (capture.requiredSubgroupSize != 0) { + VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &subgroup}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(state->physical, &properties); + require(state->computeWave32 && capture.requiredSubgroupSize >= subgroup.minSubgroupSize && capture.requiredSubgroupSize <= subgroup.maxSubgroupSize && (subgroup.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0, "captured required subgroup size is unsupported"); + } + const auto target = Target(); + require(capture.spirv[1] <= target.spirvVersion, "captured SPIR-V version exceeds enabled device support"); + for (std::size_t cursor = 5; cursor < capture.spirv.size(); cursor += capture.spirv[cursor] >> 16u) { + const auto count = capture.spirv[cursor] >> 16u; + if ((capture.spirv[cursor] & 0xffffu) == spv::OpCapability) { + require(count == 2 && (CoreVulkanCapability(capture.spirv[cursor + 1]) || std::ranges::find(target.supportedCapabilities, capture.spirv[cursor + 1]) != target.supportedCapabilities.end()), "captured SPIR-V capability is not enabled"); + } + } +#if ANYPS5_ENABLE_SPIRV_TOOLS + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(capture.spirv, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false)); +#else + throw std::runtime_error("compute pipeline replay requires ANYPS5_ENABLE_SPIRV_TOOLS=ON"); +#endif + ShaderRecompiler::RecompileResult shader; + shader.spirv = capture.spirv; + for (const auto& value : capture.specialization) shader.specialization.push_back({value.id, value.value}); + const Graphics::PipelineSpecialization specialization(shader); + const auto& limits = state->properties.limits; + require(capture.sets.size() <= limits.maxBoundDescriptorSets, "captured descriptor sets exceed device limits"); + for (const auto& range : capture.pushConstants) require(range.offset + range.size <= limits.maxPushConstantsSize, "captured push constants exceed device limits"); + struct ReplayObjects { + VkDevice device; + PFN_vkDestroyDescriptorSetLayout destroySetLayout; + PFN_vkDestroyPipelineCache destroyCache; + std::vector sets; + VkPipelineCache cache = VK_NULL_HANDLE; + ~ReplayObjects() { + for (auto layout : sets) destroySetLayout(device, layout, nullptr); + if (cache != VK_NULL_HANDLE) destroyCache(device, cache, nullptr); + } + } replay{state->device, state->DeviceFunction("vkDestroyDescriptorSetLayout"), state->DeviceFunction("vkDestroyPipelineCache"), {}}; + ComputePipelineObjects objects; + objects.device = state->device; + objects.destroyModule = state->DeviceFunction("vkDestroyShaderModule"); + objects.destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); + objects.destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); + ComputePipelineReplayResult result; + result.deviceName = state->properties.deviceName; + result.vendorId = state->properties.vendorID; + result.deviceId = state->properties.deviceID; + result.driverVersion = state->properties.driverVersion; + result.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, result.pipelineCacheUuid.begin()); + result.initialCacheBytes = initialCache.size(); + auto phase = std::chrono::steady_clock::now(); + VkPipelineCacheCreateInfo cacheInfo{VK_STRUCTURE_TYPE_PIPELINE_CACHE_CREATE_INFO}; + cacheInfo.initialDataSize = initialCache.size(); + cacheInfo.pInitialData = initialCache.data(); + check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); + result.cacheMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); + moduleInfo.pCode = capture.spirv.data(); + check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + result.moduleMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + std::array, 2> descriptorCounts{}; + bool hasUpdateAfterBindLayout = false; + for (const auto& layout : capture.sets) { + const bool updateAfterBind = (layout.flags & VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT) != 0; + hasUpdateAfterBindLayout |= updateAfterBind; + require(!updateAfterBind || state->storageBufferUpdateAfterBind, "captured update-after-bind descriptors are unsupported"); + std::vector bindings; + std::vector flags; + std::uint32_t variableCount = 0; + for (const auto& binding : layout.bindings) { + require((binding.flags & VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT) == 0 || state->descriptorBindingPartiallyBound, "captured partially-bound descriptors are unsupported"); + require((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) == 0 || state->descriptorBindingVariableDescriptorCount, "captured variable descriptor count is unsupported"); + if ((binding.flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) != 0) variableCount = binding.count; + bindings.push_back({binding.binding, binding.type, binding.count, binding.stages, nullptr}); + flags.push_back(binding.flags); + const std::size_t kind = binding.type == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? 0 : binding.type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ? 1 : binding.type == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE ? 2 : 3; + descriptorCounts[updateAfterBind ? 1 : 0][kind] += binding.count; + } + VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; + bindingFlags.bindingCount = static_cast(flags.size()); + bindingFlags.pBindingFlags = flags.data(); + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; + info.flags = layout.flags; + info.bindingCount = static_cast(bindings.size()); + info.pBindings = bindings.data(); + VkDescriptorSetVariableDescriptorCountLayoutSupport variableSupport{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_VARIABLE_DESCRIPTOR_COUNT_LAYOUT_SUPPORT}; + VkDescriptorSetLayoutSupport support{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_SUPPORT, variableCount != 0 ? &variableSupport : nullptr}; + state->DeviceFunction("vkGetDescriptorSetLayoutSupport")(state->device, &info, &support); + require(support.supported == VK_TRUE, "captured descriptor set layout is unsupported"); + require(variableCount <= variableSupport.maxVariableDescriptorCount, "captured variable descriptor count exceeds layout support"); + VkDescriptorSetLayout handle = VK_NULL_HANDLE; + check(state->DeviceFunction("vkCreateDescriptorSetLayout")(state->device, &info, nullptr, &handle), "vkCreateDescriptorSetLayout replay"); + replay.sets.push_back(handle); + } + const std::array regularLimits{std::min(limits.maxDescriptorSetStorageBuffers, limits.maxPerStageDescriptorStorageBuffers), std::min(limits.maxDescriptorSetSampledImages, limits.maxPerStageDescriptorSampledImages), std::min(limits.maxDescriptorSetStorageImages, limits.maxPerStageDescriptorStorageImages), std::min(limits.maxDescriptorSetSamplers, limits.maxPerStageDescriptorSamplers)}; + const auto& indexed = state->descriptorIndexingProperties; + const std::array indexedLimits{std::min(indexed.maxDescriptorSetUpdateAfterBindStorageBuffers, indexed.maxPerStageDescriptorUpdateAfterBindStorageBuffers), std::min(indexed.maxDescriptorSetUpdateAfterBindSampledImages, indexed.maxPerStageDescriptorUpdateAfterBindSampledImages), std::min(indexed.maxDescriptorSetUpdateAfterBindStorageImages, indexed.maxPerStageDescriptorUpdateAfterBindStorageImages), std::min(indexed.maxDescriptorSetUpdateAfterBindSamplers, indexed.maxPerStageDescriptorUpdateAfterBindSamplers)}; + std::uint64_t regularResources = 0; + std::uint64_t indexedResources = 0; + for (std::size_t kind = 0; kind < regularLimits.size(); ++kind) { + require(descriptorCounts[0][kind] <= regularLimits[kind], "captured pipeline descriptors exceed device limits"); + require(!hasUpdateAfterBindLayout || descriptorCounts[0][kind] + descriptorCounts[1][kind] <= indexedLimits[kind], "captured update-after-bind pipeline descriptors exceed device limits"); + if (kind != 3) { + regularResources += descriptorCounts[0][kind]; + indexedResources += descriptorCounts[0][kind] + descriptorCounts[1][kind]; + } + } + require(regularResources <= limits.maxPerStageResources && (!hasUpdateAfterBindLayout || indexedResources <= indexed.maxPerStageUpdateAfterBindResources), "captured pipeline resources exceed device limits"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = static_cast(replay.sets.size()); + layoutInfo.pSetLayouts = replay.sets.data(); + layoutInfo.pushConstantRangeCount = static_cast(capture.pushConstants.size()); + layoutInfo.pPushConstantRanges = capture.pushConstants.data(); + check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects.layout), "vkCreatePipelineLayout replay"); + result.layoutMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline); + result.pipelineMilliseconds = elapsed(phase); + phase = std::chrono::steady_clock::now(); + const auto getCache = state->DeviceFunction("vkGetPipelineCacheData"); + std::size_t cacheBytes = 0; + check(getCache(state->device, replay.cache, &cacheBytes, nullptr), "vkGetPipelineCacheData size replay"); + require(cacheBytes <= 64u * 1024u * 1024u, "exported replay pipeline cache exceeds 64 MiB"); + result.cache.resize(cacheBytes); + check(getCache(state->device, replay.cache, &cacheBytes, result.cache.data()), "vkGetPipelineCacheData replay"); + result.cache.resize(cacheBytes); + result.cacheMilliseconds += elapsed(phase); + result.totalMilliseconds = elapsed(start); + return result; +} + +VulkanDevice::VulkanDevice(const PresentationWindow* window) : VulkanDevice(window, false) {} + +VulkanDevice::VulkanDevice(CompileOnly) : VulkanDevice(nullptr, true) {} + +VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state(std::make_unique()), serial([] { static std::atomic serials{0}; return serials.fetch_add(1, std::memory_order_relaxed) + 1; }()) { + state->compileOnly = compileOnly; APS5_LOG_OUT("VulkanDevice constructor window=%p", static_cast(window)); #ifdef _WIN32 state->library = SDL_LoadObject("vulkan-1.dll"); @@ -966,10 +1181,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment; - Graphics::SetHostImportWindow(8ull << 20u); + if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. - if (const char* value = std::getenv("APS5_WORKING_SET_MIB")) { + if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { const SIZE_T bytes = static_cast(std::strtoull(value, nullptr, 10)) << 20u; if (!SetProcessWorkingSetSizeEx(GetCurrentProcess(), bytes, bytes * 2, QUOTA_LIMITS_HARDWS_MIN_DISABLE | QUOTA_LIMITS_HARDWS_MAX_DISABLE)) std::fprintf(stderr, "[gpu] SetProcessWorkingSetSizeEx failed: %lu\n", GetLastError()); } @@ -1147,13 +1362,16 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); state->descriptorIndexing = descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing == VK_TRUE; state->storageBufferUpdateAfterBind = descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind == VK_TRUE; + state->descriptorBindingPartiallyBound = compileOnly && descriptorIndexingFeatures.descriptorBindingPartiallyBound == VK_TRUE; + state->descriptorBindingVariableDescriptorCount = compileOnly && descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount == VK_TRUE; + state->runtimeDescriptorArray = compileOnly && descriptorIndexingFeatures.runtimeDescriptorArray == VK_TRUE; if (state->storageBufferUpdateAfterBind) { VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &state->descriptorIndexingProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); } } descriptorIndexingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT}; - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) deviceExtensions.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME); if (state->descriptorIndexing) { descriptorIndexingFeatures.shaderSampledImageArrayNonUniformIndexing = VK_TRUE; descriptorIndexingFeatures.shaderStorageImageArrayNonUniformIndexing = VK_TRUE; @@ -1163,6 +1381,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); } descriptorIndexingFeatures.descriptorBindingStorageBufferUpdateAfterBind = state->storageBufferUpdateAfterBind ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingPartiallyBound = state->descriptorBindingPartiallyBound ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.descriptorBindingVariableDescriptorCount = state->descriptorBindingVariableDescriptorCount ? VK_TRUE : VK_FALSE; + descriptorIndexingFeatures.runtimeDescriptorArray = state->runtimeDescriptorArray ? VK_TRUE : VK_FALSE; + if (state->runtimeDescriptorArray) { + state->capabilities.push_back(spv::CapabilityRuntimeDescriptorArray); + if (!state->descriptorIndexing) state->spirvExtensions.push_back("SPV_EXT_descriptor_indexing"); + } state->samplerAnisotropy = true; state->textureCompressionBC = true; deviceInfo.pEnabledFeatures = &enabled; @@ -1230,7 +1455,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u imageRobustnessFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &imageRobustnessFeatures; } - if (state->descriptorIndexing || state->storageBufferUpdateAfterBind) { + if (state->descriptorIndexing || state->storageBufferUpdateAfterBind || state->descriptorBindingPartiallyBound || state->descriptorBindingVariableDescriptorCount || state->runtimeDescriptorArray) { descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } @@ -1310,6 +1535,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u auto shaderProfile = std::make_unique(buildTarget(), deviceInfo, state->properties.limits); check(state->InstanceFunction("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice"); state->shaderProfile = std::move(shaderProfile); + if (compileOnly) return; state->DeviceFunction("vkGetDeviceQueue")(state->device, family, 0, &state->queue); APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast(state->device), reinterpret_cast(state->queue), family); VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; @@ -1443,6 +1669,14 @@ bool OpportunisticReap() { } +void VulkanDevice::StoreIdleResults(bool reapFirst) { + if (!Graphics::StorageTexture::AnyPending()) return; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + Graphics::StorageTexture::FlushAllPending("idle"); + SubmitRecorded(reapFirst); +} + void VulkanDevice::SubmitRecorded(bool reapFirst) { if (!state->recorder) return; // Finished batches are retired first (a non-blocking fence status check on the front of the @@ -3833,20 +4067,39 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec layoutInfo.pushConstantRangeCount = pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = pushStages != 0 ? &push : nullptr; check(state->DeviceFunction("vkCreatePipelineLayout")(state->device, &layoutInfo, nullptr, &objects->layout), "vkCreatePipelineLayout"); - VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; - pipelineInfo.flags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; - pipelineInfo.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; - pipelineInfo.stage.module = objects->module; - pipelineInfo.stage.pName = "main"; + const VkPipelineCreateFlags pipelineFlags = (context.pipelineExecutableInfo ? VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR : 0) | VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; const Graphics::PipelineSpecialization specialization(shader); - pipelineInfo.stage.pSpecializationInfo = specialization.Info(); - VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT}; - requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize); - if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup; - pipelineInfo.layout = objects->layout; + const auto subgroupSize = state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize) ? std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize) : 0u; + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && CaptureComputePipeline(shader.spirv.size())) { + ComputePipelineCapture capture; + capture.spirv = shader.spirv; + ComputeDescriptorSetLayout resourcesLayout; + resourcesLayout.flags = resources->LayoutUpdateAfterBind() ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; + const auto layoutBindings = resources->LayoutBindings(); + const auto bindingFlags = resources->LayoutBindingFlags(); + require(layoutBindings.size() == bindingFlags.size(), "compute capture binding flag count differs from layout"); + for (std::size_t index = 0; index < layoutBindings.size(); ++index) { + const auto& binding = layoutBindings[index]; + require(binding.pImmutableSamplers == nullptr, "compute capture does not support immutable samplers"); + resourcesLayout.bindings.push_back({binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags, bindingFlags[index]}); + } + capture.sets.push_back(std::move(resourcesLayout)); + if (shader.workgroupMemoryDwords != 0) capture.sets.push_back({0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}); + if (pushStages != 0) capture.pushConstants.push_back(push); + for (const auto& value : shader.specialization) capture.specialization.push_back({value.id, value.value}); + capture.requiredSubgroupSize = subgroupSize; + capture.flags = pipelineFlags; + capture.deviceName = state->properties.deviceName; + capture.vendorId = state->properties.vendorID; + capture.deviceId = state->properties.deviceID; + capture.driverVersion = state->properties.driverVersion; + capture.apiVersion = state->properties.apiVersion; + std::copy_n(state->properties.pipelineCacheUUID, VK_UUID_SIZE, capture.pipelineCacheUuid.begin()); + const auto path = capture.Write(directory); + std::fprintf(stderr, "[compute-capture] %s\n", path.string().c_str()); + } if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast(programAddress)); - check(state->DeviceFunction("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines"); + CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index b2b4d0978..de95acd54 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -61,12 +61,13 @@ public: // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); static VkDeviceSize SlabBlockBytes(std::size_t capacity); SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); - void PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; + VkDeviceSize PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept; std::size_t SlabBlocks(); private: @@ -117,7 +118,8 @@ private: static bool DeviceTierEnabled(); static bool DeviceTiered(VkMemoryPropertyFlags properties); static std::size_t NextCapacity(std::size_t capacity); - void destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize destroy(const BufferAllocation& allocation) noexcept; + VkDeviceSize trim(); // Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp new file mode 100644 index 000000000..5d9d14728 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -0,0 +1,41 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include + +namespace AgcDriver::Graphics { + +class StorageTexture; + +class MultisampledCmask { +public: + MultisampledCmask() = default; + MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {} + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + void SeedKeys(DccKeys seeded); + void CheckAddress(std::uint64_t cmaskAddress) const; + bool TakeClear(); + DccKeys Keys() const { return keys; } + const std::string& Refusal() const { return refusal; } + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + DccKeys keys = DccKeys::Uncompressed; + std::string refusal; +}; + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +void ClearMultisampleTargets(VkDevice device); +void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index c4fa93353..bb817d437 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -45,7 +45,7 @@ public: Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); + std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent = {}); // Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const; // The same inside a render pass another pipeline of the same attachments began (compatible by @@ -64,6 +64,7 @@ private: struct CachedFramebuffer { std::vector views; std::vector> owners; + std::vector owned; VkExtent2D extent; std::shared_ptr framebuffer; }; @@ -107,6 +108,7 @@ void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); +bool ShadersWriteMemory(std::span shaders); // Returns the color attachment locations the pixel shader writes. std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 671937306..6ae3f8ab6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::Graphics { ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram = false); +ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: // the draw cache validates them by value with the stage's capture (design_cpu_final rule RD). @@ -18,7 +19,7 @@ struct DecodeRead { std::uint64_t address; std::vector bytes; }; -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false); +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index dbfdf1581..014a06035 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -60,7 +60,7 @@ public: // The layout for `key` (binding, type, count, stage flags per binding, as ShaderResources builds // it from `bindings`), created on first use. - VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false); + VkDescriptorSetLayout Layout(std::span key, std::span bindings, bool updateAfterBind = false, std::span flags = {}); struct SetAllocation { VkDescriptorSet set = VK_NULL_HANDLE; VkDescriptorPool pool = VK_NULL_HANDLE; @@ -114,6 +114,9 @@ public: ShaderResources(const ShaderResources&) = delete; ShaderResources& operator=(const ShaderResources&) = delete; VkDescriptorSetLayout Layout() const; + std::vector LayoutBindings() const; + bool LayoutUpdateAfterBind() const; + std::span LayoutBindingFlags() const { return layoutBindingFlags; } void Bind(VkCommandBuffer commands, VkPipelineBindPoint bindPoint, VkPipelineLayout layout) const; struct DrawBindings { DrawBindings() = default; @@ -136,6 +139,7 @@ public: }; std::shared_ptr PrepareDrawBindings(Recorder& recorder, std::span moved = {}) const; std::optional> MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const; + static std::size_t MaxDrawSnapshotBytes(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs @@ -159,6 +163,7 @@ public: std::vector> StorageImages() const; std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } bool ReadsImage(const StorageTexture* image) const; + bool HasCachedStorageImages() const; const std::vector& LayoutKey() const { return layoutKey; } // Debug aid: each bound guest resource with the fraction of sampled bytes that are nonzero. std::string Describe() const; @@ -473,6 +478,7 @@ private: // still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader), // the descriptor counts the set was sized for, and the compute stage of a deferred build. std::vector bindings; + std::vector layoutBindingFlags; std::vector refreshResourceKey; struct DeferredImages { const ShaderRecompiler::DescriptorBinding* binding; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index cd0d2c3b2..bb18791e6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -45,6 +45,7 @@ struct ColorTarget { bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; std::size_t cmaskBytes = 0; + bool cmaskFastClear = false; std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; @@ -54,9 +55,13 @@ struct ColorTarget { std::uint32_t slot = 0; std::uint32_t depth = 1; std::uint32_t depthSlice = 0; + std::uint64_t arrayAddress = 0; + std::uint32_t arrayLastSlice = 0; + std::uint32_t arraySlice = 0; std::uint32_t exportIndex = 0; bool uintExport = false; std::uint32_t pipeBankXor = 0; + std::uint32_t samples = 1; }; struct DepthTarget { @@ -68,6 +73,7 @@ struct DepthTarget { std::uint8_t clearStencil; std::uint64_t htileAddress = 0; bool htileStencil = false; + std::uint32_t samples = 1; }; struct State { @@ -109,6 +115,7 @@ struct State { VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; + std::uint32_t samples = 1; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); @@ -120,9 +127,10 @@ std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve }; Mode mode; std::vector targets; + std::optional source; }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); @@ -163,7 +171,7 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, @@ -171,7 +179,7 @@ inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 9f4c7765a..a2d84f038 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -38,6 +38,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear); // A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload // still writes it (see the snapshot constructor), so the texture may go before the batch completes. @@ -142,7 +143,7 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format); // or it leaves the cache. Dispatches reusing the image meanwhile skip the round trip entirely. class StorageTexture : public std::enable_shared_from_this { public: - StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); + static std::shared_ptr Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); ~StorageTexture(); StorageTexture(const StorageTexture&) = delete; StorageTexture& operator=(const StorageTexture&) = delete; @@ -159,7 +160,7 @@ public: // Render targets live in the same images: draws attach mip 0 through a view of the color // buffer's format and mark the image dirty like a storage write. bool Attachable() const { return attachable; } - VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0); + VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0, std::uint32_t layer = 0); VkImageView AttachmentProxyView(); void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const; @@ -176,6 +177,7 @@ public: // overlapping pending image, as before). static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr); static void FlushAllPending(const char* reason); + static bool AnyPending(); static bool StoreAtFlipRequested(const char* value); // See PendingSerial: a change of a surface's source outside the registry (a unit shadow // retile) moves it too. @@ -326,6 +328,7 @@ public: VkDeviceSize AllocationBytes() const { return memoryBytes; } private: + StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel); // The regions of every array layer, or of the tracked layers `layers` selects. std::vector CopyRegions(const std::vector* layers = nullptr) const; // Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others @@ -529,7 +532,7 @@ private: std::map elementLayerViews; std::int8_t singlePassState = -1; bool attachable = false; - std::map, VkImageView> attachmentViews; + std::map, VkImageView> attachmentViews; VkImage proxyImage = VK_NULL_HANDLE; VkDeviceMemory proxyMemory = VK_NULL_HANDLE; VkImageView proxyView = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 2c0c39987..4ae8a5074 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -41,6 +41,30 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation) { + VkDeviceMemory memory = VK_NULL_HANDLE; + auto result = allocateMemory(device, &allocation, nullptr, &memory); + const auto initialResult = result; + unsigned attempts = 1; + VkDeviceSize reclaimed = 0; + if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) { + reclaimed = trim(); + if (reclaimed != 0) { + memory = VK_NULL_HANDLE; + result = allocateMemory(device, &allocation, nullptr, &memory); + ++attempts; + } + } + if (result == VK_SUCCESS) { + if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast(allocation.allocationSize), static_cast(reclaimed), static_cast(initialResult), attempts); + return memory; + } + char details[512]; + std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast(allocation.allocationSize), allocation.memoryTypeIndex, static_cast(reclaimed), static_cast(initialResult), attempts); + Check(result, details); + return VK_NULL_HANDLE; +} + bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false; const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity); @@ -80,7 +104,7 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType, if (addressable) allocation.pNext = &flags; allocation.allocationSize = blockBytes; allocation.memoryTypeIndex = memoryType; - Check(allocateMemory(device, &allocation, nullptr, &block->memory), "vkAllocateMemory buffer slab"); + block->memory = AllocateMemory(allocation, "vkAllocateMemory buffer slab"); if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) { void* mapping = nullptr; const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping); @@ -105,26 +129,27 @@ void BufferPool::freeBlock(SlabBlock& block) noexcept { freeMemory(device, block.memory, nullptr); } -void BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { +VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noexcept { std::unique_ptr released; { std::lock_guard lock(slabMutex); const auto found = slabBlocks.find(memory); - if (found == slabBlocks.end()) return; + if (found == slabBlocks.end()) return 0; auto& block = *found->second; auto& slab = slabs[block.slab]; if (block.free.empty()) slab.available.push_back(&block); block.free.push_back(static_cast(offset / block.slotBytes)); - if (--block.used != 0) return; + if (--block.used != 0) return 0; if (slab.emptyBlocks == 0) { ++slab.emptyBlocks; - return; + return 0; } slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block)); released = std::move(found->second); slabBlocks.erase(found); } freeBlock(*released); + return SlabBlockBytes(released->slotBytes); } std::size_t BufferPool::SlabBlocks() { @@ -150,16 +175,16 @@ VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& me return std::max(VkDeviceSize{512} << 20u, largest / 8u); } -void BufferPool::destroy(const BufferAllocation& allocation) noexcept { +VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { if (allocation.slab) { destroyBuffer(device, allocation.buffer, nullptr); - PutSlot(allocation.memory, allocation.offset); - return; + return PutSlot(allocation.memory, allocation.offset); } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); + return allocation.allocationBytes; } bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) { @@ -240,6 +265,43 @@ std::size_t BufferPool::MaxSlots() { return slots; } +VkDeviceSize BufferPool::trim() { + Tier retiredSmall; + Tier retiredLarge; + Tier retiredDevice; + { + std::lock_guard lock(mutex); + for (const auto& [tier, retired] : {std::pair{&smallTier, &retiredSmall}, std::pair{&largeTier, &retiredLarge}, std::pair{&deviceTier, &retiredDevice}}) { + tier->free.swap(retired->free); + tier->evictions += tier->slots; + tier->retainedBytes = 0; + tier->slots = 0; + } + } + VkDeviceSize bytes = 0; + for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { + for (const auto& [key, slots] : tier->free) { + for (const auto& slot : slots) bytes += destroy(slot.allocation); + } + } + for (;;) { + std::unique_ptr released; + { + std::lock_guard lock(slabMutex); + const auto found = std::find_if(slabBlocks.begin(), slabBlocks.end(), [](const auto& entry) { return entry.second->used == 0; }); + if (found == slabBlocks.end()) break; + auto& slab = slabs.at(found->second->slab); + slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get())); + --slab.emptyBlocks; + released = std::move(found->second); + slabBlocks.erase(found); + } + freeBlock(*released); + bytes += SlabBlockBytes(released->slotBytes); + } + return bytes; +} + void BufferPool::Put(const BufferAllocation& allocation) noexcept { // Evicted allocations are destroyed after the mutex is released (see evictOldest). The vector // may throw on growth; a Put that cannot retain simply destroys, as noexcept requires. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b29e8d1f7..0b5d5eb79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -39,9 +39,9 @@ public: info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = VK_SAMPLE_COUNT_1_BIT; + info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -113,6 +113,7 @@ public: key[10] = components.b; key[11] = components.a; if (const auto found = textures.find(key); found != textures.end()) return found->second; + Require(target.samples == 1, "sampling a multisampled depth surface is not implemented"); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; @@ -158,7 +159,7 @@ private: }; bool sameSurface(const DepthTarget& a, const DepthTarget& b) { - return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format; + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples; } std::mutex& surfacesMutex() { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index adb76dd93..b1fe81c22 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -87,6 +88,17 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } +GuestTextureResource ArraySurfaceForTarget(const ColorTarget& color) { + Require(color.arrayLastSlice != 0, "the color target is not a view of a 2D array"); + auto surface = SurfaceForTarget(color); + surface.baseAddress = color.arrayAddress; + surface.width = color.surfaceExtent.width; + surface.height = color.surfaceExtent.height; + surface.depthOrLastArray = color.arrayLastSlice; + surface.dimension = TextureDimension::k2DArray; + return surface; +} + } namespace { @@ -1025,7 +1037,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast(draw.indexAddress), static_cast(indexBytes), draw.indexSize); APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); - if (state.rectList) Require(draw.indexCount % 3 == 0, "incomplete rect-list primitive"); APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; @@ -1690,7 +1701,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4); } auto images = resources.StorageImages(); - for (const auto& target : record.targets) images.emplace_back(target->Image(), true); + for (const auto& target : record.targets) { + if (target != nullptr) images.emplace_back(target->Image(), true); + } recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()}); } std::unique_ptr scratch; @@ -1855,6 +1868,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& std::vector original; TileMipLayout mip{}; std::unique_ptr target; + bool multisampled = false; // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. std::unique_ptr dump; }; @@ -1868,6 +1882,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.color = state.colors[index]; const auto& color = binding.color; binding.gpuTiling = color.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr; + if (color.samples > 1) { + binding.multisampled = true; + timer.phase(PhaseSetup); + targetViews.push_back(MultisampleTargetView(context, color)); + timer.phase(PhaseReadTarget); + continue; + } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; @@ -1882,6 +1903,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // APS5_PACKET_EPOCH=1); the key scan runs on every lookup regardless. Lookups of a // millisecond or more are counted apart; the [texture] line says how many uploaded. binding.resident = refreshResidentTarget(context, state, color, outcome, profile, [&] { + if (color.arrayLastSlice != 0) { + Require(color.dccAddress == 0 && color.cmaskAddress == 0 && AttachmentProxyFormat(context, color.format) == VK_FORMAT_UNDEFINED, "a slice of a 2D array color target with DCC, CMASK or an attachment proxy has no resident array image"); + const auto array = ArraySurfaceForTarget(color); + Require(color.surfaceAddress == color.arrayAddress + color.arraySlice * DescribeSurface(array).layerBytes, "the array image's layer pitch differs from the color view's slice offset"); + auto resident = CachedStorageSurface(context, array); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + return resident; + } auto resident = CachedStorageSurface(context, SurfaceForTarget(color)); Require(resident->Attachable(), "storage format cannot be a color attachment"); Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout"); @@ -1891,12 +1920,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (binding.resident != nullptr) { timer.phase(PhaseReadTarget); binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED; - targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice)); + targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice, color.arraySlice)); continue; } materializeCmaskClear(context, color, nullptr); Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface"); Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface"); + Require(color.arraySlice == 0 || !ColorTileModeIsXor(color.tileMode), "rendering into a slice of a 2D array color target with an XOR tile mode needs the resident image of its array"); if (binding.gpuTiling) { binding.mip = ColorTargetMip(color, colorLayout); binding.tiled = std::make_unique(context, colorLayout.Bytes(), copies); @@ -1933,7 +1963,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); - const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr; }); + const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; const auto& contentKey = resolved.contentKey; @@ -2044,6 +2074,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; const bool lean = recorded && !drawTransitions; + const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); @@ -2064,8 +2096,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. std::vector> owners; owners.reserve(targets.size()); - for (const auto& binding : targets) owners.push_back(binding.resident); - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent); + std::vector persistent; + persistent.reserve(targets.size()); + for (const auto& binding : targets) { + owners.push_back(binding.resident); + persistent.push_back(binding.multisampled ? 1u : 0u); + } + auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2089,7 +2126,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty()) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2478,6 +2515,15 @@ std::shared_ptr metadataPassResident(const Context& context, con } void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { + if (pass.mode == ColorMetadataPass::Mode::Resolve) { + Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination"); + const auto& destination = pass.targets.front(); + Require(destination.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented"); + const auto resident = CachedStorageSurface(context, SurfaceForTarget(destination)); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 6891b95eb..73e1015e5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -113,6 +113,8 @@ struct GuestBufferMemory::AddressSpace { namespace { +struct RetiredImport; + struct AddressSpaceCache { std::atomic> current; std::atomic serials{0}; @@ -148,6 +150,7 @@ struct HostImports { PFN_vkDestroyBuffer destroyBuffer = nullptr; PFN_vkFreeMemory freeMemory = nullptr; std::map imports; + std::map>> parked; std::uint64_t liveBytes = 0; std::set failed; std::set unaliasedSpans; @@ -763,8 +766,8 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc if (state.device != context.device) { for (const auto& [address, entry] : state.imports) { if (state.device != VK_NULL_HANDLE && state.destroyBuffer != nullptr && state.freeMemory != nullptr) { - state.destroyBuffer(state.device, entry.buffer, nullptr); - state.freeMemory(state.device, entry.memory, nullptr); + state.parked[state.device].push_back(std::make_shared(state.device, state.destroyBuffer, state.freeMemory, entry)); + continue; } #ifdef _WIN32 GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias); @@ -1731,6 +1734,7 @@ void ClearImageMirrors(VkDevice device) { void ClearHostImports(VkDevice device) { auto& state = Imports(); std::lock_guard lock(state.mutex); + state.parked.erase(device); if (state.device != device) return; for (const auto& [address, entry] : state.imports) { state.destroyBuffer(state.device, entry.buffer, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp new file mode 100644 index 000000000..743b469cd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -0,0 +1,332 @@ +#include +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +bool sameTarget(const ColorTarget& a, const ColorTarget& b) { + return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; +} + +VkClearColorValue fastClearColor(const ColorTarget& target) { + Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); + std::array texels{}; + for (std::size_t offset = 0; offset < texels.size(); offset += target.elementBytes) std::memcpy(texels.data() + offset, target.clearWords.data(), target.elementBytes); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "the fast-clear color of a multisampled target has no exact clear value in its format"); + return clear; +} + +template +void recordCommands(const Context& context, Record&& record) { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder); + if (batch) batch->SubmitAndWait(); +} + +class MultisampleTarget { +public: + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) { + this->context.bufferPool.reset(); + const auto samples = static_cast(target.samples); + Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VkImageFormatProperties supported{}; + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearColorValue zero{}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } catch (...) { + release(); + throw; + } + } + ~MultisampleTarget() { release(); } + MultisampleTarget(const MultisampleTarget&) = delete; + MultisampleTarget& operator=(const MultisampleTarget&) = delete; + + void ApplyFastClear(const ColorTarget& use) { + if (!cmask.TakeClear()) return; + const auto clear = fastClearColor(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + + VkRenderPass ResolvePass() { + if (resolvePass != VK_NULL_HANDLE) return resolvePass; + std::array attachments{}; + attachments[0].format = target.format; + attachments[0].samples = static_cast(target.samples); + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].format = target.format; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + for (auto& attachment : attachments) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + } + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_GENERAL}; + const VkAttachmentReference resolve{1, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + subpass.pResolveAttachments = &resolve; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = static_cast(attachments.size()); + info.pAttachments = attachments.data(); + info.subpassCount = 1; + info.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &resolvePass), "vkCreateRenderPass resolve"); + return resolvePass; + } + + Context context; + ColorTarget target; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkRenderPass resolvePass = VK_NULL_HANDLE; + MultisampledCmask cmask; + +private: + void release() noexcept { + if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +struct GuardedRange { + VkDevice device; + std::uint64_t address; + std::uint64_t bytes; + std::uint32_t samples; +}; + +std::shared_mutex& guardedMutex() { + static std::shared_mutex mutex; + return mutex; +} + +std::vector& guarded() { + static std::vector ranges; + return ranges; +} + +std::atomic& anyGuarded() { + static std::atomic any{false}; + return any; +} + +void guardReads(std::uint64_t address, std::size_t bytes) { + RequireNoMultisampleTarget(address, bytes, "a CPU read"); +} + +void guard(const Context& context, const ColorTarget& target) { + const auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); + std::unique_lock lock(guardedMutex()); + guarded().push_back({context.device, target.address, bytes, target.samples}); + anyGuarded().store(true, std::memory_order_release); + static std::once_flag installed; + std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); +} + +std::mutex& targetsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& targets() { + static std::vector> list; + return list; +} + +MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { + for (const auto& target : targets()) { + if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + target->cmask.CheckAddress(use.cmaskAddress); + target->ApplyFastClear(use); + return *target; + } + targets().push_back(std::make_unique(context, use)); + guard(context, use); + auto& created = *targets().back(); + if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); + created.ApplyFastClear(use); + return created; +} + +} + +void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the CMASK 0x%llx+0x%llx of a multisampled color target, leaving its tiles mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + if (pattern == 0) { + keys = DccKeys::Clear0000; + } else if (pattern == 0xffffffffu) { + keys = DccKeys::Uncompressed; + } else { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast(address), static_cast(bytes), pattern); + refusal = text; + } +} + +void MultisampledCmask::SeedKeys(DccKeys seeded) { + if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) { + keys = seeded; + return; + } + char text[192]; + std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast(address), DccKeysName(seeded)); + refusal = text; +} + +void MultisampledCmask::CheckAddress(std::uint64_t cmaskAddress) const { + if (cmaskAddress == address) return; + char text[192]; + std::snprintf(text, sizeof(text), "a multisampled color target used CMASK 0x%llx after its first use with CMASK 0x%llx, which is not modeled", static_cast(cmaskAddress), static_cast(address)); + Require(false, std::string(text)); +} + +bool MultisampledCmask::TakeClear() { + Require(refusal.empty(), refusal); + if (keys != DccKeys::Clear0000) return false; + keys = DccKeys::Uncompressed; + return true; +} + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + return acquire(context, target).view; +} + +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { + std::lock_guard lock(targetsMutex()); + auto& target = acquire(context, source); + const std::array views{target.view, destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(), views, source.extent); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = target.ResolvePass(); + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, source.extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + if (recorder != nullptr) { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(framebuffer); + } + }); + destination.MarkDirty(); +} + +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + std::lock_guard lock(targetsMutex()); + for (const auto& target : targets()) { + const auto before = target->cmask.Keys(); + target->cmask.NoteFill(address, bytes, pattern); + if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; + static std::atomic reports{0}; + if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); + } +} + +void ClearMultisampleTargets(VkDevice device) { + std::lock_guard lock(targetsMutex()); + std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); + std::unique_lock guardLock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); +} + +void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { + if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; + std::shared_lock lock(guardedMutex()); + for (const auto& range : guarded()) { + if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); + throw std::runtime_error(text); + } +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d47dcb3da..27bfd7ee9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -115,23 +115,10 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::min(viewport.y, viewport.y + viewport.height) >= context.limits.viewportBoundsRange[0] && std::max(viewport.y, viewport.y + viewport.height) <= context.limits.viewportBoundsRange[1], "viewport Y exceeds device bounds"); } -namespace { - -VkShaderStageFlags pushStagesOf(const Context& context) { - return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); -} - -template -void appendKey(std::vector& key, const TValue& value) { - static_assert(std::is_trivially_copyable_v); - const auto bytes = std::as_bytes(std::span(&value, 1)); - key.insert(key.end(), bytes.begin(), bytes.end()); -} - -bool fragmentWritesMemory(std::span shaders) { +bool ShadersWriteMemory(std::span shaders) { const auto any = [](const std::vector& written, std::uint32_t count) { return written.size() < count || std::ranges::any_of(written, [](bool element) { return element; }); }; for (const auto& shader : shaders) { - if (shader.stage != ShaderRecompiler::ShaderStage::Fragment || shader.program == nullptr) continue; + if (shader.program == nullptr) continue; for (const auto& binding : shader.program->bindings) { switch (binding.role) { case ShaderRecompiler::DescriptorRole::GuestBuffers: @@ -154,6 +141,19 @@ bool fragmentWritesMemory(std::span shaders) { return false; } +namespace { + +VkShaderStageFlags pushStagesOf(const Context& context) { + return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader); +} + +template +void appendKey(std::vector& key, const TValue& value) { + static_assert(std::is_trivially_copyable_v); + const auto bytes = std::as_bytes(std::span(&value, 1)); + key.insert(key.end(), bytes.begin(), bytes.end()); +} + bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const ShaderResources& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false; @@ -186,6 +186,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias); appendKey(keys.preRasterization, state.stages.tessellation.has_value()); if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); + appendKey(keys.fragmentShader, state.samples); + appendKey(keys.fragmentOutput, state.samples); for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); @@ -209,7 +211,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput this->context.bufferPool.reset(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments"); + Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments"); @@ -270,7 +273,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = VK_SAMPLE_COUNT_1_BIT; + color.samples = static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -288,7 +291,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.depth) { VkAttachmentDescription depth{}; depth.format = state.depth->format; - depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.samples = static_cast(state.samples); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; @@ -363,7 +366,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.pNext = &provoking; } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + samples.rasterizationSamples = static_cast(state.samples); VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -379,7 +382,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (alternateAlpha) { const auto keepsStencil = [](const VkStencilOpState& face) { return face.writeMask == 0 || (face.failOp == VK_STENCIL_OP_KEEP && face.passOp == VK_STENCIL_OP_KEEP && face.depthFailOp == VK_STENCIL_OP_KEEP); }; Require(!(state.depth && (state.depthWrite || (state.stencilTest && (!keepsStencil(state.stencilFront) || !keepsStencil(state.stencilBack))))) && !state.depthBiasPerFace, "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw that writes depth or stencil is not implemented"); - Require(!fragmentWritesMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a pixel shader that writes memory is not implemented"); + Require(!ShadersWriteMemory(shaders), "blending into an 8_8 color target with the alternate component order and a separate alpha blend from a draw whose shaders write memory is not implemented"); for (std::size_t index = 0; index < secondChannels.size(); ++index) { const bool split = index < state.colors.size() && state.colors[index].componentMapping == 0xecu && state.blends[index].blendEnable; if (!split) { @@ -482,21 +485,26 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { +std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent) { Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); - const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); + bool resident = true; + for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0)); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds // them (Kept keeps its framebuffer until the batch completes). std::erase_if(framebuffers, [](const CachedFramebuffer& entry) { - return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); }); + if (entry.framebuffer.use_count() != 1) return false; + for (std::size_t i = 0; i < entry.owners.size(); ++i) { + if (entry.owned[i] && entry.owners[i].expired()) return true; + } + return false; }); for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) { if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; // View handles are recycled once a StorageTexture is destroyed, so the owners must be the // very objects the views were made for. bool same = true; - for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get(); + for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owned[i] == (owners[i] != nullptr) && it->owners[i].lock().get() == owners[i].get(); if (!same) continue; std::rotate(it, std::next(it), framebuffers.end()); return framebuffers.back().framebuffer; @@ -514,6 +522,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span& key, const Context& context, const Stat append(key, state.blends.size()); for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); + append(key, state.samples); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp index 70026869a..c21f1d06a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Resources.cpp @@ -103,7 +103,7 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag ready = true; return; } - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory"); initializeAddress(usage); if ((properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) Check(context.Function("vkMapMemory")(context.device, memory, 0, VK_WHOLE_SIZE, 0, &mapping), "vkMapMemory"); @@ -182,7 +182,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa allocation.allocationSize = ClassAllocationBytes(requirements, capacity); allocationBytes = allocation.allocationSize; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory device buffer"); + memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer"); Check(context.Function("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 486df0e35..3e89ca766 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -93,6 +93,16 @@ ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& }; } +ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports) { + for (std::uint32_t input = 0; input < pixel.interpolatorCount && input < pixel.interpolatorSettings.size(); ++input) { + auto& setting = pixel.interpolatorSettings[input]; + if ((setting & 0x20u) != 0u || std::ranges::find(parameterExports, setting & 0x1fu) != parameterExports.end()) continue; + setting = (setting & ~0x700u) | 0x20u; + if ((setting & 0x80000u) != 0u) setting = (setting & ~0x600000u) | 0x100000u; + } + return pixel; +} + ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram) { const auto inControl = nullProgram ? 0u : read(context, spiPsInControl, RegisterBank::Context); const auto inputNum = inControl & 0x3Fu; @@ -162,7 +172,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con }; } -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi) { +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); Shader shader; @@ -185,14 +195,16 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); - if (static_cast(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); + const auto bufferWord = static_cast(vertexBufferReg) + hiddenUserWords; + const auto attribWord = static_cast(vertexAttribReg) + hiddenUserWords; + if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); + if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain"); if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics"); std::array semantics{}; _readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data()); - const auto attribTableAddr = static_cast(userData[static_cast(vertexAttribReg)]) | (static_cast(userData[static_cast(vertexAttribReg) + 1u]) << 32u); - const auto bufferTableAddr = static_cast(userData[static_cast(vertexBufferReg)]) | (static_cast(userData[static_cast(vertexBufferReg) + 1u]) << 32u); + const auto attribTableAddr = static_cast(userData[attribWord]) | (static_cast(userData[attribWord + 1u]) << 32u); + const auto bufferTableAddr = static_cast(userData[bufferWord]) | (static_cast(userData[bufferWord + 1u]) << 32u); if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address"); if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address"); info.fetchEmbedded = true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b6f7cadd2..0383b7571 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -513,6 +514,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); @@ -831,13 +833,14 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (DepthSurfaceAt(viewed.baseAddress)) { char text[112]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); throw std::runtime_error(text); } static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; - if (disabled) return std::make_shared(context, *context.detiler, viewed, mip); + if (disabled) return StorageTexture::Create(context, *context.detiler, viewed, mip); static_cast(words); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); const bool profile = LookupOutcomes::Profiled(); @@ -869,7 +872,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } else { if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; if (StorageTexture::FlushPending(resource.baseAddress, static_cast(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); - CachedStorageTexture entry{key, mip, std::make_shared(context, *context.detiler, resource, mip)}; + CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)}; static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); @@ -969,18 +972,29 @@ void ClearCachedTextures(VkDevice device) { } } auto& storage = StorageTextures(); - std::lock_guard lock(storage.mutex); - for (auto it = storage.entries.begin(); it != storage.entries.end();) { - if (it->key.device != device) { - ++it; - continue; + std::vector> cleared; + { + std::lock_guard lock(storage.mutex); + for (auto it = storage.entries.begin(); it != storage.entries.end();) { + if (it->key.device != device) { + ++it; + continue; + } + cleared.push_back(it->texture); + it->texture->SetCached(false); + noteUncached(it->texture); + storage.bytes -= it->accounted; + storage.index.erase(it->key); + storage.byImage.erase(it->texture.get()); + it = storage.entries.erase(it); + } + } + for (const auto& image : cleared) { + std::vector> resources; + SharedResourceCache().RemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); } - it->texture->SetCached(false); - noteUncached(it->texture); - storage.bytes -= it->accounted; - storage.index.erase(it->key); - storage.byImage.erase(it->texture.get()); - it = storage.entries.erase(it); } } @@ -1135,14 +1149,13 @@ void ShaderResources::build(std::span shaders, const Color namespace { -VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind) { - std::vector flags; - for (const auto& binding : bindings) flags.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); +VkDescriptorSetLayout CreateSetLayout(const Context& context, std::span bindings, bool updateAfterBind, std::span flags) { + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); VkDescriptorSetLayoutBindingFlagsCreateInfo bindingFlags{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO}; bindingFlags.bindingCount = static_cast(flags.size()); bindingFlags.pBindingFlags = flags.data(); VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; - info.pNext = updateAfterBind ? &bindingFlags : nullptr; + info.pNext = std::ranges::any_of(flags, [](auto value) { return value != 0; }) ? &bindingFlags : nullptr; info.flags = updateAfterBind ? VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT : 0; info.bindingCount = static_cast(bindings.size()); info.pBindings = bindings.data(); @@ -1404,16 +1417,17 @@ void ShaderResources::buildPrepare(std::span shaders, cons std::vector description; for (const auto& binding : bindings) { description.push_back(binding.layout); + layoutBindingFlags.push_back(updateAfterBind && binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); layoutKey.insert(layoutKey.end(), {binding.layout.binding, static_cast(binding.layout.descriptorType), binding.layout.descriptorCount, binding.layout.stageFlags}); } if (updateAfterBind) layoutKey.push_back(VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); static const bool noLayoutCache = std::getenv("APS5_NO_LAYOUT_CACHE") != nullptr; static const bool noPoolCache = std::getenv("APS5_NO_POOL_CACHE") != nullptr; if (context.descriptorCache != nullptr && !noLayoutCache) { - _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind); + _layout = context.descriptorCache->Layout(layoutKey, description, updateAfterBind, layoutBindingFlags); } else { ValidateDescriptorLayout(context, description, updateAfterBind); - _layout = CreateSetLayout(context, description, updateAfterBind); + _layout = CreateSetLayout(context, description, updateAfterBind, layoutBindingFlags); ownsLayout = true; } if (!bindings.empty()) { @@ -2524,6 +2538,7 @@ void ResourceCache::noteMiss(const Key& key) { void ResourceCache::Insert(const Key& key, std::shared_ptr resources, std::vector>* evicted) { std::lock_guard lock(mutex); + if (!resources->HasCachedStorageImages()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -2531,9 +2546,6 @@ void ResourceCache::Insert(const Key& key, std::shared_ptr reso } entries.emplace_front(key, std::move(resources)); index.emplace(key, entries.begin()); - // Entries pin their textures and storage images past the texture caches' budgets, so the bound - // stays modest: APS5_RESOURCE_CACHE_ENTRIES (default 1024) covers several frames of distinct - // dispatch and draw content. static const std::size_t capacity = [] { const char* value = std::getenv("APS5_RESOURCE_CACHE_ENTRIES"); const auto parsed = value ? std::strtoull(value, nullptr, 10) : 1024ull; @@ -2617,16 +2629,24 @@ DescriptorCache::~DescriptorCache() { for (const auto& [key, layout] : layouts) destroyLayout(context.device, layout, nullptr); } -VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind) { +VkDescriptorSetLayout DescriptorCache::Layout(std::span key, std::span bindings, bool updateAfterBind, std::span flags) { + std::vector defaults; + if (flags.empty()) { + for (const auto& binding : bindings) defaults.push_back(updateAfterBind && binding.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0); + flags = defaults; + } + Require(flags.size() == bindings.size(), "descriptor binding flag count differs from layout"); std::lock_guard lock(mutex); std::vector keyCopy(key.begin(), key.end()); + keyCopy.push_back(static_cast(flags.size())); + keyCopy.insert(keyCopy.end(), flags.begin(), flags.end()); if (const auto found = layouts.find(keyCopy); found != layouts.end()) { ++stats.layoutHits; return found->second; } ValidateDescriptorLayout(context, bindings, updateAfterBind); ++stats.layoutMisses; - const auto layout = CreateSetLayout(context, bindings, updateAfterBind); + const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags); layouts.emplace(std::move(keyCopy), layout); return layout; } @@ -3144,9 +3164,9 @@ void ShaderResources::applyAnisoOverride() { if (!variant.has_value()) continue; const auto& words = *variant; if (context.samplerCache != nullptr && !SamplerCacheDisabled()) { - samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, false, source.integerBorder); + samplers[index] = context.samplerCache->Get(context, words, source.compareEnable, source.unnormalized, true, source.integerBorder); } else { - auto resource = DecodeSamplerResource(words, source.unnormalized, false, source.integerBorder); + auto resource = DecodeSamplerResource(words, source.unnormalized, true, source.integerBorder); resource.compareEnable = source.compareEnable; samplers[index] = std::make_shared(context, resource); } @@ -3322,6 +3342,16 @@ VkDescriptorSetLayout ShaderResources::Layout() const { return _layout; } +std::vector ShaderResources::LayoutBindings() const { + std::vector result; + for (const auto& binding : bindings) result.push_back(binding.layout); + return result; +} + +bool ShaderResources::LayoutUpdateAfterBind() const { + return updateAfterBind; +} + ShaderResources::DrawBindings::~DrawBindings() { if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); } @@ -3374,9 +3404,17 @@ std::optional> ShaderResources::MovedR return moved; } +std::size_t ShaderResources::MaxDrawSnapshotBytes() { + const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull; + return static_cast(mib * 1024ull * 1024ull); +} + std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved) const { if (_set == VK_NULL_HANDLE || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); + const auto maxSnapshotBytes = MaxDrawSnapshotBytes(); + std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); std::vector selected; for (std::size_t index = 0; index < allocations.size(); ++index) { @@ -3404,6 +3442,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } const auto begin = address - item.adjustment; const auto bytes = static_cast(GuestBufferMemory::ViewBytes(size, item.adjustment)); + if (override == moved.end() && (bytes > maxSnapshotBytes || totalSnapshotBytes + bytes > maxSnapshotBytes)) continue; const auto registryGeneration = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); const auto generation = GuestMemory::CollectWrites(begin, bytes); auto buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); @@ -3414,6 +3453,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi } selected.push_back(index); result->snapshots.push_back({begin, std::move(buffer)}); + totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } if (selected.empty()) return {}; @@ -3526,6 +3566,11 @@ std::vector> ShaderResources::StorageImages() const { return images; } +bool ShaderResources::HasCachedStorageImages() const { + if (std::any_of(storageTextures.begin(), storageTextures.end(), [](const auto& image) { return image != nullptr && !image->Cached(); })) return false; + return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index cf0d6b1ea..3316fd778 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -138,6 +140,14 @@ bool depthSurfaceBound(const Registers& cx) { return (z != cx.end() && (z->second & 3u) != 0) || (stencil != cx.end() && (stencil->second & 1u) != 0); } +std::uint32_t planeTests(const Registers& cx, std::uint32_t depthControl) { + const auto z = find(cx, 0x010); + const auto stencil = find(cx, 0x011); + if (z != cx.end() && (z->second & 3u) == 0) depthControl &= ~2u; + if (stencil != cx.end() && (stencil->second & 1u) == 0) depthControl &= ~1u; + return depthControl & 0xbu; +} + bool depthPlanesAbsent(const Registers& cx) { const auto z = find(cx, 0x010); const auto stencil = find(cx, 0x011); @@ -199,7 +209,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; @@ -215,6 +225,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; + depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); @@ -302,6 +313,34 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { return (depthControl & 3u) == 0 ? depthControl & ~4u : depthControl; } +constexpr std::array, 2> StandardLocations2{{{4, 4}, {-4, -4}}}; +constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; +constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; + +std::uint32_t decodeSamples(const Registers& cx) { + const auto config = read(cx, 0x2f8); + zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); + const auto log2 = config & 7u; + if (log2 == 0) { + Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); + return 1; + } + Require(log2 <= 3u, "more than eight samples are unsupported"); + Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); + const auto samples = 1u << log2; + const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = read(cx, 0x2fe + pixel * 4u + sample / 4u); + const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; + const auto x = static_cast(static_cast(bits << 4u)) >> 4; + const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; + Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + } + } + return samples; +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); @@ -579,13 +618,15 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + result.samples = decodeSamples(cx); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); const bool clear = renderControl != cx.end() && (renderControl->second & 3u) != 0; - if (clear || ((depthControl & 0xbu) != 0 && depthSurfaceBound(cx))) { + const auto tests = planeTests(cx, depthControl); + if (clear || (tests != 0 && depthSurfaceBound(cx))) { decodeDepth(cx, depthControl, result); - } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && depthPlanesAbsent(cx))) { + } else if (depthPassThrough(depthControl) || ((depthControl & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx)))) { static bool reported = false; if (!reported) { reported = true; @@ -606,8 +647,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); - zero(cx, 0x292, ScanModeMask, "scan conversion mode"); + Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); + zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); + Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); @@ -669,6 +711,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported"); color.exportIndex = index; color.uintExport = slotExport == 7; + Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; @@ -771,6 +814,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (mapping == 0xecu) { const auto destinationAlpha = [](VkBlendFactor factor) { return factor == VK_BLEND_FACTOR_DST_ALPHA || factor == VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA || factor == VK_BLEND_FACTOR_SRC_ALPHA_SATURATE; }; if (destinationAlpha(state.srcColorBlendFactor) || destinationAlpha(state.dstColorBlendFactor) || destinationAlpha(state.srcAlphaBlendFactor) || destinationAlpha(state.dstAlphaBlendFactor)) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a factor of the destination alpha, which the target keeps in its second channel, is not implemented"); + const auto weighted = [](VkBlendOp operation) { return operation != VK_BLEND_OP_MIN && operation != VK_BLEND_OP_MAX; }; + if ((weighted(state.colorBlendOp) && (constantColor(state.srcColorBlendFactor) || constantColor(state.dstColorBlendFactor))) || (weighted(state.alphaBlendOp) && (constantColor(state.srcAlphaBlendFactor) || constantColor(state.dstAlphaBlendFactor)))) throw std::runtime_error("AGC graphics: blending into an 8_8 color target with the alternate component order with a constant color factor, which Vulkan applies to the second channel with the green blend constant instead of the alpha one, is not implemented"); } } if (color.packing != ShaderRecompiler::ColorExportPacking::None) { @@ -849,17 +894,24 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap); const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); + const auto attrib = read(cx, 0x31d + stride); + zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes"); + color.samples = 1u << ((attrib >> 12u) & 7u); + Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); + Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); + const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; + Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported"); const auto viewMip = (view >> 26u) & 0xfu; - zero(cx, 0x31d + stride, ~0x20000u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; + Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); @@ -872,6 +924,8 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.depthSlice = slice; } else if ((attrib3 & 0x1fffu) != 0) { Require(slice <= (attrib3 & 0x1fffu), "the color view slice is beyond the array surface"); + color.arrayLastSlice = attrib3 & 0x1fffu; + color.arraySlice = slice; } color.extent = {((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u}; color.elementBytes = decoded.elementBytes; @@ -895,6 +949,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { color.pipeBankXor = static_cast(color.surfaceAddress & (ColorTileModeBlockBytes(color.tileMode) - 1u)); color.surfaceAddress -= color.pipeBankXor; } + color.arrayAddress = color.arrayLastSlice != 0 ? color.surfaceAddress : 0; if (slice != 0 && !volume) color.surfaceAddress += slice * ComputeSurfaceSize(ComputeElementMipLayout(ColorTextureTileMode(color.tileMode), color.elementBytes, color.surfaceExtent.width, color.surfaceExtent.height, color.mipCount), 1); color.address = color.surfaceAddress + mipOffset; color.bytes = colorLayout.Bytes(); @@ -908,15 +963,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } - if ((info & 0x2000u) != 0) { - Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) { + Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); - Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); const auto cmaskHigh = find(cx, 0x398 + slot); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); - color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); - GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported"); + if (cmask != 0) { + color.cmaskAddress = cmask; + color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); + if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + } + color.cmaskFastClear = (info & 0x2000u) != 0; } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -940,11 +999,14 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto control = find(cx, 0x202); if (control == cx.end()) return std::nullopt; const auto mode = (control->second >> 4u) & 7u; - if (mode != 2u && mode != 6u) return std::nullopt; - ColorMetadataPass pass{mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}}; + const auto aaConfig = find(cx, 0x2f8); + const bool resolve = mode == 3u && aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + if (mode != 2u && mode != 6u && !resolve) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); + const auto samples = resolve ? decodeSamples(cx) : 1u; + if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); @@ -957,16 +1019,32 @@ std::optional DecodeColorMetadataPass(const QueueState& queue intersect(covered, cx, 0x81, false); intersect(covered, cx, 0x90, false); if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); - const auto targetMask = read(cx, 0x8e); - for (std::uint32_t slot = 0; slot < 8; ++slot) { - if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + const auto requireCovered = [&](const ColorTarget& target) { const auto width = static_cast(target.extent.width); const auto height = static_cast(target.extent.height); const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height; const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height; Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target"); + }; + if (resolve) { + const auto source = DecodeColorBuffer(cx, 0); + const auto destination = DecodeColorBuffer(cx, 1); + Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); + Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); + Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + requireCovered(destination); + pass.source = source; + pass.targets.push_back(destination); + return pass; + } + const auto targetMask = read(cx, 0x8e); + for (std::uint32_t slot = 0; slot < 8; ++slot) { + if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; + const auto target = DecodeColorBuffer(cx, slot); + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); + Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + requireCovered(target); pass.targets.push_back(target); } return pass; @@ -1008,8 +1086,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { std::uint32_t z = 0, view = 0; if ((value(cx, 0x010, z) && (z & 3u) == 0) || (value(cx, 0x002, view) && (view & 0x01000000u) != 0)) return require(false, "depth clear requires a writable depth plane"); } - const bool surface = ((word & 0xbu) != 0 || depthClear || stencilClear) && depthSurfaceBound(cx); - if (!surface && !((word & 0xbu) != 0 && depthPlanesAbsent(cx)) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); + const auto tests = planeTests(cx, word); + const bool surface = (tests != 0 || depthClear || stencilClear) && depthSurfaceBound(cx); + if (!surface && !((word & 0xbu) != 0 && (tests == 0 || depthPlanesAbsent(cx))) && !depthPassThrough(word) && !IgnoreDepthTest() && (effectiveDepthControl(word) & DepthControlMask) != 0) return zeroMessage(0x200, word, "depth, stencil or conditional color writes"); if (auto reason = require((word & 8u) == 0 || surface || depthPlanesAbsent(cx), "depth bounds without a depth surface"); !reason.empty()) return reason; if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason; } @@ -1019,8 +1098,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; + if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 7a21d9720..2753e91fc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -866,9 +866,29 @@ VkFormat AttachmentProxyFormat(const Context& context, VkFormat format) { return proxy; } -StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { +std::shared_ptr StorageTexture::Create(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; PhaseTimer timer; + auto texture = std::shared_ptr(new StorageTexture(context, detiler, descriptor, mipLevel)); + uploadReason = "first"; + texture->upload(); + texture->defaultMip = mipLevel; + texture->view = texture->createView(mipLevel, false, texture->storageFormat); + { + auto& live = Live(); + std::lock_guard lock(live.mutex); + live.textures.push_back(texture.get()); + } + if (profile) { + auto& totals = Profile(); + totals.storageCreate += timer.lap(); + totals.storageBytes += texture->guestBytes; + if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); + } + return texture; +} + +StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, const GuestTextureResource& descriptor, std::uint32_t mipLevel) : context(context), detiler(detiler), descriptor(descriptor) { try { Require(!IsBlockCompressed(descriptor.format), "block-compressed textures cannot be storage images"); Require(mipLevel < descriptor.mipCount, "storage texture mip level is outside the texture"); @@ -909,7 +929,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, { VkFormatProperties properties{}; context.formatProperties(context.physical, vkFormat, &properties); - attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; + attachable = (descriptor.dimension == TextureDimension::k2D || descriptor.dimension == TextureDimension::k3D || descriptor.dimension == TextureDimension::k2DArray) && (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0; if (attachable) imageInfo.usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; } imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; @@ -923,21 +943,6 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); - uploadReason = "first"; - upload(); - defaultMip = mipLevel; - view = createView(mipLevel, false, storageFormat); - { - auto& live = Live(); - std::lock_guard lock(live.mutex); - live.textures.push_back(this); - } - if (profile) { - auto& totals = Profile(); - totals.storageCreate += timer.lap(); - totals.storageBytes += guestBytes; - if (++totals.storageCount % 50 == 0) std::fprintf(stderr, "[texture] %llu storage images (%.0f MiB): create %.0f ms, write-back %.0f ms (alloc %.0f, host copy %.0f, gpu %.0f, store %.0f), %llu reused, %llu direct uploads, %llu direct write-backs, clears %llu recorded / %llu waited\n", static_cast(totals.storageCount), totals.storageBytes / 1048576.0, totals.storageCreate, totals.storageWriteBack, totals.storageAlloc, totals.storageHostCopy, totals.storageGpu, totals.storageStore, static_cast(totals.storageReused), static_cast(totals.storageDirectUploads), static_cast(totals.storageDirectWriteBacks), static_cast(totals.storageRecordedClears), static_cast(totals.storageWaitedClears)); - } } catch (...) { release(); throw; @@ -1265,6 +1270,10 @@ bool ClearKeepsDenormals(const Context& context, VkFormat format) { return keeps; } +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { + return FillClearColor(format, elementBytes, pattern, clear); +} + VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const { Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture"); Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); @@ -1317,12 +1326,13 @@ VkImageView StorageTexture::elementLayerView(std::uint32_t level, std::uint32_t return created; } -VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice) { +VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, std::uint32_t depthSlice, std::uint32_t layer) { Require(attachable, "storage image cannot be a color attachment"); Require(mip < descriptor.mipCount, "attachment mip exceeds the storage image"); const bool volume = descriptor.dimension == TextureDimension::k3D; Require(depthSlice == 0 || (volume && mip == 0 && depthSlice <= descriptor.depthOrLastArray), "attachment slice is outside the storage image"); - const auto found = attachmentViews.find({format, mip, depthSlice}); + Require(layer == 0 || (!volume && descriptor.dimension == TextureDimension::k2DArray && descriptor.baseArray + layer <= descriptor.depthOrLastArray), "attachment layer is outside the storage image"); + const auto found = attachmentViews.find({format, mip, depthSlice, layer}); if (found != attachmentViews.end()) return found->second; VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usage.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; @@ -1332,10 +1342,10 @@ VkImageView StorageTexture::AttachmentView(VkFormat format, std::uint32_t mip, s viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.format = format; viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; - viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray, 1u}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, mip, 1u, volume ? depthSlice : descriptor.baseArray + layer, 1u}; VkImageView created = VK_NULL_HANDLE; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &created), "vkCreateImageView attachment"); - attachmentViews.emplace(std::tuple{format, mip, depthSlice}, created); + attachmentViews.emplace(std::tuple{format, mip, depthSlice, layer}, created); return created; } @@ -2970,6 +2980,12 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +bool StorageTexture::AnyPending() { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + return pending.textures.begin() != pending.textures.end(); +} + bool StorageTexture::StoreAtFlipRequested(const char* value) { if (value == nullptr) return false; if (std::strcmp(value, "1") == 0) return true; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 8d73dada8..860643bd2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -162,10 +162,12 @@ int main(int argc, char** argv) { buffer.Bytes()[255] = std::byte{0x5a}; Require(buffer.Bytes()[255] == std::byte{0x5a}, "real BDA buffer mapping failed"); if (device->RunsOnCpu()) { - std::cout << "CPU Vulkan device: BDA execution not tested\n"; + std::cout << "CPU Vulkan device: BDA read and multi-dword write execution not tested\n"; } else { RunBdaExecutionTests(device->GetContext()); } + RunBdaStoreExecutionTests(device->GetContext()); + std::cout << "BDA store boundary tests passed\n"; RunColorTransferTests(device->GetContext()); std::cout << "Vulkan BDA allocation and execution tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index bf7674cc4..5fc40da4e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -1,10 +1,13 @@ #include "BdaShader.hpp" #include "BdaAbi.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include #include #include +#include #include +#include #include #include @@ -330,3 +333,73 @@ void RunBdaExecutionTests(const Context& context) { run(guest, 8, 0, Abi::FaultReason::Permission); RunBdaDwordWriteTests(context); } + +void RunBdaStoreExecutionTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + Buffer first(context, 2, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer second(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); + Buffer table(context, sizeof(Abi::Header) + 2 * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, Abi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, 4, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + constexpr std::uint64_t guest = 0x7fff1235fffdULL; + constexpr std::uint32_t writable = Abi::Read | Abi::Write; + const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0}; + std::array ranges{}; + const auto run = [&](const char* name, std::uint64_t address, std::uint32_t bits, bool coherent, const std::array& expected, std::initializer_list writtenAddresses, Abi::FaultReason reason = {}, std::uint64_t faultAddress = 0, std::uint32_t faultBytes = 0) { + const std::string label = std::string("BDA store ") + name + (coherent ? " coherent: " : ": "); + std::memset(first.Bytes().data(), 0xcd, first.Bytes().size()); + std::memset(second.Bytes().data(), 0xcd, second.Bytes().size()); + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0, output.Bytes().size()); + Pipeline pipeline(context, MakeBdaStoreTestShader(address, 0xa1b2c3d4u, bits, coherent), {&table, &fault, &output}); + pipeline.Run(1); + std::array actual{}; + std::memcpy(actual.data(), first.Bytes().data(), 2); + std::memcpy(actual.data() + 2, second.Bytes().data(), 3); + Require(actual == expected, label + "incorrect stored bytes or writes outside the mapped span"); + Abi::Fault report{}; + std::memcpy(&report, fault.Bytes().data(), sizeof(report)); + if (reason == Abi::FaultReason{}) { + Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault"); + } else { + Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata"); + } + std::array words{}; + std::memcpy(words.data(), fault.Bytes().data(), sizeof(words)); + Require(words[Abi::WrittenOverflowWord] == 0, label + "written-page table overflowed"); + std::set recordedPages; + Require(Abi::ForEachWrittenRange(words.data(), [&](std::uint64_t address, std::size_t bytes) { + for (std::size_t offset = 0; offset < bytes; offset += std::size_t{1} << Abi::WrittenPageShift) { + Require(recordedPages.insert((address + offset) >> Abi::WrittenPageShift).second, label + "written-page table contains duplicate pages"); + } + }), label + "invalid written-page record"); + std::set expectedPages; + std::set expectedChunks; + for (const auto writtenAddress : writtenAddresses) { + expectedPages.insert(writtenAddress >> Abi::WrittenPageShift); + expectedChunks.insert(Abi::WrittenChunkKey(writtenAddress)); + } + Require(recordedPages == expectedPages, label + "incorrect written-page set"); + Require(words[Abi::WrittenCountWord] == expectedChunks.size(), label + "incorrect written-chunk claim count"); + std::uint32_t completed = 0; + std::memcpy(&completed, output.Bytes().data(), sizeof(completed)); + Require(completed == 1, label + "store did not finish its per-byte accesses"); + }; + for (const bool coherent : {false, true}) { + ranges = {{{guest, guest + 2, first.DeviceAddress(), writable, 0}, {guest + 2, guest + 5, second.DeviceAddress(), writable, 0}}}; + run("byte", guest + 1, 8, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}); + run("split short", guest + 1, 16, coherent, {0xcd, 0xd4, 0xc3, 0xcd, 0xcd}, {guest + 1, guest + 2}); + run("split unaligned dword", guest, 32, coherent, {0xd4, 0xc3, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}); + ranges[1].permissions = Abi::Read; + run("read-only byte", guest + 2, 8, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only short suffix", guest + 1, 16, coherent, {0xcd, 0xd4, 0xcd, 0xcd, 0xcd}, {guest + 1}, Abi::FaultReason::Permission, guest + 2, 1); + run("read-only suffix", guest, 32, coherent, {0xd4, 0xc3, 0xcd, 0xcd, 0xcd}, {guest}, Abi::FaultReason::Permission, guest + 2, 1); + ranges[0].end = guest + 1; + ranges[1].permissions = writable; + run("unmapped middle byte", guest, 32, coherent, {0xd4, 0xcd, 0xb2, 0xa1, 0xcd}, {guest, guest + 3}, Abi::FaultReason::Unmapped, guest + 1, 1); + ranges = {{{guest + 3, guest + 5, first.DeviceAddress(), writable, 0}, {guest + 5, guest + 8, second.DeviceAddress(), writable, 0}}}; + run("split aligned dword", guest + 3, 32, coherent, {0xcd, 0xcd, 0xcd, 0xcd, 0xcd}, {}, Abi::FaultReason::Unmapped, guest + 3, 4); + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 032e0b9d7..777c3fa77 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -746,6 +746,42 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { releaseSparse(block, reserved); } +void hostWriteWatchExclusionTests() { +#ifdef _WIN32 + if (AgcDriver::GuestMemory::WriteWatched()) { + std::uintptr_t arena = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); + bool found = false; + for (auto cursor = arena; cursor < arena + arenaBytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); + if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) { + Require(!AgcDriver::GuestMemory::Watched(cursor, 1), "an arena host hole was reported as write-watched"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(cursor, 1) == 0, "an arena host hole returned a write generation"); + Require(!AgcDriver::GuestMemory::UnchangedSince(cursor, 1, 1), "an arena host hole passed an unchanged proof"); + const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{cursor, 1, 1}}; + Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "an arena host hole passed a bulk unchanged proof"); + found = true; + break; + } + const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; + Require(next > cursor, "arena enumeration did not advance"); + cursor = next; + } + Require(found, "write-watched arena contained no host exclusion to test"); + } + std::vector host(64, 0x31); + const auto address = reinterpret_cast(host.data()); + Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched"); + Require(AgcDriver::GuestMemory::CollectWritesUncached(address, host.size()) == 0, "ordinary host heap memory returned a guest write generation"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address, host.size(), 1), "ordinary host heap memory was proven unchanged"); + const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{address, host.size(), 1}}; + Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "a host heap range passed a bulk unchanged proof"); + Require(std::all_of(host.begin(), host.end(), [](std::uint8_t value) { return value == 0x31; }), "write-watch eligibility changed host bytes"); +#endif +} + void sparseHeapMirrorTests(const Context& context, const BdaTestAccess& access) { #ifdef _WIN32 if (!GuestArena::GuestArenaAvailable_nid_postfix() || !GuestArena::GuestArenaWriteWatched_nid_postfix()) { @@ -1077,6 +1113,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { gpuMappingTests(context); stridedOverhangTests(context, access); sparseHeapTests(context, access); + hostWriteWatchExclusionTests(); sparseHeapMirrorTests(context, access); Require(AddressCopyOverflow({{0x1000, 0x3000, 0x2000, "uncommitted pages"}}, 0x2000).empty(), "copies within the limit were refused"); const auto copies = AddressCopyOverflow({{0x1000, 0x2000, 0x1000, "not mirrored"}, {0x10000, 0x30000, 0x18000, "uncommitted pages"}}, 0x2000); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index efc43683d..529457a60 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -3,6 +3,9 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include "IntermediateRepresentation/IrBuilder.hpp" +#if ANYPS5_ENABLE_SPIRV_TOOLS +#include "SpirvBackend/SpirvOptimizer.hpp" +#endif namespace { @@ -40,7 +43,7 @@ std::vector MakeShader(std::uint64_t address, bool coherent, bool state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.module.AllocateId()); SpirvValueEmitContext ctx(state); - auto& instruction = extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); + auto& instruction = writes ? program.CreateValue(IrOpcode::StoreAddressU32, IrType::Void) : extracted != 0u ? program.CreateValue(IrOpcode::LoadAddressU32x4, IrType::U32x4) : program.CreateValue(IrOpcode::LoadAddressU32, IrType::U32); IrBuilder ir(program); for (std::uint32_t dword = 0; dword < 4u; ++dword) { if ((extracted & (1u << dword)) == 0u) continue; @@ -106,3 +109,15 @@ std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std return std::vector(values.begin(), values.begin() + dwords); }); } + +std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent) { + using namespace ShaderRecompiler; + auto shader = MakeShader(address, coherent, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) { + EmitBdaStore(ctx, instruction, base, ConstantU32(ctx.state, value), bits); + return std::vector{ConstantU32(ctx.state, 1u)}; + }, 0u, true); +#if ANYPS5_ENABLE_SPIRV_TOOLS + shader = ValidateAndOptimizeSpirv(shader, VK_API_VERSION_1_1, shader[1], false, false); +#endif + return shader; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 27c99edba..abfefe977 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -9,6 +9,8 @@ std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_ std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops); std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); +std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index e1379137b..3ab9d30d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,11 @@ struct MockDevice { VkDeviceSize liveBytes = 0; std::map memoryBytes; std::map> bound; + std::set liveBuffers; + std::deque results; + VkDeviceSize limit = 0; + VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY; + std::uint64_t attempts = 0; }; MockDevice mock; @@ -45,6 +51,7 @@ VKAPI_ATTR VkResult VKAPI_CALL mockCreateBuffer(VkDevice, const VkBufferCreateIn *buffer = reinterpret_cast(static_cast(mock.next++)); mock.sizes[*buffer] = info->size; mock.usages[*buffer] = info->usage; + mock.liveBuffers.insert(*buffer); return VK_SUCCESS; } @@ -53,6 +60,17 @@ VKAPI_ATTR void VKAPI_CALL mockGetBufferMemoryRequirements(VkDevice, VkBuffer bu } VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* memory) { + ++mock.attempts; + auto result = VK_SUCCESS; + if (!mock.results.empty()) { + result = mock.results.front(); + mock.results.pop_front(); + } + if (result == VK_SUCCESS && mock.limit != 0 && mock.liveBytes + info->allocationSize > mock.limit) result = mock.pressureResult; + if (result != VK_SUCCESS) { + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return result; + } *memory = reinterpret_cast(static_cast(mock.next++)); if (info->memoryTypeIndex == 1) mock.hostMemory[*memory].resize(info->allocationSize); mock.memoryBytes[*memory] = info->allocationSize; @@ -74,11 +92,16 @@ VKAPI_ATTR VkResult VKAPI_CALL mockMapMemory(VkDevice, VkDeviceMemory memory, Vk VKAPI_ATTR void VKAPI_CALL mockUnmapMemory(VkDevice, VkDeviceMemory) {} -VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer, const VkAllocationCallbacks*) { +VKAPI_ATTR void VKAPI_CALL mockDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { + Expect(mock.liveBuffers.erase(buffer) == 1, "an unknown buffer was destroyed"); ++mock.destroyedBuffers; } VKAPI_ATTR void VKAPI_CALL mockFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { + if (!mock.memoryBytes.contains(memory)) { + Expect(false, "an undefined failed allocation output was freed"); + return; + } mock.liveBytes -= mock.memoryBytes.at(memory); mock.memoryBytes.erase(memory); mock.hostMemory.erase(memory); @@ -319,6 +342,149 @@ void Slabs() { Expect(mock.liveBytes == 0, "slab blocks outlived their pool"); } +void AllocationRecovery(VkResult pressureResult, bool host) { + mock = MockDevice{}; + { + auto context = mockContext(); + const auto make = [&](std::size_t bytes) -> std::shared_ptr { + if (host) return std::make_shared(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + return std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + }; + auto active = make(2 * MiB); + const auto activeBuffer = *mock.liveBuffers.begin(); + const auto activeMemory = mock.memoryBytes.begin()->first; + { auto idle = make(2 * MiB); } + mock.limit = 6 * MiB; + mock.pressureResult = pressureResult; + const auto before = mock.attempts; + try { + auto recovered = make(4 * MiB); + Expect(mock.attempts == before + 2, "pool pressure did not trigger exactly one allocation retry"); + Expect(mock.frees == 1 && mock.liveBytes == 6 * MiB, "recovery did not release the idle allocation"); + Expect(mock.liveBuffers.contains(activeBuffer) && mock.memoryBytes.contains(activeMemory), "recovery destroyed an active allocation"); + } catch (const std::exception& error) { + Expect(false, std::string("idle memory did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "recovery leaked a resource"); +} + +void SlabAllocationRecovery() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "a new slab block did not retry after reclaiming an idle allocation"); + Expect(context.bufferPool->SlabBlocks() == 2 && mock.memoryBytes.contains(activeMemory), "slab allocation recovery discarded its live block"); + } catch (const std::exception& error) { + Expect(false, std::string("slab allocation did not recover: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab recovery leaked a resource"); +} + +void ReclaimSlabs() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + const auto activeMemory = mock.bound.at(active.Handle()).first; + std::memset(active.Bytes().data(), 0x5a, active.Bytes().size()); + { + Buffer sameBlock(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer otherBlock(context, 600, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } + mock.limit = 6 * MiB; + const auto before = mock.attempts; + try { + Buffer recovered(context, 4 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Expect(mock.attempts == before + 2 && mock.frees == 1, "reclaim did not free the unused slab block"); + Expect(context.bufferPool->SlabBlocks() == 1 && mock.memoryBytes.contains(activeMemory), "reclaim freed a slab with a live slot"); + Expect(std::all_of(active.Bytes().begin(), active.Bytes().end(), [](std::byte value) { return value == std::byte{0x5a}; }), "reclaim changed a live slab buffer"); + } catch (const std::exception& error) { + Expect(false, std::string("empty slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "slab reclaim leaked a resource"); + mock = MockDevice{}; + { + auto context = mockContext(); + BufferPool pool(context); + const auto idle = pool.TakeSlot(context, 1, 512 * 1024, false); + pool.PutSlot(idle.memory, idle.offset); + mock.limit = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = pool.TakeSlot(context, 1, 256 * 1024, false); + Expect(mock.attempts == before + 2 && mock.frees == 1 && pool.SlabBlocks() == 1, "a spare slab without cached buffers was not reclaimed"); + pool.PutSlot(recovered.memory, recovered.offset); + } catch (const std::exception& error) { + Expect(false, std::string("spare slab did not permit recovery: ") + error.what()); + } + } + Expect(mock.liveBytes == 0, "spare slab recovery leaked device memory"); +} + +void AllocationFailure(VkResult initial, VkResult final, bool cached) { + mock = MockDevice{}; + { + auto context = mockContext(); + if (cached) { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.results = {initial, final}; + const auto before = mock.attempts; + std::string failure; + try { + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + const bool reclaimed = cached && (initial == VK_ERROR_OUT_OF_DEVICE_MEMORY || initial == VK_ERROR_OUT_OF_HOST_MEMORY); + Expect(mock.attempts == before + (reclaimed ? 2 : 1), "allocation retries were not bounded or a non-memory failure was retried"); + Expect(failure.find("Vulkan result " + std::to_string(reclaimed ? final : initial)) != std::string::npos, "allocation failure lost its final Vulkan result"); + Expect(mock.liveBuffers.size() == (cached && !reclaimed ? 1u : 0u), "a failed allocation retained its buffer"); + Expect(mock.liveBytes == (cached && !reclaimed ? 2 * MiB : 0), "a failed allocation mishandled cached memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource"); +} + +void SuccessfulAllocationKeepsPool() { + mock = MockDevice{}; + { + auto context = mockContext(); + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(mock.attempts == 2 && mock.frees == 0 && mock.liveBytes == 6 * MiB, "a successful first allocation reclaimed its cache"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "normal allocation leaked a resource"); +} + +void PartialSlabDoesNotRetry() { + mock = MockDevice{}; + { + auto context = mockContext(); + Buffer active(context, 300, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + { Buffer idle(context, 260, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + mock.limit = 2 * MiB; + const auto before = mock.attempts; + std::string failure; + try { + Buffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + } catch (const std::exception& error) { + failure = error.what(); + } + Expect(failure.find("Vulkan result -2") != std::string::npos, "a live slab's memory was treated as reclaimable"); + Expect(mock.attempts == before + 1 && mock.frees == 0, "an allocation retried without freeing device memory"); + Expect(mock.liveBuffers.size() == 1 && mock.liveBuffers.contains(active.Handle()) && context.bufferPool->SlabBlocks() == 1, "reclaim changed a live slab allocation"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource"); +} + } int main() { @@ -330,6 +496,18 @@ int main() { AddressAndHostUnchanged(); SmallDeviceClasses(); Slabs(); + for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) { + AllocationRecovery(result, false); + AllocationRecovery(result, true); + AllocationFailure(result, result, true); + AllocationFailure(result, result, false); + } + SlabAllocationRecovery(); + ReclaimSlabs(); + AllocationFailure(VK_ERROR_DEVICE_LOST, VK_ERROR_DEVICE_LOST, true); + AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); + SuccessfulAllocationKeepsPool(); + PartialSlabDoesNotRetry(); } catch (const std::exception& error) { std::fprintf(stderr, "FAIL: %s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp new file mode 100644 index 000000000..5324f2e43 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCapture.cpp @@ -0,0 +1,172 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +template +void Reject(TAction action, const char* message) { + bool rejected = false; + try { + action(); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, message); +} + +class Directory { +public: + Directory() { + path = std::filesystem::temp_directory_path() / ("anyps5-compute-capture-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(path), "could not create test directory"); + } + ~Directory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + std::filesystem::path path; +}; + +void CheckRoundTrip() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.deviceName = "test GPU \"name\""; + capture.vendorId = 0x10de; + capture.deviceId = 0x2b85; + capture.driverVersion = 0x12345678; + capture.apiVersion = VK_API_VERSION_1_3; + capture.pipelineCacheUuid[0] = 0x12; + capture.pipelineCacheUuid[15] = 0xab; + capture.requiredSubgroupSize = 32; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + capture.sets = {{VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT, + {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 5, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}, + {7, VK_DESCRIPTOR_TYPE_SAMPLER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 32}}; + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + Require(decoded.Encode() == encoded, "capture round trip changed pipeline inputs"); + Require(decoded.Identity() == capture.Identity(), "capture identity changed after decoding"); + auto changed = decoded; + ++changed.specialization[0].value; + Require(changed.Identity() != capture.Identity(), "specialization did not affect capture identity"); + changed = decoded; + ++changed.sets[0].bindings[0].count; + Require(changed.Identity() != capture.Identity(), "descriptor layout did not affect capture identity"); + changed = decoded; + changed.requiredSubgroupSize = 64; + Require(changed.Identity() != capture.Identity(), "subgroup size did not affect capture identity"); + Directory directory; + const auto file = capture.Write(directory.path); + Require(AgcDriver::ComputePipelineCapture::Read(file).Encode() == encoded, "capture file changed pipeline inputs"); + Require(capture.Write(directory.path) == file, "identical capture was not deduplicated"); + std::ofstream(file, std::ios::binary | std::ios::trunc).put('x'); + Reject([&] { capture.Write(directory.path); }, "existing damaged capture was overwritten"); + Require(std::filesystem::file_size(file) == 1, "damaged capture was changed"); +} + +void CheckLargeRecord() { + auto capture = AgcDriver::ComputePipelineFixture(); + constexpr std::size_t words = 5500000; + capture.spirv.insert(capture.spirv.end() - 2, words - capture.spirv.size(), 0x00010000u); + const auto encoded = capture.Encode(); + Require(encoded.size() > 20u * 1024u * 1024u, "large fixture did not exceed the former file bound"); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).spirv == capture.spirv, "large module changed during decoding"); + Directory directory; + const auto file = capture.Write(directory.path); + Require(AgcDriver::ComputePipelineCapture::Read(file).spirv == capture.spirv, "large module changed on disk"); + capture.spirv.resize(AgcDriver::ComputePipelineCapture::MaxSpirvWords + 1u, 0x00010000u); + Reject([&] { capture.Encode(); }, "module beyond the word bound accepted"); + std::vector oversized(AgcDriver::ComputePipelineCapture::MaxFileBytes + 1u); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(oversized); }, "record beyond the file bound accepted"); + const auto oversizedFile = directory.path / "oversized.aps5cp"; + { + std::ofstream output(oversizedFile, std::ios::binary); + output.seekp(static_cast(AgcDriver::ComputePipelineCapture::MaxFileBytes)); + output.put('x'); + Require(output.good(), "cannot write oversized capture fixture"); + } + Reject([&] { AgcDriver::ComputePipelineCapture::Read(oversizedFile); }, "file beyond the file bound accepted"); +} + +void CheckBindingFlags() { + auto capture = AgcDriver::ComputePipelineFixture(); + capture.sets = {{0, {{7, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 4, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT}, + {3, VK_DESCRIPTOR_TYPE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT}}}}; + const auto encoded = capture.Encode(); + Require(AgcDriver::ComputePipelineCapture::Decode(encoded).Encode() == encoded, "binding flags changed without update-after-bind"); + auto changed = capture; + changed.sets[0].bindings[0].flags = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT; + Require(changed.Identity() != capture.Identity(), "binding flags did not affect capture identity"); + changed = capture; + changed.sets[0].bindings[1].flags |= VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + Reject([&] { changed.Encode(); }, "non-highest variable descriptor binding accepted"); + changed = capture; + changed.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT; + Reject([&] { changed.Encode(); }, "unsupported binding flags accepted"); + capture.sets[0].flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT; + capture.sets[0].bindings[0].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + capture.sets[0].bindings[0].flags |= VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT; + Require(AgcDriver::ComputePipelineCapture::Decode(capture.Encode()).Encode() == capture.Encode(), "combined binding flags changed"); +} + +void CheckMalformedRecords() { + const auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + for (std::size_t length = 0; length < encoded.size(); ++length) { + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(std::span(encoded).first(length)); }, "truncated capture accepted"); + } + auto bytes = encoded; + bytes.push_back(std::byte{0}); + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "trailing capture bytes accepted"); + bytes = encoded; + bytes[0] ^= std::byte{1}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "invalid signature accepted"); + bytes = encoded; + bytes[8] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unknown capture version accepted"); + bytes = encoded; + for (std::size_t index = 104; index < 108; ++index) bytes[index] = std::byte{0xff}; + Reject([&] { AgcDriver::ComputePipelineCapture::Decode(bytes); }, "unbounded device name accepted"); + const auto invalid = [&](auto change) { + auto copy = capture; + change(copy); + Reject([&] { copy.Encode(); }, "unsupported pipeline input accepted"); + }; + invalid([](auto& copy) { copy.spirv[0] = 0; }); + invalid([](auto& copy) { copy.spirv[5] = 0; }); + invalid([](auto& copy) { copy.spirv[11] = 4; }); + invalid([](auto& copy) { copy.spirv[13] = 0; }); + invalid([](auto& copy) { copy.flags = VK_PIPELINE_CREATE_DERIVATIVE_BIT; }); + invalid([](auto& copy) { copy.stageFlags = VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT; }); + invalid([](auto& copy) { copy.requiredSubgroupSize = 3; }); + invalid([](auto& copy) { copy.specialization.push_back(copy.specialization[0]); }); + invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_VERTEX_BIT, 0, 4}}; }); + invalid([](auto& copy) { copy.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 1, 4}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT, 0}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT}}}}; }); + invalid([](auto& copy) { copy.sets = {{0, {{}, {}}}}; }); +} + +} + +int main() { + try { + CheckRoundTrip(); + CheckMalformedRecords(); + CheckBindingFlags(); + CheckLargeRecord(); + std::puts("compute pipeline capture: round trip, 5500000-word module, binding flags, immutable writes and bounded inputs passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline capture test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake new file mode 100644 index 000000000..fb1500757 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineCaptureRun.cmake @@ -0,0 +1,68 @@ +foreach(required IN ITEMS FIXTURE REPLAY OUTPUT_ROOT) + if(NOT DEFINED ${required}) + message(FATAL_ERROR "Missing ${required}") + endif() +endforeach() +string(RANDOM LENGTH 16 ALPHABET 0123456789abcdef runId) +set(runDirectory "${OUTPUT_ROOT}/compute-capture-${runId}") +set(captureDirectory "${runDirectory}/captures") +file(MAKE_DIRECTORY "${runDirectory}") +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/normal.log" "${output}\n${error}") +if(result STREQUAL "77" AND NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN}) + message(STATUS "compute pipeline capture run: skipped, no usable Vulkan device") + file(REMOVE_RECURSE "${runDirectory}") + return() +endif() +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Normal compute fixture failed: ${result}\n${output}\n${error}") +endif() +execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "ANYPS5_REQUIRE_VULKAN=1" "ANYPS5_NO_SHADER_CACHE=1" "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=${captureDirectory}" "APS5_DUMP_COMPUTE_MIN_WORDS=0" "${FIXTURE}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 +) +file(WRITE "${runDirectory}/capture.log" "${output}\n${error}") +if(NOT result STREQUAL "0" OR NOT output MATCHES "branch past endpgm tests passed") + message(FATAL_ERROR "Captured compute fixture failed: ${result}\n${output}\n${error}") +endif() +file(GLOB captures "${captureDirectory}/compute-*/capture.aps5cp") +list(LENGTH captures count) +if(count LESS 1 OR count GREATER 12) + message(FATAL_ERROR "Expected 1 to 12 compute captures, found ${count}") +endif() +foreach(capture IN LISTS captures) + get_filename_component(recordDirectory "${capture}" DIRECTORY) + get_filename_component(recordName "${recordDirectory}" NAME) + string(REPLACE "compute-" "" identity "${recordName}") + set(report "${runDirectory}/${recordName}.json") + execute_process( + COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "APS5_DUMP_COMPUTE_PIPELINES=" "${REPLAY}" "${capture}" --output "${report}" + WORKING_DIRECTORY "${runDirectory}" + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20 + ) + file(WRITE "${runDirectory}/${recordName}.log" "${output}\n${error}") + if(NOT result STREQUAL "0") + message(FATAL_ERROR "Captured pipeline replay failed: ${result}\n${output}\n${error}") + endif() + file(READ "${report}" reportJson) + string(JSON reportIdentity GET "${reportJson}" capture_identity) + string(JSON words GET "${reportJson}" spirv_words) + string(JSON sets GET "${reportJson}" descriptor_sets) + string(JSON ranges GET "${reportJson}" push_constant_ranges) + string(JSON dispatches GET "${reportJson}" dispatch_count) + string(JSON policy GET "${reportJson}" application_cache_policy) + string(JSON capturedDevice GET "${reportJson}" captured_device device_id) + string(JSON replayDevice GET "${reportJson}" replay_device device_id) + string(JSON capturedDriver GET "${reportJson}" captured_device driver_version_raw) + string(JSON replayDriver GET "${reportJson}" replay_device driver_version_raw) + if(NOT reportIdentity STREQUAL identity OR words LESS 54 OR sets LESS 1 OR ranges LESS 1 OR NOT dispatches EQUAL 0 OR NOT policy STREQUAL "fresh" OR NOT capturedDevice STREQUAL replayDevice OR NOT capturedDriver STREQUAL replayDriver) + message(FATAL_ERROR "Captured pipeline metadata mismatch: ${reportJson}") + endif() +endforeach() +message(STATUS "Normal and captured compute output verified; ${count} captured pipelines replayed without dispatch") +file(REMOVE_RECURSE "${runDirectory}") diff --git a/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp new file mode 100644 index 000000000..3e1f17936 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp @@ -0,0 +1,122 @@ +#include "../tools/ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckResult(const AgcDriver::ComputePipelineReplayResult& result) { + Require(!result.deviceName.empty() && result.vendorId != 0 && result.apiVersion != 0, "replay did not report its device"); + Require(result.cache.size() >= 32, "replay did not export a pipeline cache header"); + for (const auto duration : {result.moduleMilliseconds, result.layoutMilliseconds, result.pipelineMilliseconds, result.cacheMilliseconds, result.totalMilliseconds}) { + Require(std::isfinite(duration) && duration >= 0, "replay reported an invalid duration"); + } + Require(result.totalMilliseconds >= result.pipelineMilliseconds, "pipeline duration exceeds replay duration"); +} + +} + +int main() { + try { + auto capture = AgcDriver::ComputePipelineFixture(); + const auto encoded = capture.Encode(); + const auto decoded = AgcDriver::ComputePipelineCapture::Decode(encoded); + std::unique_ptr device; + try { + device = std::make_unique(AgcDriver::VulkanDevice::CompileOnly{}); + } catch (const std::exception& error) { + const std::string_view message(error.what()); + const bool absent = (message.starts_with("Vulkan loader: ") && message != "Vulkan loader: vkGetInstanceProcAddr missing") || message == "Vulkan: no Vulkan 1.1 graphics and compute queue" || message == "vkCreateInstance: Vulkan result -9"; + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || !absent) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return 77; + } + auto sampled1D = decoded; + sampled1D.spirv.insert(sampled1D.spirv.begin() + 7, {0x00020011u, 43u}); + CheckResult(device->ReplayComputePipeline(sampled1D)); + std::puts("compute pipeline replay: core Vulkan Sampled1D capability compiled"); + for (const auto capability : {6u, 0xffffffffu}) { + auto unsupported = decoded; + unsupported.spirv.insert(unsupported.spirv.begin() + 7, {0x00020011u, capability}); + bool refused = false; + try { + device->ReplayComputePipeline(unsupported); + } catch (const std::exception& error) { + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "unsupported capability failed outside the replay capability guard"); + refused = true; + } + Require(refused, "non-Vulkan or unknown SPIR-V capability accepted"); + } + std::puts("compute pipeline replay: non-Vulkan Kernel and unknown capabilities rejected"); + const auto fresh = device->ReplayComputePipeline(decoded); + CheckResult(fresh); + Require(fresh.initialCacheBytes == 0, "fresh replay imported cache bytes"); + const auto imported = device->ReplayComputePipeline(decoded, fresh.cache); + CheckResult(imported); + Require(imported.initialCacheBytes == fresh.cache.size(), "replay did not import the supplied cache"); + Require(fresh.deviceId == imported.deviceId && fresh.driverVersion == imported.driverVersion && fresh.pipelineCacheUuid == imported.pipelineCacheUuid, "replay device identity changed"); + Require(capture.Encode() == encoded, "replay modified the captured pipeline"); + auto corrupt = fresh.cache; + corrupt[8] ^= std::byte{0xff}; + bool rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache from another vendor was accepted"); + corrupt = fresh.cache; + corrupt[16] ^= std::byte{0xff}; + rejected = false; + try { + device->ReplayComputePipeline(decoded, corrupt); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "cache with another pipeline UUID was accepted"); + for (const auto flags : {VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT, static_cast(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT)}) { + auto indexed = decoded; + indexed.sets[0].bindings[0].flags = flags; + try { + CheckResult(device->ReplayComputePipeline(indexed)); + std::printf("compute pipeline replay: descriptor binding flags 0x%x compiled without update-after-bind\n", static_cast(flags)); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + const std::string_view message(error.what()); + Require(message == "Vulkan presentation: captured partially-bound descriptors are unsupported" || message == "Vulkan presentation: captured variable descriptor count is unsupported", "binding flag replay failed without an explicit feature refusal"); + std::printf("compute pipeline replay: descriptor binding flags 0x%x explicitly refused: %s\n", static_cast(flags), error.what()); + } + } + auto runtimeArray = decoded; + runtimeArray.spirv.insert(runtimeArray.spirv.begin() + 7, {0x00020011u, 5302u, 0x0008000au, 0x5f565053u, 0x5f545845u, 0x63736564u, 0x74706972u, 0x695f726fu, 0x7865646eu, 0x00676e69u}); + try { + CheckResult(device->ReplayComputePipeline(runtimeArray)); + std::puts("compute pipeline replay: runtime descriptor array capability compiled"); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS") != nullptr) throw; + Require(std::string_view(error.what()) == "Vulkan presentation: captured SPIR-V capability is not enabled", "runtime descriptor array replay failed without an explicit feature refusal"); + std::puts("compute pipeline replay: device explicitly refused runtime descriptor array capability"); + } + capture.requiredSubgroupSize = 32; + try { + const auto subgroup = device->ReplayComputePipeline(capture); + CheckResult(subgroup); + std::puts("compute pipeline replay: required subgroup size 32 compiled"); + } catch (const std::exception& error) { + Require(std::string_view(error.what()).find("captured required subgroup size is unsupported") != std::string_view::npos, "required subgroup replay failed without an explicit support refusal"); + std::puts("compute pipeline replay: device explicitly refused required subgroup size 32"); + } + std::printf("compute pipeline replay: fresh %.6f ms, imported %.6f ms, GPU %s; no dispatch\n", fresh.pipelineMilliseconds, imported.pipelineMilliseconds, fresh.deviceName.c_str()); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "compute pipeline replay test: %s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index e99c97bce..030af961d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -295,6 +295,97 @@ std::size_t recompile(const std::string& name, std::span co return result.spirv.size(); } +void verifyLargeLoopExitFallback() { + const std::vector code{ + 0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f000000u, 0x060202ffu, 0x3f001000u, 0x060202ffu, 0x3f002000u, + 0x060202ffu, 0x3f003000u, 0x060202ffu, 0x3f004000u, 0x060202ffu, 0x3f005000u, 0x060202ffu, 0x3f006000u, + 0x060202ffu, 0x3f007000u, 0x060202ffu, 0x3f008000u, 0x060202ffu, 0x3f009000u, 0x060202ffu, 0x3f00a000u, + 0x060202ffu, 0x3f00b000u, 0x060202ffu, 0x3f00c000u, 0x060202ffu, 0x3f00d000u, 0x060202ffu, 0x3f00e000u, + 0x060202ffu, 0x3f00f000u, 0x060202ffu, 0x3f010000u, 0x060202ffu, 0x3f810000u, 0x060202ffu, 0x3f000010u, + 0x060202ffu, 0x3f001010u, 0x060202ffu, 0x3f002010u, 0x060202ffu, 0x3f003010u, 0x060202ffu, 0x3f004010u, + 0x060202ffu, 0x3f005010u, 0x060202ffu, 0x3f006010u, 0x060202ffu, 0x3f007010u, 0x060202ffu, 0x3f008010u, + 0x060202ffu, 0x3f009010u, 0x060202ffu, 0x3f00a010u, 0x060202ffu, 0x3f00b010u, 0x060202ffu, 0x3f00c010u, + 0x060202ffu, 0x3f00d010u, 0x060202ffu, 0x3f00e010u, 0x060202ffu, 0x3f00f010u, 0x060202ffu, 0x3f010010u, + 0x7d880084u, 0xbf870025u, 0x060202ffu, 0x3f830000u, 0x060202ffu, 0x3f000030u, 0x060202ffu, 0x3f001030u, + 0x060202ffu, 0x3f002030u, 0x060202ffu, 0x3f003030u, 0x060202ffu, 0x3f004030u, 0x060202ffu, 0x3f005030u, + 0x060202ffu, 0x3f006030u, 0x060202ffu, 0x3f007030u, 0x060202ffu, 0x3f008030u, 0x060202ffu, 0x3f009030u, + 0x060202ffu, 0x3f00a030u, 0x060202ffu, 0x3f00b030u, 0x060202ffu, 0x3f00c030u, 0x060202ffu, 0x3f00d030u, + 0x060202ffu, 0x3f00e030u, 0x060202ffu, 0x3f00f030u, 0x060202ffu, 0x3f010030u, 0xbf820099u, 0x060202ffu, + 0x3f820000u, 0x060202ffu, 0x3f000020u, 0x060202ffu, 0x3f001020u, 0x060202ffu, 0x3f002020u, 0x060202ffu, + 0x3f003020u, 0x060202ffu, 0x3f004020u, 0x060202ffu, 0x3f005020u, 0x060202ffu, 0x3f006020u, 0x060202ffu, + 0x3f007020u, 0x060202ffu, 0x3f008020u, 0x060202ffu, 0x3f009020u, 0x060202ffu, 0x3f00a020u, 0x060202ffu, + 0x3f00b020u, 0x060202ffu, 0x3f00c020u, 0x060202ffu, 0x3f00d020u, 0x060202ffu, 0x3f00e020u, 0x060202ffu, + 0x3f00f020u, 0x060202ffu, 0x3f010020u, 0x7d880085u, 0xbf87004cu, 0x060202ffu, 0x3f850000u, 0x060202ffu, + 0x3f000050u, 0x060202ffu, 0x3f001050u, 0x060202ffu, 0x3f002050u, 0x060202ffu, 0x3f003050u, 0x060202ffu, + 0x3f004050u, 0x060202ffu, 0x3f005050u, 0x060202ffu, 0x3f006050u, 0x060202ffu, 0x3f007050u, 0x060202ffu, + 0x3f008050u, 0x060202ffu, 0x3f009050u, 0x060202ffu, 0x3f00a050u, 0x060202ffu, 0x3f00b050u, 0x060202ffu, + 0x3f00c050u, 0x060202ffu, 0x3f00d050u, 0x060202ffu, 0x3f00e050u, 0x060202ffu, 0x3f00f050u, 0x060202ffu, + 0x3f010050u, 0x7d880088u, 0xbf870001u, 0xbf82ff68u, 0x060202ffu, 0x3f870000u, 0x060202ffu, 0x3f000070u, + 0x060202ffu, 0x3f001070u, 0x060202ffu, 0x3f002070u, 0x060202ffu, 0x3f003070u, 0x060202ffu, 0x3f004070u, + 0x060202ffu, 0x3f005070u, 0x060202ffu, 0x3f006070u, 0x060202ffu, 0x3f007070u, 0x060202ffu, 0x3f008070u, + 0x060202ffu, 0x3f009070u, 0x060202ffu, 0x3f00a070u, 0x060202ffu, 0x3f00b070u, 0x060202ffu, 0x3f00c070u, + 0x060202ffu, 0x3f00d070u, 0x060202ffu, 0x3f00e070u, 0x060202ffu, 0x3f00f070u, 0x060202ffu, 0x3f010070u, + 0xbf820027u, 0x060202ffu, 0x3f840000u, 0x060202ffu, 0x3f000040u, 0x060202ffu, 0x3f001040u, 0x060202ffu, + 0x3f002040u, 0x060202ffu, 0x3f003040u, 0x060202ffu, 0x3f004040u, 0x060202ffu, 0x3f005040u, 0x060202ffu, + 0x3f006040u, 0x060202ffu, 0x3f007040u, 0x060202ffu, 0x3f008040u, 0x060202ffu, 0x3f009040u, 0x060202ffu, + 0x3f00a040u, 0x060202ffu, 0x3f00b040u, 0x060202ffu, 0x3f00c040u, 0x060202ffu, 0x3f00d040u, 0x060202ffu, + 0x3f00e040u, 0x060202ffu, 0x3f00f040u, 0x060202ffu, 0x3f010040u, 0x7d880087u, 0xbf87ff43u, 0xbf82ff8du, + 0x060202ffu, 0x3f860000u, 0x060202ffu, 0x3f000060u, 0x060202ffu, 0x3f001060u, 0x060202ffu, 0x3f002060u, + 0x060202ffu, 0x3f003060u, 0x060202ffu, 0x3f004060u, 0x060202ffu, 0x3f005060u, 0x060202ffu, 0x3f006060u, + 0x060202ffu, 0x3f007060u, 0x060202ffu, 0x3f008060u, 0x060202ffu, 0x3f009060u, 0x060202ffu, 0x3f00a060u, + 0x060202ffu, 0x3f00b060u, 0x060202ffu, 0x3f00c060u, 0x060202ffu, 0x3f00d060u, 0x060202ffu, 0x3f00e060u, + 0x060202ffu, 0x3f00f060u, 0x060202ffu, 0x3f010060u, 0xe0700000u, 0x80010100u, 0xbf810000u, + }; + const std::string name("large loop-exit shader preserves the original guarded fallback"); + static_cast(verifyGraph(name, code)); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + +void verifyNestedExitForwarders() { + constexpr auto none = InvalidControlFlowId; + constexpr std::array, 21> targets{{ + {1, none}, {2, none}, {2, 3}, {2, 4}, {5, 6}, {1, 7}, {8, 9}, {10, none}, + {11, 12}, {13, 14}, {1, 15}, {16, none}, {9, none}, {17, 18}, {19, none}, + {16, none}, {none, none}, {16, none}, {15, 20}, {10, none}, {19, none}, + }}; + constexpr std::array order{0, 1, 2, 3, 4, 6, 9, 14, 19, 10, 15, 13, 18, 20, 17, 8, 12, 11, 5, 7, 16}; + std::vector code; + std::array starts{}; + std::vector> patches; + for (std::size_t at = 0; at < order.size(); ++at) { + const auto block = order[at]; + starts[block] = code.size(); + code.insert(code.end(), {0x060202ffu, 0x3f800000u + block * 0x10000u}); + for (std::uint32_t padding = 0; padding < 17; ++padding) code.insert(code.end(), {0x060202ffu, 0x3f000000u + padding * 0x1000u + block * 0x10u}); + const auto next = at + 1 < order.size() ? order[at + 1] : none; + const auto [first, second] = targets[block]; + if (first == none) { + code.insert(code.end(), {0xe0700000u, 0x80010100u, 0xbf810000u}); + continue; + } + if (second != none) { + code.push_back(0x7d880080u + block + 3u); + patches.emplace_back(code.size(), first); + code.push_back(0xbf870000u); + } + const auto branch = second != none ? second : first; + if (branch != next) { + patches.emplace_back(code.size(), branch); + code.push_back(0xbf820000u); + } + } + for (const auto& [instruction, target] : patches) { + const auto displacement = static_cast(starts[target]) - static_cast(instruction) - 1; + require(displacement >= -32768 && displacement <= 32767, "nested exit forwarder test branch is out of range"); + code[instruction] |= static_cast(displacement) & 0xffffu; + } + const std::string name("nested exit forwarders keep conditional selections structured"); + static_cast(verifyGraph(name, code)); + static_cast(recompile(name, code, 32u)); + static_cast(recompile(name, code, 64u)); +} + void verifyProgram(const Program& program) { const std::string name(program.name); const auto result = verifyGraph(name, program.code); @@ -409,6 +500,38 @@ int main(int argc, char** argv) { return failures == 0 ? 0 : 1; } const std::vector programs{ + {"nested selections share a loop continuation", "Synthetic loop with two nested selections and a shared continue; the inner selection does not dominate the continuation.", + {0x060202ffu, 0x3f800000u, 0x060202ffu, 0x3f810000u, 0x7d880084u, 0xbf870001u, 0xbf820018u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf87000cu, 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870003u, 0x060202ffu, 0x3f870000u, 0xbf82fff0u, 0x060202ffu, 0x3f860000u, 0x7d880089u, 0xbf87fff9u, 0xbf820008u, 0x060202ffu, 0x3f830000u, 0x7d880086u, 0xbf870001u, 0xbf820003u, 0x060202ffu, 0x3f840000u, 0xbf82fff0u, 0x060202ffu, 0x3f880000u, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None}, + {"reduced shared exit junction from the public CFG fuzz report", "Public reduced word stream from #2762; synthetic regression, not title data.", + {0x060202f2u, 0x060202f4u, 0x7d880084u, 0xbf870001u, 0xbf820011u, 0x060202ffu, 0x3f820000u, 0x7d880085u, 0xbf870005u, + 0x060202ffu, 0x3f850000u, 0x7d880088u, 0xbf870009u, 0xbf82fff3u, 0x060202ffu, 0x3f840000u, 0x7d880087u, 0xbf870001u, + 0xbf820003u, 0x060202ffu, 0x3f860000u, 0xbf82ffebu, 0x060202f6u, 0xe0700000u, 0x80010100u, 0xbf810000u}, Split::None}, + {"both selection arms leave through a shared loop exit tail", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +loop: + s_cmp_lt_u32 s8, 4 + s_cbranch_scc0 done + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 right + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 1, v1 + s_branch latch +right: + v_cmp_gt_u32 vcc, 16, v0 + s_cbranch_vccz tail + v_add_nc_u32 v1, 2, v1 +latch: + s_add_u32 s8, s8, 1 + s_branch loop +tail: + v_add_nc_u32 v1, 3, v1 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0a8408u, 0xbf84000cu, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860007u, 0x4a020281u, + 0xbf820003u, 0x7d880090u, 0xbf860003u, 0x4a020282u, 0x80088108u, 0xbf82fff3u, 0x4a020283u}), Split::None}, {"shared tail", R"( v_cmp_gt_u32 vcc, 16, v0 s_cbranch_vccz outer_else @@ -520,6 +643,69 @@ done: buffer_store_dword v1, off, s[0:3], 0 s_endpgm)", Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None}, + {"nested selections in a loop whose second latch continues from inside them", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 + s_branch loop +again: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 +loop: + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 latch + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_cmp_gt_u32 vcc, 4, v0 + s_cbranch_vccnz again +latch: + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 loop + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0xbe880380u, 0x7e020280u, 0xbf820002u, 0x4a020282u, 0x80089008u, 0xbf060208u, 0xbf850004u, 0x7d880088u, 0xbf860002u, 0x7d880084u, 0xbf87fff8u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fff6u}), Split::None}, + {"an inner loop that ends the program or continues its outer loop", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + v_add_nc_u32 v1, 2, v1 +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz done + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc1 inner + s_mov_b32 s8, 0 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0x4a020282u, 0x7d880088u, 0xbf860006u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf85fffau, 0xbe880380u, 0xbf82fff7u}), Split::Route, 0}, + {"an inner loop whose body leaves both loops", R"( + v_mov_b32 v1, 0 + s_mov_b32 s8, 0 +outer: + s_cmp_lt_u32 s8, 64 + s_cbranch_scc0 done +inner: + v_cmp_gt_u32 vcc, 8, v0 + s_cbranch_vccz latch + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_eq_u32 s8, s2 + s_cbranch_scc1 done + s_branch inner +latch: + v_add_nc_u32 v1, 2, v1 + s_add_u32 s8, s8, 16 + s_branch outer +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + Store({0x7e020280u, 0xbe880380u, 0xbf0ac008u, 0xbf84000au, 0x7d880088u, 0xbf860005u, 0x4a020281u, 0x80089008u, 0xbf060208u, 0xbf850004u, + 0xbf82fff9u, 0x4a020282u, 0x80089008u, 0xbf82fff4u}), Split::Route, 0}, {"loop exit to the end of the program beside a kill exit", R"( v_mov_b32 v1, 0 s_mov_b64 s[20:21], exec @@ -1060,6 +1246,8 @@ done: } try { verifyNullSwappc(); + verifyLargeLoopExitFallback(); + verifyNestedExitForwarders(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); ++failures; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..0103b5aec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7,12 +7,15 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" #include "Optimization/ResourceProgram.hpp" @@ -275,6 +278,29 @@ void stateTests() { queue.context[0x200] = 0x007007bbu; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "depth bounds without a depth plane"); queue = makeState(); + queue.context[0x010] = 0x80000183; + queue.context[0x011] = 0x20000180; + queue.context[0x012] = 0x00001000; + queue.context[0x014] = 0x00001000; + queue.context[0x007] = 0x0001001f; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0x3f800000; + queue.context[0x200] = 0x007007b1; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x00700231; + state = AgcDriver::Graphics::DecodeState(queue); + Require(!state.depth.has_value() && !state.stencilTest && state.renderExtent.width == state.color.extent.width && state.renderExtent.height == state.color.extent.height, "a failing stencil test without a stencil plane bound the depth plane it does not test"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a failing stencil test without a stencil plane beside an untested depth plane was rejected"); + queue.context[0x200] = 0x007007b3; + state = AgcDriver::Graphics::DecodeState(queue); + Require(state.depth.has_value() && state.depthTest && !state.stencilTest && state.renderExtent.width == 32 && state.renderExtent.height == 2, "a depth test on a bound depth plane lost its surface"); + queue = makeState(); queue.context[0x10f] = 0x7fc00000; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite"); // The register facade: every register the decoders read is in DrawKeyRegisters (a wrong hit of @@ -723,6 +749,43 @@ void ColorPipeBankXorTests() { } } +void HiddenUserWordVertexTablesTests() { + constexpr std::size_t userDataOffset = 288; + constexpr std::size_t directOffset = 352; + constexpr std::size_t semanticOffset = 384; + static std::vector header(semanticOffset + sizeof(ShaderSemantic)); + static std::array bufferTable{0x1000u, 0x20000u, 64u, 0x3000u}; + static std::array attribTable{(0x2cu << 5u)}; + std::array direct{}; + direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4; + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6; + std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct)); + ShaderSemantic semantic{}; + semantic.size_in_elements = 1; + std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic)); + ShaderUserData userData{}; + userData.direct_resource_offset = reinterpret_cast(header.data() + directOffset); + userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; + std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData)); + Shader shader{}; + shader.user_data = reinterpret_cast(header.data() + userDataOffset); + shader.input_semantics = reinterpret_cast(header.data() + semanticOffset); + shader.num_input_semantics = 1; + std::memcpy(header.data(), &shader, sizeof(shader)); + const auto headerAddress = reinterpret_cast(header.data()); + const auto low = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; + const auto high = [](const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); }; + const std::array registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())}; + std::vector merged(8, 0u); + merged.insert(merged.end(), registers.begin(), registers.end()); + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8); + Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words"); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table"); + const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0); + Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved"); +} + void HeaderlessVertexStageTests() { const std::array userData{1, 2, 3, 4}; const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData); @@ -731,6 +794,28 @@ void HeaderlessVertexStageTests() { expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(truncated, 0x1000, userData)); }, "smaller than the fixed AGC header"); } +void UnexportedPixelInputTests() { + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.interpolatorCount = 5; + pixel.interpolatorSettings[0] = 0x000u; + pixel.interpolatorSettings[1] = 0x401u; + pixel.interpolatorSettings[2] = 0x302u; + pixel.interpolatorSettings[3] = 0x123u; + pixel.interpolatorSettings[4] = 0x680004u; + pixel.interpolatorSettings[5] = 0x005u; + const std::array exported{0u}; + const auto adjusted = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, exported); + Require(adjusted.interpolatorSettings[0] == 0x000u, "an input of an exported parameter was changed"); + Require(adjusted.interpolatorSettings[1] == 0x021u, "a flat input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[2] == 0x022u, "an input of an unexported parameter did not read zero"); + Require(adjusted.interpolatorSettings[3] == 0x123u, "an input that already reads its default value was changed"); + Require(adjusted.interpolatorSettings[4] == 0x180024u, "an FP16 input of an unexported parameter did not read zero in both halves"); + Require(adjusted.interpolatorSettings[5] == 0x005u, "a setting past SPI_PS_IN_CONTROL's input count was changed"); + const std::array all{0u, 1u, 2u, 4u, 5u}; + const auto unchanged = AgcDriver::Graphics::DefaultUnexportedInputs(pixel, all); + Require(std::ranges::equal(unchanged.interpolatorSettings, pixel.interpolatorSettings), "inputs of exported parameters were changed"); +} + void PixelInputLayoutTests() { using ShaderRecompiler::PixelInput; using ShaderRecompiler::PixelInputVgpr; @@ -878,6 +963,29 @@ void TuningFieldTests() { } } +void ShadersWriteMemoryTests() { + const auto binding = [](ShaderRecompiler::DescriptorKind kind, ShaderRecompiler::DescriptorRole role, std::vector written) { + ShaderRecompiler::DescriptorBinding result{}; + result.kind = kind; + result.role = role; + result.count = 1; + if (role == ShaderRecompiler::DescriptorRole::GuestImages) result.imageWritten = std::move(written); + else result.bufferWritten = std::move(written); + return result; + }; + ShaderRecompiler::RecompileResult reads; + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {false})); + reads.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageImage, ShaderRecompiler::DescriptorRole::GuestImages, {false})); + ShaderRecompiler::RecompileResult stores; + stores.bindings.push_back(binding(ShaderRecompiler::DescriptorKind::StorageBuffer, ShaderRecompiler::DescriptorRole::GuestBuffers, {true})); + using Stage = ShaderRecompiler::ShaderStage; + const auto writes = [](std::initializer_list shaders) { return AgcDriver::Graphics::ShadersWriteMemory(std::vector(shaders)); }; + Require(!writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &reads, 0}}), "shaders that only read memory were reported as writing it"); + Require(writes({{Stage::Vertex, &reads, 0}, {Stage::Fragment, &stores, 0}}), "a pixel shader that stores to a buffer was not reported"); + Require(writes({{Stage::Vertex, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a vertex shader that stores to a buffer was not reported"); + Require(writes({{Stage::Geometry, &stores, 0}, {Stage::Fragment, &reads, 0}}), "a geometry shader that stores to a buffer was not reported"); +} + void ReversedComponentOrderTests() { { auto queue = makeState(); @@ -891,6 +999,12 @@ void ReversedComponentOrderTests() { Require(!AgcDriver::Graphics::SplitsAlternateAlpha(blended), "one blend equation for color and alpha split an 8_8 SWAP_ALT draw"); queue.context[0x1e0] = 0x60000001u; Require(AgcDriver::Graphics::SplitsAlternateAlpha(AgcDriver::Graphics::DecodeState(queue)), "a separate alpha blend into an 8_8 SWAP_ALT target did not split its second channel"); + queue.context[0x1e0] = (1u << 30u) | 13u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (1u << 29u) | 1u | (1u << 16u) | (14u << 24u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "constant color factor"); + queue.context[0x1e0] = (1u << 30u) | (2u << 5u) | 13u; + Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorBlendOp == VK_BLEND_OP_MIN, "a constant color factor of a MIN blend into an 8_8 SWAP_ALT target was rejected"); } for (const auto& [swap, mapping] : {std::pair{2u, 0x1bu}, std::pair{3u, 0x93u}}) { auto queue = makeState(); @@ -1607,6 +1721,132 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +void multisampleTests() { + using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorMetadataPass; + using AgcDriver::Graphics::DecodeState; + const auto multisampled = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + auto queue = multisampled(); + auto state = DecodeState(queue); + Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection); + queue.context[0x302] = 0x44cc; + expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); + queue = multisampled(); + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + state = DecodeState(queue); + Require(state.samples == 8 && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); + queue = multisampled(); + queue.context[0x31d] = 0x1000; + expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "sample count differs"); + queue = multisampled(); + queue.context[0x292] = 2; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); + queue = multisampled(); + queue.context[0x2f8] |= 0x10; + expectFailure([&] { DecodeState(queue); }, "coverage conversion"); + queue = multisampled(); + queue.context[0x31c] |= 0x6000; + queue.context[0x31f] = 0x1234; + Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode"); + queue = makeState(); + queue.context[0x31c] |= 0x4000; + expectFailure([&] { DecodeState(queue); }, "color compression"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0x1234; + const auto fastCleared = DecodeState(queue).colors[0]; + Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x4000; + queue.context[0x31f] = 0x1234; + const auto compressed = DecodeState(queue).colors[0]; + Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0; + expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0x1234; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + static_cast(DecodeState(queue)); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset)); + + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + auto pass = DecodeColorMetadataPass(queue); + Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); + queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + queue.context[0x32b] = queue.context[0x31c] | 0x2000u; + queue.context[0x32e] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 8u); + queue.context[0x399] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 40u); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0020; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + + using AgcDriver::Graphics::DccKeys; + using AgcDriver::Graphics::MultisampledCmask; + MultisampledCmask cmask(0x10000, 0x1000); + cmask.NoteFill(0x10000, 0x1000, 0x11111111u); + expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed"); + cmask.NoteFill(0x10800, 0x1000, 0); + expectFailure([&] { cmask.TakeClear(); }, "partly covers"); + cmask.NoteFill(0x10000, 0x1000, 0); + Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once"); + cmask.NoteFill(0x10000, 0x1000, 0xffffffffu); + Require(!cmask.TakeClear(), "an expanded CMASK cleared"); + cmask.NoteFill(0x20000, 0x1000, 0x11111111u); + Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK"); + cmask.CheckAddress(0x10000); + expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); + MultisampledCmask seeded(0x30000, 0x1000); + seeded.SeedKeys(DccKeys::Clear0000); + Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + MultisampledCmask mixedSeed(0x30000, 0x1000); + mixedSeed.SeedKeys(DccKeys::Mixed); + expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); + MultisampledCmask unaddressed; + unaddressed.NoteFill(0, 0x1000, 0); + Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -2149,6 +2389,42 @@ void expectSingleAccepted(const ShaderRecompiler::DescriptorBinding& binding, st Require(mock.live == 0, std::string(what) + " leaked Vulkan objects"); } +void descriptorLayoutFlagTests() { + constexpr VkDescriptorBindingFlags indexed = VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT | VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT; + mock = MockVulkan{}; + { + AgcDriver::Graphics::DescriptorCache cache(mockContext()); + const VkDescriptorSetLayoutBinding binding{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, nullptr}; + const std::array key{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT}; + const std::array flags{indexed}; + const auto layout = cache.Layout(key, std::span(&binding, 1), false, flags); + Require(mock.layoutFlags == 0 && mock.layoutBindingFlags == std::vector{indexed}, "native layout lost binding flags without update-after-bind"); + Require(cache.Layout(key, std::span(&binding, 1), false, flags) == layout, "identical binding flags missed the layout cache"); + Require(cache.Layout(key, std::span(&binding, 1)) != layout && mock.layoutBindingFlags.empty(), "layout cache ignored binding flags"); + } + Require(mock.live == 0, "binding flag layout cache leaked Vulkan objects"); + for (const bool updateAfterBind : {false, true}) { + mock = MockVulkan{}; + auto context = mockContext(); + context.emptyBuffer = reinterpret_cast(std::uintptr_t{0x1000}); + context.descriptorIndexingLimits.maxPerStageDescriptorUpdateAfterBindStorageBuffers = 17; + context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128; + context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 32; + { + ShaderRecompiler::RecompileResult program; + const std::uint32_t count = updateAfterBind ? 17 : 1; + program.bindings.push_back(makeBinding(Role::GuestBuffers, 0, count, std::vector(4 * count))); + const AgcDriver::Graphics::CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + AgcDriver::Graphics::ShaderResources resources(context, compute); + const auto flags = resources.LayoutBindingFlags(); + Require(flags.size() == resources.LayoutBindings().size() && flags.size() == 1, "exported binding flag count differs from native layout"); + Require(flags[0] == (updateAfterBind ? VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT : 0), "exported binding flags differ from native layout"); + Require(updateAfterBind ? mock.layoutBindingFlags == std::vector(flags.begin(), flags.end()) : mock.layoutBindingFlags.empty(), "native layout did not use the exported binding flag vector"); + } + Require(mock.live == 0, "exported binding flag fixture leaked Vulkan objects"); + } +} + void descriptorPoolTests() { using namespace AgcDriver::Graphics; mock = MockVulkan{}; @@ -3025,8 +3301,20 @@ void rectListTests() { queue.userConfig[0x242] = 17; const auto state = DecodeState(queue); const Context context{}; - const AgcDriver::Pm4::DrawParameters draw{0, 4, 0, 1, 0, false}; - expectFailure([&] { Draw(context, state, draw, {}); }, "incomplete rect-list"); + std::array indices{}; + const auto drawFailure = [&](std::uint32_t count, bool indexed) { + const AgcDriver::Pm4::DrawParameters draw = indexed ? AgcDriver::Pm4::DrawParameters{reinterpret_cast(indices.data()), count, 2, 1, 0, true} : AgcDriver::Pm4::DrawParameters{0, count, 0, 1, 0, false}; + try { + Draw(context, state, draw, {}); + } catch (const std::exception& error) { + return std::string(error.what()); + } + return std::string{}; + }; + for (const auto indexed : {false, true}) { + const auto complete = drawFailure(3, indexed); + for (const auto count : {4u, 5u}) Require(drawFailure(count, indexed) == complete, "a rect-list draw with a partial last rectangle was not drawn like its complete rectangles"); + } } ShaderRecompiler::RecompileResult recompilePixel(std::initializer_list controls, std::span code) { @@ -4248,6 +4536,11 @@ int main(int argc, char** argv) { setenv("APS5_HEAP_MIRROR_MIB", "4", 1); #endif try { + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-layout-flags") { + descriptorLayoutFlagTests(); + std::cout << "Descriptor layout native and captured binding flags passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-reset-failure") { descriptorPoolResetFailureTest(); std::cerr << "descriptor pool reset failure did not terminate\n"; @@ -4305,17 +4598,21 @@ int main(int argc, char** argv) { OpaqueDestinationAlphaTests(); CompactedExportTests(); ReversedComponentOrderTests(); + ShadersWriteMemoryTests(); DualSourceBlendTests(); metadataPassTests(); cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); + multisampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); PixelInputLayoutTests(); + UnexportedPixelInputTests(); ColorPipeBankXorTests(); HeaderlessVertexStageTests(); + HiddenUserWordVertexTablesTests(); ComputeScratchTests(); shaderUserDataTailPaddingTests(); InitialContextTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index 17c06cd64..02a244738 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -34,6 +34,12 @@ struct Builder { return created; } + IrValue& truth() { + auto& created = program.CreateValue(IrOpcode::Void, IrType::Bool); + created.SetImmediateBool(true); + return created; + } + IrValue& emit(IrOpcode opcode, IrType type, std::initializer_list arguments, std::uint64_t flags = 0) { auto& created = program.CreateValue(opcode, type, flags); for (auto* argument : arguments) created.AddArgument(argument); @@ -592,14 +598,24 @@ int main() { return b.eliminate() == 0u && !removed(written); }); - const auto sample = [](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw) { + const auto wholeQuads = [](Builder& b, IrValue& live) -> IrValue& { + auto& ballot = b.emit(IrOpcode::Ballot, IrType::U32x4, {&live}); + auto& allLow = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& allHigh = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& low = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allLow, &b.constant(0u)})}); + auto& high = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&ballot, &b.constant(1u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&allHigh, &b.constant(1u)})}); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&low, &high})}); + return b.threadBit(b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)})); + }; + + const auto sample = [&](IrShaderStage stage, std::uint32_t sampleFlags, IrOpcode opcode = IrOpcode::ImageSampleRaw, bool wholeQuadExec = false) { Builder b; b.program.Resources().stage = stage; MemoryInfo memory; memory.kind = ResourceKind::Image; memory.imageSampleFlags = sampleFlags; b.program.Resources().memoryInfo.push_back(memory); - auto& exec = b.mask(16u); + auto& exec = wholeQuadExec ? wholeQuads(b, b.mask(16u)) : b.mask(16u); auto& old = b.lane(); auto& written = b.select(exec, b.add(old, 1u), old); auto& address = b.emit(IrOpcode::MakeImageAddress, IrType::ImageAddress, {&written, &old}); @@ -618,6 +634,9 @@ int main() { return !sample(IrShaderStage::Pixel, 0u); }); + passed &= run("an implicit-LOD pixel sample under whole quad mode reads no masked quad and drops the select", [&] { + return sample(IrShaderStage::Pixel, 0u, IrOpcode::ImageSampleRaw, true); + }); passed &= run("an explicit-LOD sample reads only its lane and drops the select", [&] { return sample(IrShaderStage::Pixel, RdnaImageSampleFlagLevelZero) && sample(IrShaderStage::Mesh, 0u); }); @@ -697,5 +716,54 @@ int main() { return !guarded(IrOpcode::SharedAtomicIAdd32, true, false); }); + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select in every stage", [&] { + const auto widened = [&](IrShaderStage stage) { + Builder b; + b.program.Resources().stage = stage; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }; + return widened(IrShaderStage::Pixel) && widened(IrShaderStage::Compute); + }); + + passed &= run("a write under whole quad mode read by a lane outside the widened mask keeps the select in compute", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Compute; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + + passed &= run("a write under whole quad mode read only by the lanes it was widened from drops the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.logicalAnd(live, b.mask(8u)), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() != 0u && removed(written) && !removed(next); + }); + + passed &= run("a write under whole quad mode read by other lanes keeps the select", [&] { + Builder b; + auto& live = b.mask(16u); + auto& quads = wholeQuads(b, live); + auto& old = b.lane(); + auto& written = b.select(quads, b.add(old, 1u), old); + auto& next = b.select(b.mask(32u), b.add(written, 2u), old); + b.keep(next); + return b.eliminate() == 0u && !removed(written); + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 74b7c1c60..5c6394789 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -362,6 +362,12 @@ void testMemory() { check(data[0] == 61 && data[1] == 62, "WRITE_DATA with a cache policy failed"); execute(state, makePacket(0x37, {0x40100500, low(data.data()), high(data.data()), 81, 82})); check(data[0] == 81 && data[1] == 82, "WRITE_DATA to memory from the graphics queue failed"); + execute(state, makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 91, 92})); + check(data[0] == 91 && data[1] == 92, "WRITE_DATA with destination 3 did not write memory"); + const auto fence = makePacket(0x37, {0x06100300, low(data.data()), high(data.data()), 0}); + const auto fenceLabel = AgcDriver::Pm4::DecodeLabelWrite(fence); + check(fenceLabel.has_value() && fenceLabel->address == reinterpret_cast(data.data()), "WRITE_DATA with destination 3 did not resolve as a label"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x100000, low(data.data()), high(data.data()), 1}), 0); }, "register destination"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x08000100, low(data.data()), high(data.data()), 71}), 0); }, "reserved"); expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x37, {0x80000100, low(data.data()), high(data.data()), 51}), 0); }, "engine"); execute(state, makePacket(0x83, {0xbff8, 2, low(data.data()), high(data.data())})); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a67d7e6c..5484435e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -38,6 +39,7 @@ #else #include #endif +#include #include #include #include @@ -250,6 +252,7 @@ public: function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Vulkan device: " << properties.deviceName << '\n'; context.limits = properties.limits; context.bufferDeviceAddress = true; context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); @@ -1764,6 +1767,150 @@ void drawSnapshotPatchTests(const Device& device) { } } +void drawSnapshotCapTests(const Device& device, Recorder& recorder) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + const char* previous = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + const std::string saved = previous != nullptr ? previous : ""; +#ifdef _WIN32 + Require(_putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", "1") == 0, "cannot set snapshot cap"); +#else + Require(setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", "1", 1) == 0, "cannot set snapshot cap"); +#endif + struct RestoreCap { + const char* previous; + std::string saved; + ~RestoreCap() { +#ifdef _WIN32 + _putenv_s("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str()); +#else + if (previous != nullptr) { + setenv("APS5_DRAW_SNAPSHOT_MAX_MIB", saved.c_str(), 1); + } else { + unsetenv("APS5_DRAW_SNAPSHOT_MAX_MIB"); + } +#endif + } + } restore{previous, saved}; + constexpr std::size_t bytes = 3 * 1024 * 1024; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot cap not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + std::memset(block, 0x11, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot cap not tested\n"; + return; + } + const auto smallAddress = address + 4096; + constexpr std::size_t smallBytes = 1024 * 1024 - 4096; + const auto largeAddress = address + 1024 * 1024 + 8192; + constexpr std::size_t largeBytes = 1024 * 1024 + 4096; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding smallBinding; + smallBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + smallBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + smallBinding.descriptorSet = 0; + smallBinding.binding = 0; + smallBinding.count = 1; + smallBinding.guestDescriptor = {static_cast(smallAddress), static_cast(smallAddress >> 32u) & 0xffffu, static_cast(smallBytes), 0x31000000u}; + smallBinding.bufferWritten = {false}; + program.bindings.push_back(smallBinding); + ShaderRecompiler::DescriptorBinding largeBinding; + largeBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + largeBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + largeBinding.descriptorSet = 0; + largeBinding.binding = 1; + largeBinding.count = 1; + largeBinding.guestDescriptor = {static_cast(largeAddress), static_cast(largeAddress >> 32u) & 0xffffu, static_cast(largeBytes), 0x31000000u}; + largeBinding.bufferWritten = {false}; + program.bindings.push_back(largeBinding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared"); + Require(bindings->snapshots.size() == 1, "an input exceeding the draw snapshot cap was snapshotted"); + Require(bindings->snapshots[0].address == smallAddress, "the snapshotted input does not match the smaller buffer below the cap"); + Require(snapshotRecorder.ReusableDrawSnapshot(largeAddress, largeBytes) == nullptr, "an input exceeding the draw snapshot cap was kept in the snapshot cache"); + if (Watched(address, bytes)) { + Require(snapshotRecorder.ReusableDrawSnapshot(smallAddress, smallBytes) != nullptr, "an input below the draw snapshot cap was not kept in the snapshot cache"); + } + const auto contents = bindings->snapshots[0].buffer->Bytes(); + Require(contents.size() == smallBytes && std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "a draw snapshot does not hold the guest bytes of its draw"); + std::memset(reinterpret_cast(smallAddress), 0x22, smallBytes); + std::memset(reinterpret_cast(largeAddress), 0x33, largeBytes); + Require(std::all_of(contents.begin(), contents.end(), [](std::byte value) { return value == std::byte{0x11}; }), "snapshotted input observed CPU overwrite"); + const auto* inPlace = reinterpret_cast(largeAddress); + Require(inPlace[0] == std::byte{0x33} && std::all_of(inPlace, inPlace + largeBytes, [](std::byte value) { return value == std::byte{0x33}; }), "in-place bound input did not observe CPU write"); + std::memset(block, 0x11, bytes); + snapshotRecorder.Sync(); + } + const auto firstAddress = address + 4096; + constexpr std::size_t firstBytes = 600 * 1024; + const auto secondAddress = address + 600 * 1024 + 8192; + constexpr std::size_t secondBytes = 600 * 1024; + ShaderRecompiler::RecompileResult cumulativeProgram; + ShaderRecompiler::DescriptorBinding firstBinding; + firstBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + firstBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + firstBinding.descriptorSet = 0; + firstBinding.binding = 0; + firstBinding.count = 1; + firstBinding.guestDescriptor = {static_cast(firstAddress), static_cast(firstAddress >> 32u) & 0xffffu, static_cast(firstBytes), 0x31000000u}; + firstBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(firstBinding); + ShaderRecompiler::DescriptorBinding secondBinding; + secondBinding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + secondBinding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + secondBinding.descriptorSet = 0; + secondBinding.binding = 1; + secondBinding.count = 1; + secondBinding.guestDescriptor = {static_cast(secondAddress), static_cast(secondAddress >> 32u) & 0xffffu, static_cast(secondBytes), 0x31000000u}; + secondBinding.bufferWritten = {false}; + cumulativeProgram.bindings.push_back(secondBinding); + const CompiledShader cumulativeCompute{ShaderRecompiler::ShaderStage::Compute, &cumulativeProgram, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, cumulativeCompute); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr, "read-only draw bindings were not prepared for cumulative test"); + Require(bindings->snapshots.size() == 1, "cumulative cap permitted both inputs to snapshot"); + Require(bindings->snapshots[0].address == firstAddress, "the earlier binding in the layout was not prioritised for the snapshot budget"); + snapshotRecorder.Sync(); + } + recorder.Activate(); +} void drawSnapshotEvictionTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -2201,7 +2348,7 @@ void storageRefreshTests(const Device& device, Recorder& recorder, bool watched) DccKeyProof proof; Require(ProvedClearKeys(resource, surfaceBytes, proof) == DccKeys::Clear0000, "0x00 keys are not a 0000 clear"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -2555,7 +2702,7 @@ void staleGenerationTests(const Device& device, Recorder& recorder) { resource.dccAddress = address + surfaceBytes; { Require(AgcDriver::GuestMemory::Watched(address, bytes), "(s) the block is not watched before the storage image takes its generation"); - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -2697,7 +2844,7 @@ void writeBackPaddingTests(const Device& device, Recorder& recorder, bool watche context.detiler = &detiler; resource.baseAddress = address; { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, test.mipCount, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -2874,7 +3021,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { for (auto& value : texels) value = random(); const auto movesBefore = StorageTexture::SinglePassMoves(); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(image); results[mode].uploaded = readImage(*image); Buffer staging(context, linearBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); @@ -2898,7 +3045,7 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { recorder.Submit(); device.WaitQueue(); recorder.Sync(); - auto again = std::make_shared(context, detiler, resource, 0); + auto again = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(again); results[mode].reuploaded = readImage(*again); std::vector read(bytes); @@ -2929,6 +3076,145 @@ void singlePassTests(const Device& device, Recorder& recorder, bool watched) { std::cout << "single-pass storage moves" << (watched ? " (watched)" : "") << ": ok (" << tested << " surfaces)\n"; } +class StorageUploadProbe { +public: + StorageUploadProbe(Context& context, bool failView) : context(context), resolver(context.deviceProc), failView(failView) { + Require(active == nullptr, "nested storage upload probe"); + active = this; + context.deviceProc = resolve; + } + ~StorageUploadProbe() { + for (const auto view : destroyedViews) function("vkDestroyImageView")(context.device, view, nullptr); + if (imageDestroyed) function("vkDestroyImage")(context.device, image, nullptr); + if (memoryFreed) function("vkFreeMemory")(context.device, memory, nullptr); + context.deviceProc = resolver; + active = nullptr; + } + VkImage Image() const { return image; } + bool Destroyed() const { return imageDestroyed || memoryFreed; } + bool Released() const { return imageDestroyed && memoryFreed; } + bool FailedView() const { return failedView; } + std::uint32_t Clears() const { return clears; } + +private: + template + TFunction function(const char* name) const { return reinterpret_cast(resolver(context.device, name)); } + static VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice device, const VkImageCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImage* created) { + const auto result = active->function("vkCreateImage")(device, info, callbacks, created); + if (result == VK_SUCCESS) active->image = *created; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL bindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize offset) { + const auto result = active->function("vkBindImageMemory")(device, image, memory, offset); + if (result == VK_SUCCESS && image == active->image) active->memory = memory; + return result; + } + static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) { + if (info->image == active->image && active->failView && active->clears != 0) { + active->failedView = true; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + const auto result = active->function("vkCreateImageView")(device, info, callbacks, created); + if (result == VK_SUCCESS && info->image == active->image) active->views.push_back(*created); + return result; + } + static VKAPI_ATTR void VKAPI_CALL destroyView(VkDevice device, VkImageView view, const VkAllocationCallbacks* callbacks) { + if (std::find(active->views.begin(), active->views.end(), view) != active->views.end()) active->destroyedViews.push_back(view); + else active->function("vkDestroyImageView")(device, view, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* callbacks) { + if (image == active->image) active->imageDestroyed = true; + else active->function("vkDestroyImage")(device, image, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* callbacks) { + if (memory == active->memory) active->memoryFreed = true; + else active->function("vkFreeMemory")(device, memory, callbacks); + } + static VKAPI_ATTR void VKAPI_CALL clearImage(VkCommandBuffer commands, VkImage image, VkImageLayout layout, const VkClearColorValue* value, std::uint32_t count, const VkImageSubresourceRange* ranges) { + if (image == active->image) ++active->clears; + active->function("vkCmdClearColorImage")(commands, image, layout, value, count, ranges); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateImage") == 0) return reinterpret_cast(createImage); + if (std::strcmp(name, "vkBindImageMemory") == 0) return reinterpret_cast(bindImageMemory); + if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast(createView); + if (std::strcmp(name, "vkDestroyImageView") == 0) return reinterpret_cast(destroyView); + if (std::strcmp(name, "vkDestroyImage") == 0) return reinterpret_cast(destroyImage); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(freeMemory); + if (std::strcmp(name, "vkCmdClearColorImage") == 0) return reinterpret_cast(clearImage); + return active->resolver(device, name); + } + Context& context; + PFN_vkGetDeviceProcAddr resolver; + bool failView; + bool failedView = false; + bool imageDestroyed = false; + bool memoryFreed = false; + std::uint32_t clears = 0; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + std::vector views; + std::vector destroyedViews; + static inline StorageUploadProbe* active = nullptr; +}; + +void storageUploadLifetimeTests(const Device& device, Recorder& previous, bool failView) { + auto context = device.GetContext(); + context.hostImportAlignment = 0; + TextureDetiler detiler(context); + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = 64; + resource.height = 64; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kLinear; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + const auto guestBytes = static_cast(DescribeSurface(resource).guestBytes); + std::vector texels(guestBytes + 256, std::byte{0x55}); + std::vector keys(guestBytes / 256, std::byte{0}); + resource.baseAddress = (reinterpret_cast(texels.data()) + 255) & ~std::uintptr_t{255}; + resource.dccAddress = reinterpret_cast(keys.data()); + DccKeyProof proof; + Require(ProvedClearKeys(resource, guestBytes, proof) == DccKeys::Clear0000, "storage lifetime fixture has no DCC clear"); + StorageUploadProbe probe(context, failView); + Buffer readback(device.GetContext(), resource.width * resource.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0x55, readback.Bytes().size()); + std::weak_ptr lifetime; + bool premature = false; + bool retained = true; + bool threw = false; + { + Recorder recorder(context); + recorder.Activate(); + try { + auto image = StorageTexture::Create(context, detiler, resource, 0); + lifetime = image; + image.reset(); + retained = !lifetime.expired(); + } catch (const std::exception& error) { + if (!probe.FailedView() || std::string_view(error.what()).find("vkCreateImageView storage") == std::string_view::npos) throw; + threw = true; + } + premature = probe.Destroyed(); + Require(probe.Clears() == 1 && recorder.Recording(), "storage creation did not record its initial clear"); + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {resource.width, resource.height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, probe.Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Sync(); + } + previous.Activate(); + Require(threw == failView && probe.FailedView() == failView, "storage view failure injection did not take the requested path"); + Require(std::all_of(readback.Bytes().begin(), readback.Bytes().end(), [](std::byte value) { return value == std::byte{0}; }), "recorded storage clear did not reach every texel"); + Require(lifetime.expired() && probe.Released(), "storage upload resources outlived recorder teardown"); + Require(!premature && retained, failView ? "failed storage creation released an image still referenced by its recorded clear" : "storage creation did not retain its image through the recorded clear"); + std::cout << "Storage upload lifetime" << (failView ? " after view failure" : " after caller release") << ": passed\n"; +} + void refreshProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -2987,7 +3273,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { resource.dccAddress = address + surfaceBytes; const auto proved = [] { return StorageTexture::RefreshesProved(); }; { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -3032,7 +3318,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { expectProved("the proof was not taken again after a key store"); auto other = resource; other.dccAddress = 0; - auto pending = std::make_shared(context, detiler, other, 0); + auto pending = StorageTexture::Create(context, detiler, other, 0); recorder.Keep(pending); pending->MarkDirty(); expectFull("another image's pending results over the surface were answered from the proof"); @@ -3041,7 +3327,7 @@ void refreshProofTests(const Device& device, Recorder& recorder) { pending->MarkDirty(); expectFull("another image's pending results were answered from the proof"); Require(!AgcDriver::Graphics::PendingStorageOverlaps(address, surfaceBytes, image.get()), "the full refresh left another image's results pending"); - auto fresh = std::make_shared(context, detiler, resource, 0); + auto fresh = StorageTexture::Create(context, detiler, resource, 0); const auto before = proved(); fresh->Refresh(); Require(proved() == before, "a new image of the surface was answered from another image's proof"); @@ -3127,7 +3413,7 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { resource.dstSelW = 7; Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(c) unexpected surface size"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -3240,7 +3526,7 @@ void settledBaselineTests(const Device& device, Recorder& recorder) { if (pending) recorder.NotePendingWrite(address + 4096, 4096); const auto before = recorder.Submissions(); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(image); if (pending) Require(recorder.Submissions() == before, "(b) an upload over a pending recorded store submitted the open batch to read its generation baseline"); } @@ -3330,7 +3616,7 @@ void importWindowTests(const Device& device, Recorder& recorder) { Require(!HostImportCovers(base, address, bytes), "(w) the block is imported before the cache takes its generation"); const auto cached = CollectWritesUncached(address, surfaceBytes); Require(cached != 0 && UnchangedSince(address, surfaceBytes, cached), "(w) the cache's generation is not current before the import"); - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto commands = recorder.Commands(); recorder.Keep(image); @@ -4168,6 +4454,109 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) { + std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t bytes = 5u * 65536u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the multisampled target block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { + std::string message; + try { + access(); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message); + }; + ColorTarget target{}; + target.address = address; + target.surfaceAddress = address; + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.bytes = targetBytes; + target.samples = 4; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::array texels{}; + refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + binding.guestDescriptor = { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (9u << 28u), + 0u, + 0u, + 0u, + 0u, + }; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + refused([&] { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, compute); + }, "a texture read", "a texture read"); + refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); + AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); + ClearMultisampleTargets(context.device); + AgcDriver::GuestMemory::Read(address + 256u, texels); + std::cout << "Multisampled target guest access tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -4440,7 +4829,7 @@ void singleCubeTests(const Device& device, Recorder& recorder) { resource.baseAddress = reinterpret_cast(surface); const std::span snapshot(surface, static_cast(geometry.guestBytes)); Texture texture(context, detiler, resource, identity, snapshot); - auto storage = std::make_shared(context, detiler, resource, 0); + auto storage = StorageTexture::Create(context, detiler, resource, 0); recorder.Keep(storage); Texture storageView(context, storage, resource, identity); for (std::uint32_t face = 0; face < 6; ++face) { @@ -4474,7 +4863,7 @@ void firstLayerViewTests(const Device& device, Recorder& recorder) { auto* surface = reinterpret_cast((reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}); for (std::uint32_t layer = 0; layer < 3; ++layer) std::memset(surface + geometry.GuestLayerOffset(layer), 0x20 * (layer + 1), static_cast(geometry.layerBytes)); resource.baseAddress = reinterpret_cast(surface); - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); SampleProgram program(context, recorder); expectRed(program.Red(image->FirstLayerView(0), VK_IMAGE_LAYOUT_GENERAL, 0.0f), 0x40 / 255.0f, "a first-layer view does not read the BASE_ARRAY layer"); @@ -4515,7 +4904,7 @@ void atomicViewTests(const Device& device, Recorder& recorder) { const auto geometry = DescribeSurface(resource); std::vector memory(static_cast(geometry.guestBytes) + 256); resource.baseAddress = (reinterpret_cast(memory.data()) + 255) & ~std::uintptr_t{255}; - auto image = std::make_shared(withDetiler, detiler, resource, 0); + auto image = StorageTexture::Create(withDetiler, detiler, resource, 0); recorder.Keep(image); if (format == 56u) { bool refused = false; @@ -4685,7 +5074,7 @@ void keysFillTests(const Device& device, Recorder& recorder) { resource.dccAddress = keysAddress; Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the key fill surface has an unexpected size"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; const auto draw = [&](VkClearColorValue value) { const auto commands = recorder.Commands(); @@ -4798,7 +5187,7 @@ void denormalClearTests(const Device& device, Recorder& recorder) { Require(ClearKeepsDenormals(context, VK_FORMAT_R16G16_SFLOAT) == keeps, "the denormal clear probe changed its answer"); Require(!ClearKeepsDenormals(context, VK_FORMAT_R32G32B32A32_SFLOAT), "a format without small floats probed as keeping denormals"); { - auto image = std::make_shared(context, detiler, resource, 0); + auto image = StorageTexture::Create(context, detiler, resource, 0); const auto holds = [&](std::uint32_t texel) { Buffer readback(context, side * side * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder.Commands(); @@ -4896,6 +5285,13 @@ void pipelineLibraryTests(const Device& device) { state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT; static_cast(lookup()); Require(counters().built == std::array{2, 2, 3, 3}, "another target format rebuilt more than the fragment output library"); + state.samples = 4; + state.colors.front().samples = 4; + const auto multisampled = lookup(); + Require(counters().built == std::array{2, 2, 4, 4}, "a 4x pipeline shared the 1x fragment shader or fragment output library"); + state.samples = 1; + state.colors.front().samples = 1; + Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); @@ -4933,6 +5329,10 @@ int main(int argc, char** argv) { std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && (std::string_view(argv[1]) == "--storage-upload-lifetime-only" || std::string_view(argv[1]) == "--storage-upload-failure-only")) { + storageUploadLifetimeTests(device, recorder, std::string_view(argv[1]) == "--storage-upload-failure-only"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--cube-only") { singleCubeTests(device, recorder); std::cout << "Single cube snapshot and storage sampling tests passed\n"; @@ -4974,11 +5374,20 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { + multisampleGuestAccessTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { + drawSnapshotCapTests(device, recorder); + std::cout << "Draw snapshot cap tests passed\n"; + return 0; + } readTrackingTests(device, recorder); writeSettledTests(device, recorder); @@ -4997,10 +5406,12 @@ int main(int argc, char** argv) { resourceReadTests(device, recorder); readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); + multisampleGuestAccessTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); drawDescriptorWriteTests(device, recorder); + drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index fee50cb71..b0822a499 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -23,6 +23,15 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif namespace { @@ -487,6 +496,63 @@ void verifyFloatModeSerialization() { require(back.request.context.floatMode == mode, "the float mode did not survive serialization"); } +void verifyAtomicReplacement(const std::filesystem::path& directory) { + struct Reader { +#ifdef _WIN32 + HANDLE file; + explicit Reader(const std::filesystem::path& path) + : file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)) { + require(file != INVALID_HANDLE_VALUE, "cannot retain the cache file"); + } + ~Reader() { CloseHandle(file); } + std::vector Read() { + require(SetFilePointerEx(file, {}, nullptr, FILE_BEGIN), "cannot seek the retained cache file"); + std::array bytes{}; + DWORD count = 0; + require(ReadFile(file, bytes.data(), static_cast(bytes.size()), &count, nullptr), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + count}; + } +#else + std::ifstream file; + explicit Reader(const std::filesystem::path& path) : file(path, std::ios::binary) { + require(file.is_open(), "cannot retain the cache file"); + } + std::vector Read() { + file.clear(); + file.seekg(0); + require(!file.fail(), "cannot seek the retained cache file"); + std::array bytes{}; + file.read(reinterpret_cast(bytes.data()), bytes.size()); + require(file.eof() && !file.bad(), "cannot read the retained cache file"); + return {bytes.begin(), bytes.begin() + file.gcount()}; + } +#endif + Reader(const Reader&) = delete; + Reader& operator=(const Reader&) = delete; + }; + const auto root = directory / "atomic-replacement"; + const auto path = root / "entry.bin"; + const std::vector original{std::byte{0x12}, std::byte{0x00}, std::byte{0x34}}; + const std::vector replacement{std::byte{0xab}, std::byte{0xcd}, std::byte{0x00}, std::byte{0xef}}; + require(WriteFileAtomically(path, original), "cannot write the initial cache entry"); + { + Reader old(path); + require(old.Read() == original, "the retained reader did not read the original entry"); + require(WriteFileAtomically(path, replacement), "cannot replace cache entry with a retained reader"); + require(old.Read() == original, "replacement changed the retained original entry"); + std::vector fresh; + require(ReadWholeFile(path, fresh) && fresh == replacement, "a fresh reader did not read the replacement entry"); + Reader current(path); + require(current.Read() == replacement, "the replacement reader did not retain its entry"); + require(std::filesystem::remove(path), "cannot delete the replacement while readers retain it"); + require(!ReadWholeFile(path, fresh), "the deleted cache entry remains readable by path"); + require(old.Read() == original && current.Read() == replacement, "deletion invalidated retained cache readers"); + } + require(std::filesystem::is_empty(root), "atomic replacement left a temporary file behind"); + require(std::filesystem::remove(root), "cannot remove the empty replacement directory"); +} + void verifyStore() { require(ShaderDiskCache::Enabled(), "the disk cache is not enabled"); SampleRequest sample; @@ -1034,6 +1100,7 @@ int main(int argc, char** argv) { verifyDefaultDirectory(argv[0]); setEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string()); + verifyAtomicReplacement(directory); verifyResultRoundTrip(); verifyEntryRoundTrip(); verifyArtifactStorageIsolation(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1a8234e8f..2a86162fa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5,10 +5,13 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include "Optimization/SrtWalker.hpp" #include "Optimization/SrtWalker/SrtDescriptorEvaluation.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" #include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" +#include "Optimization/SrtWalker.hpp" +#include "IntermediateRepresentation/IrBuilder.hpp" #include "SpirvBackend/SpirvAnalysis.hpp" #if ANYPS5_ENABLE_SPIRV_TOOLS #include "SpirvBackend/SpirvOptimizer.hpp" @@ -443,6 +446,9 @@ void verifySignedSrtComparison() { source.dwords[1] = &make(IrOpcode::SelectU32, IrType::U32, {&aboveMin, &greaterThanMin, &equalMin}); source.dwords[2] = &make(IrOpcode::SelectU32, IrType::U32, {&belowMaxValue, &belowMax, &equalMax}); plan.descriptorSources.push_back(source); + for (std::uint32_t dword = 0; dword < source.dwordCount; ++dword) { + require(SrtWalker().ValidateRuntimeValue(plan, source.dwords[dword]), "signed SRT comparison: the runtime validator rejects a descriptor dword the evaluator computes"); + } const std::array sourceIndex{0u}; const std::array>, 4> cases{{ {0x80000000u, {0xau, 0xdu, 0xeu}}, @@ -1356,6 +1362,101 @@ void verifyHalfWaveReduction() { expectFailure([&] { static_cast(pixel(umin, 8u, true)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "half-wave reduction: the scan built on an 8-lane subgroup"); } +void verifyNarrowVertexWave() { + using namespace ShaderRecompiler; + static constexpr std::array capabilities{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityGroupNonUniformArithmetic}; + static constexpr std::array userData{0u, 0u, 0u, 0x2020u}; + const auto vertex = [](std::span code, std::uint32_t waveSize, std::uint32_t subgroupSize, std::uint32_t userDataBase) { + RecompileRequest request{}; + request.shader = {ShaderStage::Vertex, 0x50000u, code, 0, {}}; + request.context.waveSize = waveSize; + request.context.userDataBaseRegister = userDataBase; + if (userDataBase == 0u) request.context.userData = userData; + request.context.vertex = ShaderVertexStageInfo{}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = subgroupSize; + request.target.supportedCapabilities = capabilities; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + return request; + }; + const auto prepared = [&vertex](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { + return GetPreparedArtifact(*PrepareShader(vertex(code, 64u, subgroupSize, userDataBase))).spirv; + }; + const auto prepares = [&prepared](std::span code, std::uint32_t subgroupSize, std::uint32_t userDataBase = 8u) { + return !prepared(code, subgroupSize, userDataBase).empty(); + }; + const auto reductions = [](const std::vector& spirv) { + std::uint32_t count = 0; + for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) { + if ((spirv[cursor] & 0xffffu) == spv::OpGroupNonUniformBitwiseOr && (spirv[cursor] >> 16u) == 6u && spirv[cursor + 4u] == spv::GroupOperationReduce) ++count; + } + return count; + }; + const std::vector culling{ + 0xd7650015u, 0x000100c1u, 0xd7660015u, 0x00022ac1u, 0x7e0e0280u, 0x7da206f9u, 0x81060015u, 0x360e0a81u, + 0xbefe04c1u, 0xd4c50002u, 0x00020e80u, 0xd763000fu, 0x00022a81u, 0xd5010000u, 0x000a1e80u, + 0x380000fau, 0xff091100u, 0x380000fau, 0xff091200u, 0x380000fau, 0xff091400u, 0x380000fau, 0xff091800u, + 0xd778000fu, 0x03058300u, 0x38001f00u, 0xd7600002u, 0x00013f00u, 0xd760006bu, 0x00017f00u, 0x88026b02u, + 0x7e020202u, 0xf80008cfu, 0x01010101u, 0xbf810000u, + }; + const auto patched = [&culling](std::initializer_list> words) { + auto code = culling; + for (const auto& [index, word] : words) code[index] = word; + return code; + }; + const char* lane63 = "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup"; + require(reductions(prepared(culling, 32u)) == 1u, "narrow vertex wave: the NGG culling scan, keyed on lanes below the primitive count in s3, did not read lane 31 as a subgroup OR on 32 lanes"); + const auto wide64 = prepared(culling, 64u); + require(!wide64.empty() && reductions(wide64) == 0u, "narrow vertex wave: the NGG culling scan, keyed on lanes below 64 on 64 lanes, did not keep its DPP scan"); + require(prepares(patched({{6, 0x80060015u}}), 32u), "narrow vertex wave: the scan keyed on lanes below the vertex count in s3 did not prepare on 32 lanes"); + static constexpr std::array noArithmetic{spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}; + const auto otherKeys = patched({{7, 0x360e0a82u}}); + auto withoutArithmetic = vertex(otherKeys, 64u, 32u, 8u); + withoutArithmetic.target.supportedCapabilities = noArithmetic; + const auto shuffled = GetPreparedArtifact(*PrepareShader(withoutArithmetic)).spirv; + require(!shuffled.empty() && reductions(shuffled) == 0u, "narrow vertex wave: without subgroup arithmetic, the scan did not keep lane 31 as a shuffle with lane 63 read as 0"); + require(prepares(patched({{5, 0x7da82aa0u}, {6, 0xbf800000u}}), 32u), "narrow vertex wave: the scan keyed on lanes below 32 did not prepare on 32 lanes"); + expectFailure([&] { static_cast(prepares(patched({{5, 0x7da82aa1u}, {6, 0xbf800000u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on lanes below 33 read lane 63"); + expectFailure([&] { static_cast(prepares(culling, 32u, 0u)); }, lane63, "narrow vertex wave: a scan keyed on a count from user data read lane 63"); + expectFailure([&] { static_cast(prepares(patched({{6, 0x81060005u}}), 32u)); }, lane63, "narrow vertex wave: a scan keyed on the vertex index instead of the lane read lane 63"); + auto otherRegister = patched({{21, 0x380200fau}, {24, 0x03058301u}, {25, 0x38001f01u}}); + otherRegister.insert(otherRegister.begin(), 0x7e020305u); + require(reductions(prepared(otherRegister, 32u)) == 1u, "narrow vertex wave: a scan whose bound_ctrl:1 step is written to another register did not read lane 31 as a subgroup OR on 32 lanes"); + otherRegister[23] = 0xff011800u; + expectFailure([&] { static_cast(prepares(otherRegister, 32u)); }, lane63, "narrow vertex wave: a scan step written to another register without bound_ctrl:1, which keeps that register's lanes outside the row, read lane 63"); + auto threeSteps = culling; + threeSteps.erase(threeSteps.begin() + 21, threeSteps.begin() + 23); + const auto partialScan = prepared(threeSteps, 32u); + require(!partialScan.empty() && reductions(partialScan) == 0u, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of 0 in lanes 32-63 did not read 0, or lane 31 became a subgroup OR"); + threeSteps[5] = 0x7da82aa1u; + threeSteps[6] = 0xbf800000u; + expectFailure([&] { static_cast(prepares(threeSteps, 32u)); }, lane63, "narrow vertex wave: lane 63 of row_shr 1/2/4 steps over keys of lanes below 33 read 0"); + + std::array, 2> narrow; + std::array, 2> wide; + for (const bool narrowFirst : {false, true}) { + const std::vector code{0xbf800001u + static_cast(narrowFirst), 0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + const auto narrowRequest = vertex(code, 64u, 32u, 8u); + const auto wideRequest = vertex(code, 64u, 64u, 8u); + ShaderPreparationContext preparation; + const auto first = PrepareShader(narrowFirst ? narrowRequest : wideRequest, &preparation); + const auto second = PrepareShader(narrowFirst ? wideRequest : narrowRequest, &preparation); + const auto& narrowHandle = narrowFirst ? *first : *second; + const auto& wideHandle = narrowFirst ? *second : *first; + require(first->source != second->source, "narrow vertex wave: the program prepared for 32 and for 64 lanes shares one translated source"); + require(MatchesPreparedShader(narrowRequest, narrowHandle) && MatchesPreparedShader(wideRequest, wideHandle), "narrow vertex wave: a prepared program did not match its own request"); + require(!MatchesPreparedShader(narrowRequest, wideHandle) && !MatchesPreparedShader(wideRequest, narrowHandle), "narrow vertex wave: a program prepared for one subgroup width matched a request for the other"); + narrow[narrowFirst ? 1u : 0u] = GetPreparedArtifact(narrowHandle).spirv; + wide[narrowFirst ? 1u : 0u] = GetPreparedArtifact(wideHandle).spirv; + } + require(narrow[0] == narrow[1] && wide[0] == wide[1], "narrow vertex wave: preparing for one subgroup width after the other gave another program than preparing it first"); + require(narrow[0] != wide[0], "narrow vertex wave: s3 of a wave64 vertex program was the same on 32 and 64 lanes"); + const std::vector wave32{0x7e020203u, 0xf80008cfu, 0x01010101u, 0xbf810000u}; + require(GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 32u, 8u))).spirv == GetPreparedArtifact(*PrepareShader(vertex(wave32, 32u, 64u, 8u))).spirv, "narrow vertex wave: s3 of a wave32 vertex program depended on the subgroup width"); +} + void verifyMeshConfiguration() { using namespace ShaderRecompiler; ShaderMeshInputInfo list; @@ -2655,6 +2756,36 @@ void verifyFunctionLdsBound() { require(unsized == FunctionLdsDwordLimit, "function LDS: an access without a known width must keep the full array"); } +void verifySrtReadPatching() { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + program.Resources().memoryInfo.push_back(memory); + + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& address = builder.Emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&builder.Constant(0u), &builder.Constant(0u)}); + auto& active = program.CreateValue(IrOpcode::Void, IrType::U1); + active.SetImmediateBool(true); + auto& read = builder.Emit(IrOpcode::LoadAddressU32, IrType::U32, {&address, &builder.Constant(0u), &builder.Constant(0u), &active}, MemoryFlags{0u, 0u}); + auto& repeatedUse = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&read, &read}); + auto& secondUser = builder.Emit(IrOpcode::IMul32, IrType::U32, {&read, &builder.Constant(2u)}); + + SrtWalker{}.BuildPlan(program); + + require(program.Resources().srtReads.size() == 1u, "SRT read patching: the scalar read was not planned"); + const auto* replacement = repeatedUse.Argument(0); + require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); + require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const auto& remainingUses = read.OperandUses(); + require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); +} + struct NestedRequest { std::vector code; std::array userData{}; @@ -2912,6 +3043,24 @@ void verifyCubeDepthCompareMode() { require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode"); } +void verifySrgbDepthCompareModes() { + using namespace ShaderRecompiler; + for (const auto decodeFormats : {0u, 1u, 2u, 3u}) { + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + image.depthCompare = true; + image.srgbDecodeCompatible = false; + image.srgbDecodeFormats = decodeFormats; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + for (const auto format : {128u, 129u, 130u}) { + const DescriptorValue descriptor{{0x100u, format << 20u, 0u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(!modes[selected].srgbDecode && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "sRGB color comparison did not select its emulated runtime mode independently of decode support"); + } + } +} + int main(int argc, char** argv) { try { using namespace ShaderRecompiler; @@ -2925,6 +3074,11 @@ int main(int argc, char** argv) { std::cout << "Bindless mapping, indexing capabilities and strict validation passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { + verifySrtReadPatching(); + std::cout << "SRT read patching replaces repeated and multiple uses\n"; + return 0; + } require(argc == 1, "unknown shader memory test arguments"); verifyRegisterSources(); verifyEvaluatedValues(); @@ -2939,6 +3093,7 @@ int main(int argc, char** argv) { verifyProgramCounterRelativeData(); verifyLanesOutsideHostSubgroup(); verifyHalfWaveReduction(); + verifyNarrowVertexWave(); verifyMeshConfiguration(); verifyPixelInputs(); verifyPixelRequestSerialization(); @@ -2956,8 +3111,10 @@ int main(int argc, char** argv) { verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); verifyFunctionLdsBound(); + verifySrtReadPatching(); verifyGuardedNullPointers(); verifyCubeDepthCompareMode(); + verifySrgbDepthCompareModes(); #if ANYPS5_ENABLE_SPIRV_TOOLS const std::vector minimalSpirv{ 0x07230203u, 0x00010000u, 0u, 5u, 0u, diff --git a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp index 895cbc8de..cd11b3797 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SharedWrites.cpp @@ -118,6 +118,59 @@ void smallStores() { require(StoreOwnBytes(0, std::span{}) == 0, "empty store produced a version"); } +void partialCollect() { + constexpr std::size_t page = 4096; + Mapping mapping(page * 4); + auto* data = static_cast(mapping.data); +#ifdef _WIN32 + void* alias = nullptr; + require(sceKernelMapDirectMemory(&alias, mapping.size, 3, 0, mapping.physical, 65536) == 0, "partial collect alias failed"); +#endif + require(WriteWatched(), "write tracking unavailable for partial collect"); + const auto before = CollectWritesUncached(mapping.Address(), mapping.size); + require(before != 0, "partial collect range is not watched"); + data[page * 3] = std::byte{91}; + require(CollectWritesUncached(mapping.Address(), page) != 0, "prefix collect failed"); + require(!UnchangedSinceCollected(mapping.Address() + page * 3, page, before), "partial collect lost a neighboring guest write"); + const auto settled = CollectWritesUncached(mapping.Address(), mapping.size); + require(UnchangedSinceCollected(mapping.Address(), mapping.size, settled), "fully collected shared range remained dirty"); +#ifdef _WIN32 + const auto collect = [&](void* view, std::size_t offset, std::size_t bytes, std::size_t capacity, bool clear) { + std::array pages{}; + std::size_t count = capacity; + const auto address = reinterpret_cast(view); + require(GuestArena::GuestArenaCollectWrites_nid_postfix(address + offset, bytes, pages.data(), &count, clear), "shared subpage collection failed"); + std::vector offsets; + for (std::size_t i = 0; i < count; ++i) offsets.push_back(reinterpret_cast(pages[i]) - address); + return offsets; + }; + const std::vector all{0, page, page * 2, page * 3}; + collect(alias, 0, mapping.size, 4, true); + data[page * 3] = std::byte{92}; + require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe missed a shared write"); + require(collect(mapping.data, 0, page, 4, false) == std::vector{0}, "probe consumed a shared write"); + require(collect(mapping.data, 0, page, 4, true) == std::vector{0}, "prefix reset missed a shared write"); + require(collect(alias, 0, mapping.size, 4, false) == all, "prefix reset consumed an alias write"); + require(collect(mapping.data, page * 3, page, 4, true) == std::vector{page * 3}, "prefix reset consumed a neighboring write"); + require(collect(mapping.data, page * 3, page, 4, false).empty(), "reset subpage remained dirty"); + require(collect(mapping.data, page, page * 2, 4, true) == std::vector{page, page * 2}, "partial resets consumed intervening writes"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "collected subpages remained dirty"); + require(collect(alias, 0, mapping.size, 4, true) == all, "collecting one view consumed an alias write"); + static_cast(alias)[page] = std::byte{93}; + require(collect(mapping.data, page * 2, page, 4, true) == std::vector{page * 2}, "alias write did not invalidate the other view"); + require(collect(mapping.data, 0, mapping.size, 4, true) == std::vector{0, page, page * 3}, "full collect after an interior reset did not preserve pending subpages"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "full collect left pending subpages"); + static_cast(alias)[page * 3] = std::byte{94}; + require(collect(mapping.data, 0, mapping.size, 1, true) == std::vector{0}, "limited collect returned the wrong prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false) == all, "limited collect consumed unreported writes"); + require(collect(mapping.data, page, page * 3, 3, true) == std::vector{page, page * 2, page * 3}, "limited collect continuation lost writes"); + require(collect(mapping.data, 0, page, 1, true) == std::vector{0}, "limited collect consumed its unreset prefix"); + require(collect(mapping.data, 0, mapping.size, 4, false).empty(), "limited collection did not settle"); + require(sceKernelMunmap(alias, mapping.size) == 0, "partial collect alias unmap failed"); +#endif + std::cout << "Partial shared write collection tests passed\n"; +} + template void fails(TAction&& fn) { bool failed = false; try { fn(); } catch (const std::runtime_error&) { failed = true; } @@ -208,7 +261,14 @@ void run() { int main(int argc, char** argv) { try { - if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); + if (argc == 2 && std::string_view(argv[1]) == "--partial-collect") { + if (!WriteWatched()) { + std::cout << "Partial shared write collection skipped: write tracking unavailable\n"; + return 77; + } + partialCollect(); + } + else if (argc == 2 && std::string_view(argv[1]) == "--benchmark") benchmark(); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels") benchmark(true); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-fallback") benchmark(false, false); else if (argc == 2 && std::string_view(argv[1]) == "--benchmark-labels-fallback") benchmark(true, false); diff --git a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp index 9fdc01513..385d2830e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Structurizer.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -104,6 +105,75 @@ void requireExactPostDominators(const ControlFlowGraph& graph, const char* name) } } +void requireDominatedMerges(const ControlFlowGraph& graph, const char* name) { + for (const auto& block : graph.blocks) { + const auto merge = block.terminator.mergeBlock; + if (merge != InvalidControlFlowId && !graph.Dominates(block.id, merge)) { + throw std::runtime_error(std::string(name) + ": header " + std::to_string(block.id) + " does not dominate its merge " + std::to_string(merge)); + } + } +} + +using Routes = std::map; + +std::uint32_t originalBlock(const ControlFlowGraph& graph, std::uint32_t blockId, Routes& routes) { + for (std::size_t step = 0; step <= graph.blocks.size(); ++step) { + const auto& block = graph.FindBlock(blockId); + const auto& terminator = block.terminator; + if (block.instructionBegin != block.instructionEnd) return blockId; + if (terminator.gotoValue >= 0) routes[terminator.gotoVariable] = terminator.gotoValue != 0; + if (terminator.kind == TerminatorKind::Branch) { + blockId = terminator.trueBlock; + } else if (terminator.kind == TerminatorKind::ConditionalBranch && terminator.condition == BranchCondition::GotoVariable && routes.contains(terminator.gotoVariable)) { + blockId = routes.at(terminator.gotoVariable) ? terminator.trueBlock : terminator.falseBlock; + } else { + break; + } + } + throw std::runtime_error("block " + std::to_string(blockId) + " does not lead to an original block"); +} + +void requireOriginalBranches(const ControlFlowGraph& graph, const std::vector>& successors, const char* name) { + std::vector seen(successors.size(), false); + Routes entryRoutes; + std::vector> pending{{originalBlock(graph, graph.entryBlock, entryRoutes), entryRoutes}}; + std::vector> visited; + while (!pending.empty()) { + auto state = std::move(pending.back()); + pending.pop_back(); + if (std::find(visited.begin(), visited.end(), state) != visited.end()) continue; + visited.push_back(state); + const auto& block = graph.FindBlock(state.first); + const auto original = block.instructionBegin / 2; + std::vector branchTargets; + if (block.terminator.kind == TerminatorKind::Branch) branchTargets = {block.terminator.trueBlock}; + if (block.terminator.kind == TerminatorKind::ConditionalBranch) branchTargets = {block.terminator.trueBlock, block.terminator.falseBlock}; + std::vector targets; + for (const auto target : branchTargets) { + auto routes = state.second; + const auto reached = originalBlock(graph, target, routes); + targets.push_back(graph.FindBlock(reached).instructionBegin / 2); + pending.emplace_back(reached, std::move(routes)); + } + if (targets != successors[original]) { + throw std::runtime_error(std::string(name) + ": block " + std::to_string(original) + " no longer branches to its original targets"); + } + seen[original] = true; + } + if (std::find(seen.begin(), seen.end(), false) != seen.end()) { + throw std::runtime_error(std::string(name) + ": an original block is missing"); + } +} + +void requireStructuredLoop(const std::vector>& successors, const char* name) { + auto graph = makeGraph(successors); + Structurizer{}.Structurize(graph); + requireExactPostDominators(graph, name); + requireStructuredBranches(graph, name); + requireDominatedMerges(graph, name); + requireOriginalBranches(graph, successors, name); +} + std::vector> originalPostDominators(const ControlFlowGraph& graph) { std::vector all; for (std::uint32_t id = 0; id < graph.blocks.size(); ++id) all.push_back(id); @@ -210,6 +280,13 @@ void requireOriginalDominanceSets(ControlFlowGraph graph, const char* name) { int main() { try { + auto largeBlocks = makeGraph({{1}, {2, 3}, {1, 4}, {5, 6}, {1, 7}, {8, 7}, {9, 4}, {10}, {6, 10}, {10}, {}}); + for (auto& block : largeBlocks.blocks) block.estimatedSpirvWords = 1000u; + Structurizer{}.Structurize(largeBlocks); + requireStructuredBranches(largeBlocks, "large loop exit blocks retain the baseline result"); + auto exitJunction = makeGraph({{1}, {2, 3}, {9}, {4, 6}, {5, 9}, {1}, {7, 8}, {9}, {1}, {}}); + Structurizer{}.Structurize(exitJunction); + requireStructuredBranches(exitJunction, "shared loop exit junctions retain structured branches"); auto nested = makeGraph({{1}, {2}, {5, 3}, {5, 4}, {7}, {6, 7}, {}, {1}}); Structurizer{}.Structurize(nested); requireExactPostDominators(nested, "nested selections"); @@ -238,17 +315,19 @@ int main() { Structurizer{}.Structurize(innerEndingExit); requireExactPostDominators(innerEndingExit, "an inner loop exit that ends the program"); requireStructuredBranches(innerEndingExit, "an inner loop exit that ends the program"); + requireStructuredLoop({{1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}}, "nested selections in a loop with two latches"); + requireStructuredLoop({{1}, {5, 2}, {5, 3}, {6, 4}, {7, 5}, {8, 1}, {1}, {1}, {}}, "nested selections on the false side in a loop with three latches"); + requireStructuredLoop({{1}, {2, 5}, {3, 4}, {5, 6}, {5}, {1, 7}, {1}, {}}, "an if-else in a loop whose one arm continues"); + requireStructuredLoop({{1}, {2}, {3, 4}, {2, 5}, {}, {1}}, "an inner loop that returns or continues its outer loop"); + requireStructuredLoop({{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}, "an inner loop that leaves both loops or continues its outer loop"); + requireStructuredLoop({{1, 3}, {2, 4}, {3, 1}, {4}, {}}, "a loop whose latch exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {3, 2}, {4}, {4, 1}, {}}, "a loop whose header exits into a block the branch before the loop also reaches"); + requireStructuredLoop({{1, 2}, {4}, {3, 1}, {4, 2}, {}}, "a loop whose header exits into a block the branch before the loop reaches first"); auto earlyReturn = makeGraph({{2, 1}, {4, 2}, {3, 5}, {5}, {}, {}}); Structurizer{}.Structurize(earlyReturn); requireExactPostDominators(earlyReturn, "an early return inside a selection that joins its parent's merge"); requireStructuredBranches(earlyReturn, "an early return inside a selection that joins its parent's merge"); - for (const auto& block : earlyReturn.blocks) { - const auto merge = block.terminator.mergeBlock; - if (merge != InvalidControlFlowId && !earlyReturn.Dominates(block.id, merge)) { - std::fprintf(stderr, "an early return inside a selection: header %u does not dominate its merge %u\n", block.id, merge); - return 1; - } - } + requireDominatedMerges(earlyReturn, "an early return inside a selection that joins its parent's merge"); std::vector> diamonds; for (std::uint32_t diamond = 0; diamond < 50u; ++diamond) { const auto head = diamond * 3u; @@ -260,6 +339,7 @@ int main() { auto chain = makeGraph(diamonds); Structurizer{}.Structurize(chain); requireExactPostDominators(chain, "a chain of fifty selections"); + requireStructuredLoop({{1}, {2, 8}, {3, 5}, {4, 8}, {7}, {6, 7}, {7, 8}, {1}, {}}, "nested selections sharing a loop continuation"); auto sharedDiscard = makeGraph({{5, 1}, {4, 2}, {7, 3}, {7, 4}, {5}, {7, 6}, {}, {}}); const std::vector sharedDiscardWords{444, 1944, 68, 1128, 8, 2084, 148, 28}; for (auto& block : sharedDiscard.blocks) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 10fd4fc9c..da1c9ba0b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -629,6 +629,30 @@ void testProgramWorkgroupUse() { check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry"); } +void testRegisteredVertexWorkgroupRouting() { + using namespace AgcDriver::DriverDetail; + ShaderSnapshot snapshot{0x20000, 0x30000, 2, {0xd8340000u, 0x00000100u, 0xbf810000u, 0xbf810000u}, {}}; + snapshot.header.resize(sizeof(Shader)); + const ShaderRegistry registry{{snapshot.codeAddress, {std::make_shared(std::move(snapshot))}}}; + AgcDriver::QueueState queue{}; + queue.shader = {{0xc8, 0x200}, {0xc9, 0}}; + queue.context[0x2d5] = 0x2000; + check(VertexWorkgroupRequired(queue, registry), "a registered NGG vertex program using LDS did not need a workgroup"); + for (const auto routing : {0u, 0x2020u, 0x2004u, 0x02002000u}) { + queue.context[0x2d5] = routing; + check(!VertexWorkgroupRequired(queue, registry), "legacy, geometry, tessellation or passthrough routing changed to an NGG vertex workgroup"); + } + queue.context[0x2d5] = 0x2000; + auto unaligned = registry; + const auto aligned = unaligned.begin()->second.front(); + ShaderSnapshot shifted{0x1ffff, 0x30000, 2, aligned->code, aligned->header}; + unaligned.clear(); + unaligned[shifted.codeAddress].push_back(std::make_shared(std::move(shifted))); + check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, unaligned)); }).find("not dword aligned") != std::string::npos, "an unaligned registered entry was accepted"); + queue.shader[0xc9] = 0x100; + check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted"); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -722,6 +746,7 @@ int main() { #endif testProgramSnapshots(); testProgramWorkgroupUse(); + testRegisteredVertexWorkgroupRouting(); testEvents(); testValidation(); testClearState(); diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp new file mode 100644 index 000000000..3e852184b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -0,0 +1,123 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +constexpr std::uint32_t MoveM0Two = 0xbefc0382u; +constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; +constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; +constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; +constexpr std::uint32_t MovreldV4V1 = 0x7e088501u; +constexpr std::uint32_t MoveV1V5 = 0x7e020305u; +constexpr std::uint32_t EndProgram = 0xbf810000u; + +struct Shape { + std::size_t functionArrays = 0; + std::uint32_t arrayLength = 0; + std::size_t accessChains = 0; + std::size_t selects = 0; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RecompileProgram(std::span code) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformVote, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses}; + const std::array userData{}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 4u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + Require(!result.spirv.empty(), "the relative-addressing program did not recompile"); + return std::vector(result.spirv.Words().begin(), result.spirv.Words().end()); +} + +Shape Scan(std::span words) { + Shape shape; + std::vector> constants; + std::vector> arrays; + std::vector> functionPointers; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpConstant && count == 4) constants.emplace_back(words[cursor + 2], words[cursor + 3]); + if (op == spv::OpTypeArray) arrays.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.emplace_back(words[cursor + 1], words[cursor + 3]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction) { + for (const auto& [pointer, pointee] : functionPointers) { + if (pointer != words[cursor + 1]) continue; + for (const auto& [array, length] : arrays) { + if (array != pointee) continue; + ++shape.functionArrays; + for (const auto& [constant, value] : constants) { + if (constant == length) shape.arrayLength = value; + } + } + } + } + if (op == spv::OpAccessChain) ++shape.accessChains; + if (op == spv::OpSelect) ++shape.selects; + cursor += count; + } + return shape; +} + +bool run(const char* name, bool (*test)()) { + try { + if (test()) return true; + std::fprintf(stderr, "%s\n", name); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s: %s\n", name, error.what()); + } + return false; +} + +} + +int main() { + bool passed = true; + + passed &= run("a relative read and write index a function-scope window instead of selecting over the registers", [] { + const std::array code{MoveM0Two, MoveV4One, MoveV5Two, MoveV6Four, MovrelsV1V4, MovreldV4V1, MoveV1V5, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + if (shape.functionArrays != 1 || shape.arrayLength != 3 || shape.accessChains < 2 || shape.selects > 4) std::fprintf(stderr, " window shape: arrays %zu length %u access chains %zu selects %zu\n", shape.functionArrays, shape.arrayLength, shape.accessChains, shape.selects); + return shape.functionArrays == 1 && shape.arrayLength == 3 && shape.accessChains >= 2 && shape.selects <= 4; + }); + + passed &= run("a program without relative addressing keeps its registers in SSA values", [] { + const std::array code{MoveV4One, MoveV5Two, MoveV6Four, MoveV1V5, EndProgram}; + const auto shape = Scan(RecompileProgram(code)); + return shape.functionArrays == 0 && shape.accessChains == 0; + }); + + return passed ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp new file mode 100644 index 000000000..f362f67d6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Vop3InterpDecode.cpp @@ -0,0 +1,108 @@ +#include "RdnaDecoder/RdnaInstructionDecoder.hpp" +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct Accepted { + std::vector words; + RdnaOpcode op; + const char* name; + RdnaOperandKind source0Kind; + std::uint32_t source0Value; + std::uint32_t attribute; + std::uint32_t channel; + bool absolute; + bool negate; + bool clamp; + std::uint32_t omod; +}; + +struct Refused { + std::vector words; + const char* reason; + const char* name; +}; + +} + +int main() { + constexpr std::uint32_t Destination = 5; + const std::vector accepted{ + {{0xd6000005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 0u}, + {{0xd6010005u, 0x02020440u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, v2, attr0.y", RdnaOperandKind::VectorRegister, 2u, 0u, 1u, false, false, false, 0u}, + {{0xd6020005u, 0x02000200u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p20, attr0.x", RdnaOperandKind::IntegerInlineConstant, 1u, 0u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0202041fu}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr31.x", RdnaOperandKind::VectorRegister, 2u, 31u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x02020420u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr32.x", RdnaOperandKind::VectorRegister, 2u, 32u, 0u, false, false, false, 0u}, + {{0xd6000005u, 0x0203fe00u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v255, attr0.x", RdnaOperandKind::VectorRegister, 255u, 0u, 0u, false, false, false, 0u}, + {{0xd6000205u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, |v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, false, 0u}, + {{0xd6000005u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -v2, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, true, false, 0u}, + {{0xd6008005u, 0x02020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, true, 0u}, + {{0xd6000005u, 0x0a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 1u}, + {{0xd6000005u, 0x12020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x mul:4", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 2u}, + {{0xd6000005u, 0x1a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, v2, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, false, false, false, 3u}, + {{0xd6000205u, 0x42020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 0u}, + {{0xd6000205u, 0x5a020400u}, RdnaOpcode::VInterpP1F32, "v_interp_p1_f32_e64 v5, -|v2|, attr0.x div:2", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, true, false, 3u}, + {{0xd6010005u, 0x42020481u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, -v2, attr1.z", RdnaOperandKind::VectorRegister, 2u, 1u, 2u, false, true, false, 0u}, + {{0xd6018205u, 0x02020400u}, RdnaOpcode::VInterpP2F32, "v_interp_p2_f32_e64 v5, |v2|, attr0.x clamp", RdnaOperandKind::VectorRegister, 2u, 0u, 0u, true, false, true, 0u}, + {{0xd6028005u, 0x02000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x clamp", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, true, 0u}, + {{0xd6020005u, 0x0a000000u}, RdnaOpcode::VInterpMovF32, "v_interp_mov_f32_e64 v5, p10, attr0.x mul:2", RdnaOperandKind::IntegerInlineConstant, 0u, 0u, 0u, false, false, false, 1u}, + }; + const std::vector refused{ + {{0xd6000005u, 0x02000400u}, "not a vector register", "v_interp_p1_f32_e64 with a non-VGPR coordinate"}, + {{0xd6020005u, 0x02000600u}, "not an interpolation parameter", "v_interp_mov_f32_e64 with a reserved parameter"}, + {{0xd6000005u, 0x02060400u}, "third source is not the inline constant 0", "v_interp_p1_f32_e64 with a third source other than the inline constant 0"}, + {{0xd6000805u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with op_sel"}, + {{0xd6000005u, 0x02020500u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with the packed-half selector"}, + {{0xd6000105u, 0x02020400u}, "modifiers are not implemented", "v_interp_p1_f32_e64 with an absolute source 0"}, + {{0xd6020005u, 0x22000200u}, "modifiers are not implemented", "v_interp_mov_f32_e64 with a negated parameter"}, + }; + int failures = 0; + for (const auto& test : accepted) { + auto code = test.words; + code.push_back(0xbf810000u); + try { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + if (decoded.instructions.empty()) { + std::fprintf(stderr, "%s decoded no instruction\n", test.name); + ++failures; + continue; + } + const auto& instruction = decoded.instructions[0]; + const auto source0Value = test.source0Kind == RdnaOperandKind::VectorRegister ? instruction.source0.reg : instruction.source0.value; + if (instruction.op != test.op || instruction.wordCount != test.words.size() || instruction.family != RdnaInstructionFamily::VOP3 || + instruction.destination.kind != RdnaOperandKind::VectorRegister || instruction.destination.reg != Destination || + instruction.destination.clamp != test.clamp || instruction.destination.omod != test.omod || instruction.sourceCount != 3u || + instruction.source0.kind != test.source0Kind || source0Value != test.source0Value || + instruction.source0.absolute != test.absolute || instruction.source0.negate != test.negate || + instruction.source1.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source1.value != test.attribute || + instruction.source2.kind != RdnaOperandKind::IntegerInlineConstant || instruction.source2.value != test.channel) { + std::fprintf(stderr, "%s decoded to the wrong opcode or operands\n", test.name); + ++failures; + } + } catch (const std::exception& error) { + std::fprintf(stderr, "%s was refused: %s\n", test.name, error.what()); + ++failures; + } + } + for (const auto& test : refused) { + auto code = test.words; + code.push_back(0xbf810000u); + std::string refusal; + try { + static_cast(RdnaInstructionDecoder{}.Decode(code)); + } catch (const std::exception& error) { + refusal = error.what(); + } + if (refusal.find(test.reason) == std::string::npos) { + std::fprintf(stderr, "%s was not refused\n", test.name); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp index 5bfd23f7f..72c88cbc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WaitShutdown.cpp @@ -1,9 +1,11 @@ #include "prx/libSceAgcDriver/Submit/include/Acb.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libc/include/Shutdown.hpp" #include #include #include +#include #include #include #include @@ -22,6 +24,10 @@ void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } +void DelayExit() { + std::this_thread::sleep_for(std::chrono::seconds(1)); +} + std::array WriteData(volatile std::uint32_t* destination) { const auto address = static_cast(reinterpret_cast(destination)); return {0xc0033700, 0x00100200, static_cast(address), static_cast(address >> 32u), 1}; @@ -59,7 +65,7 @@ void CheckShutdownCancellation() { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::seconds(1)); for (const auto& state : states) { Require(state.label == 0, "unsatisfied label was unexpectedly signalled"); Require(state.after == 0 && state.queued == 0, "unsatisfied wait completed before shutdown"); @@ -77,7 +83,14 @@ void CheckShutdownCancellation() { } -int main() { +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "--resolution-shutdown") == 0) { + AgcDriverWaitIdle_nid_postfix(); + LibcRegisterShutdown_nid_postfix(DelayExit); + LibcRequestExit_nid_postfix(0); + AgcDriverResolveShaderAbi_nid_postfix(nullptr, {}, {}); + return 1; + } try { CheckShutdownCancellation(); std::puts("unsatisfied label waits cancelled cleanly at shutdown"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp new file mode 100644 index 000000000..53887c1b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ArrayLayerTarget.cpp @@ -0,0 +1,289 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +using AgcDriver::Graphics::TextureDimension; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Side = 128; +constexpr std::uint32_t Layers = 4; +constexpr std::size_t LayerBytes = 65536; +constexpr std::size_t SurfaceBytes = LayerBytes * Layers; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2DArray = 13; +constexpr std::uint32_t Color32FloatInfo = (4u << 2u) | (7u << 8u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Initial = 0x5a5a5a5au; +constexpr std::uint32_t ScissorWidth = 16; +constexpr float Drawn = 0.25f; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, Layers> LoadCodes{}; +alignas(256) std::array PixelCode{}; +alignas(256) std::array Output{}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, LayerBytes)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + LayerBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + LayerBytes - 1) & ~(static_cast(LayerBytes) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + LayerBytes) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + LayerBytes - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "array layer target: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); +#else + munmap(block, SurfaceBytes); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, SurfaceBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +std::array ArrayDescriptor(std::uint64_t address) { + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2DArray << 28u), + Layers - 1u, + 0u, + 0u, + 0u, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = AddressOf(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::span code, const std::vector& userData) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void LoadLayer(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t layer) { + Output.fill(0xdeadbeefu); + auto& code = LoadCodes[layer]; + code = {0x7e020280, 0x7e040280u | layer, 0xf0001128, 0x00010300, 0x34080082, 0xbf8c3f70, 0xe0701000, 0x80000304, 0xbf810000}; + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + const auto texture = ArrayDescriptor(surface); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + Dispatch(device, code, userData); +} + +void RequireOutput(std::uint32_t inside, std::uint32_t outside, const std::string& what) { + for (std::uint32_t x = 0; x < Threads; ++x) { + const auto expected = x < ScissorWidth ? inside : outside; + Require(Output[x] == expected, what + ": texel (" + std::to_string(x) + ", 0) reads " + Hex(Output[x]) + ", expected " + Hex(expected)); + } +} + +AgcDriver::Graphics::ColorTarget DecodeTarget(std::uint64_t address, std::uint32_t slice) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(address >> 8u); + cx[0x31b] = slice | (slice << 13u); + cx[0x31c] = Color32FloatInfo; + cx[0x31d] = 0; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3 | (Layers - 1u); + cx[0x390] = static_cast(address >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R32_SFLOAT && color.bytes == LayerBytes && color.address == address + slice * LayerBytes, "the COLOR_32 float view did not decode as a 128x128 R32 slice of the array"); + Require(color.arrayAddress == address && color.arrayLastSlice == Layers - 1u && color.arraySlice == slice, "the color view did not keep its array and slice"); + return color; +} + +void DrawSlice(AgcDriver::VulkanDevice& device, std::uint64_t surface, std::uint32_t slice) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::size_t at = 0; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + PixelCode[at++] = 0x7e0002ffu | ((4u + channel) << 17u); + PixelCode[at++] = std::bit_cast(Drawn); + } + PixelCode[at++] = 0xf800180fu; + PixelCode[at++] = 0x07060504u; + PixelCode[at++] = 0xbf810000u; + const std::span pixelCode(PixelCode.data(), at); + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const auto color = DecodeTarget(surface, slice); + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + const auto surface = AddressOf(texels); + std::fill_n(reinterpret_cast(texels), SurfaceBytes / 4u, Initial); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const auto drawn = std::bit_cast(Drawn); + DrawSlice(device, surface, 1); + const auto pending = StorageTexture::FindPending(surface, SurfaceBytes); + Require(pending != nullptr && pending->Descriptor().dimension == TextureDimension::k2DArray && pending->Descriptor().baseAddress == surface && pending->Descriptor().depthOrLastArray == Layers - 1u, "the draw into slice 1 did not render into the image of its array"); + LoadLayer(device, surface, 1); + RequireOutput(drawn, Initial, "slice 1 read through the array after the draw"); + for (const std::uint32_t other : {0u, 2u, 3u}) { + LoadLayer(device, surface, other); + RequireOutput(Initial, Initial, "slice " + std::to_string(other) + " after the draw into slice 1"); + } + StorageTexture::FlushPending(surface, SurfaceBytes, nullptr, "test"); + device.WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); + AgcDriver::GuestMemory::CollectWrites(surface, SurfaceBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + LoadLayer(device, surface, 1); + RequireOutput(drawn, Initial, "slice 1 uploaded again from guest memory"); + AgcDriver::Graphics::ClearCachedTextures(device.Device()); +} + +} + +int main() { + try { + GuestBlock block; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, block.Data()); + std::puts("array layer target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp new file mode 100644 index 000000000..af31f0874 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Dpp8PkFmac.cpp @@ -0,0 +1,113 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Stride = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +std::array Code(std::uint32_t selectors) { + const auto dpp8 = [&](std::uint32_t source) { return (selectors << 8u) | source; }; + return { + 0x34080084u, + 0xe03c1000u, 0x80000104u, + 0xbf8c3f70u, + 0x7e140301u, + 0x781406e9u, dpp8(2u), + 0x7e1a02e9u, dpp8(2u), + 0x7e160301u, + 0x7816070du, + 0x7e180301u, + 0x78180702u, + 0xe07c1000u, 0x80010a04u, + 0xbf810000u, + }; +} + +std::uint32_t Selectors(const std::array& lanes) { + std::uint32_t value = 0; + for (std::uint32_t lane = 0; lane < 8u; ++lane) value |= lanes[lane] << (lane * 3u); + return value; +} + +std::uint16_t Half(std::uint32_t seed) { + const auto sign = (seed >> 15u) & 1u; + const auto exponent = 10u + seed % 10u; + const auto mantissa = (seed >> 4u) & 0x3ffu; + return static_cast((sign << 15u) | (exponent << 10u) | mantissa); +} + +std::array BufferDescriptor(const void* data, std::size_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(bytes), 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t wave, const std::array& lanes, const std::string& what) { + for (std::uint32_t lane = 0; lane < MaxThreads; ++lane) { + for (std::uint32_t word = 0; word < 3u; ++word) { + const auto seed = lane * 2654435761u + word * 40503u + 12345u; + Input[lane * Stride + word] = static_cast(Half(seed)) | (static_cast(Half(seed * 7u + 3u)) << 16u); + } + } + Output.fill(0xdeadbeefu); + const auto code = Code(Selectors(lanes)); + std::vector userData; + for (const auto& descriptor : {BufferDescriptor(Input.data(), sizeof(Input)), BufferDescriptor(Output.data(), sizeof(Output))}) { + userData.insert(userData.end(), descriptor.begin(), descriptor.end()); + } + const std::span program(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(program)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{wave, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), program, 0, {}}, + {wave, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(wave), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + bool permuted = false; + for (std::uint32_t lane = 0; lane < wave; ++lane) { + const auto fused = Output[lane * Stride]; + const auto separate = Output[lane * Stride + 1u]; + const auto plain = Output[lane * Stride + 2u]; + Require(fused == separate, what + " (wave" + std::to_string(wave) + "): lane " + std::to_string(lane) + " v_pk_fmac_f16 dpp8 gave " + std::to_string(fused) + + ", v_mov_b32 dpp8 then v_pk_fmac_f16 gave " + std::to_string(separate)); + permuted = permuted || fused != plain; + } + Require(permuted, what + " (wave" + std::to_string(wave) + "): the dpp8 result equals the unpermuted one in every lane"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t wave : {32u, 64u}) { + Run(*device, wave, {7, 6, 5, 4, 3, 2, 1, 0}, "reversed groups of eight"); + Run(*device, wave, {1, 0, 3, 2, 5, 4, 7, 6}, "swapped neighbours"); + } + std::puts("dpp8 v_pk_fmac_f16 tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index 8a4da6082..cc3452070 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -236,10 +237,11 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false) { +std::uint64_t Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false, bool useCache = false, bool array = false, std::uint64_t expectedVariant = 0) { Output.fill(-1.0f); const std::span code = offsets ? std::span(OffsetCode) : array ? std::span(ArrayCode) : std::span(Code); const auto result = Compile(device, Format8888UNorm, sampler, code, useCache); + if (expectedVariant != 0) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); for (const auto& binding : result.bindings) { Require(binding.role != ShaderRecompiler::DescriptorRole::GuestSamplers, "emulated comparison retained a sampler binding"); Require(std::all_of(binding.imageSamplers.begin(), binding.imageSamplers.end(), [](auto mask) { return mask == 0u; }), "emulated comparison retained an unused sampler association"); @@ -251,6 +253,7 @@ void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* na const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f; Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); } + return result.variantId; } constexpr std::uint32_t Select(std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint32_t w) { @@ -306,9 +309,28 @@ bool BindsDepthCompare(const ShaderRecompiler::RecompileResult& result) { }); } -void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { +void RunSrgbCubeZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name) { + FillCubeTexels(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + CubeInput[tid * 4u + 0u] = 0.5f; + CubeInput[tid * 4u + 1u] = 1.0f; + CubeInput[tid * 4u + 2u] = 0.0f; + CubeInput[tid * 4u + 3u] = 0.0f; + } Output.fill(-1.0f); - const auto result = Compile(device, format, sampler); + const auto result = Compile(device, format, sampler, CubeCode, false, true, true); + Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(CubeCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Require(Output[tid] == 0.0f, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected 0"); + } +} + +void RunSrgbZero(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, const char* name, bool useCache, std::uint64_t expectedVariant) { + Output.fill(-1.0f); + const auto result = Compile(device, format, sampler, Code, useCache); + if (useCache) Require(result.cacheHit && result.variantId == expectedVariant, std::string(name) + " did not reuse the compiled comparison artifact"); Require(!BindsDepthCompare(result), std::string(name) + " kept a depth-compare binding"); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); @@ -333,10 +355,18 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + const auto* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::string_view(forced) == "1") Require((device->Target().srgbDecodeFormats & 3u) == 3u, "forced R8 and RG8 sRGB shader decoding is unavailable"); FillTexels(); FillInput(); RunCube(*device, {ClampEdge, FilterPoint}, "point cube, clamp to edge"); Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear}, "bilinear cube comparison requires DISABLE_CUBE_WRAP", CubeCode, true, true); + for (std::uint32_t function = 0; function < 8u; ++function) { + const auto name = "bilinear 8_8_8_8 srgb cube function " + std::to_string(function); + RunSrgbCubeZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, name.c_str()); + } + RunSrgbCubeZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb cube without DISABLE_CUBE_WRAP"); + RunSrgbCubeZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb cube without DISABLE_CUBE_WRAP"); RunCube(*device, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, true}, "bilinear cube with DISABLE_CUBE_WRAP"); Reject(*device, Format9995Float, {ClampEdge, FilterPoint}, "implemented only for float, unorm and snorm formats"); Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear"); @@ -344,16 +374,19 @@ int main() { FillTexels(); Require(BindsDepthCompare(Compile(*device, Format32Float, {ClampEdge, FilterBilinear})), "an R32 float texture left the native comparison path"); Require(!BindsDepthCompare(Compile(*device, Format8888UNorm, {ClampEdge, FilterBilinear})), "a color texture kept a depth-compare binding"); - for (std::uint32_t function = 0; function < 8u; ++function) { - const auto point = "point 8_8_8_8 srgb function " + std::to_string(function); - const auto bilinear = "bilinear 8_8_8_8 srgb function " + std::to_string(function); - RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterPoint, BorderBlack, 0u, 0u, false, function}, point.c_str()); - RunSrgbZero(*device, Format8888Srgb, {ClampEdge, FilterBilinear, BorderBlack, 0u, 0u, false, function}, bilinear.c_str()); + for (const bool useCache : {false, true}) { + const auto variant = useCache ? Run(*device, {ClampEdge, FilterPoint}, "cached UNORM before sRGB", false, true) : 0u; + if (useCache) Require(variant != 0, "the cached comparison has no compiled artifact identity"); + for (const auto format : {Format8Srgb, Format8_8Srgb, Format8888Srgb}) { + for (std::uint32_t function = 0; function < 8u; ++function) { + for (const auto filter : {FilterPoint, FilterBilinear}) { + const auto name = std::string(useCache ? "cached" : "uncached") + " sRGB format " + std::to_string(format) + " function " + std::to_string(function) + (filter == FilterPoint ? " point" : " bilinear"); + RunSrgbZero(*device, format, {ClampEdge, filter, BorderBlack, 0u, 0u, false, function}, name.c_str(), useCache, variant); + } + } + } + if (useCache) Run(*device, {ClampEdge, FilterPoint}, "cached UNORM after sRGB", false, true, false, variant); } - RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterPoint}, "point 8 srgb"); - RunSrgbZero(*device, Format8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8 srgb"); - RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterPoint}, "point 8_8 srgb"); - RunSrgbZero(*device, Format8_8Srgb, {ClampEdge, FilterBilinear}, "bilinear 8_8 srgb"); Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap"); Run(*device, {ClampWrap, FilterPoint}, "point, wrap with negative offsets", true); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index b089628a9..374eaa63a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -23,6 +23,8 @@ constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; constexpr std::uint32_t Results = 19; constexpr std::uint32_t TinyResults = 16; +constexpr std::uint32_t TinyProductLane = 20; +constexpr std::uint32_t SplitTinyProductSum = 0x00800000u; alignas(256) std::array Input{}; alignas(256) std::array Output{}; @@ -411,9 +413,15 @@ void Run(AgcDriver::VulkanDevice& device, const std::optional= 15u; if (floatResult && IsNan(Expected[tid][i])) { Require(IsNan(out[i]), std::string("f32 denormal flush: lane ") + std::to_string(tid) + " " + mode + " " + Names[i] + " is " + Hex(out[i]) + ", expected a NaN"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp index c404a90f9..72f895af1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlatApertures.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -497,9 +498,10 @@ int main(int argc, char** argv) { const bool int64Atomics = TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics); GuestBlock guest; std::size_t checked = 0; - const auto rows = Rows(); - for (std::size_t index = part; index < rows.size(); index += parts) { - const Row& row = rows[index]; + std::map, std::size_t> shaders; + for (const Row& row : Rows()) { + const auto shader = shaders.emplace(std::pair{Kernel(row), row.waveSize}, shaders.size()).first->second; + if (shader % parts != part) continue; if (row.waveSize == 64u && !wave64) continue; if (row.atomic && row.width == 64u && !int64Atomics) continue; Run(*device, row, guest); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp new file mode 100644 index 000000000..8c6b30f64 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16MinMaxBits.cpp @@ -0,0 +1,305 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Stride = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ScalarCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0x74140b04, + 0x72160b04, + 0xd539810c, 0x20020d04, + 0xd53a800d, 0x40020d05, + 0xd751000e, 0x041a0b04, + 0xd754280f, 0x441a0b04, + 0xd7570010, 0x041a0b04, + 0xd7579111, 0x841e0b04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +alignas(256) constexpr std::array PackedCode{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc11400a, 0x18020b04, + 0xcc12400b, 0x18020b04, + 0xcc12ca0c, 0x30020d04, + 0xcc11c00d, 0x58020f05, + 0xe0781000, 0x80010a03, 0xbf810000, +}; + +enum class Kind { Min, Max, Min3, Max3, Med3 }; + +struct Source { + std::uint32_t reg; + bool opSel = false; + bool opSelHi = true; + bool absolute = false; + bool negate = false; + bool negateHi = false; +}; + +struct Check { + const char* name; + Kind kind; + std::array sources; + bool clamp = false; +}; + +const std::array ScalarChecks{{ + {"v_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_max_f16 v12, -|v4|, v6 clamp", Kind::Max, {{{0u, false, true, true, true}, {2u}, {}}}, true}, + {"v_min_f16 v13, v5, -v6 clamp", Kind::Min, {{{1u}, {2u, false, true, false, true}, {}}}, true}, + {"v_min3_f16 v14, v4, v5, v6", Kind::Min3, {{{0u}, {1u}, {2u}}}}, + {"v_max3_f16 v15, v4, -v5, v6 op_sel:[1,0,1,0]", Kind::Max3, {{{0u, true}, {1u, false, true, false, true}, {2u, true}}}}, + {"v_med3_f16 v16, v4, v5, v6", Kind::Med3, {{{0u}, {1u}, {2u}}}}, + {"v_med3_f16 v17, |v4|, v5, -v7 op_sel:[0,1,0,0] clamp", Kind::Med3, {{{0u, false, true, true}, {1u, true}, {3u, false, true, false, true}}}, true}, +}}; + +const std::array PackedChecks{{ + {"v_pk_min_f16 v10, v4, v5", Kind::Min, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v11, v4, v5", Kind::Max, {{{0u}, {1u}, {}}}}, + {"v_pk_max_f16 v12, v4, v6 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1] clamp", Kind::Max, {{{0u, true, false, false, true, false}, {2u, false, true, false, false, true}, {}}}, true}, + {"v_pk_min_f16 v13, v5, v7 neg_lo:[0,1] clamp", Kind::Min, {{{1u}, {3u, false, true, false, true}, {}}}, true}, +}}; + +struct Mode { + const char* name; + std::optional floatMode; + bool packed = true; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, + {"f16 denormals flushed, dx10 ieee", ShaderRecompiler::ShaderFloatMode{0x30u, true, true, false}, false}, + {"f16 input denormals flushed", ShaderRecompiler::ShaderFloatMode{0xb0u, false, false, false}, false}, +}}; + +std::uint64_t Seed = 0x2545f4914f6cdd1dull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 9u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | 0x3c00u | (bits & 0x3u); + default: return bits & 0xffffu; + } +} + +bool IsNan(std::uint32_t bits) { return (bits & 0x7fffu) > 0x7c00u; } + +bool IsSnan(std::uint32_t bits) { return IsNan(bits) && (bits & 0x0200u) == 0u; } + +double Value(std::uint32_t bits) { + const double sign = (bits & 0x8000u) != 0u ? -1.0 : 1.0; + const std::uint32_t exponent = (bits >> 10u) & 0x1fu; + const std::uint32_t mantissa = bits & 0x3ffu; + if (exponent == 0u) return sign * std::ldexp(static_cast(mantissa), -24); + if (exponent == 31u) return mantissa != 0u ? std::nan("") : sign * INFINITY; + return sign * std::ldexp(static_cast(mantissa | 0x400u), static_cast(exponent) - 25); +} + +struct Model { + bool ieee; + bool dx10; + bool flushInputs; + + struct Half { + double value; + std::uint32_t bits; + }; + + std::uint32_t Quiet(std::uint32_t bits) const { return ieee ? bits | 0x0200u : bits; } + + Half Numeric(bool maxValue, const Half& lhs, const Half& rhs) const { + const bool ordered = maxValue ? lhs.value >= rhs.value : lhs.value < rhs.value; + const bool bothZero = ((lhs.bits | rhs.bits) & 0x7fffu) == 0u; + const std::uint32_t zero = maxValue ? lhs.bits & rhs.bits : lhs.bits | rhs.bits; + return {ordered ? lhs.value : rhs.value, bothZero ? zero : (ordered ? lhs.bits : rhs.bits)}; + } + + Half Pair(bool maxValue, const Half& lhs, const Half& rhs) const { + const Half numeric = Numeric(maxValue, lhs, rhs); + std::uint32_t bits = IsNan(lhs.bits) ? rhs.bits : numeric.bits; + bits = IsNan(rhs.bits) ? lhs.bits : bits; + if (ieee) { + bits = IsSnan(rhs.bits) ? Quiet(rhs.bits) : bits; + bits = IsSnan(lhs.bits) ? Quiet(lhs.bits) : bits; + } + return {IsNan(rhs.bits) ? lhs.value : numeric.value, bits}; + } + + std::uint32_t Clamp(bool clamp, std::uint32_t bits) const { + if (!clamp) return bits; + const std::uint32_t magnitude = bits & 0x7fffu; + const bool zero = bits > 0x7fffu || magnitude > 0x7c00u; + const std::uint32_t clamped = zero ? 0u : (magnitude > 0x3c00u ? 0x3c00u : bits); + return dx10 || magnitude <= 0x7c00u ? clamped : bits; + } + + Half Read(std::uint32_t bits) const { + if (flushInputs && (bits & 0x7fffu) < 0x0400u) bits &= 0x8000u; + return {Value(bits), bits}; + } + + std::uint32_t Scalar(const Check& check, const std::uint32_t* registers) const { + std::array args{}; + for (std::uint32_t index = 0; index < 3u; ++index) { + const Source& source = check.sources[index]; + std::uint32_t bits = (registers[source.reg] >> (source.opSel ? 16u : 0u)) & 0xffffu; + if (source.absolute) bits &= 0x7fffu; + if (source.negate) bits ^= 0x8000u; + args[index] = Read(bits); + } + std::uint32_t result = 0u; + switch (check.kind) { + case Kind::Min: result = Pair(false, args[0], args[1]).bits; break; + case Kind::Max: result = Pair(true, args[0], args[1]).bits; break; + case Kind::Min3: result = Pair(false, Pair(false, args[0], args[1]), args[2]).bits; break; + case Kind::Max3: result = Pair(true, Pair(true, args[0], args[1]), args[2]).bits; break; + case Kind::Med3: { + const bool anyNan = IsNan(args[0].bits) || IsNan(args[1].bits) || IsNan(args[2].bits); + const Half minimum = Pair(false, Pair(false, args[0], args[1]), args[2]); + const Half median = Numeric(true, Numeric(false, args[0], args[1]), Numeric(false, Numeric(true, args[0], args[1]), args[2])); + result = anyNan ? minimum.bits : median.bits; + break; + } + } + return Clamp(check.clamp, result); + } + + std::uint32_t Packed(const Check& check, const std::uint32_t* registers) const { + std::uint32_t result = 0u; + for (std::uint32_t high = 0; high < 2u; ++high) { + std::array bits{}; + for (std::uint32_t index = 0; index < 2u; ++index) { + const Source& source = check.sources[index]; + const bool selectHigh = high != 0u ? source.opSelHi : source.opSel; + bits[index] = (registers[source.reg] >> (selectHigh ? 16u : 0u)) & 0xffffu; + if (high != 0u ? source.negateHi : source.negate) bits[index] ^= 0x8000u; + } + std::uint32_t lane = Numeric(check.kind == Kind::Max, Read(bits[0]), Read(bits[1])).bits; + lane = IsNan(bits[0]) ? bits[1] : lane; + lane = IsNan(bits[1]) ? bits[0] : lane; + if (ieee) { + lane = IsSnan(bits[1]) ? Quiet(bits[1]) : lane; + lane = IsSnan(bits[0]) ? Quiet(bits[0]) : lane; + } + result |= Clamp(check.clamp, lane) << (16u * high); + } + return result; + } +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](std::span code, const Mode& mode) { + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + std::uint64_t compared = 0u; + for (const auto& mode : Modes) { + const bool flushInputs = mode.floatMode.has_value() && (mode.floatMode->floatMode & 0x40u) == 0u; + const Model model{mode.floatMode.has_value() && mode.floatMode->ieeeMode, !mode.floatMode.has_value() || mode.floatMode->dx10Clamp, flushInputs}; + const auto scalar = compile(ScalarCode, mode); + std::optional packed; + if (mode.packed) packed = compile(PackedCode, mode); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t* row = &Input[tid * Inputs]; + for (std::uint32_t index = 0; index < Inputs; ++index) row[index] = Half() | (Half() << 16u); + if (tid % 8u == 0u) row[1] = row[0]; + if (tid % 8u == 1u) row[1] = row[0] ^ 0x80008000u; + if (tid % 8u == 2u) row[2] = row[1]; + if (tid % 8u == 3u) row[2] = row[0] ^ 0x80008000u; + } + const auto run = [&](const ShaderRecompiler::RecompileResult& result, std::span code, std::span checks, bool isPacked) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* row = &Input[tid * Inputs]; + for (std::uint32_t index = 0; index < checks.size(); ++index) { + const std::uint32_t actual = Output[tid * Stride + index]; + const std::uint32_t expected = isPacked ? model.Packed(checks[index], row) : model.Scalar(checks[index], row); + Require(actual == expected, std::string("f16 min/max: ") + mode.name + ", " + checks[index].name + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + ++compared; + } + } + }; + run(scalar, ScalarCode, ScalarChecks, false); + if (packed) run(*packed, PackedCode, PackedChecks, true); + } + } + std::printf("f16 min/max tests passed (%llu results)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp index fe73e2b3a..8fe09da45 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GatedDispatchCapture.cpp @@ -23,6 +23,7 @@ alignas(256) std::array ReaderCode{0xf4040100u, 0xfa000000u, constexpr std::uint32_t StaleValue = 0x57a1e000u; constexpr std::uint32_t FreshValue = 0xf7e54000u; +constexpr std::uint32_t SpinIterationsPerDispatch = 1u << 20u; struct Slot { alignas(256) std::array stale{}; @@ -81,14 +82,14 @@ void Submit(std::uint32_t queue, std::vector& words) { enum class Outcome { Fresh, Stale, Inconclusive }; -Outcome Run(Slot& slot, std::uint32_t iterations) { +Outcome Run(Slot& slot, std::uint32_t spinDispatches) { slot.stale[0] = StaleValue; slot.fresh[0] = FreshValue; slot.table = {Low(slot.stale.data()), High(slot.stale.data())}; slot.outputDescriptor = Descriptor(slot.output.data(), sizeof(slot.output)); slot.spinDescriptor = Descriptor(slot.spinOutput.data(), sizeof(slot.spinOutput)); std::vector producer; - Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), iterations}); + for (std::uint32_t index = 0; index < spinDispatches; ++index) Dispatch(producer, SpinCode.data(), {Low(slot.spinDescriptor.data()), High(slot.spinDescriptor.data()), SpinIterationsPerDispatch}); WriteData(producer, &slot.label, 1); std::vector consumer; WaitEqual(consumer, &slot.label, 1); @@ -118,16 +119,16 @@ int main() { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); - std::uint32_t iterations = 1u << 26u; + std::uint32_t spinDispatches = (1u << 26u) / SpinIterationsPerDispatch; for (auto& slot : Slots) { - const auto outcome = Run(slot, iterations); + const auto outcome = Run(slot, spinDispatches); Require(outcome != Outcome::Stale, "a dispatch behind a cross-queue WAIT_REG_MEM read its table before the producer's label reached memory, so it missed the table the CPU filled after submitting"); if (outcome == Outcome::Fresh) { - std::cout << "gated dispatch capture test passed (" << iterations << " producer iterations)\n"; + std::cout << "gated dispatch capture test passed (" << spinDispatches << " producer dispatches of " << SpinIterationsPerDispatch << " iterations)\n"; AgcDriverShutdown_nid_postfix(); return 0; } - iterations *= 2u; + spinDispatches *= 2u; } std::cout << "the producer's label always reached memory before the table was filled: gated dispatch capture not tested\n"; AgcDriverShutdown_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp index cc39b8471..28a89a1f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLineImplicitLod.cpp @@ -32,8 +32,8 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; -alignas(256) constexpr std::array PixelCode{ - 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, +alignas(256) constexpr std::array PixelCode{ + 0xbefe0a7e, 0x10040010, 0x06060300, 0x10060611, 0xf0800f08, 0x00610402, 0xbf8c3f70, 0xf800180f, 0x07060504, 0xbf810000, }; alignas(256) constexpr std::array BiasPixelCode{ diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index 965994506..e106d7cfa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -27,6 +27,8 @@ constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; +constexpr std::uint32_t FilterAnisoBilinear = 3; +constexpr std::uint32_t AnisoOverride = 1u << 29u; constexpr std::uint32_t ForceDegamma = 1u << 20u; alignas(256) std::array Input{}; @@ -57,8 +59,8 @@ std::array TextureDescriptor(std::uint32_t format) { }; } -std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced) { - return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +std::array SamplerDescriptor(std::uint32_t filter, std::uint32_t forced, std::uint32_t filters) { + return {0x92u | forced, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u) | filters, 0u}; } std::uint8_t Code8(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { @@ -92,12 +94,12 @@ float SrgbToLinear(std::uint8_t code) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); } -std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced) { +std::array Sample(AgcDriver::VulkanDevice& device, std::uint32_t format, std::uint32_t filter, std::uint32_t forced, std::uint32_t filters = 0u) { std::vector userData(20, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); const auto texture = TextureDescriptor(format); - const auto sampler = SamplerDescriptor(filter, forced); + const auto sampler = SamplerDescriptor(filter, forced, filters); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); @@ -118,9 +120,9 @@ std::array Sample(AgcDriver::VulkanDevice& device, std::uint return Output; } -void RequireSame(const std::array& forced, const std::array& plain, const char* name) { +void RequireSame(const std::array& forced, const std::array& plain, const char* name, const char* first = "through FORCE_DEGAMMA", const char* second = "without it") { for (std::uint32_t index = 0; index < forced.size(); ++index) { - Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " through FORCE_DEGAMMA, " + std::to_string(plain[index]) + " without it"); + Require(std::bit_cast(forced[index]) == std::bit_cast(plain[index]), std::string(name) + ": thread " + std::to_string(index / 4u) + " component " + std::to_string(index % 4u) + " sampled " + std::to_string(forced[index]) + " " + first + ", " + std::to_string(plain[index]) + " " + second); } } @@ -161,6 +163,7 @@ int main() { FillInput(false); const auto forcedBilinear = Sample(*device, Format8888Srgb, FilterBilinear, ForceDegamma); RequireSame(forcedBilinear, Sample(*device, Format8888Srgb, FilterBilinear, 0u), "8_8_8_8_SRGB, bilinear"); + RequireSame(Sample(*device, Format8888Srgb, FilterAnisoBilinear, ForceDegamma, AnisoOverride), forcedBilinear, "8_8_8_8_SRGB, single level", "anisotropically under ANISO_OVERRIDE", "bilinearly"); Reject(*device, Format8888UNorm, "guest format 56, which is not sRGB, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp index a1f35fc45..ec759a3f7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -66,12 +66,21 @@ constexpr std::array SamplerCode(std::uint32_t word0) { }; } +constexpr std::array MixedSamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, 0x7e2c0280u, 0xf0bc0108u, 0x00610a14u, 0xbf8c3f70u, + 0xe0701000u, 0x80000a03u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0701004u, 0x80000a03u, 0xbf810000u, + }; +} + alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu); alignas(256) constexpr auto LoadXy = LoadCode(0x3u); alignas(256) constexpr auto StoreX = StoreCode(0x1u); alignas(256) constexpr auto StoreXy = StoreCode(0x3u); alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); +alignas(256) constexpr auto MixedSampleLz = MixedSamplerCode(0xf09c0f08u); +alignas(256) constexpr auto MixedGather4Lz = MixedSamplerCode(0xf11c0108u); class GuestBlock { public: @@ -235,6 +244,8 @@ int main() { CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores"); RequireRefused(*device, SampleLz, "image_sample_lz"); RequireRefused(*device, Gather4Lz, "image_gather4_lz"); + RequireRefused(*device, MixedSampleLz, "image_sample_c_lz followed by image_sample_lz"); + RequireRefused(*device, MixedGather4Lz, "image_sample_c_lz followed by image_gather4_lz"); std::puts("image sRGB load tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp index d8c1506e3..f7d68a606 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MixPrecision.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -115,6 +116,30 @@ constexpr std::array RoundingRows{{ {0x00003c01u, 0x00004200u, 0x00800000u, 3u, 0x4202u}, }}; +struct DirectedRow { + std::uint32_t a, b, c; + std::array expected; +}; + +constexpr std::array DirectedRows{{ + {0xbfc00000u, 0x33800000u, 0x00800000u, {0x8001u, 0x8001u, 0x8002u, 0x8001u}}, + {0x3fc00000u, 0x33800000u, 0x00800000u, {0x0002u, 0x0002u, 0x0001u, 0x0001u}}, + {0x3f800000u, 0x3f801400u, 0x00000000u, {0x3c01u, 0x3c01u, 0x3c00u, 0x3c00u}}, + {0xbf800000u, 0x3f801400u, 0x00000000u, {0xbc01u, 0xbc00u, 0xbc01u, 0xbc00u}}, + {0x477ff000u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0xc77ff000u, 0x3f800000u, 0x00000000u, {0xfc00u, 0xfbffu, 0xfc00u, 0xfbffu}}, + {0x49742400u, 0x3f800000u, 0x00000000u, {0x7c00u, 0x7c00u, 0x7bffu, 0x7bffu}}, + {0x3fc00000u, 0x40000000u, 0x3e800000u, {0x4280u, 0x4280u, 0x4280u, 0x4280u}}, + {0x3f800000u, 0x3f800000u, 0xbf7fffffu, {0x0001u, 0x0001u, 0x0001u, 0x0001u}}, + {0xbf800000u, 0x3f800000u, 0x3f7fffffu, {0x8001u, 0x8001u, 0x8001u, 0x8001u}}, + {0x3f800000u, 0x3f800000u, 0xbf800000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x40000000u, 0x3f800000u, 0xc0000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x00000000u, 0x3f800000u, 0x80000000u, {0x0000u, 0x0000u, 0x8000u, 0x0000u}}, + {0x80000000u, 0x3f800000u, 0x80000000u, {0x8000u, 0x8000u, 0x8000u, 0x8000u}}, + {0x33000000u, 0x3f000000u, 0x00000000u, {0x0000u, 0x0001u, 0x0000u, 0x0000u}}, + {0xb3000000u, 0x3f000000u, 0x00000000u, {0x8000u, 0x8000u, 0x8001u, 0x8000u}}, +}}; + float Quarter(std::uint32_t index) { return static_cast(static_cast(index % 17u) - 8) / 4.0f; } @@ -176,7 +201,7 @@ void FillRoundingInput() { } } -void Run(AgcDriver::VulkanDevice& device, std::span code) { +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::optional& floatMode = std::nullopt) { Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); @@ -192,6 +217,7 @@ void Run(AgcDriver::VulkanDevice& device, std::span code) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = floatMode; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); @@ -235,6 +261,25 @@ void CheckRounding() { } } +void FillDirectedInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + std::copy_n(std::array{row.a, row.b, row.c}.begin(), 3, Input.begin() + tid * Inputs); + } +} + +void CheckDirected(std::uint32_t rounding) { + for (std::uint32_t tid = 0; tid < DirectedRows.size(); ++tid) { + const auto& row = DirectedRows[tid]; + const auto* out = &Output[tid * Results]; + const std::uint32_t expected = row.expected[rounding]; + const auto where = "mix f16 rounding " + std::to_string(rounding) + ": thread " + std::to_string(tid) + " (" + Hex(row.a) + ", " + Hex(row.b) + ", " + Hex(row.c) + ")"; + Require(out[0] == (0xabcd0000u | expected), where + " v_fma_mixlo_f16 is " + Hex(out[0]) + ", expected " + Hex(0xabcd0000u | expected)); + Require(out[1] == ((expected << 16u) | 0xabcdu), where + " v_fma_mixhi_f16 is " + Hex(out[1]) + ", expected " + Hex((expected << 16u) | 0xabcdu)); + } +} + } int main() { @@ -252,6 +297,11 @@ int main() { FillRoundingInput(); Run(*device, RoundingCode); CheckRounding(); + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + FillDirectedInput(); + Run(*device, RoundingCode, ShaderRecompiler::ShaderFloatMode{0xc0u | (rounding << 2u), true, false, false}); + CheckDirected(rounding); + } std::puts("mix precision tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp new file mode 100644 index 000000000..e5822da0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/NggVertexWaveReduction.cpp @@ -0,0 +1,149 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t Columns = 7; +constexpr std::uint32_t Rows = 5; +alignas(256) std::array Pixels{}; + +alignas(256) constexpr std::array VertexCode{ + 0xd7650015, 0x000100c1, 0xd7660015, 0x00022ac1, 0x7e0e0280, 0x7da206f9, 0x81060015, 0x360e0a81, + 0xbefe04c1, 0xd4c50002, 0x00020e80, 0xd763000f, 0x00022a81, 0xd5010000, 0x000a1e80, 0x380000fa, + 0xff091100, 0x380000fa, 0xff091200, 0x380000fa, 0xff091400, 0x380000fa, 0xff091800, 0xd778000f, + 0x03058300, 0x38001f00, 0x88040302, 0xd7600002, 0x00013f00, 0xd760006b, 0x00017f00, 0x88026b02, + 0xbf060402, 0x850580f2, 0xe0382000, 0x80020005, 0xbf8c3f70, 0x10000005, 0x10020205, 0x10040405, + 0x10060605, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +std::vector> Triangles() { + std::vector> vertices; + for (std::uint32_t cell = 0; cell < Columns * Rows; ++cell) { + const float left = -1.0f + 2.0f * static_cast(cell % Columns) / Columns; + const float right = -1.0f + 2.0f * static_cast(cell % Columns + 1u) / Columns; + const float bottom = -1.0f + 2.0f * static_cast(cell / Columns) / Rows; + const float top = -1.0f + 2.0f * static_cast(cell / Columns + 1u) / Rows; + for (const auto& corner : {std::array{left, bottom}, std::array{right, bottom}, std::array{right, top}, std::array{left, bottom}, std::array{right, top}, std::array{left, top}}) { + vertices.push_back({corner[0], corner[1], 0.5f, 1.0f}); + } + } + return vertices; +} + +void Draw(AgcDriver::VulkanDevice& device) { + static const auto triangles = Triangles(); + Pixels.fill(std::byte{0}); + const auto target = device.Target(); + + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(triangles.data(), 16u, static_cast(triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 8, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check() { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto actual = std::to_integer(Pixels[pixel * 4u]); + Require(actual == 255u, "NGG vertex wave reduction: pixel " + std::to_string(pixel) + " is " + std::to_string(actual) + ", expected 255 (a vertex whose wave OR of its lanes' bits, read from lanes 31 and 63, differs from its wave's ballot collapses its triangle)"); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto subgroupSize = device->Target().subgroupSize; + if (subgroupSize != 32u) { + std::printf("skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has %u lanes\n", subgroupSize); + return VulkanTestSkipped; + } + if ((device->SubgroupStages() & VK_SHADER_STAGE_VERTEX_BIT) == 0u || !TargetHasCapability(device->Target(), spv::CapabilityGroupNonUniformArithmetic)) { + std::puts("skipped, the device has no subgroup arithmetic in vertex shaders"); + return VulkanTestSkipped; + } + for (std::uint32_t pass = 0; pass < 3; ++pass) { + Draw(*device); + Check(); + } + std::puts("NGG vertex wave reduction tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp new file mode 100644 index 000000000..092a17fd7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedFloat16Native.cpp @@ -0,0 +1,215 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Batches = 32; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060085, 0xe0381000, 0x80000401, 0xbf8c3f70, + 0xcc0f400a, 0x18020b04, + 0xcc10400b, 0x18020b04, + 0xcc0e400c, 0x1c1a0b04, + 0xcc0f4a0d, 0x30020b04, + 0xcc10c00e, 0x18020d04, + 0xcc0e330f, 0x8c1a0b04, + 0x7e200306, 0x78200b04, + 0xcc0ec011, 0x3c1a0f04, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xbf810000, +}; + +constexpr const char* Names[Results] = { + "v_pk_add_f16 v10, v4, v5", + "v_pk_mul_f16 v11, v4, v5", + "v_pk_fma_f16 v12, v4, v5, v6", + "v_pk_add_f16 v13, v4, v5 op_sel:[1,0] op_sel_hi:[0,1] neg_lo:[1,0] neg_hi:[0,1]", + "v_pk_mul_f16 v14, v4, v6 clamp", + "v_pk_fma_f16 v15, v4, v5, v6 op_sel:[0,1,1] op_sel_hi:[1,0,0] neg_lo:[0,0,1] neg_hi:[1,1,0]", + "v_pk_fmac_f16 v16, v4, v5", + "v_pk_fma_f16 v17, v4, v7, v6 neg_lo:[1,0,0] clamp", +}; + +struct Mode { + const char* name; + std::optional floatMode; +}; + +const std::array Modes{{ + {"no FLOAT_MODE", std::nullopt}, + {"dx10 ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, true}}, + {"dx10 ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}}, + {"dx10 ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, true}}, + {"dx10", ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}}, + {"ieee ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, true}}, + {"ieee", ShaderRecompiler::ShaderFloatMode{0xc0u, false, true, false}}, + {"ovfl", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, true}}, + {"none", ShaderRecompiler::ShaderFloatMode{0xc0u, false, false, false}}, +}}; + +std::uint64_t Seed = 0x9e3779b97f4a7c15ull; + +std::uint32_t Next() { + Seed += 0x9e3779b97f4a7c15ull; + std::uint64_t z = Seed; + z = (z ^ (z >> 30u)) * 0xbf58476d1ce4e5b9ull; + z = (z ^ (z >> 27u)) * 0x94d049bb133111ebull; + return static_cast(z ^ (z >> 31u)); +} + +std::uint32_t Half() { + const std::uint32_t bits = Next(); + const std::uint32_t sign = (bits >> 31u) << 15u; + const std::uint32_t mantissa = bits & 0x3ffu; + switch ((bits >> 16u) % 10u) { + case 0u: return sign; + case 1u: return sign | std::max(mantissa, 1u); + case 2u: return sign | 0x7c00u; + case 3u: return sign | 0x7c00u | std::max(mantissa, 1u); + case 4u: return sign | ((0x1du + (bits >> 20u) % 2u) << 10u) | mantissa; + case 5u: return sign | ((1u + (bits >> 20u) % 4u) << 10u) | mantissa; + case 6u: return sign | ((0x0du + (bits >> 20u) % 5u) << 10u) | mantissa; + default: return bits & 0xffffu; + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +struct F16x2Math { + bool add = false; + bool mul = false; + bool fma = false; +}; + +F16x2Math FindF16x2Math(std::span spirv) { + std::optional half; + std::optional pair; + F16x2Math math; + for (std::size_t offset = 5u; offset < spirv.size();) { + const std::uint32_t words = spirv[offset] >> 16u; + const std::uint32_t opcode = spirv[offset] & 0xffffu; + Require(words != 0u && offset + words <= spirv.size(), "packed f16 native: malformed SPIR-V"); + if (opcode == spv::OpTypeFloat) { + Require(words >= 3u, "packed f16 native: malformed float type"); + if (spirv[offset + 2u] == 16u) half = spirv[offset + 1u]; + } + if (opcode == spv::OpTypeVector) { + Require(words >= 4u, "packed f16 native: malformed vector type"); + if (half && spirv[offset + 2u] == *half && spirv[offset + 3u] == 2u) pair = spirv[offset + 1u]; + } + if (opcode == spv::OpFAdd || opcode == spv::OpFMul || opcode == spv::OpExtInst) { + Require(words >= 5u, "packed f16 native: malformed arithmetic instruction"); + if (pair && spirv[offset + 1u] == *pair) { + math.add = math.add || opcode == spv::OpFAdd; + math.mul = math.mul || opcode == spv::OpFMul; + math.fma = math.fma || opcode == spv::OpExtInst; + } + } + offset += words; + } + return math; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->Target(); + const auto supports = [&](spv::Capability capability) { + return std::ranges::find(target.supportedCapabilities, static_cast(capability)) != target.supportedCapabilities.end(); + }; + if (!supports(spv::CapabilityFloat16) || !supports(spv::CapabilityRoundingModeRTE) || !supports(spv::CapabilityDenormPreserve)) { + std::puts("packed f16 native: the device has no native f16 with RTE rounding and kept denormals, skipped"); + return VulkanTestSkipped; + } + std::vector withoutFloat16; + std::ranges::copy_if(target.supportedCapabilities, std::back_inserter(withoutFloat16), [](std::uint32_t capability) { return capability != spv::CapabilityFloat16; }); + auto emulatedTarget = target; + emulatedTarget.supportedCapabilities = withoutFloat16; + + const std::span code(Code); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const auto compile = [&](const ShaderRecompiler::SpirvTarget& shaderTarget, const Mode& mode) { + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + shaderTarget, + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode.floatMode; + return ShaderRecompiler::Recompile(request); + }; + const auto run = [&](const auto& result) { + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + return Output; + }; + + std::uint64_t compared = 0u; + for (const auto& mode : Modes) { + const auto native = compile(target, mode); + const auto emulated = compile(emulatedTarget, mode); + const F16x2Math nativeMath = FindF16x2Math(native.spirv.Words()); + const F16x2Math emulatedMath = FindF16x2Math(emulated.spirv.Words()); + Require(nativeMath.add && nativeMath.mul, std::string("packed f16 native: ") + mode.name + " does not use f16vec2 add and mul"); + Require(!nativeMath.fma, std::string("packed f16 native: ") + mode.name + " uses an f16vec2 Fma, which Vulkan does not require to be fused"); + Require(!emulatedMath.add && !emulatedMath.mul && !emulatedMath.fma, std::string("packed f16 native: ") + mode.name + " uses f16vec2 math without Float16"); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (auto& word : Input) word = Half() | (Half() << 16u); + const auto nativeOutput = run(native); + const auto emulatedOutput = run(emulated); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const std::uint32_t actual = nativeOutput[tid * Results + index]; + const std::uint32_t expected = emulatedOutput[tid * Results + index]; + const std::uint32_t* row = &Input[tid * Inputs]; + Require(actual == expected, std::string("packed f16 native: ") + mode.name + ", " + Names[index] + " with v4=" + Hex(row[0]) + " v5=" + Hex(row[1]) + " v6=" + Hex(row[2]) + " v7=" + Hex(row[3]) + " is " + Hex(actual) + ", f32 path gives " + Hex(expected)); + } + } + compared += Threads * Results * 2u; + } + } + std::printf("packed f16 native tests passed (%llu halves)\n", static_cast(compared)); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp new file mode 100644 index 000000000..e4e66f07f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelineOptimizedReadback.cpp @@ -0,0 +1,276 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using namespace AgcDriver::Graphics; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; +alignas(256) constexpr std::array MaskedPixelCode{ + 0x7e040f00, 0x36060481, 0x7da40680, 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +alignas(256) constexpr std::array FullPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; +constexpr std::array, 12> DepthTriangles{{ + {-1.0f, -1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 1.0f, 0.25f, 1.0f}, + {-1.0f, 1.0f, 0.25f, 1.0f}, {0.0f, -1.0f, 0.25f, 1.0f}, {0.0f, 1.0f, 0.25f, 1.0f}, + {0.0f, -1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {0.0f, 1.0f, 0.75f, 1.0f}, + {0.0f, 1.0f, 0.75f, 1.0f}, {1.0f, -1.0f, 0.75f, 1.0f}, {1.0f, 1.0f, 0.75f, 1.0f} +}}; + +struct PipelineTrace { + PFN_vkGetDeviceProcAddr resolve = nullptr; + PFN_vkCreateGraphicsPipelines create = nullptr; + PFN_vkCmdBindPipeline bind = nullptr; + PFN_vkCmdDraw draw = nullptr; + DeviceFunctions functions{}; + std::mutex mutex; + std::condition_variable released; + bool allowOptimization = false; + std::vector fast; + std::vector optimized; + VkCommandBuffer boundCommands = VK_NULL_HANDLE; + VkPipeline bound = VK_NULL_HANDLE; + std::vector drawn; + + void ReleaseOptimization() { + { + std::lock_guard lock(mutex); + allowOptimization = true; + } + released.notify_all(); + } +}; + +PipelineTrace* Trace = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL CreatePipelines(VkDevice device, VkPipelineCache cache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks* allocator, VkPipeline* pipelines) { + const bool optimizing = std::any_of(infos, infos + count, [](const auto& info) { return (info.flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0; }); + if (optimizing) { + std::unique_lock lock(Trace->mutex); + Trace->released.wait(lock, [] { return Trace->allowOptimization; }); + } + const auto result = Trace->create(device, cache, count, infos, allocator, pipelines); + std::lock_guard lock(Trace->mutex); + for (std::uint32_t index = 0; index < count; ++index) { + if (pipelines[index] == VK_NULL_HANDLE || (infos[index].flags & VK_PIPELINE_CREATE_LIBRARY_BIT_KHR) != 0) continue; + auto& handles = (infos[index].flags & VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT) != 0 ? Trace->optimized : Trace->fast; + handles.push_back(pipelines[index]); + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL BindPipeline(VkCommandBuffer commands, VkPipelineBindPoint point, VkPipeline pipeline) { + if (point == VK_PIPELINE_BIND_POINT_GRAPHICS) { + Trace->boundCommands = commands; + Trace->bound = pipeline; + } + Trace->bind(commands, point, pipeline); +} + +VKAPI_ATTR void VKAPI_CALL DrawCommands(VkCommandBuffer commands, std::uint32_t vertices, std::uint32_t instances, std::uint32_t firstVertex, std::uint32_t firstInstance) { + Trace->drawn.push_back(Trace->boundCommands == commands ? Trace->bound : VK_NULL_HANDLE); + Trace->draw(commands, vertices, instances, firstVertex, firstInstance); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateGraphicsPipelines") == 0) return reinterpret_cast(CreatePipelines); + if (std::strcmp(name, "vkCmdBindPipeline") == 0) return reinterpret_cast(BindPipeline); + if (std::strcmp(name, "vkCmdDraw") == 0) return reinterpret_cast(DrawCommands); + return Trace->resolve(device, name); +} + +struct OptimizationDrain { + VkDevice device; + ~OptimizationDrain() { + Trace->ReleaseOptimization(); + WaitForOptimizedPipelines(device); + } +}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void CheckPixels(bool masked, std::string_view phase) { + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const bool drawn = masked ? (pixel % Width) % 2u == 0u : pixel % Width < Width / 2u; + const auto expected = drawn ? std::byte{255} : Kept; + for (std::uint32_t channel = 0; channel < 4; ++channel) { + const auto actual = Pixels[pixel * 4u + channel]; + Require(actual == expected, std::string(phase) + ": pixel " + std::to_string(pixel) + " channel " + std::to_string(channel) + " is " + std::to_string(static_cast(actual)) + ", expected " + std::to_string(static_cast(expected))); + } + } +} + +void CheckDrawn(PipelineTrace& trace, bool optimized) { + std::lock_guard lock(trace.mutex); + const auto& expected = optimized ? trace.optimized : trace.fast; + Require(trace.drawn.size() == 1, "pipeline readback did not observe exactly one real draw"); + Require(std::find(expected.begin(), expected.end(), trace.drawn.front()) != expected.end(), optimized ? "readback draw did not bind an optimized pipeline" : "readback draw did not bind a fast-linked pipeline"); +} + +void Run(AgcDriver::VulkanDevice& device, Context context, PipelineTrace& trace, std::uint32_t waveSize, VkFormat depthFormat, bool optimize) { + const bool masked = depthFormat == VK_FORMAT_UNDEFINED; + const auto target = device.Target(); + const auto vertices = masked ? std::span>(Triangle) : std::span>(DepthTriangles); + const auto vertexBuffer = BufferDescriptor(vertices.data(), 16u, static_cast(vertices.size())); + const std::vector vertexUserData(vertexBuffer.begin(), vertexBuffer.end()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto pixelCode = masked ? std::span(MaskedPixelCode) : std::span(FullPixelCode); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = waveSize == 32u; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + const std::vector pixelUserData(8, 0u); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {waveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + State state{}; + state.stages = {ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + if (!masked) { + state.depth = DepthTarget{reinterpret_cast(DepthMemory.data()), 0, {Width, Height}, depthFormat, 0.5f, 0}; + state.depthTest = true; + state.depthCompare = VK_COMPARE_OP_LESS; + } + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(vertices.size()), 0, 1, 0, false}; + const auto render = [&](bool optimized, std::string_view phase) { + Pixels.fill(Kept); + trace.drawn.clear(); + trace.bound = VK_NULL_HANDLE; + trace.boundCommands = VK_NULL_HANDLE; + AgcDriver::Graphics::Draw(context, state, draw, shaders); + device.WaitIdle(); + CheckDrawn(trace, optimized); + CheckPixels(masked, phase); + }; + render(false, "fast link"); + trace.ReleaseOptimization(); + WaitForOptimizedPipelines(context.device); + Require(PipelineLibraryCountersOf(context.device).linked == 1, "the readback pipeline was not linked from graphics libraries"); + Require(trace.optimized.size() == (optimize ? 1u : 0u), "the optimized pipeline creation count differs from the requested mode"); + render(optimize, "completed link"); + std::printf("pipeline readback passed: fast draws %u, optimized draws %u, optimized creations %zu\n", optimize ? 1u : 2u, optimize ? 1u : 0u, trace.optimized.size()); +} + +} + +int main(int argc, char** argv) { + try { + std::string_view mode = "d32"; + bool force = false; + for (int index = 1; index < argc; ++index) { + const std::string_view argument = argv[index]; + if (argument == "--force-lto") force = true; + else { + Require(argument == "color32" || argument == "color64" || argument == "d32" || argument == "d16", "unknown pipeline readback case"); + mode = argument; + } + } + PipelineTrace trace; + Trace = &trace; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.graphicsPipelineLibrary) { + std::puts("skipped, graphics pipeline libraries are unavailable or disabled"); + return VulkanTestSkipped; + } + const bool masked = mode == "color32" || mode == "color64"; + if (masked && device->Target().subgroupSize < 32u) { + std::puts("skipped, the masked pixel shader requires at least 32 subgroup lanes"); + return VulkanTestSkipped; + } + if (force) context.pipelineLinkTimeOptimization = true; + const bool optimize = context.pipelineLinkTimeOptimization && std::getenv("APS5_NO_PIPELINE_LTO") == nullptr; + std::printf("pipeline readback: %s, %.*s, LTO %s%s\n", device->DeviceName().c_str(), static_cast(mode.size()), mode.data(), optimize ? "enabled" : "disabled", force ? " (explicit test override)" : " (production policy)"); + trace.resolve = context.deviceProc; + trace.create = context.Function("vkCreateGraphicsPipelines"); + trace.bind = context.Function("vkCmdBindPipeline"); + trace.draw = context.Function("vkCmdDraw"); + trace.functions = context.functions != nullptr ? *context.functions : DeviceFunctions{}; + trace.functions.cmdBindPipeline = BindPipeline; + trace.functions.cmdDraw = DrawCommands; + context.functions = &trace.functions; + context.deviceProc = Resolve; + OptimizationDrain drain{context.device}; + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + Run(*device, context, trace, mode == "color32" ? 32u : 64u, masked ? VK_FORMAT_UNDEFINED : (mode == "d16" ? VK_FORMAT_D16_UNORM : VK_FORMAT_D32_SFLOAT), optimize); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index b6abe53ff..d96b7a9ec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace { @@ -277,6 +278,28 @@ void Registration(AgcDriver::VulkanDevice& device) { Require(registry->at(0x10000).back() == metadata, "changed shader metadata was treated as identical"); } +void TransactionLock() { + ShaderSnapshot shader{}; + const auto publish = [&] { + ShaderPreparationTransaction other; + other.Edit(shader).rectangleRequested = true; + other.Commit(); + }; + std::optional open(std::in_place); + auto unread = std::async(std::launch::async, publish); + const bool unblocked = unread.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + if (!unblocked) open.reset(); + unread.get(); + Require(unblocked, "an open transaction that read nothing blocked another one"); + static_cast(open->Read(shader)); + auto waiting = std::async(std::launch::async, publish); + const bool excluded = waiting.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open->Commit(); + open.reset(); + waiting.get(); + Require(excluded, "a transaction ran beside one that had read prepared state"); +} + } int main() { @@ -284,6 +307,7 @@ int main() { Transactions(); HeaderSelection(); FusedHullHeaderSelection(); + TransactionLock(); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Registration(*device); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 93dffc420..4aa54af7d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -525,6 +525,54 @@ void Registration(bool indirect) { Require(destination == 0, "failed dispatch executed a subsequent memory write"); } +void RegisteredVertexWorkgroup(const ShaderRecompiler::SpirvTarget& target, bool registrationOnly = false) { + if (!registrationOnly && ((target.subgroupSize != 32u && target.subgroupSize != 64u) || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityMeshShadingEXT) == target.supportedCapabilities.end())) { + std::cout << "registered NGG wave64 preparation not run: requires mesh shading and a 32- or 64-lane host subgroup; subgroup=" << target.subgroupSize << '\n'; + return; + } + alignas(256) static constexpr std::array code{ + 0xd8340000u, 0u, 0xd7600000u, 0x00017f00u, 0x7e020200u, 0xf80008cfu, 0x04030201u, 0xbf810000u + }; + struct Header { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1ff, 64}, {0x2ab, 4}, {0x2ce, 0}}}; + ShaderSpecialRegs specials{{0x25b, 0x8040}, {0x2d5, 0x2000}, 0, {}, {}, {0x29b, 2}, {0x260, 0}}; + ShaderUserData userData{}; + } header; + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 2; + header.shader.user_data = &header.userData; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = header.context.size(); + header.shader.specials = &header.specials; + header.registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8a, 0x622c0187}, {0x8b, 5u << 19u}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + if (registrationOnly) { + header.shader.user_data = nullptr; + AgcDriverRegisterShader_nid_postfix(&header.shader); + header.specials.vgt_shader_stages_en.value = 0; + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "missing AGC user-data header"); + header.shader.user_data = &header.userData; + if (target.subgroupSize < 64u && std::getenv("APS5_SYNC_SHADER_PREPARE") != nullptr) { + ExpectFailure([&] { AgcDriverRegisterShader_nid_postfix(&header.shader); }, "v_readlane_b32 of lane 63 is outside"); + } + std::cout << "registered NGG preparation deferral passed; subgroup=" << target.subgroupSize << '\n'; + return; + } + const std::array primitive{{{0x242, 4}}}; + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, primitive); + std::cout << "registered NGG wave64 preparation passed; subgroup=" << target.subgroupSize << '\n'; +} + void DeferredRegistration() { alignas(256) std::array code{0xbe842104u, 0xbf810000u}; struct Header { @@ -585,7 +633,14 @@ void DeferredUndecodableRegistration() { int main(int argc, char** argv) { try { - Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable")), "invalid test arguments"); + Require(argc == 1 || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup")), "invalid test arguments"); + if (argc == 2 && std::string_view(argv[1]) == "--registered-workgroup") { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + RegisteredVertexWorkgroup(device->Target(), true); + AgcDriverShutdown_nid_postfix(); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--deferred") { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; DeferredRegistration(); @@ -601,6 +656,7 @@ int main(int argc, char** argv) { auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Run(*device); + RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp index 8b2ab8f12..225501329 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RelativeIndexing.cpp @@ -17,14 +17,15 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t MaxThreads = 64; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 16; -alignas(256) std::array Input{}; -alignas(256) std::array Output{}; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t Checked = 19; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ - 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060087, 0xe0301000, 0x80000401, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, 0x7e1c0280, 0x7e1e0280, 0x7e200280, 0x7e220280, 0x7e240280, 0x7e260280, 0x7e280280, 0xbf8c3f70, 0x7e3c0304, 0xd525001f, 0x00010304, 0xd5250020, 0x00010504, 0xd5250021, 0x00010704, 0xd5250022, 0x00010904, 0xd5250023, 0x00010b04, 0xd5250024, 0x00010d04, 0xd5250025, 0x00010f04, 0xbefc0382, @@ -34,44 +35,21 @@ alignas(256) constexpr std::array Code{ 0x0000011f, 0x7e280323, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03, 0xe070100c, 0x80010d03, 0xe0701010, 0x80010e03, 0xe0701014, 0x80010f03, 0xe0701018, 0x80011003, 0xe070101c, 0x80011103, 0xe0701020, 0x80011203, 0xe0701024, 0x80011303, 0xe0701028, 0x80011403, - 0xbf810000, + 0xbefc03c1, 0x7e2a8720, 0xe070102c, 0x80011503, 0x7e428504, 0xe0701030, 0x80012003, 0x7e468922, + 0xe0701034, 0x80012203, 0xe0701038, 0x80012103, 0xbefc03ff, 0x000103ff, 0x7e3ed120, 0xe070103c, + 0x80012003, 0x7e48911e, 0xe0701040, 0x80012503, 0x7e2c0327, 0xe0701044, 0x80011603, 0xbefc0388, + 0x7e2e871e, 0xe0701048, 0x80011703, 0xbf810000, }; -constexpr std::uint32_t Expected[32][11] = { - {0x00001002u, 0x00001002u, 0x00001001u, 0x00001030u, 0x00001020u, 0x00001000u, 0x00001007u, 0x00001006u, 0x00001007u, 0x00001005u, 0x00001005u}, - {0x00002002u, 0x00002002u, 0x00002001u, 0x00002030u, 0x00002020u, 0x00002000u, 0x00002007u, 0x00002006u, 0x00002007u, 0x00002005u, 0x00002005u}, - {0x00003002u, 0x00003002u, 0x00003001u, 0x00003030u, 0x00003020u, 0x00003000u, 0x00003007u, 0x00003006u, 0x00003007u, 0x00003005u, 0x00003005u}, - {0x00004002u, 0x00004002u, 0x00004001u, 0x00004030u, 0x00004020u, 0x00004000u, 0x00004007u, 0x00004006u, 0x00004007u, 0x00004005u, 0x00004005u}, - {0x00005002u, 0x00005002u, 0x00005001u, 0x00005030u, 0x00005020u, 0x00005000u, 0x00005007u, 0x00005006u, 0x00005007u, 0x00005005u, 0x00005005u}, - {0x00006002u, 0x00006002u, 0x00006001u, 0x00006030u, 0x00006020u, 0x00006000u, 0x00006007u, 0x00006006u, 0x00006007u, 0x00006005u, 0x00006005u}, - {0x00007002u, 0x00007002u, 0x00007001u, 0x00007030u, 0x00007020u, 0x00007000u, 0x00007007u, 0x00007006u, 0x00007007u, 0x00007005u, 0x00007005u}, - {0x00008002u, 0x00008002u, 0x00008001u, 0x00008030u, 0x00008020u, 0x00008000u, 0x00008007u, 0x00008006u, 0x00008007u, 0x00008005u, 0x00008005u}, - {0x00009002u, 0x00009002u, 0x00009001u, 0x00009030u, 0x00009020u, 0x00009000u, 0x00009007u, 0x00009006u, 0x00009007u, 0x00009005u, 0x00009005u}, - {0x0000a002u, 0x0000a002u, 0x0000a001u, 0x0000a030u, 0x0000a020u, 0x0000a000u, 0x0000a007u, 0x0000a006u, 0x0000a007u, 0x0000a005u, 0x0000a005u}, - {0x0000b002u, 0x0000b002u, 0x0000b001u, 0x0000b030u, 0x0000b020u, 0x0000b000u, 0x0000b007u, 0x0000b006u, 0x0000b007u, 0x0000b005u, 0x0000b005u}, - {0x0000c002u, 0x0000c002u, 0x0000c001u, 0x0000c030u, 0x0000c020u, 0x0000c000u, 0x0000c007u, 0x0000c006u, 0x0000c007u, 0x0000c005u, 0x0000c005u}, - {0x0000d002u, 0x0000d002u, 0x0000d001u, 0x0000d030u, 0x0000d020u, 0x0000d000u, 0x0000d007u, 0x0000d006u, 0x0000d007u, 0x0000d005u, 0x0000d005u}, - {0x0000e002u, 0x0000e002u, 0x0000e001u, 0x0000e030u, 0x0000e020u, 0x0000e000u, 0x0000e007u, 0x0000e006u, 0x0000e007u, 0x0000e005u, 0x0000e005u}, - {0x0000f002u, 0x0000f002u, 0x0000f001u, 0x0000f030u, 0x0000f020u, 0x0000f000u, 0x0000f007u, 0x0000f006u, 0x0000f007u, 0x0000f005u, 0x0000f005u}, - {0x00010002u, 0x00010002u, 0x00010001u, 0x00010030u, 0x00010020u, 0x00010000u, 0x00010007u, 0x00010006u, 0x00010007u, 0x00010005u, 0x00010005u}, - {0x00011002u, 0x00011002u, 0x00011001u, 0x00011030u, 0x00011020u, 0x00011000u, 0x00011007u, 0x00011006u, 0x00011007u, 0x00011005u, 0x00011005u}, - {0x00012002u, 0x00012002u, 0x00012001u, 0x00012030u, 0x00012020u, 0x00012000u, 0x00012007u, 0x00012006u, 0x00012007u, 0x00012005u, 0x00012005u}, - {0x00013002u, 0x00013002u, 0x00013001u, 0x00013030u, 0x00013020u, 0x00013000u, 0x00013007u, 0x00013006u, 0x00013007u, 0x00013005u, 0x00013005u}, - {0x00014002u, 0x00014002u, 0x00014001u, 0x00014030u, 0x00014020u, 0x00014000u, 0x00014007u, 0x00014006u, 0x00014007u, 0x00014005u, 0x00014005u}, - {0x00015002u, 0x00015002u, 0x00015001u, 0x00015030u, 0x00015020u, 0x00015000u, 0x00015007u, 0x00015006u, 0x00015007u, 0x00015005u, 0x00015005u}, - {0x00016002u, 0x00016002u, 0x00016001u, 0x00016030u, 0x00016020u, 0x00016000u, 0x00016007u, 0x00016006u, 0x00016007u, 0x00016005u, 0x00016005u}, - {0x00017002u, 0x00017002u, 0x00017001u, 0x00017030u, 0x00017020u, 0x00017000u, 0x00017007u, 0x00017006u, 0x00017007u, 0x00017005u, 0x00017005u}, - {0x00018002u, 0x00018002u, 0x00018001u, 0x00018030u, 0x00018020u, 0x00018000u, 0x00018007u, 0x00018006u, 0x00018007u, 0x00018005u, 0x00018005u}, - {0x00019002u, 0x00019002u, 0x00019001u, 0x00019030u, 0x00019020u, 0x00019000u, 0x00019007u, 0x00019006u, 0x00019007u, 0x00019005u, 0x00019005u}, - {0x0001a002u, 0x0001a002u, 0x0001a001u, 0x0001a030u, 0x0001a020u, 0x0001a000u, 0x0001a007u, 0x0001a006u, 0x0001a007u, 0x0001a005u, 0x0001a005u}, - {0x0001b002u, 0x0001b002u, 0x0001b001u, 0x0001b030u, 0x0001b020u, 0x0001b000u, 0x0001b007u, 0x0001b006u, 0x0001b007u, 0x0001b005u, 0x0001b005u}, - {0x0001c002u, 0x0001c002u, 0x0001c001u, 0x0001c030u, 0x0001c020u, 0x0001c000u, 0x0001c007u, 0x0001c006u, 0x0001c007u, 0x0001c005u, 0x0001c005u}, - {0x0001d002u, 0x0001d002u, 0x0001d001u, 0x0001d030u, 0x0001d020u, 0x0001d000u, 0x0001d007u, 0x0001d006u, 0x0001d007u, 0x0001d005u, 0x0001d005u}, - {0x0001e002u, 0x0001e002u, 0x0001e001u, 0x0001e030u, 0x0001e020u, 0x0001e000u, 0x0001e007u, 0x0001e006u, 0x0001e007u, 0x0001e005u, 0x0001e005u}, - {0x0001f002u, 0x0001f002u, 0x0001f001u, 0x0001f030u, 0x0001f020u, 0x0001f000u, 0x0001f007u, 0x0001f006u, 0x0001f007u, 0x0001f005u, 0x0001f005u}, - {0x00020002u, 0x00020002u, 0x00020001u, 0x00020030u, 0x00020020u, 0x00020000u, 0x00020007u, 0x00020006u, 0x00020007u, 0x00020005u, 0x00020005u}, -}; -constexpr const char* Names[11] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_out_of_range"}; +constexpr const char* Names[Checked] = {"movrelsd", "swapped_2", "sd2_untouched", "swaprel_dst", "swaprel_src", "swaprel_other", "swap_dst", "swap_src", "swaprel_same", "swaprel_same_other", "movrelsd_e64_dst", + "movrels_out_of_range", "movreld_out_of_range_neighbour", "movrelsd_out_of_range_source", "movrelsd_out_of_range_other", "swaprel_one_side_out_of_range", "movrelsd_2_source_out_of_range", + "unwritten_direct", "unwritten_relative"}; + +std::array ExpectedFor(std::uint32_t tid) { + const std::uint32_t x = 0x1000u * (tid + 1u); + return {x + 2u, x + 2u, x + 1u, x + 0x30u, x + 0x20u, x, x + 7u, x + 6u, x + 7u, x + 5u, x + 5u, + tid, x + 0x30u, x + 2u, x + 0x20u, x + 0x30u, tid, 0u, 0u}; +} void Fill(std::uint32_t tid, std::uint32_t* words) { words[0] = 0x1000u * (tid + 1u); @@ -88,12 +66,12 @@ std::string Hex(std::uint32_t value) { return text; } -void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { - Require(actual == expected, std::string("relative indexing: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +void Expect(std::uint32_t waveSize, std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("relative indexing: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); } -void Run(AgcDriver::VulkanDevice& device) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); @@ -102,10 +80,10 @@ void Run(AgcDriver::VulkanDevice& device) { std::copy(output.begin(), output.end(), userData.begin() + 4); const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, - {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; @@ -115,11 +93,11 @@ void Run(AgcDriver::VulkanDevice& device) { device.WaitIdle(); } -void Check() { - for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const std::uint32_t* in = &Input[tid * Inputs]; +void Check(std::uint32_t waveSize, std::uint32_t threads) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t index = 0; index < 11; ++index) Expect(tid, out[index], Expected[tid][index], Names[index]); + const auto expected = ExpectedFor(tid); + for (std::uint32_t index = 0; index < Checked; ++index) Expect(waveSize, tid, out[index], expected[index], Names[index]); } } @@ -129,8 +107,10 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); - Check(); + Run(*device, 32, 32); + Check(32, 32); + Run(*device, 64, 64); + Check(64, 64); std::puts("relative indexing tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index e7318fa76..0cf28122d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -5,6 +5,8 @@ #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "VulkanTestDevice.hpp" #include @@ -45,7 +47,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u; class Device { public: - Device() { + explicit Device(bool resourceImages = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -93,6 +95,11 @@ public: queue.pQueuePriorities = &priority; VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; + if (resourceImages) { + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); + enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; + } address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { @@ -207,6 +214,115 @@ GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) return DecodeTextureResource(words); } +std::shared_ptr ImageResources(const Context& context, const Block& block, std::size_t first, std::uint32_t count, bool sampled) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u; + binding.count = count; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(count, false); + binding.imageSamplers.assign(count, 0u); + for (std::uint32_t element = 0; element < count; ++element) { + const auto words = Descriptor(block.Surface(first + element)); + binding.guestDescriptor.insert(binding.guestDescriptor.end(), words.begin(), words.end()); + } + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + return std::make_shared(context, shader); +} + +bool resourceCacheTests(Context context, const std::string& mode) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + Block block(context); + if (context.hostImportAlignment == 0 || HostImportFor(context, block.Surface(0), BlockBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "resource cache tests require host imports"); + std::puts("skipped, resource cache tests require host imports"); + return false; + } + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + if (mode == "resource-clear") { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + const std::weak_ptr image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; + cache.Insert({1}, storage); + cache.Insert({2}, sampled); + Require(cache.Size() == 2, "the resource cache did not accept the live descriptor sets"); + sampled.reset(); + ClearCachedTextures(context.device); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == nullptr && cache.Size() == 0, "clearing storage images left their resources cached"); + Require(sampledSet.expired(), "cache clear kept the sampled descriptor set alive"); + Require(!image.expired() && !storageSet.expired(), "cache clear destroyed an active caller's resources"); + storage.reset(); + Require(image.expired() && storageSet.expired(), "cleared storage image outlived its last active resource"); + } else if (mode == "resource-oversized") { + for (const bool sampledImages : {false, true}) { + const std::weak_ptr early = CachedStorageSurface(context, Resource(block.Surface(0))); + auto oversized = ImageResources(context, block, 0, static_cast(Surfaces), sampledImages); + const std::weak_ptr rejected = oversized; + auto image = early.lock(); + Require(oversized->Reusable(), "the oversized descriptor set is not eligible for resource caching"); + Require(image != nullptr && !image->Cached(), "the oversized descriptor set did not evict an early image while building"); + if (sampledImages) Require(oversized->SampledTextures().size() == Surfaces && oversized->SampledTextures().front()->StorageSource() == image.get(), "the oversized sampled descriptor set does not retain the early storage image"); + std::vector> displaced; + cache.Insert({1}, oversized, &displaced); + Require(cache.Size() == 0 && displaced.empty(), sampledImages ? "an oversized sampled descriptor set entered the resource cache" : "an oversized storage descriptor set entered the resource cache"); + image.reset(); + oversized.reset(); + TextureCacheUsage(); + Require(rejected.expired() && early.expired(), "the rejected oversized descriptor set retained an evicted storage image"); + std::puts(sampledImages ? "resource cache oversized sampled lifetime passed" : "resource cache oversized storage lifetime passed"); + ClearCachedTextures(context.device); + } + } else { + auto storage = ImageResources(context, block, 0, 1, false); + auto sampled = ImageResources(context, block, 0, 1, true); + Require(storage->Reusable() && sampled->Reusable(), "the stale descriptor sets are not eligible for resource caching"); + Require(sampled->SampledTextures().front()->ViewsStorageImage(), "the sampled resource is not a storage-image view"); + auto image = StorageTexture::FindLive(block.Surface(0), SurfaceBytes); + const std::weak_ptr staleImage = image; + const std::weak_ptr storageSet = storage; + const std::weak_ptr sampledSet = sampled; + Require(image != nullptr && image->Cached(), "the resource has no cached storage image"); + for (std::size_t surface = 1; surface < Surfaces && image->Cached(); ++surface) CachedStorageSurface(context, Resource(block.Surface(surface))); + Require(!image->Cached(), "the test never evicted the oldest storage image"); + auto live = ImageResources(context, block, Surfaces - 1, 1, false); + cache.Insert({1}, live); + Require(cache.Find({1}) == live, "the resource cache rejected a live descriptor set"); + std::vector> displaced; + for (const auto& stale : {storage, sampled}) { + cache.Insert({2}, stale, &displaced); + Require(cache.Find({2}) == nullptr && cache.Size() == 1 && displaced.empty(), "an uncached image was admitted to the resource cache"); + cache.Insert({1}, stale, &displaced); + Require(cache.Find({1}) == live && cache.Size() == 1 && displaced.empty(), "an uncached image replaced a live resource-cache entry"); + } + image.reset(); + storage.reset(); + Require(storageSet.expired() && !sampledSet.expired() && !staleImage.expired(), "stale storage release did not preserve the sampled caller's image"); + std::puts("resource cache stale storage caller release passed"); + sampled.reset(); + TextureCacheUsage(); + Require(sampledSet.expired() && staleImage.expired(), "the rejected descriptor sets retained an evicted storage image"); + std::puts("resource cache stale sampled caller release passed"); + } + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + return true; +} + void viewTests(const Context& context) { if (context.hostImportAlignment == 0) { std::puts("host imports unavailable: views of storage images not tested"); @@ -482,6 +598,8 @@ void memoryTests(Context context) { int main(int argc, char** argv) { try { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; + const std::string mode = argc > 1 ? argv[1] : ""; + const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); #else @@ -489,7 +607,7 @@ int main(int argc, char** argv) { #endif std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(resources); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); @@ -498,6 +616,12 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; + if (resources) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!resourceCacheTests(context, mode)) return VulkanTestSkipped; + std::puts("resource cache tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp new file mode 100644 index 000000000..cec60574e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StoreIdleResults.cpp @@ -0,0 +1,116 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 0x60000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, 0x12345678u, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::vector Commands(const std::uint32_t* texels) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(StoreCode.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{static_cast(userData.size()) << 1u}; + registers(0x213, resource); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + return words; +} + +void Store(const std::uint32_t* texels) { + auto words = Commands(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + std::vector storage((BlockBytes + 0x10000u) / 4u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + 0xffffu) & ~std::uintptr_t{0xffffu}); + std::fill(texels, texels + BlockBytes / 4u, Untouched); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, BlockBytes, true, true, true); + } + const auto bytes = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes; + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == 0x12345678u) return true; + } + return false; + }; + + Store(texels); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while (!stored() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(stored(), "the queue-idle store did not bring the result to guest memory without a direct flush or further submission"); + + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + std::puts("store idle results tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp new file mode 100644 index 000000000..4944237df --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Columns = 9; +constexpr std::uint32_t Ones = 0xffffffffu; +constexpr std::uint32_t Marker = 0x5a5a5a5au; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array ExecKeptCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6, + 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01, + 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01, + 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201, + 0xbf810000, +}; + +alignas(256) constexpr std::array ExecResetCode{ + 0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280, + 0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481, + 0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850002, 0xbefe04c1, + 0xbe26fff5, 0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, + 0x80010a01, 0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, + 0x80010e01, 0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, + 0x80011201, 0xbf810000, +}; + +constexpr std::array Names{"passes", "last pass", "s38 in the pass", "exec lo in the pass", "exec hi in the pass", "s38 after the loop", "exec lo after the loop", "exec hi after the loop", "total passes"}; + +struct Group { + std::uint32_t first; + std::uint32_t last; + std::array values; +}; + +struct Case { + const char* name; + std::uint64_t mask; + std::vector groups; +}; + +const std::vector ExecKeptCases{ + {"all lanes", ~0ull, { + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 2}}, + {32, 63, {1, 2, Ones, 0, Ones, Ones, Ones, Ones, 2}}, + }}, + {"low half", 0xffffffffull, { + {0, 31, {1, 1, 0, Ones, 0, 0, Ones, 0, 1}}, + {32, 63, {0, 0, 0, 0, 0, 0, Ones, 0, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 31, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 8, 8, 0x100u, 2}}, + {3, 3, {1, 1, 0x100u, 8, 0, 8, 8, 0x100u, 2}}, + {40, 40, {1, 2, 8, 0, 0x100u, 8, 8, 0x100u, 2}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 8, 0, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 8, 0, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, 0x100u, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, 0x100u, 1}}, + }}, +}; + +const std::vector ExecResetCases{ + {"all lanes", ~0ull, { + {0, 63, {0, 0, 0, 0, 0, Ones, Ones, Ones, 1}}, + {0, 31, {1, 1, Ones, Ones, 0, Ones, Ones, Ones, 1}}, + }}, + {"low half", 0xffffffffull, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {0, 31, {1, 1, 0, Ones, 0, 0, 0, Ones, 1}}, + }}, + {"high half", 0xffffffffull << 32, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {32, 63, {1, 1, 0, 0, Ones, 0, 0, Ones, 1}}, + }}, + {"no lane", 0ull, { + {0, 63, {0, 0, 0, 0, 0, Marker, 0, 0, 0}}, + }}, + {"lanes 3 and 40", (1ull << 3) | (1ull << 40), { + {0, 63, {0, 0, 0, 0, 0, 0x100u, 0x100u, Ones, 1}}, + {3, 3, {1, 1, 0x100u, 8, 0, 0x100u, 0x100u, Ones, 1}}, + }}, + {"lane 3", 1ull << 3, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {3, 3, {1, 1, 0, 8, 0, 0, 0, Ones, 1}}, + }}, + {"lane 40", 1ull << 40, { + {0, 63, {0, 0, 0, 0, 0, 0, 0, Ones, 1}}, + {40, 40, {1, 1, 0, 0, 0x100u, 0, 0, Ones, 1}}, + }}, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint64_t mask) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Input[lane] = ((mask >> lane) & 1u) != 0u ? 1u : 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {64, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* variant, const Case& testCase) { + std::array, Threads> expected{}; + for (const auto& group : testCase.groups) { + for (std::uint32_t lane = group.first; lane <= group.last; ++lane) expected[lane] = group.values; + } + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + for (std::uint32_t column = 0; column < Columns; ++column) { + const auto actual = Output[column * Threads + lane]; + Require(actual == expected[lane][column], std::string("subvector loop ") + variant + ", initial exec " + testCase.name + ": lane " + std::to_string(lane) + " " + Names[column] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[lane][column])); + } + } +} + +void CheckWave32Throws(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(8, 0u); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{32, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, 128} + }; + request.useCache = false; + try { + static_cast(ShaderRecompiler::Recompile(request)); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("s_subvector_loop_begin in a wave32 shader is not measured") != std::string::npos, std::string("wave32 subvector loop threw an unexpected error: ") + error.what()); + return; + } + Require(false, "a wave32 subvector loop was recompiled"); +} + +void RunCases(AgcDriver::VulkanDevice& device, const char* variant, std::span code, const std::vector& cases) { + for (const auto& testCase : cases) { + Run(device, code, testCase.mask); + Check(variant, testCase); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckWave32Throws(*device, ExecKeptCode); + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + RunCases(*device, "exec kept by the body", ExecKeptCode, ExecKeptCases); + RunCases(*device, "body resets exec", ExecResetCode, ExecResetCases); + std::puts("subvector loop tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp index 432c19bb8..393d98a1f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SwapAltBlend.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include namespace { @@ -53,6 +54,12 @@ alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; +alignas(256) constexpr std::array StoringVertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xe0700000, 0x80010000, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) std::array Stored{}; + constexpr std::array, 3> Triangle{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; @@ -163,16 +170,19 @@ VkPipelineColorBlendAttachmentState Blend(VkBlendFactor source, VkBlendFactor de return blend; } -void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span pixelCode) { +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, const VkPipelineColorBlendAttachmentState& blend, std::span pixelCode, std::span vertexCode = VertexCode) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; - std::vector vertexUserData(4, 0u); + std::vector vertexUserData(8, 0u); const auto address = reinterpret_cast(Triangle.data()); const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); - const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + const auto stored = reinterpret_cast(Stored.data()); + const std::array storeBuffer{static_cast(stored), static_cast((stored >> 32u) & 0xffffu), static_cast(sizeof(Stored)), 0x01016facu}; + std::copy(storeBuffer.begin(), storeBuffer.end(), vertexUserData.begin() + 4); + const std::array vertexMemory{{{reinterpret_cast(vertexCode.data()), std::as_bytes(vertexCode)}}}; ShaderRecompiler::RecompileRequest vertex{ - {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {ShaderStage::Vertex, reinterpret_cast(vertexCode.data()), vertexCode, 0, {}}, {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, target, {0, 0, 0, 64} @@ -281,6 +291,18 @@ int main() { {"ONE + ONE_MINUS_SRC_ALPHA for color and alpha", Blend(VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ONE, VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VK_BLEND_OP_ADD), false, [&](float r, float g) { return std::array{x + r * (1.0f - w), w + g * (1.0f - w)}; }}, }}; + { + Fill(block, layout); + const auto color = DecodeTarget(block); + bool rejected = false; + try { + Draw(*device, color, cases[0].blend, Program(exported), StoringVertexCode); + } catch (const std::exception& error) { + rejected = std::string_view(error.what()).find("a draw whose shaders write memory") != std::string_view::npos; + if (!rejected) throw; + } + Require(rejected, "a split 8_8 SWAP_ALT draw whose vertex shader stores to memory was not rejected"); + } for (const auto& test : cases) { const auto before = Fill(block, layout); const auto color = DecodeTarget(block); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp new file mode 100644 index 000000000..5f6fcd90d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpEqF16Clamp.cpp @@ -0,0 +1,143 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t VccLow = 106u; +constexpr std::uint32_t VectorSourceBase = 256u; +constexpr std::uint32_t ShaderCmpEqF16 = 0xcau; +constexpr std::uint32_t ShaderCmpLeF64 = 0x23u; +constexpr std::uint32_t ShaderCmpNgtF64 = 0x2bu; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct Row { + std::array sources; + std::uint32_t expected; +}; + +struct Vop3CompareWords { + std::uint32_t word0; + std::uint32_t word1; +}; + +Vop3CompareWords Vop3Compare(std::uint32_t opcode, std::uint32_t source0, std::uint32_t source1) { + const std::uint32_t encoded0 = VectorSourceBase + source0; + const std::uint32_t encoded1 = VectorSourceBase + source1; + return {0xd4000000u | (opcode << 16u) | VccLow | 0x8000u, encoded0 | (encoded1 << 9u)}; +} + +std::vector ClampCode(const Vop3CompareWords& compare) { + return { + 0x34020082u, 0x34060082u, + 0xe0302000u, 0x80000401u, + 0xe0302004u, 0x80000501u, + 0xe0302008u, 0x80000601u, + 0xe030200cu, 0x80000701u, + 0xbf8c3f70u, + compare.word0, compare.word1, + 0xd5010014u, 0x01a90280u, + 0xe0702000u, 0x80011403u, + 0xbf810000u, + }; +} + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void RunClampRows(AgcDriver::VulkanDevice& device, const char* name, const Vop3CompareWords& compare, + const ShaderRecompiler::ShaderFloatMode& floatMode, const std::vector& rows) { + Input.fill(0u); + for (std::size_t index = 0; index < rows.size(); ++index) { + std::copy(rows[index].sources.begin(), rows[index].sources.end(), + Input.begin() + static_cast(index * Inputs)); + } + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const auto code = ClampCode(compare); + const std::span words(code); + const std::array memory{{{reinterpret_cast(words.data()), std::as_bytes(words)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(words.data()), words, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.context.floatMode = floatMode; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(words.data())); + device.WaitIdle(); + for (std::size_t index = 0; index < rows.size(); ++index) { + const auto actual = Output[index * Results]; + Require(actual == rows[index].expected, std::string(name) + " clamp row " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(rows[index].expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const ShaderRecompiler::ShaderFloatMode denormalsKept{0xf0u, true, true, false}; + const ShaderRecompiler::ShaderFloatMode denorm32Flushed{0xc0u, true, false, false}; + RunClampRows(*device, "v_cmp_eq_f16_e64 vcc_lo, v4, v5", Vop3Compare(ShaderCmpEqF16, 4u, 5u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x0000bc00u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00003555u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00000001u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x0000fc00u, 0x00008000u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00007c01u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0x00006400u, 0x00003c00u, 0x00000000u, 0x00000000u}, 0x00000000u}, + {{0xbeef8000u, 0xbeefbc00u, 0xbeef0000u, 0x00000000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_le_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpLeF64, 4u, 6u), denorm32Flushed, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000000u}, + {{0x00000000u, 0xfff00000u, 0x00000000u, 0x40040000u}, 0x00000001u}, + {{0x00000001u, 0x7ff00000u, 0x00000000u, 0x41e00000u}, 0x00000000u}, + }); + RunClampRows(*device, "v_cmp_ngt_f64_e64 vcc_lo, v[4:5], v[6:7]", Vop3Compare(ShaderCmpNgtF64, 4u, 6u), denormalsKept, { + {{0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, 0x00000001u}, + {{0x00000000u, 0xbff00000u, 0x00000000u, 0xfff00000u}, 0x00000000u}, + {{0x00000000u, 0xc0040000u, 0x00000000u, 0xbff00000u}, 0x00000001u}, + {{0x00000000u, 0x00100000u, 0x00000001u, 0x7ff00000u}, 0x00000001u}, + }); + std::puts("v_cmp clamp tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp new file mode 100644 index 000000000..d7a66c2b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtI16F16OutputModifiers.cpp @@ -0,0 +1,136 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 16; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +constexpr std::uint32_t RowCount = 16; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, + 0x7e1402ffu, 0x5a5a5a5au, 0xd5d3000au, 0x00000104u, + 0xe0781000u, 0x80010a03u, 0xbf810000u, +}; + +struct Variant { + const char* modifiers; + std::uint32_t clamp; + std::uint32_t omod; +}; + +constexpr Variant Variants[] = { + {"mul:2", 0, 1}, + {"mul:4", 0, 2}, + {"div:2", 0, 3}, + {"clamp", 1, 0}, + {"clamp mul:2", 1, 1}, +}; + +constexpr std::uint32_t Rows[RowCount] = { + 0x00003c00u, + 0x0000bc00u, + 0x0000c100u, + 0x00004200u, + 0x00006400u, + 0x00005640u, + 0x0000d640u, + 0x00007bffu, + 0x0000fbffu, + 0x00007e00u, + 0xbeef3c00u, + 0xbeefc100u, + 0x00007c00u, + 0x0000fc00u, + 0x00007c01u, + 0x00003e00u, +}; + +constexpr std::uint32_t Expected[RowCount] = { + 0x5a5a0001u, 0x5a5affffu, 0x5a5afffeu, 0x5a5a0003u, + 0x5a5a0400u, 0x5a5a0064u, 0x5a5aff9cu, 0x5a5a7fffu, + 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, 0x5a5afffeu, + 0x5a5a7fffu, 0x5a5a8000u, 0x5a5a0000u, 0x5a5a0001u, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::ShaderFloatMode& floatMode, const Variant& variant) { + Code[7] = 0xd5d3000au | variant.clamp << 15u; + Code[8] = 0x00000104u | variant.omod << 27u; + Input.fill(0u); + for (std::uint32_t tid = 0; tid < RowCount; ++tid) Input[tid * Inputs] = Rows[tid]; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* mode, const Variant& variant) { + for (std::uint32_t tid = 0; tid < RowCount; ++tid) { + const std::uint32_t actual = Output[tid * Results]; + Require(actual == Expected[tid], std::string("v_cvt_i16_f16 ") + variant.modifiers + " " + mode + ": lane " + std::to_string(tid) + + " of " + Hex(Rows[tid]) + " is " + Hex(actual) + ", expected " + Hex(Expected[tid])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const Variant& variant : Variants) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, variant); + Check("M1", variant); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, variant); + Check("M2", variant); + } + std::puts("v_cvt_i16_f16 output modifiers tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp new file mode 100644 index 000000000..4e90961e5 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPknormF16SourceModifiers.cpp @@ -0,0 +1,174 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 2; +constexpr std::uint32_t RowCount = 8; +constexpr std::uint32_t VariantCount = 8; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) std::array Code{ + 0x34020084u, 0x34060083u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd712000au, 0x00020b04u, + 0xd713000bu, 0x00020b04u, 0xe0701000u, 0x80010a03u, 0xe0701004u, 0x80010b03u, 0xbf810000u, +}; + +struct Variant { + const char* operands; + std::uint32_t abs; + std::uint32_t neg; + std::uint32_t opSel; +}; + +constexpr Variant Variants[VariantCount] = { + {"|v4|, v5", 1, 0, 0}, + {"v4, |v5|", 2, 0, 0}, + {"-v4, v5", 0, 1, 0}, + {"v4, -v5", 0, 2, 0}, + {"-|v4|, v5", 1, 1, 0}, + {"v4, -|v5|", 2, 2, 0}, + {"v4, v5 op_sel:[1,0,0]", 0, 0, 1}, + {"v4, v5 op_sel:[0,1,0]", 0, 0, 2}, +}; + +constexpr std::uint32_t Rows[RowCount][2] = { + {0x00000400u, 0x00000400u}, + {0x000083ffu, 0x000083ffu}, + {0x00003555u, 0x0000d640u}, + {0x0000bc00u, 0x00003c00u}, + {0x00007bffu, 0x00000400u}, + {0xbeef0000u, 0xbeef3c00u}, + {0xbeef8000u, 0xbeef0000u}, + {0xbeef0000u, 0xbeeffc00u}, +}; + +constexpr std::uint32_t Expected[VariantCount][Results][RowCount] = { + { + {0x00020002u, 0xfffe0002u, 0x80012aa8u, 0x7fff7fffu, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040004u, 0x00000004u, 0x00005550u, 0xffffffffu, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020002u, 0x0002fffeu, 0x7fff2aa8u, 0x7fff8001u, 0x00027fffu, 0x7fff0000u, 0x00000000u, 0x7fff0000u}, + {0x00040004u, 0x00040000u, 0xffff5550u, 0xffff0000u, 0x0004ffffu, 0xffff0000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffe0002u, 0x8001d558u, 0x7fff7fffu, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000004u, 0x00000000u, 0xffffffffu, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0x0002fffeu, 0x7fff2aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x7fff0000u}, + {0x00000004u, 0x00040000u, 0xffff5550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0xffff0000u}, + }, + { + {0x0002fffeu, 0xfffefffeu, 0x8001d558u, 0x7fff8001u, 0x00028001u, 0x7fff0000u, 0x00000000u, 0x80010000u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0xfffe0002u, 0xfffefffeu, 0x80012aa8u, 0x80018001u, 0xfffe7fffu, 0x80010000u, 0x00000000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00020000u, 0xfffe0000u, 0x80010000u, 0x7fff0000u, 0x00020000u, 0x7fff8001u, 0x00008001u, 0x80018001u}, + {0x00040000u, 0x00000000u, 0x00000000u, 0xffff0000u, 0x00040000u, 0xffff0000u, 0x00000000u, 0x00000000u}, + }, + { + {0x00000002u, 0x0000fffeu, 0x00002aa8u, 0x00008001u, 0x00007fffu, 0x80010000u, 0x80010000u, 0x80010000u}, + {0x00000004u, 0x00000000u, 0x00005550u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u}, + }, +}; + +constexpr const char* Opcodes[Results] = {"v_cvt_pknorm_i16_f16_e64 v10, ", "v_cvt_pknorm_u16_f16_e64 v11, "}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, const Variant& variant) { + const std::uint32_t first = 0xd7120000u | variant.opSel << 11u | variant.abs << 8u | 10u; + const std::uint32_t second = 0xd7130000u | variant.opSel << 11u | variant.abs << 8u | 11u; + const std::uint32_t sources = 0x00020b04u | variant.neg << 29u; + Code[5] = first; + Code[6] = sources; + Code[7] = second; + Code[8] = sources; + for (std::uint32_t index = 0; index < RowCount; ++index) { + std::copy(std::begin(Rows[index]), std::end(Rows[index]), &Input[index * Inputs]); + } + for (std::uint32_t index = RowCount * Inputs; index < Input.size(); ++index) Input[index] = 0u; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = mode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const char* modeName, std::uint32_t variantIndex) { + for (std::uint32_t slot = 0; slot < Results; ++slot) { + for (std::uint32_t index = 0; index < RowCount; ++index) { + const std::uint32_t actual = Output[index * Results + slot]; + const std::uint32_t expected = Expected[variantIndex][slot][index]; + Require(actual == expected, + std::string("v_cvt_pknorm f16 source modifiers: ") + modeName + " row " + std::to_string(index) + " " + + Opcodes[slot] + Variants[variantIndex].operands + " of " + Hex(Rows[index][0]) + " and " + + Hex(Rows[index][1]) + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (std::uint32_t index = 0; index < VariantCount; ++index) { + Run(*device, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}, Variants[index]); + Check("FLOAT_MODE 0xf0", index); + Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, Variants[index]); + Check("FLOAT_MODE 0xc0", index); + } + std::puts("v_cvt_pknorm f16 source modifiers tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp new file mode 100644 index 000000000..ea59418ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineFixture.hpp @@ -0,0 +1,35 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TOOLS_COMPUTEPIPELINEFIXTURE_HPP + +#include "prx/libSceAgcDriver/Execution/include/ComputePipelineCapture.hpp" + +namespace AgcDriver { + +inline ComputePipelineCapture ComputePipelineFixture() { + ComputePipelineCapture capture; + capture.spirv = { + 0x07230203u, 0x00010000u, 0u, 7u, 0u, + 0x00020011u, 1u, + 0x0003000eu, 0u, 1u, + 0x0005000fu, 5u, 3u, 0x6e69616du, 0u, + 0x00060010u, 3u, 17u, 1u, 1u, 1u, + 0x00040047u, 6u, 1u, 7u, + 0x00020013u, 1u, + 0x00030021u, 2u, 1u, + 0x00040015u, 5u, 32u, 0u, + 0x00040032u, 5u, 6u, 1u, + 0x00050036u, 1u, 3u, 0u, 2u, + 0x000200f8u, 4u, + 0x000100fdu, + 0x00010038u + }; + capture.specialization.push_back({7u, 3u}); + capture.sets = {{0, {{3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2, VK_SHADER_STAGE_COMPUTE_BIT, 0}}}}; + capture.pushConstants = {{VK_SHADER_STAGE_COMPUTE_BIT, 16, 16}}; + capture.flags = VK_PIPELINE_CREATE_DISPATCH_BASE_BIT; + return capture; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp new file mode 100644 index 000000000..5baabe910 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tools/ComputePipelineReplay.cpp @@ -0,0 +1,154 @@ +#include "ComputePipelineFixture.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Options { + std::filesystem::path source; + std::filesystem::path cacheIn; + std::filesystem::path cacheOut; + std::filesystem::path output; + std::filesystem::path fixtureDirectory; + bool fixture = false; +}; + +Options Parse(int argc, char** argv) { + Options result; + for (int index = 1; index < argc; ++index) { + const std::string_view argument(argv[index]); + const auto value = [&](std::filesystem::path& destination) { + if (!destination.empty() || ++index == argc || *argv[index] == '\0') throw std::runtime_error("missing or repeated option: " + std::string(argument)); + destination = argv[index]; + }; + if (argument == "--cache-in") value(result.cacheIn); + else if (argument == "--cache-out") value(result.cacheOut); + else if (argument == "--output") value(result.output); + else if (argument == "--write-fixture") value(result.fixtureDirectory); + else if (argument == "--fixture" && !result.fixture) result.fixture = true; + else if (argument.starts_with("--") || !result.source.empty()) throw std::runtime_error("unknown or repeated argument: " + std::string(argument)); + else result.source = argument; + } + const auto sources = static_cast(!result.source.empty()) + static_cast(result.fixture) + static_cast(!result.fixtureDirectory.empty()); + if (sources != 1) throw std::runtime_error("select a capture file, --fixture, or --write-fixture DIRECTORY"); + if (!result.fixtureDirectory.empty() && (!result.cacheIn.empty() || !result.cacheOut.empty() || !result.output.empty())) throw std::runtime_error("--write-fixture cannot be combined with replay options"); + for (const auto& path : {result.cacheOut, result.output}) { + if (!path.empty() && std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + } + if (!result.cacheOut.empty() && !result.output.empty() && std::filesystem::absolute(result.cacheOut).lexically_normal() == std::filesystem::absolute(result.output).lexically_normal()) throw std::runtime_error("cache and report must use different paths"); + return result; +} + +std::vector ReadCache(const std::filesystem::path& path) { + if (path.empty()) return {}; + const auto bytes = std::filesystem::file_size(path); + if (bytes == 0 || bytes > 64u * 1024u * 1024u) throw std::runtime_error("imported pipeline cache must contain 1 byte to 64 MiB"); + std::vector result(static_cast(bytes)); + std::ifstream input(path, std::ios::binary); + input.read(reinterpret_cast(result.data()), static_cast(result.size())); + if (!input || input.peek() != std::char_traits::eof()) throw std::runtime_error("could not read complete pipeline cache: " + path.string()); + return result; +} + +void WriteBytes(const std::filesystem::path& path, std::span bytes) { + if (std::filesystem::exists(path)) throw std::runtime_error("output already exists: " + path.string()); + std::ofstream output(path, std::ios::binary); + output.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + output.close(); + if (!output) throw std::runtime_error("could not write complete output: " + path.string()); +} + +std::string Quote(std::string_view text) { + std::ostringstream result; + result << '"'; + for (const unsigned char character : text) { + if (character == '\\' || character == '"') result << '\\' << character; + else if (character < 0x20u) result << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(character); + else result << character; + } + result << '"'; + return result.str(); +} + +std::string Uuid(std::span bytes) { + std::ostringstream result; + for (const auto byte : bytes) result << std::hex << std::setw(2) << std::setfill('0') << static_cast(byte); + return result.str(); +} + +template +void WriteDevice(std::ostream& output, const TDevice& device) { + output << "{\"name\":" << Quote(device.deviceName) + << ",\"vendor_id\":" << device.vendorId << ",\"device_id\":" << device.deviceId + << ",\"driver_version_raw\":" << device.driverVersion << ",\"api_version_raw\":" << device.apiVersion + << ",\"pipeline_cache_uuid\":" << Quote(Uuid(device.pipelineCacheUuid)) << '}'; +} + +std::string Report(const Options& options, const AgcDriver::ComputePipelineCapture& capture, const AgcDriver::ComputePipelineReplayResult& replay) { + std::ostringstream output; + output << std::setprecision(12) + << "{\n \"schema_version\":1,\n \"source\":" << Quote(options.fixture ? "synthetic-no-op-compute" : options.source.string()) + << ",\n \"capture_identity\":" << Quote(capture.Identity()) + << ",\n \"capture_identity_algorithm\":\"fnv1a64\"" + << ",\n \"spirv_words\":" << capture.spirv.size() + << ",\n \"descriptor_sets\":" << capture.sets.size() + << ",\n \"push_constant_ranges\":" << capture.pushConstants.size() + << ",\n \"specialization_constants\":" << capture.specialization.size() + << ",\n \"required_subgroup_size\":" << capture.requiredSubgroupSize + << ",\n \"pipeline_flags\":" << capture.flags + << ",\n \"stage_flags\":" << capture.stageFlags + << ",\n \"captured_device\":"; + WriteDevice(output, capture); + output << ",\n \"replay_device\":"; + WriteDevice(output, replay); + output << ",\n \"application_cache_policy\":" << Quote(options.cacheIn.empty() ? "fresh" : "imported") + << ",\n \"cache_import_path\":" << Quote(options.cacheIn.string()) + << ",\n \"cache_import_bytes\":" << replay.initialCacheBytes + << ",\n \"cache_export_path\":" << Quote(options.cacheOut.string()) + << ",\n \"cache_export_bytes\":" << (options.cacheOut.empty() ? 0u : replay.cache.size()) + << ",\n \"driver_cache_state\":\"uncontrolled\"" + << ",\n \"dispatch_count\":0,\n \"pipeline_statistics\":false" + << ",\n \"milliseconds\":{\"shader_module\":" << replay.moduleMilliseconds + << ",\"layout\":" << replay.layoutMilliseconds << ",\"pipeline_create\":" << replay.pipelineMilliseconds + << ",\"cache\":" << replay.cacheMilliseconds << ",\"replay_total\":" << replay.totalMilliseconds << "}\n}\n"; + return output.str(); +} + +} + +int main(int argc, char** argv) { + if (argc == 2 && std::string_view(argv[1]) == "--help") { + std::cout << "agc_compute_pipeline_replay CAPTURE|--fixture [--cache-in FILE] [--cache-out FILE] [--output FILE]\n" + "agc_compute_pipeline_replay --write-fixture DIRECTORY\n" + "Compiles one compute pipeline without dispatch. Fresh application cache does not clear driver caches.\n"; + return 0; + } + try { + const auto options = Parse(argc, argv); + if (!options.fixtureDirectory.empty()) { + std::cout << AgcDriver::ComputePipelineFixture().Write(options.fixtureDirectory).string() << '\n'; + return 0; + } + const auto* statistics = std::getenv("APS5_PIPELINE_STATS"); + if (statistics != nullptr && std::string_view(statistics) != "0" && *statistics != '\0') throw std::runtime_error("unset APS5_PIPELINE_STATS before benchmarking"); + const auto capture = options.fixture ? AgcDriver::ComputePipelineFixture() : AgcDriver::ComputePipelineCapture::Read(options.source); + if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) throw std::runtime_error("capture requests pipeline statistics; capture again with APS5_PIPELINE_STATS disabled"); + const auto cache = ReadCache(options.cacheIn); + AgcDriver::VulkanDevice device(AgcDriver::VulkanDevice::CompileOnly{}); + const auto replay = device.ReplayComputePipeline(capture, cache); + if (!options.cacheOut.empty()) WriteBytes(options.cacheOut, replay.cache); + const auto report = Report(options, capture, replay); + if (!options.output.empty()) WriteBytes(options.output, std::as_bytes(std::span(report.data(), report.size()))); + std::cout << report; + return 0; + } catch (const std::exception& error) { + std::cerr << "compute pipeline replay: " << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAmpr/CMakeLists.txt b/core/libs/prx/libSceAmpr/CMakeLists.txt index 09a966fea..896ffaf20 100644 --- a/core/libs/prx/libSceAmpr/CMakeLists.txt +++ b/core/libs/prx/libSceAmpr/CMakeLists.txt @@ -14,6 +14,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_amm_placement_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmmPlacement.cpp) + target_include_directories(guest_amm_placement_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_amm_placement_tests PRIVATE libSceAmpr libkernel libc) + configure_windows_unwind(guest_amm_placement_tests) + add_test(NAME guest_amm_placement COMMAND guest_amm_placement_tests) + if(WIN32) + set_tests_properties(guest_amm_placement PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_ampr_threads_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmprThreads.cpp) target_include_directories(guest_ampr_threads_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_ampr_threads_tests PRIVATE libSceAmpr libkernel libc) diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index d54946870..db869eb29 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -234,6 +234,8 @@ static std::atomic_flag sharedPtrSpinLock; extern "C" { int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +int SignalCondition_nid_no_patch(void** handle); } static KernelTimespec XtimeDeadline(const Xtime& time) { @@ -895,18 +897,16 @@ int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_n return 0; } -int APS5_VABI _Cnd_signal_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { + return SignalCondition_nid_no_patch(handle); } void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void) { sharedPtrSpinLock.clear(std::memory_order_release); } -int APS5_VABI _Cnd_init_with_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); } int APS5_VABI _ZTVN10__cxxabiv120__function_type_infoE_nid_postfix(void) { diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index 50e917ee5..b07ad2150 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -21,6 +21,7 @@ constexpr int AUDIO_IN_ERROR_PORT_FULL = static_cast(0x80260107); constexpr int AUDIO_IN_ERROR_BUSY = static_cast(0x8026010A); constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 1; constexpr std::uint32_t MAX_QUEUED_BLOCKS = 4; +constexpr std::uint32_t MAX_ASYNC_LEN = 384; using Clock = std::chrono::steady_clock; @@ -75,6 +76,32 @@ Port* find(int handle) { return port.used ? &port : nullptr; } +int checkArguments(std::uint32_t type, std::uint32_t index, std::uint32_t len, std::uint32_t freq) { + if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; + if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; + if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; + if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; + return 0; +} + +int openPort(std::uint32_t len, std::uint32_t freq, std::uint32_t param, const char* function) { + if (param == 0x10) { + NotImplemented_nid_no_patch(function); + return 0; + } + Format format{}; + if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; + std::lock_guard lock(g_mutex); + for (std::size_t i = 0; i < g_ports.size(); ++i) { + if (g_ports[i].used) continue; + const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); + const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); + g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; + return static_cast(i + 1); + } + return AUDIO_IN_ERROR_PORT_FULL; +} + std::size_t capture(SDL_AudioDeviceID device, std::uint8_t* dest, std::size_t bytes, Clock::time_point deadline) { while (SDL_GetQueuedAudioSize(device) < bytes && Clock::now() < deadline) PreciseSleepUs(1000); const std::size_t queued = SDL_GetQueuedAudioSize(device); @@ -136,25 +163,8 @@ int APS5_VABI sceAudioInInput(int handle, void* dest) { int APS5_VABI sceAudioInOpen(int user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { (void)user_id; - if (type != 0 && type != 1) return AUDIO_IN_ERROR_INVALID_TYPE; - if (index != 0) return AUDIO_IN_ERROR_INVALID_PARAM; - if (len != 128 && len != 256) return AUDIO_IN_ERROR_INVALID_SIZE; - if (freq != 48000 && freq != 16000) return AUDIO_IN_ERROR_INVALID_FREQ; - if (param == 0x10) { - NotImplemented_nid_no_patch(__func__); - return 0; - } - Format format{}; - if (!formatOf(param, format)) return AUDIO_IN_ERROR_INVALID_PARAM; - std::lock_guard lock(g_mutex); - for (std::size_t i = 0; i < g_ports.size(); ++i) { - if (g_ports[i].used) continue; - const auto frameBytes = static_cast(SDL_AUDIO_BITSIZE(format.format) / 8 * format.channels); - const auto grain = std::chrono::duration_cast(std::chrono::microseconds(1000000ull * len / freq)); - g_ports[i] = {true, false, len, frameBytes, openDevice(freq, len, format), grain, {}}; - return static_cast(i + 1); - } - return AUDIO_IN_ERROR_PORT_FULL; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInClose(int32_t handle) { @@ -168,29 +178,28 @@ int32_t APS5_VABI sceAudioInClose(int32_t handle) { } int32_t APS5_VABI sceAudioInHqOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + if (const int error = checkArguments(type, index, len, freq); error != 0) return error; + if (len != 128 || freq != 48000) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + return openPort(len, freq, param, __func__); } int32_t APS5_VABI sceAudioInAsyncOpen(int32_t user_id, uint32_t type, uint32_t index, uint32_t len, uint32_t freq, uint32_t param) { - (void)user_id; - (void)type; - (void)index; - (void)len; - (void)freq; - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)user_id; + const int error = checkArguments(type, index, len, freq); + if (error == AUDIO_IN_ERROR_INVALID_SIZE && len <= MAX_ASYNC_LEN) { + NotImplemented_nid_no_patch(__func__); + return 0; + } + if (error != 0) return error; + return openPort(len, freq, param, __func__); } -APS5_EXPORT("X+4jdIS75P0", sceAudioInUnknown_X4jdIS75P0); -int32_t APS5_VABI sceAudioInUnknown_X4jdIS75P0(void) { +int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) { + (void)handle; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index d6baf7b3d..768358e69 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAvPlayer/include/AvPlayer.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include #include @@ -243,7 +244,9 @@ public: format->pb = replacement->Context(); format->flags |= AVFMT_FLAG_CUSTOM_IO; } else { - const auto resolved = ResolvePath_nid_no_patch(path.c_str()).u8string(); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(path.c_str(), hostPath) != 0) return false; + const auto resolved = hostPath.u8string(); url = "file:" + std::string(resolved.begin(), resolved.end()); } if (const int error = avformat_open_input(&format, replacement ? nullptr : url.c_str(), nullptr, nullptr); error < 0) { diff --git a/core/libs/prx/libSceContentExport/CMakeLists.txt b/core/libs/prx/libSceContentExport/CMakeLists.txt index 7efbba37e..59e62f64b 100644 --- a/core/libs/prx/libSceContentExport/CMakeLists.txt +++ b/core/libs/prx/libSceContentExport/CMakeLists.txt @@ -15,7 +15,7 @@ if(BUILD_TESTING) add_test_executable(guest_content_unimplemented_tests ${LIBS_INCLUDE_DIR}/tests/GuestContentUnimplemented.cpp) target_include_directories(guest_content_unimplemented_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentExport libSceContentSearch libSceContentDelete libc) + target_link_libraries(guest_content_unimplemented_tests PRIVATE libSceContentSearch libSceContentDelete libc) configure_windows_unwind(guest_content_unimplemented_tests) add_test(NAME guest_content_unimplemented COMMAND guest_content_unimplemented_tests) if(WIN32) diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index a626f6a42..f618daeea 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -3,11 +3,23 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include +#include +#include #include #include namespace { +constexpr std::size_t MaxExecution = 10; std::atomic g_initialized{false}; +std::mutex g_sessionMutex; +std::set g_sessions; +std::int32_t g_lastSessionId = 0; + +void RequireSession(const char* function, std::int32_t expId) { + if (!g_initialized.load()) throw std::logic_error(std::string(function) + ": not initialized"); + if (!g_sessions.contains(expId)) throw std::invalid_argument(std::string(function) + ": unknown export session"); +} } extern "C" { @@ -21,9 +33,11 @@ int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param) { return 0; } -int APS5_VABI sceContentExportFinish(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFinish(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + g_sessions.erase(expId); + return 0; } int APS5_VABI sceContentExportFromData(void) { @@ -42,26 +56,36 @@ int APS5_VABI sceContentExportFromFileWithThumbnail(void) { } int APS5_VABI sceContentExportStart(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_sessionMutex); + if (!g_initialized.load()) throw std::logic_error(std::string(__func__) + ": not initialized"); + if (g_sessions.size() >= MaxExecution) throw std::runtime_error(std::string(__func__) + ": behaviour beyond 10 live export sessions is unknown"); + if (g_lastSessionId == std::numeric_limits::max()) throw std::runtime_error(std::string(__func__) + ": export session ids exhausted"); + g_sessions.insert(++g_lastSessionId); + return g_lastSessionId; } -int APS5_VABI sceContentExportGetProgress(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportGetProgress(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": progress of a session with no running export is unknown"); } -int APS5_VABI sceContentExportCancel(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportCancel(std::int32_t expId) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": cancelling a session with no running export is unknown"); } -int APS5_VABI sceContentExportFromDataWithThumbnail(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength) { + std::lock_guard lock(g_sessionMutex); + RequireSession(__func__, expId); + throw std::runtime_error(std::string(__func__) + ": the host has no media library to export into"); } int APS5_VABI sceContentExportTerm(void) { + std::lock_guard lock(g_sessionMutex); + if (!g_sessions.empty()) throw std::runtime_error(std::string(__func__) + ": terminating with live export sessions is unknown"); bool expected = true; if (!g_initialized.compare_exchange_strong(expected, false)) throw std::logic_error(std::string(__func__) + ": not initialized"); return 0; diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index 2ac60a0cd..ec43b33b0 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -27,4 +27,9 @@ int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { return 0; } +int APS5_VABI sceConvertKeycodeGetCharacter(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFiber/Export.cpp b/core/libs/prx/libSceFiber/Export.cpp index 83572e7d1..5935f1bad 100644 --- a/core/libs/prx/libSceFiber/Export.cpp +++ b/core/libs/prx/libSceFiber/Export.cpp @@ -458,6 +458,10 @@ int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_poin return SCE_OK; } +int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(uint64_t* addr_frame_pointer) { + return sceFiberGetThreadFramePointerAddress(addr_frame_pointer); +} + int32_t APS5_VABI sceFiberStartContextSizeCheck(uint32_t flags) { if (flags != 0) return SCE_FIBER_ERROR_INVALID; bool expected = false; diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index ae87bf31b..ef118bd83 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -15,6 +15,7 @@ #include #include "prx/libSceFont/include/FontInternal.hpp" +#include "prx/libc/include/FilesystemError.hpp" namespace { @@ -280,7 +281,11 @@ int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::ui return SCE_FONT_ERROR_INVALID_LIBRARY; } if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; - const std::filesystem::path hostPath = path ? ResolvePath_nid_no_patch(path) : std::filesystem::path{}; + std::filesystem::path hostPath; + if (path && ResolvePathError_nid_no_patch(path, hostPath) != 0) { + *pFontHandle = nullptr; + return SCE_FONT_ERROR_FS_OPEN_FAILED; + } const std::string hostPathString = hostPath.string(); std::uint32_t libraryLock = 0; const int beginRc = BeginOpen(library, pFontHandle, libraryLock); diff --git a/core/libs/prx/libSceFont/src/Text.cpp b/core/libs/prx/libSceFont/src/Text.cpp index 5a43ff63f..c34ed9a2d 100644 --- a/core/libs/prx/libSceFont/src/Text.cpp +++ b/core/libs/prx/libSceFont/src/Text.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "prx/libSceFont/include/FontInternal.hpp" @@ -63,10 +64,11 @@ struct FontWritingData { bool hasCurrentStep = false; bool maskFormatCharacters = true; bool ownsStorage = false; + bool hasRawGlyphMetrics = false; FontWritingStep step{}; FontWritingMetrics metrics{}; FontGlyphMetrics rawGlyphMetrics{}; - bool hasRawGlyphMetrics = false; + FontWritingLetterStep letterStep{}; }; static_assert(sizeof(FontWritingData) <= sizeof(FontWriting)); @@ -491,6 +493,16 @@ int APS5_VABI sceFontCharacterGetBidiLevel(const FontTextCharacter* textCharacte return SCE_FONT_OK; } +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter* textCharacter, int* syllableStringState) { + if (!textCharacter || !syllableStringState) { + if (syllableStringState) *syllableStringState = 0; + return SCE_FONT_ERROR_INVALID_PARAMETER; + } + if (textCharacter->characterCode >= 0x0E00 && textCharacter->characterCode <= 0x0E7F) throw std::runtime_error("libSceFont: sceFontCharacterGetSyllableStringState for Thai syllable clusters is not implemented"); + *syllableStringState = 0; + return SCE_FONT_OK; +} + int APS5_VABI sceFontCharacterGetTextFontCode(const FontTextCharacter* textCharacter, FontHandle* pFontHandle, std::uint32_t* textCode) { if (!textCharacter) { if (pFontHandle) *pFontHandle = nullptr; @@ -596,21 +608,22 @@ const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting* fon return &writing->step; } -FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, FontWritingLetterStep* letterStep) { - if (letterStep) std::memset(letterStep, 0, sizeof(*letterStep)); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting* fontWriting, const FontWritingLetterStep** pLetterStep) { + if (pLetterStep) *pLetterStep = nullptr; auto* writing = GetWritingData(fontWriting); if (!writing || !writing->hasCurrentStep || !writing->storage || writing->currentIndex >= writing->storage->characters.size()) return nullptr; - if (letterStep) { - letterStep->x = writing->step.x; - letterStep->y = writing->step.y; - letterStep->advanceX = writing->step.advanceX; - letterStep->advanceY = writing->step.advanceY; - letterStep->Components.textsCount = 1; - letterStep->Components.textsIndex = static_cast(writing->currentIndex); - letterStep->Components.glyphsCount = 1; - letterStep->Components.glyphsIndex = static_cast(writing->currentIndex); - letterStep->Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); - } + auto& letterStep = writing->letterStep; + letterStep = {}; + letterStep.x = writing->step.x; + letterStep.y = writing->step.y; + letterStep.advanceX = writing->step.advanceX; + letterStep.advanceY = writing->step.advanceY; + letterStep.Components.textsCount = 1; + letterStep.Components.textsIndex = static_cast(writing->currentIndex); + letterStep.Components.glyphsCount = 1; + letterStep.Components.glyphsIndex = static_cast(writing->currentIndex); + letterStep.Components.characterTextCount = static_cast(std::max(writing->step.Profile.characterCount, 1)); + if (pLetterStep) *pLetterStep = &letterStep; return &writing->storage->characters[writing->currentIndex]; } diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index 53b3e2ec9..b3c2e79b7 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -7,11 +7,6 @@ extern "C" { -int APS5_VABI sceFontCharacterGetSyllableStringState() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontCharactersRefersTextCodes() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index 2e1740373..9cfafc35a 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -21,6 +21,11 @@ int APS5_VABI sceHmd2Close() { return 0; } +int APS5_VABI sceHmd2GazeGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2GazeGetResultForFoveatedRendering() { NotImplemented_nid_no_patch(__func__); return 0; @@ -46,16 +51,36 @@ int APS5_VABI sceHmd2ReprojectionBeginFrame() { return 0; } +int APS5_VABI sceHmd2ReprojectionDisableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionDisableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionEnableMirroring() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionEnableVrMode() { NotImplemented_nid_no_patch(__func__); return 0; } +int APS5_VABI sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionGetPredictedDisplayTime() { NotImplemented_nid_no_patch(__func__); return 0; @@ -81,6 +106,16 @@ int APS5_VABI sceHmd2ReprojectionSetAllowPositionalReprojection() { return 0; } +int APS5_VABI sceHmd2ReprojectionSetMirroringOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionSetParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2ReprojectionSetParamWithBuffer() { NotImplemented_nid_no_patch(__func__); return 0; @@ -91,6 +126,16 @@ int APS5_VABI sceHmd2ReprojectionSetRenderConfig() { return 0; } +int APS5_VABI sceHmd2ReprojectionSetTiming() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHmd2ReprojectionTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceHmd2SetVibration() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 5c930a4e2..48a098bec 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -5,7 +5,8 @@ configure_windows_unwind(libSceHttp) if(BUILD_TESTING) add_test_executable(guest_http_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp.cpp) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_http_tests PRIVATE libSceHttp) + target_link_libraries(guest_http_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_tests) add_test(NAME guest_http COMMAND guest_http_tests) add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 3b9216532..86f20a870 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -1,16 +1,24 @@ #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" #include #include +#include #include #include +#include #include #include #include -#include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. @@ -22,6 +30,91 @@ struct RequestStatusRegistration { }; static std::mutex g_statusMutex; static std::map g_statusCallbacks; +struct HttpsCaList { + void* certs; + int certsNum; +}; + +#ifdef _WIN32 +struct HostMutex { + SRWLOCK native = SRWLOCK_INIT; + + HostMutex() = default; + HostMutex(const HostMutex&) = delete; + HostMutex& operator=(const HostMutex&) = delete; + + void lock() { AcquireSRWLockExclusive(&native); } + void unlock() { ReleaseSRWLockExclusive(&native); } +}; +#else +using HostMutex = std::mutex; +#endif + +class HostCondition { +public: + HostCondition() = default; + HostCondition(const HostCondition&) = delete; + HostCondition& operator=(const HostCondition&) = delete; + + void NotifyAll() { +#ifdef _WIN32 + WakeAllConditionVariable(&native); +#else + native.notify_all(); +#endif + } + + void Wait(std::unique_lock& lock) { +#ifdef _WIN32 + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, INFINITE, 0)) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait(lock); +#endif + } + + void WaitUntil(std::unique_lock& lock, std::chrono::steady_clock::time_point deadline) { +#ifdef _WIN32 + const auto now = std::chrono::steady_clock::now(); + if (deadline <= now) return; + const auto milliseconds = std::chrono::ceil(deadline - now).count(); + if (!SleepConditionVariableSRW(&native, &lock.mutex()->native, static_cast(milliseconds), 0) && + GetLastError() != ERROR_TIMEOUT) + throw std::runtime_error("libSceHttp: native condition wait failed"); +#else + native.wait_until(lock, deadline); +#endif + } + +private: +#ifdef _WIN32 + CONDITION_VARIABLE native = CONDITION_VARIABLE_INIT; +#else + std::condition_variable native; +#endif +}; + +struct HttpEpoll { + HostMutex mutex; + HostCondition wake; + bool aborted = false; + bool destroyed = false; +}; + +static std::mutex g_stateMutex; +static std::unordered_set g_contexts; +static std::unordered_map> g_epolls; + +static std::shared_ptr FindEpoll(HttpEpollHandle eh) { + const auto it = g_epolls.find(eh); + return it == g_epolls.end() ? nullptr : it->second; +} + +static void MarkDestroyed(HttpEpoll& epoll) { + std::lock_guard lock(epoll.mutex); + epoll.destroyed = true; + epoll.wake.NotifyAll(); +} extern "C" { @@ -55,9 +148,13 @@ int APS5_VABI sceHttpCreateConnectionWithURL(int tmpl_id, const char* url, int e } int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { - (void)http_ctx_id; + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; if (!eh) return ERROR_INVALID_VALUE; - *eh = new HttpEpoll{}; + auto epoll = std::make_shared(); + *eh = epoll.get(); + g_epolls.emplace(epoll.get(), std::move(epoll)); return 0; } @@ -102,8 +199,17 @@ int APS5_VABI sceHttpDeleteTemplate(int tmpl_id) { } int APS5_VABI sceHttpDestroyEpoll(int http_ctx_id, HttpEpollHandle eh) { - (void)http_ctx_id; - delete eh; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + g_epolls.erase(eh); + } + MarkDestroyed(*epoll); return 0; } @@ -131,7 +237,10 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s (void)memid; (void)ssl_ctx_id; (void)pool_size; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_stateMutex); + g_contexts.insert(id); + return id; } int APS5_VABI sceHttpsDisableOption(int id, uint32_t ssl_flags) { @@ -248,7 +357,16 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - (void)http_ctx_id; + std::vector> epolls; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; + if (!g_contexts.empty()) return 0; + for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); + g_epolls.clear(); + } + for (const auto& epoll : epolls) MarkDestroyed(*epoll); return 0; } @@ -258,10 +376,23 @@ int APS5_VABI sceHttpUnsetEpoll(int id) { } int APS5_VABI sceHttpWaitRequest(HttpEpollHandle eh, HttpNBEvent* nbev, int maxevents, int timeout) { - if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; - if (timeout < 0) NotImplemented_nid_no_patch(__func__); - std::this_thread::sleep_for(std::chrono::microseconds(timeout)); - return 0; + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh || !nbev || maxevents <= 0) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::unique_lock lock(epoll->mutex); + const auto woken = [&] { return epoll->aborted || epoll->destroyed; }; + if (timeout < 0) { + while (!woken()) epoll->wake.Wait(lock); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(timeout); + while (!woken() && std::chrono::steady_clock::now() < deadline) epoll->wake.WaitUntil(lock, deadline); + } + return epoll->aborted ? ERROR_ABORTED : 0; } int APS5_VABI sceHttpCreateRequestWithURL(int conn_id, int method, const char* url, uint64_t content_length) { @@ -434,8 +565,34 @@ int APS5_VABI sceHttpSetRedirectCallback(int id, HttpRedirectCallback cbfunc, vo return 0; } -int APS5_VABI sceHttpAbortWaitRequest(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle eh) { + std::shared_ptr epoll; + { + std::lock_guard lock(g_stateMutex); + if (g_contexts.empty()) return ERROR_BEFORE_INIT; + if (!eh) return ERROR_INVALID_VALUE; + epoll = FindEpoll(eh); + if (!epoll) return ERROR_INVALID_ID; + } + std::lock_guard lock(epoll->mutex); + epoll->aborted = true; + epoll->wake.NotifyAll(); + return 0; +} + +int APS5_VABI sceHttpsGetCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; + return 0; +} + +int APS5_VABI sceHttpsFreeCaList(int http_ctx_id, HttpsCaList* ca_list) { + (void)http_ctx_id; + if (!ca_list) return ERROR_INVALID_VALUE; + ca_list->certs = nullptr; + ca_list->certsNum = 0; return 0; } } diff --git a/core/libs/prx/libSceHttp/src/HttpErrors.hpp b/core/libs/prx/libSceHttp/src/HttpErrors.hpp index b95f8d25b..1b18ec3e1 100644 --- a/core/libs/prx/libSceHttp/src/HttpErrors.hpp +++ b/core/libs/prx/libSceHttp/src/HttpErrors.hpp @@ -1,8 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP #define CORE_LIBS_PRX_LIBSCEHTTP_SRC_HTTPERRORS_HPP +inline constexpr int ERROR_BEFORE_INIT = static_cast(0x80431001); inline constexpr int ERROR_OUT_OF_MEMORY = static_cast(0x80431022); inline constexpr int ERROR_NETWORK = static_cast(0x80431063); +inline constexpr int ERROR_ABORTED = static_cast(0x80431080); +inline constexpr int ERROR_INVALID_ID = static_cast(0x80431100); inline constexpr int ERROR_INVALID_VALUE = static_cast(0x804311FE); inline constexpr int ERROR_INVALID_URL = static_cast(0x80433060); inline constexpr int ERROR_PARSE_HTTP_INVALID_RESPONSE = static_cast(0x80432060); diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 6aac69db3..710a66572 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -182,7 +182,7 @@ int APS5_VABI sceHttpUriBuild(char* out, size_t* require, size_t prepare, const std::string uri; if (option & URI_BUILD_WITH_SCHEME && !scheme.empty()) uri.append(scheme).push_back(':'); - if (!src_element->opaque && option & URI_BUILD_WITH_HOSTNAME) uri.append("//"); + if (!src_element->opaque) uri.append("//"); uri.append(username); if (!password.empty()) uri.append(":").append(password); if (!username.empty() || !password.empty()) uri.push_back('@'); diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index e4869b148..f037bf7f9 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -6,19 +6,58 @@ #include "prx/libkernel/Equeue/Equeue.hpp" #include #include -#include #include #include #include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. static constexpr int ERROR_NETWORK = static_cast(0x80436063); static constexpr int ERROR_INVALID_ID = static_cast(0x817B1100); static constexpr int ERROR_NULL_POINTER = static_cast(0x817B1225); + +enum class Http2Kind : std::uint8_t { Context, Template, Request, CookieBox }; + +struct Http2Handle { + Http2Kind kind; + int owner; + std::size_t poolSize; +}; + static std::atomic g_nextHandle{1}; -static std::mutex g_poolsMutex; -static std::map g_pools; +static std::mutex g_handlesMutex; +static std::unordered_map g_handles; + +static Http2Handle* FindHandle(int id, Http2Kind kind) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() && entry->second.kind == kind ? &entry->second : nullptr; +} + +static Http2Handle* FindAnyHandle(int id) { + const auto entry = g_handles.find(id); + return entry != g_handles.end() ? &entry->second : nullptr; +} + +static std::vector ReleaseOwnedHandles(int owner) { + std::vector owned; + for (const auto& entry : g_handles) { + if (entry.second.owner == owner) owned.push_back(entry.first); + } + std::vector removed; + for (const int id : owned) { + const std::vector descendants = ReleaseOwnedHandles(id); + removed.insert(removed.end(), descendants.begin(), descendants.end()); + g_handles.erase(id); + removed.push_back(id); + } + return removed; +} + +static bool HandleIsLive(int id) { + std::lock_guard lock(g_handlesMutex); + return FindAnyHandle(id) != nullptr; +} struct Http2MemoryPoolStats { size_t pool_size; @@ -30,21 +69,36 @@ struct Http2MemoryPoolStats { static std::mutex g_completionsMutex; static std::unordered_map> g_completions; -static void CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { +static void EraseCompletions(const std::vector& ids) { + std::lock_guard lock(g_completionsMutex); + for (const int id : ids) g_completions.erase(id); +} + +static int CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) { if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function); if (option != nullptr) NotImplemented_nid_no_patch(function); + bool queued = false; { - std::lock_guard lock(g_completionsMutex); - Http2AsyncResult completion{}; - completion.req_id = req_id; - completion.result = ERROR_NETWORK; - g_completions[req_id].push_back(completion); + std::lock_guard handlesLock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) != nullptr) { + std::lock_guard completionsLock(g_completionsMutex); + Http2AsyncResult completion{}; + completion.req_id = req_id; + completion.result = ERROR_NETWORK; + g_completions[req_id].push_back(completion); + queued = true; + } } + if (!queued) return ERROR_INVALID_ID; if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) { - std::lock_guard lock(g_completionsMutex); - g_completions[req_id].pop_back(); + { + std::lock_guard lock(g_completionsMutex); + const auto pending = g_completions.find(req_id); + if (pending != g_completions.end() && !pending->second.empty()) pending->second.pop_back(); + } NotImplemented_nid_no_patch(function); } + return 0; } extern "C" { @@ -58,29 +112,46 @@ int APS5_VABI sceHttp2AddRequestHeader(int id, const char* name, const char* val } int APS5_VABI sceHttp2CreateRequestWithURL(int tmpl_id, const char* method, const char* url, uint64_t content_length) { - (void)tmpl_id; (void)method; (void)url; (void)content_length; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::Request, tmpl_id, 0}; + return id; } int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agent, int http_ver, int is_auto_proxy_conf) { - (void)lib_http2_ctx_id; (void)user_agent; (void)http_ver; (void)is_auto_proxy_conf; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::Template, lib_http2_ctx_id, 0}; + return id; } int APS5_VABI sceHttp2DeleteRequest(int req_id) { - std::lock_guard lock(g_completionsMutex); - g_completions.erase(req_id); + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(req_id, Http2Kind::Request) == nullptr) return ERROR_INVALID_ID; + g_handles.erase(req_id); + } + EraseCompletions({req_id}); return 0; } int APS5_VABI sceHttp2DeleteTemplate(int tmpl_id) { - (void)tmpl_id; + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(tmpl_id, Http2Kind::Template) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(tmpl_id); + g_handles.erase(tmpl_id); + } + EraseCompletions(removed); return 0; } @@ -109,8 +180,8 @@ int APS5_VABI sceHttp2Init(int libnet_mem_id, int libssl_ctx_id, size_t pool_siz (void)libssl_ctx_id; (void)max_concurrent_request; const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); - std::lock_guard lock(g_poolsMutex); - g_pools[id] = pool_size; + std::lock_guard lock(g_handlesMutex); + g_handles[id] = {Http2Kind::Context, 0, pool_size}; return id; } @@ -140,8 +211,7 @@ int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) { (void)post_data; (void)size; - CompleteAsync(__func__, req_id, kqueue_option, option); - return 0; + return CompleteAsync(__func__, req_id, kqueue_option, option); } int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) { @@ -219,26 +289,29 @@ int APS5_VABI sceHttp2SetTimeOut(int id, uint32_t usec) { } int APS5_VABI sceHttp2SslDisableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SslEnableOption(int id, uint32_t ssl_flags) { - (void)id; (void)ssl_flags; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2SetMinSslVersion(int id, uint32_t ssl_version) { - (void)id; (void)ssl_version; - return 0; + return HandleIsLive(id) ? 0 : ERROR_INVALID_ID; } int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) { - std::lock_guard lock(g_poolsMutex); - g_pools.erase(lib_http2_ctx_id); + std::vector removed; + { + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + removed = ReleaseOwnedHandles(lib_http2_ctx_id); + g_handles.erase(lib_http2_ctx_id); + } + EraseCompletions(removed); return 0; } @@ -260,13 +333,13 @@ int APS5_VABI sceHttp2AbortRequest(int req_id) { int APS5_VABI sceHttp2GetMemoryPoolStats(int lib_http2_ctx_id, Http2MemoryPoolStats* stats) { if (stats == nullptr) return ERROR_NULL_POINTER; - std::lock_guard lock(g_poolsMutex); - const auto pool = g_pools.find(lib_http2_ctx_id); - if (pool == g_pools.end()) { + std::lock_guard lock(g_handlesMutex); + const Http2Handle* handle = FindHandle(lib_http2_ctx_id, Http2Kind::Context); + if (handle == nullptr) { std::memset(stats, 0, sizeof(*stats)); return ERROR_INVALID_ID; } - *stats = {pool->second, 0, 0, 0}; + *stats = {handle->poolSize, 0, 0, 0}; return 0; } @@ -276,8 +349,11 @@ int APS5_VABI sceHttp2CookieFlush(int id) { } int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { - (void)lib_http2_ctx_id; - return g_nextHandle.fetch_add(1, std::memory_order_relaxed); + std::lock_guard lock(g_handlesMutex); + if (FindHandle(lib_http2_ctx_id, Http2Kind::Context) == nullptr) return ERROR_INVALID_ID; + const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); + g_handles[id] = {Http2Kind::CookieBox, lib_http2_ctx_id, 0}; + return id; } int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index 498690ee6..39965c3d7 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -529,6 +529,7 @@ std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String* self) { return bool APS5_VABI _ZNK3sce4Json6StringeqEPKc(const String* self, const char* text) { return text != nullptr && *self->text == text; } void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; @@ -539,6 +540,14 @@ ArrayIterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(ArrayIterator* result, cons ArrayIterator* APS5_VABI _ZNK3sce4Json5Array3endEv(ArrayIterator* result, const Array* self) { result->it = self->items->end(); return result; } bool APS5_VABI _ZNK3sce4Json5Array5emptyEv(const Array* self) { return self->items->empty(); } std::size_t APS5_VABI _ZNK3sce4Json5Array4sizeEv(const Array* self) { return self->items->size(); } +void APS5_VABI _ZN3sce4Json5Array5clearEv(Array* self) { + for (auto& item : *self->items) Destroy(item); + self->items->clear(); +} +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array* self) { + if (self->items->empty()) throw std::out_of_range("sce::Json::Array::front: empty array"); + return &self->items->front(); +} const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { if (self->items->empty()) throw std::out_of_range("sce::Json::Array::back: empty array"); return &self->items->back(); @@ -546,6 +555,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Array4backEv(const Array* self) { void APS5_VABI _ZN3sce4Json5Array8iteratorD1Ev(ArrayIterator* self) { (void)self; } ArrayIterator* APS5_VABI _ZN3sce4Json5Array8iteratorppEv(ArrayIterator* self) { ++self->it; return self; } Value* APS5_VABI _ZNK3sce4Json5Array8iteratordeEv(const ArrayIterator* self) { return &*self->it; } +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const ArrayIterator* self) { return &*self->it; } bool APS5_VABI _ZNK3sce4Json5Array8iteratorneERKS2_(const ArrayIterator* self, const ArrayIterator* other) { return self->it != other->it; } void APS5_VABI _ZN3sce4Json6ObjectC1Ev(Object* self) { Construct(*self); } @@ -755,6 +765,18 @@ int APS5_VABI _ZNK3sce4Json5Value8toStringERNS0_6StringE(const Value* self, Stri return 0; } +using ParserReadCallback = int (APS5_VABI*)(char&, void*); + +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size); + +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value* out, ParserReadCallback read, void* context) { + if (read == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null read callback"); + std::string text; + char ch = 0; + while (read(ch, context) == 0 && ch != '\0') text.push_back(ch); + return _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(out, text.data(), text.size()); +} + int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* text, std::size_t size) { if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); Node parsed{}; @@ -816,9 +838,8 @@ int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { return 0; } -int APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(InitParameter2* self) { + _ZN3sce4Json14InitParameter2C1Ev(self); } int APS5_VABI _ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv(void) { @@ -831,6 +852,26 @@ int APS5_VABI _ZN3sce4Json5Value21setNullAccessCallBackEPFRKS1_NS0_9ValueTypeEPS return 0; } +int APS5_VABI _ZN3sce4Json5Value30setElementAccessFailureHandlerEPFviPKNS0_6StringEPKmPKS1_S8_PvES9_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Value22setSpecialFloatHandlerEPFKS1_NS0_12FunctionTypeEdPS2_PvPbES5_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Value22setTypeMismatchHandlerEPFRKS1_NS0_9ValueTypeEPS2_S5_PvES6_(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZNK3sce4Json5ValuecvbEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI _ZN3sce4Json6Object4PairC1ERKNS0_6StringERKNS0_5ValueE() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 40dbd1e76..adb9ecdf4 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -2,8 +2,24 @@ add_library(libSceLibcInternal SHARED EXCLUDE_FROM_ALL Export.cpp FileTime.cpp U target_include_directories(libSceLibcInternal PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibcInternal PRIVATE ${CMAKE_DL_LIBS}) configure_windows_unwind(libSceLibcInternal) +target_sources(libSceLibcInternal PRIVATE FilesystemStatistics.cpp) if(BUILD_TESTING) + add_test_executable(guest_fstatvfs_conversion_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfsConversion.cpp FilesystemStatistics.cpp) + target_include_directories(guest_fstatvfs_conversion_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_fstatvfs_conversion_tests) + add_test(NAME guest_fstatvfs_conversion COMMAND guest_fstatvfs_conversion_tests) + set_tests_properties(guest_fstatvfs_conversion PROPERTIES TIMEOUT 20) + add_test_executable(guest_fstatvfs_tests ${LIBS_INCLUDE_DIR}/tests/GuestFstatvfs.cpp) + target_include_directories(guest_fstatvfs_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fstatvfs_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_fstatvfs_tests) + add_test(NAME guest_fstatvfs COMMAND guest_fstatvfs_tests) + set_tests_properties(guest_fstatvfs PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_fstatvfs PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_utime_forwarding_tests ${LIBS_INCLUDE_DIR}/tests/GuestUtimeForwarding.cpp FileTime.cpp) target_include_directories(guest_utime_forwarding_tests PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(guest_utime_forwarding_tests) @@ -39,11 +55,14 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_atexit PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) - target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) - add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) - set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + add_test_executable(guest_force_tls_destructor_tests ${LIBS_INCLUDE_DIR}/tests/GuestForceTlsDestructor.cpp) + target_include_directories(guest_force_tls_destructor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_force_tls_destructor_tests PRIVATE libSceLibcInternal libkernel libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_force_tls_destructor_tests) + add_test(NAME guest_force_tls_destructor COMMAND guest_force_tls_destructor_tests) + set_tests_properties(guest_force_tls_destructor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_force_tls_destructor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp new file mode 100644 index 000000000..083431cef --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp @@ -0,0 +1,46 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include +#include + +struct LibcStatvfs { + std::uint64_t f_bavail; + std::uint64_t f_bfree; + std::uint64_t f_blocks; + std::uint64_t f_favail; + std::uint64_t f_ffree; + std::uint64_t f_files; + std::uint64_t f_bsize; + std::uint64_t f_flag; + std::uint64_t f_frsize; + std::uint64_t f_fsid; + std::uint64_t f_namemax; +}; +static_assert(sizeof(LibcStatvfs) == 88); +static_assert(offsetof(LibcStatvfs, f_bsize) == 48); +static_assert(offsetof(LibcStatvfs, f_namemax) == 80); + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int, KernelStatfs*); +extern "C" int* APS5_VABI __error_nid_postfix(); + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int descriptor, LibcStatvfs* output) { + int* const error = __error_nid_postfix(); + const int saved = *error; + KernelStatfs volume{}; + const int status = _fstatfs_nid_postfix(descriptor, &volume); + if (status != 0) return status; + if (output == nullptr) { + *error = 14; + return -1; + } + const LibcStatvfs result{ + static_cast(volume.f_bavail), volume.f_bfree, volume.f_blocks, + static_cast(volume.f_ffree), static_cast(volume.f_ffree), volume.f_files, + volume.f_iosize, (volume.f_flags & 1) | ((volume.f_flags & 8) >> 2), volume.f_bsize, + 0, volume.f_namemax + }; + std::memcpy(output, &result, sizeof(result)); + *error = saved; + return 0; +} diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index aa2b08ccd..bcf7a8ccf 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -3,6 +3,18 @@ target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) + target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_datagram_receive_tests) + foreach(api IN ITEMS recv recvfrom sceNetRecv sceNetRecvfrom) + add_test(NAME guest_datagram_${api} COMMAND guest_datagram_receive_tests ${api}) + set_tests_properties(guest_datagram_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_datagram_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0775ad06b..3bfaa1229 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -369,6 +369,7 @@ std::int64_t message_length(const NetMsghdr* message) { extern "C" { extern const std::uint32_t sce_net_in6addr_any[4] = {}; +alignas(std::uint32_t) extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16] = {0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; int* APS5_VABI sceNetErrnoLoc(void) { return errno_slot(); @@ -645,6 +646,9 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { } const int native_flags = flags == 2 ? MSG_PEEK : 0; const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); +#ifdef _WIN32 + if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); +#endif return result >= 0 ? result : fail(native_error()); } @@ -663,8 +667,11 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); const int native_flags = flags == 2 ? MSG_PEEK : 0; - const int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); +#ifdef _WIN32 + if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); +#endif if (result < 0) return fail(native_error()); if (from && !native_to_guest_address(peer, from, fromlen)) return fail(NET_EAFNOSUPPORT); return result; diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 8d100ce35..79d43e8ae 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -2,7 +2,7 @@ include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) add_sce_ngs2_library(libSceNgs2.native) if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom Parse Stream UserFx Report) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp index 62b848071..3287f78d9 100644 --- a/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp @@ -37,6 +37,21 @@ int APS5_VABI sceNgs2GeomResetSourceParam(Ngs2GeomSourceParam* out_source_param) return 0; } +int APS5_VABI sceNgs2ReportRegisterHandler(uint32_t report_type, void* handler, uintptr_t user_data, Ngs2Handle* out_handle) { + (void)report_type; + (void)handler; + (void)user_data; + (void)out_handle; + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNgs2ReportUnregisterHandler(Ngs2Handle report_handle) { + (void)report_handle; + NotImplemented_nid_no_patch(__func__); + return 0; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp new file mode 100644 index 000000000..f76d61f46 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Report.cpp @@ -0,0 +1,33 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceNgs2ReportRegisterHandler(std::uint32_t reportType, void* handler, std::uintptr_t userData, std::uintptr_t* outHandle); +int APS5_VABI sceNgs2ReportUnregisterHandler(std::uintptr_t reportHandle); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(Call call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +} + +int main() { + std::uintptr_t handle = 0x1234; + Require(Throws([&] { sceNgs2ReportRegisterHandler(5, nullptr, 0, &handle); })); + Require(handle == 0x1234); + Require(Throws([] { sceNgs2ReportUnregisterHandler(0x1234); })); +} diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index d0bc1f710..90aeb0b06 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -135,23 +135,19 @@ int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_NP_ERROR_SIGNED_OUT; } int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index b28941f1b..b694abb6f 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -93,4 +93,10 @@ int APS5_VABI sceNpSessionSignalingGetConnectionFromPeerAddress(void) { NotImplemented_nid_no_patch(__func__); return 0; } + +int APS5_VABI sceNpSessionSignalingGetGroupFromSessionId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp index 09fde57f5..5e97582aa 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GroupInfo.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetGroupInfo(int context, int handle, int group_id, NpTrophy2GroupDetails* details, NpTrophy2GroupData* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp index 0f19c9782..5090940b5 100644 --- a/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/TrophyInfo.cpp @@ -10,7 +10,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetTrophyInfo(int context, int handle, int trophy_id, NpTrophy2Details* details, NpTrophy2Data* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 38bb59ca5..10f597db2 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -73,7 +73,7 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } int APS5_VABI scePadGetHandle(int user_id, int type, int index) { - if (!g_opened || !ValidPort(user_id, type, index)) return PAD_ERROR_DEVICE_NO_HANDLE; + if (!g_opened || !ValidPort(user_id, type, index) || type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NO_HANDLE; return PAD_HANDLE; } @@ -136,6 +136,7 @@ int APS5_VABI scePadOpen_nid_postfix(int userId, int type, int index, const void if (!ValidPort(userId, type, index)) { return PAD_ERROR_INVALID_ARG; } + if (type == PAD_PORT_TYPE_SPECIAL) return PAD_ERROR_DEVICE_NOT_CONNECTED; g_opened = true; return PAD_HANDLE; } @@ -236,7 +237,7 @@ int APS5_VABI scePadSetVibration(int handle, const PadVibrationParam* param) { int APS5_VABI scePadSetVibrationMode(int handle, int mode) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - if (mode < 0 || mode > 2) return PAD_ERROR_INVALID_ARG; + if (mode < 0 || mode > 3) return PAD_ERROR_INVALID_ARG; Pad::SetVibrationMode(mode); return PAD_OK; } @@ -256,6 +257,11 @@ int APS5_VABI scePadVrControllerRead() { return 0; } +int APS5_VABI scePadVrControllerGetTriggerEffectState() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadVrControllerReadState(int handle, void* state) { (void)handle; (void)state; @@ -263,6 +269,16 @@ int APS5_VABI scePadVrControllerReadState(int handle, void* state) { return 0; } +int APS5_VABI scePadVrControllerSetTriggerEffect() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerSetTriggerEffects() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI scePadIsRemoteController(int handle, bool* remote) { if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (remote == nullptr) return PAD_ERROR_INVALID_ARG; diff --git a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt index c786173f6..fddddc543 100644 --- a/core/libs/prx/libScePosixForWebKit/CMakeLists.txt +++ b/core/libs/prx/libScePosixForWebKit/CMakeLists.txt @@ -17,6 +17,15 @@ if(BUILD_TESTING) set_tests_properties(guest_posix_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_fnmatch_tests ${LIBS_INCLUDE_DIR}/tests/GuestFnmatch.cpp) + target_include_directories(guest_fnmatch_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_fnmatch_tests PRIVATE libScePosixForWebKit libc) + configure_windows_unwind(guest_fnmatch_tests) + add_test(NAME guest_fnmatch COMMAND guest_fnmatch_tests) + if(WIN32) + set_tests_properties(guest_fnmatch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_resolver_tests ${LIBS_INCLUDE_DIR}/tests/GuestResolver.cpp) target_include_directories(guest_resolver_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_resolver_tests PRIVATE libScePosixForWebKit libSceNet libc) diff --git a/core/libs/prx/libScePosixForWebKit/Export.cpp b/core/libs/prx/libScePosixForWebKit/Export.cpp index 8d405a75d..0c3b4b758 100644 --- a/core/libs/prx/libScePosixForWebKit/Export.cpp +++ b/core/libs/prx/libScePosixForWebKit/Export.cpp @@ -5,9 +5,156 @@ #include #include #include +#include + +namespace { + +constexpr int FnmNomatch = 1; +constexpr int FnmNoescape = 0x01; +constexpr int FnmPathname = 0x02; +constexpr int FnmPeriod = 0x04; +constexpr int FnmLeadingDir = 0x08; +constexpr int FnmCasefold = 0x10; + +enum class RangeResult { Match, NoMatch, Error }; + +unsigned char FoldCase(unsigned char c, int flags) { + return (flags & FnmCasefold) && c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; +} + +RangeResult MatchRange(const char* pattern, unsigned char test, int flags, const char** next) { + const bool negate = *pattern == '!' || *pattern == '^'; + if (negate) ++pattern; + test = FoldCase(test, flags); + bool ok = false; + const char* const origin = pattern; + for (;;) { + if (*pattern == ']' && pattern > origin) { + ++pattern; + break; + } + if (*pattern == '\0') return RangeResult::Error; + if (*pattern == '/' && (flags & FnmPathname)) return RangeResult::NoMatch; + if (*pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char low = FoldCase(static_cast(*pattern), flags); + if (*pattern != '\0') ++pattern; + if (*pattern == '-' && pattern[1] != '\0' && pattern[1] != ']') { + if (*++pattern == '\\' && !(flags & FnmNoescape)) ++pattern; + const unsigned char high = static_cast(*pattern); + if (high == '\0') return RangeResult::Error; + ++pattern; + if (low <= test && test <= FoldCase(high, flags)) ok = true; + } else if (low == test) { + ok = true; + } + } + *next = pattern; + return ok == negate ? RangeResult::NoMatch : RangeResult::Match; +} + +int MatchPattern(const char* pattern, const char* string, int flags) { + const char* const start = string; + const char* backtrackPattern = nullptr; + const char* backtrackString = nullptr; + const auto leadingPeriod = [&](const char* at) { + return *at == '.' && (flags & FnmPeriod) && (at == start || ((flags & FnmPathname) && at[-1] == '/')); + }; + for (;;) { + const unsigned char pc = static_cast(*pattern); + if (pc != '\0') ++pattern; + const unsigned char sc = static_cast(*string); + bool mismatch = false; + bool literal = false; + unsigned char expected = pc; + switch (pc) { + case '\0': + if ((flags & FnmLeadingDir) && sc == '/') return 0; + if (sc == '\0') return 0; + mismatch = true; + break; + case '?': + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + ++string; + break; + case '*': { + char c = *pattern; + while (c == '*') c = *++pattern; + if (leadingPeriod(string)) { + mismatch = true; + break; + } + if (c == '\0') { + if (flags & FnmPathname) return (flags & FnmLeadingDir) || std::strchr(string, '/') == nullptr ? 0 : FnmNomatch; + return 0; + } + if (c == '/' && (flags & FnmPathname)) { + string = std::strchr(string, '/'); + if (string == nullptr) return FnmNomatch; + break; + } + backtrackPattern = pattern; + backtrackString = string; + break; + } + case '[': { + if (sc == '\0') return FnmNomatch; + if ((sc == '/' && (flags & FnmPathname)) || leadingPeriod(string)) { + mismatch = true; + break; + } + const char* next = nullptr; + const RangeResult result = MatchRange(pattern, sc, flags, &next); + if (result == RangeResult::Error) { + literal = true; + } else if (result == RangeResult::Match) { + pattern = next; + ++string; + } else { + mismatch = true; + } + break; + } + case '\\': + if (!(flags & FnmNoescape)) { + expected = static_cast(*pattern); + if (expected == '\0') return FnmNomatch; + ++pattern; + } + literal = true; + break; + default: + literal = true; + break; + } + if (literal) { + if (sc != '\0') ++string; + if (FoldCase(expected, flags) != FoldCase(sc, flags)) mismatch = true; + } + if (mismatch) { + if (backtrackPattern == nullptr) return FnmNomatch; + const unsigned char bc = static_cast(*backtrackString); + if (bc == '\0') return FnmNomatch; + if (bc == '/' && (flags & FnmPathname)) return FnmNomatch; + ++backtrackString; + pattern = backtrackPattern; + string = backtrackString; + } + } +} + +} extern "C" { +int APS5_VABI fnmatch_nid_postfix(const char* pattern, const char* string, int flags) { + if (pattern == nullptr || string == nullptr) throw std::invalid_argument("fnmatch: null pattern or string"); + return MatchPattern(pattern, string, flags); +} + char* APS5_VABI strcasestr_nid_postfix(const char* text, const char* needle) { if (*needle == '\0') return const_cast(text); for (; *text != '\0'; ++text) { diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index c837587a5..7b418022c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -24,6 +24,13 @@ int APS5_VABI sceRazorCpuJobManagerSequence(const void* args) { return 0; } +int APS5_VABI sceRazorCpuPushMarker(const char* label, uint32_t color, uint32_t flags) { + (void)label; + (void)color; + (void)flags; + return 0; +} + int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, uint32_t color, uint32_t flags) { (void)name; (void)color; diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 132ea291f..cff5949a7 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -101,6 +101,11 @@ int APS5_VABI sceRudpEnableInternalIOThread(uint32_t stack_size, uint32_t priori return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; } +int APS5_VABI sceRudpEnableInternalIOThread2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { std::lock_guard lk(g_mutex); g_handler = handler; @@ -150,6 +155,11 @@ int APS5_VABI sceRudpSetOption(int ctx_id, int option, const void* value, uint32 return valid_ctx(ctx_id) ? 0 : RUDP_ERROR_INVALID_CONTEXT_ID; } +int APS5_VABI sceRudpGetOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpInitiate(int ctx_id, const void* to, uint32_t tolen, uint16_t vport) { (void)to; (void)tolen; @@ -178,6 +188,11 @@ int APS5_VABI sceRudpGetContextStatus(int ctx_id, void* status, uint32_t size) { return 0; } +int APS5_VABI sceRudpGetRemoteInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceRudpGetNumberOfPacketsToRead(int ctx_id) { std::lock_guard lk(g_mutex); if (!g_inited) { @@ -242,4 +257,25 @@ int APS5_VABI sceRudpRead(int ctx_id, void* data, uint32_t len, int flags, void* } return valid_ctx(ctx_id) ? RUDP_ERROR_CONN_RESET : RUDP_ERROR_INVALID_CONTEXT_ID; } + +int APS5_VABI sceRudpNetFlush(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpProcessEvents(void) { + std::lock_guard lk(g_mutex); + return g_inited ? 0 : RUDP_ERROR_NOT_INITIALIZED; +} + +int APS5_VABI sceRudpNetReceived(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceRudpGetMaxSegmentSize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSaveData.native/CMakeLists.txt b/core/libs/prx/libSceSaveData.native/CMakeLists.txt index 3b596955e..6b03a2c86 100644 --- a/core/libs/prx/libSceSaveData.native/CMakeLists.txt +++ b/core/libs/prx/libSceSaveData.native/CMakeLists.txt @@ -22,6 +22,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_savedata_convert_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataConvert.cpp) + target_include_directories(guest_savedata_convert_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_savedata_convert_tests PRIVATE libSceSaveData.native libc) + add_test(NAME guest_savedata_convert COMMAND guest_savedata_convert_tests) + if(WIN32) + set_tests_properties(guest_savedata_convert PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_savedata_mount_status_tests ${LIBS_INCLUDE_DIR}/tests/GuestSaveDataMountStatus.cpp) target_include_directories(guest_savedata_mount_status_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_savedata_mount_status_tests PRIVATE libSceSaveData.native libc) diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a030bac77..b84549901 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -270,13 +270,22 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS } result->hit_num = 0; result->set_num = 0; + const std::uint32_t key = cond->key; + if ((key > SAVE_DATA_SORT_KEY_MTIME && key != SAVE_DATA_SORT_KEY_FREE_BLOCKS) || cond->order > SAVE_DATA_SORT_ORDER_DESCENT) { + return SAVE_DATA_ERROR_PARAMETER; + } const char* pattern = (cond->dir_name != nullptr) ? cond->dir_name->data : nullptr; const std::string root = save_root(); if (!std::filesystem::is_directory(root)) { return SAVE_DATA_OK; } - std::uint32_t hit = 0; - std::uint32_t set = 0; + struct Match { + std::string name; + std::string path; + SaveDataParam param; + }; + const bool sortByParam = key == SAVE_DATA_SORT_KEY_USER_PARAM || key == SAVE_DATA_SORT_KEY_MTIME; + std::vector matches; for (const auto& entry : std::filesystem::directory_iterator(root)) { if (!entry.is_directory()) { continue; @@ -285,16 +294,29 @@ static int dirNameSearch(const SaveDataDirNameSearchCond* cond, SaveDataDirNameS if (!dir_name_match(name.c_str(), pattern)) { continue; } - hit++; - if (result->dir_names != nullptr && set < result->dir_names_num) { - std::snprintf(result->dir_names[set].data, sizeof(result->dir_names[set].data), "%s", name.c_str()); - if (result->params != nullptr) { - result->params[set] = load_param(entry.path().string()); - } - set++; + Match match{name, entry.path().string(), {}}; + if (sortByParam) { + match.param = load_param(match.path); + } + matches.push_back(std::move(match)); + } + std::sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.name < b.name; }); + if (key == SAVE_DATA_SORT_KEY_USER_PARAM) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.user_param < b.param.user_param; }); + } else if (key == SAVE_DATA_SORT_KEY_MTIME) { + std::stable_sort(matches.begin(), matches.end(), [](const Match& a, const Match& b) { return a.param.mtime < b.param.mtime; }); + } + if (cond->order == SAVE_DATA_SORT_ORDER_DESCENT) { + std::reverse(matches.begin(), matches.end()); + } + const std::uint32_t set = result->dir_names == nullptr ? 0 : static_cast(std::min(result->dir_names_num, matches.size())); + for (std::uint32_t i = 0; i < set; i++) { + std::snprintf(result->dir_names[i].data, sizeof(result->dir_names[i].data), "%s", matches[i].name.c_str()); + if (result->params != nullptr) { + result->params[i] = sortByParam ? matches[i].param : load_param(matches[i].path); } } - result->hit_num = hit; + result->hit_num = static_cast(matches.size()); result->set_num = set; return SAVE_DATA_OK; } @@ -740,9 +762,28 @@ int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond* cond, return rc; } -int APS5_VABI sceSaveDataConvert() { - NotImplemented_nid_no_patch(__func__); - return 0; +static int convert(const SaveDataConvert* param) { + if (param == nullptr || param->dir_name == nullptr) { + throw std::runtime_error("sceSaveDataConvert: null argument"); + } + const auto* nameEnd = static_cast(std::memchr(param->dir_name->data, '\0', sizeof(param->dir_name->data))); + if (nameEnd == nullptr) { + return SAVE_DATA_ERROR_PARAMETER; + } + const std::string dirName(param->dir_name->data, static_cast(nameEnd - param->dir_name->data)); + if (dirName.empty() || dirName == "." || dirName == ".." || dirName.find_first_of("/\\:") != std::string::npos) { + return SAVE_DATA_ERROR_PARAMETER; + } + if (!std::filesystem::is_directory(save_root() + "/" + dirName)) { + return SAVE_DATA_ERROR_NOT_FOUND; + } + return SAVE_DATA_OK; +} + +int APS5_VABI sceSaveDataConvert(const SaveDataConvert* param) { + const int rc = convert(param); + SAVEDATA_TRACE("convert user=%d dir=%s -> 0x%08x", param != nullptr ? param->user_id : -1, param != nullptr && param->dir_name != nullptr ? param->dir_name->data : "(null)", static_cast(rc)); + return rc; } int APS5_VABI sceSaveDataGetConvertProgress() { diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 20d02ad5a..ceffb09c0 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -33,6 +33,14 @@ constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_DETAIL = 3; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_USER_PARAM = 4; constexpr std::uint32_t SAVE_DATA_PARAM_TYPE_MTIME = 5; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_DIRNAME = 0; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_USER_PARAM = 1; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_BLOCKS = 2; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_MTIME = 3; +constexpr std::uint32_t SAVE_DATA_SORT_KEY_FREE_BLOCKS = 5; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_ASCENT = 0; +constexpr std::uint32_t SAVE_DATA_SORT_ORDER_DESCENT = 1; + constexpr std::size_t SAVE_DATA_MOUNT_SLOTS = 16; struct MountSlot { diff --git a/core/libs/prx/libSceSigninDialog/Export.cpp b/core/libs/prx/libSceSigninDialog/Export.cpp index ea21b63b2..761bf3330 100644 --- a/core/libs/prx/libSceSigninDialog/Export.cpp +++ b/core/libs/prx/libSceSigninDialog/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -12,8 +13,15 @@ constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +struct SceSigninDialogResult { + std::int32_t result = COMMON_DIALOG_RESULT_USER_CANCELED; + std::int32_t reserved[3] = {}; +}; std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; @@ -22,19 +30,23 @@ std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; extern "C" { int APS5_VABI sceSigninDialogClose(void) { - NotImplemented_nid_no_patch(__func__); + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceSigninDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + const SceSigninDialogResult dialogResult{}; + std::memcpy(result, &dialogResult, sizeof(dialogResult)); return 0; } int APS5_VABI sceSigninDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return g_status.load(); } int APS5_VABI sceSigninDialogInitialize(void) { diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index e8fd49499..4b3e23a4e 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -159,4 +159,34 @@ int APS5_VABI sceSslGetSubjectName(void) { return 0; } +int APS5_VABI sceSslCreateConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslDeleteConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslEnableVerifyOption(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslConnect(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslRead(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSysmodule/Export.cpp b/core/libs/prx/libSceSysmodule/Export.cpp index 0dd8ea293..8ed923947 100644 --- a/core/libs/prx/libSceSysmodule/Export.cpp +++ b/core/libs/prx/libSceSysmodule/Export.cpp @@ -20,6 +20,7 @@ extern "C" void* APS5_VABI dlopen_nid_postfix(const char* path, int flags); namespace { +constexpr int SCE_SYSMODULE_ERROR_INVALID_VALUE = static_cast(0x805A1000); constexpr int SCE_SYSMODULE_ERROR_UNLOADED = static_cast(0x805A1001); constexpr int kRtldNow = 2; @@ -117,7 +118,7 @@ int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleIsLoaded: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -134,7 +135,7 @@ int APS5_VABI sceSysmoduleIsLoaded(std::uint16_t id) { int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleLoadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -151,7 +152,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, (void)argv; (void)unk; if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternalWithArg: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternalWithArg: unknown id ") + std::to_string(id)); @@ -166,7 +167,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { if (id == 0) { - throw std::runtime_error("sceSysmoduleUnloadModule: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { APS5_LOG_OUT("unknown id: %u", static_cast(id)); @@ -183,7 +184,7 @@ int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id) { int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleLoadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleLoadModuleInternal: unknown id ") + std::to_string(id)); @@ -195,7 +196,7 @@ int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id) { int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id) { if ((id & 0x7fffffffu) == 0) { - throw std::runtime_error("sceSysmoduleUnloadModuleInternal: invalid id 0"); + return SCE_SYSMODULE_ERROR_INVALID_VALUE; } if (!findModuleName(id)) { throw std::runtime_error(std::string("sceSysmoduleUnloadModuleInternal: unknown id ") + std::to_string(id)); diff --git a/core/libs/prx/libSceSysmodule/ModuleTable.hpp b/core/libs/prx/libSceSysmodule/ModuleTable.hpp index 4840450be..14642ee07 100644 --- a/core/libs/prx/libSceSysmodule/ModuleTable.hpp +++ b/core/libs/prx/libSceSysmodule/ModuleTable.hpp @@ -275,6 +275,7 @@ inline const std::unordered_map kModuleTable = { {0x8000008c, "libSceHttp2"}, {0x8000008d, "libSceNpGameIntent"}, {0x8000008f, "libSceNpWebApi2"}, + {0x80000094, "libSceAgc"}, {0x80000098, "libScePosixForWebKit"}, {0x8000009a, "libSceNpRemotePlaySessionSignaling"}, {0x800000aa, "libicu"}, diff --git a/core/libs/prx/libSceTextToSpeech2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp index 8e344ce25..09950022b 100644 --- a/core/libs/prx/libSceTextToSpeech2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -12,8 +12,7 @@ constexpr std::int32_t SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0 extern "C" { int APS5_VABI sceTextToSpeech2GetSystemStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { @@ -22,33 +21,27 @@ std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param) { } int APS5_VABI sceTextToSpeech2Cancel() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Close() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2GetSpeechStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Open() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Speak() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } int APS5_VABI sceTextToSpeech2Terminate() { - NotImplemented_nid_no_patch(__func__); - return 0; + return SCE_KERNEL_ERROR_EOPNOTSUPP; } } diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 3c818ddb3..a0b1d880f 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -580,6 +580,22 @@ int APS5_VABI _sceUltSemaphoreCreate(void* semaphore, const char* name, std::int return sceUltSemaphoreCreate(semaphore, name, numInitialResource, waitingQueueResourcePool, optParam, buildVersion); } +int APS5_VABI _sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uint32_t buildVersion) { + return sceUltMutexOptParamInitialize(optParam, buildVersion); +} + +int APS5_VABI _sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { + return sceUltMutexCreate(mutex, name, waitingQueueResourcePool, optParam, buildVersion); +} + +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void* pool, const char* name, std::uint32_t numData, std::uint64_t dataSize, std::uint32_t numQueueObject, void* waitingQueueResourcePool, void* workArea, const void* optParam, std::uint32_t buildVersion) { + return sceUltQueueDataResourcePoolCreate(pool, name, numData, dataSize, numQueueObject, waitingQueueResourcePool, workArea, optParam, buildVersion); +} + +int APS5_VABI _sceUltQueueCreate(void* queue, const char* name, std::uint64_t dataSize, void* waitingQueueResourcePool, void* queueDataResourcePool, const void* optParam, std::uint32_t buildVersion) { + return sceUltQueueCreate(queue, name, dataSize, waitingQueueResourcePool, queueDataResourcePool, optParam, buildVersion); +} + int APS5_VABI _sceUltUlthreadRuntimeOptParamInitialize(UltUlthreadRuntimeOptParam* optParam, std::uint32_t buildVersion) { return sceUltUlthreadRuntimeOptParamInitialize(optParam, buildVersion); } diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index a67889f5c..d0be202ca 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -50,6 +50,14 @@ static_assert(offsetof(UsbdTransfer, timeout) == 12 && offsetof(UsbdTransfer, ca void* g_emptyDeviceList[1] = {nullptr}; +std::string AbsentDevice(const char* function) { + return std::string(function) + ": the device list is always empty, so the device was not returned by sceUsbdGetDeviceList"; +} + +std::string UnopenedHandle(const char* function) { + return std::string(function) + ": no device can be opened, so the handle was not returned by sceUsbdOpen"; +} + } extern "C" { @@ -116,99 +124,112 @@ void APS5_VABI sceUsbdFreeConfigDescriptor(void* config) { if (config != nullptr) throw std::runtime_error(std::string(__func__) + ": descriptor was not returned by this library"); } -int APS5_VABI sceUsbdAttachKernelDriver() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdCheckConnected() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdClaimInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdClose() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdClose(void* deviceHandle) { + if (deviceHandle != nullptr) throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdControlTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout) { + (void)requestType; + (void)request; + (void)value; + (void)index; + (void)timeout; + if (deviceHandle == nullptr || (length != 0 && data == nullptr)) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdGetActiveConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config) { + (void)config; + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetBusNumber() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device) { + if (device == nullptr) return 0; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetConfigDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config) { + (void)configIndex; + if (device == nullptr || config == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetDeviceAddress() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device) { + if (device == nullptr) return 0; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetDeviceDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor) { + if (device == nullptr || descriptor == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdGetStringDescriptor() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length) { + (void)descriptorIndex; + (void)languageId; + if (deviceHandle == nullptr || data == nullptr || length < 1) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdKernelDriverActive() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle) { + if (device == nullptr) throw std::runtime_error(std::string(__func__) + ": null device"); + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdRefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void* APS5_VABI sceUsbdRefDevice(void* device) { + if (device == nullptr) return nullptr; + throw std::runtime_error(AbsentDevice(__func__)); } -int APS5_VABI sceUsbdReleaseInterface() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber) { + (void)interfaceNumber; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdResetDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle) { + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdSetConfiguration() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration) { + (void)configuration; + if (deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdSubmitTransfer() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI sceUsbdSubmitTransfer(UsbdTransfer* transfer) { + if (transfer == nullptr || transfer->deviceHandle == nullptr) return SCE_USBD_ERROR_INVALID_ARG; + throw std::runtime_error(UnopenedHandle(__func__)); } -int APS5_VABI sceUsbdUnrefDevice() { - NotImplemented_nid_no_patch(__func__); - return 0; +void APS5_VABI sceUsbdUnrefDevice(void* device) { + if (device != nullptr) throw std::runtime_error(AbsentDevice(__func__)); } } diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index 3b7bb08b9..7485b2123 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -33,6 +33,9 @@ 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) + set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING + "SDL video driver for VideoOut flip tests") + set_target_properties(video_out_flip_tests PROPERTIES EXCLUDE_FROM_ALL OFF) foreach(flipMode IN ITEMS lifetime reopen decode controls overlay) if(flipMode STREQUAL "lifetime") @@ -40,7 +43,7 @@ if(BUILD_TESTING) else() add_test(NAME video_out_flip_${flipMode} COMMAND video_out_flip_tests ${flipMode}) endif() - set_tests_properties(video_out_flip_${flipMode} PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 RESOURCE_LOCK videoout_app0 ENVIRONMENT "SDL_VIDEODRIVER=offscreen") + 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:$") diff --git a/core/libs/prx/libSceVideoOut/include/Buffer.hpp b/core/libs/prx/libSceVideoOut/include/Buffer.hpp index 903e4029c..41a36673a 100644 --- a/core/libs/prx/libSceVideoOut/include/Buffer.hpp +++ b/core/libs/prx/libSceVideoOut/include/Buffer.hpp @@ -10,6 +10,7 @@ extern "C" { int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int set_index, int buffer_index_start, const VideoOutBuffers* buffers, int buffer_num, const VideoOutBufferAttribute2* attribute, int category, void* option); void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, uint32_t dcc_control, uint64_t dcc_cb_register_clear_color); +void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option); 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); diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 481c92c2f..965e37b3d 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -47,6 +47,13 @@ void APS5_VABI sceVideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribut attribute->dcc_control = dccControl; } +void APS5_VABI sceVideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, uint64_t option) { + if (attribute == nullptr) { + throw std::runtime_error("sceVideoOutAddBufferAttributeOption: null attribute"); + } + attribute->option |= option; +} + int APS5_VABI sceVideoOutRegisterBuffers2(int handle, int setIndex, int bufferIndexStart, const VideoOutBuffers* buffers, int bufferNum, const VideoOutBufferAttribute2* attribute, int category, void* option) try { auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 607870626..85d87719a 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -233,6 +233,16 @@ void testDecode() { group.attribute.option = option; check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted"); } + sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0, 0); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY && group.attribute.width == 259 && group.attribute.height == 137, "an added buffer option was not stored"); + sceVideoOutAddBufferAttributeOption(&group.attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY); + check(group.attribute.option == VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY, "adding a buffer option twice changed it"); + check(DescribeVideoOutBuffer(registered, group).address == plain.address, "a buffer with an added supported option was rejected"); + sceVideoOutAddBufferAttributeOption(&group.attribute, 0x20); + check(group.attribute.option == 0x28, "an added buffer option did not keep the existing bits"); + check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option 40") != std::string::npos, "an unsupported added buffer option was accepted"); + check(expectFailure([&] { sceVideoOutAddBufferAttributeOption(nullptr, 0x20); }).find("null attribute") != std::string::npos, "a null attribute was accepted"); VideoOutBuffer dccBuffer{0, reinterpret_cast(tiled.data()), 0x7f0000}; sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344); group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4ab9fb411..4756143ee 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -9,6 +9,7 @@ namespace { constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; constexpr std::int32_t VOICE_QOS_APP_TYPE_10000000 = 0x10000000; constexpr std::uint32_t VOICE_QOS_MEMORY_SIZE = 0x40000; +constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT = static_cast(0x804E0901); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID = static_cast(0x804E0902); constexpr int SCE_VOICE_ERROR_LIBVOICEQOS_INITIALIZED = static_cast(0x804E0905); @@ -28,8 +29,10 @@ int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_ty } int APS5_VABI sceVoiceQoSEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return SCE_VOICE_ERROR_LIBVOICEQOS_NOT_INIT; + g_initialized = false; + return 0; } int APS5_VABI sceVoiceQoSCreateLocalEndpoint() { diff --git a/core/libs/prx/libSceVrTracker2/Export.cpp b/core/libs/prx/libSceVrTracker2/Export.cpp index 34024033d..75a4fce48 100644 --- a/core/libs/prx/libSceVrTracker2/Export.cpp +++ b/core/libs/prx/libSceVrTracker2/Export.cpp @@ -5,6 +5,16 @@ extern "C" { +int APS5_VABI sceVrTracker2Finalize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrTracker2GetPlayAreaOrientedBoundingBox() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2GetResult() { NotImplemented_nid_no_patch(__func__); return 0; @@ -15,6 +25,11 @@ int APS5_VABI sceVrTracker2Initialize() { return 0; } +int APS5_VABI sceVrTracker2LocateCoordinateSystem() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVrTracker2QueryMemory() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceXml/Export.cpp b/core/libs/prx/libSceXml/Export.cpp new file mode 100644 index 000000000..fb34206c2 --- /dev/null +++ b/core/libs/prx/libSceXml/Export.cpp @@ -0,0 +1,10 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_DUMMY_FUN + +} diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ddfe85664..d68768c32 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/General.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/CreationMask.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp @@ -302,6 +303,16 @@ if(BUILD_TESTING) set_tests_properties(guest_interfaces PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_atoi_tests ${LIBS_INCLUDE_DIR}/tests/GuestAtoi.cpp) + target_include_directories(guest_atoi_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_atoi_tests PRIVATE libc) + configure_windows_unwind(guest_atoi_tests) + add_test(NAME guest_atoi COMMAND guest_atoi_tests) + set_tests_properties(guest_atoi PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_atoi PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_reallocalign_tests ${LIBS_INCLUDE_DIR}/tests/GuestReallocalign.cpp) target_include_directories(guest_reallocalign_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_reallocalign_tests PRIVATE libc) @@ -491,6 +502,15 @@ if(BUILD_TESTING) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) endif() + add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) + target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(log_macros_tests PRIVATE libc) + configure_windows_unwind(log_macros_tests) + add_test(NAME log_macros COMMAND log_macros_tests) + if(WIN32) + set_tests_properties(log_macros PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() if(WIN32) @@ -569,5 +589,11 @@ if(MINGW) set_target_properties(windows_exception_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) add_test(NAME windows_exceptions COMMAND windows_exception_tests) set_tests_properties(windows_exceptions PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(windows_backtrace_report_tests ${LIBS_INCLUDE_DIR}/tests/WindowsBacktraceReport.cpp) + target_link_libraries(windows_backtrace_report_tests PRIVATE libc) + add_test(NAME windows_backtrace_report COMMAND windows_backtrace_report_tests) + set_tests_properties(windows_backtrace_report PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" + PASS_REGULAR_EXPRESSION "aborting[^~]*SIGABRT. on thread[^~]*stack return address candidates:[^~]*rsp[+]0x") endif() endif() diff --git a/core/libs/prx/libc/include/CreationMask.hpp b/core/libs/prx/libc/include/CreationMask.hpp new file mode 100644 index 000000000..d6d419b02 --- /dev/null +++ b/core/libs/prx/libc/include/CreationMask.hpp @@ -0,0 +1,13 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CREATIONMASK_HPP + +#include + +namespace File { + +extern "C" std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask); +extern "C" std::uint16_t GetCreationMask_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index e31bedd2a..4bec836c8 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESYSTEMERROR_HPP #include +#include "General.hpp" inline int FilesystemError(const std::error_code& error) { if (error == std::errc::no_such_file_or_directory) return 2; @@ -22,4 +23,14 @@ inline int FilesystemError(const std::error_code& error) { return 5; } +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved) { + try { + resolved = ResolvePath_nid_no_patch(path); + return 0; + } catch (const std::filesystem::filesystem_error& error) { + if (error.code() == std::errc::illegal_byte_sequence) return 2; + return FilesystemError(error.code()); + } +} + #endif diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 2f02aeebd..b42cd1951 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -19,6 +19,7 @@ bool GuestArenaAvailable_nid_postfix(); bool GuestArenaContains_nid_postfix(const void* pointer, std::size_t bytes); void* GuestArenaAllocate_nid_postfix(std::size_t bytes, std::size_t alignment); void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t bytes, std::size_t alignment); +std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment); void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes); void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes); // The reserved range, and whether it was reserved with page write watching (Windows MEM_WRITE_WATCH). diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 95026c4dc..029b47226 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSFORMATTING_HPP #include "SceTypes.hpp" +#include #include #include #include @@ -14,15 +15,43 @@ namespace LibcDetail { +enum class ArgumentClass : unsigned char { Integer, Double, LongDouble }; + class FormatArguments { VaList args; + std::vector> numbered; + size_t position = 1; public: explicit FormatArguments(const void* source) { std::memcpy(&args, source, sizeof(args)); } + void Number(const std::vector& classes) { + numbered.resize(classes.size()); + for (size_t index = 0; index < classes.size(); ++index) { + const auto store = [&](auto value) { std::memcpy(numbered[index].data(), &value, sizeof(value)); }; + if (classes[index] == ArgumentClass::LongDouble) store(Fetch()); + else if (classes[index] == ArgumentClass::Double) store(Fetch()); + else store(Fetch()); + } + } + + void Seek(size_t index) { position = index; } + + template T At(size_t index) { + if (index == 0 || index > numbered.size()) throw std::out_of_range("Format argument position out of range"); + T value; + std::memcpy(&value, numbered[index - 1].data(), sizeof(value)); + return value; + } + template T Next() { + return numbered.empty() ? Fetch() : At(position++); + } + +private: + template T Fetch() { const void* address; if constexpr (std::is_same_v) { if (args.fp_offset < 176) { @@ -158,11 +187,67 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) { return std::string(padding, ' ') + "0x" + digits; } +constexpr size_t maximumArgumentPosition = 99; + +inline size_t ArgumentPosition(const char*& format) { + const char* end = format; + while (*end >= '0' && *end <= '9') ++end; + if (end == format || *end != '$') return 0; + size_t position = 0; + for (const char* digit = format; digit != end && position <= maximumArgumentPosition; ++digit) + position = position * 10 + static_cast(*digit - '0'); + if (position == 0 || position > maximumArgumentPosition) throw std::invalid_argument("Format argument position out of range"); + format = end + 1; + return position; +} + +inline std::vector NumberedArgumentClasses(const char* format) { + if (!std::strchr(format, '$')) return {}; + std::vector classes; + bool numbered = false; + size_t next = 1; + const auto take = [&](size_t position, ArgumentClass type) { + if (classes.size() < position) classes.resize(position); + classes[position - 1] = type; + }; + while ((format = std::strchr(format, '%')) != nullptr) { + ++format; + if (*format == '%') { + ++format; + continue; + } + if (const size_t position = ArgumentPosition(format)) { + next = position; + numbered = true; + } + while (*format && std::strchr("-+ #0'123456789.*", *format)) { + if (*format++ != '*') continue; + const size_t position = ArgumentPosition(format); + if (position) numbered = true; + take(position ? position : next++, ArgumentClass::Integer); + } + bool longDouble = false; + while (*format && std::strchr("hljztL", *format)) longDouble = *format++ == 'L'; + const char conversion = *format; + if (!conversion) break; + ++format; + if (std::strchr("aAeEfFgG", conversion)) take(next++, longDouble ? ArgumentClass::LongDouble : ArgumentClass::Double); + else if (std::strchr("diouxXcCsSpn", conversion)) take(next++, ArgumentClass::Integer); + } + if (!numbered) classes.clear(); + return classes; +} + inline int FormatWindows(char* buffer, size_t size, const char* format, const void* source, std::string* complete = nullptr) { if (!format || !source) throw std::invalid_argument("Null formatting argument"); const char* const formatStart = format; FormatArguments args(source); + args.Number(NumberedArgumentClasses(format)); FormatOutput output(buffer, size, complete); + const auto star = [&] { + const size_t position = ArgumentPosition(format); + return position ? args.At(position) : args.Next(); + }; while (*format) { const char* literal = format; while (*format && *format != '%') ++format; @@ -173,11 +258,13 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo output.Append(format++, 1); continue; } + if (const size_t position = ArgumentPosition(format)) args.Seek(position); std::string spec = "%"; - while (*format && std::strchr("-+ #0", *format)) spec += *format++; + for (; *format && std::strchr("-+ #0'", *format); ++format) + if (*format != '\'') spec += *format; if (*format == '*') { ++format; - const int width = args.Next(); + const int width = star(); if (width < 0) spec += '-'; spec += std::to_string(width < 0 ? -static_cast(width) : width); } else { @@ -188,7 +275,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo ++format; if (*format == '*') { ++format; - const int precision = args.Next(); + const int precision = star(); if (precision >= 0) { spec += "." + std::to_string(precision); precisionLimit = static_cast(precision); @@ -232,6 +319,8 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == 'o' && value == 0 && precisionLimit == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; output.Value(spec + "ll" + conversion, value); } else if (std::strchr("aAeEfFgG", conversion)) { if (length == "L") { diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index b518d9cb8..9d33b9b0b 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -235,7 +235,7 @@ public: const auto found = views.find(base); if (found != views.end()) { auto& view = found->second; - const auto stop = std::min(end, base + pageBytes); + const auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); if (view.protection == PAGE_NOACCESS) return false; const bool pinned = view.page->pins != 0; if (pinned || view.seen != view.page->generation) { @@ -255,8 +255,8 @@ public: if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); other.armed = true; } - view.seen = view.page->generation; - rememberClean(base, base + pageBytes); + if (cursor == base && stop == base + pageBytes) view.seen = view.page->generation; + rememberClean(cursor, stop); } cursor = stop; } else { diff --git a/core/libs/prx/libc/include/general/LogMacros.hpp b/core/libs/prx/libc/include/general/LogMacros.hpp index 9142a9c73..c10306ecc 100644 --- a/core/libs/prx/libc/include/general/LogMacros.hpp +++ b/core/libs/prx/libc/include/general/LogMacros.hpp @@ -1,7 +1,34 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_LOGMACROS_HPP +#include +#include #include +#include + +#ifdef __MINGW_PRINTF_FORMAT +[[gnu::format(__MINGW_PRINTF_FORMAT, 2, 3)]] +#else +[[gnu::format(printf, 2, 3)]] +#endif +inline int WriteLogLine(std::FILE* stream, const char* format, ...) { + char small[1024]; + std::va_list arguments; + va_start(arguments, format); + std::va_list again; + va_copy(again, arguments); + const int length = std::vsnprintf(small, sizeof(small), format, arguments); + va_end(arguments); + std::string large; + if (length >= static_cast(sizeof(small))) { + large.resize(static_cast(length)); + std::vsnprintf(large.data(), large.size() + 1, format, again); + } + va_end(again); + if (length < 0) return length; + std::fwrite(large.empty() ? small : large.data(), 1, static_cast(length), stream); + return std::fflush(stream); +} inline const char* TrimNidPostfix(const char* func) { return func; @@ -30,12 +57,10 @@ constexpr const char* BaseName(const char* path) { #define _APS5_FILE_ BaseName(__FILE__) #define _APS5_LOG_IMPL(stream, fmt, ...) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__), __VA_ARGS__) #define _APS5_LOG_IMPL_NF(stream, fmt) \ -(std::fprintf(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)), \ -std::fflush(stream)) +WriteLogLine(stream, "[%s:%d %s] " fmt "\n", _APS5_FILE_, __LINE__, TrimNidPostfix(__func__)) #define APS5_LOG_OUT(fmt, ...) _APS5_LOG_IMPL(stdout, fmt, __VA_ARGS__) #define APS5_LOG_ERR(fmt, ...) _APS5_LOG_IMPL(stderr, fmt, __VA_ARGS__) diff --git a/core/libs/prx/libc/src/CreationMask.cpp b/core/libs/prx/libc/src/CreationMask.cpp new file mode 100644 index 000000000..a4491eccd --- /dev/null +++ b/core/libs/prx/libc/src/CreationMask.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/CreationMask.hpp" + +#include + +namespace { + +std::atomic creationMask{022}; + +} + +namespace File { + +std::uint16_t ExchangeCreationMask_nid_no_patch(std::uint16_t mask) { + return creationMask.exchange(static_cast(mask & 07777), std::memory_order_relaxed); +} + +std::uint16_t GetCreationMask_nid_no_patch() { + return creationMask.load(std::memory_order_relaxed); +} + +} diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index c3eefced4..27c7332b1 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include #include #include @@ -79,7 +80,7 @@ void* APS5_VABI opendir_nid_postfix(const char* path) { if (!*path) { errno = 2; return nullptr; } try { auto directory = std::make_unique(); - directory->path = ResolvePath_nid_no_patch(path); + if (const int error = ResolvePathError_nid_no_patch(path, directory->path)) { errno = error; return nullptr; } directory->native = OpenNative(directory->path); if (!directory->native) { errno = DirectoryError(errno); return nullptr; } return directory.release(); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 20527e876..87c352e2b 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include #include #include @@ -13,6 +15,7 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" struct GuestFileMode { @@ -83,7 +86,15 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode const auto parsed = ParseFileMode(mode); if (!parsed) { errno = 22; return nullptr; } try { - const std::filesystem::path path = *filename ? ResolvePath_nid_no_patch(filename) : std::filesystem::path{}; + std::filesystem::path path; + if (*filename) { + if (const int error = ResolvePathError_nid_no_patch(filename, path)) { + stream->Close(); + if (stream->IsDynamic()) delete stream; + errno = error; + return nullptr; + } + } if (stream->Reopen(path, parsed->native.c_str())) { if (!path.empty() && parsed->writes) RecordWrittenPath_nid_no_patch(path); return stream; @@ -103,7 +114,8 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) errno = EINVAL; return nullptr; } - const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); + std::filesystem::path fpath; + if (const int error = ResolvePathError_nid_no_patch(filename, fpath)) { errno = error; return nullptr; } const auto abs_path = fpath.string(); std::unique_ptr handle(OpenNative(fpath, parsed->native), std::fclose); if (!handle) { @@ -284,6 +296,17 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); } +void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { + try { + return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); + } catch (const std::bad_alloc&) { + } catch (const std::length_error&) { + } + ApplicationHeapFree_nid_no_patch(ptr); + errno = 12; + return nullptr; +} + void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { return ApplicationHeapAlign_nid_no_patch(alignment, size); } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 370ff3f13..fefc33af4 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -115,6 +115,29 @@ struct WorkingDirectory { std::filesystem::path current = root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { + if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; + return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); +} +void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { + const auto host = HostPath(state, guest); + std::error_code error; + const auto status = std::filesystem::status(host, error); + if (std::filesystem::is_directory(status)) return; + if (!error) error = std::make_error_code(std::filesystem::exists(status) ? std::errc::not_a_directory : std::errc::no_such_file_or_directory); +#ifdef _WIN32 + if (error == std::errc::no_such_file_or_directory) { + for (auto prefix = host.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + std::error_code prefixError; + const auto prefixStatus = std::filesystem::status(prefix, prefixError); + if (!std::filesystem::exists(prefixStatus)) continue; + if (!std::filesystem::is_directory(prefixStatus)) error = std::make_error_code(std::errc::not_a_directory); + break; + } + } +#endif + throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); +} std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; @@ -123,10 +146,17 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { // Preserve the existing ability to pass explicit native drive paths. if (input.has_root_name()) return input; #endif - auto guest = (std::filesystem::path("/") / state.current.lexically_relative(state.root)); - guest = (input.is_absolute() ? input : guest / input).lexically_normal(); - if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; - return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); + const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); + const auto relative = (input.is_absolute() ? input : current / input).relative_path(); + std::filesystem::path guest("/"); + for (auto part = relative.begin(); part != relative.end(); ++part) { + if (*part == "..") { + const auto parent = guest.lexically_normal(); + if (parent.has_relative_path()) RequireHostDirectory(state, parent); + } + guest /= *part; + } + return HostPath(state, guest.lexically_normal()); } #ifdef _WIN32 using WriteMark = FILETIME; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index fea67b8cc..025f4faaa 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -32,10 +32,15 @@ std::atomic commitGeneration{1}; std::atomic privateMappingObserver{nullptr}; #endif -constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; -constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; -constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; -constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +#ifdef ANYPS5_ENABLE_ASAN +constexpr std::uintptr_t GuestArenaOffset = 0x0000100000000000ull; +#else +constexpr std::uintptr_t GuestArenaOffset = 0; +#endif +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { return (value + alignment - 1) & ~(static_cast(alignment) - 1); @@ -79,6 +84,21 @@ public: return reinterpret_cast(candidate); } + std::uintptr_t HighestFree(std::size_t bytes, std::size_t alignment) { + if (_base == 0) throw std::runtime_error("guest address space arena is unavailable"); + if (bytes == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest arena window"); + std::lock_guard lock(_lock); + auto limit = _end; + while (limit >= _base && bytes <= limit - _base) { + const auto candidate = (limit - bytes) & ~(alignment - 1); + if (candidate < _base) break; + const auto next = _used.lower_bound(candidate + bytes); + if (next == _used.begin() || std::prev(next)->second <= candidate) return candidate; + limit = std::prev(next)->first; + } + return 0; + } + bool MarkUsedForCommit(const void* pointer, std::size_t bytes) { std::lock_guard lock(_lock); const auto start = reinterpret_cast(pointer); @@ -264,6 +284,10 @@ void* GuestArenaAllocateAtOrAbove_nid_postfix(std::uintptr_t hint, std::size_t b return Arena::Get().AllocateAtOrAbove(hint, bytes, alignment); } +std::uintptr_t GuestArenaHighestFree_nid_no_patch(std::size_t bytes, std::size_t alignment) { + return Arena::Get().HighestFree(bytes, alignment); +} + void GuestArenaMarkUsed_nid_postfix(const void* pointer, std::size_t bytes) { Arena::Get().MarkUsed(pointer, bytes); } diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 109286ce8..55f991f04 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -333,7 +333,7 @@ public: begin = pageStart(begin); end = pageEnd(end); if (!_available || end <= begin) return; - std::vector pages((end - begin) / _page, Written); + std::vector pages((end - begin) / _page, Written | Fresh); std::vector queried(pages.size(), false); const Lock lock(_lock); for (auto cursor = begin; cursor < end;) { @@ -347,7 +347,7 @@ public: const auto last = std::min(end, address + size); const std::uint8_t protection = ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0); for (auto page = first; page < last; page += _page) { - pages[(page - begin) / _page] = Written | protection; + pages[(page - begin) / _page] = Written | Fresh | protection; queried[(page - begin) / _page] = true; } cursor = last; @@ -356,7 +356,7 @@ public: auto& entry = pages[(page - begin) / _page]; const std::uint8_t recorded = state & ProtectionMask; const bool armed = (state & Protected) != 0 && (entry & ProtectionMask) == (recorded & ~PROT_WRITE); - if (armed || !queried[(page - begin) / _page]) entry = static_cast(Written | recorded); + if (armed || !queried[(page - begin) / _page]) entry = static_cast(Written | Fresh | recorded); else state = static_cast(state & ~Protected); }); erase(begin, end); @@ -377,7 +377,9 @@ public: return covers(begin, end); } - bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context) { + enum class Take { All, Armed, Fresh }; + + bool Collect(std::uintptr_t begin, std::uintptr_t end, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context, Take take = Take::All) { if (!_available) return false; begin = pageStart(begin); end = pageEnd(end); @@ -385,9 +387,12 @@ public: std::vector> runs; { const Lock lock(_lock); - if (!covers(begin, end)) return false; + if (take != Take::Fresh && !covers(begin, end)) return false; forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { if ((state & Written) == 0) return; + const bool fresh = (state & Fresh) != 0; + if ((take == Take::Armed && fresh) || (take == Take::Fresh && !fresh)) return; + state &= static_cast(~Fresh); if (!runs.empty() && runs.back().second == page) runs.back().second = page + _page; else runs.emplace_back(page, page + _page); for (const auto& [first, last] : _hostWrites) @@ -407,7 +412,7 @@ public: if (!_available || end <= begin) return; const Lock lock(_lock); forEachPage(begin, end, [&](std::uintptr_t, std::uint8_t& state) { - state = static_cast(Written | (protection & ProtectionMask)); + state = static_cast(Written | (state & Fresh) | (protection & ProtectionMask)); }); } @@ -419,7 +424,7 @@ public: _hostWrites.emplace_back(begin, end); forEachPage(begin, end, [&](std::uintptr_t page, std::uint8_t& state) { if ((state & Protected) != 0) ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); - state = static_cast((state & ProtectionMask) | Written); + state = static_cast((state & (ProtectionMask | Fresh)) | Written); }); } @@ -453,6 +458,7 @@ private: static constexpr std::uint8_t ProtectionMask = PROT_READ | PROT_WRITE | PROT_EXEC; static constexpr std::uint8_t Written = 0x10; static constexpr std::uint8_t Protected = 0x20; + static constexpr std::uint8_t Fresh = 0x40; struct Range { std::uintptr_t end; @@ -512,7 +518,7 @@ private: if (page >= it->second.end) return false; auto& state = it->second.pages[(page - it->first) / _page]; if ((state & Protected) != 0) { - state = static_cast((state & ProtectionMask) | Written); + state = static_cast((state & (ProtectionMask | Fresh)) | Written); ::mprotect(reinterpret_cast(page), _page, state & ProtectionMask); return true; } @@ -708,9 +714,8 @@ bool GuestWriteWatchCollectFresh_nid_postfix(std::uintptr_t address, std::size_t if (bytes == 0 || address + bytes < address) return false; return Watch::Get().CollectFresh(address, address + bytes, written, context); #elif defined(__APPLE__) - static_cast(written); - static_cast(context); - return bytes != 0 && address + bytes >= address; + if (bytes == 0 || address + bytes < address) return false; + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Fresh); #else static_cast(address); static_cast(bytes); @@ -726,7 +731,7 @@ bool GuestWriteWatchCollectArmed_nid_postfix(std::uintptr_t address, std::size_t return Watch::Get().CollectArmed(address, address + bytes, written, context); #elif defined(__APPLE__) if (bytes == 0 || address + bytes < address) return false; - return Watch::Get().Collect(address, address + bytes, written, context); + return Watch::Get().Collect(address, address + bytes, written, context, Watch::Take::Armed); #else static_cast(address); static_cast(bytes); diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 9d6a8aebd..91dc67c43 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -36,7 +37,12 @@ std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t numerator, std::int64_t den } float APS5_VABI fmodf_nid_postfix(float x, float y) { return std::fmod(x, y); } -float APS5_VABI asinf_nid_postfix(float x) { return std::asin(x); } +float APS5_VABI asinf_nid_postfix(float x) { + const int saved_errno = errno; + const float result = std::asin(x); + errno = saved_errno; + return result; +} float APS5_VABI acosf_nid_postfix(float x) { return std::acos(x); } float APS5_VABI atan2f_nid_postfix(float y, float x) { return std::atan2(y, x); } float APS5_VABI hypotf_nid_postfix(float x, float y) { return static_cast(std::hypot(static_cast(x), static_cast(y))); } @@ -70,7 +76,13 @@ int APS5_VABI __isnormalf_nid_postfix(float x) { return std::isnormal(x) ? 1 : 0 int APS5_VABI __isinff_nid_postfix(float x) { return std::isinf(x) ? 1 : 0; } double APS5_VABI cbrt_nid_postfix(double x) { return std::cbrt(x); } -double APS5_VABI asin_nid_postfix(double x) { return std::asin(x); } +double APS5_VABI asin_nid_postfix(double x) { + if (std::isgreater(x, 1.0) || std::isless(x, -1.0)) { + std::feraiseexcept(FE_INVALID); + return -std::numeric_limits::quiet_NaN(); + } + return std::asin(x); +} double APS5_VABI acos_nid_postfix(double x) { return std::acos(x); } double APS5_VABI sqrt_nid_postfix(double x) { return std::sqrt(x); } float APS5_VABI sqrtf_nid_postfix(float x) { return std::sqrt(x); } @@ -121,6 +133,18 @@ float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } float APS5_VABI powf_nid_postfix(float base, float exp) { return std::pow(base, exp); } double APS5_VABI pow_nid_postfix(double base, double exp) { return std::pow(base, exp); } +float APS5_VABI __powisf2_nid_postfix(float base, int exponent) { + const bool reciprocal = exponent < 0; + float result = 1.0f; + unsigned int magnitude = reciprocal ? 0u - static_cast(exponent) : static_cast(exponent); + while (magnitude != 0u) { + if ((magnitude & 1u) != 0u) result *= base; + magnitude >>= 1u; + if (magnitude == 0u) break; + base *= base; + } + return reciprocal ? 1.0f / result : result; +} float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } @@ -150,6 +174,22 @@ double APS5_VABI modf_nid_postfix(double x, double* integral) { return std::modf float APS5_VABI modff_nid_postfix(float x, float* integral) { return std::modf(x, integral); } double APS5_VABI tanh_nid_postfix(double x) { return std::tanh(x); } float APS5_VABI tanhf_nid_postfix(float x) { return std::tanh(x); } +long double APS5_VABI atanl_nid_postfix(long double x) { return std::atan(x); } +long double APS5_VABI sinl_nid_postfix(long double x) { return std::sin(x); } +long double APS5_VABI cosl_nid_postfix(long double x) { return std::cos(x); } +long double APS5_VABI tanl_nid_postfix(long double x) { return std::tan(x); } +long double APS5_VABI sinhl_nid_postfix(long double x) { return std::sinh(x); } +long double APS5_VABI coshl_nid_postfix(long double x) { return std::cosh(x); } +long double APS5_VABI tanhl_nid_postfix(long double x) { return std::tanh(x); } +long double APS5_VABI expl_nid_postfix(long double x) { return std::exp(x); } +long double APS5_VABI exp2l_nid_postfix(long double x) { return std::exp2(x); } +long double APS5_VABI logl_nid_postfix(long double x) { return std::log(x); } +long double APS5_VABI log10l_nid_postfix(long double x) { return std::log10(x); } +long double APS5_VABI powl_nid_postfix(long double base, long double exp) { return std::pow(base, exp); } +long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } +long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } +double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index d64d02d4a..16e7c5792 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -16,6 +16,7 @@ namespace { constexpr unsigned MspaceThreadUnsafe = 1; constexpr std::uintptr_t Granule = 16; +constexpr std::uintptr_t MinimumAlignment = 32; constexpr std::uintptr_t ArenaHeaderBytes = 64; struct Chunk { @@ -60,7 +61,7 @@ void RemoveFree(Arena& arena, std::map::iterator chunk) { void* Allocate(Arena* arena, std::size_t size, std::size_t alignment) { if (!arena) return nullptr; - alignment = std::max(alignment, Granule); + alignment = std::max(alignment, MinimumAlignment); const auto needed = AlignUp(std::max(size, 1), Granule); if (needed < size || needed > std::numeric_limits::max() - alignment) { Error(12); diff --git a/core/libs/prx/libc/src/Random.cpp b/core/libs/prx/libc/src/Random.cpp index d53812833..49b48e16c 100644 --- a/core/libs/prx/libc/src/Random.cpp +++ b/core/libs/prx/libc/src/Random.cpp @@ -95,6 +95,8 @@ void SaveTypeWordLocked() { StoreWord(state.table, 0, static_cast(word)); } +std::uint16_t g_rand48Seed[3]{0x330e, 0xabcd, 0x1234}; + std::uint64_t NextRand48(std::uint16_t* seed, const char* function) { if (seed == nullptr) throw std::invalid_argument(std::string(function) + ": null seed"); constexpr std::uint64_t Multiplier = 0x5deece66dull; @@ -124,6 +126,28 @@ std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t* seed) { return static_cast(static_cast(NextRand48(seed, __func__) >> 16)); } +double APS5_VABI drand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return std::ldexp(static_cast(NextRand48(g_rand48Seed, __func__)), -48); +} + +std::int64_t APS5_VABI lrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(NextRand48(g_rand48Seed, __func__) >> 17); +} + +std::int64_t APS5_VABI mrand48_nid_postfix() { + std::lock_guard lock(g_randomLock); + return static_cast(static_cast(NextRand48(g_rand48Seed, __func__) >> 16)); +} + +void APS5_VABI srand48_nid_postfix(std::int64_t seedval) { + std::lock_guard lock(g_randomLock); + g_rand48Seed[0] = 0x330e; + g_rand48Seed[1] = static_cast(static_cast(seedval)); + g_rand48Seed[2] = static_cast(static_cast(seedval) >> 16); +} + std::int64_t APS5_VABI random_nid_postfix() { std::lock_guard lock(g_randomLock); return static_cast(NextLocked()); diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 6c54283da..69be0d848 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -12,6 +12,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif #include "prx/libc/include/General.hpp" #include "prx/libc/include/SonyCrt.hpp" @@ -32,6 +35,20 @@ std::mutex g_exitMutex; std::vector g_exitDestructors; bool g_exitRunnerRegistered = false; +#ifdef _WIN32 +struct UcrtStream { + char* pointer; + char* base; + int count; + long flags; + long file; + int charBuffer; + int bufferSize; + char* temporaryName; + CRITICAL_SECTION lock; +}; +#endif + } extern "C" { @@ -142,6 +159,15 @@ void APS5_VABI funlockfile_nid_postfix(FileStream* stream) { _Unlockfilelock_nid_postfix(stream); } +int APS5_VABI ftrylockfile_nid_postfix(FileStream* stream) { + if (stream == nullptr) throw std::invalid_argument("ftrylockfile: null stream"); +#ifdef _WIN32 + return TryEnterCriticalSection(&reinterpret_cast(GetNativeStream(stream))->lock) ? 0 : -1; +#else + return ftrylockfile(GetNativeStream(stream)) == 0 ? 0 : -1; +#endif +} + std::uint64_t APS5_VABI _Stoul_nid_postfix(const char* str, char** endptr, int base) { if (StopAtPrefixLetter_nid_no_patch(str, endptr, base)) return 0; return std::strtoull(str, endptr, base); diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 978a499aa..564458ace 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -176,7 +176,7 @@ long double APS5_VABI strtold_nid_postfix(const char* str, char** endptr) { } int APS5_VABI atoi_nid_postfix(const char* str) { - return std::atoi(str); + return static_cast(strtol_nid_postfix(str, nullptr, 10)); } std::int64_t APS5_VABI atol_nid_postfix(const char* str) { @@ -427,6 +427,25 @@ size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept) { return count; } +char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last) { + if (text == nullptr) text = *last; + if (text == nullptr) return nullptr; + while (*text != 0 && Contains(delimiters, *text)) ++text; + if (*text == 0) { + *last = nullptr; + return nullptr; + } + char16_t* end = text; + while (*end != 0 && !Contains(delimiters, *end)) ++end; + if (*end == 0) { + *last = nullptr; + } else { + *end = 0; + *last = end + 1; + } + return text; +} + char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, size_t n) { for (size_t index = 0; index < n; ++index) s[index] = c; return s; diff --git a/core/libs/prx/libc/src/Syslog.cpp b/core/libs/prx/libc/src/Syslog.cpp index c7cc00b3e..e582d236e 100644 --- a/core/libs/prx/libc/src/Syslog.cpp +++ b/core/libs/prx/libc/src/Syslog.cpp @@ -19,6 +19,7 @@ constexpr int priorityMask = 0x07; constexpr int facilityMask = 0x3f8; constexpr int userFacility = 1 << 3; constexpr int internalLogPriority = 3 | 0x02 | 0x20 | 0x01; +int logMask = 0xff; std::mutex& OutputMutex() { static std::mutex mutex; @@ -26,7 +27,6 @@ std::mutex& OutputMutex() { } void Emit(int priority, const std::string& message) { - std::lock_guard lock(OutputMutex()); std::fprintf(stderr, "[syslog:%d] %s", priority, message.c_str()); if (message.empty() || message.back() != '\n') std::fputc('\n', stderr); std::fflush(stderr); @@ -75,16 +75,28 @@ std::string Format(const char* format, VaList* args) { } extern "C" { +int APS5_VABI setlogmask_nid_postfix(int mask) { + std::lock_guard lock(OutputMutex()); + const int previous = logMask; + if (mask != 0) logMask = mask; + return previous; +} + void APS5_VABI vsyslog_nid_postfix(int priority, const char* format, VaList* args) { const int saved = *__error_nid_postfix(); + std::lock_guard lock(OutputMutex()); if (priority & ~(priorityMask | facilityMask)) { - char complaint[64]; - std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); - Emit(internalLogPriority, complaint); + if (logMask & (1 << (internalLogPriority & priorityMask))) { + char complaint[64]; + std::snprintf(complaint, sizeof(complaint), "syslog: unknown facility/priority: %x", priority); + Emit(internalLogPriority, complaint); + } priority &= priorityMask | facilityMask; } - if ((priority & facilityMask) == 0) priority |= userFacility; - Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + if (logMask & (1 << (priority & priorityMask))) { + if ((priority & facilityMask) == 0) priority |= userFacility; + Emit(priority, format ? Format(ExpandErrorText(format, saved).c_str(), args) : std::string()); + } *__error_nid_postfix() = saved; } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index 5399fd351..4565d8331 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -173,4 +173,9 @@ int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { return threadSuccess; } +int SignalCondition_nid_no_patch(void** handle) { + ResolveCondition(handle, "_Cnd_signal")->native.notify_one(); + return threadSuccess; +} + } diff --git a/core/libs/prx/libc/src/exception/Exports.cpp b/core/libs/prx/libc/src/exception/Exports.cpp index 2bf7ef211..dbc0c7acb 100644 --- a/core/libs/prx/libc/src/exception/Exports.cpp +++ b/core/libs/prx/libc/src/exception/Exports.cpp @@ -52,7 +52,7 @@ void NativeFreeException(void* object) asm("__cxa_free_exception"); void NativeFreeException(void* object) { __cxa_free_exception_nid_postfix(object); } [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) asm("__cxa_throw"); [[noreturn]] void NativeThrow(void* object, std::type_info* type, void (*destructor)(void*)) { - __cxa_throw_nid_postfix(object, type, destructor); + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); } void* NativeBeginCatch(void* exception) asm("__cxa_begin_catch"); void* NativeBeginCatch(void* exception) { return __cxa_begin_catch_nid_postfix(exception); } diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index b9e26d77f..7b18999b7 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -229,7 +229,7 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { std::free(allocation); } -[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { +[[noreturn]] static void ThrowFromSite(void* object, std::type_info* type, void (*destructor)(void*), void* throwSite) { using namespace LibcException; auto* header = FromObject(object); header->type = type; @@ -240,11 +240,15 @@ void APS5_VABI __cxa_free_exception_nid_postfix(void* object) { header->unwind.exception_cleanup = Cleanup; ++globals.uncaught; _Unwind_RaiseException_nid_postfix(&header->unwind); - RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", __builtin_return_address(0)); + RawLog("[libc] unhandled exception '%s' thrown from %p\n", type ? type->name() : "?", throwSite); __cxa_begin_catch_nid_postfix(&header->unwind); InvokeTerminate(header->terminate); } +[[noreturn]] void APS5_VABI __cxa_throw_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { + ThrowFromSite(object, type, destructor, __builtin_return_address(0)); +} + LibcException::Header* __cxa_init_primary_exception_nid_postfix(void* object, std::type_info* type, void (*destructor)(void*)) { using namespace LibcException; auto* header = FromObject(object); diff --git a/core/libs/prx/libc/src/exception/Unwind.cpp b/core/libs/prx/libc/src/exception/Unwind.cpp index b958b18f8..e38aa9fb1 100644 --- a/core/libs/prx/libc/src/exception/Unwind.cpp +++ b/core/libs/prx/libc/src/exception/Unwind.cpp @@ -1,7 +1,11 @@ #include "prx/libc/include/exceptions/Unwind.hpp" +#include "prx/libc/include/exceptions/Runtime.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" #include "prx/libc/src/specifics/x86_64/RegisterContext.cpp" +#if defined(__linux__) +#include +#endif #ifdef _WIN32 #include #include @@ -502,6 +506,14 @@ _Unwind_Reason_Code APS5_VABI _Unwind_RaiseException_nid_postfix(_Unwind_Excepti } [[noreturn]] void APS5_VABI _Unwind_Resume_nid_postfix(_Unwind_Exception* exception) { +#if defined(__linux__) + if (!LibcException::Native(exception->exception_class) && !exception->private_1) { + using Resume = void (*)(_Unwind_Exception*); + const auto resume = reinterpret_cast(dlsym(RTLD_NEXT, "_Unwind_Resume")); + if (!resume) std::abort(); + resume(exception); + } +#endif _Unwind_Context context; LibcCaptureRegisters(context.registers); if (!LibcUnwind::Step(context)) std::abort(); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 099d6392a..53d651430 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -31,6 +31,7 @@ void Report(const char* format, ...) { } void ReportAllThreads(); +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span); void DescribeAddress(std::uint64_t address, char* buffer, std::size_t size) { HMODULE module = nullptr; @@ -296,16 +297,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { if (next <= frame) break; frame = next; } - Report(" stack return address candidates:\n"); - int printed = 0; - for (std::uint64_t slot = context->Rsp; printed < 16 && slot < context->Rsp + 0x2000; slot += 8) { - if (!IsReadable(slot)) break; - const auto value = *reinterpret_cast(slot); - if (!IsExecutable(value)) continue; - DescribeAddress(value, line, sizeof(line)); - Report(" [rsp+0x%llx] %s\n", static_cast(slot - context->Rsp), line); - ++printed; - } + ReportStackCandidates(context->Rsp, 16, 0x2000); if (context->Rip == 0 || !IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. @@ -433,6 +425,20 @@ DWORD WINAPI HangWatchdog(LPVOID param) { return 0; } +void ReportStackCandidates(std::uint64_t rsp, int limit, std::uint64_t span) { + char line[256]; + Report(" stack return address candidates:\n"); + int printed = 0; + for (std::uint64_t slot = rsp; printed < limit && slot < rsp + span; slot += 8) { + if (!IsReadable(slot)) break; + const auto value = *reinterpret_cast(slot); + if (!IsExecutable(value)) continue; + DescribeAddress(value, line, sizeof(line)); + Report(" [rsp+0x%llx] %s\n", static_cast(slot - rsp), line); + ++printed; + } +} + // abort() and the UCRT's invalid-parameter path end the process with a silent fast fail // (0xc0000409); both are reported with the calling thread's stack first. void ReportBacktrace(const char* what, bool reportThreads) { @@ -444,6 +450,7 @@ void ReportBacktrace(const char* what, bool reportThreads) { DescribeAddress(reinterpret_cast(frames[i]), line, sizeof(line)); Report(" #%u %s\n", static_cast(i), line); } + ReportStackCandidates(reinterpret_cast(__builtin_frame_address(0)), 32, 0x4000); if (reportThreads) ReportAllThreads(); std::fflush(stderr); } diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index 040773e22..a89f42561 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -11,6 +11,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #ifndef _WIN32 @@ -28,6 +29,10 @@ constexpr std::int32_t AioRequestNumMax = 128; constexpr std::int32_t AioIdNumMax = 128; constexpr std::uint32_t AioWaitAnd = 1; constexpr std::uint32_t AioWaitOr = 2; +constexpr std::int32_t AioSchedulingWindowMax = 128; +constexpr std::int32_t AioDelayedCountMax = 128; +constexpr std::uint32_t AioSplitSizeMax = 0x1000000; +constexpr std::uint32_t AioSplitChunkSizeMax = 0x1000000; std::mutex g_mutex; std::array g_states{}; @@ -76,7 +81,8 @@ std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, s } return NativePositioned_nid_no_patch(fd, const_cast(buf), nbyte, offset, true); #else - return static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(fd, buf, nbyte, offset); + return result == -2 ? static_cast(::pwrite(fd, buf, nbyte, static_cast(offset))) : result; #endif } @@ -204,6 +210,20 @@ void APS5_VABI sceKernelAioInitializeParam(void* param) { if (param == nullptr) throw std::invalid_argument("sceKernelAioInitializeParam: param is null"); } +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, int32_t schedulingWindowSize, int32_t delayedCountLimit, uint32_t enableSplit, uint32_t splitSize, uint32_t splitChunkSize) { + if (param == nullptr) throw std::invalid_argument("sceKernelAioSetParam: param is null"); + if (schedulingWindowSize < 0 || schedulingWindowSize > AioSchedulingWindowMax) throw std::runtime_error("sceKernelAioSetParam: a scheduling window outside 0..128 is not modelled"); + if (delayedCountLimit < 0 || delayedCountLimit > AioDelayedCountMax) throw std::runtime_error("sceKernelAioSetParam: a delayed count limit outside 0..128 is not modelled"); + if (enableSplit > 1) throw std::runtime_error("sceKernelAioSetParam: a split flag other than 0 or 1 is not modelled"); + if (splitSize > AioSplitSizeMax || splitChunkSize > AioSplitChunkSizeMax) throw std::runtime_error("sceKernelAioSetParam: a split size above 0x1000000 is not modelled"); + param->schedulingWindowSize = schedulingWindowSize; + param->delayedCountLimit = delayedCountLimit; + param->enableSplit = enableSplit; + param->splitSize = splitSize; + param->splitChunkSize = splitChunkSize; + return 0; +} + int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { (void)prio; return SubmitCommands(req, size, false, id); diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index 3645c476d..4f515a105 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -95,6 +95,27 @@ std::uint32_t parseHex4(const std::string& text, std::size_t& position) { return value; } +void appendRawUtf8(const std::string& text, std::size_t& position, unsigned char first, std::string& result) { + unsigned count; + std::uint32_t codepoint; + std::uint32_t minimum; + if (first >= 0xC2 && first <= 0xDF) { count = 1; codepoint = first & 0x1F; minimum = 0x80; } + else if (first >= 0xE0 && first <= 0xEF) { count = 2; codepoint = first & 0x0F; minimum = 0x800; } + else if (first >= 0xF0 && first <= 0xF4) { count = 3; codepoint = first & 0x07; minimum = 0x10000; } + else throw std::runtime_error("invalid UTF-8 in param.json"); + if (count > text.size() - position) throw std::runtime_error("truncated UTF-8 in param.json"); + for (unsigned index = 0; index < count; ++index) { + const auto byte = static_cast(text[position + index]); + if ((byte & 0xC0) != 0x80) throw std::runtime_error("invalid UTF-8 continuation in param.json"); + codepoint = (codepoint << 6) | (byte & 0x3F); + } + if (codepoint < minimum || codepoint > 0x10FFFF || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) + throw std::runtime_error("invalid UTF-8 codepoint in param.json"); + result.push_back(static_cast(first)); + result.append(text, position, count); + position += count; +} + std::string parseJsonString(const std::string& text, std::size_t& position) { expectChar(text, position, '"'); std::string result; @@ -103,7 +124,11 @@ std::string parseJsonString(const std::string& text, std::size_t& position) { const char c = text[position++]; if (c == '"') break; if (static_cast(c) < 0x20) throw std::runtime_error("control character in param.json string"); - if (c != '\\') { result.push_back(c); continue; } + if (c != '\\') { + if (static_cast(c) < 0x80) result.push_back(c); + else appendRawUtf8(text, position, static_cast(c), result); + continue; + } if (position >= text.size()) throw std::runtime_error("unterminated escape in param.json"); const char escape = text[position++]; switch (escape) { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 8aa37a332..8e307d014 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -1,7 +1,12 @@ +#include +#include +#include +#include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/PreciseWait.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" @@ -107,7 +112,8 @@ int _fail(int guestErrno) { bool _resolve(const char* guestPath, std::uint32_t* id, std::uint64_t* size) { if (!guestPath) return false; - auto hostPath = ResolvePath_nid_no_patch(guestPath); + std::filesystem::path hostPath; + if (ResolvePathError_nid_no_patch(guestPath, hostPath) != 0) return false; std::uint64_t bytes = 0; if (!_fileSize(hostPath, bytes)) return false; std::lock_guard lock(g_filesLock); @@ -242,12 +248,16 @@ bool _waitSatisfied(std::uint32_t compare, std::uint64_t value, std::uint64_t re template TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { + Apr::CommandHeader header; + std::memcpy(&header, buffer.base + cursor, sizeof(header)); + if (header.bytes < sizeof(TCommand)) throw std::runtime_error("APR: truncated command payload (opcode " + std::to_string(static_cast(header.opcode)) + ", has " + std::to_string(header.bytes) + ", needs " + std::to_string(sizeof(TCommand)) + ")"); TCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); return command; } constexpr std::uint64_t AmmRangeBytes = 32ull << 30; +constexpr std::uint64_t AmmRangeAlignment = 2ull << 20; constexpr int GuestMapFixed = 0x10; constexpr int GuestMapNoOverwrite = 0x80; @@ -279,7 +289,9 @@ std::uintptr_t _ammBase(AmmState& state) { std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); if (arenaBase == 0 || arenaBytes < 2 * AmmRangeBytes) throw std::runtime_error("AMM: the guest address space arena cannot hold the virtual address range"); - void* address = reinterpret_cast(arenaBase + arenaBytes - 2 * AmmRangeBytes); + const auto top = GuestArena::GuestArenaHighestFree_nid_no_patch(2 * AmmRangeBytes, AmmRangeAlignment); + if (top == 0) throw std::runtime_error("AMM: the guest address space arena has no free virtual address range"); + void* address = reinterpret_cast(top); if (DoReserveVirtual(&address, 2 * AmmRangeBytes, GuestMapFixed | GuestMapNoOverwrite, 0) != 0) throw std::runtime_error("AMM: cannot reserve the virtual address range"); state.base = reinterpret_cast(address); } @@ -523,23 +535,28 @@ void _readResolved(Apr::Opcode opcode, Apr::ReadFileCommand command, ReadCursor& read = {true, command.fileId, command.destination + command.size, command.offset + command.size}; } -void _execute(const Apr::CommandBufferObject& buffer) { +struct Deferred { std::uint32_t cursor = 0; ReadCursor read; +}; + +std::optional _execute(const Apr::CommandBufferObject& buffer, std::uint32_t start = 0, ReadCursor read = {}, bool defer = false) { + std::uint32_t cursor = start; while (cursor < buffer.offset) { if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; std::memcpy(&header, buffer.base + cursor, sizeof(header)); if ((static_cast(header.opcode) & 0xffu) == static_cast(Apr::Opcode::CompactReadFileGather)) { if (sizeof(Apr::CompactReadFileGatherCommand) > buffer.offset - cursor) throw std::runtime_error("APR: truncated compact gather command"); - const auto command = _read(buffer, cursor); + Apr::CompactReadFileGatherCommand command; + std::memcpy(&command, buffer.base + cursor, sizeof(command)); const auto offset = static_cast(command.opcodeAndOffsetHigh >> 8u) << 32u | command.offsetLow; if (offset >= 0x10000000000ull) throw std::runtime_error("APR: malformed compact gather offset"); _readResolved(Apr::Opcode::ReadFileGather, Apr::ReadFileCommand{{}, 0, 0, 0, static_cast(command.sizeMinusOne) + 1u, offset}, read); cursor += sizeof(command); continue; } - if (header.bytes < sizeof(header) || cursor + header.bytes > buffer.offset) throw std::runtime_error("APR: malformed command"); + if (header.bytes < sizeof(header) || header.bytes > buffer.offset - cursor) throw std::runtime_error("APR: malformed command"); switch (header.opcode) { case Apr::Opcode::Nop: case Apr::Opcode::PushMarker: @@ -585,8 +602,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { case Apr::Opcode::MapEnd: break; case Apr::Opcode::WriteAddress: { - Apr::WriteAddressCommand command; - std::memcpy(&command, buffer.base + cursor, sizeof(command)); + const auto command = _read(buffer, cursor); _writeAddress(command); break; } @@ -604,6 +620,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { return std::atomic_ref(*reinterpret_cast(command.address)).load(std::memory_order_acquire); }; const std::uint64_t reference = (command.reference & command.mask) << unused; + if (defer && !_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) return Deferred{cursor, read}; while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) PreciseSleepUs(50); break; } @@ -630,6 +647,7 @@ void _execute(const Apr::CommandBufferObject& buffer) { } cursor += header.bytes; } + return std::nullopt; } } @@ -658,16 +676,107 @@ void AmmVirtualAddressRanges_nid_no_patch(std::uint64_t* start, std::uint64_t* e if (multimapEnd) *multimapEnd = base + 2 * AmmRangeBytes; } +struct AprQueue { + std::mutex submit; + std::mutex mutex; + std::condition_variable ready; + std::condition_variable done; + struct Work { + std::vector commands; + Deferred resume; + std::uint32_t id = 0; + }; + std::deque pending; + bool running = false; + std::uint32_t submitted = 0; + std::uint32_t completed = 0; +}; + +AprQueue* _startQueue() { + auto* created = new AprQueue(); + std::thread([created] { + for (;;) { + AprQueue::Work work; + { + std::unique_lock lock(created->mutex); + created->ready.wait(lock, [&] { return !created->pending.empty(); }); + work = std::move(created->pending.front()); + created->pending.pop_front(); + created->running = true; + } + try { + const auto bytes = static_cast(work.commands.size()); + _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, work.commands.data()}, work.resume.cursor, work.resume.read); + } catch (const std::exception& error) { + std::fprintf(stderr, "APR: %s\n", error.what()); + std::fflush(stderr); + std::abort(); + } + { + std::lock_guard lock(created->mutex); + created->running = false; + created->completed = std::max(created->completed, work.id); + } + created->done.notify_all(); + } + }).detach(); + return created; +} + +AprQueue& _queue(bool amm) { + static AprQueue* const queues[2] = {_startQueue(), _startQueue()}; + return *queues[amm ? 1 : 0]; +} + +std::uint32_t _submit(const std::uint8_t* base, std::uint32_t bytes, bool amm) { + auto& queue = _queue(amm); + std::lock_guard submitLock(queue.submit); + std::uint32_t id; + bool queued; + { + std::lock_guard lock(queue.mutex); + id = ++queue.submitted; + queued = queue.running || !queue.pending.empty(); + } + const auto enqueue = [&](Deferred resume) { + { + std::lock_guard lock(queue.mutex); + queue.pending.push_back({std::vector(base, base + bytes), resume, id}); + } + queue.ready.notify_one(); + }; + if (queued) { + enqueue({}); + return id; + } + if (const auto deferred = _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, const_cast(base)}, 0, {}, true)) { + enqueue(*deferred); + return id; + } + { + std::lock_guard lock(queue.mutex); + queue.completed = std::max(queue.completed, id); + } + queue.done.notify_all(); + return id; +} + +void _waitSubmitted(std::uint32_t id, bool amm) { + auto& queue = _queue(amm); + std::unique_lock lock(queue.mutex); + queue.done.wait(lock, [&] { return queue.completed >= id || id > queue.submitted; }); +} + std::uint32_t AmmSubmit_nid_no_patch(void* base, std::uint32_t bytes) { - _execute(Apr::CommandBufferObject{Apr::BufferType::Generic, 0, bytes, 0, bytes, static_cast(base)}); - auto& last = _amm().lastSubmit; - std::uint32_t id = last.fetch_add(1) + 1; - while (id == 0) id = last.fetch_add(1) + 1; + const auto id = _submit(static_cast(base), bytes, true); + _amm().lastSubmit.store(id); return id; } bool AmmSubmitted_nid_no_patch(std::uint32_t id) { - return id != 0 && id <= _amm().lastSubmit.load(); + if (id == 0 || id > _amm().lastSubmit.load()) return false; + _waitSubmitted(id, true); + return true; } @@ -729,16 +838,15 @@ int APS5_VABI sceKernelAprGetFileStat(uint32_t id, FileStat* stat) { int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject* buffer, uint32_t priority) { (void)priority; if (!buffer) return _fail(GUEST_EINVAL); - _execute(*buffer); + _submit(buffer->base, buffer->offset, false); return 0; } int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject* buffer, uint32_t priority, uint32_t* id) { if (!id) return _fail(GUEST_EINVAL); - const int result = sceKernelAprSubmitCommandBuffer(buffer, priority); - if (result != 0) return result; - static std::atomic nextId{1}; - *id = nextId.fetch_add(1); + (void)priority; + if (!buffer) return _fail(GUEST_EINVAL); + *id = _submit(buffer->base, buffer->offset, false); return 0; } @@ -751,7 +859,7 @@ int APS5_VABI sceKernelAprSubmitCommandBufferAndGetResult(const Apr::CommandBuff } int APS5_VABI sceKernelAprWaitCommandBuffer(uint32_t id) { - (void)id; + _waitSubmitted(id, false); return 0; } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..a85c9ecaa 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Once.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/AddressConversion.cpp @@ -314,6 +315,16 @@ if(BUILD_TESTING) set_tests_properties(guest_thread_self PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_spin_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSpin.cpp) + target_include_directories(guest_pthread_spin_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_spin_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_spin_tests) + add_test(NAME guest_pthread_spin COMMAND guest_pthread_spin_tests) + set_tests_properties(guest_pthread_spin PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_spin PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_getname_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadGetname.cpp) target_include_directories(guest_pthread_getname_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_getname_tests PRIVATE libkernel libc) @@ -394,6 +405,16 @@ if(BUILD_TESTING) set_tests_properties(guest_cond_unowned_mutex PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getitimer_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetitimer.cpp) + target_include_directories(guest_getitimer_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getitimer_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getitimer_tests) + add_test(NAME guest_getitimer COMMAND guest_getitimer_tests) + set_tests_properties(guest_getitimer PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getitimer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_cpu_time_clocks_tests ${LIBS_INCLUDE_DIR}/tests/GuestCpuTimeClocks.cpp) target_include_directories(guest_cpu_time_clocks_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cpu_time_clocks_tests PRIVATE libkernel libc) @@ -496,6 +517,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_key PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_destructor_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadDestructorLifetime.cpp) + target_include_directories(guest_pthread_destructor_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_destructor_lifetime_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_destructor_lifetime_tests) + add_test(NAME guest_pthread_destructor_lifetime COMMAND guest_pthread_destructor_lifetime_tests) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_destructor_lifetime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_user_context_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserContext.cpp) target_include_directories(guest_user_context_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_user_context_tests PRIVATE libSceLibcInternal libkernel libc) @@ -517,6 +548,15 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_ioctl_getsid_tests ${LIBS_INCLUDE_DIR}/tests/GuestIoctlGetsid.cpp) + target_include_directories(guest_ioctl_getsid_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ioctl_getsid_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_ioctl_getsid_tests) + add_test(NAME guest_ioctl_getsid COMMAND guest_ioctl_getsid_tests) + if(WIN32) + set_tests_properties(guest_ioctl_getsid PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sigtimedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigtimedwait.cpp) target_include_directories(guest_sigtimedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sigtimedwait_tests PRIVATE libkernel libc) @@ -701,6 +741,16 @@ if(BUILD_TESTING) set_tests_properties(guest_execvp PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sendfile_tests ${LIBS_INCLUDE_DIR}/tests/GuestSendfile.cpp) + target_include_directories(guest_sendfile_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sendfile_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sendfile_tests) + add_test(NAME guest_sendfile COMMAND guest_sendfile_tests) + set_tests_properties(guest_sendfile PROPERTIES TIMEOUT 60) + if(WIN32) + set_tests_properties(guest_sendfile PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp) target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) @@ -736,6 +786,38 @@ if(BUILD_TESTING) target_link_libraries(guest_shm_open_tests PRIVATE libkernel libc) configure_windows_unwind(guest_shm_open_tests) add_test(NAME guest_shm_open COMMAND guest_shm_open_tests) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_named_shm_open_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmOpen.cpp) + target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_open_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) + set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_lifecycle_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmLifecycle.cpp) + target_include_directories(guest_named_shm_lifecycle_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_lifecycle_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_lifecycle COMMAND guest_named_shm_lifecycle_tests) + set_tests_properties(guest_named_shm_lifecycle PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmDescriptorReuse.cpp) + target_include_directories(guest_named_shm_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_descriptor_reuse_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_descriptor_reuse COMMAND guest_named_shm_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_descriptor_reuse PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_permissions_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmPermissions.cpp) + target_include_directories(guest_named_shm_permissions_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_permissions_tests PRIVATE libkernel_sys libkernel libc rt) + add_test(NAME guest_named_shm_permissions COMMAND guest_named_shm_permissions_tests) + set_tests_properties(guest_named_shm_permissions PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_io_descriptor_reuse_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmIoDescriptorReuse.cpp) + target_include_directories(guest_named_shm_io_descriptor_reuse_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_io_descriptor_reuse_tests PRIVATE libkernel libc rt) + add_test(NAME guest_named_shm_io_descriptor_reuse COMMAND guest_named_shm_io_descriptor_reuse_tests) + set_tests_properties(guest_named_shm_io_descriptor_reuse PROPERTIES TIMEOUT 20) + add_test_executable(guest_named_shm_concurrency_tests ${LIBS_INCLUDE_DIR}/tests/GuestNamedShmConcurrency.cpp) + target_include_directories(guest_named_shm_concurrency_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_named_shm_concurrency_tests PRIVATE libkernel libc rt ${CMAKE_DL_LIBS}) + add_test(NAME guest_named_shm_concurrency COMMAND guest_named_shm_concurrency_tests) + set_tests_properties(guest_named_shm_concurrency PROPERTIES TIMEOUT 20) + endif() if(WIN32) set_tests_properties(guest_shm_open PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") @@ -767,6 +849,10 @@ if(BUILD_TESTING) set_tests_properties(guest_system_command PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_socket_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketError.cpp) + target_include_directories(guest_socket_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_socket_error_tests) + add_test(NAME guest_socket_error COMMAND guest_socket_error_tests) add_test_executable(guest_udp_tests ${LIBS_INCLUDE_DIR}/tests/GuestUdp.cpp) target_include_directories(guest_udp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_udp_tests PRIVATE libkernel libc) @@ -797,6 +883,14 @@ if(BUILD_TESTING) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_test_executable(guest_select_tests ${LIBS_INCLUDE_DIR}/tests/GuestSelect.cpp) + target_include_directories(guest_select_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_select_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_select_tests) + add_test(NAME guest_select COMMAND guest_select_tests) + set_tests_properties(guest_select PROPERTIES TIMEOUT 20) + endif() add_test_executable(guest_tcp_tests ${LIBS_INCLUDE_DIR}/tests/GuestTcp.cpp) target_include_directories(guest_tcp_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_tcp_tests PRIVATE libkernel libc) @@ -870,12 +964,12 @@ if(BUILD_TESTING) target_include_directories(guest_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_memory_tests PRIVATE libkernel libc) configure_windows_unwind(guest_memory_tests) + add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) + set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") + add_dependencies(guest_memory_tests guest_memory_module) + target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") add_test(NAME guest_memory COMMAND guest_memory_tests) if(WIN32) - add_library(guest_memory_module SHARED ${LIBS_INCLUDE_DIR}/tests/GuestMemoryModule.cpp) - set_target_properties(guest_memory_module PROPERTIES PREFIX "" SUFFIX ".guest.prx") - add_dependencies(guest_memory_tests guest_memory_module) - target_compile_definitions(guest_memory_tests PRIVATE GUEST_MEMORY_MODULE="$") set_tests_properties(guest_memory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() @@ -908,6 +1002,15 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_buffer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_directory_rewind_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryRewind.cpp) + target_include_directories(guest_directory_rewind_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_rewind_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_directory_rewind_tests) + add_test(NAME guest_directory_rewind COMMAND guest_directory_rewind_tests) + set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(NOT WIN32) add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 1fae6ec14..8d1916f7b 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -830,6 +830,45 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment) { return 0; } +int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment) { + ValidateOutput(addr); + if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || (offset & (PS5_PAGE_SIZE - 1)) != 0 || len > static_cast(std::numeric_limits::max()) || offset > static_cast(std::numeric_limits::max()) - len) return SCE_KERNEL_ERROR_EINVAL; +#if defined(__linux__) + const bool shared = (flags & 3) == 1; + GuestAllocations::Mutation mutation; + const int placementFlags = flags & (GuestMapFixedFlag | 0x80 | 0x400000); + flags = PlaceHintInReservation(*addr, len, placementFlags); + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; + ReplaceFixedOverlap(mutation, *addr, len, flags); + void* mapped = MapPlaced(*addr, len, PROT_NONE, flags, alignment); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(mapped, len); + const int nativeProtection = LinuxProtFromSce(prot); + const int nativeFlags = (shared ? MAP_SHARED : MAP_PRIVATE) | MAP_FIXED; + if (::mmap(mapped, len, nativeProtection, nativeFlags, descriptor, static_cast(offset)) == MAP_FAILED) { + const int error = errno; + Unmap(mapped, len); + throw std::system_error(error, std::generic_category(), "map guest shared memory"); + } + try { + mutation.Add(mapped, len, (prot & 3) != 0, (prot & 2) != 0, false); + } catch (...) { + Unmap(mapped, len); + throw; + } + *addr = mapped; + EraseReservations(mapped, len); + RecordProtection(mapped, len, prot); + Trace("map file %p+0x%zx offset=0x%llx prot=0x%x flags=0x%x", mapped, len, static_cast(offset), prot, flags); + return 0; +#else + static_cast(prot); + static_cast(flags); + static_cast(descriptor); + static_cast(offset); + throw std::system_error(ENOTSUP, std::generic_category(), "file-backed guest mmap is not supported on this host"); +#endif +} + #ifdef _WIN32 static bool IsGuestModuleImage(const void* base) { std::wstring path(MAX_PATH, L'\0'); @@ -882,6 +921,10 @@ int DoMprotect(const void* addr, size_t len, int prot) { else if (IsGuestModuleImage(memory.AllocationBase)) mutation.RegisterImage(memory.AllocationBase); else throw std::invalid_argument("Memory protection of a foreign image is not supported"); } + if (memory.Type == MEM_PRIVATE && memory.State == MEM_COMMIT && memory.RegionSize >= bytes && !mutation.Overlaps(pointer, bytes)) { + if (mprotect(const_cast(pointer), bytes, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "mprotect failed"); + return 0; + } #else mutation.RegisterMainImage(); #endif diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index 591dabb17..e6d1a5a60 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -30,6 +30,7 @@ bool DirectMemoryLargestFree(int64_t searchStart, int64_t searchEnd, size_t alig size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags, size_t alignment = PS5_PAGE_SIZE); +int DoMapFileBacked_nid_no_patch(void** addr, size_t len, int prot, int flags, int descriptor, std::uint64_t offset, size_t alignment); int DoMprotect(const void* addr, size_t len, int prot); int DoMtypeprotect(const void* addr, size_t len, int type, int prot); int DoMunmap(void* addr, size_t len); diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 91c0a1f6b..7deed55ab 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -30,6 +30,8 @@ #include #include +extern "C" std::uint64_t GuestImageSegmentBytes_nid_no_patch(); + namespace { constexpr size_t FLEXIBLE_MEMORY_SIZE = 448ULL * 1024 * 1024; @@ -90,9 +92,11 @@ size_t _flexibleUsedLocked() { } int _mapFlexible(void** addr, size_t len, int prot, int flags) { + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked())) { - std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, _flexibleUsedLocked(), static_cast(FLEXIBLE_MEMORY_SIZE)); + const size_t used = images + _flexibleUsedLocked(); + if (len > FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, used)) { + std::fprintf(stderr, "[memory] flexible memory exhausted: request 0x%zx with 0x%zx of 0x%zx in use\n", len, used, static_cast(FLEXIBLE_MEMORY_SIZE)); return SCE_KERNEL_ERROR_ENOMEM; } const int result = DoMapAnon(addr, len, prot, flags); @@ -350,8 +354,9 @@ int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { int APS5_VABI sceKernelAvailableFlexibleMemorySize(size_t* size) { if (!size) return SCE_KERNEL_ERROR_EINVAL; + const size_t images = GuestImageSegmentBytes_nid_no_patch(); std::lock_guard lock(g_flexibleLock); - *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, _flexibleUsedLocked()); + *size = FLEXIBLE_MEMORY_SIZE - std::min(FLEXIBLE_MEMORY_SIZE, images + _flexibleUsedLocked()); return 0; } diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index 650734b5e..479203763 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -2,9 +2,16 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include +#include #include #include +#include +#if defined(__linux__) +#include +#include +#endif extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI sceKernelMlock_nid_postfix(void* address, std::uint64_t length); @@ -15,9 +22,16 @@ namespace { // FreeBSD/PS5 ABI values, independent of the host's errno and mmap constants. constexpr int GuestInvalid = 22; constexpr int GuestNoMemory = 12; +constexpr int GuestBadDescriptor = 9; +constexpr int GuestAccess = 13; +constexpr int GuestIoError = 5; constexpr int GuestNotSupported = 45; +constexpr int GuestShared = 0x1; constexpr int GuestPrivate = 0x2; constexpr int GuestAnonymous = 0x1000; +constexpr int GuestMapFixed = 0x10; +constexpr int GuestMapNoOverwrite = 0x80; +constexpr int GuestMapNoCoalesce = 0x400000; constexpr int GuestSyncAsync = 0x1; constexpr int GuestSyncInvalidate = 0x2; constexpr int GuestAdviceCore = 9; @@ -26,6 +40,25 @@ constexpr std::uintptr_t GuestUserAddressEnd = 0x800000000000; constexpr int GuestAlignedSuper = 0x1000000; constexpr std::size_t SuperpageSize = std::size_t{1} << 21; +#if defined(__linux__) +class ScopedDescriptor { +public: + explicit ScopedDescriptor(int value) noexcept : descriptor(fcntl(value, F_DUPFD_CLOEXEC, 0)), error(descriptor < 0 ? errno : 0) {} + ~ScopedDescriptor() { + const int savedError = errno; + if (descriptor >= 0) close(descriptor); + errno = savedError; + } + ScopedDescriptor(const ScopedDescriptor&) = delete; + ScopedDescriptor& operator=(const ScopedDescriptor&) = delete; + int Get() const noexcept { return descriptor; } + int Error() const noexcept { return error; } +private: + int descriptor = -1; + int error = 0; +}; +#endif + bool RoundLength(std::size_t length, std::size_t& rounded) { constexpr auto mask = PS5_PAGE_SIZE - 1; if (length == 0 || length > std::numeric_limits::max() - mask) @@ -55,17 +88,60 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti std::size_t rounded; if (!RoundLength(length, rounded) || (protection & ~7) != 0) return failed(GuestInvalid); - if ((flags & ~GuestAlignedSuper) != (GuestPrivate | GuestAnonymous)) + const int mappingFlags = flags & ~GuestAlignedSuper; + if (mappingFlags == (GuestPrivate | GuestAnonymous)) { + if (descriptor != -1 || offset != 0) return failed(GuestInvalid); + void* mapped = nullptr; + try { + const auto result = DoMapAnon(&mapped, rounded, protection, 0, (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE); + if (result != 0) return failed(GuestNoMemory); + return mapped; + } catch (const std::bad_alloc&) { + return failed(GuestNoMemory); + } catch (const std::exception&) { + return failed(GuestNoMemory); + } + } + const int kind = mappingFlags & (GuestShared | GuestPrivate); + int placement = mappingFlags & (GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce); + if ((mappingFlags & GuestAnonymous) != 0 || + (kind != GuestShared && kind != GuestPrivate) || + (mappingFlags & ~(GuestShared | GuestPrivate | GuestMapFixed | GuestMapNoOverwrite | GuestMapNoCoalesce)) != 0) return failed(GuestNotSupported); - if (descriptor != -1 || offset != 0) + if (descriptor < 0) return failed(GuestBadDescriptor); + if (offset < 0 || (static_cast(offset) & (PS5_PAGE_SIZE - 1)) != 0 || + rounded > static_cast(std::numeric_limits::max()) || + static_cast(offset) > static_cast(std::numeric_limits::max()) - rounded) return failed(GuestInvalid); - // A non-fixed address is a hint; the kernel may choose another address. - (void)address; +#if defined(__linux__) + const ScopedDescriptor pinnedDescriptor(descriptor); + if (pinnedDescriptor.Get() < 0) return failed(pinnedDescriptor.Error()); + const int mappingDescriptor = pinnedDescriptor.Get(); + const int descriptorFlags = fcntl(mappingDescriptor, F_GETFL); + if (descriptorFlags < 0) return failed(errno); + const int accessMode = descriptorFlags & O_ACCMODE; + if ((protection & 1) != 0 && accessMode == O_WRONLY) return failed(GuestAccess); + if (kind == GuestShared && (protection & 2) != 0 && accessMode == O_RDONLY) return failed(GuestAccess); + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); +#else + const int mappingDescriptor = descriptor; + if (!File::IsSharedMemoryDescriptor_nid_no_patch(mappingDescriptor)) return failed(GuestNotSupported); +#endif + const std::size_t alignment = (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; void* mapped = nullptr; try { - const auto result = DoMapAnon(&mapped, rounded, protection, 0, (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE); - if (result != 0) return failed(GuestNoMemory); + mapped = address; + const auto result = DoMapFileBacked_nid_no_patch(&mapped, rounded, protection, kind | placement, mappingDescriptor, static_cast(offset), alignment); + if (result != 0) return failed(result == SCE_KERNEL_ERROR_EINVAL ? GuestInvalid : GuestNoMemory); return mapped; + } catch (const std::invalid_argument&) { + return failed(GuestInvalid); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + if (hostError == ENOTSUP || hostError == EOPNOTSUPP) return failed(GuestNotSupported); + if (hostError == EACCES || hostError == EPERM) return failed(GuestAccess); + if (hostError == EBADF) return failed(GuestBadDescriptor); + return failed(hostError > 0 && hostError <= 34 ? hostError : GuestIoError); } catch (const std::bad_alloc&) { return failed(GuestNoMemory); } catch (const std::exception&) { @@ -109,6 +185,9 @@ int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protec return 0; } catch (const std::bad_alloc&) { return failed(GuestNoMemory); + } catch (const std::system_error& error) { + const int hostError = error.code().value(); + return failed(hostError == EACCES || hostError == EPERM ? GuestAccess : GuestInvalid); } catch (const std::exception&) { return failed(GuestInvalid); } diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 9efda2d7c..2031b6c4c 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP #define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#include #include #include @@ -11,6 +12,7 @@ namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); } diff --git a/core/libs/prx/libkernel/File/include/KernelStatfs.hpp b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp new file mode 100644 index 000000000..7ecfe985f --- /dev/null +++ b/core/libs/prx/libkernel/File/include/KernelStatfs.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +struct KernelStatfs { + std::uint32_t f_version; + std::uint32_t f_type; + std::uint64_t f_flags; + std::uint64_t f_bsize; + std::uint64_t f_iosize; + std::uint64_t f_blocks; + std::uint64_t f_bfree; + std::int64_t f_bavail; + std::uint64_t f_files; + std::int64_t f_ffree; + std::uint64_t f_syncwrites; + std::uint64_t f_asyncwrites; + std::uint64_t f_syncreads; + std::uint64_t f_asyncreads; + std::uint64_t f_spare[10]; + std::uint32_t f_namemax; + std::uint32_t f_owner; + std::int32_t f_fsid[2]; + char f_charspare[80]; + char f_fstypename[16]; + char f_mntfromname[88]; + char f_mntonname[88]; +}; +static_assert(sizeof(KernelStatfs) == 472); +static_assert(offsetof(KernelStatfs, f_bavail) == 48); +static_assert(offsetof(KernelStatfs, f_namemax) == 184); +static_assert(offsetof(KernelStatfs, f_fstypename) == 280); +static_assert(offsetof(KernelStatfs, f_mntonname) == 384); diff --git a/core/libs/prx/libkernel/File/include/SharedMemory.hpp b/core/libs/prx/libkernel/File/include/SharedMemory.hpp new file mode 100644 index 000000000..5c9ffc9f4 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/SharedMemory.hpp @@ -0,0 +1,24 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_SHAREDMEMORY_HPP + +#include +#include +#ifndef _WIN32 +#include +#endif + +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor); +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size); +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset); +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence); +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length); +#ifndef _WIN32 +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count); +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset); +#endif + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 06b665940..9bbe6569b 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -7,11 +7,17 @@ #ifdef _WIN32 +#include "prx/libc/include/General.hpp" + +#include #include +#include #include +#include #include #include #include +#include #include #include #include @@ -23,6 +29,7 @@ constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; struct DirectoryState { + std::mutex mutex; std::filesystem::path path; std::vector> entries; std::size_t cursor = 0; @@ -30,7 +37,26 @@ struct DirectoryState { }; std::mutex g_mutex; -std::map g_directories; +std::map> g_directories; +std::atomic g_directoryCount{0}; + +std::shared_ptr Find(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + return found == g_directories.end() ? nullptr : found->second; +} + +void Load(DirectoryState& state) { + if (state.loaded) return; + state.loaded = true; + state.entries.emplace_back(".", GuestDirectoryType); + state.entries.emplace_back("..", GuestDirectoryType); + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } +} } @@ -40,37 +66,63 @@ int OpenDirectoryDescriptor(const std::filesystem::path& path) { const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); if (fd < 0) return -1; std::lock_guard lock(g_mutex); - g_directories[fd] = DirectoryState{path}; + const auto state = std::make_shared(); + state->path = path; + g_directories[fd] = state; + g_directoryCount = g_directories.size(); return fd; } std::optional DirectoryDescriptorPath(int fd) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - return found->second.path; + const auto state = Find(fd); + if (!state) return std::nullopt; + return state->path; } void ForgetDirectoryDescriptor(int fd) { + if (g_directoryCount == 0) return; std::lock_guard lock(g_mutex); g_directories.erase(fd); + g_directoryCount = g_directories.size(); +} + +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { + constexpr int SeekSet = 0; + constexpr int SeekCurrent = 1; + if (g_directoryCount == 0) return std::nullopt; + const auto found = Find(fd); + if (!found) return std::nullopt; + auto& state = *found; + std::lock_guard lock(state.mutex); + if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); + const auto cursor = static_cast(state.cursor); + if (whence == SeekCurrent && offset == 0) return cursor; + if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { + errno = EOVERFLOW; + return std::int64_t{-1}; + } + const std::int64_t target = whence == SeekSet ? offset : cursor + offset; + if (target < 0) { + errno = EINVAL; + return std::int64_t{-1}; + } + if (target == 0) { + state.entries.clear(); + state.loaded = false; + state.cursor = 0; + return std::int64_t{0}; + } + Load(state); + state.cursor = static_cast(target); + return target; } int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; - auto& state = found->second; - if (!state.loaded) { - state.loaded = true; - state.entries.emplace_back(".", GuestDirectoryType); - state.entries.emplace_back("..", GuestDirectoryType); - std::error_code error; - for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); - } - } + const auto found = Find(fd); + if (!found) return SCE_KERNEL_ERROR_ENOTDIR; + auto& state = *found; + std::lock_guard lock(state.mutex); + Load(state); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ca7cbe5f1..bfcf004c5 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -1,8 +1,10 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -20,11 +22,59 @@ #include #include #include +#include +static int OpenError(DWORD error) { + switch (error) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + case ERROR_INVALID_DRIVE: + case ERROR_BAD_NETPATH: + case ERROR_BAD_NET_NAME: + case ERROR_BAD_PATHNAME: + case ERROR_FILENAME_EXCED_RANGE: + return ENOENT; + case ERROR_ACCESS_DENIED: + case ERROR_SHARING_VIOLATION: + case ERROR_LOCK_VIOLATION: + case ERROR_WRITE_PROTECT: + case ERROR_NETWORK_ACCESS_DENIED: + case ERROR_CANNOT_MAKE: + case ERROR_DRIVE_LOCKED: + return EACCES; + case ERROR_FILE_EXISTS: + case ERROR_ALREADY_EXISTS: + return EEXIST; + case ERROR_TOO_MANY_OPEN_FILES: + return EMFILE; + case ERROR_NOT_ENOUGH_MEMORY: + case ERROR_OUTOFMEMORY: + return ENOMEM; + case ERROR_DISK_FULL: + return ENOSPC; + default: + return EINVAL; + } +} static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint16_t mode) { - return ::_wopen(p.wstring().c_str(), nativeFlags, static_cast(mode)); + const int access = nativeFlags & (_O_RDONLY | _O_WRONLY | _O_RDWR); + const DWORD desired = access == _O_RDONLY ? GENERIC_READ : access == _O_WRONLY ? GENERIC_WRITE : GENERIC_READ | GENERIC_WRITE; + DWORD disposition = OPEN_EXISTING; + if (nativeFlags & _O_CREAT) disposition = (nativeFlags & _O_EXCL) ? CREATE_NEW : (nativeFlags & _O_TRUNC) ? CREATE_ALWAYS : OPEN_ALWAYS; + else if (nativeFlags & _O_TRUNC) disposition = TRUNCATE_EXISTING; + const DWORD attributes = (nativeFlags & _O_CREAT) && !(mode & _S_IWRITE) ? FILE_ATTRIBUTE_READONLY : FILE_ATTRIBUTE_NORMAL; + SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE}; + const auto handle = CreateFileW(p.c_str(), desired, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, &security, disposition, attributes, nullptr); + if (handle == INVALID_HANDLE_VALUE) { + errno = OpenError(GetLastError()); + return -1; + } + const int descriptor = ::_open_osfhandle(reinterpret_cast(handle), nativeFlags & (_O_APPEND | _O_RDONLY | _O_BINARY)); + if (descriptor < 0) CloseHandle(handle); + return descriptor; } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::_lseeki64(fd, offset, whence); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + return result == -2 ? ::_lseeki64(fd, offset, whence) : result; } static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { @@ -36,7 +86,8 @@ static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - return ::_write(fd, buf, static_cast(n)); + const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + return result == -2 ? ::_write(fd, buf, static_cast(n)) : static_cast(result); } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} @@ -78,13 +129,15 @@ static int NativeOpen(const std::filesystem::path& p, int nativeFlags, std::uint return ::open(p.c_str(), nativeFlags, static_cast(mode)); } static std::int64_t NativeLseek(int fd, std::int64_t offset, int whence) { - return ::lseek(fd, static_cast(offset), whence); + const auto result = File::SeekSharedMemory_nid_no_patch(fd, offset, whence); + return result == -2 ? ::lseek(fd, static_cast(offset), whence) : result; } static std::int64_t NativeRead(int fd, void* buf, std::size_t n) { return ::read(fd, buf, n); } static std::int64_t NativeWrite(int fd, const void* buf, std::size_t n) { - return ::write(fd, buf, n); + const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + return result == -2 ? ::write(fd, buf, n) : result; } static int NativeClose(int fd) { return ::close(fd); } static int NativeUnlink(const std::filesystem::path& p) { @@ -142,7 +195,8 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { const int fd = File::OpenRandomDevice(); return fd < 0 ? SceErrorFromErrno(errno) : fd; } - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); #ifdef _WIN32 if ((flags & (SCE_KERNEL_O_DIRECTORY | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_DIRECTORY) { std::error_code error; @@ -215,6 +269,12 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } +#ifdef _WIN32 + if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { + if (*directory < 0) return SceIoErrorFromErrno(errno); + return *directory; + } +#endif std::int64_t result = NativeLseek(d, offset, whence); if (result < 0) return SceIoErrorFromErrno(errno); return result; @@ -229,11 +289,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; - try { - native = ResolvePath_nid_no_patch(path); - } catch (const std::filesystem::filesystem_error&) { - return SceErrorFromErrno(2); - } + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; const auto status = std::filesystem::status(native, error); @@ -252,7 +308,8 @@ int APS5_VABI sceKernelUnlink(const char* path) { throw std::invalid_argument(std::string(__func__) + ": path is null"); } if (*path == '\0') return SceErrorFromErrno(2); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); diff --git a/core/libs/prx/libkernel/File/src/SharedMemory.cpp b/core/libs/prx/libkernel/File/src/SharedMemory.cpp index 94bb3ee39..8202db42e 100644 --- a/core/libs/prx/libkernel/File/src/SharedMemory.cpp +++ b/core/libs/prx/libkernel/File/src/SharedMemory.cpp @@ -1,26 +1,85 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libc/include/CreationMask.hpp" +#include "prx/libkernel/File/include/File.hpp" #include +#include +#include +#include #include #include +#include +#include +#include +#include #ifdef _WIN32 #include #include #include #include -#include #include #include #else +#include "nid/Sha1.hpp" +#include #include +#include +#include +#include +#include +#include #endif extern "C" int* APS5_VABI __error_nid_postfix(); namespace { +bool IsHostSharedMemoryDescriptor(int descriptor) { +#ifdef __linux__ + char descriptorPath[64]; + std::snprintf(descriptorPath, sizeof(descriptorPath), "/proc/self/fd/%d", descriptor); + char objectPath[4096]; + const auto length = ::readlink(descriptorPath, objectPath, sizeof(objectPath) - 1); + if (length < 0) return false; + objectPath[length] = '\0'; + return std::string_view(objectPath).starts_with("/dev/shm/anyps5-shm-") || + std::string_view(objectPath).starts_with("/memfd:anyps5 shm_open"); +#else + (void)descriptor; + return false; +#endif +} + +class HostShmLock { +public: + HostShmLock() { +#ifdef __linux__ + descriptor = ::open("/dev/shm", O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (descriptor < 0 || ::flock(descriptor, LOCK_EX) != 0) error = errno; +#endif + } + + ~HostShmLock() { +#ifdef __linux__ + const int savedError = errno; + if (descriptor >= 0) { + if (!error) ::flock(descriptor, LOCK_UN); + ::close(descriptor); + } + errno = savedError; +#endif + } + + int Error() const { return error; } + +private: + int descriptor = -1; + int error = 0; +}; + constexpr int GuestNoMemory = 12; constexpr int GuestInvalid = 22; constexpr int GuestTooManyFilesInSystem = 23; @@ -48,6 +107,88 @@ int GuestError(int error) { } } +#ifndef _WIN32 +int NamedGuestError(int error) { + switch (error) { + case ENOENT: + return 2; + case EINTR: + return 4; + case EACCES: + return 13; + case EFAULT: + return 14; + case EEXIST: + return 17; + case EINVAL: + return GuestInvalid; + case ENOSPC: + return 28; + case EROFS: + return 30; + case ENAMETOOLONG: + return 63; + default: + return GuestError(error); + } +} + +std::string NamedHostPath(const char* path, int& error) { + if (path == nullptr) { error = EFAULT; return {}; } + std::size_t length = 0; + while (length < 1024 && path[length] != '\0') ++length; + if (length == 1024) { error = ENAMETOOLONG; return {}; } + if (length == 0 || path[0] != '/') { error = EINVAL; return {}; } + const auto digest = Nid::Sha1(std::vector(path, path + length)); + std::string hostName = "/anyps5-shm-"; + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + error = 0; + return hostName; +} + +int OpenNamedFile(const char* path, int flags, int mode) { + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_open: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } + int hostFlags = (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR ? O_RDWR : O_RDONLY; + if (flags & SCE_KERNEL_O_CREAT) hostFlags |= O_CREAT; + if (flags & SCE_KERNEL_O_EXCL) hostFlags |= O_EXCL; + if ((flags & SCE_KERNEL_O_TRUNC) && (flags & SCE_KERNEL_O_ACCMODE) == SCE_KERNEL_O_RDWR) hostFlags |= O_TRUNC; + const auto permissions = (mode & 0777) & ~File::GetCreationMask_nid_no_patch(); + bool created = false; + int descriptor; + if (flags & SCE_KERNEL_O_CREAT) { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC | O_EXCL, permissions); + created = descriptor >= 0; + if (descriptor < 0 && errno == EEXIST && !(flags & SCE_KERNEL_O_EXCL)) { + descriptor = ::shm_open(hostName.c_str(), (hostFlags & ~(O_CREAT | O_EXCL)) | O_CLOEXEC, 0); + } + } else { + descriptor = ::shm_open(hostName.c_str(), hostFlags | O_CLOEXEC, 0); + } + if (descriptor >= 0) { + if (!created || ::fchmod(descriptor, permissions) == 0) return descriptor; + const int error = errno; + ::close(descriptor); + ::shm_unlink(hostName.c_str()); + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: setting guest creation permissions failed, errno=" + std::to_string(error)); + } + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_open: opening the host named object failed, errno=" + std::to_string(error)); +} +#endif + int CreateAnonymousFile() { #ifdef _WIN32 static std::atomic sequence{0}; @@ -68,17 +209,258 @@ int CreateAnonymousFile() { errno = EEXIST; return -1; #else - return memfd_create("anyps5 shm_open", MFD_CLOEXEC); + const int descriptor = memfd_create("anyps5 shm_open", MFD_CLOEXEC); + return descriptor; #endif } } -extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int) { +namespace File { + +bool IsSharedMemoryDescriptor_nid_no_patch(int descriptor) { + return IsHostSharedMemoryDescriptor(descriptor); +} + +namespace { + +class SharedMemoryLock { +public: + explicit SharedMemoryLock(int value) { +#ifdef __linux__ + struct stat originalStatus{}; + if (::fstat(value, &originalStatus) != 0) { shared = true; error = errno; return; } +#endif + if (!IsHostSharedMemoryDescriptor(value)) { +#ifdef __linux__ + struct stat currentStatus{}; + if (::fstat(value, ¤tStatus) != 0) { shared = true; error = errno; return; } + if (currentStatus.st_dev != originalStatus.st_dev || currentStatus.st_ino != originalStatus.st_ino) { + shared = true; + error = EBADF; + } +#endif + return; + } + shared = true; + hostLock.emplace(); + if (hostLock->Error() != 0) { error = hostLock->Error(); return; } + descriptor = Duplicate(value); + if (descriptor < 0) { error = errno; return; } + if (!IsHostSharedMemoryDescriptor(descriptor)) { error = EBADF; return; } + if (!Refresh()) { error = errno; return; } +#ifdef __linux__ + if (status.st_dev != originalStatus.st_dev || status.st_ino != originalStatus.st_ino) error = EBADF; +#endif + } + + ~SharedMemoryLock() { + if (descriptor >= 0) { + const int savedError = errno; +#ifdef _WIN32 + ::_close(descriptor); +#else + ::close(descriptor); +#endif + errno = savedError; + } + } + + bool IsShared() const { return shared; } + int Error() const { return error; } + int Descriptor() const { return descriptor; } + bool Refresh() { +#ifdef _WIN32 + struct _stat64 nativeStatus{}; + if (::_fstat64(descriptor, &nativeStatus) != 0) return false; + status.st_size = nativeStatus.st_size; + return true; +#else + return ::fstat(descriptor, &status) == 0; +#endif + } + struct stat status{}; + +private: + static int Duplicate(int value) { +#ifdef _WIN32 + return ::_dup(value); +#else + return ::fcntl(value, F_DUPFD_CLOEXEC, 0); +#endif + } + + int descriptor = -1; + std::optional hostLock; + bool shared = false; + int error = 0; +}; +} + +std::int64_t WriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; +#ifdef _WIN32 + const auto position = ::_lseeki64(fd, 0, SEEK_CUR); +#else + const auto position = ::lseek(fd, 0, SEEK_CUR); +#endif + if (position < 0) return -1; + if (position >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - position)); +#ifdef _WIN32 + const auto result = ::_write(fd, buffer, static_cast(std::min(count, std::numeric_limits::max()))); +#else + const auto result = ::write(fd, buffer, static_cast(count)); +#endif + return static_cast(result); +} + +std::int64_t PwriteSharedMemory_nid_no_patch(int descriptor, const void* buffer, std::size_t size, std::int64_t offset) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); +#ifndef _WIN32 + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } +#endif + const auto& status = guard.status; + if (offset < 0) { errno = EINVAL; return -1; } + if (offset >= status.st_size) return 0; + const auto count = std::min(size, static_cast(status.st_size - offset)); +#ifdef _WIN32 + const auto result = NativePositioned_nid_no_patch(fd, const_cast(buffer), static_cast(count), offset, true); +#else + const auto result = ::pwrite(fd, buffer, static_cast(count), static_cast(offset)); +#endif + return static_cast(result); +} + +std::int64_t SeekSharedMemory_nid_no_patch(int descriptor, std::int64_t offset, int whence) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + if (!guard.Refresh()) return -1; + const auto fd = guard.Descriptor(); + const auto& status = guard.status; + std::int64_t base = 0; + if (whence == SEEK_CUR) { +#ifdef _WIN32 + base = ::_lseeki64(fd, 0, SEEK_CUR); +#else + base = ::lseek(fd, 0, SEEK_CUR); +#endif + if (base < 0) return -1; + } else if (whence == SEEK_END) { + base = static_cast(status.st_size); + } else if (whence != SEEK_SET) { + errno = EINVAL; + return -1; + } + if ((offset > 0 && base > std::numeric_limits::max() - offset) || + (offset < 0 && base < std::numeric_limits::min() - offset)) { + errno = EOVERFLOW; + return -1; + } + const auto target = base + offset; + if (target < 0 || target > status.st_size) { + errno = EINVAL; + return -1; + } +#ifdef _WIN32 + return ::_lseeki64(fd, target, SEEK_SET); +#else + return ::lseek(fd, static_cast(target), SEEK_SET); +#endif +} + +#ifndef _WIN32 +static std::int64_t TransferSharedMemoryVectors(int descriptor, const struct iovec* vectors, int count, + std::int64_t offset, bool positioned) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); + const int access = ::fcntl(fd, F_GETFL); + if (access < 0) return -1; + if ((access & O_ACCMODE) == O_RDONLY) { errno = EBADF; return -1; } + if (positioned && offset < 0) { errno = EINVAL; return -1; } + struct stat status{}; + if (::fstat(fd, &status) != 0) return -1; + std::uint64_t total = 0; + constexpr auto maxTransfer = static_cast(std::numeric_limits::max()); + for (int i = 0; i < count; ++i) { + if (vectors[i].iov_len > maxTransfer - total) { errno = EINVAL; return -1; } + total += vectors[i].iov_len; + } + std::uint64_t available = 0; + if (positioned) { + if (offset < status.st_size) available = static_cast(status.st_size - offset); + if (total > static_cast(std::numeric_limits::max() - offset)) { errno = EINVAL; return -1; } + } else { + const auto position = ::lseek(fd, 0, SEEK_CUR); + if (position < 0) return -1; + if (position < status.st_size) available = static_cast(status.st_size - position); + } + std::vector bounded; + bounded.reserve(static_cast(count)); + for (int i = 0; i < count && available > 0; ++i) { + const auto length = static_cast(std::min(vectors[i].iov_len, available)); + if (length != 0) bounded.push_back({vectors[i].iov_base, length}); + available -= length; + } + if (bounded.empty()) return 0; + return positioned ? ::pwritev(fd, bounded.data(), static_cast(bounded.size()), static_cast(offset)) + : ::writev(fd, bounded.data(), static_cast(bounded.size())); +} + +std::int64_t WritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count) { + return TransferSharedMemoryVectors(descriptor, vectors, count, 0, false); +} + +std::int64_t PwritevSharedMemory_nid_no_patch(int descriptor, const struct iovec* vectors, int count, std::int64_t offset) { + return TransferSharedMemoryVectors(descriptor, vectors, count, offset, true); +} +#endif + +int TruncateSharedMemory_nid_no_patch(int descriptor, std::int64_t length) { + SharedMemoryLock guard(descriptor); + if (!guard.IsShared()) return -2; + if (guard.Error() != 0) { errno = guard.Error(); return -1; } + const auto fd = guard.Descriptor(); +#ifdef _WIN32 + const int result = static_cast(::_chsize_s(fd, length)); +#else + const int result = ::ftruncate(fd, static_cast(length)); +#endif + return result; +} + +} + +extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int mode) { const int accessMode = flags & SCE_KERNEL_O_ACCMODE; if (accessMode != SCE_KERNEL_O_RDONLY && accessMode != SCE_KERNEL_O_RDWR) return Fail(GuestInvalid); if ((flags & ~AcceptedFlags) != 0) return Fail(GuestInvalid); - if (path != anonymousObject) NotImplemented_nid_no_patch("shm_open of a named object"); + if (path != anonymousObject) { +#ifdef _WIN32 + NotImplemented_nid_no_patch("shm_open of a named object"); +#else + return OpenNamedFile(path, flags, mode); +#endif + } if (accessMode == SCE_KERNEL_O_RDONLY) return Fail(GuestInvalid); const int descriptor = CreateAnonymousFile(); if (descriptor >= 0) return descriptor; @@ -86,3 +468,30 @@ extern "C" int APS5_VABI shm_open_nid_postfix(const char* path, int flags, int) if (const int guest = GuestError(error)) return Fail(guest); throw std::runtime_error("shm_open: creating the host anonymous file failed, errno=" + std::to_string(error)); } + +extern "C" int APS5_VABI shm_unlink_nid_postfix(const char* path) { +#ifdef _WIN32 + (void)path; + NotImplemented_nid_no_patch("shm_unlink of a named object"); +#else + int validationError = 0; + const auto hostName = NamedHostPath(path, validationError); + if (validationError != 0) return Fail(NamedGuestError(validationError) ? NamedGuestError(validationError) : GuestInvalid); + HostShmLock namespaceLock; + if (namespaceLock.Error() != 0) { + errno = namespaceLock.Error(); + if (const int guest = NamedGuestError(errno)) return Fail(guest); + throw std::runtime_error("shm_unlink: locking the host shared-memory namespace failed, errno=" + std::to_string(errno)); + } + const auto objectPath = "/dev/shm" + hostName; + if (::faccessat(AT_FDCWD, objectPath.c_str(), R_OK | W_OK, AT_EACCESS) != 0) { + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: checking shared-memory permissions failed, errno=" + std::to_string(error)); + } + if (::shm_unlink(hostName.c_str()) == 0) return 0; + const int error = errno; + if (const int guest = NamedGuestError(error)) return Fail(guest); + throw std::runtime_error("shm_unlink: removing the host named object failed, errno=" + std::to_string(error)); +#endif +} diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 255a3d9fe..fbe87450b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -4,10 +4,13 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -31,36 +34,6 @@ static_assert(sizeof(KernelIovec) == sizeof(GuestSockets::Iovec)); static constexpr int KERNEL_IOV_MAX = 1024; -struct KernelStatfs { - std::uint32_t f_version; - std::uint32_t f_type; - std::uint64_t f_flags; - std::uint64_t f_bsize; - std::uint64_t f_iosize; - std::uint64_t f_blocks; - std::uint64_t f_bfree; - std::int64_t f_bavail; - std::uint64_t f_files; - std::int64_t f_ffree; - std::uint64_t f_syncwrites; - std::uint64_t f_asyncwrites; - std::uint64_t f_syncreads; - std::uint64_t f_asyncreads; - std::uint64_t f_spare[10]; - std::uint32_t f_namemax; - std::uint32_t f_owner; - std::int32_t f_fsid[2]; - char f_charspare[80]; - char f_fstypename[16]; - char f_mntfromname[88]; - char f_mntonname[88]; -}; -static_assert(sizeof(KernelStatfs) == 472); -static_assert(offsetof(KernelStatfs, f_bavail) == 48); -static_assert(offsetof(KernelStatfs, f_namemax) == 184); -static_assert(offsetof(KernelStatfs, f_fstypename) == 280); -static_assert(offsetof(KernelStatfs, f_mntonname) == 384); - static constexpr std::uint32_t KERNEL_STATFS_VERSION = 0x20030518; static constexpr std::uint64_t KERNEL_MNT_RDONLY = 0x1; static constexpr std::uint64_t KERNEL_MNT_NOEXEC = 0x4; @@ -118,7 +91,8 @@ static int NativeFchmod(int descriptor, int mode) { return path ? NativeChmod(*path, mode) : -1; } static int NativeFtruncate(int descriptor, std::int64_t length) { - return static_cast(::_chsize_s(descriptor, length)); + const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); + return result == -2 ? static_cast(::_chsize_s(descriptor, length)) : result; } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { const int descriptor = ::_wopen(path.c_str(), _O_WRONLY | _O_BINARY); @@ -161,6 +135,24 @@ static int NativeFutimes(int descriptor, const KernelTimeval* times) { } return result; } +static int NativeSync(int descriptor) { + constexpr DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + HANDLE handle = INVALID_HANDLE_VALUE; + if (const auto directory = File::DirectoryDescriptorPath(descriptor)) { + handle = CreateFileW(directory->c_str(), GENERIC_WRITE, share, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + } else { + const int previous = errno; + if (::_commit(descriptor) == 0) return 0; + const auto original = reinterpret_cast(::_get_osfhandle(descriptor)); + if (original == INVALID_HANDLE_VALUE || GetFileType(original) != FILE_TYPE_DISK) { errno = EBADF; return -1; } + handle = ReOpenFile(original, GENERIC_WRITE, share, 0); + errno = previous; + } + if (handle == INVALID_HANDLE_VALUE) return GetLastError() == ERROR_ACCESS_DENIED ? 0 : File::WindowsFileTime::Failure(GetLastError()); + const int result = FlushFileBuffers(handle) ? 0 : File::WindowsFileTime::Failure(GetLastError()); + CloseHandle(handle); + return result; +} static int NativeFlock(int descriptor, int operation) { return File::Flock(descriptor, operation); } @@ -168,6 +160,10 @@ std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_ if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativePositioned: nbytes exceeds platform limit"); } + if (write) { + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + if (result != -2) return result; + } const auto handle = reinterpret_cast(::_get_osfhandle(descriptor)); if (handle == INVALID_HANDLE_VALUE) { errno = EBADF; @@ -199,6 +195,10 @@ static std::int64_t NativeTransfer(int descriptor, void* buf, std::size_t nbytes if (nbytes > static_cast(std::numeric_limits::max())) { throw std::runtime_error("NativeTransfer: nbytes exceeds platform limit"); } + if (write) { + const auto result = File::WriteSharedMemory_nid_no_patch(descriptor, buf, nbytes); + if (result != -2) return result; + } return write ? ::_write(descriptor, buf, static_cast(nbytes)) : ::_read(descriptor, buf, static_cast(nbytes)); } static bool NativeIsDisk(int descriptor) { @@ -236,7 +236,8 @@ static int NativeFchmod(int descriptor, int mode) { return ::fchmod(descriptor, static_cast(mode)); } static int NativeFtruncate(int descriptor, std::int64_t length) { - return ::ftruncate(descriptor, static_cast(length)); + const int result = File::TruncateSharedMemory_nid_no_patch(descriptor, length); + return result == -2 ? ::ftruncate(descriptor, static_cast(length)) : result; } static int NativeTruncate(const std::filesystem::path& path, std::int64_t length) { return ::truncate(path.c_str(), static_cast(length)) == 0 ? 0 : errno; @@ -260,7 +261,8 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s return static_cast(::pread(descriptor, buf, nbytes, static_cast(offset))); } static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { - return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); + const auto result = File::PwriteSharedMemory_nid_no_patch(descriptor, buf, nbytes, offset); + return result == -2 ? static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))) : result; } static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); @@ -282,6 +284,10 @@ static constexpr int GUEST_ENOTDIR = 20; static constexpr int GUEST_EISDIR = 21; static constexpr int GUEST_EWOULDBLOCK = 35; static constexpr int GUEST_ENOTEMPTY = 66; +static constexpr int GUEST_EDEADLK = 11; +static constexpr int GUEST_ELOOP = 62; +static constexpr int GUEST_EDQUOT = 69; +static constexpr int GUEST_EOVERFLOW = 84; static int SceErrorFromErrno(int error); @@ -297,7 +303,23 @@ static bool FailPosixPositionedGuestSocket(int descriptor) { } static int SceErrorFromErrno(int error) { - return static_cast(0x80020000u | static_cast(error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO)); + int guest = error > 0 && error <= 34 ? error : error == GUEST_ENOTEMPTY ? error : GUEST_EIO; + if (error == EAGAIN) guest = GUEST_EWOULDBLOCK; +#ifdef EDEADLK + else if (error == EDEADLK) guest = GUEST_EDEADLK; +#endif + else if (error == ENAMETOOLONG) guest = GUEST_ENAMETOOLONG; + else if (error == ENOTEMPTY) guest = GUEST_ENOTEMPTY; +#ifdef ELOOP + else if (error == ELOOP) guest = GUEST_ELOOP; +#endif +#ifdef EDQUOT + else if (error == EDQUOT) guest = GUEST_EDQUOT; +#endif +#ifdef EOVERFLOW + else if (error == EOVERFLOW) guest = GUEST_EOVERFLOW; +#endif + return static_cast(0x80020000u | static_cast(guest)); } extern "C" int* APS5_VABI __error_nid_postfix(); @@ -436,7 +458,8 @@ extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { if (const int error = PathError(path)) return PosixFailure(error); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeChmod(native, mode & 07777) != 0) return PosixResult(SceErrorFromErrno(errno)); return 0; } @@ -575,7 +598,8 @@ int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { int APS5_VABI mkdir_nid_postfix(const char* path, uint16_t mode) { if (const int error = PathError(path)) return PosixFailure(error); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); if (NativeMkdir(native, mode) != 0) { const int nativeError = errno; const bool blockedByFile = (nativeError == ENOENT || nativeError == EINVAL) && HasNonDirectoryPrefix(native); @@ -631,18 +655,12 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off } int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int d, const void* buf, size_t nbytes, int64_t offset) { - if (buf == nullptr) { - APS5_INVALID_ARG_EX; - } - if (offset < 0) { - APS5_INVALID_ARG_EX; - } + if (buf == nullptr && nbytes != 0) return PosixFailure(GUEST_EFAULT); + if (offset < 0) return PosixFailure(GUEST_EINVAL); if (FailPosixPositionedGuestSocket(d)) return -1; - auto n = NativePwrite(d, buf, nbytes, offset); - if (n < 0) { - throw std::runtime_error(std::string(__func__) + ": pwrite failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } - return n; + char empty = 0; + const auto result = NativePwrite(d, buf == nullptr ? &empty : buf, nbytes, offset); + return result < 0 ? PosixFailure(SceErrorFromErrno(errno) & 0xffff) : result; } int64_t APS5_VABI read_nid_postfix(int d, void* buf, uint64_t nbytes) { @@ -685,7 +703,9 @@ int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb) { int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb) { if (sb == nullptr) return PosixFailure(GUEST_EFAULT); if (const int error = PathError(path)) return PosixFailure(error); - if (!File::FillLinkStat(ResolvePath_nid_no_patch(path), sb)) return PosixResult(SceErrorFromErrno(errno)); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return PosixFailure(error); + if (!File::FillLinkStat(native, sb)) return PosixResult(SceErrorFromErrno(errno)); return 0; } @@ -698,8 +718,10 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (path == nullptr) return SCE_KERNEL_ERROR_EINVAL; if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; - return std::filesystem::exists(ResolvePath_nid_no_patch(path), error) ? 0 : SCE_KERNEL_ERROR_ENOENT; + return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelFstat(int d, FileStat* sb) { @@ -709,17 +731,19 @@ int APS5_VABI sceKernelFstat(int d, FileStat* sb) { } int APS5_VABI sceKernelFsync(int fd) { + if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - return ::_commit(fd); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else - return ::fsync(fd); + if (::fsync(fd) != 0) return SceErrorFromErrno(errno); #endif + return 0; } int APS5_VABI sceKernelFdatasync(int fd) { if (fd >= GuestSockets::FirstDescriptor) return SceErrorFromErrno(GuestSockets::IsOpen(fd) ? GUEST_EINVAL : GUEST_EBADF); #ifdef _WIN32 - if (::_commit(fd) != 0) return SceErrorFromErrno(errno); + if (NativeSync(fd) != 0) return SceErrorFromErrno(errno); #else if (::fdatasync(fd) != 0) return SceErrorFromErrno(errno); #endif @@ -808,7 +832,8 @@ int APS5_VABI sceKernelMkdir(const char* path, uint16_t mode) { (void)mode; if (path == nullptr) throw std::invalid_argument("sceKernelMkdir: path is null"); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); - auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); while (!native.has_filename() && native.has_relative_path()) native = native.parent_path(); std::error_code error; if (std::filesystem::exists(native, error)) return SceErrorFromErrno(GUEST_EEXIST); @@ -963,6 +988,8 @@ int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { const auto result = GuestSockets::Writev(d, reinterpret_cast(iov), iovcnt); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } + const auto sharedResult = File::WritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt); + if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -982,6 +1009,8 @@ int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, in if (const int error = CheckIovecs(iov, iovcnt)) return error; if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); if (const auto error = PositionedGuestSocketError(d); error != 0) return error; + const auto sharedResult = File::PwritevSharedMemory_nid_no_patch(d, NativeIovecs(iov), iovcnt, offset); + if (sharedResult != -2) return sharedResult < 0 ? SceErrorFromErrno(errno) : sharedResult; const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); return result < 0 ? SceErrorFromErrno(errno) : result; } @@ -1001,10 +1030,12 @@ int64_t APS5_VABI pwritev_nid_postfix(int d, const KernelIovec* iov, int iovcnt, int APS5_VABI sceKernelRename(const char* from, const char* to) { if (from == nullptr || to == nullptr) throw std::invalid_argument("sceKernelRename: path is null"); if (*from == '\0' || *to == '\0') return SceErrorFromErrno(GUEST_ENOENT); - const auto source = ResolvePath_nid_no_patch(from); + std::filesystem::path source; + if (const int error = ResolvePathError_nid_no_patch(from, source)) return SceKernelError(error); std::error_code error; if (!std::filesystem::exists(source, error)) return SceErrorFromErrno(GUEST_ENOENT); - const auto destination = ResolvePath_nid_no_patch(to); + std::filesystem::path destination; + if (const int result = ResolvePathError_nid_no_patch(to, destination)) return SceKernelError(result); std::filesystem::rename(source, destination, error); if (error) return SceErrorFromErrno(GUEST_EIO); RecordWrittenPath_nid_no_patch(source); @@ -1015,7 +1046,8 @@ int APS5_VABI sceKernelRename(const char* from, const char* to) { int APS5_VABI sceKernelRmdir(const char* path) { if (path == nullptr) throw std::invalid_argument("sceKernelRmdir: path is null"); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; if (!std::filesystem::is_directory(native, error)) { const bool blockedByFile = HasNonDirectoryPrefix(native) || std::filesystem::exists(WithoutTrailingSeparator(native), error); @@ -1040,7 +1072,8 @@ extern "C" { int APS5_VABI sceKernelChmod_nid_postfix(const char* path, std::uint16_t mode) { if (!path) return SceErrorFromErrno(GUEST_EFAULT); if (!*path) return SceErrorFromErrno(GUEST_ENOENT); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeChmod(native, mode & 07777) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1059,7 +1092,8 @@ int APS5_VABI sceKernelTruncate_nid_postfix(const char* path, std::int64_t lengt if (path == nullptr) throw std::invalid_argument("sceKernelTruncate: path is null"); if (*path == '\0') return SceErrorFromErrno(GUEST_ENOENT); if (length < 0) return SceErrorFromErrno(GUEST_EINVAL); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); std::error_code error; const auto status = std::filesystem::status(native, error); if (!std::filesystem::exists(status)) return SceErrorFromErrno(GUEST_ENOENT); @@ -1082,7 +1116,8 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* if (times[i].tv_usec < 0 || times[i].tv_usec >= 1000000) return SCE_KERNEL_ERROR_EINVAL; } } - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); if (NativeUtimes(native, times) != 0) return SceErrorFromErrno(errno); return 0; } @@ -1108,8 +1143,7 @@ int APS5_VABI futimes_nid_postfix(int d, const KernelTimeval* times) { } int APS5_VABI fsync_nid_postfix(int fd) { - if (sceKernelFsync(fd) != 0) return PosixFailure(errno); - return 0; + return PosixResult(sceKernelFsync(fd)); } int APS5_VABI fdatasync_nid_postfix(int fd) { diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp index 66dc83653..90873a76b 100644 --- a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -10,6 +10,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Module/EhFrame.hpp" #ifdef _WIN32 @@ -22,7 +23,9 @@ #else #include #include +#include #include +#include #endif extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native); @@ -63,6 +66,11 @@ struct GuestImage { std::uintptr_t address; }; +std::uint64_t PageSpan(std::uint64_t address, std::uint64_t size) { + constexpr std::uint64_t pageMask = PS5_PAGE_SIZE - 1; + return ((address + size + pageMask) & ~pageMask) - (address & ~pageMask); +} + #ifndef _WIN32 template struct ImageSearch { @@ -81,6 +89,24 @@ bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t siz return false; } +struct FrameTableCache { + std::mutex mutex; + unsigned long long unloads = 0; + std::unordered_map tables; +}; + +EhFrame::Tables FrameTables(const dl_phdr_info& image, std::uintptr_t header) { + static auto& cache = *new FrameTableCache(); + std::lock_guard lock(cache.mutex); + if (cache.unloads != image.dlpi_subs) { + cache.tables.clear(); + cache.unloads = image.dlpi_subs; + } + if (const auto found = cache.tables.find(header); found != cache.tables.end()) return found->second; + const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; + return cache.tables.emplace(header, EhFrame::ReadTables(contains, header, Caller)).first->second; +} + std::string ImageName(const dl_phdr_info& image) { std::string path = image.dlpi_name ? image.dlpi_name : ""; if (path.empty()) { @@ -111,8 +137,7 @@ void Fill(const dl_phdr_info& image, TInfo& info) { info.tls_size = ToU32(header.p_memsz, "TLS size"); info.tls_align = ToU32(header.p_align, "TLS alignment"); } else if (header.p_type == PT_GNU_EH_FRAME) { - const auto contains = [&](std::uintptr_t begin, std::uint64_t size) { return Contains(image, begin, size); }; - const auto tables = EhFrame::ReadTables(contains, address, Caller); + const auto tables = FrameTables(image, address); info.eh_frame_hdr_addr = address; info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); info.eh_frame_addr = tables.frames; @@ -162,6 +187,24 @@ std::vector GuestImages() { } return images; } + +int AddGuestSegmentBytes(dl_phdr_info* image, std::size_t, void* data) { + const std::string_view name = image->dlpi_name ? image->dlpi_name : ""; + if (!name.empty() && !name.ends_with(GuestModuleSuffix)) return 0; + constexpr auto allAccess = PF_R | PF_W | PF_X; + for (std::uint16_t i = 0; i < image->dlpi_phnum; ++i) { + const auto& header = image->dlpi_phdr[i]; + if (header.p_type != PT_LOAD || header.p_offset == 0 || (header.p_flags & allAccess) == allAccess) continue; + *static_cast(data) += PageSpan(header.p_vaddr, header.p_memsz); + } + return 0; +} + +std::uint64_t GuestSegmentBytes() { + std::uint64_t bytes = 0; + dl_iterate_phdr(AddGuestSegmentBytes, &bytes); + return bytes; +} #else bool SectionNamed(const IMAGE_SECTION_HEADER& section, const char* name) { return std::strncmp(reinterpret_cast(section.Name), name, IMAGE_SIZEOF_SHORT_NAME) == 0; @@ -267,6 +310,37 @@ std::vector GuestImages() { } return images; } + +std::uint64_t ImageSegmentBytes(std::uintptr_t base) { + const auto* dos = reinterpret_cast(base); + const auto* nt = reinterpret_cast(base + static_cast(dos->e_lfanew)); + const auto* sections = IMAGE_FIRST_SECTION(nt); + const auto sectionCount = nt->FileHeader.NumberOfSections; + bool relinked = false; + for (unsigned i = 0; i < sectionCount; ++i) relinked = relinked || SectionNamed(sections[i], ".elf0"); + std::uint64_t bytes = 0; + for (unsigned i = 0; i < sectionCount; ++i) { + const auto& section = sections[i]; + if (SectionProtection(section) == 0 || (relinked && std::strncmp(reinterpret_cast(section.Name), ".elf", 4) != 0)) continue; + bytes += PageSpan(section.VirtualAddress, section.Misc.VirtualSize); + } + return bytes; +} + +std::uint64_t GuestSegmentBytes() { + const auto executable = reinterpret_cast(GetModuleHandleW(nullptr)); + constexpr std::wstring_view suffix = L".guest.prx"; + std::uint64_t bytes = 0; + for (const auto& image : GuestImages()) { + if (image.address != executable) { + const auto path = ModulePath(reinterpret_cast(image.address)); + if (path.size() < suffix.size() || CompareStringOrdinal(path.data() + path.size() - suffix.size(), static_cast(suffix.size()), suffix.data(), static_cast(suffix.size()), TRUE) != CSTR_EQUAL) + continue; + } + bytes += ImageSegmentBytes(image.address); + } + return bytes; +} #endif } @@ -323,4 +397,8 @@ int APS5_VABI sceKernelGetModuleInfo(KernelModule handle, ModuleInfo* info) { return SCE_KERNEL_ERROR_ESRCH; } +std::uint64_t GuestImageSegmentBytes_nid_no_patch() { + return GuestSegmentBytes(); +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp index b9a7b211c..5c5e64d18 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Cond.cpp @@ -41,6 +41,10 @@ int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex return toPosix(CondOperations::AbsoluteTimedwait(cond, mutex, abstime)); } +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { + return toPosix(scePthreadCondTimedwait(cond, mutex, usec)); +} + int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex) { return toPosix(scePthreadCondWait(cond, mutex)); } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e71072e2a..d11bda07a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,14 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + { + std::lock_guard lock(semaphore->lock); + if (semaphore->count > 0) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp new file mode 100644 index 000000000..3990f0d29 --- /dev/null +++ b/core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp @@ -0,0 +1,76 @@ +#include "SceTypes.hpp" +#include "Common.hpp" +#include +#include +#include +#include + +struct PthreadSpinlockPrivate { + std::atomic owner{}; +}; + +namespace { + +constexpr int GUEST_EPERM = 1; +constexpr int GUEST_ENOMEM = 12; +constexpr int GUEST_EBUSY = 16; +constexpr int PROCESS_PRIVATE = 0; +constexpr int PROCESS_SHARED = 1; +constexpr int SPIN_COUNT = 100000; + +bool tryAcquire(PthreadSpinlockPrivate* spinlock) { + std::thread::id unowned{}; + return spinlock->owner.compare_exchange_strong(unowned, std::this_thread::get_id(), std::memory_order_acquire, std::memory_order_relaxed); +} + +} + +extern "C" { + +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlockPrivate** lock, int pshared) { + if (!lock) return PosixThread::GUEST_EINVAL; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return PosixThread::GUEST_EINVAL; + auto* spinlock = new (std::nothrow) PthreadSpinlockPrivate; + if (!spinlock) return GUEST_ENOMEM; + *lock = spinlock; + return 0; +} + +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + delete *lock; + *lock = nullptr; + return 0; +} + +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + return tryAcquire(*lock) ? 0 : GUEST_EBUSY; +} + +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto* spinlock = *lock; + if (spinlock->owner.load(std::memory_order_relaxed) == std::this_thread::get_id()) + throw std::runtime_error("pthread_spin_lock: the calling thread already holds the lock and would spin forever"); + int count = SPIN_COUNT; + while (!tryAcquire(spinlock)) { + while (spinlock->owner.load(std::memory_order_relaxed) != std::thread::id{}) { + if (--count <= 0) { + count = SPIN_COUNT; + std::this_thread::yield(); + } + } + } + return 0; +} + +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlockPrivate** lock) { + if (!lock || !*lock) return PosixThread::GUEST_EINVAL; + auto owner = std::this_thread::get_id(); + if (!(*lock)->owner.compare_exchange_strong(owner, std::thread::id{}, std::memory_order_release, std::memory_order_relaxed)) + return GUEST_EPERM; + return 0; +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 11e577c3c..37beb1141 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -40,20 +40,26 @@ KernelCpumask APS5_VABI sceKernelGetAvailableCpumask(void); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI cpuset_getaffinity_nid_postfix(int level, int which, std::int64_t id, std::size_t size, void* mask) { - constexpr int LevelWhich = 3, WhichThread = 1; + constexpr int LevelWhich = 3, WhichThread = 1, WhichProcess = 2; constexpr int GuestEfault = 14, GuestErange = 34; constexpr std::size_t MaximumSize = 256 / 8; if (size < sizeof(KernelCpumask) || size > MaximumSize) { *__error_nid_postfix() = GuestErange; return -1; } - if (level != LevelWhich || which != WhichThread || id != -1) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread"); + const bool thread = level == LevelWhich && which == WhichThread && id == -1; + const bool process = level == LevelWhich && which == WhichProcess && (id == -1 || id == getpid_nid_postfix()); + if (!thread && !process) NotImplemented_nid_no_patch("cpuset_getaffinity other than the calling thread or process"); if (!mask) { *__error_nid_postfix() = GuestEfault; return -1; } KernelCpumask affinity = 0; - scePthreadGetaffinity(scePthreadSelf(), &affinity); + if (thread) scePthreadGetaffinity(scePthreadSelf(), &affinity); + else { + scePthreadSelf(); + affinity = LiveGuestThreadsAffinity(); + } std::memset(mask, 0, size); std::memcpy(mask, &affinity, sizeof(affinity)); return 0; diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index f2df5c9b2..05177ac38 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -151,5 +151,6 @@ void NoteGuestStackProtection(std::uintptr_t start, std::size_t bytes, bool read int GuestThreadCpuClockId(const PthreadPrivate* thread); bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos); void SetLiveGuestThreadsAffinity(KernelCpumask mask); +KernelCpumask LiveGuestThreadsAffinity(); #endif diff --git a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp index f342bb0ca..68deb724f 100644 --- a/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp +++ b/core/libs/prx/libkernel/Pthread/include/ThreadLifecycle.hpp @@ -8,6 +8,7 @@ public: static void SetThreadDtors(thread_dtors_func_t callback); static void SetThreadAtexitCount(get_thread_atexit_count_func_t callback); static void SetThreadAtexitReport(thread_atexit_report_func_t callback); + static void RunKeyDtors(); }; extern "C" void APS5_VABI scePthreadExit(void* retval); diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 7689b9c76..cd8890dcd 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -7,6 +7,7 @@ #include static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; @@ -105,10 +106,14 @@ int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); lock->_lock.unlock(); - } else { - lock->_readers.fetch_sub(1, std::memory_order_release); - lock->_lock.unlock_shared(); + return SCE_OK; } + if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{}) return SCE_KERNEL_ERROR_EPERM; + int readers = lock->_readers.load(std::memory_order_acquire); + do { + if (readers == 0) return SCE_KERNEL_ERROR_EPERM; + } while (!lock->_readers.compare_exchange_weak(readers, readers - 1, std::memory_order_acq_rel, std::memory_order_acquire)); + lock->_lock.unlock_shared(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 11400bdd2..fada8e72a 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -240,6 +240,16 @@ void SetLiveGuestThreadsAffinity(KernelCpumask mask) { } } +KernelCpumask LiveGuestThreadsAffinity() { + auto& registry = CpuClocks(); + std::lock_guard lock(registry.lock); + KernelCpumask mask = 0; + for (const auto& [id, thread] : registry.threads) { + if (!thread->_finished.load(std::memory_order_acquire)) mask |= thread->affinity.load(std::memory_order_relaxed); + } + return mask; +} + bool GuestThreadCpuNanos(int cpuClockThread, std::uint64_t* nanos) { auto& registry = CpuClocks(); std::lock_guard lock(registry.lock); @@ -340,9 +350,11 @@ static void finishThread(PthreadPrivate* self, void* retval) { if (CurrentGuestState().finishing) throw std::runtime_error("Guest thread is already finishing"); CurrentGuestState().finishing = true; + CurrentGuestState().exiting = true; UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); + ThreadLifecycle::RunKeyDtors(); { std::unique_lock lk(self->_join_mtx); self->_retval = retval; diff --git a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp index f04da09b6..f9fa4ecc6 100644 --- a/core/libs/prx/libkernel/Pthread/src/Tsd.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Tsd.cpp @@ -1,4 +1,5 @@ #include "../include/Pthread.hpp" +#include "../include/ThreadLifecycle.hpp" #include "../include/WindowsThreadLocal.hpp" #include "prx/libc/include/General.hpp" #include @@ -32,8 +33,15 @@ static std::uint64_t g_lastSequence = FREE_KEY; struct ThreadValues { std::array values{}; + bool destroyed = false; ~ThreadValues() { + RunDestructors(); + } + + void RunDestructors() { + if (destroyed) return; + destroyed = true; for (int round = 0; round < DESTRUCTOR_ITERATIONS; ++round) { bool called = false; for (int key = 0; key < MAX_KEYS; ++key) { @@ -52,6 +60,7 @@ struct ThreadValues { } if (!called) return; } + values = {}; } }; @@ -64,6 +73,10 @@ static ThreadValues& Values() { #endif } +void ThreadLifecycle::RunKeyDtors() { + Values().RunDestructors(); +} + extern "C" { int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor) { diff --git a/core/libs/prx/libkernel/Socket/include/SocketError.hpp b/core/libs/prx/libkernel/Socket/include/SocketError.hpp new file mode 100644 index 000000000..a7438c651 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/include/SocketError.hpp @@ -0,0 +1,99 @@ +#pragma once +#ifdef _WIN32 +#include +#else +#include +#endif + +namespace GuestSockets { + +constexpr int GuestErrorIo = 5; + +constexpr int GuestErrorFromHost(int hostError) { + switch (hostError) { +#ifdef _WIN32 + case WSAEINTR: return 4; + case WSAEBADF: return 9; + case WSAEACCES: return 13; + case WSAEFAULT: return 14; + case WSAEINVAL: return 22; + case WSAEMFILE: return 24; + case WSAEWOULDBLOCK: return 35; + case WSAEINPROGRESS: return 36; + case WSAEALREADY: return 37; + case WSAENOTSOCK: return 38; + case WSAEDESTADDRREQ: return 39; + case WSAEMSGSIZE: return 40; + case WSAEPROTOTYPE: return 41; + case WSAENOPROTOOPT: return 42; + case WSAEPROTONOSUPPORT: return 43; + case WSAESOCKTNOSUPPORT: return 44; + case WSAEOPNOTSUPP: return 45; + case WSAEPFNOSUPPORT: return 46; + case WSAEAFNOSUPPORT: return 47; + case WSAEADDRINUSE: return 48; + case WSAEADDRNOTAVAIL: return 49; + case WSAENETDOWN: return 50; + case WSAENETUNREACH: return 51; + case WSAENETRESET: return 52; + case WSAECONNABORTED: return 53; + case WSAECONNRESET: return 54; + case WSAENOBUFS: return 55; + case WSAEISCONN: return 56; + case WSAENOTCONN: return 57; + case WSAESHUTDOWN: return 32; + case WSAETOOMANYREFS: return 59; + case WSAETIMEDOUT: return 60; + case WSAECONNREFUSED: return 61; + case WSAEHOSTDOWN: return 64; + case WSAEHOSTUNREACH: return 65; +#else + case EPERM: return 1; + case EINTR: return 4; + case EBADF: return 9; + case ENOMEM: return 12; + case EACCES: return 13; + case EFAULT: return 14; + case EINVAL: return 22; + case ENFILE: return 23; + case EMFILE: return 24; + case EPIPE: return 32; + case EAGAIN: return 35; +#if EWOULDBLOCK != EAGAIN + case EWOULDBLOCK: return 35; +#endif + case EINPROGRESS: return 36; + case EALREADY: return 37; + case ENOTSOCK: return 38; + case EDESTADDRREQ: return 39; + case EMSGSIZE: return 40; + case EPROTOTYPE: return 41; + case ENOPROTOOPT: return 42; + case EPROTONOSUPPORT: return 43; + case ESOCKTNOSUPPORT: return 44; + case EOPNOTSUPP: return 45; + case EPFNOSUPPORT: return 46; + case EAFNOSUPPORT: return 47; + case EADDRINUSE: return 48; + case EADDRNOTAVAIL: return 49; + case ENETDOWN: return 50; + case ENETUNREACH: return 51; + case ENETRESET: return 52; + case ECONNABORTED: return 53; + case ECONNRESET: return 54; + case ENOBUFS: return 55; + case EISCONN: return 56; + case ENOTCONN: return 57; + case ESHUTDOWN: return 58; + case ETOOMANYREFS: return 59; + case ETIMEDOUT: return 60; + case ECONNREFUSED: return 61; + case EHOSTDOWN: return 64; + case EHOSTUNREACH: return 65; + case EPROTO: return 92; +#endif + default: return GuestErrorIo; + } +} + +} diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index b10fb9633..1ed1a2e20 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -1,47 +1,217 @@ -#ifdef _WIN32 -#define WIN32_LEAN_AND_MEAN -#include -#include -#else -#include -#endif #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include extern "C" int* APS5_VABI __error_nid_postfix(); +namespace { +constexpr int kGuestInet = 2; +constexpr int kGuestInet6 = 28; +constexpr std::size_t kMaxText = 46; + +bool ParseDecimalPart(const char* begin, const char* end, std::uint8_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 3) return false; + if (length > 1 && *begin == '0') return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + if (*cursor < '0' || *cursor > '9') return false; + total = total * 10 + static_cast(*cursor - '0'); + } + if (total > 255) return false; + value = static_cast(total); + return true; +} + +bool ParseQuad(const char* begin, const char* end, std::uint8_t (&bytes)[4]) { + const char* partBegin = begin; + int parts = 0; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != '.') continue; + if (parts == 4 || !ParseDecimalPart(partBegin, cursor, bytes[parts])) return false; + ++parts; + if (cursor == end) break; + partBegin = cursor + 1; + } + return parts == 4; +} + +int HexValue(char symbol) { + if (symbol >= '0' && symbol <= '9') return symbol - '0'; + if (symbol >= 'a' && symbol <= 'f') return symbol - 'a' + 10; + if (symbol >= 'A' && symbol <= 'F') return symbol - 'A' + 10; + return -1; +} + +bool ParseHexGroup(const char* begin, const char* end, std::uint16_t& value) { + const std::ptrdiff_t length = end - begin; + if (length < 1 || length > 4) return false; + unsigned total = 0; + for (const char* cursor = begin; cursor != end; ++cursor) { + const int digit = HexValue(*cursor); + if (digit < 0) return false; + total = total * 16 + static_cast(digit); + } + value = static_cast(total); + return true; +} + +bool ParseGroupList(const char* begin, const char* end, bool mayEndWithQuad, + std::uint16_t (&groups)[8], int& count) { + count = 0; + if (begin == end) return true; + const char* groupBegin = begin; + for (const char* cursor = begin;; ++cursor) { + if (cursor != end && *cursor != ':') continue; + const bool last = cursor == end; + bool hasDot = false; + for (const char* probe = groupBegin; probe != cursor; ++probe) hasDot = hasDot || *probe == '.'; + if (hasDot) { + std::uint8_t quad[4]; + if (!last || !mayEndWithQuad || count > 6 || !ParseQuad(groupBegin, cursor, quad)) return false; + groups[count++] = static_cast(quad[0] << 8 | quad[1]); + groups[count++] = static_cast(quad[2] << 8 | quad[3]); + return true; + } + if (count == 8 || !ParseHexGroup(groupBegin, cursor, groups[count])) return false; + ++count; + if (last) return true; + groupBegin = cursor + 1; + } +} + +bool TextToV6(const char* text, std::uint8_t (&bytes)[16]) { + const char* const end = text + std::strlen(text); + const char* gap = std::strstr(text, "::"); + std::uint16_t head[8]; + std::uint16_t tail[8]; + int headCount = 0; + int tailCount = 0; + if (!gap) { + if (!ParseGroupList(text, end, true, head, headCount) || headCount != 8) return false; + } else { + if (std::strstr(gap + 1, "::")) return false; + if (!ParseGroupList(text, gap, false, head, headCount)) return false; + if (!ParseGroupList(gap + 2, end, true, tail, tailCount)) return false; + if (headCount + tailCount > 7) return false; + } + std::uint16_t words[8]{}; + for (int index = 0; index < headCount; ++index) words[index] = head[index]; + for (int index = 0; index < tailCount; ++index) words[8 - tailCount + index] = tail[index]; + for (int index = 0; index < 8; ++index) { + bytes[index * 2] = static_cast(words[index] >> 8); + bytes[index * 2 + 1] = static_cast(words[index]); + } + return true; +} + +std::size_t AppendDecimal(char* out, unsigned value) { + char digits[3]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +std::size_t FormatQuad(const std::uint8_t* bytes, char* out) { + std::size_t length = 0; + for (int index = 0; index < 4; ++index) { + if (index) out[length++] = '.'; + length += AppendDecimal(out + length, bytes[index]); + } + return length; +} + +std::size_t AppendHex(char* out, unsigned value) { + static const char symbols[] = "0123456789abcdef"; + char digits[4]; + std::size_t count = 0; + do { + digits[count++] = symbols[value & 15]; + value >>= 4; + } while (value); + for (std::size_t index = 0; index < count; ++index) out[index] = digits[count - 1 - index]; + return count; +} + +void FindLongestZeroRun(const std::uint16_t (&words)[8], int& start, int& length) { + start = -1; + length = 0; + for (int from = 0; from < 8; ++from) { + if (words[from] != 0) continue; + int run = 0; + while (from + run < 8 && words[from + run] == 0) ++run; + if (run > length) { + start = from; + length = run; + } + from += run; + } + if (length < 2) { + start = -1; + length = 0; + } +} + +std::size_t V6ToText(const std::uint8_t* bytes, char* out) { + std::uint16_t words[8]; + for (int index = 0; index < 8; ++index) + words[index] = static_cast(bytes[index * 2] << 8 | bytes[index * 2 + 1]); + int runStart; + int runLength; + FindLongestZeroRun(words, runStart, runLength); + std::size_t length = 0; + if (runStart == 0 && (runLength == 6 || (runLength == 5 && words[5] == 0xffff))) { + const char* prefix = runLength == 6 ? "::" : "::ffff:"; + length = std::strlen(prefix); + std::memcpy(out, prefix, length); + return length + FormatQuad(bytes + 12, out + length); + } + for (int index = 0; index < 8;) { + if (index == runStart) { + out[length++] = ':'; + if (index + runLength == 8) out[length++] = ':'; + index += runLength; + continue; + } + if (index) out[length++] = ':'; + length += AppendHex(out + length, words[index]); + ++index; + } + return length; +} +} + extern "C" { int APS5_VABI __inet_pton_nid_postfix(int family, const char* text, void* destination) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return -1; } + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return -1; } if (!text || !destination) { *__error_nid_postfix() = 14; return -1; } - unsigned char address[16]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const int result = InetPtonA(nativeFamily, text, address); -#else - const int result = ::inet_pton(nativeFamily, text, address); -#endif - if (result == 1) std::memcpy(destination, address, family == 2 ? 4 : 16); - else if (result == -1) *__error_nid_postfix() = 22; - return result; + if (family == kGuestInet) { + std::uint8_t quad[4]; + if (!ParseQuad(text, text + std::strlen(text), quad)) return 0; + std::memcpy(destination, quad, sizeof(quad)); + return 1; + } + std::uint8_t address[16]; + if (!TextToV6(text, address)) return 0; + std::memcpy(destination, address, sizeof(address)); + return 1; } const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, char* destination, std::uint32_t capacity) { - if (family != 2 && family != 28) { *__error_nid_postfix() = 47; return nullptr; } + if (family != kGuestInet && family != kGuestInet6) { *__error_nid_postfix() = 47; return nullptr; } if (!source || !destination) { *__error_nid_postfix() = 14; return nullptr; } - char text[INET6_ADDRSTRLEN]{}; - const int nativeFamily = family == 2 ? AF_INET : AF_INET6; -#ifdef _WIN32 - const auto* result = InetNtopA(nativeFamily, const_cast(source), text, sizeof(text)); -#else - const auto* result = ::inet_ntop(nativeFamily, source, text, sizeof(text)); -#endif - if (!result) { *__error_nid_postfix() = 22; return nullptr; } - const auto size = std::strlen(text) + 1; - if (size > capacity) { *__error_nid_postfix() = 28; return nullptr; } - std::memcpy(destination, text, size); + char text[kMaxText + 1]; + const auto* bytes = static_cast(source); + const std::size_t length = family == kGuestInet ? FormatQuad(bytes, text) : V6ToText(bytes, text); + text[length] = '\0'; + if (length + 1 > capacity) { *__error_nid_postfix() = 28; return nullptr; } + std::memcpy(destination, text, length + 1); return destination; } } diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index f7e393e53..3f71f4f1c 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -5,18 +5,25 @@ #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketSelectDeadline.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include #include +#include #include #include #include #include +#ifndef _WIN32 +#include +#endif + namespace { constexpr int GuestEinval = 22; constexpr int SelectDescriptorLimit = 1024; +constexpr int SelectPollSliceMilliseconds = 10; struct GuestTimeval { std::int64_t seconds; @@ -38,6 +45,45 @@ void ClearDescriptor(void* set, int descriptor) { if (set != nullptr) static_cast(set)[descriptor / 64] &= ~(std::uint64_t{1} << (descriptor % 64)); } +#ifndef _WIN32 + +short HostEventFlags(short events) { + short flags = 0; + if ((events & KernelSocketPoll::Readable) != 0) flags |= POLLIN; + if ((events & KernelSocketPoll::Writable) != 0) flags |= POLLOUT; + return flags; +} + +short KernelEventFlags(short revents) { + short flags = 0; + if ((revents & POLLIN) != 0) flags |= KernelSocketPoll::Readable; + if ((revents & POLLOUT) != 0) flags |= KernelSocketPoll::Writable; + if ((revents & POLLERR) != 0) flags |= KernelSocketPoll::Error; + if ((revents & POLLHUP) != 0) flags |= KernelSocketPoll::HangUp; + return flags; +} + +int HostPoll(KernelSocketPoll::Entry* descriptors, std::size_t count, int timeoutMilliseconds) { + std::vector native(count); + for (std::size_t index = 0; index < count; ++index) { + native[index].fd = descriptors[index].descriptor; + native[index].events = descriptors[index].events; + native[index].revents = 0; + } + const int result = ::poll(native.data(), static_cast(count), timeoutMilliseconds); + if (result < 0) return -1; + for (std::size_t index = 0; index < count; ++index) { + if ((native[index].revents & POLLNVAL) != 0) { + errno = EBADF; + return -1; + } + descriptors[index].revents = KernelEventFlags(native[index].revents); + } + return result; +} + +#endif + } extern "C" { @@ -65,8 +111,12 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* *__error_nid_postfix() = GuestEinval; return -1; } - std::vector entries; + std::vector sockets; + std::vector socketSlots; + std::vector hosts; + std::vector hostSlots; std::vector devices; + std::vector requested; for (int descriptor = 0; descriptor < nfds; ++descriptor) { short events = 0; if (DescriptorSet(readfds, descriptor)) events |= KernelSocketPoll::Readable; @@ -75,11 +125,56 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* if (events == 0) continue; if (File::IsRandomDevice(descriptor)) devices.push_back({descriptor, events, static_cast(events & (KernelSocketPoll::Readable | KernelSocketPoll::Writable))}); else if (descriptor <= 2) devices.push_back({descriptor, events, static_cast(descriptor == 0 ? 0 : events & KernelSocketPoll::Writable)}); - else entries.push_back({descriptor, events, 0}); + else { + sockets.push_back({descriptor, events, 0}); + socketSlots.push_back(requested.size()); + requested.push_back({descriptor, events, 0}); + } } - const auto sockets = entries.size(); + for (const auto& device : devices) requested.push_back(device); const bool devicesReady = std::any_of(devices.begin(), devices.end(), [](const auto& entry) { return entry.revents != 0; }); - entries.insert(entries.end(), devices.begin(), devices.end()); + const auto poller = g_socketPoller.load(); + if (!sockets.empty()) { + if (poller == nullptr) { + for (std::size_t index = 0; index < sockets.size();) { + const int descriptor = sockets[index].descriptor; +#ifndef _WIN32 + if (descriptor < GuestSockets::FirstDescriptor) { + hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); + hostSlots.push_back(socketSlots[index]); + sockets.erase(sockets.begin() + static_cast(index)); + socketSlots.erase(socketSlots.begin() + static_cast(index)); + continue; + } +#endif + UnknownDescriptor(descriptor, false); + } + } else { + for (auto& socket : sockets) socket.revents = 0; + const int probe = poller(sockets.data(), static_cast(sockets.size()), 0); + if (probe < 0) { + *__error_nid_postfix() = -probe; + return -1; + } + for (std::size_t index = 0; index < sockets.size();) { + if ((sockets[index].revents & KernelSocketPoll::Unknown) == 0) { + ++index; + continue; + } + const int descriptor = sockets[index].descriptor; +#ifndef _WIN32 + if (descriptor < GuestSockets::FirstDescriptor) { + hosts.push_back({descriptor, HostEventFlags(sockets[index].events), 0}); + hostSlots.push_back(socketSlots[index]); + sockets.erase(sockets.begin() + static_cast(index)); + socketSlots.erase(socketSlots.begin() + static_cast(index)); + continue; + } +#endif + UnknownDescriptor(descriptor, true); + } + } + } const auto start = std::chrono::steady_clock::now(); const auto deadline = limit == nullptr ? std::chrono::steady_clock::time_point::max() : KernelSocketSelect::DeadlineAfter(start, limit->seconds, limit->microseconds); @@ -91,20 +186,48 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* waitMilliseconds = static_cast(std::clamp(roundedMilliseconds, 0, 100)); } if (devicesReady) waitMilliseconds = 0; - if (sockets != 0) { - const auto poller = g_socketPoller.load(); - if (poller == nullptr) UnknownDescriptor(entries.front().descriptor, false); - for (std::size_t index = 0; index < sockets; ++index) entries[index].revents = 0; - const int result = poller(entries.data(), static_cast(sockets), waitMilliseconds); + if (!hosts.empty() && !sockets.empty()) { + if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); + const auto sliceEnd = std::chrono::steady_clock::now() + std::chrono::milliseconds(waitMilliseconds); + for (;;) { + for (auto& host : hosts) host.revents = 0; + const int hostReady = HostPoll(hosts.data(), hosts.size(), SelectPollSliceMilliseconds); + if (hostReady < 0) { + *__error_nid_postfix() = errno; + return -1; + } + for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; + for (auto& socket : sockets) socket.revents = 0; + const int socketReady = poller(sockets.data(), static_cast(sockets.size()), SelectPollSliceMilliseconds); + if (socketReady < 0) { + *__error_nid_postfix() = -socketReady; + return -1; + } + for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; + if (hostReady > 0 || socketReady > 0) break; + if (std::chrono::steady_clock::now() >= sliceEnd) break; + } + } else if (!hosts.empty()) { + for (auto& host : hosts) host.revents = 0; + if (HostPoll(hosts.data(), hosts.size(), waitMilliseconds) < 0) { + *__error_nid_postfix() = errno; + return -1; + } + for (std::size_t index = 0; index < hosts.size(); ++index) requested[hostSlots[index]].revents = hosts[index].revents; + } else if (!sockets.empty()) { + if (poller == nullptr) UnknownDescriptor(sockets.front().descriptor, false); + for (auto& socket : sockets) socket.revents = 0; + const int result = poller(sockets.data(), static_cast(sockets.size()), waitMilliseconds); if (result < 0) { *__error_nid_postfix() = -result; return -1; } + for (std::size_t index = 0; index < sockets.size(); ++index) requested[socketSlots[index]].revents = sockets[index].revents; } else if (waitMilliseconds > 0) { std::this_thread::sleep_for(std::chrono::milliseconds(waitMilliseconds)); } int ready = 0; - for (const auto& entry : entries) { + for (const auto& entry : requested) { if ((entry.revents & KernelSocketPoll::Unknown) != 0) UnknownDescriptor(entry.descriptor, true); const bool readable = (entry.events & KernelSocketPoll::Readable) != 0 && (entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) != 0; const bool writable = (entry.events & KernelSocketPoll::Writable) != 0 && (entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) != 0; @@ -112,7 +235,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* ready += static_cast(readable) + static_cast(writable) + static_cast(urgent); } if (ready > 0) { - for (const auto& entry : entries) { + for (const auto& entry : requested) { if ((entry.revents & (KernelSocketPoll::Readable | KernelSocketPoll::HangUp | KernelSocketPoll::Error)) == 0) ClearDescriptor(readfds, entry.descriptor); if ((entry.revents & (KernelSocketPoll::Writable | KernelSocketPoll::Error)) == 0) ClearDescriptor(writefds, entry.descriptor); if ((entry.revents & KernelSocketPoll::Urgent) == 0) ClearDescriptor(exceptfds, entry.descriptor); @@ -120,7 +243,7 @@ int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* return ready; } if (limit != nullptr && std::chrono::steady_clock::now() >= deadline) { - for (const auto& entry : entries) { + for (const auto& entry : requested) { ClearDescriptor(readfds, entry.descriptor); ClearDescriptor(writefds, entry.descriptor); ClearDescriptor(exceptfds, entry.descriptor); diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b5f852307..52f37a428 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #endif #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" @@ -54,47 +55,9 @@ int NativeSendFlags(int flags) { } int NativeError() { #ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEWOULDBLOCK: return 35; - case WSAEADDRINUSE: return 48; - case WSAEADDRNOTAVAIL: return 49; - case WSAEACCES: return 13; - case WSAEMSGSIZE: return 40; - case WSAENETUNREACH: return 51; - case WSAEHOSTUNREACH: return 65; - case WSAECONNRESET: return 54; - case WSAECONNABORTED: return 53; - case WSAEISCONN: return 56; - case WSAENOTCONN: return 57; - case WSAENOBUFS: return 55; - case WSAETIMEDOUT: return 60; - case WSAECONNREFUSED: return 61; - case WSAEINTR: return 4; - case WSAEINVAL: return 22; - case WSAESHUTDOWN: return 32; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(WSAGetLastError()); #else - switch (errno) { - case EAGAIN: return 35; - case EADDRINUSE: return 48; - case EADDRNOTAVAIL: return 49; - case EACCES: return 13; - case EMSGSIZE: return 40; - case ENETUNREACH: return 51; - case EHOSTUNREACH: return 65; - case ECONNREFUSED: return 61; - case ECONNRESET: return 54; - case ECONNABORTED: return 53; - case EISCONN: return 56; - case ENOTCONN: return 57; - case ENOBUFS: return 55; - case ETIMEDOUT: return 60; - case EINTR: return 4; - case EINVAL: return 22; - case EPIPE: return 32; - default: return 5; - } + return GuestSockets::GuestErrorFromHost(errno); #endif } struct Socket { @@ -354,21 +317,6 @@ namespace { constexpr int GuestUnix = 1; constexpr int GuestCloseOnExec = 0x10000000; constexpr int GuestNonBlocking = 0x20000000; -int PairError() { -#ifdef _WIN32 - switch (WSAGetLastError()) { - case WSAEMFILE: return 24; - default: return NativeError(); - } -#else - switch (errno) { - case EMFILE: return 24; - case ENFILE: return 23; - case ENOMEM: return 12; - default: return NativeError(); - } -#endif -} int SetNonBlocking(Socket& socket) { #ifdef _WIN32 unsigned long value = 1; @@ -394,23 +342,23 @@ int NativePair(int type, NativeSocket (&pair)[2]) { loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK); const int nativeType = type == 1 ? SOCK_STREAM : SOCK_DGRAM; Socket first(::socket(AF_INET, nativeType, 0), 2, type); - if (first.value == Invalid) return PairError(); + if (first.value == Invalid) return NativeError(); const int exclusive = 1; setsockopt(first.value, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&exclusive), sizeof(exclusive)); - if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(first.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in firstName{}; - if (!LoopbackName(first.value, firstName)) return PairError(); + if (!LoopbackName(first.value, firstName)) return NativeError(); Socket second(::socket(AF_INET, nativeType, 0), 2, type); - if (second.value == Invalid) return PairError(); + if (second.value == Invalid) return NativeError(); if (type == 1) { - if (listen(first.value, 1)) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (listen(first.value, 1)) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); sockaddr_in peer{}; int peerSize = sizeof(peer); Socket accepted(accept(first.value, reinterpret_cast(&peer), &peerSize), 2, type); - if (accepted.value == Invalid) return PairError(); + if (accepted.value == Invalid) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); if (peer.sin_port != secondName.sin_port) return 53; const int noDelay = 1; setsockopt(second.value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&noDelay), sizeof(noDelay)); @@ -421,11 +369,11 @@ int NativePair(int type, NativeSocket (&pair)[2]) { accepted.value = Invalid; return 0; } - if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return PairError(); + if (bind(second.value, reinterpret_cast(&loopback), sizeof(loopback))) return NativeError(); sockaddr_in secondName{}; - if (!LoopbackName(second.value, secondName)) return PairError(); - if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return PairError(); - if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return PairError(); + if (!LoopbackName(second.value, secondName)) return NativeError(); + if (connect(first.value, reinterpret_cast(&secondName), sizeof(secondName))) return NativeError(); + if (connect(second.value, reinterpret_cast(&firstName), sizeof(firstName))) return NativeError(); pair[0] = first.value; pair[1] = second.value; first.value = Invalid; @@ -436,7 +384,7 @@ int NativePair(int type, NativeSocket (&pair)[2]) { int NativePair(int type, NativeSocket (&pair)[2]) { const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : SOCK_SEQPACKET; int native[2]; - if (::socketpair(AF_UNIX, nativeType, 0, native)) return PairError(); + if (::socketpair(AF_UNIX, nativeType, 0, native)) return NativeError(); pair[0] = native[0]; pair[1] = native[1]; return 0; @@ -659,6 +607,9 @@ std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint6 if (!buffer && length) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); +#endif return result < 0 ? Fail(NativeError()) : result; } int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_t length) { @@ -750,8 +701,11 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u if ((!buffer && length) || (address && !addressLength)) return Fail(14); sockaddr_storage native{}; socklen_t size = sizeof(native); - const auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), + auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); +#ifdef _WIN32 + if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); +#endif if (result < 0) return Fail(NativeError()); if (address && socket->family == GuestUnix) { if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength); @@ -852,6 +806,86 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, message->controlLength = 0; return received; } +struct GuestSendfileHeaders { + GuestIovec* headers; + int headerCount; + GuestIovec* trailers; + int trailerCount; +}; +static_assert(sizeof(GuestSendfileHeaders) == 32 && offsetof(GuestSendfileHeaders, trailers) == 16); +std::int64_t APS5_VABI sceKernelPread(int descriptor, void* buffer, std::size_t length, std::int64_t offset); +static bool SendfileIovecs(const GuestIovec* iov, int count) { + if (!iov) return true; + if (count < 0 || count > 1024) return false; + std::uint64_t total = 0; + for (int i = 0; i < count; ++i) { + if (iov[i].length > INT_MAX - total) return false; + total += iov[i].length; + } + return true; +} +static int SendAll(const Socket& socket, const void* data, std::uint64_t length, std::int64_t& sent) { + if (!data && length) return 14; + const char* bytes = static_cast(data); + while (length) { + const auto result = ::send(socket.value, bytes, static_cast(std::min(length, INT_MAX)), NativeSendFlags(GuestNoSignal)); + if (result < 0) return NativeError(); + bytes += result; + length -= static_cast(result); + sent += result; + } + return 0; +} +static int SendIovecs(const Socket& socket, const GuestIovec* iov, int count, std::int64_t& sent) { + for (int i = 0; iov && i < count; ++i) + if (const int error = SendAll(socket, iov[i].base, iov[i].length, sent)) return error; + return 0; +} +int APS5_VABI sendfile_nid_postfix(int file, int descriptor, std::int64_t offset, std::uint64_t length, + const GuestSendfileHeaders* headers, std::int64_t* sbytes, int flags) { + (void)flags; + if (offset < 0) return Fail(22); + if (headers && (!SendfileIovecs(headers->headers, headers->headerCount) || !SendfileIovecs(headers->trailers, headers->trailerCount))) + return Fail(22); + std::vector chunk; + try { + chunk.resize(0x10000); + } catch (const std::bad_alloc&) { + return Fail(12); + } + const auto read = [&](std::int64_t position, std::uint64_t remaining) { + const auto result = sceKernelPread(file, chunk.data(), static_cast(std::min(remaining, chunk.size())), position); + return result < 0 ? -(result & 0xffff) : result; + }; + std::uint64_t remaining = length ? length : UINT64_MAX; + auto received = read(offset, remaining); + if (received == -9) return Fail(9); + std::int64_t sent = 0; + const auto finish = [&](int error) { + if (sbytes) *sbytes = sent; + return error ? Fail(error) : 0; + }; + if (received < 0) return finish(received == -21 || received == -29 ? 22 : static_cast(-received)); + const auto socket = Lookup(descriptor); + if (!socket) return finish(9); + if (socket->type != 1) return finish(22); + sockaddr_storage peer{}; + socklen_t size = sizeof(peer); + if (::getpeername(socket->value, reinterpret_cast(&peer), &size)) return finish(57); + if (headers) + if (const int error = SendIovecs(*socket, headers->headers, headers->headerCount, sent)) return finish(error); + while (received > 0) { + if (const int error = SendAll(*socket, chunk.data(), static_cast(received), sent)) return finish(error); + offset += received; + remaining -= static_cast(received); + if (remaining == 0) break; + received = read(offset, remaining); + if (received < 0) return finish(static_cast(-received)); + } + if (headers) + if (const int error = SendIovecs(*socket, headers->trailers, headers->trailerCount, sent)) return finish(error); + return finish(0); +} int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t count, int timeout) { if (timeout < -1) return Fail(22); if (count && !descriptors) return Fail(14); diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index bf4dc53cb..dda99c2ed 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -27,6 +27,7 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; constexpr int GUEST_CLOCK_REALTIME = 0; @@ -183,6 +184,21 @@ int UmtxWake(void* object, std::uint64_t count, bool isPrivate) { return 0; } +int UmtxNwakePrivate(void* object, std::uint64_t value) { + const auto count = static_cast(value); + if (count <= 0) { + return 0; + } + if (object == nullptr) { + return GUEST_EFAULT; + } + const auto* addresses = static_cast(object); + for (std::int32_t index = 0; index < count; ++index) { + WakeAddress(reinterpret_cast(addresses[index]), std::numeric_limits::max(), true); + } + return 0; +} + } // namespace extern "C" { @@ -252,6 +268,9 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_WAKE_PRIVATE: error = UmtxWake(object, value, operation == UMTX_OP_WAKE_PRIVATE); break; + case UMTX_OP_NWAKE_PRIVATE: + error = UmtxNwakePrivate(object, value); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..f4d546837 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -200,6 +200,12 @@ int APS5_VABI getpid_nid_postfix(void) { return static_cast(pid); } +int APS5_VABI getsid_nid_postfix(int pid) { + (void)pid; + NotImplemented_nid_no_patch("getsid: process sessions"); + return -1; +} + int APS5_VABI getppid_nid_postfix(void) { #ifdef _WIN32 const DWORD self = GetCurrentProcessId(); @@ -288,6 +294,13 @@ int APS5_VABI execvp_nid_postfix(const char* file, char* const* arguments) { return -1; } +int APS5_VABI execv_nid_postfix(const char* path, char* const* arguments) { + (void)path; + (void)arguments; + NotImplemented_nid_no_patch("execv: executable replacement"); + return -1; +} + int APS5_VABI sceKernelGetCurrentCpu(void) { #ifdef _WIN32 PROCESSOR_NUMBER processor{}; diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index 62d698299..0e06f1538 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -150,6 +150,37 @@ extern "C" unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int seconds) return remaining; } +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" int APS5_VABI getitimer_nid_postfix(int which, GuestItimerval* output) { + if (which < 0 || which > 2) { + *__error_nid_postfix() = 22; + return -1; + } + if (which != 0) throw std::runtime_error("getitimer: CPU interval timers are not implemented"); + if (output == nullptr) { + *__error_nid_postfix() = 14; + return -1; + } + GuestItimerval result{}; + auto& alarms = Alarms(); + { + std::lock_guard lock(alarms.lock); + const auto now = std::chrono::steady_clock::now(); + if (alarms.deadline && *alarms.deadline > now) { + const auto micros = std::chrono::duration_cast(*alarms.deadline - now).count(); + result.value.tv_sec = micros / 1000000; + result.value.tv_usec = micros % 1000000; + } + } + *output = result; + return 0; +} + struct GuestStack { void* sp; std::size_t size; @@ -257,6 +288,25 @@ int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet* set, void* info, co NotImplemented_nid_no_patch("sigtimedwait: waiting for a pending signal"); return -1; } + +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet* set, void* info) { + (void)set; + (void)info; + NotImplemented_nid_no_patch("sigwaitinfo: waiting for a pending signal"); + return -1; +} + +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet* set, int* received) { + (void)set; + (void)received; + NotImplemented_nid_no_patch("sigwait: waiting for a pending signal"); + return -1; +} + +int APS5_VABI pause_nid_postfix(void) { + NotImplemented_nid_no_patch("pause: waiting for a delivered signal"); + return -1; +} } extern "C" { diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index abbd8d0ea..106e28393 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -16,6 +16,7 @@ #endif extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI getpid_nid_postfix(); struct SceKernelSystemSwVersion { std::uint64_t size; @@ -90,19 +91,46 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 14; return -1; } - if (nameLength != 2 || name[0] != 6 || name[1] != 3) { - std::string mib; - for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); - throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); + if (nameLength == 2 && name[0] == 6 && name[1] == 3) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t processors = sysconf_nid_postfix(58); + if (processors <= 0) return -1; + const int value = static_cast(processors); + return CopyOut(&value, sizeof(value), oldValue, oldLength); } - if (newValue != nullptr) { - *__error_nid_postfix() = 1; - return -1; + if (nameLength == 2 && name[0] == 6 && name[1] == 5) { + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const std::int64_t pages = sysconf_nid_postfix(121); + if (pages <= 0) return -1; + const std::uint64_t physmem = static_cast(pages) * PS5_PAGE_SIZE; + return CopyOut(&physmem, sizeof(physmem), oldValue, oldLength); } - const std::int64_t processors = sysconf_nid_postfix(58); - if (processors <= 0) return -1; - const int value = static_cast(processors); - return CopyOut(&value, sizeof(value), oldValue, oldLength); + if (nameLength >= 3 && name[0] == 1 && name[1] == 14 && name[2] == 12) { + if (nameLength != 4) { + *__error_nid_postfix() = 22; + return -1; + } + if (newValue != nullptr) { + *__error_nid_postfix() = 1; + return -1; + } + const int pid = name[3]; + if (pid != -1 && pid != getpid_nid_postfix()) { + *__error_nid_postfix() = 3; + return -1; + } + static constexpr char path[] = "/app0/eboot.bin"; + return CopyOut(path, sizeof(path), oldValue, oldLength); + } + std::string mib; + for (std::uint32_t i = 0; i < nameLength; ++i) mib += (i == 0 ? "" : ".") + std::to_string(name[i]); + throw std::runtime_error(std::string(__func__) + ": unsupported MIB " + mib); } int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::size_t* oldLength, const void* newValue, std::size_t newLength) { @@ -110,6 +138,14 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si *__error_nid_postfix() = 14; return -1; } + if (std::strcmp(name, "machdep.cacheline_size") == 0) { + *__error_nid_postfix() = 2; + return -1; + } + if (std::strcmp(name, "kern.proc.pathname") == 0) { + *__error_nid_postfix() = 22; + return -1; + } if (std::strcmp(name, "kern.rng_pseudo") == 0) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -121,9 +157,15 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (std::size_t i = 0; i < *oldLength; ++i) bytes[i] = static_cast(device()); return 0; } - if (std::strcmp(name, "hw.ncpu") != 0) throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); - static constexpr int mib[] = {6, 3}; - return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + if (std::strcmp(name, "hw.ncpu") == 0) { + static constexpr int mib[] = {6, 3}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + if (std::strcmp(name, "hw.physmem") == 0) { + static constexpr int mib[] = {6, 5}; + return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); + } + throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 581e81bb5..0f86dbf4d 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -473,8 +473,7 @@ int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz) { } int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { - KernelTimespec ignoredResolution{}; - if (res == nullptr) res = &ignoredResolution; + if (res == nullptr) return 0; if (IsCpuClock(clockId)) return clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, res); if (clockId == 1 || clockId == 2) { diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 630e46d51..f3554d51e 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -15,7 +14,18 @@ #endif #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/FilesystemError.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libc/include/CreationMask.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif +#endif extern "C" int* APS5_VABI __error_nid_postfix(); @@ -27,11 +37,12 @@ constexpr int GuestEfault = 14; constexpr int GuestEinval = 22; constexpr int GuestEexist = 17; constexpr int GuestExdev = 18; -#ifdef _WIN32 -constexpr int WindowsPrivilegeNotHeld = 1314; -#endif - -std::atomic creationMask{022}; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEagain = 35; +constexpr int GuestEloop = 62; +constexpr int GuestEnametoolong = 63; +constexpr int GuestEnotempty = 66; +constexpr int GuestEoverflow = 84; std::int64_t Fail(int error) { *__error_nid_postfix() = error; @@ -39,7 +50,14 @@ std::int64_t Fail(int error) { } int GuestError(const std::error_code& error) { - const int value = error.default_error_condition().value(); + const auto condition = error.default_error_condition(); + if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; + if (condition == std::errc::resource_deadlock_would_occur) return GuestEdeadlk; + if (condition == std::errc::too_many_symbolic_link_levels) return GuestEloop; + if (condition == std::errc::filename_too_long) return GuestEnametoolong; + if (condition == std::errc::directory_not_empty) return GuestEnotempty; + if (condition == std::errc::value_too_large) return GuestEoverflow; + const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } } @@ -56,7 +74,7 @@ int APS5_VABI setsid_nid_postfix(void) { } std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t mask) { - return creationMask.exchange(static_cast(mask & 07777)); + return File::ExchangeCreationMask_nid_no_patch(mask); } int APS5_VABI _fstatfs_nid_postfix(int d, void* buf); @@ -69,7 +87,15 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); - const auto native = ResolvePath_nid_no_patch(path); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + NotImplemented_nid_no_patch("readlink of a Windows symbolic link"); + return -1; + } +#endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); if (error) return Fail(GuestError(error)); @@ -86,8 +112,9 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (existing == nullptr || created == nullptr) return static_cast(Fail(GuestEfault)); if (*existing == '\0' || *created == '\0') return static_cast(Fail(GuestEnoent)); - const auto source = ResolvePath_nid_no_patch(existing); - const auto destination = ResolvePath_nid_no_patch(created); + std::filesystem::path source, destination; + if (const int error = ResolvePathError_nid_no_patch(existing, source)) return static_cast(Fail(error)); + if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); std::error_code error; const auto status = std::filesystem::symlink_status(source, error); if (!std::filesystem::exists(status)) return static_cast(Fail(GuestEnoent)); @@ -121,19 +148,34 @@ int APS5_VABI symlink_nid_postfix(const char* target, const char* created) { NotImplemented_nid_no_patch("symlink: absolute targets"); return -1; } - const auto native = ResolvePath_nid_no_patch(created); + std::filesystem::path native; + if (const int error = ResolvePathError_nid_no_patch(created, native)) return static_cast(Fail(error)); std::error_code error; +#ifdef _WIN32 + if (GetFileAttributesW(native.wstring().c_str()) != INVALID_FILE_ATTRIBUTES) return static_cast(Fail(GuestEexist)); +#else if (std::filesystem::exists(std::filesystem::symlink_status(native, error))) return static_cast(Fail(GuestEexist)); +#endif if (!std::filesystem::is_directory(native.parent_path(), error)) return static_cast(Fail(GuestEnoent)); const std::filesystem::path contents(target); - if (std::filesystem::is_directory(native.parent_path() / contents, error)) + const bool directory = std::filesystem::is_directory(native.parent_path() / contents, error); +#ifdef _WIN32 + DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + if (directory) flags |= SYMBOLIC_LINK_FLAG_DIRECTORY; + if (!CreateSymbolicLinkW(native.wstring().c_str(), std::filesystem::path(contents).make_preferred().wstring().c_str(), flags)) { + const DWORD code = GetLastError(); + if (code == ERROR_PRIVILEGE_NOT_HELD) return static_cast(Fail(GuestEperm)); + if (code == ERROR_ALREADY_EXISTS || code == ERROR_FILE_EXISTS) return static_cast(Fail(GuestEexist)); + if (code == ERROR_PATH_NOT_FOUND) return static_cast(Fail(GuestEnoent)); + throw std::system_error(static_cast(code), std::system_category(), "symlink"); + } +#else + if (directory) std::filesystem::create_directory_symlink(contents, native, error); else std::filesystem::create_symlink(contents, native, error); -#ifdef _WIN32 - if (error.category() == std::system_category() && error.value() == WindowsPrivilegeNotHeld) return static_cast(Fail(GuestEperm)); -#endif if (error) throw std::system_error(error, "symlink"); +#endif RecordWrittenPath_nid_no_patch(native); return 0; } diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 6cd531139..04adba57c 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -11,6 +11,7 @@ extern "C" { int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +void* APS5_VABI reallocf_nid_postfix(void*, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; @@ -148,6 +149,24 @@ int main(int argc, char** argv) { pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); + pointer = static_cast(reallocf_nid_postfix(nullptr, 24)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 24); + std::memset(pointer, 0x3c, 24); + pointer = static_cast(reallocf_nid_postfix(pointer, 200)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 200); + for (unsigned i = 0; i < 24; ++i) require(pointer[i] == 0x3c); + pointer = static_cast(reallocf_nid_postfix(pointer, 9)); + require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); + for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); for (std::size_t alignment : {16, 64, 4096}) { auto* aligned = ApplicationHeapAlign_nid_no_patch(alignment, 37); require(reinterpret_cast(aligned) % alignment == 0); @@ -211,7 +230,14 @@ int main(int argc, char** argv) { require(frees == 6); _ZdlPvmSt11align_val_t_nid_postfix(nullptr, 37, 64); require(frees == 6); + require(reallocf_nid_postfix(storage.data(), 80) == storage.data() && lastSize == 80 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 16) == storage.data() && lastSize == 16 && frees == 6); + require(reallocf_nid_postfix(storage.data(), 0) == nullptr && lastSize == 16 && frees == 7); fail = true; + *__error_nid_postfix() = 0; + require(reallocf_nid_postfix(storage.data(), 48) == nullptr && lastSize == 48 && frees == 8 && *__error_nid_postfix() == 12); + reject([] { ApplicationHeapReallocate_nid_no_patch(storage.data(), 48); }); + require(frees == 8); require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index da0721409..ccd01ed4a 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -18,6 +18,7 @@ std::int32_t APS5_VABI sceFiberReturnToThread(std::uint64_t, std::uint64_t*); std::int32_t APS5_VABI sceFiberGetSelf(FiberObject**); std::int32_t APS5_VABI sceFiberGetInfo(FiberObject*, FiberInfo*); std::int32_t APS5_VABI sceFiberGetThreadFramePointerAddress(std::uint64_t*); +std::int32_t APS5_VABI _sceFiberGetThreadFramePointerAddress(std::uint64_t*); std::int32_t APS5_VABI sceFiberStartContextSizeCheck(std::uint32_t); std::int32_t APS5_VABI sceFiberStopContextSizeCheck(void); } @@ -54,6 +55,9 @@ void APS5_VABI FirstEntry(std::uint64_t argOnInitialize, std::uint64_t argOnRun) std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "frame pointer null"); Require(sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "thread frame pointer"); + framePointer = 0; + Require(_sceFiberGetThreadFramePointerAddress(nullptr) == FiberErrorNull, "underscore frame pointer null"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == 0 && framePointer == g_threadFramePointer, "underscore thread frame pointer"); volatile double carried = 1.5; std::uint64_t received = 0; Require(sceFiberSwitch(g_second, 200, &received) == 0, "switch to second fiber"); @@ -124,6 +128,7 @@ int main() { std::uint64_t returned = 0; std::uint64_t framePointer = 0; Require(sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "frame pointer outside a fiber"); + Require(_sceFiberGetThreadFramePointerAddress(&framePointer) == FiberErrorPermission, "underscore frame pointer outside a fiber"); Require(RunFirst(&returned) == 0 && returned == 250, "run first until second returns"); Require(_mm_getcsr() == threadCsr, "thread MXCSR restored after the fibers"); std::thread([&] { diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index b6134b7b9..0b83a58c1 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -92,9 +92,7 @@ static bool CheckWidePrecision() { Require(snprintf_nid_postfix(output, sizeof(output), "%.*S", 3, u"\u00e9\u20ac") == 2 && std::strcmp(output, "\xc3\xa9") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "[%ls]", static_cast(nullptr)) == 8 && std::strcmp(output, "[(null)]") == 0); Require(snprintf_nid_postfix(output, sizeof(output), "a%lcb%C", 0, 0) == 4 && std::memcmp(output, "a\0b\0", 5) == 0); -#ifndef _WIN32 Require(FormatList(output, sizeof(output), "%%ls:%2$d:%1$d", 3, 7) == 7 && std::strcmp(output, "%ls:7:3") == 0); -#endif for (std::size_t capacity = 0; capacity <= 6; ++capacity) { std::memset(output, '!', sizeof(output)); int count = -1; @@ -126,6 +124,33 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(std::strcmp(buffer, "1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.3Lf %d %.1f", 1.125L, 7, 2.5); Require(std::strcmp(buffer, "1.125 7 2.5") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%'d|%'u|%'.2f|%'-8ld|%'+05d|%'g", 1234567, 4000000000u, 1234567.891, 1234567LL, 42, 1e6) == 50); + Require(std::strcmp(buffer, "1234567|4000000000|1234567.89|1234567 |+0042|1e+06") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'lld %'zu", 1234567890123LL, static_cast(9876543210)) == 24 && std::strcmp(buffer, "1234567890123 9876543210") == 0); + Require(FormatList(buffer, sizeof(buffer), "%'d %ls", 1234567, u"x") == 9 && std::strcmp(buffer, "1234567 x") == 0); + Require(sprintf_nid_postfix(buffer, "%2$s %1$d %2$s", 7, "x") == 5 && std::strcmp(buffer, "x 7 x") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "%3$.1Lf|%2$.2f|%1$hhd|%4$s", 257, 2.5, 1.5L, "end") == 14); + Require(std::strcmp(buffer, "1.5|2.50|1|end") == 0); + Require(sprintf_nid_postfix(buffer, "%9$d%8$d%7$d%6$d%5$d%4$d%3$d%2$d%1$d", 1, 2, 3, 4, 5, 6, 7, 8, 9) == 9 && std::strcmp(buffer, "987654321") == 0); + Require(FormatList(buffer, sizeof(buffer), "%11$d %10$g %9$g %8$g %7$g %6$g %5$g %4$g %3$g %2$g %1$g", 1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11) == 23 && + std::strcmp(buffer, "11 10 9 8 7 6 5 4 3 2 1") == 0); + Require(snprintf_nid_postfix(buffer, sizeof(buffer), "[%1$*2$.*3$f][%1$-*2$.1f][%4$*2$c]", 3.5, 7, 2, 'z') == 27); + Require(std::strcmp(buffer, "[ 3.50][3.5 ][ z]") == 0); + Require(sprintf_nid_postfix(buffer, "[%2$*1$d][%2$.*1$d]", -4, 5) == 9 && std::strcmp(buffer, "[5 ][5]") == 0); + Require(sprintf_nid_postfix(buffer, "[%1$*2$d]", 5, 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$ls|%1$C|%3$d", 0x41, u"\u00e9", 7) == 6 && std::strcmp(buffer, "\xc3\xa9|A|7") == 0); + Require(FormatList(buffer, sizeof(buffer), "%2$d %d %ls|%*5$d|%1$d", 1, 2, 3, u"x", 4) == 12 && std::strcmp(buffer, "2 3 x| 4|1") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%*3$d]%ls", 5, u"", 3) == 5 && std::strcmp(buffer, "[ 5]") == 0); + const char* const invalidPositions[] = {"%0$d%ls", "%100$d%ls", "%*0$d%ls"}; + for (const char* format : invalidPositions) { + bool threw = false; + try { + FormatList(buffer, sizeof(buffer), format, 1, u""); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + } char expected[1024]; std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); @@ -155,6 +180,10 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(snprintf_nid_postfix(buffer, 1, "%d", 123) == 3 && buffer[0] == 0); Require(sprintf_nid_postfix(buffer, "%hhd %hhu %hd %hu %%", 255, 257, 65535, 65537) == 11); Require(std::strcmp(buffer, "-1 1 -1 1 %") == 0); + Require(sprintf_nid_postfix(buffer, "[%#.0o][%#.o][%#5.0o][%-#4.0o][%#.*o][%#.0o][%.0o][%#o][%#.3o]", 0u, 0u, 0u, 0u, 0, 0u, 8u, 0u, 0u, 0u) == 37); + Require(std::strcmp(buffer, "[0][0][ 0][0 ][0][010][][0][000]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0lo][%#.0hho][%#.0x]", 0ull, 0u, 0u) == 8 && std::strcmp(buffer, "[0][0][]") == 0); + Require(FormatList(buffer, sizeof(buffer), "[%#.0o][%ls]", 0u, u"x") == 6 && std::strcmp(buffer, "[0][x]") == 0); Require(sprintf_nid_postfix(buffer, "%d %d %d %d %d %d", 1, 2, 3, 4, 5, 6) == 11); Require(std::strcmp(buffer, "1 2 3 4 5 6") == 0); int first = 0, second = 0, third = 0, fourth = 0, fifth = 0, sixth = 0; diff --git a/core/libs/tests/GuestAcm.cpp b/core/libs/tests/GuestAcm.cpp new file mode 100644 index 000000000..e360f8444 --- /dev/null +++ b/core/libs/tests/GuestAcm.cpp @@ -0,0 +1,41 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceAcmContextCreate(std::uint32_t*); +int APS5_VABI sceAcmContextDestroy(std::uint32_t); +int APS5_VABI sceAcmBatchStartBuffer(std::uint32_t, const void*, std::size_t, void*, std::uint32_t*); +int APS5_VABI sceAcmBatchStartBuffers(std::uint32_t, std::uint32_t, const void* const[], void*, std::uint32_t*); +int APS5_VABI sceAcmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAcm_ConvReverb_SharedInput(void); +int APS5_VABI sceAcm_FFT(void); +int APS5_VABI sceAcm_Panner(void); +} + +static void Require(bool value) { + if (!value) std::abort(); +} + +int main() { + std::uint32_t context = 0; + Require(sceAcmContextCreate(nullptr) != 0); + Require(sceAcmContextCreate(&context) == 0 && context != 0); + + std::uint32_t batch = 0; + Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, nullptr) != 0); + Require(sceAcmBatchStartBuffer(context, nullptr, 0, nullptr, &batch) == 0 && batch != 0); + Require(sceAcmBatchWait(context, batch, 0) == 0); + + std::uint32_t batch2 = 0; + Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, nullptr) != 0); + Require(sceAcmBatchStartBuffers(context, 0, nullptr, nullptr, &batch2) == 0 && batch2 != 0); + Require(sceAcmBatchWait(context, batch2, 0) == 0); + + Require(sceAcm_ConvReverb_SharedInput() == 0); + Require(sceAcm_FFT() == 0); + Require(sceAcm_Panner() == 0); + + Require(sceAcmContextDestroy(context) == 0); +} diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 458ec1b13..675da4ed6 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -31,4 +31,49 @@ int main() { Require(__inet_pton_nid_postfix(2, "256.1.2.3", address.data()) == 0); Require(__inet_pton_nid_postfix(28, "::ffff:192.0.2.17", address.data()) == 1); Require(address[10] == 255 && address[11] == 255 && address[12] == 192 && address[15] == 17); + + for (const char* rejected : {"01.2.3.4", "1.2.3", "1.2.3.4.5", "1.2.3.4.", " 1.2.3.4", "0x1.2.3.4", "1..3.4", "1.2.3.-4"}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(2, rejected, address.data()) == 0); + Require(address[0] == 0xA5); + } + for (const char* rejected : {"::ffff:01.2.3.4", "::ffff:1.2.3.04", "1:2:3:4:5:6:7:8::", "::1:2:3:4:5:6:7:8", "1::2::3", ":::", ":1::", "1::2:", ":1:2:3:4:5:6:7", + "1:2:3:4:5:6:7", "1:2:3:4:5:6:7:8:9", "12345::", "fe80::1%eth0", "1.2.3.4::", "::1.2.3.4:1", "::g", "1:2:3:4:5:6:1.2.3.4:7", + "1:2:3:4:5:6:7:1.2.3.4", ""}) { + address.fill(0xA5); + Require(__inet_pton_nid_postfix(28, rejected, address.data()) == 0); + Require(address[0] == 0xA5 && address[15] == 0xA5); + } + Require(__inet_pton_nid_postfix(28, "1:2:3:4:5:6:7::", address.data()) == 1); + Require(address[13] == 7 && address[14] == 0 && address[1] == 1); + Require(__inet_pton_nid_postfix(28, "ABCD:ef01::1", address.data()) == 1); + Require(address[0] == 0xAB && address[1] == 0xCD && address[2] == 0xEF && address[3] == 0x01 && address[15] == 1); + + struct Text { const char* bytesAsText; const char* expected; }; + const Text samples[] = { + {"::ffff:0.0.0.1", "::ffff:0.0.0.1"}, {"::ffff:0:1:0", "::ffff:0:1:0"}, {"::1", "::1"}, {"::", "::"}, {"1::", "1::"}, + {"::2", "::2"}, {"::1.2.3.4", "::1.2.3.4"}, {"1:0:0:2:0:0:0:3", "1:0:0:2::3"}, {"1:0:2:0:3:0:4:0", "1:0:2:0:3:0:4:0"}, + {"0:0:1:0:0:2:0:0", "::1:0:0:2:0:0"}, {"::ffff:1.2.3.4", "::ffff:1.2.3.4"}, {"2001:DB8::17", "2001:db8::17"}}; + for (const Text& sample : samples) { + Require(__inet_pton_nid_postfix(28, sample.bytesAsText, address.data()) == 1); + char output[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), output, sizeof(output)) == output); + Require(std::strcmp(output, sample.expected) == 0); + const auto length = std::strlen(sample.expected); + std::memset(output, 'x', sizeof(output)); + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length + 1)) == output); + Require(std::strcmp(output, sample.expected) == 0); + std::memset(output, 'x', sizeof(output)); + *__error_nid_postfix() = 0; + Require(__inet_ntop_nid_postfix(28, address.data(), output, static_cast(length)) == nullptr); + Require(*__error_nid_postfix() == 28 && output[0] == 'x'); + } + address.fill(0xFF); + char widest[64]{}; + Require(__inet_ntop_nid_postfix(28, address.data(), widest, sizeof(widest)) == widest); + Require(std::strcmp(widest, "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") == 0); + const unsigned char fourth[4] = {255, 0, 7, 100}; + Require(__inet_ntop_nid_postfix(2, fourth, widest, 12) == widest && std::strcmp(widest, "255.0.7.100") == 0); + Require(__inet_ntop_nid_postfix(2, fourth, widest, 11) == nullptr && *__error_nid_postfix() == 28); + Require(__inet_ntop_nid_postfix(10, fourth, widest, sizeof(widest)) == nullptr && *__error_nid_postfix() == 47); } diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 0bbc3a952..e1a51c74e 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -22,6 +22,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret); int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size); void APS5_VABI sceKernelAioInitializeParam(void* param); +int APS5_VABI sceKernelAioSetParam(KernelAioSchedulingParam* param, std::int32_t schedulingWindowSize, std::int32_t delayedCountLimit, std::uint32_t enableSplit, std::uint32_t splitSize, std::uint32_t splitChunkSize); int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioPollRequest(std::int32_t id, std::int32_t* state); @@ -48,7 +49,35 @@ int main() { const auto file = root / "data.bin"; const std::string seed = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; { std::ofstream stream(file, std::ios::binary); stream << seed; } - Check(sceKernelAioInitializeImpl(nullptr, 0) == 0); + static_assert(sizeof(KernelAioSchedulingParam) == 20); + std::array schedulingParams{}; + schedulingParams.fill({7, 7, 7, 7, 7}); + Check(sceKernelAioSetParam(&schedulingParams[1], 64, 16, 1, 0x200000, 0x40000) == 0); + Check(schedulingParams[1].schedulingWindowSize == 64 && schedulingParams[1].delayedCountLimit == 16); + Check(schedulingParams[1].enableSplit == 1 && schedulingParams[1].splitSize == 0x200000 && schedulingParams[1].splitChunkSize == 0x40000); + Check(schedulingParams[0].schedulingWindowSize == 7 && schedulingParams[0].splitChunkSize == 7); + Check(schedulingParams[2].schedulingWindowSize == 7 && schedulingParams[2].splitChunkSize == 7); + Check(sceKernelAioSetParam(&schedulingParams[2], 128, 128, 0, 0x1000000, 0x1000000) == 0); + Check(schedulingParams[2].schedulingWindowSize == 128 && schedulingParams[2].delayedCountLimit == 128); + Check(schedulingParams[2].enableSplit == 0 && schedulingParams[2].splitSize == 0x1000000 && schedulingParams[2].splitChunkSize == 0x1000000); + const auto setParamThrows = [&](std::int32_t window, std::int32_t delayed, std::uint32_t split, std::uint32_t size, std::uint32_t chunk) { + KernelAioSchedulingParam untouchedParam{7, 7, 7, 7, 7}; + bool thrown = false; + try { sceKernelAioSetParam(&untouchedParam, window, delayed, split, size, chunk); } catch (const std::exception&) { thrown = true; } + return thrown && untouchedParam.schedulingWindowSize == 7 && untouchedParam.delayedCountLimit == 7 && + untouchedParam.enableSplit == 7 && untouchedParam.splitSize == 7 && untouchedParam.splitChunkSize == 7; + }; + Check(setParamThrows(129, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(-1, 32, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 129, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, -1, 1, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 2, 0x100000, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x1000001, 0x100000)); + Check(setParamThrows(32, 32, 1, 0x100000, 0x1000001)); + bool nullThrew = false; + try { sceKernelAioSetParam(nullptr, 32, 32, 1, 0x100000, 0x100000); } catch (const std::invalid_argument&) { nullThrew = true; } + Check(nullThrew); + Check(sceKernelAioInitializeImpl(schedulingParams.data(), static_cast(sizeof(schedulingParams))) == 0); int initParam = 0; sceKernelAioInitializeParam(&initParam); const int fd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDONLY, 0); diff --git a/core/libs/tests/GuestAmmPlacement.cpp b/core/libs/tests/GuestAmmPlacement.cpp new file mode 100644 index 000000000..e52fe22b5 --- /dev/null +++ b/core/libs/tests/GuestAmmPlacement.cpp @@ -0,0 +1,40 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAmprAmmGetVirtualAddressRanges(std::uint64_t*, std::uint64_t*, std::uint64_t*, std::uint64_t*); +} + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int main() { + try { + std::uintptr_t arenaBase = 0; + std::size_t arenaBytes = 0; + GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); + Require(arenaBase != 0 && arenaBytes != 0, "the guest arena is unavailable"); + const std::uint64_t arenaEnd = arenaBase + arenaBytes; + constexpr std::uint64_t occupied = 4ull << 20; + GuestArena::GuestArenaMarkUsed_nid_postfix(reinterpret_cast(arenaEnd - occupied), occupied); + + std::uint64_t start = 0; + std::uint64_t end = 0; + std::uint64_t multimapStart = 0; + std::uint64_t multimapEnd = 0; + Require(sceAmprAmmGetVirtualAddressRanges(&start, &end, &multimapStart, &multimapEnd) == 0, "AMM ranges were not reported"); + Require(end == multimapStart && multimapEnd - start == 64ull << 30, "AMM ranges are not two adjacent 32 GiB ranges"); + Require(start >= arenaBase && multimapEnd <= arenaEnd - occupied, "AMM ranges overlap the used top of the guest arena"); + Require(GuestArena::GuestArenaHighestFree_nid_no_patch(64ull << 30, 2ull << 20) < start, "AMM ranges are not at the top of the free guest arena"); + std::puts("AMM ranges placed below the used top of the guest arena"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 43b223fac..d2a646ac1 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -3,6 +3,7 @@ #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include +#include #include #include #include @@ -10,6 +11,8 @@ #include #include #include +#include +#include #include #include @@ -33,6 +36,8 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker(); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char*); std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char*, std::uint32_t); int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); +int APS5_VABI sceKernelAprSubmitCommandBufferAndGetId(const Apr::CommandBufferObject*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceKernelAprWaitCommandBuffer(std::uint32_t); int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWaitOnCounter(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t, std::uint8_t, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteCounterOnCompletion(Apr::CommandBufferObject*, std::uint8_t, std::uint32_t); @@ -301,6 +306,53 @@ void TestWaits() { Require(submitted.get() == 0 && done == 1); } +void TestWaitSatisfiedAfterSubmission() { + Recorder first; + Recorder second; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + alignas(8) std::uint64_t later = 0; + Require(sceAmprCommandBufferWaitOnAddress(&first.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&first.buffer, &done, 1) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&second.buffer, &later, 2) == 0); + std::uint32_t firstId = 0; + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBufferAndGetId(&first.buffer, 0, &firstId); }); + const bool returned = submitted.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(submitted.get() == 0 && firstId != 0); + std::uint32_t secondId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&second.buffer, 0, &secondId) == 0 && secondId > firstId); + Require(std::atomic_ref(done).load(std::memory_order_acquire) == 0); + Require(std::atomic_ref(later).load(std::memory_order_acquire) == 0); + std::atomic_ref(flag).store(1, std::memory_order_release); + Require(sceKernelAprWaitCommandBuffer(secondId) == 0); + Require(done == 1 && later == 2); + Require(sceKernelAprWaitCommandBuffer(firstId) == 0); +} + +void TestAmmNotBehindAprWait() { + Recorder reader; + Recorder maps; + alignas(8) std::uint64_t flag = 0; + alignas(8) std::uint64_t done = 0; + Require(sceAmprCommandBufferWaitOnAddress(&reader.buffer, &flag, 1, 0, 0) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&reader.buffer, &done, 1) == 0); + Require(sceAmprAmmCommandBufferConstructor(&maps.buffer) == 0); + Require(sceAmprCommandBufferWriteAddressOnCompletion(&maps.buffer, &flag, 1) == 0); + std::uint32_t readerId = 0; + Require(sceKernelAprSubmitCommandBufferAndGetId(&reader.buffer, 0, &readerId) == 0 && readerId != 0); + std::uint32_t mapsId = 0; + Require(sceAmprAmmSubmitCommandBuffer3(maps.memory.data(), maps.Offset(), 0, &mapsId) == 0 && mapsId != 0); + auto completed = std::async(std::launch::async, [&]() { return sceAmprAmmWaitCommandBufferCompletion(mapsId); }); + const bool returned = completed.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + if (!returned) std::atomic_ref(flag).store(1, std::memory_order_release); + Require(returned); + Require(completed.get() == 0); + Require(sceKernelAprWaitCommandBuffer(readerId) == 0); + Require(done == 1); +} + void TestCounters() { Recorder recorder; alignas(8) std::uint64_t single = 0; @@ -576,9 +628,47 @@ void TestResolveFailures() { Require(sceKernelAprResolveFilepathsToIdsAndFileSizesForEach(paths, 2, ids, sizes, results) == 1); Require(results[0] == 0 && results[1] == static_cast(0x80020002)); Require(ids[1] == 0xffffffffu && sizes[0] == 4 && sizes[1] == 0); + for (const char* invalid : {"ampr_resolve_missing.bin/../ampr_resolve_existing.bin", "ampr_resolve_existing.bin/../ampr_resolve_existing.bin"}) { + Require(sceKernelAprResolveFilepathsToIds(&invalid, 1, ids, &failed) == -1); + Require(*__error_nid_postfix() == 2 && failed == 0 && ids[0] == 0xffffffffu); + } std::remove(paths[0]); } +void TestMalformedCommandSizes() { + const auto requireFailure = [](Apr::Opcode opcode, std::uint32_t recordBytes, std::uint32_t bufferBytes, const char* expected) { + std::array memory{}; + const Apr::CommandHeader header{opcode, recordBytes}; + std::memcpy(memory.data(), &header, sizeof(header)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, bufferBytes, 1, bufferBytes, memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with(expected); + } + Require(rejected); + }; + + requireFailure(Apr::Opcode::ReadFile, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::WriteAddress, sizeof(Apr::CommandHeader), sizeof(Apr::CommandHeader), "APR: truncated command payload"); + requireFailure(Apr::Opcode::Nop, std::numeric_limits::max(), 2u * sizeof(Apr::CommandHeader), "APR: malformed command"); + + std::array memory{}; + const Apr::CommandHeader first{Apr::Opcode::Nop, sizeof(Apr::CommandHeader)}; + const Apr::CommandHeader overflowing{Apr::Opcode::Nop, std::numeric_limits::max()}; + std::memcpy(memory.data(), &first, sizeof(first)); + std::memcpy(memory.data() + sizeof(first), &overflowing, sizeof(overflowing)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 2, sizeof(memory), memory.data()}; + bool rejected = false; + try { + sceKernelAprSubmitCommandBuffer(&buffer, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).starts_with("APR: malformed command"); + } + Require(rejected); +} + void TestGatherScatter() { const char* path = "ampr_gather_scatter.bin"; { @@ -690,7 +780,7 @@ void TestAmmRangesAboveHintedMaps() { std::uintptr_t arenaBase = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arenaBase, &arenaBytes); - Require(multimapEnd == arenaBase + arenaBytes); + Require(start >= arenaBase && multimapEnd <= arenaBase + arenaBytes && heap + 4 * step <= start); for (std::uint64_t hint = heap + step; hint < heap + 4 * step; hint += step) { void* mapped = reinterpret_cast(hint); @@ -1045,6 +1135,8 @@ int main() { TestSubmission(); TestKernelEventQueue(); TestWaits(); + TestWaitSatisfiedAfterSubmission(); + TestAmmNotBehindAprWait(); TestCounters(); TestRejectedWaitsAndCounters(); TestNops(); @@ -1054,6 +1146,7 @@ int main() { TestConstructed(); TestZeroFilledBuffer(); TestResolveFailures(); + TestMalformedCommandSizes(); TestGatherScatter(); TestAmmRangesAboveHintedMaps(); TestAmm(); diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 81ea82946..6fa491548 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -24,6 +25,16 @@ int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_with_name_nid_postfix(void** handle, const char* name); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); +int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); } static constexpr int ThrdSuccess = 0; @@ -197,10 +208,119 @@ static void TestWcstombs() { Require(result == Failed); } +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +struct SignalContext { + void* condition = nullptr; + void* mutex = nullptr; + int waiting = 0; + int tokens = 0; + int consumed = 0; + int wakeups = 0; +}; + +static void Lock(SignalContext& context) { + Require(_Mtx_lock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void Unlock(SignalContext& context) { + Require(_Mtx_unlock_nid_postfix(&context.mutex) == ThrdSuccess); +} + +static void* APS5_VABI ConsumeToken(void* arg) { + auto& context = *static_cast(arg); + Lock(context); + ++context.waiting; + while (context.tokens == 0) { + Require(_Cnd_wait_nid_postfix(&context.condition, &context.mutex) == ThrdSuccess); + ++context.wakeups; + } + --context.tokens; + ++context.consumed; + Unlock(context); + return nullptr; +} + +template +static void WaitUntil(SignalContext& context, TPredicate predicate) { + for (;;) { + Lock(context); + const bool done = predicate(); + Unlock(context); + if (done) return; + std::this_thread::yield(); + } +} + +static void Signal(SignalContext& context) { + Lock(context); + ++context.tokens; + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + Unlock(context); +} + +static void TestConditionSignal() { + SignalContext context; + context.condition = reinterpret_cast(std::uintptr_t{1}); + Require(_Cnd_init_with_name_nid_postfix(&context.condition, "AmprSignal") == ThrdSuccess && context.condition != nullptr); + Require(_Mtx_init_nid_postfix(&context.mutex, 0) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&context.condition) == ThrdSuccess); + + const auto single = Start(ConsumeToken, &context); + WaitUntil(context, [&] { return context.waiting == 1; }); + Signal(context); + Require(_Thrd_join_nid_postfix(single, nullptr) == ThrdSuccess); + Require(context.consumed == 1 && context.tokens == 0); + + context.waiting = 0; + context.consumed = 0; + context.wakeups = 0; + const std::array pair{Start(ConsumeToken, &context), Start(ConsumeToken, &context)}; + WaitUntil(context, [&] { return context.waiting == 2; }); + Signal(context); + WaitUntil(context, [&] { return context.consumed == 1; }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Lock(context); + Require(context.consumed == 1 && context.wakeups == 1 && context.tokens == 0); + Unlock(context); + Signal(context); + for (auto thread : pair) Require(_Thrd_join_nid_postfix(thread, nullptr) == ThrdSuccess); + Require(context.consumed == 2 && context.tokens == 0); + Require(_Cnd_broadcast_nid_postfix(&context.condition) == ThrdSuccess); + + RequireThrows([] { _Cnd_signal_nid_postfix(nullptr); }); + void* empty = nullptr; + RequireThrows([&] { _Cnd_signal_nid_postfix(&empty); }); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.mutex); }); + RequireThrows([] { _Cnd_init_with_name_nid_postfix(nullptr, "AmprSignal"); }); + _Cnd_destroy_nid_postfix(&context.condition); + Require(context.condition == nullptr); + RequireThrows([&] { _Cnd_signal_nid_postfix(&context.condition); }); + + void* unnamed = nullptr; + Require(_Cnd_init_with_name_nid_postfix(&unnamed, nullptr) == ThrdSuccess && unnamed != nullptr); + Require(_Cnd_signal_nid_postfix(&unnamed) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&unnamed); + void* plain = nullptr; + Require(_Cnd_init_nid_postfix(&plain) == ThrdSuccess); + Require(_Cnd_signal_nid_postfix(&plain) == ThrdSuccess); + _Cnd_destroy_nid_postfix(&plain); + _Mtx_destroy_nid_postfix(&context.mutex); +} + int main() { TestThreadJoin(); TestThreadSleep(); TestSpinLock(); TestWcstombs(); + TestConditionSignal(); return 0; } diff --git a/core/libs/tests/GuestAtoi.cpp b/core/libs/tests/GuestAtoi.cpp new file mode 100644 index 000000000..0c4d77ddf --- /dev/null +++ b/core/libs/tests/GuestAtoi.cpp @@ -0,0 +1,55 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI atoi_nid_postfix(const char*); +int* APS5_VABI __error_nid_postfix(); +} + +int main() { + struct Case { + const char* text; + int value; + int error; + }; + constexpr int SavedError = 1234; + constexpr Case cases[] = { + {"0", 0, SavedError}, + {"+42", 42, SavedError}, + {" \t\n\r\f\v-0012tail", -12, SavedError}, + {"010", 10, SavedError}, + {"0x10", 0, SavedError}, + {"0b10", 0, SavedError}, + {"", 0, SavedError}, + {"words", 0, SavedError}, + {" +", 0, SavedError}, + {"2147483647", INT32_MAX, SavedError}, + {"-2147483648", INT32_MIN, SavedError}, + {"2147483648", INT32_MIN, SavedError}, + {"-2147483649", INT32_MAX, SavedError}, + {"4294967295", -1, SavedError}, + {"-4294967295", 1, SavedError}, + {"4294967296", 0, SavedError}, + {"-4294967296", 0, SavedError}, + {" \t+4294967297tail", 1, SavedError}, + {"9223372036854775807", -1, SavedError}, + {"-9223372036854775808", 0, SavedError}, + {"9223372036854775808", -1, 34}, + {"-9223372036854775809", 0, 34}, + {"999999999999999999999999999999999999", -1, 34}, + {"-999999999999999999999999999999999999", 0, 34}, + }; + bool correct = true; + for (const auto& test : cases) { + *__error_nid_postfix() = SavedError; + const int value = atoi_nid_postfix(test.text); + const int error = *__error_nid_postfix(); + if (value != test.value || error != test.error) { + std::fprintf(stderr, "atoi(%s): expected %d, errno %d; received %d, errno %d\n", + test.text, test.value, test.error, value, error); + correct = false; + } + } + return correct ? 0 : 1; +} diff --git a/core/libs/tests/GuestAudioIn.cpp b/core/libs/tests/GuestAudioIn.cpp index f3b3d3ff4..d9e957911 100644 --- a/core/libs/tests/GuestAudioIn.cpp +++ b/core/libs/tests/GuestAudioIn.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -6,12 +7,15 @@ #include #include #include +#include #include #include extern "C" { int APS5_VABI sceAudioInInit(); int APS5_VABI sceAudioInOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInHqOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); +int APS5_VABI sceAudioInAsyncOpen(int, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAudioInInput(int, void*); int APS5_VABI sceAudioInGetSilentState(int); int APS5_VABI sceAudioInClose(int); @@ -19,6 +23,16 @@ int APS5_VABI sceAudioInClose(int); static void Require(bool value) { if (!value) std::abort(); } +template +static bool ReportsNotImplemented(const char* name, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()) == std::string(name) + " not implemented"; + } + return false; +} + static void SetEnvironment(const char* name, const std::string& value) { #ifdef _WIN32 _putenv_s(name, value.c_str()); @@ -42,6 +56,22 @@ void TestValidation() { Require(sceAudioInOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 3) == invalidParam); Require(sceAudioInOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(sceAudioInHqOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInHqOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInHqOpen(user, 0, 0, 512, 48000, 2) == invalidSize); + Require(sceAudioInHqOpen(user, 0, 0, 128, 44100, 2) == invalidFreq); + Require(sceAudioInHqOpen(user, 0, 0, 128, 48000, 3) == invalidParam); + Require(sceAudioInAsyncOpen(user, 2, 0, 128, 48000, 2) == invalidType); + Require(sceAudioInAsyncOpen(user, 0, 1, 128, 48000, 2) == invalidParam); + Require(sceAudioInAsyncOpen(user, 0, 0, 385, 48000, 2) == invalidSize); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 44100, 2) == invalidFreq); + Require(sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 0) == invalidParam); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 256, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 16000, 2); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 384, 48000, 2); })); + Require(ReportsNotImplemented("sceAudioInOpen", [] { sceAudioInOpen(user, 0, 0, 256, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInHqOpen", [] { sceAudioInHqOpen(user, 0, 0, 128, 48000, 0x10); })); + Require(ReportsNotImplemented("sceAudioInAsyncOpen", [] { sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 0x10); })); } void TestCapture() { @@ -66,6 +96,11 @@ void TestCapture() { Require(sceAudioInInput(handle, block.data()) == 256); for (std::size_t i = 0; i < 256 * 2; ++i) Require(block[i] == 0); Require(sceAudioInClose(handle) == 0); + const int async = sceAudioInAsyncOpen(user, 0, 0, 256, 48000, 2); + Require(async == handle && sceAudioInGetSilentState(async) == 0); + Require(sceAudioInInput(async, block.data()) == 256); + Require(std::equal(block.begin(), block.end() - 1, recorded.begin()) && block.back() == 0x5555); + Require(sceAudioInClose(async) == 0); std::filesystem::remove(path); } @@ -87,8 +122,30 @@ void TestNoDevice() { for (std::size_t i = 0; i < 256 * 8; ++i) Require(buffer[i] == 0); Require(buffer[256 * 8] == 0xCC); Require(sceAudioInInput(handle, nullptr) == 0); - for (int port = 1; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); + const int hq = sceAudioInHqOpen(user, 0, 0, 128, 48000, 2); + const int async = sceAudioInAsyncOpen(user, 1, 0, 256, 16000, 0x11); + Require(hq > 0 && async > 0 && hq != handle && async != handle && hq != async); + Require(sceAudioInGetSilentState(hq) == 1 && sceAudioInGetSilentState(async) == 1); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(hq, buffer.data()) == 128); + for (std::size_t i = 0; i < 128 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[128 * 4] == 0xCC); + std::fill(buffer.begin(), buffer.end(), 0xCC); + Require(sceAudioInInput(async, buffer.data()) == 256); + for (std::size_t i = 0; i < 256 * 4; ++i) Require(buffer[i] == 0); + Require(buffer[256 * 4] == 0xCC); + Require(sceAudioInInput(async, nullptr) == 0); + for (int port = 3; port < 8; ++port) Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) > 0); Require(sceAudioInOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInAsyncOpen(user, 1, 0, 128, 48000, 1) == portFull); + Require(sceAudioInClose(hq) == 0); + Require(sceAudioInClose(hq) == invalidHandle); + Require(sceAudioInGetSilentState(hq) == invalidHandle); + Require(sceAudioInAsyncOpen(user, 0, 0, 128, 48000, 2) == hq); + Require(sceAudioInClose(async) == 0); + Require(sceAudioInInput(async, buffer.data()) == invalidHandle); + Require(sceAudioInHqOpen(user, 1, 0, 128, 48000, 0x12) == async); Require(sceAudioInClose(handle) == 0); Require(sceAudioInClose(handle) == invalidHandle); Require(sceAudioInInput(handle, buffer.data()) == invalidHandle); diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index dad55fead..d6ca10995 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -723,6 +723,8 @@ void TestPlayback() { Check(sceAvPlayerStreamCount(player) == OperationFailed, "stream count without a source"); Check(!sceAvPlayerIsActive(player), "active without a source"); Check(sceAvPlayerAddSource(player, "/app0/missing.mp4") == OperationFailed, "missing file accepted"); + Check(sceAvPlayerAddSource(player, "/app0/missing/../avplayer.mp4") == OperationFailed, "missing parent accepted"); + Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4/../avplayer.mp4") == OperationFailed, "file parent accepted"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == 0, "add source failed"); Check(sceAvPlayerAddSource(player, "/app0/avplayer.mp4") == OperationFailed, "second source accepted"); Check(WaitFor([&] { return events.Seen(EventReady); }), "ready event missing"); diff --git a/core/libs/tests/GuestAvailableCpumask.cpp b/core/libs/tests/GuestAvailableCpumask.cpp index 6485fcffe..cf40f562b 100644 --- a/core/libs/tests/GuestAvailableCpumask.cpp +++ b/core/libs/tests/GuestAvailableCpumask.cpp @@ -198,6 +198,31 @@ int main() { Require(changed && restored && created && joined && inherited[0] == 0 && inherited[1] == last); Require(explicitSet && explicitCreated && explicitJoined && explicitAffinity[0] == 0 && explicitAffinity[1] == available); } + + std::uint64_t processSet[4] = {~0ull, ~0ull, ~0ull, ~0ull}; + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, sizeof(processSet), processSet) == 0); + Require(processSet[0] == available && processSet[1] == 0 && processSet[2] == 0 && processSet[3] == 0); + processSet[0] = 0; + Require(cpuset_getaffinity_nid_postfix(3, 2, getpid_nid_postfix(), 8, processSet) == 0 && processSet[0] == available); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 4, processSet) == -1 && *__error_nid_postfix() == GUEST_ERANGE); + Require(cpuset_getaffinity_nid_postfix(3, 2, -1, 8, nullptr) == -1 && *__error_nid_postfix() == GUEST_EFAULT); + if (first != last) { + std::promise park; + auto parked = park.get_future(); + Pthread pinned = nullptr; + Require(scePthreadCreate(&pinned, &attr, Worker, &parked, nullptr) == SCE_OK); + Require(scePthreadSetaffinity(pinned, first) == SCE_OK); + Require(scePthreadSetaffinity(scePthreadSelf(), last) == SCE_OK); + std::uint64_t unionSet[1] = {0}; + const int unionResult = cpuset_getaffinity_nid_postfix(3, 2, -1, 8, unionSet); + std::uint64_t ownSet[1] = {0}; + const int ownResult = cpuset_getaffinity_nid_postfix(3, 1, -1, 8, ownSet); + Require(scePthreadSetaffinity(scePthreadSelf(), available) == SCE_OK); + park.set_value(); + Require(scePthreadJoin(pinned, nullptr) == SCE_OK); + Require(unionResult == 0 && unionSet[0] == (first | last)); + Require(ownResult == 0 && ownSet[0] == last); + } Require(scePthreadAttrDestroy(&attr) == SCE_OK); Require(ReadCpuset(pthread_self_nid_postfix()) == available); diff --git a/core/libs/tests/GuestCondTimedwait.cpp b/core/libs/tests/GuestCondTimedwait.cpp index 310812871..5a9190e1e 100644 --- a/core/libs/tests/GuestCondTimedwait.cpp +++ b/core/libs/tests/GuestCondTimedwait.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include #include @@ -10,9 +11,12 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +int APS5_VABI pthread_cond_reltimedwait_np_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); int APS5_VABI __cxa_atexit_nid_postfix(void (APS5_VABI *)(void*), void*, void*); void APS5_VABI __pthread_cxa_finalize_nid_postfix(void*); unsigned int APS5_VABI sceKernelSleep(unsigned int seconds); @@ -23,6 +27,9 @@ static void APS5_VABI Cleanup(void*) { cleanupAt = 0; } static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); +static constexpr int POSIX_EPERM = 1; +static constexpr int POSIX_ETIMEDOUT = 60; static void Require(bool value) { if (!value) std::abort(); } @@ -39,7 +46,67 @@ static void* APS5_VABI Contender(void* arg) { return nullptr; } +struct Signalled { + PthreadMutex mutex = nullptr; + PthreadCond cond = nullptr; + bool ready = false; + int trylock = SCE_OK; +}; + +static void* APS5_VABI Trylocker(void* arg) { + auto& signalled = *static_cast(arg); + signalled.trylock = scePthreadMutexTrylock(&signalled.mutex); + return nullptr; +} + +static void* APS5_VABI Signaller(void* arg) { + auto& signalled = *static_cast(arg); + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + signalled.ready = true; + Require(scePthreadCondSignal(&signalled.cond) == SCE_OK); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + return nullptr; +} + +static void RequireHeldByCaller(Signalled& signalled) { + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Trylocker, &signalled, nullptr) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(signalled.trylock == SCE_KERNEL_ERROR_EBUSY); +} + +static void RelativeWait() { + Signalled signalled; + Require(scePthreadMutexInit(&signalled.mutex, nullptr, nullptr) == SCE_OK); + Require(scePthreadCondInit(&signalled.cond, nullptr, nullptr) == SCE_OK); + + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 1000) == POSIX_EPERM); + + Require(scePthreadMutexLock(&signalled.mutex) == SCE_OK); + const auto start = std::chrono::steady_clock::now(); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 50000) == POSIX_ETIMEDOUT); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40)); + RequireHeldByCaller(signalled); + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 0) == POSIX_ETIMEDOUT); + RequireHeldByCaller(signalled); + + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Signaller, &signalled, nullptr) == SCE_OK); + const auto waitStart = std::chrono::steady_clock::now(); + while (!signalled.ready) + Require(pthread_cond_reltimedwait_np_nid_postfix(&signalled.cond, &signalled.mutex, 10000000) == 0); + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(5)); + RequireHeldByCaller(signalled); + Require(scePthreadMutexUnlock(&signalled.mutex) == SCE_OK); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + Require(scePthreadCondDestroy(&signalled.cond) == SCE_OK); + Require(scePthreadMutexDestroy(&signalled.mutex) == SCE_OK); +} + int main() { + RelativeWait(); + Context context; PthreadCond cond = nullptr; Require(scePthreadMutexInit(&context.mutex, nullptr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestContentExport.cpp b/core/libs/tests/GuestContentExport.cpp index 12038531a..ef295d6ec 100644 --- a/core/libs/tests/GuestContentExport.cpp +++ b/core/libs/tests/GuestContentExport.cpp @@ -1,25 +1,41 @@ #include "SceTypes.hpp" +#include #include +#include #include extern "C" int APS5_VABI sceContentExportInit2(const ContentExportInitParam2* init_param); extern "C" int APS5_VABI sceContentExportTerm(void); +extern "C" int APS5_VABI sceContentExportStart(void); +extern "C" int APS5_VABI sceContentExportFinish(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportGetProgress(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportCancel(std::int32_t expId); +extern "C" int APS5_VABI sceContentExportFromDataWithThumbnail(std::int32_t expId, const ContentExportParam* param, const ContentExportCallbackParam* contentCallbackParam, + const char* thumbnailType, const ContentExportCallbackParam* thumbnailCallbackParam, char* path, std::size_t pathBufferLength); namespace { void Require(bool value) { if (!value) std::abort(); } -template +template bool Throws(TAction action) { try { action(); - } catch (const std::logic_error&) { + } catch (const TException&) { return true; } return false; } int allocator = 0; +char path[1024]{}; + +void Export(std::int32_t expId) { + ContentExportParam param{}; + ContentExportCallbackParam content{}; + ContentExportCallbackParam thumbnail{}; + sceContentExportFromDataWithThumbnail(expId, ¶m, &content, "image/png", &thumbnail, path, sizeof(path)); +} } @@ -27,6 +43,8 @@ int main() { ContentExportInitParam2 param{&allocator, &allocator, nullptr, 0x10000, 0, 0}; Require(Throws([] { sceContentExportInit2(nullptr); })); Require(Throws([] { sceContentExportTerm(); })); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportFinish(1); })); ContentExportInitParam2 missing = param; missing.free_func = nullptr; Require(Throws([&] { sceContentExportInit2(&missing); })); @@ -38,5 +56,33 @@ int main() { Require(sceContentExportTerm() == 0); Require(Throws([] { sceContentExportTerm(); })); Require(sceContentExportInit2(¶m) == 0); + + std::set sessions; + for (int i = 0; i < 10; ++i) { + int session = sceContentExportStart(); + Require(session > 0); + Require(sessions.insert(session).second); + } + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([] { sceContentExportTerm(); })); + int first = *sessions.begin(); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(path[0] == 0); + Require(sceContentExportFinish(first) == 0); + Require(Throws([&] { sceContentExportFinish(first); })); + Require(Throws([&] { sceContentExportGetProgress(first); })); + Require(Throws([&] { sceContentExportCancel(first); })); + Require(Throws([&] { Export(first); })); + Require(Throws([] { sceContentExportFinish(0); })); + Require(Throws([] { sceContentExportFinish(-1); })); + sessions.erase(first); + int reopened = sceContentExportStart(); + Require(reopened > 0 && reopened != first); + Require(sessions.insert(reopened).second); + for (int session : sessions) Require(sceContentExportFinish(session) == 0); Require(sceContentExportTerm() == 0); + Require(Throws([] { sceContentExportStart(); })); + Require(Throws([&] { sceContentExportFinish(reopened); })); } diff --git a/core/libs/tests/GuestContentUnimplemented.cpp b/core/libs/tests/GuestContentUnimplemented.cpp index 00a291f52..549946300 100644 --- a/core/libs/tests/GuestContentUnimplemented.cpp +++ b/core/libs/tests/GuestContentUnimplemented.cpp @@ -6,9 +6,6 @@ extern "C" { int APS5_VABI sceContentDeleteById(); -int APS5_VABI sceContentExportGetProgress(); -int APS5_VABI sceContentExportCancel(); -int APS5_VABI sceContentExportFromDataWithThumbnail(); int APS5_VABI sceContentSearchGetContentLastUpdateId(); int APS5_VABI sceContentSearchOpenMetadataByContentId(); int APS5_VABI sceContentSearchGetTotalContentSize(); @@ -34,9 +31,6 @@ bool ReportsNotImplemented(const char* name, int (APS5_VABI *function)()) { int main() { bool correct = true; correct &= ReportsNotImplemented("sceContentDeleteById", sceContentDeleteById); - correct &= ReportsNotImplemented("sceContentExportGetProgress", sceContentExportGetProgress); - correct &= ReportsNotImplemented("sceContentExportCancel", sceContentExportCancel); - correct &= ReportsNotImplemented("sceContentExportFromDataWithThumbnail", sceContentExportFromDataWithThumbnail); correct &= ReportsNotImplemented("sceContentSearchGetContentLastUpdateId", sceContentSearchGetContentLastUpdateId); correct &= ReportsNotImplemented("sceContentSearchOpenMetadataByContentId", sceContentSearchOpenMetadataByContentId); correct &= ReportsNotImplemented("sceContentSearchGetTotalContentSize", sceContentSearchGetTotalContentSize); diff --git a/core/libs/tests/GuestDatagramReceive.cpp b/core/libs/tests/GuestDatagramReceive.cpp new file mode 100644 index 000000000..22cf4caaf --- /dev/null +++ b/core/libs/tests/GuestDatagramReceive.cpp @@ -0,0 +1,146 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +int APS5_VABI sceNetSocketClose(int); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::exit(1); + } +} + +static void Check(bool net, bool from, int family) { + const auto create = [&] { return net ? sceNetSocket(nullptr, family, 2, 17) : socket_nid_postfix(family, 2, 17); }; + const int receiver = create(), sender = create(); + Require(receiver >= 0 && sender >= 0, "create UDP sockets"); + std::array destination{}; + const std::uint32_t addressSize = family == 2 ? 16 : 28; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const auto bind = [&](int descriptor) { + return net ? sceNetBind_nid_postfix(descriptor, destination.data(), addressSize) : + bind_nid_postfix(descriptor, destination.data(), addressSize); + }; + Require(bind(receiver) == 0 && bind(sender) == 0, "bind loopback sockets"); + auto senderAddress = destination; + std::uint32_t size = addressSize; + Require((net ? sceNetGetsockname(sender, senderAddress.data(), &size) : + getsockname_nid_postfix(sender, senderAddress.data(), &size)) == 0, "get sender address"); + size = addressSize; + Require((net ? sceNetGetsockname(receiver, destination.data(), &size) : + getsockname_nid_postfix(receiver, destination.data(), &size)) == 0, "get receiver address"); + Require((net ? sceNetConnect(receiver, senderAddress.data(), addressSize) : + connect_nid_postfix(receiver, senderAddress.data(), addressSize)) == 0, "connect receiver"); + const int enabled = 1; + Require((net ? sceNetSetsockopt(receiver, 0xffff, 0x1200, &enabled, sizeof(enabled)) : + fcntl_nid_postfix(receiver, 4, 6)) == 0, "make receiver nonblocking"); + int* error = net ? sceNetErrnoLoc() : __error_nid_postfix(); + const auto receive = [&](void* buffer, std::size_t length, int flags, void* address, std::uint32_t* addressLength) { + if (net) return from ? sceNetRecvfrom(receiver, buffer, length, flags, address, addressLength) : + sceNetRecv(receiver, buffer, length, flags); + return from ? recvfrom_nid_postfix(receiver, buffer, length, flags, address, addressLength) : + recv_nid_postfix(receiver, buffer, length, flags); + }; + const auto send = [&](const char* data, std::size_t length) { + Require((net ? sceNetSendto(sender, data, length, 0, destination.data(), addressSize) : + sendto_nid_postfix(sender, data, length, 0, destination.data(), addressSize)) == + static_cast(length), "send datagram"); + }; + const char message[] = "UDP header/body"; + for (const std::size_t capacity : {std::size_t{4}, sizeof(message), sizeof(message) + 4, std::size_t{0}}) { + send(message, sizeof(message)); + std::array buffer{}; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(buffer.data(), buffer.size(), 2, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == sizeof(message), "wait for complete datagram"); + for (const int flags : {2, 0}) { + buffer.fill('!'); + std::array address{}; + address.fill(0xa5); + size = address.size(); + *error = 123; + result = receive(buffer.data() + 1, capacity, flags, address.data(), &size); + const auto expected = std::min(capacity, sizeof(message)); + if (result != static_cast(expected)) { + std::fprintf(stderr, "%s%s IPv%d capacity=%zu flags=%d: expected %zu, got %lld errno=%d\n", + net ? "sceNetRecv" : "recv", from ? "from" : "", family == 2 ? 4 : 6, + capacity, flags, expected, static_cast(result), *error); + std::exit(1); + } + Require(*error == 123, "successful receive preserves errno"); + Require(buffer[0] == '!' && std::memcmp(buffer.data() + 1, message, expected) == 0 && + std::all_of(buffer.begin() + 1 + expected, buffer.end(), [](char value) { return value == '!'; }), + "copied prefix and buffer guards"); + if (from) { + Require(size == addressSize && std::memcmp(address.data(), senderAddress.data(), addressSize) == 0, + "source address on truncated receive"); + Require(std::all_of(address.begin() + addressSize, address.end(), [](unsigned char value) { return value == 0xa5; }), + "source address guards"); + } + if (flags == 2) { + Require(receive(buffer.data(), buffer.size(), 2, nullptr, nullptr) == sizeof(message) && + std::memcmp(buffer.data(), message, sizeof(message)) == 0, "short peek preserves complete packet"); + } + } + Require(receive(buffer.data(), buffer.size(), 0, nullptr, nullptr) < 0 && *error == 35, + "consuming receive discards remainder"); + } + send("", 0); + char byte = '!'; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + std::int64_t result; + do { + result = receive(&byte, 1, 0, nullptr, nullptr); + if (result >= 0 || *error != 35) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } while (std::chrono::steady_clock::now() < deadline); + Require(result == 0 && byte == '!', "empty datagram"); + Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected receive API argument"); + const bool net = std::strncmp(argv[1], "sce", 3) == 0; + const bool from = std::strstr(argv[1], "from") != nullptr; + if (net) Require(sceNetInit_nid_postfix() == 0, "initialize libSceNet"); + Check(net, from, 2); + Check(net, from, 28); +} diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp new file mode 100644 index 000000000..ee576b3c0 --- /dev/null +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -0,0 +1,116 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory rewind check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + + +static std::set ReadAll(int directory) { + std::set names; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0); + if (bytes == 0) return names; + for (int offset = 0; offset < bytes;) { + names.emplace(buffer.data() + offset + 8); + offset += static_cast(buffer[offset + 4]) | (static_cast(buffer[offset + 5]) << 8); + } + } +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-rewind-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream file(root / "a"); Require(static_cast(file)); } + { std::ofstream file(root / "b"); Require(static_cast(file)); } + + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + const std::set expected{".", "..", "a", "b"}; + Require(ReadAll(directory) == expected); + Require(ReadAll(directory).empty()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == expected); + + { std::ofstream file(root / "c"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + + std::array two{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const auto position = sceKernelLseek(directory, 0, 1); + Require(position == 2); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto rest = ReadAll(directory); + Require(sceKernelLseek(directory, position, 0) == position); + Require(ReadAll(directory) == rest); + Require(!rest.contains(".") && !rest.contains("..")); + + Require(sceKernelLseek(directory, 100, 0) == 100); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 0); + Require(sceKernelLseek(directory, 0, 1) == 100); + Require(sceKernelLseek(directory, -98, 1) == 2); + Require(ReadAll(directory) == rest); + + Require(sceKernelLseek(directory, 3, 0) == 3); + Require(sceKernelLseek(directory, -1, 0) == static_cast(0x80020016u)); + Require(sceKernelLseek(directory, -4, 1) == static_cast(0x80020016u)); + Require(sceKernelLseek(directory, 0, 1) == 3); + Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); + Require(sceKernelLseek(directory, 1, 1) == static_cast(0x80020054u)); + Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto afterQuery = ReadAll(directory); + Require(afterQuery == rest); + + std::array tiny{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + { std::ofstream file(root / "d"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 1) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c", "d"})); + + Require(sceKernelClose(directory) == 0); + + const int reused = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(reused == directory); + Require(sceKernelLseek(reused, 5, 0) == 5); + Require(sceKernelLseek(reused, 0, 1) == 5); + Require(sceKernelLseek(reused, 0, 2) == 0); + Require(sceKernelClose(reused) == 0); + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 5bfffc521..370be6be8 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -16,6 +16,7 @@ static int MakePipe(int* ends) { return ::_pipe(ends, 64, _O_BINARY); } static int ClosePipe(int end) { return ::_close(end); } #else +#include #include static int MakePipe(int* ends) { return ::pipe(ends); } static int ClosePipe(int end) { return ::close(end); } @@ -59,6 +60,7 @@ static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); static constexpr std::int64_t ErrorEspipe = static_cast(0x8002001Du); +static constexpr std::int64_t ErrorEagain = static_cast(0x80020023u); static std::string Contents(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); @@ -206,6 +208,11 @@ int main() { Require(std::memcmp(first, "xyz", 3) == 0); Require(sceKernelPreadv(ends[0], reads, 2, 0) == ErrorEspipe); Require(sceKernelPwritev(ends[1], message, 1, 0) == ErrorEspipe); +#ifndef _WIN32 + Require(::fcntl(ends[0], F_SETFL, ::fcntl(ends[0], F_GETFL) | O_NONBLOCK) == 0); + Require(sceKernelReadv(ends[0], reads, 2) == ErrorEagain); + Require(readv_nid_postfix(ends[0], reads, 2) == -1 && *__error_nid_postfix() == 35); +#endif Require(ClosePipe(ends[0]) == 0 && ClosePipe(ends[1]) == 0); const int random = sceKernelOpen("/dev/urandom", 0, 0); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b741a61b1..f759e12d3 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/FileStream.hpp" +#include "prx/libkernel/File/include/File.hpp" #include #include #include @@ -27,6 +30,7 @@ int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI sceKernelFsync(int); int APS5_VABI fdatasync_nid_postfix(int); +int APS5_VABI fsync_nid_postfix(int); int APS5_VABI sceKernelFdatasync(int); int APS5_VABI sceKernelWriteThrottlingStatus(std::uint64_t*); int APS5_VABI sceKernelFtruncate(int, long long); @@ -51,6 +55,13 @@ int APS5_VABI sceKernelUnlink(const char*); int APS5_VABI sceKernelRename(const char*, const char*); int APS5_VABI sceKernelRmdir(const char*); int APS5_VABI sceKernelMkdir(const char*, unsigned short); +int APS5_VABI sceKernelRename(const char*, const char*); +int APS5_VABI sceKernelCheckReachability(const char*); +int APS5_VABI access_nid_postfix(const char*, int); +int APS5_VABI chdir_nid_postfix(const char*); +void* APS5_VABI opendir_nid_postfix(const char*); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); int* APS5_VABI __error_nid_postfix(); int APS5_VABI pipe_nid_postfix(int*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::size_t); @@ -76,7 +87,91 @@ static void Check(bool value, int line) { } } #define Require(value) Check((value), __LINE__) + +static void TestParentTraversal() { + const auto root = std::filesystem::path("anyps5-parent-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root / "Existing")); + const auto present = (root / "present.txt").generic_string(); + { std::ofstream file(present); file << "preserved"; } + const auto directory = (root / "victim").generic_string(); + Require(std::filesystem::create_directory(directory)); + const auto checkInvalid = [&](const std::string& prefix, int error) { + const auto path = prefix + "/present.txt"; + const auto missingLeaf = prefix + "/new.txt"; + const int kernelError = static_cast(0x80020000u | static_cast(error)); + FileStat status{}; + Require(stat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(lstat_nid_postfix(path.c_str(), &status) == -1 && *__error_nid_postfix() == error); + Require(sceKernelStat(path.c_str(), &status) == kernelError); + Require(open_nid_postfix(path.c_str(), 0, 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelOpen(path.c_str(), 0x601, 0600) == kernelError); + Require(sceKernelOpen(missingLeaf.c_str(), 0x601, 0600) == kernelError); + Require(access_nid_postfix(path.c_str(), 0) == -1 && *__error_nid_postfix() == error); + Require(sceKernelCheckReachability(path.c_str()) == kernelError); + Require(chmod_nid_postfix(path.c_str(), 0400) == -1 && *__error_nid_postfix() == error); + Require(sceKernelChmod_nid_postfix(path.c_str(), 0400) == kernelError); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 0) == kernelError); + Require(sceKernelUtimes_nid_postfix(path.c_str(), nullptr) == kernelError); + Require(unlink_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelUnlink(path.c_str()) == kernelError); + Require(remove_nid_postfix(path.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(path.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(rename_nid_postfix(present.c_str(), missingLeaf.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRename(path.c_str(), missingLeaf.c_str()) == kernelError); + Require(sceKernelRename(present.c_str(), missingLeaf.c_str()) == kernelError); + const auto victim = prefix + "/victim"; + Require(rmdir_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(sceKernelRmdir(victim.c_str()) == kernelError); + Require(remove_nid_postfix(victim.c_str()) == -1 && *__error_nid_postfix() == error); + Require(mkdir_nid_postfix(missingLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == error); + Require(sceKernelMkdir(missingLeaf.c_str(), 0700) == kernelError); + Require(chdir_nid_postfix(prefix.c_str()) == -1 && *__error_nid_postfix() == error); + Require(opendir_nid_postfix(prefix.c_str()) == nullptr && *__error_nid_postfix() == error); + Require(fopen_nid_postfix(path.c_str(), "wb") == nullptr && *__error_nid_postfix() == error); + auto* stream = fopen_nid_postfix(present.c_str(), "rb"); + Require(stream != nullptr); + const int descriptor = stream->GuestState().descriptor; + Require(freopen_nid_postfix(path.c_str(), "wb", stream) == nullptr && *__error_nid_postfix() == error); + Require(!DescriptorIsOpen_nid_no_patch(descriptor)); + std::ifstream file(present); + std::string contents; + std::getline(file, contents); + Require(contents == "preserved" && std::filesystem::is_directory(directory)); + Require(!std::filesystem::exists(root / "new.txt")); + }; + checkInvalid(root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/present.txt/..", 20); + checkInvalid(root.generic_string() + "/present.txt/child/../..", 20); + checkInvalid("/" + root.generic_string() + "/missing/..", 2); + checkInvalid(root.generic_string() + "/missing/../../" + root.generic_string(), 2); + AddPathAlias_nid_no_patch("parent-test-mount", std::filesystem::absolute(root).string().c_str()); + checkInvalid("/PARENT-TEST-MOUNT/missing/..", 2); + checkInvalid("/parent-test-mount/present.txt/..", 20); + FileStat status{}; + Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); + RemovePathAlias_nid_no_patch("parent-test-mount"); + std::error_code linkError; + std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); + if (!linkError) { + Require(sceKernelStat((root.generic_string() + "/directory-link/../present.txt").c_str(), &status) == 0 && status.st_size == 9); + std::filesystem::create_symlink("present.txt", root / "file-link"); + std::filesystem::create_directory_symlink("missing", root / "dangling-link"); + checkInvalid(root.generic_string() + "/file-link/..", 20); + checkInvalid(root.generic_string() + "/dangling-link/..", 2); + } + Require(chdir_nid_postfix(root.generic_string().c_str()) == 0); + Require(sceKernelStat("missing/../present.txt", &status) == static_cast(0x80020002u)); + Require(sceKernelStat("present.txt/..", &status) == static_cast(0x80020014u)); + Require(sceKernelStat("EXISTING/../present.txt", &status) == 0 && status.st_size == 9); + const int created = sceKernelOpen("Existing/../new.txt", 0x601, 0600); + Require(created >= 0 && sceKernelClose(created) == 0); + Require(chdir_nid_postfix("/") == 0); + std::filesystem::remove_all(root); +} + int main() { + TestParentTraversal(); Require(chmod_nid_postfix(nullptr, 0600) == -1 && *__error_nid_postfix() == 14); Require(sceKernelChmod_nid_postfix(nullptr, 0600) == static_cast(0x8002000eu)); Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2); @@ -136,6 +231,21 @@ int main() { Require(mkdir_nid_postfix(nullptr, 0700) == -1 && *__error_nid_postfix() == 14); const auto file = root / "file.txt"; { std::ofstream stream(file); stream << "retained until removal"; } + const auto held = root / "held.txt"; + const auto heldName = held.string(); + { std::ofstream stream(held); stream << "held"; } + const int heldDescriptor = sceKernelOpen(heldName.c_str(), 2, 0); + Require(heldDescriptor >= 0 && sceKernelUnlink(heldName.c_str()) == 0 && !std::filesystem::exists(held)); + char heldBytes[4]{}; + Require(read_nid_postfix(heldDescriptor, heldBytes, sizeof(heldBytes)) == sizeof(heldBytes) && std::memcmp(heldBytes, "held", 4) == 0); + const int recreated = sceKernelOpen(heldName.c_str(), 0x0a01, 0644); + Require(recreated >= 0 && sceKernelClose(heldDescriptor) == 0 && std::filesystem::file_size(held) == 0); + const auto moved = root / "moved.txt"; + Require(rename_nid_postfix(heldName.c_str(), moved.string().c_str()) == 0 && !std::filesystem::exists(held)); + Require(write_nid_postfix(recreated, "moved", 5) == 5 && sceKernelClose(recreated) == 0 && std::filesystem::file_size(moved) == 5); + const int removing = open_nid_postfix(moved.string().c_str(), 0, 0); + Require(removing >= 0 && remove_nid_postfix(moved.string().c_str()) == 0 && !std::filesystem::exists(moved)); + Require(close_nid_postfix(removing) == 0); Require(remove_nid_postfix(root.string().c_str()) == -1); Require(*__error_nid_postfix() == 66); Require(std::filesystem::is_regular_file(file)); @@ -249,6 +359,8 @@ int main() { Require(futimes_nid_postfix(descriptor, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(descriptor) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(descriptor) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(descriptor) == static_cast(0x80020009u)); const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFchmod(socket, 0600) == static_cast(0x80020016u)); @@ -256,12 +368,17 @@ int main() { Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 22); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); Require(sceKernelFdatasync(socket) == static_cast(0x80020016u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 22); + Require(sceKernelFsync(socket) == static_cast(0x80020016u)); Require(close_nid_postfix(socket) == 0); Require(fchmod_nid_postfix(socket, 0600) == -1 && *__error_nid_postfix() == 9); Require(futimes_nid_postfix(socket, nullptr) == -1 && *__error_nid_postfix() == 9); Require(fdatasync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); Require(sceKernelFdatasync(socket) == static_cast(0x80020009u)); + Require(fsync_nid_postfix(socket) == -1 && *__error_nid_postfix() == 9); + Require(sceKernelFsync(socket) == static_cast(0x80020009u)); Require(sceKernelFdatasync(0x7fffffff) == static_cast(0x80020009u)); + Require(sceKernelFsync(0x7fffffff) == static_cast(0x80020009u)); Require(remove_nid_postfix(sized.string().c_str()) == 0); const auto present = root / "present.txt"; const auto presentName = present.string(); @@ -283,7 +400,6 @@ int main() { const int emptyUtimes = sceKernelUtimes_nid_postfix("", nullptr); std::error_code restoreTimeError; std::filesystem::last_write_time(".", cwdWriteTime, restoreTimeError); - Require(!restoreTimeError); Require(emptyUtimes == static_cast(0x80020002u)); Require(stat_nid_postfix(presentName.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(missingName.c_str(), &status) == -1 && *__error_nid_postfix() == 2); @@ -295,6 +411,8 @@ int main() { Require(stat_nid_postfix("\xff\xfe", &status) == -1 && *__error_nid_postfix() == 2); FileStat dirStatus{}; Require(stat_nid_postfix((rootName + "/").c_str(), &dirStatus) == 0 && (dirStatus.st_mode & 0170000) == 0040000); + const auto viaParent = rootName + "/../" + root.filename().string() + "/present.txt"; + Require(stat_nid_postfix(viaParent.c_str(), &status) == 0 && status.st_size == 5); Require(stat_nid_postfix(nullptr, &status) == -1 && *__error_nid_postfix() == 14); Require(stat_nid_postfix(presentName.c_str(), nullptr) == -1 && *__error_nid_postfix() == 14); FileStat linkStatus{}; @@ -316,6 +434,12 @@ int main() { Require(lstat_nid_postfix(dangling.string().c_str(), &linkStatus) == 0 && (linkStatus.st_mode & 0170000) == 0120000); Require(stat_nid_postfix(dangling.string().c_str(), &status) == -1 && *__error_nid_postfix() == 2); Require(lstat_nid_postfix((root / "present.txt" / "child").string().c_str(), &linkStatus) == -1 && *__error_nid_postfix() == 20); + const auto viaMissing = rootName + "/missing/../present.txt"; + Require(stat_nid_postfix(viaMissing.c_str(), &status) == -1 && *__error_nid_postfix() == 2); + Require(open_nid_postfix(viaMissing.c_str(), 0, 0) == -1 && *__error_nid_postfix() == 2); + Require(unlink_nid_postfix(viaMissing.c_str()) == -1 && *__error_nid_postfix() == 2 && std::filesystem::exists(present)); + Require(stat_nid_postfix((presentName + "/..").c_str(), &status) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelStat((presentName + "/../present.txt").c_str(), &status) == static_cast(0x80020014u)); Require(unlink_nid_postfix(link.string().c_str()) == 0 && unlink_nid_postfix(dangling.string().c_str()) == 0); #else const auto fileLink = root / "file-link"; @@ -429,6 +553,16 @@ int main() { Require(open_nid_postfix(rootName.c_str(), 0x0200, 0644) == -1 && *__error_nid_postfix() == 21); Require(open_nid_postfix(rootName.c_str(), 0x0002, 0) == -1 && *__error_nid_postfix() == 21); Require(open_nid_postfix(rootName.c_str(), 0x0001, 0) == -1 && *__error_nid_postfix() == 21); + const int syncedDirectory = open_nid_postfix(rootName.c_str(), 0, 0); + Require(syncedDirectory >= 0 && sceKernelFsync(syncedDirectory) == 0 && fdatasync_nid_postfix(syncedDirectory) == 0); + Require(sceKernelFdatasync(syncedDirectory) == 0 && close_nid_postfix(syncedDirectory) == 0); + const int syncedReader = open_nid_postfix(presentName.c_str(), 0, 0); + Require(syncedReader >= 0 && sceKernelFsync(syncedReader) == 0 && fdatasync_nid_postfix(syncedReader) == 0); + Require(sceKernelFdatasync(syncedReader) == 0 && close_nid_postfix(syncedReader) == 0); + Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0400) == 0); + const int syncedLocked = open_nid_postfix(presentName.c_str(), 0, 0); + Require(syncedLocked >= 0 && sceKernelFsync(syncedLocked) == 0 && close_nid_postfix(syncedLocked) == 0); + Require(sceKernelChmod_nid_postfix(presentName.c_str(), 0600) == 0); const int directoryDescriptor = open_nid_postfix(rootName.c_str(), 0, 0); Require(directoryDescriptor >= 0 && close_nid_postfix(directoryDescriptor) == 0); const int flaggedDirectory = sceKernelOpen(rootName.c_str(), 0x20000, 0); @@ -443,6 +577,20 @@ int main() { Require(sceKernelOpen("", 0, 0) == static_cast(0x80020002u)); Require(open_nid_postfix(nullptr, 0, 0) == -1 && *__error_nid_postfix() == 14); Require(_open_nid_postfix(nullptr, 0) == -1 && *__error_nid_postfix() == 14); + const auto created = root / "created.txt"; + const int createdDescriptor = creat_nid_postfix(created.string().c_str(), 0644); + Require(createdDescriptor >= 0); + Require(write_nid_postfix(createdDescriptor, "created", 7) == 7 && close_nid_postfix(createdDescriptor) == 0); + Require(std::filesystem::file_size(created) == 7); + const int truncatedDescriptor = creat_nid_postfix(created.string().c_str(), 0600); + Require(truncatedDescriptor >= 0 && close_nid_postfix(truncatedDescriptor) == 0); + Require(std::filesystem::file_size(created) == 0); + Require(unlink_nid_postfix(created.string().c_str()) == 0); + Require(creat_nid_postfix((root / "absent" / "child.txt").string().c_str(), 0644) == -1 && + *__error_nid_postfix() == 2); + Require(creat_nid_postfix(rootName.c_str(), 0644) == -1 && *__error_nid_postfix() == 21); + Require(creat_nid_postfix("", 0644) == -1 && *__error_nid_postfix() == 2); + Require(creat_nid_postfix(nullptr, 0644) == -1 && *__error_nid_postfix() == 14); Require(rmdir_nid_postfix(rootName.c_str()) == -1 && *__error_nid_postfix() == 66); Require(sceKernelRmdir(rootName.c_str()) == static_cast(0x80020042u)); Require(rmdir_nid_postfix(presentName.c_str()) == -1 && *__error_nid_postfix() == 20); @@ -500,6 +648,11 @@ int main() { Require(remove_nid_postfix(dotMissingName.c_str()) == -1 && *__error_nid_postfix() == 2); const auto missingParentLeaf = rootName + "/missing/leaf"; Require(sceKernelMkdir(missingParentLeaf.c_str(), 0700) == static_cast(0x80020002u)); +#ifndef _WIN32 + const auto longLeaf = rootName + "/" + std::string(300, 'n'); + Require(sceKernelMkdir(longLeaf.c_str(), 0700) == static_cast(0x8002003fu)); + Require(mkdir_nid_postfix(longLeaf.c_str(), 0700) == -1 && *__error_nid_postfix() == 63); +#endif const auto invalidNameUnderFile = presentName + "/a:b/x"; Require(mkdir_nid_postfix(invalidNameUnderFile.c_str(), 0700) == -1 && *__error_nid_postfix() == 20); Require(sceKernelMkdir(invalidNameUnderFile.c_str(), 0700) == static_cast(0x80020014u)); diff --git a/core/libs/tests/GuestFnmatch.cpp b/core/libs/tests/GuestFnmatch.cpp new file mode 100644 index 000000000..32b4e5b16 --- /dev/null +++ b/core/libs/tests/GuestFnmatch.cpp @@ -0,0 +1,79 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" int APS5_VABI fnmatch_nid_postfix(const char*, const char*, int); +struct Case { + const char* pattern; + const char* string; + int flags; + int result; +}; +int main() { + static const Case cases[] = { + {"*.txt", "notes.txt", 0x00, 0}, + {"*.txt", "notes.TXT", 0x00, 1}, + {"*.txt", "notes.TXT", 0x10, 0}, + {"a?c", "abc", 0x00, 0}, + {"a?c", "ac", 0x00, 1}, + {"a[bc]d", "acd", 0x00, 0}, + {"a[!bc]d", "acd", 0x00, 1}, + {"a[^bc]d", "aed", 0x00, 0}, + {"a[a-c]d", "abd", 0x00, 0}, + {"a[a-c]d", "add", 0x00, 1}, + {"a[]]d", "a]d", 0x00, 0}, + {"a[a-]d", "a-d", 0x00, 0}, + {"[A-C]x", "bx", 0x10, 0}, + {"*", "dir/file", 0x00, 0}, + {"*", "dir/file", 0x02, 1}, + {"dir/*", "dir/file", 0x02, 0}, + {"*/file", "dir/file", 0x02, 0}, + {"dir?file", "dir/file", 0x02, 1}, + {"dir[/]file", "dir/file", 0x02, 1}, + {"*", ".hidden", 0x04, 1}, + {".*", ".hidden", 0x04, 0}, + {"?hidden", ".hidden", 0x04, 1}, + {"dir/*", "dir/.hidden", 0x06, 1}, + {"dir/*", "dir/.hidden", 0x04, 0}, + {"dir/.*", "dir/.hidden", 0x06, 0}, + {"dir", "dir/sub/file", 0x08, 0}, + {"dir/*", "dir/sub/file", 0x0a, 0}, + {"dir", "dirx", 0x08, 1}, + {"\\*", "*", 0x00, 0}, + {"\\*", "x", 0x00, 1}, + {"\\*", "\\*", 0x01, 0}, + {"a\\", "a", 0x00, 1}, + {"a\\", "a\\", 0x01, 0}, + {"a*b*c", "axxbyyc", 0x00, 0}, + {"a*b*c", "axxbyy", 0x00, 1}, + {"*a*", "bbb", 0x00, 1}, + {"**a", "ba", 0x00, 0}, + {"", "", 0x00, 0}, + {"", "x", 0x00, 1}, + {"x", "", 0x00, 1}, + {"[", "[", 0x00, 0}, + {"[a", "[a", 0x00, 0}, + {"a[", "a[", 0x00, 0}, + {"[!]x", "x", 0x00, 1}, + {"*/", "dir/", 0x02, 0}, + {"*/", "dir", 0x02, 1}, + {"[[:digit:]]", "7", 0x00, 1}, + {"[[:digit:]]", "d]", 0x00, 0}, + {"[[:digit:]]", ":]", 0x00, 0}, + {"a[[:alpha:]]", "ab", 0x00, 1}, + }; + for (const Case& entry : cases) { + if (fnmatch_nid_postfix(entry.pattern, entry.string, entry.flags) != entry.result) { + std::fprintf(stderr, "fnmatch(\"%s\", \"%s\", 0x%x) != %d\n", entry.pattern, entry.string, entry.flags, entry.result); + std::abort(); + } + } + bool threw = false; + try { + fnmatch_nid_postfix(nullptr, "x", 0); + } catch (const std::invalid_argument&) { + threw = true; + } + if (!threw) std::abort(); + return 0; +} diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index f87745e78..fd90eb334 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -25,6 +25,7 @@ int APS5_VABI sceFontSupportSystemFonts(FontLibrary); int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontFile(FontLibrary, const char*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); @@ -45,8 +46,16 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint int APS5_VABI sceFontGenerateCharGlyph(FontHandle, std::uint32_t, const FontGenerateGlyphDetail*, FontGlyph*); int APS5_VABI sceFontGlyphDefineAttribute(FontGlyph, std::uint32_t, std::uint64_t); int APS5_VABI sceFontDeleteGlyph(const FontMemory*, FontGlyph*); +int APS5_VABI sceFontTextSourceInit(FontTextSource*, const void*, std::uint32_t, FontTextParseFunction, void*); +int APS5_VABI sceFontCreateString(const FontMemory*, FontTextSource*, const FontCreateStringDetail*, FontString*); +int APS5_VABI sceFontDestroyString(FontString*); +FontTextCharacter* APS5_VABI sceFontStringRefersTextCharacters(FontString, std::uint32_t*); +int APS5_VABI sceFontWritingInit(FontWriting*, FontString, const FontTextCharacter*); +const FontWritingStep* APS5_VABI sceFontWritingRefersRenderStep(FontWriting*); +FontTextCharacter* APS5_VABI sceFontWritingRefersRenderStepCharacter(FontWriting*, const FontWritingLetterStep**); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); +int APS5_VABI sceFontCharacterGetSyllableStringState(const FontTextCharacter*, int*); } static void Check(bool value, int line) { @@ -126,6 +135,24 @@ static std::vector KerningFont() { }); } +struct ParsedText { + const char* text; + std::size_t at; + FontHandle font; +}; + +static std::int32_t APS5_VABI ParseText(FontTextSource* source, void** order, FontTextParseResult* result) { + auto* parsed = static_cast(source->textObject); + *order = const_cast(parsed->text + parsed->at); + if (parsed->text[parsed->at] == 0) { + result->Terminate.terminateCode = 0; + return 0; + } + result->FontCode.font = parsed->font; + result->FontCode.code = static_cast(parsed->text[parsed->at++]); + return 1; +} + static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } @@ -202,6 +229,8 @@ int main() { Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FUNCTION && font == nullptr); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_OK); Require(sceFontSupportExternalFonts(library, 4, 0x52) == SCE_FONT_ERROR_ALREADY_SPECIFIED); + font = reinterpret_cast(&memory); + Require(sceFontOpenFontFile(library, "anyps5-missing-font-parent/../font.ttf", 1, nullptr, &font) == SCE_FONT_ERROR_FS_OPEN_FAILED && font == nullptr); Require(sceFontOpenFontMemory(library, nullptr, 0, nullptr, &font) == SCE_FONT_ERROR_INVALID_PARAMETER && font == nullptr); Require(sceFontOpenFontMemory(library, notAFont, sizeof(notAFont), nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FORMAT && font == nullptr); @@ -307,6 +336,33 @@ int main() { Require(sceFontGetRenderScaledKerning(&unopened, 'A', 'V', &kerning) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && KerningIs(kerning, 0.0f)); Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + + ParsedText parsed{"AV", 0, font}; + FontTextSource source{}; + Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); + FontString string = nullptr; + Require(sceFontCreateString(&memory, &source, nullptr, &string) == SCE_FONT_OK && string != nullptr); + std::uint32_t characterCount = 0; + FontTextCharacter* characters = sceFontStringRefersTextCharacters(string, &characterCount); + Require(characters != nullptr && characterCount == 2); + FontWriting writing{}; + Require(sceFontWritingInit(&writing, string, characters) == SCE_FONT_OK); + constexpr std::uint64_t guard = 0x5a5a5a5a5a5a5a5aull; + struct { + const FontWritingLetterStep* letter; + std::uint64_t guard; + } out{reinterpret_cast(&memory), guard}; + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == nullptr && out.letter == nullptr && out.guard == guard); + const FontWritingStep* step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters && out.letter != nullptr && out.guard == guard); + Require(out.letter->x == step->x && out.letter->advanceX == step->advanceX && out.letter->Components.textsIndex == 0 && out.letter->Components.characterTextCount == 1); + step = sceFontWritingRefersRenderStep(&writing); + Require(step != nullptr); + Require(sceFontWritingRefersRenderStepCharacter(&writing, &out.letter) == characters + 1 && out.guard == guard); + Require(out.letter->x == step->x && out.letter->Components.textsIndex == 1 && out.letter->Components.glyphsIndex == 1); + Require(sceFontWritingRefersRenderStepCharacter(&writing, nullptr) == characters + 1); + Require(sceFontDestroyString(&string) == SCE_FONT_OK); Require(sceFontCloseFont(font) == SCE_FONT_OK); Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK && renderer == nullptr); @@ -317,5 +373,12 @@ int main() { Require(allocations == 0); Require(sceFontMemoryTerm(&memory) == SCE_FONT_OK); Require(sceFontMemoryTerm(&memory) == SCE_FONT_ERROR_INVALID_MEMORY); + FontTextCharacter character{}; + character.characterCode = 'A'; + int syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(&character, &syllableState) == SCE_FONT_OK && syllableState == 0); + syllableState = -1; + Require(sceFontCharacterGetSyllableStringState(nullptr, &syllableState) == SCE_FONT_ERROR_INVALID_PARAMETER && syllableState == 0); + Require(sceFontCharacterGetSyllableStringState(&character, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); std::filesystem::remove_all(fontRoot); } diff --git a/core/libs/tests/GuestForceTlsDestructor.cpp b/core/libs/tests/GuestForceTlsDestructor.cpp index 49264c929..6f900b939 100644 --- a/core/libs/tests/GuestForceTlsDestructor.cpp +++ b/core/libs/tests/GuestForceTlsDestructor.cpp @@ -1,10 +1,15 @@ #include "SceTypes.hpp" #include #include -#include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); @@ -69,7 +74,11 @@ bool ForceThrows(KernelModule handle) { int main() { self = ModuleOf(&dsoHandle); +#ifdef _WIN32 + void* otherDso = reinterpret_cast(GetProcAddress(GetModuleHandleA("libSceLibcInternal.prx"), "_sceLibcInternalThreadDtors_nid_postfix")); +#else void* otherDso = dlsym(RTLD_NEXT, "_sceLibcInternalThreadDtors_nid_postfix"); +#endif Require(otherDso != nullptr); const KernelModule other = ModuleOf(otherDso); Require(other != self && other != 0); diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index be5f30434..d75ca2ec4 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -1,13 +1,17 @@ #include "SceTypes.hpp" #include +#include #include +#include #include +#include extern "C" { int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); @@ -32,6 +36,34 @@ static constexpr std::int64_t GIVE_UP_NANOS = 10 * NANOS_PER_SECOND; static void Require(bool value) { if (!value) std::abort(); } +static bool OmittedResolutionSucceeds() { + constexpr int clocks[] = {0, 1, 2, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 3, 6, 9999, -1, INT_MIN, INT_MAX}; + for (const int clock : clocks) { + *__error_nid_postfix() = 77; + try { + if (clock_getres_nid_postfix(clock, nullptr) == SCE_OK && *__error_nid_postfix() == 77) continue; + } catch (const std::runtime_error&) { + } + std::fprintf(stderr, "clock_getres(%d, nullptr) must succeed without changing errno\n", clock); + return false; + } + return true; +} + +static void UnsupportedResolutionStillThrows() { + KernelTimespec resolution{-1, -1}; + bool rejected = false; + try { + clock_getres_nid_postfix(3, &resolution); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected && resolution.tv_sec == -1 && resolution.tv_nsec == -1); + constexpr int fault = static_cast(0x8002000eu); + Require(sceKernelClockGetres(GUEST_CLOCK_REALTIME, nullptr) == fault); + Require(sceKernelClockGetres(3, nullptr) == fault); +} + static KernelTimespec Read(int clockId) { KernelTimespec time{-1, -1}; Require(clock_gettime_nid_postfix(clockId, &time) == SCE_OK); @@ -168,6 +200,8 @@ static void CalendarConversionsInitializeTheirState() { } int main() { + if (!OmittedResolutionSucceeds()) return 1; + UnsupportedResolutionStillThrows(); CalendarConversionsInitializeTheirState(); SecondClockReportsWholeSeconds(); KernelTimezone zone{-1, -1}; diff --git a/core/libs/tests/GuestFstatvfs.cpp b/core/libs/tests/GuestFstatvfs.cpp new file mode 100644 index 000000000..487e8bacc --- /dev/null +++ b/core/libs/tests/GuestFstatvfs.cpp @@ -0,0 +1,97 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fstatvfs_nid_postfix(int, void*); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static void RequireFailure(int descriptor, int error) { + std::array output; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + *__error_nid_postfix() = 0; + Require(fstatvfs_nid_postfix(descriptor, output.data()) == -1); + Require(*__error_nid_postfix() == error); + Require(output == saved); +} + +static void RequireVolume(int descriptor, const std::filesystem::path& path) { + std::array guarded; + guarded.fill(0xA5A5A5A5A5A5A5A5ull); + *__error_nid_postfix() = 12345; + Require(fstatvfs_nid_postfix(descriptor, guarded.data() + 1) == 0); + Require(*__error_nid_postfix() == 12345); + Require(guarded.front() == 0xA5A5A5A5A5A5A5A5ull && guarded.back() == guarded.front()); + const auto* fields = guarded.data() + 1; + const auto capacity = std::filesystem::space(path).capacity; + Require(fields[6] > 0 && fields[8] > 0); + Require(fields[2] > 0 && fields[2] == capacity / fields[8]); + Require(fields[0] <= fields[1] && fields[1] <= fields[2]); + Require(fields[3] == fields[4]); + Require((fields[7] & ~std::uint64_t{3}) == 0); + Require(fields[9] == 0 && fields[10] >= 255); +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-fstatvfs-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto filePath = root / "file.txt"; + { + std::ofstream file(filePath, std::ios::binary); + file << "fstatvfs"; + Require(static_cast(file)); + } + AddPathAlias_nid_no_patch("/anyps5-fstatvfs-test", root.string().c_str()); + const int file = sceKernelOpen("/anyps5-fstatvfs-test/file.txt", SCE_KERNEL_O_RDONLY, 0); + Require(file >= 0); + Require(sceKernelLseek(file, 3, 0) == 3); + RequireVolume(file, filePath); + Require(sceKernelLseek(file, 0, 1) == 3); + Require(fstatvfs_nid_postfix(file, nullptr) == -1 && *__error_nid_postfix() == 14); + + const int directory = sceKernelOpen("/anyps5-fstatvfs-test", SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + RequireVolume(directory, root); + Require(close_nid_postfix(directory) == 0); + + int pipes[2]; + Require(pipe_nid_postfix(pipes) == 0); + RequireFailure(pipes[0], 22); + RequireFailure(pipes[1], 22); + for (int fd : pipes) Require(close_nid_postfix(fd) == 0); + int sockets[2]; + Require(socketpair_nid_postfix(1, 1, 0, sockets) == 0); + RequireFailure(sockets[0], 22); + for (int fd : sockets) Require(close_nid_postfix(fd) == 0); + RequireFailure(sockets[0], 9); + RequireFailure(-1, 9); + Require(close_nid_postfix(file) == 0); + RemovePathAlias_nid_no_patch("/anyps5-fstatvfs-test"); + Require(std::filesystem::remove(filePath)); + Require(std::filesystem::remove(root)); +} diff --git a/core/libs/tests/GuestFstatvfsConversion.cpp b/core/libs/tests/GuestFstatvfsConversion.cpp new file mode 100644 index 000000000..f93e221e6 --- /dev/null +++ b/core/libs/tests/GuestFstatvfsConversion.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/KernelStatfs.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI fstatvfs_nid_postfix(int, void*); + +namespace { +KernelStatfs backend; +int backendError; +int guestError; +int calls; + +void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "fstatvfs conversion check failed at line %d\n", line); + std::abort(); + } +} +} +#define Require(value) Check((value), __LINE__) + +extern "C" int* APS5_VABI __error_nid_postfix() { return &guestError; } + +extern "C" int APS5_VABI _fstatfs_nid_postfix(int descriptor, KernelStatfs* output) { + Require(descriptor == 42); + ++calls; + *output = backend; + guestError = backendError ? backendError : 71; + return backendError ? -1 : 0; +} + +int main() { + backend.f_blocks = 0x100000002ull; + backend.f_bfree = 0x100000001ull; + backend.f_bavail = 0x100000000ll; + backend.f_files = 0x200000002ull; + backend.f_ffree = 0x200000001ll; + backend.f_iosize = 65536; + backend.f_bsize = 4096; + backend.f_namemax = 255; + backend.f_fsid[0] = 123; + for (std::uint64_t flags = 0; flags != 16; ++flags) { + backend.f_flags = flags; + std::array output; + output.fill(0xA5A5A5A5A5A5A5A5ull); + const std::array expected{ + 0x100000000ull, 0x100000001ull, 0x100000002ull, + 0x200000001ull, 0x200000001ull, 0x200000002ull, + 65536, (flags & 1) | ((flags & 8) ? 2u : 0u), 4096, 0, 255 + }; + const int before = calls; + guestError = 12345; + Require(fstatvfs_nid_postfix(42, output.data() + 1) == 0); + Require(calls == before + 1 && guestError == 12345); + Require(output.front() == 0xA5A5A5A5A5A5A5A5ull && output.back() == output.front()); + Require(std::memcmp(output.data() + 1, expected.data(), sizeof(expected)) == 0); + } + backend.f_bavail = -1; + backend.f_ffree = -2; + std::array output{}; + Require(fstatvfs_nid_postfix(42, output.data()) == 0); + Require(output[0] == std::numeric_limits::max()); + Require(output[3] == std::numeric_limits::max() - 1 && output[4] == output[3]); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == 14); + for (int failure : {5, 9, 22, 45}) { + backendError = failure; + output.fill(0x5A5A5A5A5A5A5A5Aull); + const auto saved = output; + Require(fstatvfs_nid_postfix(42, output.data()) == -1 && guestError == failure); + Require(output == saved); + Require(fstatvfs_nid_postfix(42, nullptr) == -1 && guestError == failure); + } +} diff --git a/core/libs/tests/GuestGetitimer.cpp b/core/libs/tests/GuestGetitimer.cpp new file mode 100644 index 000000000..57e484e58 --- /dev/null +++ b/core/libs/tests/GuestGetitimer.cpp @@ -0,0 +1,99 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestItimerval { + KernelTimeval interval; + KernelTimeval value; +}; + +static_assert(sizeof(GuestItimerval) == 32 && offsetof(GuestItimerval, value) == 16); + +extern "C" { +int APS5_VABI getitimer_nid_postfix(int, GuestItimerval*); +unsigned int APS5_VABI GuestAlarm_nid_no_patch(unsigned int); +int* APS5_VABI __error_nid_postfix(); +using Handler = void (APS5_VABI *)(int); +Handler APS5_VABI signal_nid_postfix(int, Handler); +} + +static void Require(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static std::int64_t ReadRemaining() { + struct { + std::uint64_t before; + GuestItimerval timer; + std::uint64_t after; + } guarded; + std::memset(&guarded, 0xA5, sizeof(guarded)); + *__error_nid_postfix() = 12345; + Require(getitimer_nid_postfix(0, &guarded.timer) == 0, "getitimer failed"); + Require(*__error_nid_postfix() == 12345, "getitimer changed errno on success"); + Require(guarded.before == 0xA5A5A5A5A5A5A5A5ull && guarded.after == guarded.before, "getitimer wrote outside itimerval"); + Require(guarded.timer.interval.tv_sec == 0 && guarded.timer.interval.tv_usec == 0, "alarm acquired a repeat interval"); + Require(guarded.timer.value.tv_sec >= 0 && guarded.timer.value.tv_usec >= 0 + && guarded.timer.value.tv_usec < 1000000, "invalid timeval normalization"); + return guarded.timer.value.tv_sec * 1000000 + guarded.timer.value.tv_usec; +} + +int main() { + GuestAlarm_nid_no_patch(0); + Require(ReadRemaining() == 0, "inactive alarm is not zero"); + + for (int invalid : {-1, 3, std::numeric_limits::min(), std::numeric_limits::max()}) { + GuestItimerval output{{11, 22}, {33, 44}}; + const auto saved = output; + Require(getitimer_nid_postfix(invalid, &output) == -1 && *__error_nid_postfix() == 22, "invalid timer did not report EINVAL"); + Require(std::memcmp(&output, &saved, sizeof(output)) == 0, "invalid timer changed output"); + Require(getitimer_nid_postfix(invalid, nullptr) == -1 && *__error_nid_postfix() == 22, "invalid timer validation order changed"); + } + Require(getitimer_nid_postfix(0, nullptr) == -1 && *__error_nid_postfix() == 14, "null output did not report EFAULT"); + for (int unsupported : {1, 2}) { + GuestItimerval output{{11, 22}, {33, 44}}; + const auto saved = output; + bool threw = false; + try { + getitimer_nid_postfix(unsupported, &output); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw && std::memcmp(&output, &saved, sizeof(output)) == 0, "unsupported CPU timer reported fabricated state"); + } + + const auto ignore = reinterpret_cast(std::uintptr_t{1}); + Require(signal_nid_postfix(14, ignore) == nullptr, "SIGALRM was not initially defaulted"); + Require(GuestAlarm_nid_no_patch(30) == 0, "alarm was already active"); + const auto first = ReadRemaining(); + Require(first > 0 && first <= 30000000, "armed alarm is not visible"); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); + Require(ReadRemaining() < first, "getitimer does not count down"); + std::array readers; + for (auto& reader : readers) reader = std::thread([] { + for (int i = 0; i < 100; ++i) Require(ReadRemaining() > 0, "reading consumed the alarm"); + }); + for (auto& reader : readers) reader.join(); + Require(GuestAlarm_nid_no_patch(0) > 0, "getitimer cancelled the alarm"); + Require(ReadRemaining() == 0, "cancelled alarm is not zero"); + + GuestAlarm_nid_no_patch(std::numeric_limits::max()); + Require(ReadRemaining() > static_cast(std::numeric_limits::max()) * 1000000, + "long alarm was truncated to signed 32 bits"); + GuestAlarm_nid_no_patch(1); + std::this_thread::sleep_for(std::chrono::milliseconds(1100)); + Require(ReadRemaining() == 0, "expired alarm is not zero"); + GuestAlarm_nid_no_patch(0); +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 9d18283f0..aa184acce 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -1,9 +1,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include +#include extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); @@ -18,6 +21,9 @@ int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); int APS5_VABI sceHttpWaitRequest(HttpEpollHandle, HttpNBEvent*, int, int); +int APS5_VABI sceHttpAbortWaitRequest(HttpEpollHandle); +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); int APS5_VABI sceHttpReadData(int, void*, std::size_t); int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t); int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); @@ -78,8 +84,19 @@ int main() { Require(sceHttpUriBuild(built, nullptr, required - 1, &element, buildAll) == outOfMemory); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "https://user:secret@example.com:8443/a/c?x=1&y=2#top")); + Require(sceHttpUriBuild(nullptr, &required, 0, &element, 0x01) == 0); + Require(required == sizeof("https://")); + std::memset(built, 'Z', sizeof(built)); + Require(sceHttpUriBuild(built, nullptr, required - 1, &element, 0x01) == outOfMemory); + for (char c : built) Require(c == 'Z'); + Require(sceHttpUriBuild(built, nullptr, required, &element, 0x01) == 0); + Require(Equal(built, "https://") && built[required] == 'Z'); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "//")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x02) == 0); + Require(Equal(built, "//example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x08 | 0x40) == 0); - Require(Equal(built, "/a/c?x=1&y=2")); + Require(Equal(built, "///a/c?x=1&y=2")); Require(sceHttpUriParse(&element, "HTTP://Example.com/", pool, nullptr, sizeof(pool)) == 0); Require(element.port == 80 && Equal(element.username, "") && Equal(element.query, "")); @@ -96,6 +113,10 @@ int main() { Require(Equal(element.username, "someone") && Equal(element.hostname, "example.com")); Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, buildAll) == 0); Require(Equal(built, "mailto:someone@example.com")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0x01) == 0); + Require(Equal(built, "mailto:")); + Require(sceHttpUriBuild(built, nullptr, sizeof(built), &element, 0) == 0); + Require(Equal(built, "")); Require(sceHttpUriParse(&element, "/path/only?q", pool, nullptr, sizeof(pool)) == 0); Require(Equal(element.scheme, "") && Equal(element.hostname, "") && Equal(element.path, "/path/only")); @@ -199,16 +220,65 @@ int main() { Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); Require(required == 123); + constexpr int beforeInit = static_cast(0x80431001); + constexpr int aborted = static_cast(0x80431080); + constexpr int invalidId = static_cast(0x80431100); HttpEpollHandle epoll = nullptr; - Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); - Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); HttpNBEvent events[2]{}; + Require(sceHttpCreateEpoll(1, &epoll) == beforeInit); + Require(sceHttpAbortWaitRequest(nullptr) == beforeInit); + Require(sceHttpTerm_nid_postfix(1) == beforeInit); + const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(context > 0); + Require(sceHttpCreateEpoll(-1, &epoll) == invalidId); + Require(sceHttpCreateEpoll(context, nullptr) == invalidValue); + Require(sceHttpCreateEpoll(context, &epoll) == 0 && epoll != nullptr); Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); Require(sceHttpWaitRequest(epoll, events, 2, 1000) == 0); Require(sceHttpWaitRequest(epoll, nullptr, 2, 0) == invalidValue); Require(sceHttpWaitRequest(epoll, events, 0, 0) == invalidValue); Require(sceHttpWaitRequest(nullptr, events, 2, 0) == invalidValue); - Require(sceHttpDestroyEpoll(1, epoll) == 0); + const auto unknownEpoll = reinterpret_cast(&events); + Require(sceHttpAbortWaitRequest(nullptr) == invalidValue); + Require(sceHttpAbortWaitRequest(unknownEpoll) == invalidId); + Require(sceHttpWaitRequest(unknownEpoll, events, 2, -1) == invalidId); + Require(sceHttpDestroyEpoll(context, nullptr) == invalidValue); + Require(sceHttpDestroyEpoll(context, unknownEpoll) == invalidId); + Require(sceHttpDestroyEpoll(-1, epoll) == invalidId); + + auto waitWokenBy = [&](auto wake) { + std::atomic started{false}; + std::atomic result{1}; + std::thread waiter([&] { + started = true; + result = sceHttpWaitRequest(epoll, events, 2, -1); + }); + while (!started) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(result == 1); + wake(); + waiter.join(); + return result.load(); + }; + Require(waitWokenBy([&] { Require(sceHttpAbortWaitRequest(epoll) == 0); }) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == aborted); + Require(sceHttpWaitRequest(epoll, events, 2, -1) == aborted); + Require(sceHttpDestroyEpoll(context, epoll) == 0); + Require(sceHttpAbortWaitRequest(epoll) == invalidId); + Require(sceHttpDestroyEpoll(context, epoll) == invalidId); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + Require(waitWokenBy([&] { Require(sceHttpDestroyEpoll(context, epoll) == 0); }) == 0); + + Require(sceHttpCreateEpoll(context, &epoll) == 0); + const int otherContext = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(otherContext > 0 && otherContext != context); + Require(sceHttpTerm_nid_postfix(otherContext) == 0); + Require(sceHttpTerm_nid_postfix(otherContext) == invalidId); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == 0); + Require(waitWokenBy([&] { Require(sceHttpTerm_nid_postfix(context) == 0); }) == 0); + Require(sceHttpWaitRequest(epoll, events, 2, 0) == beforeInit); + Require(sceHttpTerm_nid_postfix(context) == beforeInit); char data[16]; Require(sceHttpReadData(1, data, sizeof(data)) == network); diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp index 4bfb83c4f..e64d3053f 100644 --- a/core/libs/tests/GuestHttp2.cpp +++ b/core/libs/tests/GuestHttp2.cpp @@ -15,6 +15,10 @@ int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); int APS5_VABI sceHttp2SendRequestAsync(int, const void*, std::size_t, Http2AsyncOption*, void*); int APS5_VABI sceHttp2WaitAsync(int, Http2AsyncResult*, std::uint32_t*, void*); int APS5_VABI sceHttp2DeleteRequest(int); +int APS5_VABI sceHttp2DeleteTemplate(int); +int APS5_VABI sceHttp2SetMinSslVersion(int, std::uint32_t); +int APS5_VABI sceHttp2SslEnableOption(int, std::uint32_t); +int APS5_VABI sceHttp2SslDisableOption(int, std::uint32_t); int APS5_VABI sceHttp2Term(int); int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq); @@ -40,6 +44,14 @@ int APS5_VABI sceHttp2WebSocketSetPingInterval(); static void Require(bool value) { if (!value) std::abort(); } +static constexpr int InvalidId = static_cast(0x817B1100); + +static void RequireSslSetters(int id, int expected) { + Require(sceHttp2SetMinSslVersion(id, 0) == expected); + Require(sceHttp2SslEnableOption(id, 0) == expected); + Require(sceHttp2SslDisableOption(id, 0) == expected); +} + template static bool Throws(TCall call) { try { @@ -72,6 +84,50 @@ int main() { Require(sceHttp2CookieFlush(context) == 0); Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + RequireSslSetters(0, InvalidId); + RequireSslSetters(-1, InvalidId); + RequireSslSetters(0x7fffffff, InvalidId); + RequireSslSetters(context, 0); + RequireSslSetters(box, 0); + RequireSslSetters(other, 0); + RequireSslSetters(tmpl, 0); + RequireSslSetters(request, 0); + + const int doomedRequest = sceHttp2CreateRequestWithURL(tmpl, "GET", "https://example.com/doomed", 0); + Require(doomedRequest > 0); + Require(sceHttp2DeleteRequest(doomedRequest) == 0); + RequireSslSetters(doomedRequest, InvalidId); + Require(sceHttp2DeleteRequest(doomedRequest) == InvalidId); + + const int doomedTemplate = sceHttp2CreateTemplate(context, "agent", 2, 0); + Require(doomedTemplate > 0); + const int doomedTemplateRequest = sceHttp2CreateRequestWithURL(doomedTemplate, "GET", "https://example.com/child", 0); + Require(doomedTemplateRequest > 0); + Require(sceHttp2DeleteTemplate(doomedTemplate) == 0); + RequireSslSetters(doomedTemplate, InvalidId); + RequireSslSetters(doomedTemplateRequest, InvalidId); + Require(sceHttp2DeleteTemplate(doomedTemplate) == InvalidId); + + const int doomedContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(doomedContext > 0); + const int doomedContextBox = sceHttp2CreateCookieBox(doomedContext); + const int doomedContextTemplate = sceHttp2CreateTemplate(doomedContext, "agent", 2, 0); + Require(doomedContextBox > 0 && doomedContextTemplate > 0); + const int doomedContextRequest = sceHttp2CreateRequestWithURL(doomedContextTemplate, "GET", "https://example.com/descendant", 0); + Require(doomedContextRequest > 0); + RequireSslSetters(doomedContext, 0); + Require(sceHttp2Term(doomedContext) == 0); + RequireSslSetters(doomedContext, InvalidId); + RequireSslSetters(doomedContextBox, InvalidId); + RequireSslSetters(doomedContextTemplate, InvalidId); + RequireSslSetters(doomedContextRequest, InvalidId); + Require(sceHttp2Term(doomedContext) == InvalidId); + + const int freshContext = sceHttp2Init(1, 1, 0x2000, 4); + Require(freshContext > 0); + RequireSslSetters(freshContext, 0); + Require(sceHttp2Term(freshContext) == 0); + KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "http2") == 0); Require(sceKernelAddUserEventEdge(eq, request) == 0); @@ -98,4 +154,58 @@ int main() { Require(sceHttp2DeleteRequest(request) == 0); Require(sceKernelDeleteEqueue(eq) == 0); Require(sceHttp2Term(context) == 0); + + KernelEqueue templateEqueue = 0; + Require(sceKernelCreateEqueue(&templateEqueue, "http2-template") == 0); + const int cascadeContext = sceHttp2Init(1, 1, 0x3000, 4); + Require(cascadeContext > 0); + const int cascadeTemplate = sceHttp2CreateTemplate(cascadeContext, "agent", 2, 0); + Require(cascadeTemplate > 0); + const int cascadeRequest = sceHttp2CreateRequestWithURL(cascadeTemplate, "GET", "https://example.com/cascade", 0); + Require(cascadeRequest > 0); + Require(sceKernelAddUserEventEdge(templateEqueue, cascadeRequest) == 0); + Http2AsyncOption cascadeOption{}; + cascadeOption.equeue = templateEqueue; + cascadeOption.user_event_id = cascadeRequest; + cascadeOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == 0); + KernelEvent cascadeEvent{}; + int cascadeCount = 0; + Require(sceKernelWaitEqueue(templateEqueue, &cascadeEvent, 1, &cascadeCount, &timeout) == 0 && cascadeCount == 1); + Require(sceHttp2DeleteTemplate(cascadeTemplate) == 0); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2SendRequestAsync(cascadeRequest, nullptr, 0, &cascadeOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(cascadeRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(cascadeRequest) == InvalidId); + RequireSslSetters(cascadeRequest, InvalidId); + Require(sceHttp2Term(cascadeContext) == 0); + Require(sceKernelDeleteUserEvent(templateEqueue, cascadeRequest) == 0); + Require(sceKernelDeleteEqueue(templateEqueue) == 0); + + KernelEqueue termEqueue = 0; + Require(sceKernelCreateEqueue(&termEqueue, "http2-term") == 0); + const int terminatedContext = sceHttp2Init(1, 1, 0x4000, 4); + Require(terminatedContext > 0); + const int terminatedTemplate = sceHttp2CreateTemplate(terminatedContext, "agent", 2, 0); + Require(terminatedTemplate > 0); + const int terminatedRequest = sceHttp2CreateRequestWithURL(terminatedTemplate, "GET", "https://example.com/terminated", 0); + Require(terminatedRequest > 0); + Require(sceKernelAddUserEventEdge(termEqueue, terminatedRequest) == 0); + Http2AsyncOption termOption{}; + termOption.equeue = termEqueue; + termOption.user_event_id = terminatedRequest; + termOption.user_data = &tag; + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == 0); + KernelEvent termEvent{}; + int termCount = 0; + Require(sceKernelWaitEqueue(termEqueue, &termEvent, 1, &termCount, &timeout) == 0 && termCount == 1); + Require(sceHttp2Term(terminatedContext) == 0); + Require(sceHttp2SendRequestAsync(terminatedRequest, nullptr, 0, &termOption, nullptr) == InvalidId); + Require(Throws([&] { sceHttp2WaitAsync(terminatedRequest, &result, nullptr, nullptr); })); + Require(sceHttp2DeleteRequest(terminatedRequest) == InvalidId); + Require(sceHttp2DeleteTemplate(terminatedTemplate) == InvalidId); + RequireSslSetters(terminatedContext, InvalidId); + RequireSslSetters(terminatedRequest, InvalidId); + Require(sceKernelDeleteUserEvent(termEqueue, terminatedRequest) == 0); + Require(sceKernelDeleteEqueue(termEqueue) == 0); } diff --git a/core/libs/tests/GuestIoctlGetsid.cpp b/core/libs/tests/GuestIoctlGetsid.cpp new file mode 100644 index 000000000..288e0a8c3 --- /dev/null +++ b/core/libs/tests/GuestIoctlGetsid.cpp @@ -0,0 +1,21 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +extern "C" { +int APS5_VABI getsid_nid_postfix(int); +} +static void Require(bool value) { if (!value) std::abort(); } +template +static bool ReportsNotImplemented(Call call, const std::string& message) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} +int main() { + Require(ReportsNotImplemented([] { getsid_nid_postfix(0); }, "getsid: process sessions not implemented")); + Require(ReportsNotImplemented([] { getsid_nid_postfix(1); }, "getsid: process sessions not implemented")); +} diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index 8150bc249..6b7309123 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -35,6 +35,10 @@ void APS5_VABI _ZN3sce4Json6StringD1Ev(String*); const char* APS5_VABI _ZNK3sce4Json6String5c_strEv(const String*); std::size_t APS5_VABI _ZNK3sce4Json6String6lengthEv(const String*); void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array*); +void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array*, const Array*); +void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); +const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); +Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const Iterator*); void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); @@ -75,6 +79,7 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value*, const Str const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value*, std::size_t); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(Value*, int (APS5_VABI*)(char&, void*), void*); bool APS5_VABI _ZNK3sce4Json6Object5emptyEv(const Object*); std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); @@ -83,6 +88,7 @@ const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value*, std::size_t) const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value*, const String*); Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value*, std::size_t); void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(void*); +void APS5_VABI _ZN3sce4Json14InitParameter2C2Ev(void*); void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); @@ -268,6 +274,24 @@ static void ObjectsAndArrays() { _ZNK3sce4Json5Array3endEv(&end, &array); for (; _ZNK3sce4Json5Array8iteratorneERKS2_(&it, &end); _ZN3sce4Json5Array8iteratorppEv(&it)) ++visited; Require(visited == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(&array))); + _ZNK3sce4Json5Array5beginEv(&it, &array); + Require(_ZNK3sce4Json5Array8iteratorptEv(&it) == _ZNK3sce4Json5Array8iteratordeEv(&it)); + Array copy{}; + _ZN3sce4Json5ArrayC1ERKS1_(©, &array); + _ZN3sce4Json5Array5clearEv(&array); + Require(_ZNK3sce4Json5Array4sizeEv(&array) == 0); + bool threw = false; + try { + _ZNK3sce4Json5Array5frontEv(&array); + } catch (const std::out_of_range&) { + threw = true; + } + Require(threw); + Require(_ZNK3sce4Json5Array4sizeEv(©) == 2); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(©))); + Require(*_ZNK3sce4Json5Value7getRealEv(_ZNK3sce4Json5Array4backEv(©)) == 2.5); + _ZN3sce4Json5ArrayD1Ev(©); _ZN3sce4Json5ArrayD1Ev(&array); } @@ -361,6 +385,43 @@ static void ValueAccess() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +static void InitParameter2BaseConstructor() { + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + _ZN3sce4Json14InitParameter2C1Ev(parameter); + alignas(16) std::uint8_t baseStorage[72]; + std::memset(baseStorage, 0xff, sizeof(baseStorage)); + auto* baseParameter = baseStorage + 16; + _ZN3sce4Json14InitParameter2C2Ev(baseParameter); + Require(std::memcmp(parameter, baseParameter, sizeof(parameter)) == 0); + void* defaultAllocator; + void* defaultUserData; + std::size_t defaultFileBufferSize; + std::memcpy(&defaultAllocator, baseParameter, sizeof(defaultAllocator)); + std::memcpy(&defaultUserData, baseParameter + sizeof(void*), sizeof(defaultUserData)); + std::memcpy(&defaultFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(defaultFileBufferSize)); + Require(defaultAllocator == nullptr && defaultUserData == nullptr && defaultFileBufferSize == 0); + int allocator = 0; + int userData = 0; + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(baseParameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(baseParameter, 4096); + void* storedAllocator; + void* storedUserData; + std::size_t storedFileBufferSize; + std::memcpy(&storedAllocator, baseParameter, sizeof(storedAllocator)); + std::memcpy(&storedUserData, baseParameter + sizeof(void*), sizeof(storedUserData)); + std::memcpy(&storedFileBufferSize, baseParameter + 2 * sizeof(void*), sizeof(storedFileBufferSize)); + Require(storedAllocator == &allocator && storedUserData == &userData && storedFileBufferSize == 4096); + for (std::size_t i = 0; i < 16; ++i) Require(baseStorage[i] == 0xff); + for (std::size_t i = 16 + sizeof(parameter); i < sizeof(baseStorage); ++i) + Require(baseStorage[i] == 0xff); + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, baseParameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); +} + static void ValueClear() { Value value{}; _ZN3sce4Json5ValueC1Ev(&value); @@ -398,6 +459,37 @@ static void ValueClear() { _ZN3sce4Json5ValueD1Ev(&value); } +struct ReadState { + const char* text; + std::size_t position; + int calls; +}; + +static int APS5_VABI ReadNext(char& ch, void* context) { + auto* state = static_cast(context); + ++state->calls; + if (state->text[state->position] == '\0') { + ch = '\0'; + return -1; + } + ch = state->text[state->position++]; + return 0; +} + +static void StreamParse() { + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + ReadState valid{"{\"list\": [1, 2.5, true]}", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &valid) == 0); + Require(valid.calls == static_cast(std::strlen(valid.text)) + 1); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject); + ReadState invalid{"{\"list\": [1, 2.5", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &invalid) < 0); + ReadState trailing{"[1] 2", 0, 0}; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPFiRcPvES5_(&root, ReadNext, &trailing) < 0); + _ZN3sce4Json5ValueD1Ev(&root); +} + int main() { ParseAndRoundTrip(); NestingDepth(); @@ -405,5 +497,7 @@ int main() { NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallBackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); NullAccess(_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_); ValueAccess(); + InitParameter2BaseConstructor(); ValueClear(); + StreamParse(); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a34349584..2603b7db9 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -9,6 +9,9 @@ #include #include #include +#ifdef _WIN32 +#include +#endif #ifdef __linux__ #include #include @@ -64,11 +67,25 @@ int main() { Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); char buffer[64]; + for (const auto& component : {std::string("missing"), std::string("file.txt")}) { + const auto prefix = root.generic_string() + "/" + component + "/../"; + const auto invalid = prefix + "file.txt"; + const auto created = prefix + "new.txt"; + const int expected = component == "missing" ? 2 : 20; + Require(readlink_nid_postfix(invalid.c_str(), buffer, sizeof(buffer)) == -1 && Error() == expected); + Require(link_nid_postfix(invalid.c_str(), created.c_str()) == -1 && Error() == expected); + Require(link_nid_postfix(file.c_str(), created.c_str()) == -1 && Error() == expected); + Require(symlink_nid_postfix("file.txt", created.c_str()) == -1 && Error() == expected); + Require(std::filesystem::hard_link_count(file) == 1 && !std::filesystem::exists(root / "new.txt")); + } Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); +#ifndef _WIN32 + Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); +#endif std::error_code error; const auto link = (root / "link").string(); std::filesystem::create_symlink("sub/target.txt", link, error); @@ -82,6 +99,11 @@ int main() { Require(readlink_nid_postfix(link.c_str(), nullptr, 0) == 0); Require(readlink_nid_postfix(link.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); Require(readlink_nid_postfix((root / "LINK").string().c_str(), buffer, sizeof(buffer)) == 14); +#ifndef _WIN32 + std::filesystem::create_symlink("loopB", root / "loopA"); + std::filesystem::create_symlink("loopA", root / "loopB"); + Require(readlink_nid_postfix((root / "loopA" / "inner").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 62); +#endif } else { std::fprintf(stderr, "symbolic links unavailable: the link cases not tested\n"); } @@ -130,13 +152,22 @@ int main() { const auto symbolic = (root / "symbolic").string(); const int created = symlink_nid_postfix("original.bin", symbolic.c_str()); if (created == 0) { + const auto dangling = (root / "dangling").string(); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(symbolic).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); + Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); +#else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); Require(std::filesystem::file_size(symbolic) == std::filesystem::file_size(original)); Require(symlink_nid_postfix("original.bin", symbolic.c_str()) == -1 && Error() == 17); - const auto dangling = (root / "dangling").string(); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && !std::filesystem::exists(dangling)); + Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); +#endif } else { Require(created == -1 && Error() == 1); std::fprintf(stderr, "symbolic links unavailable: the symlink success cases not tested\n"); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 6d56671b5..fed99e4a4 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -40,6 +40,7 @@ int APS5_VABI __fpclassifyf_nid_postfix(float); float APS5_VABI fmodf_nid_postfix(float, float); float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); +double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); @@ -53,6 +54,7 @@ float APS5_VABI log1pf_nid_postfix(float); double APS5_VABI expm1_nid_postfix(double); float APS5_VABI logbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); +float APS5_VABI __powisf2_nid_postfix(float, int); double APS5_VABI ldexp_nid_postfix(double, int); double APS5_VABI scalbn_nid_postfix(double, int); float APS5_VABI scalbnf_nid_postfix(float, int); @@ -81,6 +83,22 @@ std::lldiv_t APS5_VABI lldiv_nid_postfix(long long, long long); std::lldiv_t APS5_VABI ldiv_nid_postfix(std::int64_t, std::int64_t); float APS5_VABI nanf_nid_postfix(const char*); double APS5_VABI nan_nid_postfix(const char*); +long double APS5_VABI atanl_nid_postfix(long double); +long double APS5_VABI sinl_nid_postfix(long double); +long double APS5_VABI cosl_nid_postfix(long double); +long double APS5_VABI tanl_nid_postfix(long double); +long double APS5_VABI sinhl_nid_postfix(long double); +long double APS5_VABI coshl_nid_postfix(long double); +long double APS5_VABI tanhl_nid_postfix(long double); +long double APS5_VABI expl_nid_postfix(long double); +long double APS5_VABI exp2l_nid_postfix(long double); +long double APS5_VABI logl_nid_postfix(long double); +long double APS5_VABI log10l_nid_postfix(long double); +long double APS5_VABI powl_nid_postfix(long double, long double); +long double APS5_VABI ldexpl_nid_postfix(long double, int); +long double APS5_VABI truncl_nid_postfix(long double); +double APS5_VABI logb_nid_postfix(double); +double APS5_VABI remainder_nid_postfix(double, double); } static void Require(bool value) { if (!value) std::abort(); } @@ -128,6 +146,12 @@ static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), -INFINITY)) == 0x7ff0000000000000ull); } +static void CheckAsinfErrno() { + errno = 0; + (void)asinf_nid_postfix(2.0f); + Require(errno == 0); +} + static void CheckLongDouble() { Require(ExtendedBits(acosl_nid_postfix(-1.0L), 0xc90fdaa22168c235ull, 0x4000)); Require(ExtendedBits(acosl_nid_postfix(0.0L), 0xc90fdaa22168c235ull, 0x3fff)); @@ -281,6 +305,70 @@ static void CheckIntegerConversions() { static bool Near(double value, double expected, double tolerance) { return std::abs(value - expected) <= tolerance * std::abs(expected); } +static void CheckAsin() { + const double pi = 3.14159265358979323846; + const double pi_2 = pi / 2; + const double nan = std::numeric_limits::quiet_NaN(); + const double infinity = std::numeric_limits::infinity(); + + Require(asin_nid_postfix(0.) == 0.); + Require(std::signbit(asin_nid_postfix(-0.))); + Require(Near(asin_nid_postfix(1.), pi_2, 1e-15)); + Require(Near(asin_nid_postfix(-1.), -pi_2, 1e-15)); + Require(std::isnan(asin_nid_postfix(2.0)) && std::signbit(asin_nid_postfix(2.0))); + Require(std::isnan(asin_nid_postfix(-2.0)) && std::signbit(asin_nid_postfix(-2.0))); + Require(std::isnan(asin_nid_postfix(infinity)) && std::signbit(asin_nid_postfix(infinity))); + Require(std::isnan(asin_nid_postfix(-infinity)) && std::signbit(asin_nid_postfix(-infinity))); + Require(std::isnan(asin_nid_postfix(nan))); + Require(Near(asin_nid_postfix(0.5), pi/6, 1e-15)); + Require(Near(asin_nid_postfix(-0.5), -pi/6, 1e-15)); + Require(Near(asin_nid_postfix(1./std::sqrt(2)), pi/4, 1e-15)); + Require(asin_nid_postfix(1e-300) == 1e-300); + Require(asin_nid_postfix(-1e-300) == -1e-300); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 0; + asin_nid_postfix(2.0); + Require(std::fetestexcept(FE_INVALID)); + Require(errno == 0); + std::feclearexcept(FE_ALL_EXCEPT); + asin_nid_postfix(nan); + Require(!std::fetestexcept(FE_INVALID)); +} + +static void CheckExtendedMath() { + Require(ExtendedBits(atanl_nid_postfix(INFINITY), 0xc90fdaa22168c235ull, 0x3fff)); + Require(std::fabs(atanl_nid_postfix(1.0L) - 0.785398163397448309616L) < 1e-18L); + Require(std::fabs(sinl_nid_postfix(0.5L) - 0.479425538604203000273L) < 1e-18L); + Require(std::fabs(cosl_nid_postfix(0.5L) - 0.877582561890372716116L) < 1e-18L); + Require(std::fabs(tanl_nid_postfix(0.5L) - 0.546302489843790513255L) < 1e-18L); + Require(std::fabs(sinhl_nid_postfix(1.0L) - 1.17520119364380145688L) < 1e-18L); + Require(std::fabs(coshl_nid_postfix(1.0L) - 1.54308063481524377848L) < 1e-18L); + Require(std::fabs(tanhl_nid_postfix(0.5L) - 0.462117157260009758502L) < 1e-18L); + Require(std::fabs(expl_nid_postfix(1.0L) - 2.71828182845904523536L) < 1e-18L); + Require(std::fabs(logl_nid_postfix(2.0L) - 0.693147180559945309417L) < 1e-18L); + Require(sinl_nid_postfix(0.0L) == 0.0L && std::signbit(sinl_nid_postfix(-0.0L)) && cosl_nid_postfix(0.0L) == 1.0L); + Require(std::isnan(sinl_nid_postfix(INFINITY)) && std::isnan(cosl_nid_postfix(-INFINITY)) && std::isnan(tanl_nid_postfix(INFINITY))); + Require(sinhl_nid_postfix(-INFINITY) == -INFINITY && coshl_nid_postfix(-INFINITY) == INFINITY && tanhl_nid_postfix(INFINITY) == 1.0L); + Require(sinhl_nid_postfix(11358.0L) == INFINITY && std::isfinite(sinhl_nid_postfix(11356.0L)) && std::isfinite(coshl_nid_postfix(11356.0L))); + Require(expl_nid_postfix(0.0L) == 1.0L && expl_nid_postfix(-INFINITY) == 0.0L && expl_nid_postfix(11357.0L) == INFINITY); + Require(exp2l_nid_postfix(10.0L) == 1024.0L && exp2l_nid_postfix(-16446.0L) == 0.0L && exp2l_nid_postfix(16384.0L) == INFINITY); + Require(ExtendedBits(exp2l_nid_postfix(-16445.0L), 1, 0) && ExtendedBits(exp2l_nid_postfix(-16444.0L), 2, 0)); + Require(logl_nid_postfix(1.0L) == 0.0L && logl_nid_postfix(0.0L) == -INFINITY && std::isnan(logl_nid_postfix(-1.0L))); + Require(std::fabs(log10l_nid_postfix(1000.0L) - 3.0L) < 1e-18L && log10l_nid_postfix(1.0L) == 0.0L); + Require(log10l_nid_postfix(0.0L) == -INFINITY && std::isnan(log10l_nid_postfix(-1.0L))); + Require(powl_nid_postfix(2.0L, 64.0L) == 18446744073709551616.0L && powl_nid_postfix(-2.0L, 3.0L) == -8.0L); + Require(powl_nid_postfix(0.0L, -1.0L) == INFINITY && powl_nid_postfix(1.0L, NAN) == 1.0L && std::isnan(powl_nid_postfix(-2.0L, 0.5L))); + Require(ldexpl_nid_postfix(1.0L, 16383) == 0x1p16383L && ldexpl_nid_postfix(1.0L, 16384) == INFINITY && ldexpl_nid_postfix(3.0L, -2) == 0.75L); + Require(ExtendedBits(ldexpl_nid_postfix(1.0L, -16445), 1, 0) && ldexpl_nid_postfix(1.0L, -16446) == 0.0L); + Require(truncl_nid_postfix(2.7L) == 2.0L && truncl_nid_postfix(-2.7L) == -2.0L && ExtendedBits(truncl_nid_postfix(-0.5L), 0, 0x8000)); + Require(truncl_nid_postfix(9223372036854775807.5L) == 9223372036854775807.0L && truncl_nid_postfix(-INFINITY) == -INFINITY); + Require(logb_nid_postfix(8.0) == 3.0 && logb_nid_postfix(0.75) == -1.0 && logb_nid_postfix(0x1p-1074) == -1074.0); + Require(logb_nid_postfix(0.0) == -INFINITY && logb_nid_postfix(-INFINITY) == INFINITY && std::isnan(logb_nid_postfix(NAN))); + Require(remainder_nid_postfix(5.0, 3.0) == -1.0 && remainder_nid_postfix(7.0, 2.0) == -1.0 && remainder_nid_postfix(5.0, 2.0) == 1.0); + Require(std::signbit(remainder_nid_postfix(-4.0, 2.0)) && remainder_nid_postfix(1.0, INFINITY) == 1.0); + Require(std::isnan(remainder_nid_postfix(1.0, 0.0)) && std::isnan(remainder_nid_postfix(INFINITY, 1.0))); +} + static void CheckHyperbolic() { const double infinity = std::numeric_limits::infinity(); Require(sinh_nid_postfix(0.) == 0. && std::signbit(sinh_nid_postfix(-0.))); @@ -353,6 +441,7 @@ static void CheckFloatRounding(float value, std::int64_t expected, bool invalid) int main() { CheckFloatClassification(); CheckIntegerConversions(); + CheckExtendedMath(); Require(atof_nid_postfix(" -12.5tail") == -12.5); char* end = nullptr; const char input[] = "0x1.8p+2 remainder"; @@ -478,7 +567,9 @@ int main() { Require(__isnormalf_nid_postfix(std::numeric_limits::denorm_min()) == 0); Require(__isnormal_nid_postfix(1.) == 1 && __isnormal_nid_postfix(0.) == 0); Require(__isnormal_nid_postfix(std::numeric_limits::denorm_min()) == 0); + CheckAsin(); const auto quotient = div_nid_postfix(7, 2); + CheckAsinfErrno(); Require(quotient.quot == 3 && quotient.rem == 1); const auto negativeNumerator = div_nid_postfix(-7, 2); Require(negativeNumerator.quot == -3 && negativeNumerator.rem == -1); @@ -491,6 +582,21 @@ int main() { Require(abs_nid_postfix(-42) == 42); Require(abs_nid_postfix(std::numeric_limits::max()) == std::numeric_limits::max()); Require(abs_nid_postfix(std::numeric_limits::min()) == std::numeric_limits::min()); + Require(__powisf2_nid_postfix(2.0f, 10) == 1024.0f); + Require(__powisf2_nid_postfix(2.0f, -10) == 0.0009765625f); + Require(__powisf2_nid_postfix(-2.0f, 3) == -8.0f); + Require(__powisf2_nid_postfix(-2.0f, -3) == -0.125f); + Require(__powisf2_nid_postfix(-2.0f, 4) == 16.0f); + Require(__powisf2_nid_postfix(0.0f, 0) == 1.0f); + Require(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 0) == 1.0f); + Require(std::isnan(__powisf2_nid_postfix(std::numeric_limits::quiet_NaN(), 1))); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::min()) == 1.0f); + Require(__powisf2_nid_postfix(-1.0f, std::numeric_limits::max()) == -1.0f); + Require(__powisf2_nid_postfix(2.0f, std::numeric_limits::min()) == 0.0f); + Require(std::isinf(__powisf2_nid_postfix(0.0f, -1))); + Require(std::signbit(__powisf2_nid_postfix(-0.0f, 3))); + Require(std::isinf(__powisf2_nid_postfix(-0.0f, -3)) && std::signbit(__powisf2_nid_postfix(-0.0f, -3))); + Require(__powisf2_nid_postfix(2.0f, -127) == 0x1p-127f); CheckHyperbolic(); CheckHypotSpecialValue(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index bfab22098..427a6d064 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -178,6 +178,21 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckGuestModuleUsesFlexibleMemory() { + constexpr std::size_t moduleData = 0x10000; + const auto path = std::filesystem::relative(GUEST_MEMORY_MODULE).generic_string(); + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* module = dlopen_nid_postfix(path.c_str(), 2); + Require(module != nullptr); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available + moduleData <= before); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, before, 3, 0) == SCE_KERNEL_ERROR_ENOMEM && mapped == nullptr); + Require(dlclose_nid_postfix(module) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); +} + static void CheckBatchMapStopsAtInvalidEntry() { constexpr std::size_t page = 0x4000; constexpr int mapFlexible = 3; @@ -813,8 +828,10 @@ static void CheckGuestModuleImageProtection() { static void CheckUnhintedKernelMappingsLandInTheWindow() { constexpr std::size_t page = 0x4000; - constexpr std::uintptr_t windowStart = 0x200000000ull; - constexpr std::uintptr_t windowEnd = 0xFC00000000ull; + std::uintptr_t windowStart = 0; + std::size_t windowSize = 0; + GuestArena::GuestArenaRange_nid_postfix(&windowStart, &windowSize); + const auto windowEnd = windowStart + windowSize; std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); void* direct = nullptr; @@ -889,11 +906,17 @@ static void CheckArenaHonoursHintsWithoutGoingBelowThem() { static void CheckFixedMappingsReachTheApplicationAreaEnd() { constexpr std::size_t page = 0x4000; - constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull; std::uintptr_t base = 0; std::size_t size = 0; GuestArena::GuestArenaRange_nid_postfix(&base, &size); - Require(base == 0x200000000ull && base + size == applicationAreaEnd); + constexpr std::uintptr_t expectedOffset = +#ifdef ANYPS5_ENABLE_ASAN + 0x0000100000000000ull; +#else + 0; +#endif + constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull + expectedOffset; + Require(base == 0x200000000ull + expectedOffset && base + size == applicationAreaEnd); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); void* const requested = reinterpret_cast(applicationAreaEnd - page * 2); @@ -919,7 +942,7 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); } -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) static std::size_t LockedKilobytes() { std::ifstream status("/proc/self/status"); std::string line; @@ -943,7 +966,7 @@ static void CheckMlock() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0); auto* bytes = static_cast(mapped); Require(sceKernelMlock_nid_postfix(mapped, 0) == 0); -#if defined(__linux__) +#if defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) const auto lockedBefore = LockedKilobytes(); #endif Require(sceKernelMlock_nid_postfix(bytes + 1, length - page) == 0); @@ -954,7 +977,7 @@ static void CheckMlock() { Require(GetProcessWorkingSetSizeEx(GetCurrentProcess(), &minimum, &maximum, &limits) && minimum >= length && maximum > minimum); Require(VirtualUnlock(mapped, length)); Require(!VirtualUnlock(mapped, length) && GetLastError() == ERROR_NOT_LOCKED); -#elif defined(__linux__) +#elif defined(__linux__) && !defined(ANYPS5_ENABLE_ASAN) Require(LockedKilobytes() - lockedBefore == length / 1024); #endif Require(sceKernelMlock_nid_postfix(mapped, length) == 0); @@ -963,10 +986,14 @@ static void CheckMlock() { Require(sceKernelMlock_nid_postfix(reinterpret_cast(std::numeric_limits::max() - page + 1), page * 2) == invalid); void* reserved = nullptr; Require(sceKernelReserveVirtualRange(&reserved, page, 0, 0) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(reserved, page) == outOfMemory); +#endif Require(sceKernelMunmap(reserved, page) == 0); Require(sceKernelMunmap(mapped, length) == 0); +#ifndef ANYPS5_ENABLE_ASAN Require(sceKernelMlock_nid_postfix(mapped, page) == outOfMemory); +#endif } static void CheckSharedDirectMemoryLifecycle() { @@ -1671,18 +1698,38 @@ static void CheckAvailableDirectMemoryIsLargestFreeBlock() { Require(sceKernelAvailableDirectMemorySize(top, top + window, 0, &start, &size) == 0); Require(start == top && size == static_cast(window)); } +#ifdef _WIN32 +static void CheckMprotectOfHostMemoryOutsideTheGuestRegistry() { + constexpr std::size_t length = 0x40000; + void* host = VirtualAlloc(nullptr, length, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + Require(host != nullptr); + Require(sceKernelMprotect(host, length, 0x3) == 0); + static_cast(host)[length - 1] = 7; + Require(static_cast(host)[length - 1] == 7); + Require(sceKernelMprotect(host, length, 0x1) == 0); + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READONLY); + Require(sceKernelMprotect(host, length, 0x3) == 0); + Require(VirtualQuery(host, &info, sizeof(info)) == sizeof(info) && info.Protect == PAGE_READWRITE); + Require(VirtualFree(host, 0, MEM_RELEASE) != 0); +} +#endif int main() { #ifdef _WIN32 _putenv_s("APS5_PIN_WAIT_MS", "1000"); #endif CheckAvailableDirectMemoryIsLargestFreeBlock(); +#ifdef _WIN32 + CheckMprotectOfHostMemoryOutsideTheGuestRegistry(); +#endif CheckVirtualQuerySplitFlexibleRanges(); CheckVirtualQueryForNonReadableGuestRange(); CheckReleaseFlexibleMemory(); CheckVirtualQueryPartialMunmap(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckGuestModuleUsesFlexibleMemory(); CheckBatchMapStopsAtInvalidEntry(); CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp index b63e20a04..40eacdf81 100644 --- a/core/libs/tests/GuestModuleInfo.cpp +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -114,5 +114,16 @@ int main(int argc, char** argv) { ModuleInfoEx unset{}; unset.st_size = 0xf0000; Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &unset) == 0 && unset.st_size == sizeof(ModuleInfoEx)); + const auto again = Query(add, 0); + Require(again.eh_frame_hdr_addr == info.eh_frame_hdr_addr && again.eh_frame_hdr_size == info.eh_frame_hdr_size); + Require(again.eh_frame_addr == info.eh_frame_addr && again.eh_frame_size == info.eh_frame_size); Require(dlclose_nid_postfix(module) == 0); + void* reloaded = dlopen_nid_postfix(argv[1], 2); + Require(reloaded != nullptr); + const void* reloadedAdd = dlsym_nid_postfix(reloaded, "GuestModuleAdd"); + Require(reloadedAdd != nullptr); + const auto fresh = Query(reloadedAdd, 0); + Require(fresh.eh_frame_size == info.eh_frame_size && fresh.eh_frame_hdr_size == info.eh_frame_hdr_size); + Require(fresh.eh_frame_addr - fresh.segments[0].address == info.eh_frame_addr - info.segments[0].address); + Require(dlclose_nid_postfix(reloaded) == 0); } diff --git a/core/libs/tests/GuestNamedShmConcurrency.cpp b/core/libs/tests/GuestNamedShmConcurrency.cpp new file mode 100644 index 000000000..60d91794a --- /dev/null +++ b/core/libs/tests/GuestNamedShmConcurrency.cpp @@ -0,0 +1,167 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +int APS5_VABI fchmod_nid_postfix(int, int); +int APS5_VABI close_nid_postfix(int); +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +using Unlink = int (*)(const char*); +constexpr std::int64_t Page = 0x4000; +constexpr int ReadWriteCreate = 0xa02; + +struct Object { + std::string name = "/anyps5-concurrent-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } +}; + +bool Race(int descriptor) { + for (int iteration = 0; iteration < 500; ++iteration) { + const auto size = (iteration & 1) ? 0 : Page; + if (ftruncate_nid_postfix(descriptor, size) != 0) return false; + const auto written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page / 2); + if (written < 0) return false; + FileStat info{}; + if (fstat_nid_disambig1_nid_postfix(descriptor, &info) != 0 || + (info.st_size != 0 && info.st_size != Page)) return false; + } + return true; +} + +bool NoDescriptorFallback(int descriptor) { + if (ftruncate_nid_postfix(descriptor, Page) != 0) return false; + const pid_t child = fork(); + if (child < 0) return false; + if (child == 0) { + const struct rlimit limit{0, 0}; + if (setrlimit(RLIMIT_NOFILE, &limit) != 0) _exit(2); + std::int64_t written = -2; + try { + written = pwrite_nid_disambig1_nid_postfix(descriptor, "x", 1, Page); + } catch (const std::exception& exception) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw=%s guest_errno=%d native_errno=%d\n", + descriptor, exception.what(), *__error_nid_postfix(), errno); + _exit(3); + } catch (...) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite threw unknown exception guest_errno=%d native_errno=%d\n", + descriptor, *__error_nid_postfix(), errno); + _exit(3); + } + const int guestError = *__error_nid_postfix(); + const int nativeError = errno; + struct stat info{}; + if (::fstat(descriptor, &info) != 0) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d fstat_errno=%d\n", + descriptor, static_cast(written), guestError, nativeError, errno); + _exit(4); + } + if (written != 0 && !(written == -1 && guestError == 24)) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld\n", + descriptor, static_cast(written), guestError, nativeError, static_cast(info.st_size)); + _exit(5); + } + if (info.st_size != Page) { + std::fprintf(stderr, "low-RLIMIT child: fd=%d pwrite=%lld guest_errno=%d native_errno=%d st_size=%lld expected=%lld\n", + descriptor, static_cast(written), guestError, nativeError, + static_cast(info.st_size), static_cast(Page)); + _exit(6); + } + _exit(0); + } + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0; +} + +bool ChildrenSucceeded(const pid_t* children, int count) { + bool okay = true; + for (int index = 0; index < count; ++index) { + int status = 0; + const auto result = waitpid(children[index], &status, 0); + okay = result == children[index] && WIFEXITED(status) && WEXITSTATUS(status) == 0 && okay; + } + return okay; +} + +void Run() { + Object object; + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + if (unlinkObject == nullptr) throw std::runtime_error("guest shm_unlink export is missing"); + const int descriptor = shm_open_nid_postfix(object.name.c_str(), ReadWriteCreate, 0600); + if (descriptor < 0) throw std::runtime_error("shared object creation failed"); + + pid_t children[4]{}; + int childCount = 0; + bool okay = true; + for (int index = 0; index < 4; ++index) { + const pid_t child = fork(); + if (child < 0) { + okay = false; + break; + } + if (child == 0) { + const int own = (index & 1) ? shm_open_nid_postfix(object.name.c_str(), 2, 0600) : descriptor; + const bool childOkay = own >= 0 && Race(own); + if ((index & 1) && own >= 0) close_nid_postfix(own); + _exit(childOkay ? 0 : 1); + } + children[childCount++] = child; + } + okay = Race(descriptor) && okay; + okay = ChildrenSucceeded(children, childCount) && okay; + if (!okay) throw std::runtime_error("interleaved resize/write produced an invalid shared-object size"); + if (!NoDescriptorFallback(descriptor)) + throw std::runtime_error("EOF write with no spare descriptors grew the shared object or returned an unexpected error"); + + if (fchmod_nid_postfix(descriptor, 0400) != 0) + throw std::runtime_error("changing shared-object permissions to read-only failed"); + *__error_nid_postfix() = 0; + if (unlinkObject(object.name.c_str()) != -1 || *__error_nid_postfix() != 13) + throw std::runtime_error("unlink of a read-only shared object did not return EACCES"); + if (fchmod_nid_postfix(descriptor, 0600) != 0) + throw std::runtime_error("restoring shared-object permissions failed"); + if (unlinkObject(object.name.c_str()) != 0) + throw std::runtime_error("unlink after restoring write permission failed"); + if (close_nid_postfix(descriptor) != 0) + throw std::runtime_error("shared-object close failed"); + std::puts("cross-process truncate/write, descriptor sharing and unlink permissions passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp new file mode 100644 index 000000000..e2b7263e2 --- /dev/null +++ b/core/libs/tests/GuestNamedShmDescriptorReuse.cpp @@ -0,0 +1,177 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int* APS5_VABI __error_nid_postfix(); +} + +namespace { +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr std::size_t Page = 0x4000; +int armedDescriptor = -1; +int replacementDescriptor = -1; +bool armed = false; +bool swapped = false; + +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-descriptor-reuse-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-descriptor-reuse-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; +} + +extern "C" int fcntl(int descriptor, int command, ...) { + long argument = 0; + switch (command) { + case F_GETFD: + case F_GETFL: + case F_GETOWN: +#ifdef F_GETSIG + case F_GETSIG: +#endif +#ifdef F_GETLEASE + case F_GETLEASE: +#endif +#ifdef F_GETPIPE_SZ + case F_GETPIPE_SZ: +#endif +#ifdef F_GET_SEALS + case F_GET_SEALS: +#endif + break; +#ifdef F_GETLK + case F_GETLK: + case F_SETLK: + case F_SETLKW: { + va_list args; + va_start(args, command); + argument = reinterpret_cast(va_arg(args, void*)); + va_end(args); + break; + } +#endif + case F_DUPFD: + case F_DUPFD_CLOEXEC: + case F_SETFD: + case F_SETFL: + case F_SETOWN: +#ifdef F_SETSIG + case F_SETSIG: +#endif +#ifdef F_SETLEASE + case F_SETLEASE: +#endif +#ifdef F_NOTIFY + case F_NOTIFY: +#endif +#ifdef F_SETPIPE_SZ + case F_SETPIPE_SZ: +#endif +#ifdef F_ADD_SEALS + case F_ADD_SEALS: +#endif + { + va_list args; + va_start(args, command); + argument = va_arg(args, int); + va_end(args); + break; + } + default: + errno = EINVAL; + return -1; + } + const long result = ::syscall(SYS_fcntl, descriptor, command, argument); + if (command == F_GETFL && armed && !swapped) { + swapped = true; + if (::syscall(SYS_dup2, replacementDescriptor, armedDescriptor) != armedDescriptor) return -1; + } + return static_cast(result); +} + +int main() { + try { + Object object; + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0 && ftruncate_nid_postfix(descriptor, Page) == 0, "shared-memory setup failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "original", 8, 0) == 8, "shared-memory seed failed"); + const int verifier = object.Open(ReadWrite); + Require(verifier >= 0, "shared-memory verifier open failed"); + const int replacement = CreateOrdinaryFile(); + Require(replacement >= 0, "ordinary-file setup failed"); + armedDescriptor = descriptor; + replacementDescriptor = replacement; + armed = true; + auto* mapped = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + armed = false; + Require(swapped, "native F_GETFL interposer did not replace the guest descriptor"); + Require(mapped != reinterpret_cast(static_cast(-1)), "guest mmap failed after descriptor replacement"); + Require(std::memcmp(mapped, "original", 8) == 0, "mapping followed the replacement descriptor instead of the pinned shared-memory object"); + mapped[0] = 0x70; + char byte = 0; + Require(pread_nid_postfix(verifier, &byte, 1, 0) == 1 && byte == 0x70, "mapping did not update the original shared-memory object"); + Require(munmap_nid_postfix(mapped, Page) == 0, "guest mapping cleanup failed"); + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(verifier) == 0, "guest descriptor cleanup failed"); + Require(::close(replacement) == 0, "replacement descriptor cleanup failed"); + std::puts("named shared-memory descriptor reuse checks passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp new file mode 100644 index 000000000..368fb3cc6 --- /dev/null +++ b/core/libs/tests/GuestNamedShmIoDescriptorReuse.cpp @@ -0,0 +1,139 @@ +#include "nid/Sha1.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { void* base; std::size_t length; }; +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} +namespace { +int original = -1; +int replacement = -1; +bool armed = false; +bool replaceAtProbe = false; +bool replaceBeforeProbe = false; +bool swapped = false; +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} +struct Object { + explicit Object(const char* suffix) : name("/anyps5-io-reuse-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + descriptor = shm_open_nid_postfix(name.c_str(), 2 | 0x200 | 0x800, 0600); + Require(descriptor >= 0, "object creation failed"); + Require(::ftruncate(descriptor, 0x4000) == 0, "object sizing failed"); + Require(::pwrite(descriptor, "safe", 4, 0) == 4, "object seeding failed"); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + std::string hostName = "/anyps5-shm-"; + int descriptor = -1; +}; +void Run(int operation, int boundary) { + replaceAtProbe = boundary == 1; + replaceBeforeProbe = boundary == 2; + Object first(("-first-" + std::to_string(operation)).c_str()); + Object second(("-second-" + std::to_string(operation)).c_str()); + if (replaceBeforeProbe) { + char path[] = "/tmp/anyps5-io-reuse-XXXXXX"; + const int temporary = ::mkstemp(path); + Require(temporary >= 0, "ordinary replacement creation failed"); + Require(::unlink(path) == 0, "ordinary replacement unlink failed"); + Require(::ftruncate(temporary, 0x4000) == 0 && ::pwrite(temporary, "safe", 4, 0) == 4, + "ordinary replacement initialization failed"); + Require(::dup2(temporary, second.descriptor) == second.descriptor, "ordinary replacement installation failed"); + ::close(temporary); + } + original = first.descriptor; + replacement = second.descriptor; + swapped = false; + armed = true; + char payload[] = "bad!"; + GuestIovec vector{payload, 4}; + std::int64_t result = 0; + switch (operation) { + case 0: result = write_nid_postfix(original, payload, 4); break; + case 1: result = pwrite_nid_disambig1_nid_postfix(original, payload, 4, 0); break; + case 2: result = ftruncate_nid_postfix(original, 7); break; + case 3: result = lseek_nid_postfix(original, 7, SEEK_SET); break; + case 4: result = writev_nid_postfix(original, &vector, 1); break; + case 5: result = pwritev_nid_postfix(original, &vector, 1, 0); break; + } + armed = false; + Require(swapped, "descriptor replacement did not run at the selected acquisition boundary"); + struct stat status{}; + char bytes[4]{}; + Require(::fstat(replacement, &status) == 0 && status.st_size == 0x4000, "replacement object size changed"); + Require(::pread(replacement, bytes, 4, 0) == 4 && std::memcmp(bytes, "safe", 4) == 0, "replacement object data changed"); + Require(::lseek(replacement, 0, SEEK_CUR) == 0, "replacement object offset changed"); + Require(result == -1 && *__error_nid_postfix() == 9, "descriptor identity change was not rejected with EBADF"); +} +} +static bool ReplaceDescriptor() { + swapped = true; + bool replaced = false; + std::thread reuse([&] { + replaced = close_nid_postfix(original) == 0 && dup2_nid_postfix(replacement, original) == original; + }); + reuse.join(); + return replaced; +} +extern "C" ssize_t readlink(const char* path, char* buffer, std::size_t length) { + if (armed && replaceBeforeProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + const auto result = static_cast(::syscall(SYS_readlink, path, buffer, length)); + if (result >= 0 && armed && replaceAtProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +extern "C" int flock(int descriptor, int operation) { + const int result = static_cast(::syscall(SYS_flock, descriptor, operation)); + if (result == 0 && operation == LOCK_EX && armed && !replaceAtProbe && !replaceBeforeProbe && !swapped) { + if (!ReplaceDescriptor()) return -1; + } + return result; +} +int main() { + int failures = 0; + for (int scenario = 0; scenario < 18; ++scenario) { + const int operation = scenario % 6; + try { + Run(operation, scenario / 6); + } catch (const std::exception& error) { + std::fprintf(stderr, "scenario %d: %s\n", scenario, error.what()); + ++failures; + } + } + if (failures != 0) return 1; + std::puts("guest shared-memory I/O descriptor reuse passed"); + return 0; +} diff --git a/core/libs/tests/GuestNamedShmLifecycle.cpp b/core/libs/tests/GuestNamedShmLifecycle.cpp new file mode 100644 index 000000000..a5c26b8be --- /dev/null +++ b/core/libs/tests/GuestNamedShmLifecycle.cpp @@ -0,0 +1,228 @@ +#include "SceTypes.hpp" +#include "nid/Sha1.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI shm_open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI ftruncate_nid_postfix(int, std::int64_t); +std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI pread_nid_postfix(int, void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI pwrite_nid_disambig1_nid_postfix(int, const void*, std::size_t, std::int64_t); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI dup2_nid_postfix(int, int); +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int); +int APS5_VABI open_nid_postfix(const char*, int, int); +std::int64_t APS5_VABI writev_nid_postfix(int, const struct GuestIovec*, int); +std::int64_t APS5_VABI pwritev_nid_postfix(int, const struct GuestIovec*, int, std::int64_t); +int* APS5_VABI __error_nid_postfix(); +} + +struct GuestIovec { + void* base; + std::size_t length; +}; + +namespace { +constexpr int ReadOnly = 0; +constexpr int ReadWrite = 2; +constexpr int Create = 0x200; +constexpr int Exclusive = 0x800; +constexpr int Shared = 0x1; +constexpr int Private = 0x2; +constexpr std::size_t Page = 0x4000; +using Unlink = int (*)(const char*); + +int CreateOrdinaryFile() { + char path[] = "/tmp/anyps5-lifecycle-XXXXXX"; + const int descriptor = ::mkstemp(path); + if (descriptor < 0) throw std::runtime_error("ordinary file creation failed"); + if (::unlink(path) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file unlink failed"); + } + constexpr char contents[] = "ordinary file contents"; + if (::write(descriptor, contents, sizeof(contents)) != static_cast(sizeof(contents)) || + ::lseek(descriptor, 0, SEEK_SET) != 0) { + ::close(descriptor); + throw std::runtime_error("ordinary file initialization failed"); + } + return descriptor; +} + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); +} + +struct Object { + std::string name = "/anyps5-lifecycle-" + std::to_string(getpid()); + std::string hostName = "/anyps5-shm-"; + Object() { + constexpr char hex[] = "0123456789abcdef"; + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += hex[byte >> 4]; + hostName += hex[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags) const { return shm_open_nid_postfix(name.c_str(), flags, 0600); } +}; + +void Run() { + Object object; + const char* anonymousName = reinterpret_cast(1); + const int anonymous = shm_open_nid_postfix(anonymousName, 0x100e02, 0600); + Require(anonymous >= 0 && ftruncate_nid_postfix(anonymous, Page) == 0, "anonymous shared-memory setup failed"); + auto* anonymousMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, anonymous, 0)); + Require(anonymousMap != reinterpret_cast(static_cast(-1)), "anonymous MAP_SHARED failed"); + anonymousMap[0] = 'a'; + char anonymousByte = 0; + Require(pread_nid_postfix(anonymous, &anonymousByte, 1, 0) == 1 && anonymousByte == 'a', + "anonymous MAP_SHARED write was not visible through the descriptor"); + Require(munmap_nid_postfix(anonymousMap, Page) == 0 && close_nid_postfix(anonymous) == 0, "anonymous shared-memory cleanup failed"); + const int descriptor = object.Open(ReadWrite | Create | Exclusive); + Require(descriptor >= 0, "named object creation failed"); + Require(ftruncate_nid_postfix(descriptor, Page) == 0, "guest resize failed"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, "old", 4, 0) == 4, "guest positioned write failed"); + char bytes[8]{}; + Require(pread_nid_postfix(descriptor, bytes, sizeof(bytes), Page) == 0, "read at EOF was not empty"); + Require(pwrite_nid_disambig1_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-length write failed"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, Page + 2, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "seek past EOF did not fail with EINVAL"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "seek to EOF returned the wrong position"); + Require(write_nid_postfix(descriptor, "z", 1) == 0, "write at EOF grew the shared object"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek near EOF failed"); + Require(write_nid_postfix(descriptor, "xy", 2) == 1, "cross-EOF write did not return its short length"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "cross-EOF write changed object size"); + char vectorBytes[] = "uv"; + GuestIovec vectorWrite{vectorBytes, 2}; + Require(pwritev_nid_postfix(descriptor, nullptr, 0, Page) == 0, "zero-vector positioned write on a writable descriptor failed"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page) == 0, "positioned vector write at EOF grew the object"); + Require(pwritev_nid_postfix(descriptor, &vectorWrite, 1, Page - 1) == 1, "positioned vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, Page - 1, SEEK_SET) == static_cast(Page - 1), "seek before vector write failed"); + Require(writev_nid_postfix(descriptor, &vectorWrite, 1) == 1, "vector write past EOF was not short"); + Require(lseek_nid_postfix(descriptor, 0, SEEK_END) == static_cast(Page), "vector write changed object size"); + *__error_nid_postfix() = 0; + Require(lseek_nid_postfix(descriptor, -1, SEEK_SET) == -1 && *__error_nid_postfix() == 22, + "negative seek did not return EINVAL"); + Require(lseek_nid_postfix(descriptor, std::numeric_limits::max(), SEEK_CUR) == -1 && + *__error_nid_postfix() == 84, "overflowing seek did not return EOVERFLOW"); + + const int second = object.Open(ReadWrite); + Require(second >= 0 && pread_nid_postfix(second, bytes, 4, 0) == 4 && std::memcmp(bytes, "old", 4) == 0, + "independent open did not see the shared object"); + auto* firstMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, descriptor, 0)); + Require(firstMap != reinterpret_cast(static_cast(-1)), "guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(firstMap), Page), + "file-backed shared mapping was registered with write watch"); + auto* secondMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Shared, second, 0)); + Require(secondMap != reinterpret_cast(static_cast(-1)), "second guest shared mapping failed"); + Require(!GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(secondMap), Page), + "file-backed shared alias was registered with write watch"); + *__error_nid_postfix() = 0; + Require(mmap_nid_postfix(nullptr, Page, 1, Shared, second, 1) == reinterpret_cast(static_cast(-1)) && + *__error_nid_postfix() == 22, "unaligned shared mapping offset did not return EINVAL"); + firstMap[0] = 'n'; + Require(secondMap[0] == 'n', "shared mappings did not observe each other's writes"); + auto* privateMap = static_cast(mmap_nid_postfix(nullptr, Page, 3, Private, second, 0)); + Require(privateMap != reinterpret_cast(static_cast(-1)), "guest private mapping failed"); + firstMap[1] = 'v'; + Require(privateMap[1] == 'v', "private mapping did not reflect backing changes before its first write"); + privateMap[0] = 'p'; + Require(firstMap[0] == 'n', "private mapping write escaped copy-on-write isolation"); + Require(pread_nid_postfix(second, bytes, 1, 0) == 1 && bytes[0] == 'n', "private mapping write changed the backing object"); + const int readOnly = object.Open(ReadOnly); + Require(readOnly >= 0, "read-only open failed"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, 0) == -1 && *__error_nid_postfix() == 9, + "positioned write on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwrite_nid_disambig1_nid_postfix(readOnly, "x", 1, Page) == -1 && *__error_nid_postfix() == 9, + "positioned write at EOF on a read-only descriptor did not return EBADF"); + *__error_nid_postfix() = 0; + Require(pwritev_nid_postfix(readOnly, nullptr, 0, 0) == -1 && *__error_nid_postfix() == 9, + "zero-vector positioned write on a read-only descriptor did not return EBADF"); + FileStat readOnlyInfo{}; + Require(fstat_nid_disambig1_nid_postfix(readOnly, &readOnlyInfo) == 0 && readOnlyInfo.st_size == Page, + "read-only EOF write changed the shared-object size"); + void* rejected = mmap_nid_postfix(nullptr, Page, 3, Shared, readOnly, 0); + Require(rejected == reinterpret_cast(static_cast(-1)), "writable shared mapping accepted a read-only descriptor"); + auto* writableMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, descriptor, 0)); + Require(writableMap != reinterpret_cast(static_cast(-1)), "read-protected writable-fd mapping failed"); + Require(mprotect_nid_postfix(writableMap, Page, 3) == 0, "mprotect could not restore write access from a writable descriptor"); + writableMap[2] = 'm'; + Require(firstMap[2] == 'm', "mprotect-upgraded mapping did not remain shared"); + Require(munmap_nid_postfix(writableMap, Page) == 0, "upgraded mapping unmap failed"); + auto* readOnlyMap = static_cast(mmap_nid_postfix(nullptr, Page, 1, Shared, readOnly, 0)); + Require(readOnlyMap != reinterpret_cast(static_cast(-1)), "read-only shared mapping failed"); + *__error_nid_postfix() = 0; + Require(mprotect_nid_postfix(readOnlyMap, Page, 3) == -1 && *__error_nid_postfix() == 13, + "mprotect on a read-only shared mapping did not return EACCES"); + Require(munmap_nid_postfix(readOnlyMap, Page) == 0, "read-only mapping unmap failed"); + + const int ordinary = CreateOrdinaryFile(); + Require(ordinary >= 0, "ordinary file setup failed"); + Require(dup2_nid_postfix(ordinary, second) == second, "dup2 onto shared descriptor failed"); + Require(lseek_nid_postfix(second, 0, SEEK_SET) == 0 && read_nid_postfix(second, bytes, sizeof(bytes)) > 0, + "dup2 target did not become the ordinary file"); + Require(close_nid_postfix(second) == 0, "close after dup2 failed"); + Require(lseek_nid_postfix(ordinary, 0, SEEK_SET) == 0 && read_nid_postfix(ordinary, bytes, sizeof(bytes)) > 0, + "closing reused descriptor damaged the ordinary file"); + Require(close_nid_postfix(ordinary) == 0, "ordinary file close failed"); + const int reused = object.Open(ReadWrite); + Require(reused >= 0, "shared object reopen after descriptor close failed"); + Require(dup2_nid_postfix(reused, second) == second, "guest dup2 into the freed descriptor slot failed"); + Require(pread_nid_postfix(second, bytes, 3, 0) == 3 && std::memcmp(bytes, "nvm", 3) == 0, + "reused descriptor slot retained ordinary-file state"); + Require(close_nid_postfix(second) == 0, "reused descriptor slot close failed"); + if (reused != second) Require(close_nid_postfix(reused) == 0, "reopened descriptor close failed"); + + Require(close_nid_postfix(descriptor) == 0 && close_nid_postfix(readOnly) == 0, "descriptor close failed"); + Require(firstMap[0] == 'n', "mapping did not survive descriptor close"); + const auto unlinkObject = reinterpret_cast(dlsym(RTLD_DEFAULT, "shm_unlink_nid_postfix")); + Require(unlinkObject != nullptr, "guest shm_unlink export is missing"); + Require(unlinkObject(object.name.c_str()) == 0, "guest unlink failed"); + *__error_nid_postfix() = 0; + Require(unlinkObject(object.name.c_str()) == -1 && *__error_nid_postfix() == 2, "missing-name unlink did not return ENOENT"); + const int recreated = object.Open(ReadWrite | Create | Exclusive); + Require(recreated >= 0, "name recreation after unlink failed"); + Require(ftruncate_nid_postfix(recreated, Page) == 0, "recreated object resize failed"); + Require(pread_nid_postfix(recreated, bytes, 4, 0) == 4 && std::memcmp(bytes, "\0\0\0\0", 4) == 0, + "recreated name still refers to the unlinked object"); + Require(firstMap[0] == 'n', "unlink or recreation changed the old mapping"); + Require(close_nid_postfix(recreated) == 0, "recreated descriptor close failed"); + Require(munmap_nid_postfix(firstMap, Page) == 0 && munmap_nid_postfix(secondMap, Page) == 0 && munmap_nid_postfix(privateMap, Page) == 0, + "guest unmap failed"); + std::puts("named shared-memory lifecycle checks passed"); +} +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmOpen.cpp b/core/libs/tests/GuestNamedShmOpen.cpp new file mode 100644 index 000000000..dc6cebf44 --- /dev/null +++ b/core/libs/tests/GuestNamedShmOpen.cpp @@ -0,0 +1,126 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" int* APS5_VABI __error_nid_postfix(); + +static void Require(bool value, const char* message) { + if (!value) { + throw std::runtime_error(std::string(message) + " (guest errno=" + std::to_string(*__error_nid_postfix()) + ")"); + } +} + +struct Object { + std::string name; + std::string hostName = "/anyps5-shm-"; + explicit Object(std::string suffix) : name("/anyps5-test-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (const auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { ::shm_unlink(hostName.c_str()); } + int Open(int flags, int mode = 0600) const { return shm_open_nid_postfix(name.c_str(), flags, mode); } +}; + +static void RequireError(const char* name, int flags, int error) { + *__error_nid_postfix() = 0; + Require(shm_open_nid_postfix(name, flags, 0600) == -1, "expected open failure"); + Require(*__error_nid_postfix() == error, "incorrect guest errno"); +} + +static void Run() { + constexpr int readWrite = 2, create = 0x200, exclusive = 0x800, truncate = 0x400; + Object object("/nested"); + RequireError(object.name.c_str(), readWrite, 2); + *__error_nid_postfix() = 73; + const int first = object.Open(readWrite | create | exclusive); + Require(first >= 0, "named creation failed"); + Require(*__error_nid_postfix() == 73, "successful creation overwrote guest errno"); + Require((fcntl(first, F_GETFD) & FD_CLOEXEC) != 0, "descriptor is not close-on-exec"); + struct stat info{}; + Require(fstat(first, &info) == 0 && info.st_size == 0, "new object is not empty"); + Require(ftruncate(first, 0x4000) == 0, "resize failed"); + auto* mapped = static_cast(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, first, 0)); + Require(mapped != MAP_FAILED, "host shared mapping failed"); + std::memcpy(mapped, "shared", 7); + RequireError(object.name.c_str(), readWrite | create | exclusive, 17); + const int reader = object.Open(truncate); + Require(reader >= 0, "read-only reopen failed"); + Require(fstat(reader, &info) == 0 && info.st_size == 0x4000, "read-only O_TRUNC truncated the object"); + char bytes[7]{}; + Require(pread(reader, bytes, sizeof(bytes), 0) == sizeof(bytes) && std::memcmp(bytes, "shared", 7) == 0, "reopened object has different contents"); + Require(pwrite(reader, bytes, sizeof(bytes), 0) == -1, "read-only descriptor accepted a write"); + Require(::mmap(nullptr, 0x4000, PROT_READ | PROT_WRITE, MAP_SHARED, reader, 0) == MAP_FAILED, "read-only descriptor accepted a writable shared mapping"); + Object separate("/different"); + const int different = separate.Open(readWrite | create | exclusive, 0400); + Require(different >= 0 && fstat(different, &info) == 0 && info.st_size == 0, "different names share an object"); + Require(ftruncate(different, 0x4000) == 0 && pwrite(different, "y", 1, 0) == 1, "creation permissions incorrectly restrict the new descriptor"); + Require(mapped[0] == 's', "different object write changed the first object"); + Require(close(different) == 0, "different object close failed"); + const auto child = fork(); + Require(child >= 0, "fork failed"); + if (child == 0) { + const int reopened = object.Open(readWrite); + char contents[7]{}; + const bool okay = reopened >= 0 && pread(reopened, contents, sizeof(contents), 0) == sizeof(contents) && std::memcmp(contents, "shared", 7) == 0; + if (reopened >= 0) close(reopened); + _exit(okay ? 0 : 1); + } + int status = 0; + Require(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0, "cross-process reopen failed"); + Require(::munmap(mapped, 0x4000) == 0, "host unmap failed"); + Require(close(reader) == 0 && close(first) == 0, "close failed"); + const int truncated = object.Open(readWrite | truncate); + Require(truncated >= 0 && fstat(truncated, &info) == 0 && info.st_size == 0, "read-write truncation failed"); + Require(close(truncated) == 0, "close after truncation failed"); + RequireError(nullptr, readWrite, 14); + RequireError("", readWrite, 22); + RequireError("relative", readWrite, 22); + RequireError(object.name.c_str(), 1, 22); + RequireError(object.name.c_str(), readWrite | 8, 22); + const std::string tooLong(1024, '/'); + RequireError(tooLong.c_str(), readWrite, 63); + const auto prefixLength = std::string("/anyps5-test-").size() + std::to_string(getpid()).size(); + Object longName(std::string(1023 - prefixLength, 'x')); + Require(longName.name.size() == 1023, "incorrect boundary test length"); + const auto previousMask = umask(0077); + const int longDescriptor = longName.Open(create | readWrite, 07777); + umask(previousMask); + Require(longDescriptor >= 0 && fstat(longDescriptor, &info) == 0, "long name creation failed"); + Require((info.st_mode & 07777) == 0755, "default guest umask was not applied independently of host umask"); + Require(close(longDescriptor) == 0, "long name close failed"); + const int longReader = longName.Open(0); + Require(longReader >= 0 && close(longReader) == 0, "maximum-length name reopen failed"); + const auto* anonymous = reinterpret_cast(1); + const int anonymousFirst = shm_open_nid_postfix(anonymous, readWrite, 0); + const int anonymousSecond = shm_open_nid_postfix(anonymous, readWrite | create | exclusive | truncate, 0400); + Require(anonymousFirst >= 0 && anonymousSecond >= 0, "anonymous creation regressed"); + Require(ftruncate(anonymousFirst, 0x4000) == 0 && pwrite(anonymousFirst, "x", 1, 0) == 1, "anonymous write failed"); + Require(pread(anonymousSecond, bytes, 1, 0) == 0, "anonymous objects share contents"); + Require(close(anonymousFirst) == 0 && close(anonymousSecond) == 0, "anonymous close failed"); + RequireError(anonymous, 0, 22); + RequireError(anonymous, 3, 22); + std::puts("named shared-memory checks passed"); +} + +int main() { + try { + Run(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNamedShmPermissions.cpp b/core/libs/tests/GuestNamedShmPermissions.cpp new file mode 100644 index 000000000..c6f8d4c1b --- /dev/null +++ b/core/libs/tests/GuestNamedShmPermissions.cpp @@ -0,0 +1,83 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "nid/Sha1.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI shm_open_nid_postfix(const char*, int, int); +extern "C" std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); + +static void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Masks { +public: + Masks(std::uint16_t guest, mode_t host) : guest(umask_nid_postfix(guest)), host(::umask(host)) {} + ~Masks() { ::umask(host); umask_nid_postfix(guest); } + Masks(const Masks&) = delete; + Masks& operator=(const Masks&) = delete; +private: + std::uint16_t guest; + mode_t host; +}; + +class Object { +public: + explicit Object(const char* suffix) : name("/anyps5-permissions-" + std::to_string(getpid()) + suffix) { + const auto digest = Nid::Sha1(std::vector(name.begin(), name.end())); + for (auto byte : digest) { + hostName += "0123456789abcdef"[byte >> 4]; + hostName += "0123456789abcdef"[byte & 15]; + } + ::shm_unlink(hostName.c_str()); + } + ~Object() { if (descriptor >= 0) ::close(descriptor); ::shm_unlink(hostName.c_str()); } + Object(const Object&) = delete; + Object& operator=(const Object&) = delete; + std::string name; + int descriptor = -1; +private: + std::string hostName = "/anyps5-shm-"; +}; + +static void CheckMode(const char* suffix, std::uint16_t guestMask, mode_t hostMask, int requested, mode_t expected) { + Masks masks(guestMask, hostMask); + Object object(suffix); + object.descriptor = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200 | 0x800, requested); + Require(object.descriptor >= 0, "shared-memory creation failed"); + struct stat info{}; + Require(::fstat(object.descriptor, &info) == 0, "stat failed"); + if ((info.st_mode & 07777) != expected) { + std::fprintf(stderr, "requested=%o guest-mask=%o host-mask=%o actual=%o expected=%o\n", requested, guestMask, hostMask, info.st_mode & 07777, expected); + throw std::runtime_error("shared-memory mode did not follow the guest umask"); + } + const auto observedHostMask = ::umask(hostMask); + Require(observedHostMask == hostMask, "guest creation changed host umask"); + if (expected == 0600) { + const int reopened = shm_open_nid_postfix(object.name.c_str(), 2 | 0x200, 0777); + Require(reopened >= 0, "existing-object reopen failed"); + const int result = ::fstat(reopened, &info); + ::close(reopened); + Require(result == 0 && (info.st_mode & 07777) == expected, "reopen changed existing permissions"); + } +} + +int main() { + try { + CheckMode("-guest-restricts", 0077, 0000, 0666, 0600); + CheckMode("-host-restricts", 0000, 0077, 0666, 0666); + CheckMode("-guest-denies", 0777, 0000, 0777, 0000); + CheckMode("-special-bits", 0022, 0077, 07777, 0755); + std::puts("guest shared-memory permissions passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 6a5111fc9..165d4347f 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -7,6 +7,8 @@ extern "C" { int APS5_VABI sceNpTrophy2RegisterUnlockCallback(void*, void*); int APS5_VABI sceNpTrophy2UnregisterUnlockCallback(); int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); +int APS5_VABI sceNpTrophy2GetGroupInfo(int, int, int, NpTrophy2GroupDetails*, NpTrophy2GroupData*); +int APS5_VABI sceNpTrophy2GetTrophyInfo(int, int, int, NpTrophy2Details*, NpTrophy2Data*); int APS5_VABI sceNpTrophy2ShowTrophyList(int); } @@ -14,6 +16,15 @@ static void Require(bool value) { if (!value) std::abort(); } static void APS5_VABI OnUnlock(int, int, void*) {} +template static bool ThrowsInvalidArgument(Call call) { + try { + call(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} + int main() { int userdata = 0; Require(sceNpTrophy2UnregisterUnlockCallback() == 0); @@ -29,11 +40,21 @@ int main() { Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); Require(details.num_trophies != 0); Require(sceNpTrophy2GetGameInfo(1, 1, nullptr, &data) == 0); - bool threw = false; - try { - sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); })); + + NpTrophy2GroupDetails groupDetails{}; + NpTrophy2GroupData groupData{}; + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, &groupDetails, nullptr) == 0); + Require(groupDetails.group_id == 2); + Require(sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, &groupData) == 0); + Require(groupData.group_id == 2); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetGroupInfo(1, 1, 2, nullptr, nullptr); })); + + NpTrophy2Details trophyDetails{}; + NpTrophy2Data trophyData{}; + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, &trophyDetails, nullptr) == 0); + Require(trophyDetails.trophy_id == 3); + Require(sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, &trophyData) == 0); + Require(trophyData.trophy_id == 3); + Require(ThrowsInvalidArgument([] { sceNpTrophy2GetTrophyInfo(1, 1, 3, nullptr, nullptr); })); } diff --git a/core/libs/tests/GuestOfflineImports.cpp b/core/libs/tests/GuestOfflineImports.cpp index 853a4bf1c..6f064ed62 100644 --- a/core/libs/tests/GuestOfflineImports.cpp +++ b/core/libs/tests/GuestOfflineImports.cpp @@ -17,6 +17,10 @@ int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList(); int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList(); +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(); +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(); int APS5_VABI sceRudpInit_nid_postfix(void*, int); int APS5_VABI sceRudpActivate(); int APS5_VABI sceRudpGetStatus(void*, std::size_t); @@ -67,6 +71,10 @@ int main() { Require(sceNpEntitlementAccessPollUnifiedEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessRequestServiceEntitlementInfoList() == signedOut); Require(sceNpEntitlementAccessPollServiceEntitlementInfoList() == signedOut); + Require(sceNpEntitlementAccessRequestUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollUnifiedEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessRequestServiceEntitlementInfo() == signedOut); + Require(sceNpEntitlementAccessPollServiceEntitlementInfo() == signedOut); Require(sceRudpActivate() == 0); std::array status; diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 992f0f7b9..5a7f7fab9 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -183,10 +183,14 @@ int main() { Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 1, wheel) == PAD_ERROR_INVALID_ARG); Require(scePadOpenExt(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(user, 0, 0) == noHandle); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); + Require(scePadGetHandle(user, 0, 0) == noHandle); const int handle = scePadOpen_nid_postfix(user, 0, 0, nullptr); Require(handle > 0); Require(scePadGetHandle(user, 0, 0) == handle); - Require(scePadGetHandle(user, 2, 0) == handle); + Require(scePadOpen_nid_postfix(user, PAD_PORT_TYPE_SPECIAL, 0, nullptr) == static_cast(0x80920007)); + Require(scePadGetHandle(user, PAD_PORT_TYPE_SPECIAL, 0) == noHandle); + Require(scePadGetHandle(user, 0, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); CheckReadQueue(handle); @@ -195,7 +199,8 @@ int main() { CheckInjectedState(path); Require(scePadSetVibrationMode(handle, 1) == 0); Require(scePadSetVibrationMode(handle, 2) == 0); - Require(scePadSetVibrationMode(handle, 3) == PAD_ERROR_INVALID_ARG); + Require(scePadSetVibrationMode(handle, 3) == 0); + Require(scePadSetVibrationMode(handle, 4) == PAD_ERROR_INVALID_ARG); Require(scePadSetVibrationMode(handle, -1) == PAD_ERROR_INVALID_ARG); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1ae..f6a296312 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/core/libs/tests/GuestPthreadDestructorLifetime.cpp b/core/libs/tests/GuestPthreadDestructorLifetime.cpp new file mode 100644 index 000000000..4bddcb951 --- /dev/null +++ b/core/libs/tests/GuestPthreadDestructorLifetime.cpp @@ -0,0 +1,96 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadCancel(Pthread thread); +void APS5_VABI scePthreadExit(void* retval); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadGetthreadid(); +int APS5_VABI scePthreadGetname(Pthread thread, char* name); +int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t destructor); +int APS5_VABI scePthreadKeyDelete(PthreadKey key); +void* APS5_VABI scePthreadGetspecific(PthreadKey key); +int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +} + +struct Context { + PthreadKey key = -1; + Pthread expected = nullptr; + int expectedId = 0; + unsigned calls = 0; + unsigned failures = 0; + bool exitExplicitly = false; + bool pendingCancel = false; + + void Check(bool passed, const char* message) { + if (passed) return; + std::fprintf(stderr, "%s: %s\n", exitExplicitly ? "explicit exit" : "return", message); + ++failures; + } +}; + +static void APS5_VABI DestroyValue(void* value) { + auto& context = *static_cast(value); + ++context.calls; + context.Check(scePthreadGetspecific(context.key) == nullptr, "key value was not cleared before its destructor"); + if (context.expected) { + const auto self = scePthreadSelf(); + context.Check(self == context.expected, "key destructor lost the exiting thread identity"); + context.Check(scePthreadGetthreadid() == context.expectedId, "key destructor lost the exiting thread ID"); + char name[32]{}; + context.Check(scePthreadGetname(self, name) == 0 && std::strcmp(name, "KeyDestructor") == 0, + "key destructor lost the exiting thread name"); + context.Check(sceKernelUsleep_nid_postfix(1000) == 0, "key destructor could not wait"); + } + context.Check(scePthreadSetspecific(context.key, value) == 0, "key destructor could not restore its value"); +} + +static void* APS5_VABI Worker(void* value) { + auto& context = *static_cast(value); + context.expected = scePthreadSelf(); + context.expectedId = scePthreadGetthreadid(); + context.Check(scePthreadSetspecific(context.key, value) == 0, "worker could not bind its key value"); + if (context.pendingCancel) context.Check(scePthreadCancel(context.expected) == 0, "worker could not request cancellation"); + if (context.exitExplicitly) scePthreadExit(value); + return value; +} + +static bool RunGuest(bool exitExplicitly, bool pendingCancel = false) { + Context context; + context.exitExplicitly = exitExplicitly; + context.pendingCancel = pendingCancel; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + Pthread thread = nullptr; + if (scePthreadCreate(&thread, nullptr, Worker, &context, "KeyDestructor") != 0) return false; + void* result = nullptr; + context.Check(scePthreadJoin(thread, &result) == 0 && result == &context, "thread did not join with its return value"); + context.Check(context.calls == 4, "key destructor did not stop after four iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "key deletion failed"); + return context.failures == 0; +} + +static bool RunHost() { + Context context; + if (scePthreadKeyCreate(&context.key, reinterpret_cast(DestroyValue)) != 0) return false; + std::thread worker([&] { + context.Check(scePthreadSetspecific(context.key, &context) == 0, "host worker could not bind its key value"); + }); + worker.join(); + context.Check(context.calls == 4, "host thread did not run four key destructor iterations"); + context.Check(scePthreadKeyDelete(context.key) == 0, "host key deletion failed"); + return context.failures == 0; +} + +int main() { + const bool returned = RunGuest(false); + const bool exited = RunGuest(true); + const bool canceled = RunGuest(false, true); + const bool host = RunHost(); + if (!returned || !exited || !canceled || !host) return 1; + std::puts("PASS: pthread key destructors retain thread identity on return and explicit exit; pending cancellation, four iterations and host cleanup preserved"); +} diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index ffae6559a..ff27c538e 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -29,6 +29,7 @@ using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EBUSY = 16; static constexpr int GUEST_EINVAL = 22; @@ -223,6 +224,12 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &deadline) == GUEST_EDEADLK); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); + Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Holder reader{&rwlock, false}; Start(reader); @@ -242,6 +249,7 @@ int main() { Holder writer{&rwlock, true}; Start(writer); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); @@ -253,6 +261,7 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &deadline) == 0); Require(scePthreadJoin(writer.thread, nullptr) == SCE_OK); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); + Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); bool rejected = false; try { pthread_rwlock_timedrdlock_nid_postfix(&rwlock, nullptr); } diff --git a/core/libs/tests/GuestPthreadSpin.cpp b/core/libs/tests/GuestPthreadSpin.cpp new file mode 100644 index 000000000..9783df2dd --- /dev/null +++ b/core/libs/tests/GuestPthreadSpin.cpp @@ -0,0 +1,122 @@ +#include "SceTypes.hpp" +#include +#include + +struct PthreadSpinlockPrivate; +using PthreadSpinlock = PthreadSpinlockPrivate*; + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI pthread_spin_init_nid_postfix(PthreadSpinlock* lock, int pshared); +int APS5_VABI pthread_spin_destroy_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_lock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_trylock_nid_postfix(PthreadSpinlock* lock); +int APS5_VABI pthread_spin_unlock_nid_postfix(PthreadSpinlock* lock); +} + +static constexpr int GUEST_EPERM = 1; +static constexpr int GUEST_EBUSY = 16; +static constexpr int GUEST_EINVAL = 22; +static constexpr int PTHREAD_PROCESS_PRIVATE = 0; +static constexpr int PTHREAD_PROCESS_SHARED = 1; +static constexpr int Increments = 200000; + +static void Require(bool value) { if (!value) std::abort(); } + +struct Shared { + PthreadSpinlock lock = nullptr; + long counter = 0; + int trylock = -1; + int unlock = -1; +}; + +static void* APS5_VABI Contender(void* arg) { + auto& shared = *static_cast(arg); + shared.trylock = pthread_spin_trylock_nid_postfix(&shared.lock); + shared.unlock = pthread_spin_unlock_nid_postfix(&shared.lock); + return nullptr; +} + +static void* APS5_VABI Incrementer(void* arg) { + auto& shared = *static_cast(arg); + for (int i = 0; i < Increments; ++i) { + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + ++shared.counter; + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + } + return nullptr; +} + +static void ArgumentErrors() { + Require(pthread_spin_init_nid_postfix(nullptr, PTHREAD_PROCESS_PRIVATE) == GUEST_EINVAL); + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, 2) == GUEST_EINVAL); + Require(pthread_spin_init_nid_postfix(&lock, -1) == GUEST_EINVAL); + Require(lock == nullptr); + Require(pthread_spin_lock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(nullptr) == GUEST_EINVAL); + Require(pthread_spin_lock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EINVAL); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void SingleThread(int pshared) { + PthreadSpinlock lock = nullptr; + Require(pthread_spin_init_nid_postfix(&lock, pshared) == 0); + Require(lock != nullptr); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_lock_nid_postfix(&lock) == 0); + Require(pthread_spin_trylock_nid_postfix(&lock) == GUEST_EBUSY); + bool threw = false; + try { + pthread_spin_lock_nid_postfix(&lock); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == GUEST_EPERM); + Require(pthread_spin_trylock_nid_postfix(&lock) == 0); + Require(pthread_spin_unlock_nid_postfix(&lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&lock) == 0); + Require(lock == nullptr); + Require(pthread_spin_destroy_nid_postfix(&lock) == GUEST_EINVAL); +} + +static void HeldByAnotherThread() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Require(pthread_spin_lock_nid_postfix(&shared.lock) == 0); + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Contender, &shared, nullptr) == 0); + Require(scePthreadJoin(thread, nullptr) == 0); + Require(shared.trylock == GUEST_EBUSY); + Require(shared.unlock == GUEST_EPERM); + Require(pthread_spin_unlock_nid_postfix(&shared.lock) == 0); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +static void MutualExclusion() { + Shared shared; + Require(pthread_spin_init_nid_postfix(&shared.lock, PTHREAD_PROCESS_PRIVATE) == 0); + Pthread first = nullptr; + Pthread second = nullptr; + Require(scePthreadCreate(&first, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadCreate(&second, nullptr, Incrementer, &shared, nullptr) == 0); + Require(scePthreadJoin(first, nullptr) == 0); + Require(scePthreadJoin(second, nullptr) == 0); + Require(shared.counter == 2L * Increments); + Require(pthread_spin_destroy_nid_postfix(&shared.lock) == 0); +} + +int main() { + ArgumentErrors(); + SingleThread(PTHREAD_PROCESS_PRIVATE); + SingleThread(PTHREAD_PROCESS_SHARED); + HeldByAnotherThread(); + MutualExclusion(); +} diff --git a/core/libs/tests/GuestRandomState.cpp b/core/libs/tests/GuestRandomState.cpp index 22267baa5..59b336db3 100644 --- a/core/libs/tests/GuestRandomState.cpp +++ b/core/libs/tests/GuestRandomState.cpp @@ -19,6 +19,10 @@ int* APS5_VABI __error_nid_postfix(); double APS5_VABI erand48_nid_postfix(std::uint16_t*); std::int64_t APS5_VABI nrand48_nid_postfix(std::uint16_t*); std::int64_t APS5_VABI jrand48_nid_postfix(std::uint16_t*); +double APS5_VABI drand48_nid_postfix(); +std::int64_t APS5_VABI lrand48_nid_postfix(); +std::int64_t APS5_VABI mrand48_nid_postfix(); +void APS5_VABI srand48_nid_postfix(std::int64_t); } static int failures = 0; @@ -63,6 +67,13 @@ static void CheckRand48() { const double value = erand48_nid_postfix(top); Require(value >= 0.0 && value < 1.0, "erand48 stays below 1"); + srand48_nid_postfix(0x1234abcd); + std::uint16_t expectedSeed[3] = {0x330e, 0xabcd, 0x1234}; + Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 matches erand48 on initialized seed"); + Require(lrand48_nid_postfix() == nrand48_nid_postfix(expectedSeed), "lrand48 matches nrand48 on initialized seed"); + Require(mrand48_nid_postfix() == jrand48_nid_postfix(expectedSeed), "mrand48 matches jrand48 on initialized seed"); + Require(drand48_nid_postfix() == erand48_nid_postfix(expectedSeed), "drand48 fourth value matches erand48"); + #ifndef _WIN32 std::uint16_t guest[3] = {0x1234, 0x5678, 0x9abc}; unsigned short host[3] = {0x1234, 0x5678, 0x9abc}; @@ -71,6 +82,13 @@ static void CheckRand48() { Require(nrand48_nid_postfix(guest) == ::nrand48(host), "nrand48 matches the host"); Require(jrand48_nid_postfix(guest) == ::jrand48(host), "jrand48 matches the host"); } + ::srand48(1234567); + srand48_nid_postfix(1234567); + for (int round = 0; round < 32; ++round) { + Require(drand48_nid_postfix() == ::drand48(), "drand48 matches the host"); + Require(lrand48_nid_postfix() == ::lrand48(), "lrand48 matches the host"); + Require(mrand48_nid_postfix() == ::mrand48(), "mrand48 matches the host"); + } #endif } diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index f8fdab4d8..e4af18eed 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -7,6 +7,7 @@ std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); int APS5_VABI sceRazorCpuJobManagerDispatch(const void* args); int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); +int APS5_VABI sceRazorCpuPushMarker(const char* label, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); @@ -29,6 +30,8 @@ int main() { Require(sceRazorCpuPushMarkerStatic("inner", 0x80ffffffu, 2) == 0); Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPopMarker() == 0); + Require(sceRazorCpuPushMarker("copied", 0xff00ff00u, 1) == 0); + Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp new file mode 100644 index 000000000..36cf63cb4 --- /dev/null +++ b/core/libs/tests/GuestRudpPump.cpp @@ -0,0 +1,47 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceRudpInit_nid_postfix(void*, int); +int APS5_VABI sceRudpTerminate(); +int APS5_VABI sceRudpProcessEvents(); +int APS5_VABI sceRudpNetFlush(); +int APS5_VABI sceRudpGetMaxSegmentSize(); +int APS5_VABI sceRudpNetReceived(); +} + +static void Require(bool value, int line) { + if (!value) { + std::fprintf(stderr, "rudp check failed at line %d\n", line); + std::abort(); + } +} +#define CHECK(value) Require((value), __LINE__) + +int main() { + constexpr int notInitialized = static_cast(0x80770001); + CHECK(sceRudpProcessEvents() == notInitialized); + CHECK(sceRudpNetFlush() == notInitialized); + + alignas(8) static unsigned char pool[0xC58]; + CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + CHECK(sceRudpProcessEvents() == 0); + CHECK(sceRudpNetFlush() == 0); + CHECK(sceRudpTerminate() == 0); + + for (const auto call : {&sceRudpNetReceived, &sceRudpGetMaxSegmentSize}) { + bool threw = false; + try { + call(); + } catch (const std::runtime_error&) { + threw = true; + } + CHECK(threw); + } + return 0; +} diff --git a/core/libs/tests/GuestSaveDataConvert.cpp b/core/libs/tests/GuestSaveDataConvert.cpp new file mode 100644 index 000000000..8df9b84da --- /dev/null +++ b/core/libs/tests/GuestSaveDataConvert.cpp @@ -0,0 +1,73 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceSaveDataConvert(const SaveDataConvert*); +int APS5_VABI sceSaveDataInitialize3(const void*); +} + +namespace { + +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotFound = -2137063416; + +int failures = 0; + +void Check(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "savedata convert check failed: %s\n", what.c_str()); + ++failures; +} + +int Convert(const char* data, std::size_t size) { + SceSaveDataDirName name{}; + std::memcpy(name.data, data, size); + SaveDataConvert convert{}; + convert.user_id = 0x10000000; + convert.dir_name = &name; + return sceSaveDataConvert(&convert); +} + +std::vector Read(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return {std::istreambuf_iterator(stream), std::istreambuf_iterator()}; +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-savedata-convert-" + std::to_string(std::random_device{}())); + const auto work = root / "work"; + const auto kept = work / "_sd" / "kept" / "data.bin"; + std::filesystem::create_directories(kept.parent_path()); + std::ofstream(kept) << "data"; + const auto previous = std::filesystem::current_path(); + std::filesystem::current_path(work); + + Check(sceSaveDataInitialize3(nullptr) == 0, "SaveData initializes"); + Check(Convert("kept", sizeof("kept")) == 0, "an existing save converts"); + Check(Read(kept) == std::vector{'d', 'a', 't', 'a'}, "the converted save keeps its files"); + Check(Convert("missing", sizeof("missing")) == SaveDataErrorNotFound, "a missing save is not found"); + for (const char* invalid : {"../kept", "", ".", "..", "a\\b", "c:d"}) { + Check(Convert(invalid, std::strlen(invalid) + 1) == SaveDataErrorParameter, std::string("\"") + invalid + "\" is rejected"); + } + char unterminated[sizeof(SceSaveDataDirName::data)]; + std::memset(unterminated, 'a', sizeof(unterminated)); + Check(Convert(unterminated, sizeof(unterminated)) == SaveDataErrorParameter, "an unterminated name is rejected"); + + std::filesystem::current_path(previous); + std::error_code error; + std::filesystem::remove_all(root, error); + if (failures != 0) return 1; + std::printf("savedata convert tests passed\n"); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataSearch.cpp b/core/libs/tests/GuestSaveDataSearch.cpp index cc24049af..0be7b2d33 100644 --- a/core/libs/tests/GuestSaveDataSearch.cpp +++ b/core/libs/tests/GuestSaveDataSearch.cpp @@ -8,17 +8,79 @@ #include #include #include +#include extern "C" { int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); int APS5_VABI sceSaveDataMount3(const SaveDataMount3*, SaveDataMountResult*); int APS5_VABI sceSaveDataUmount2(std::uint32_t, const SaveDataMountPoint*); +int APS5_VABI sceSaveDataSetParam(const SaveDataMountPoint*, std::uint32_t, const void*, std::size_t); } +static constexpr int SaveDataErrorParameter = -2137063424; + static void Require(bool value, const char* message) { if (!value) throw std::runtime_error(message); } +static std::vector Search(std::uint32_t key, std::uint32_t order, std::uint32_t capacity, std::uint32_t* hits) { + SceSaveDataDirName pattern{}; + std::snprintf(pattern.data, sizeof(pattern.data), "SORT_%%"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.dir_name = &pattern; + cond.key = key; + cond.order = order; + std::array names{}; + SaveDataDirNameSearchResult result{}; + result.dir_names = names.data(); + result.dir_names_num = capacity; + Require(sceSaveDataDirNameSearch(&cond, &result) == 0, "sorted search failed"); + *hits = result.hit_num; + std::vector found; + for (std::uint32_t i = 0; i < result.set_num; ++i) found.emplace_back(names[i].data); + return found; +} + +static void SortByKey() { + struct Save { + const char* name; + std::uint32_t userParam; + std::int64_t mtime; + }; + const std::array saves{{{"SORT_B", 1, 1000}, {"SORT_A", 3, 2000}, {"SORT_C", 2, 3000}}}; + for (const auto& save : saves) { + std::filesystem::create_directories(std::filesystem::path("_sd") / save.name); + SceSaveDataDirName dirName{}; + std::snprintf(dirName.data, sizeof(dirName.data), "%s", save.name); + SaveDataMount3 mount{}; + mount.user_id = 1; + mount.dir_name = &dirName; + mount.mount_mode = 2; + SaveDataMountResult mounted{}; + Require(sceSaveDataMount3(&mount, &mounted) == 0, "mount for parameters failed"); + Require(sceSaveDataSetParam(&mounted.mount_point, 4, &save.userParam, sizeof(save.userParam)) == 0, "user parameter was not set"); + Require(sceSaveDataSetParam(&mounted.mount_point, 5, &save.mtime, sizeof(save.mtime)) == 0, "mtime was not set"); + Require(sceSaveDataUmount2(0, &mounted.mount_point) == 0, "unmount failed"); + } + using Names = std::vector; + std::uint32_t hits = 0; + Require(Search(0, 0, 3, &hits) == Names{"SORT_A", "SORT_B", "SORT_C"} && hits == 3, "directory names must sort ascending"); + Require(Search(0, 1, 3, &hits) == Names{"SORT_C", "SORT_B", "SORT_A"}, "directory names must sort descending"); + Require(Search(1, 0, 3, &hits) == Names{"SORT_B", "SORT_C", "SORT_A"}, "user parameters must sort ascending"); + Require(Search(1, 1, 3, &hits) == Names{"SORT_A", "SORT_C", "SORT_B"}, "user parameters must sort descending"); + Require(Search(3, 0, 3, &hits) == Names{"SORT_B", "SORT_A", "SORT_C"}, "modification times must sort ascending"); + Require(Search(3, 1, 1, &hits) == Names{"SORT_C"} && hits == 3, "the newest save must come first in descending modification time"); + SaveDataDirNameSearchCond cond{}; + cond.user_id = 1; + cond.key = 6; + SaveDataDirNameSearchResult result{}; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort key must be rejected"); + cond.key = 0; + cond.order = 2; + Require(sceSaveDataDirNameSearch(&cond, &result) == SaveDataErrorParameter, "an unknown sort order must be rejected"); +} + static void SearchAndMount() { const std::array names{std::string(31, 'A'), std::string(31, 'B')}; for (const auto& name : names) std::filesystem::create_directories(std::filesystem::path("_sd") / name); @@ -64,6 +126,7 @@ int main() { int status = 0; try { SearchAndMount(); + SortByKey(); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); status = 1; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e71c5d65c..1579e6645 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -46,6 +46,7 @@ int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); int APS5_VABI sceNetEpollDestroy(int); int APS5_VABI sceNetEpollAbort(int, int); extern const std::uint32_t sce_net_in6addr_any[4]; +extern const std::uint8_t sce_net_in6addr_linklocal_allnodes[16]; int APS5_VABI sceNetResolverCreate(const char*, int, int); int APS5_VABI sceNetResolverStartNtoa(int, const char*, void*, int, int, int); int APS5_VABI sceNetResolverStartNtoaMultipleRecordsEx(int, const char*, void*, int, int, int); @@ -255,6 +256,21 @@ static bool HostIpv6LoopbackUnavailable() { #endif } +static void CheckLinkLocalAllNodesIpv6() { + Require(reinterpret_cast(sce_net_in6addr_linklocal_allnodes) % alignof(std::uint32_t) == 0); + std::array address{}; + Require(sceNetInetPton(28, "ff02::1", address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); + std::array text; + text.fill('x'); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 7) == nullptr && *sceNetErrnoLoc() == 28); + Require(sceNetInetNtop(28, sce_net_in6addr_linklocal_allnodes, text.data(), 8) == text.data()); + Require(std::strcmp(text.data(), "ff02::1") == 0 && text[8] == 'x'); + address.fill(0xa5); + Require(sceNetInetPton(28, text.data(), address.data()) == 1); + Require(std::memcmp(sce_net_in6addr_linklocal_allnodes, address.data(), address.size()) == 0); +} + static void CheckUnspecifiedIpv6() { std::array unspecified{}; Require(sceNetInetPton(28, "::", unspecified.data()) == 1); @@ -319,6 +335,7 @@ int main() { CheckAddressText(2, "255.255.255.255"); CheckAddressText(28, "::1"); CheckAddressText(28, "1234:5678:9abc:def0:1234:5678:9abc:def0"); + CheckLinkLocalAllNodesIpv6(); CheckUnspecifiedIpv6(); const int listener = sceNetSocket(nullptr, 2, 1, 6); diff --git a/core/libs/tests/GuestSelect.cpp b/core/libs/tests/GuestSelect.cpp new file mode 100644 index 000000000..bd6cd8d50 --- /dev/null +++ b/core/libs/tests/GuestSelect.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +struct GuestTimeval { + std::int64_t seconds; + std::int64_t microseconds; +}; + +struct GuestDescriptorSet { + std::array words{}; + + void Add(int descriptor) { + words[descriptor / 64] |= (std::uint64_t{1} << (descriptor % 64)); + } + + bool Has(int descriptor) const { + return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; + } +}; + +extern "C" { +int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; +constexpr int GuestDescriptorLimit = 1024; + +int main() { + int descriptors[2]{-1, -1}; + Require(pipe_nid_postfix(descriptors) == 0); + const int reader = descriptors[0]; + const int writer = descriptors[1]; + Require(reader >= 0 && writer >= 0 && reader != writer); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 200000}; + const auto start = std::chrono::steady_clock::now(); + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(!read.Has(reader)); + } + + { + const char byte = 's'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestDescriptorSet except{}; + read.Add(reader); + except.Add(reader); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, except.words.data(), &limit) == 1); + Require(read.Has(reader)); + Require(!except.Has(reader)); + char buffer[1]{}; + Require(read_nid_postfix(reader, buffer, sizeof(buffer)) == 1); + Require(buffer[0] == byte); + } + + { + GuestDescriptorSet write{}; + write.Add(writer); + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(writer + 1, nullptr, write.words.data(), nullptr, &limit) == 1); + Require(write.Has(writer)); + } + + { + const char byte = 'e'; + Require(write_nid_postfix(writer, &byte, 1) == 1); + GuestDescriptorSet read{}; + GuestDescriptorSet write{}; + read.Add(reader); + write.Add(writer); + const int highest = reader > writer ? reader : writer; + GuestTimeval limit{0, 200000}; + Require(select_nid_postfix(highest + 1, read.words.data(), write.words.data(), nullptr, &limit) == 2); + Require(read.Has(reader) && write.Has(writer)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 100000}; + Require(select_nid_postfix(reader, read.words.data(), nullptr, nullptr, &limit) == 0); + Require(read.Has(reader)); + } + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(GuestDescriptorLimit + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEinval); + } + + Require(close_nid_postfix(reader) == 0); + Require(close_nid_postfix(writer) == 0); + + { + GuestDescriptorSet read{}; + read.Add(reader); + GuestTimeval limit{0, 50000}; + Require(select_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit) == -1); + Require(*__error_nid_postfix() == GuestEbadf); + } +} diff --git a/core/libs/tests/GuestSendfile.cpp b/core/libs/tests/GuestSendfile.cpp new file mode 100644 index 000000000..d8aa5f363 --- /dev/null +++ b/core/libs/tests/GuestSendfile.cpp @@ -0,0 +1,118 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +struct Iovec { + const void* base; + std::uint64_t length; +}; +struct Hdtr { + const Iovec* headers; + int headerCount; + const Iovec* trailers; + int trailerCount; +}; +extern "C" { +int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI close_nid_postfix(int); +std::int64_t APS5_VABI lseek_nid_postfix(int, std::int64_t, int); +int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI socketpair_nid_postfix(int, int, int, int*); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +int APS5_VABI sendfile_nid_postfix(int, int, std::int64_t, std::uint64_t, const Hdtr*, std::int64_t*, int); +int* APS5_VABI __error_nid_postfix(); +} +static void Require(bool value) { if (!value) std::abort(); } +static bool Fails(int result, int error) { return result == -1 && *__error_nid_postfix() == error; } +static std::string Receive(int descriptor, std::size_t length) { + std::string data(length, 0); + std::size_t done = 0; + while (done < length) { + const auto result = recv_nid_postfix(descriptor, data.data() + done, length - done, 0); + Require(result > 0); + done += static_cast(result); + } + return data; +} +static std::string Content(std::size_t size) { + std::string content(size, 0); + for (std::size_t i = 0; i < size; ++i) content[i] = static_cast(i * 7 % 251); + return content; +} +static int OpenWritten(const char* path, const std::string& content) { + { + std::ofstream stream(path, std::ios::binary); + stream.write(content.data(), static_cast(content.size())); + } + const int file = open_nid_postfix(path, 0, 0); + Require(file >= 0); + return file; +} +int main() { + const std::string content = Content(200000); + const auto size = static_cast(content.size()); + const int file = OpenWritten("sendfile_probe.bin", content); + int pair[2]; + Require(socketpair_nid_postfix(1, 1, 0, pair) == 0); + const Iovec header[] = {{"GET", 3}, {" ", 1}}; + const Iovec trailer[] = {{"END", 3}}; + const Hdtr parts{header, 2, trailer, 1}; + std::int64_t sent = -1; + Require(sendfile_nid_postfix(file, pair[0], 5, 10, &parts, &sent, 0) == 0 && sent == 17); + Require(Receive(pair[1], 17) == "GET " + content.substr(5, 10) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + std::string rest; + std::thread reader([&] { rest = Receive(pair[1], content.size() - 100); }); + Require(sendfile_nid_postfix(file, pair[0], 100, 0, nullptr, &sent, 0) == 0 && sent == size - 100); + reader.join(); + Require(rest == content.substr(100)); + Require(sendfile_nid_postfix(file, pair[0], size - 4, 1000, nullptr, &sent, 0) == 0 && sent == 4); + Require(Receive(pair[1], 4) == content.substr(content.size() - 4)); + Require(sendfile_nid_postfix(file, pair[0], size + 10, 0, &parts, &sent, 0) == 0 && sent == 7); + Require(Receive(pair[1], 7) == "GET END"); + const Hdtr nullHeaders{nullptr, 5, trailer, 1}; + Require(sendfile_nid_postfix(file, pair[0], 0, 3, &nullHeaders, nullptr, 0) == 0); + Require(Receive(pair[1], 6) == content.substr(0, 3) + "END"); + Require(lseek_nid_postfix(file, 0, 1) == 0); + + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], -1, 0, nullptr, &sent, 0), 22) && sent == 77); + const std::vector many(1025, Iovec{"x", 1}); + const Hdtr tooMany{many.data(), 1025, nullptr, 0}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &tooMany, &sent, 0), 22) && sent == 77); + const Hdtr negative{nullptr, 0, trailer, -1}; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, &negative, &sent, 0), 22) && sent == 77); + Require(Fails(sendfile_nid_postfix(pair[1], pair[0], 0, 0, nullptr, &sent, 0), 22) && sent == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, file, 0, 0, nullptr, &sent, 0), 9) && sent == 0); + const int datagram = socket_nid_postfix(2, 2, 0); + Require(datagram >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, datagram, 0, 0, nullptr, &sent, 0), 22) && sent == 0); + const int unconnected = socket_nid_postfix(2, 1, 0); + Require(unconnected >= 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, unconnected, size + 10, 0, nullptr, &sent, 0), 57) && sent == 0); + + const std::string large = Content(32 << 20); + const int largeFile = OpenWritten("sendfile_large.bin", large); + int on = 1; + Require(ioctl_nid_postfix(pair[0], 0x8004667e, &on) == 0); + sent = -1; + Require(Fails(sendfile_nid_postfix(largeFile, pair[0], 0, 0, nullptr, &sent, 0), 35)); + Require(sent > 0 && sent < static_cast(large.size())); + Require(Receive(pair[1], static_cast(sent)) == large.substr(0, static_cast(sent))); + + Require(close_nid_postfix(largeFile) == 0 && close_nid_postfix(file) == 0); + sent = 77; + Require(Fails(sendfile_nid_postfix(file, pair[0], 0, 0, nullptr, &sent, 0), 9) && sent == 77); + Require(close_nid_postfix(datagram) == 0 && close_nid_postfix(unconnected) == 0); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + std::filesystem::remove("sendfile_probe.bin"); + std::filesystem::remove("sendfile_large.bin"); +} diff --git a/core/libs/tests/GuestShmOpen.cpp b/core/libs/tests/GuestShmOpen.cpp index 112ae3c90..c4f3c16f4 100644 --- a/core/libs/tests/GuestShmOpen.cpp +++ b/core/libs/tests/GuestShmOpen.cpp @@ -37,6 +37,10 @@ int main() { Require(pread_nid_postfix(first, &last, 1, 0x3fff) == 1 && last == 0); Require(pread_nid_postfix(first, &last, 1, 0x4000) == 0); Require(pread_nid_postfix(second, back, sizeof(back), 0) == 0); +#ifndef _WIN32 + Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == 0); +#endif + Require(ftruncate_nid_postfix(third, 0x4000) == 0); Require(pwrite_nid_disambig1_nid_postfix(third, text, sizeof(text), 0) == static_cast(sizeof(text))); Require(close_nid_postfix(first) == 0 && close_nid_postfix(second) == 0 && close_nid_postfix(third) == 0); RequireInvalid(anonymous, 0x0); @@ -47,6 +51,7 @@ int main() { RequireInvalid(anonymous, 0x2 | 0x4); RequireInvalid("/anyps5-shm", 0x1); RequireInvalid("/anyps5-shm", 0x202 | 0x8); +#ifdef _WIN32 bool threw = false; try { shm_open_nid_postfix("/anyps5-shm", 0x202, 0600); @@ -54,4 +59,7 @@ int main() { threw = true; } Require(threw); +#else + RequireInvalid("", 0x202); +#endif } diff --git a/core/libs/tests/GuestSigninDialog.cpp b/core/libs/tests/GuestSigninDialog.cpp new file mode 100644 index 000000000..d8410fcbd --- /dev/null +++ b/core/libs/tests/GuestSigninDialog.cpp @@ -0,0 +1,77 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceSigninDialogInitialize(void); +int APS5_VABI sceSigninDialogOpen(const void* param); +int APS5_VABI sceSigninDialogUpdateStatus(void); +int APS5_VABI sceSigninDialogGetStatus(void); +int APS5_VABI sceSigninDialogGetResult(void* result); +int APS5_VABI sceSigninDialogClose(void); +int APS5_VABI sceSigninDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusNone = 0; +constexpr int kStatusInitialized = 1; +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[4]; + std::memset(result, 0x5a, sizeof(result)); + + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogOpen(¶m) == kErrNotInitialized); + Require(sceSigninDialogClose() == kErrNotInitialized); + Require(sceSigninDialogGetResult(result) == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + Require(sceSigninDialogUpdateStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogInitialize() == kErrAlreadyInitialized); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + + Require(sceSigninDialogOpen(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == kErrNotFinished); + + Require(sceSigninDialogOpen(¶m) == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + Require(sceSigninDialogUpdateStatus() == kStatusFinished); + + Require(sceSigninDialogGetResult(nullptr) == kErrArgNull); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogTerminate() == 0); + Require(sceSigninDialogTerminate() == kErrNotInitialized); + Require(sceSigninDialogGetStatus() == kStatusNone); + + Require(sceSigninDialogInitialize() == 0); + Require(sceSigninDialogGetStatus() == kStatusInitialized); + Require(sceSigninDialogClose() == 0); + Require(sceSigninDialogGetStatus() == kStatusFinished); + std::memset(result, 0xa5, sizeof(result)); + Require(sceSigninDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == 0); + Require(result[2] == 0); + Require(result[3] == 0); + Require(sceSigninDialogTerminate() == 0); +} diff --git a/core/libs/tests/GuestSigtimedwait.cpp b/core/libs/tests/GuestSigtimedwait.cpp index d3b056aa7..83d93847a 100644 --- a/core/libs/tests/GuestSigtimedwait.cpp +++ b/core/libs/tests/GuestSigtimedwait.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include struct GuestSignalSet { @@ -10,22 +11,71 @@ struct GuestTimespec { std::int64_t seconds; std::int64_t nanoseconds; }; -extern "C" int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +using Handler = void (APS5_VABI *)(int); +extern "C" { +int APS5_VABI sigtimedwait_nid_postfix(const GuestSignalSet*, void*, const GuestTimespec*); +int APS5_VABI sigwaitinfo_nid_postfix(const GuestSignalSet*, void*); +int APS5_VABI sigwait_nid_postfix(const GuestSignalSet*, int*); +int APS5_VABI pause_nid_postfix(void); +Handler APS5_VABI signal_nid_postfix(int, Handler); +int APS5_VABI raise_nid_postfix(int); +int APS5_VABI _sigprocmask_nid_postfix(int, const GuestSignalSet*, GuestSignalSet*); +} +volatile int received = 0; +void APS5_VABI Callback(int value) { received = value; } static void Require(bool value) { if (!value) std::abort(); } -static bool ReportsNotImplemented(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { +template +static bool ReportsNotImplemented(TCall call, const char* message) { try { - sigtimedwait_nid_postfix(set, info, timeout); + call(); } catch (const std::runtime_error& error) { - return std::string(error.what()) == "sigtimedwait: waiting for a pending signal not implemented"; + return std::string(error.what()) == message; } return false; } +static bool SigtimedwaitReportsNotImplemented(const GuestSignalSet* set, void* info, const GuestTimespec* timeout) { + return ReportsNotImplemented([&] { sigtimedwait_nid_postfix(set, info, timeout); }, "sigtimedwait: waiting for a pending signal not implemented"); +} +static bool SigwaitinfoReportsNotImplemented(const GuestSignalSet* set, void* info) { + return ReportsNotImplemented([&] { sigwaitinfo_nid_postfix(set, info); }, "sigwaitinfo: waiting for a pending signal not implemented"); +} +static bool SigwaitReportsNotImplemented(const GuestSignalSet* set, int* taken) { + return ReportsNotImplemented([&] { sigwait_nid_postfix(set, taken); }, "sigwait: waiting for a pending signal not implemented"); +} int main() { const GuestSignalSet pipe{{1u << 12, 0, 0, 0}}; + const GuestSignalSet empty{{0, 0, 0, 0}}; + const GuestSignalSet interrupt{{1u << 1, 0, 0, 0}}; unsigned char info[64] = {}; + const unsigned char untouched[64] = {}; const GuestTimespec poll{0, 0}; const GuestTimespec second{1, 0}; - Require(ReportsNotImplemented(&pipe, info, &poll)); - Require(ReportsNotImplemented(&pipe, nullptr, &second)); - Require(ReportsNotImplemented(&pipe, info, nullptr)); + Require(SigtimedwaitReportsNotImplemented(&pipe, info, &poll)); + Require(SigtimedwaitReportsNotImplemented(&pipe, nullptr, &second)); + Require(SigtimedwaitReportsNotImplemented(&pipe, info, nullptr)); + + Require(SigwaitinfoReportsNotImplemented(&pipe, info)); + Require(SigwaitinfoReportsNotImplemented(&pipe, nullptr)); + Require(SigwaitinfoReportsNotImplemented(&empty, info)); + Require(std::memcmp(info, untouched, sizeof(info)) == 0); + + int taken = -1; + Require(SigwaitReportsNotImplemented(&pipe, &taken)); + Require(SigwaitReportsNotImplemented(&empty, &taken)); + Require(taken == -1); + + Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); + + GuestSignalSet previous{}; + Require(signal_nid_postfix(2, Callback) != reinterpret_cast(static_cast(-1))); + Require(_sigprocmask_nid_postfix(1, &interrupt, &previous) == 0); + Require(raise_nid_postfix(2) == 0 && received == 0); + Require(SigwaitReportsNotImplemented(&interrupt, &taken) && taken == -1); + Require(SigwaitinfoReportsNotImplemented(&interrupt, info)); + Require(std::memcmp(info, untouched, sizeof(info)) == 0); + GuestSignalSet current{}; + Require(_sigprocmask_nid_postfix(1, nullptr, ¤t) == 0 && current.bits[0] == (previous.bits[0] | interrupt.bits[0])); + Require(_sigprocmask_nid_postfix(3, &previous, nullptr) == 0); + Require(raise_nid_postfix(2) == 0 && received == 2); + Require(ReportsNotImplemented([] { pause_nid_postfix(); }, "pause: waiting for a delivered signal not implemented")); } diff --git a/core/libs/tests/GuestSocketError.cpp b/core/libs/tests/GuestSocketError.cpp new file mode 100644 index 000000000..466bb1b8e --- /dev/null +++ b/core/libs/tests/GuestSocketError.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/Socket/include/SocketError.hpp" +#include +#include +#include + +struct ErrorCase { + const char* name; + int host; + int guest; +}; + +#define HostError(prefix, name, guest) {#prefix #name, prefix##name, guest} + +#ifdef _WIN32 +static constexpr ErrorCase Cases[] = { + HostError(WSA, EINTR, 4), + HostError(WSA, EBADF, 9), + HostError(WSA, EACCES, 13), + HostError(WSA, EFAULT, 14), + HostError(WSA, EINVAL, 22), + HostError(WSA, EMFILE, 24), + HostError(WSA, EWOULDBLOCK, 35), + HostError(WSA, EINPROGRESS, 36), + HostError(WSA, EALREADY, 37), + HostError(WSA, ENOTSOCK, 38), + HostError(WSA, EDESTADDRREQ, 39), + HostError(WSA, EMSGSIZE, 40), + HostError(WSA, EPROTOTYPE, 41), + HostError(WSA, ENOPROTOOPT, 42), + HostError(WSA, EPROTONOSUPPORT, 43), + HostError(WSA, ESOCKTNOSUPPORT, 44), + HostError(WSA, EOPNOTSUPP, 45), + HostError(WSA, EPFNOSUPPORT, 46), + HostError(WSA, EAFNOSUPPORT, 47), + HostError(WSA, EADDRINUSE, 48), + HostError(WSA, EADDRNOTAVAIL, 49), + HostError(WSA, ENETDOWN, 50), + HostError(WSA, ENETUNREACH, 51), + HostError(WSA, ENETRESET, 52), + HostError(WSA, ECONNABORTED, 53), + HostError(WSA, ECONNRESET, 54), + HostError(WSA, ENOBUFS, 55), + HostError(WSA, EISCONN, 56), + HostError(WSA, ENOTCONN, 57), + HostError(WSA, ETOOMANYREFS, 59), + HostError(WSA, ETIMEDOUT, 60), + HostError(WSA, ECONNREFUSED, 61), + HostError(WSA, EHOSTDOWN, 64), + HostError(WSA, EHOSTUNREACH, 65), +}; +static constexpr int UnmappedHost = WSAEDISCON; +static constexpr int ShutdownHost = WSAESHUTDOWN; +#else +static constexpr ErrorCase Cases[] = { + HostError(, EPERM, 1), + HostError(, EINTR, 4), + HostError(, EBADF, 9), + HostError(, ENOMEM, 12), + HostError(, EACCES, 13), + HostError(, EFAULT, 14), + HostError(, EINVAL, 22), + HostError(, ENFILE, 23), + HostError(, EMFILE, 24), + HostError(, EPIPE, 32), + HostError(, EAGAIN, 35), + HostError(, EWOULDBLOCK, 35), + HostError(, EINPROGRESS, 36), + HostError(, EALREADY, 37), + HostError(, ENOTSOCK, 38), + HostError(, EDESTADDRREQ, 39), + HostError(, EMSGSIZE, 40), + HostError(, EPROTOTYPE, 41), + HostError(, ENOPROTOOPT, 42), + HostError(, EPROTONOSUPPORT, 43), + HostError(, ESOCKTNOSUPPORT, 44), + HostError(, EOPNOTSUPP, 45), + HostError(, EPFNOSUPPORT, 46), + HostError(, EAFNOSUPPORT, 47), + HostError(, EADDRINUSE, 48), + HostError(, EADDRNOTAVAIL, 49), + HostError(, ENETDOWN, 50), + HostError(, ENETUNREACH, 51), + HostError(, ENETRESET, 52), + HostError(, ECONNABORTED, 53), + HostError(, ECONNRESET, 54), + HostError(, ENOBUFS, 55), + HostError(, EISCONN, 56), + HostError(, ENOTCONN, 57), + HostError(, ESHUTDOWN, 58), + HostError(, ETOOMANYREFS, 59), + HostError(, ETIMEDOUT, 60), + HostError(, ECONNREFUSED, 61), + HostError(, EHOSTDOWN, 64), + HostError(, EHOSTUNREACH, 65), + HostError(, EPROTO, 92), +}; +static constexpr int UnmappedHost = ENOENT; +#endif + +int main() { + int failures = 0; + for (const auto& entry : Cases) { + const int guest = GuestSockets::GuestErrorFromHost(entry.host); + if (guest != entry.guest) { + std::fprintf(stderr, "%s (%d): guest %d, FreeBSD %d\n", entry.name, entry.host, guest, entry.guest); + ++failures; + } + } + if (GuestSockets::GuestErrorFromHost(UnmappedHost) != 5) { + std::fprintf(stderr, "a host error with no FreeBSD socket counterpart is not EIO\n"); + ++failures; + } + for (const int host : {0, -1}) { + if (GuestSockets::GuestErrorFromHost(host) != 5) { + std::fprintf(stderr, "host error %d is not EIO\n", host); + ++failures; + } + } +#ifdef _WIN32 + if (GuestSockets::GuestErrorFromHost(ShutdownHost) != 32) { + std::fprintf(stderr, "WSAESHUTDOWN is not EPIPE\n"); + ++failures; + } +#endif + return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index cb428b927..5de08e2cb 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #ifndef _WIN32 #include #endif @@ -55,6 +56,7 @@ int APS5_VABI _Getmbcurmax_nid_postfix(); int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI flockfile_nid_postfix(FileStream*); void APS5_VABI funlockfile_nid_postfix(FileStream*); +int APS5_VABI ftrylockfile_nid_postfix(FileStream*); } static void Require(bool value) { if (!value) std::abort(); } static int APS5_VABI WriteFormatted(FileStream* stream, const char* format, ...) { @@ -134,6 +136,57 @@ static void CheckUnlockedStreams() { Require(std::filesystem::remove(filename)); } +static void CheckTryLock() { + FileStream stream(std::tmpfile()); + const auto tryFromOtherThread = [&] { + int result = 0; + std::thread other([&] { + result = ftrylockfile_nid_postfix(&stream); + if (result == 0) funlockfile_nid_postfix(&stream); + }); + other.join(); + return result; + }; + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(ftrylockfile_nid_postfix(&stream) == 0); + Require(tryFromOtherThread() == -1); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == -1); + std::atomic written = false; + std::thread writer([&] { + Require(fputc_nid_postfix('B', &stream) == 'B'); + written = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(!written && fputc_nid_postfix('A', &stream) == 'A'); + funlockfile_nid_postfix(&stream); + writer.join(); + Require(written && tryFromOtherThread() == 0); + std::atomic held = false, release = false; + std::thread owner([&] { + flockfile_nid_postfix(&stream); + held = true; + while (!release) std::this_thread::yield(); + funlockfile_nid_postfix(&stream); + }); + while (!held) std::this_thread::yield(); + const int contended = ftrylockfile_nid_postfix(&stream); + release = true; + owner.join(); + Require(contended == -1 && ftrylockfile_nid_postfix(&stream) == 0); + funlockfile_nid_postfix(&stream); + Require(tryFromOtherThread() == 0); + Require(fseeko_nid_postfix(&stream, 0, SEEK_SET) == 0); + Require(fgetc_nid_postfix(&stream) == 'A' && fgetc_nid_postfix(&stream) == 'B'); + stream.Close(); + bool threw = false; + try { ftrylockfile_nid_postfix(&stream); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + threw = false; + try { ftrylockfile_nid_postfix(nullptr); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); +} + static void CheckUnlockedStreamLock(bool write) { FileStream stream(std::tmpfile()); Require(std::fputc('A', stream.GetHandle()) == 'A'); @@ -461,5 +514,6 @@ int main() { CheckUnlockedStreams(); CheckUnlockedStreamLock(false); CheckUnlockedStreamLock(true); + CheckTryLock(); return CheckBinaryModes() ? 0 : 1; } diff --git a/core/libs/tests/GuestSyslog.cpp b/core/libs/tests/GuestSyslog.cpp index a4634c5d6..3f0270eea 100644 --- a/core/libs/tests/GuestSyslog.cpp +++ b/core/libs/tests/GuestSyslog.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #ifdef _WIN32 #include #else @@ -10,6 +11,7 @@ extern "C" { void APS5_VABI syslog_nid_postfix(int, const char*, ...); +int APS5_VABI setlogmask_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -25,11 +27,38 @@ int main() { Require(saved >= 0 && ::dup2(::fileno(captured), ::fileno(stderr)) == ::fileno(stderr)); #endif *__error_nid_postfix() = 2; + Require(setlogmask_nid_postfix(0) == 0xff); syslog_nid_postfix(6, "station %s at %d kbps", "radio", 128); const int afterFirst = *__error_nid_postfix(); syslog_nid_postfix(16 | 3, "open: %m, %%m kept, %d%%\n", 50); syslog_nid_postfix(0x10000 | 4, "masked"); syslog_nid_postfix(7, ""); + Require(setlogmask_nid_postfix(1 << 3) == 0xff); + Require(setlogmask_nid_postfix(0) == (1 << 3)); + int count = -1; + syslog_nid_postfix(7, "suppressed %m%n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + syslog_nid_postfix(16 | 3, "allowed: %m"); + Require(*__error_nid_postfix() == 2); + syslog_nid_postfix(0x10000 | 7, "suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(1 << 4) == (1 << 3)); + syslog_nid_postfix(0x10000 | 4, "warning without diagnostic"); + syslog_nid_postfix(0x10000 | 7, "both suppressed %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(0x100) == (1 << 4)); + Require(setlogmask_nid_postfix(0) == 0x100); + syslog_nid_postfix(0x10000 | 3, "no priorities enabled %n", &count); + Require(count == -1 && *__error_nid_postfix() == 2); + Require(setlogmask_nid_postfix(-1) == 0x100); + Require(setlogmask_nid_postfix(0) == -1); + syslog_nid_postfix(7, "negative mask"); + Require(setlogmask_nid_postfix(0x55) == -1); + for (int priority = 0; priority < 8; ++priority) + syslog_nid_postfix(16 | priority, "level %d", priority); + std::thread([] { Require(setlogmask_nid_postfix(0xff) == 0x55); }).join(); + Require(setlogmask_nid_postfix(0) == 0xff); + syslog_nid_postfix(7, "mask shared across threads"); std::fflush(stderr); #ifdef _WIN32 Require(::_dup2(saved, ::_fileno(stderr)) == 0 && ::_close(saved) == 0); @@ -38,14 +67,23 @@ int main() { #endif Require(afterFirst == 2 && *__error_nid_postfix() == 2); std::rewind(captured); - char text[512]{}; + char text[1024]{}; const auto length = std::fread(text, 1, sizeof(text) - 1, captured); const char* expected = "[syslog:14] station radio at 128 kbps\n" "[syslog:19] open: No such file or directory, %m kept, 50%\n" "[syslog:35] syslog: unknown facility/priority: 10004\n" "[syslog:12] masked\n" - "[syslog:15] \n"; + "[syslog:15] \n" + "[syslog:19] allowed: No such file or directory\n" + "[syslog:35] syslog: unknown facility/priority: 10007\n" + "[syslog:12] warning without diagnostic\n" + "[syslog:15] negative mask\n" + "[syslog:16] level 0\n" + "[syslog:18] level 2\n" + "[syslog:20] level 4\n" + "[syslog:22] level 6\n" + "[syslog:15] mask shared across threads\n"; Require(length == std::strlen(expected) && std::strcmp(text, expected) == 0); Require(std::fclose(captured) == 0); } diff --git a/core/libs/tests/GuestSysmodule.cpp b/core/libs/tests/GuestSysmodule.cpp index 4a63bbd37..77ec859f5 100644 --- a/core/libs/tests/GuestSysmodule.cpp +++ b/core/libs/tests/GuestSysmodule.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #ifdef __linux__ @@ -17,6 +18,7 @@ int APS5_VABI sceSysmoduleLoadModule(std::uint16_t id); int APS5_VABI sceSysmoduleUnloadModule(std::uint16_t id); int APS5_VABI sceSysmoduleLoadModuleInternal(std::uint32_t id); int APS5_VABI sceSysmoduleUnloadModuleInternal(std::uint32_t id); +int APS5_VABI sceSysmoduleLoadModuleInternalWithArg(std::uint32_t id, int argc, void* argv, std::uint64_t unk, int* ret); int APS5_VABI sceSysmoduleGetModuleInfoForUnwind(std::uint64_t addr, int flags, ModuleInfoForUnwind* info); int APS5_VABI sceSysmoduleGetModuleHandleInternal(std::uint32_t id, std::int32_t* handle); int APS5_VABI sceKernelDlsym(std::int32_t handle, const char* symbol, void** addr); @@ -38,8 +40,11 @@ bool ThrowsRuntimeError(TFunction function) { constexpr std::uint16_t kFiberModuleId = 0x0006; constexpr std::uint16_t kUltModuleId = 0x0007; +constexpr int kInvalidValue = static_cast(0x805A1000); constexpr int kModuleUnloaded = static_cast(0x805A1001); +constexpr std::uint32_t kInternalFlag = 0x80000000u; constexpr std::uint16_t kTrophy2ModuleId = 0x0110; +constexpr std::uint32_t kAgcModuleId = 0x80000094; #ifdef __linux__ struct Mapping { @@ -98,6 +103,17 @@ void CheckUnwindInfo() { } int main() { + Require(sceSysmoduleIsLoaded(0) == kInvalidValue); + Require(sceSysmoduleLoadModule(0) == kInvalidValue); + Require(sceSysmoduleUnloadModule(0) == kInvalidValue); + for (std::uint32_t id : {0u, kInternalFlag}) { + Require(sceSysmoduleLoadModuleInternal(id) == kInvalidValue); + Require(sceSysmoduleUnloadModuleInternal(id) == kInvalidValue); + int result = 0x5a5a5a5a; + Require(sceSysmoduleLoadModuleInternalWithArg(id, 0, nullptr, 0, &result) == kInvalidValue); + Require(result == 0x5a5a5a5a); + } + Require(sceSysmoduleIsLoaded(kFiberModuleId) == kModuleUnloaded); Require(sceSysmoduleLoadModuleInternal(kFiberModuleId) == 0); Require(sceSysmoduleIsLoaded(kFiberModuleId) == 0); @@ -124,6 +140,13 @@ int main() { Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == 0); Require(sceSysmoduleIsLoaded(kTrophy2ModuleId) == kModuleUnloaded); Require(sceSysmoduleUnloadModuleInternal(kTrophy2ModuleId) == kModuleUnloaded); + + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); + Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleLoadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == 0); + Require(sceSysmoduleUnloadModuleInternal(kAgcModuleId) == kModuleUnloaded); #ifdef __linux__ CheckUnwindInfo(); #endif diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index ffba40d37..e29adaa60 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -21,6 +21,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sysctl_nid_postfix(const int*, std::uint32_t, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sysctlbyname_nid_postfix(const char*, void*, std::size_t*, const void*, std::size_t); int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion*); +int APS5_VABI getpid_nid_postfix(); extern char** environ_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -56,7 +57,7 @@ static void CheckProcessorCountSysctl() { } Require(threw); threw = false; - const int unknown[] = {6, 5}; + const int unknown[] = {6, 99}; try { sysctl_nid_postfix(unknown, 2, &value, &length, nullptr, 0); } catch (const std::runtime_error&) { @@ -64,6 +65,77 @@ static void CheckProcessorCountSysctl() { } Require(threw); } +static void CheckPhysicalMemorySysctl() { + const int mib[] = {6, 5}; + const std::uint64_t expected = static_cast(sysconf_nid_postfix(121)) * 0x4000; + *__error_nid_postfix() = 13; + std::size_t length = 0; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == 0 && length == sizeof(std::uint64_t)); + std::uint64_t value = 0; + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + value = 0; + length = 16; + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, nullptr, 0) == 0 && value == expected && length == sizeof(std::uint64_t)); + Require(*__error_nid_postfix() == 13); + unsigned char bytes[8] = {0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa}; + length = 4; + Require(sysctlbyname_nid_postfix("hw.physmem", bytes, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 4 && bytes[4] == 0xaa); + length = sizeof(value); + Require(sysctl_nid_postfix(mib, 2, &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); + Require(sysctlbyname_nid_postfix("hw.physmem", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); +} +static void CheckMachdepCachelineSizeSysctl() { + *__error_nid_postfix() = 13; + int value = 0x5a5a5a5a; + std::size_t length = sizeof(value); + Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 2); + Require(value == 0x5a5a5a5a); +} +static void CheckPathnameSysctl() { + *__error_nid_postfix() = 13; + char buffer[32] = {}; + std::size_t length = sizeof(buffer); + Require(sysctlbyname_nid_postfix("kern.proc.pathname", buffer, &length, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 22); + + const int mibTooShort[] = {1, 14, 12}; + Require(sysctl_nid_postfix(mibTooShort, 3, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibTooLong[] = {1, 14, 12, -1, 0}; + Require(sysctl_nid_postfix(mibTooLong, 5, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 22); + + const int mibSelf[] = {1, 14, 12, -1}; + length = 0; + Require(sysctl_nid_postfix(mibSelf, 4, nullptr, &length, nullptr, 0) == 0 && length == sizeof("/app0/eboot.bin")); + + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int callerPid = getpid_nid_postfix(); + const int mibPid[] = {1, 14, 12, callerPid}; + std::memset(buffer, 0xaa, sizeof(buffer)); + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibPid, 4, buffer, &length, nullptr, 0) == 0); + Require(length == sizeof("/app0/eboot.bin")); + Require(std::strcmp(buffer, "/app0/eboot.bin") == 0); + + const int mibForeign[] = {1, 14, 12, callerPid + 1000}; + Require(sysctl_nid_postfix(mibForeign, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 3); + + length = 5; + std::memset(buffer, 0xaa, sizeof(buffer)); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 12); + Require(length == 5 && buffer[4] == '0' && buffer[5] == static_cast(0xaa)); + + length = sizeof(buffer); + Require(sysctl_nid_postfix(mibSelf, 4, buffer, &length, buffer, sizeof(buffer)) == -1 && *__error_nid_postfix() == 1); +} static void CheckSystemSwVersion() { *__error_nid_postfix() = 13; Require(sceKernelGetSystemSwVersion_nid_postfix(nullptr) == SCE_KERNEL_ERROR_EFAULT && *__error_nid_postfix() == 13); @@ -109,6 +181,9 @@ static void CheckPseudoRandomSysctl() { } int main() { CheckProcessorCountSysctl(); + CheckPhysicalMemorySysctl(); + CheckMachdepCachelineSizeSysctl(); + CheckPathnameSysctl(); CheckSystemSwVersion(); CheckPseudoRandomSysctl(); *__error_nid_postfix() = 13; diff --git a/core/libs/tests/GuestTextToSpeech2.cpp b/core/libs/tests/GuestTextToSpeech2.cpp index f3ee2c80e..9bac0096e 100644 --- a/core/libs/tests/GuestTextToSpeech2.cpp +++ b/core/libs/tests/GuestTextToSpeech2.cpp @@ -3,27 +3,30 @@ #include #include #include -#include extern "C" { std::int32_t APS5_VABI sceTextToSpeech2Initialize(const void* param); int APS5_VABI sceTextToSpeech2Open(); +int APS5_VABI sceTextToSpeech2Cancel(); +int APS5_VABI sceTextToSpeech2Close(); +int APS5_VABI sceTextToSpeech2GetSpeechStatus(); +int APS5_VABI sceTextToSpeech2Speak(); +int APS5_VABI sceTextToSpeech2Terminate(); +int APS5_VABI sceTextToSpeech2GetSystemStatus(); } int main() { + constexpr std::int32_t unsupported = static_cast(0x8002002D); const std::uint32_t param[12]{0x2000000, 0x26c}; - if (sceTextToSpeech2Initialize(param) != static_cast(0x8002002D)) { + if (sceTextToSpeech2Initialize(param) != unsupported) { std::puts("TextToSpeech2: initialization did not report the unsupported operation"); std::abort(); } - bool threw = false; - try { - sceTextToSpeech2Open(); - } catch (const std::runtime_error&) { - threw = true; - } - if (!threw) { - std::puts("TextToSpeech2: open after failed initialization did not throw"); + if (sceTextToSpeech2Open() != unsupported || sceTextToSpeech2Cancel() != unsupported || + sceTextToSpeech2Close() != unsupported || sceTextToSpeech2GetSpeechStatus() != unsupported || + sceTextToSpeech2Speak() != unsupported || sceTextToSpeech2Terminate() != unsupported || + sceTextToSpeech2GetSystemStatus() != unsupported) { + std::puts("TextToSpeech2: later calls did not report the unsupported operation"); std::abort(); } std::puts("TextToSpeech2 tests passed"); diff --git a/core/libs/tests/GuestTruncate.cpp b/core/libs/tests/GuestTruncate.cpp index 57f387546..0f8d4b1e5 100644 --- a/core/libs/tests/GuestTruncate.cpp +++ b/core/libs/tests/GuestTruncate.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -10,6 +11,8 @@ #define NOMINMAX #endif #include +#else +#include #endif extern "C" { @@ -74,8 +77,12 @@ int main() { Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); #else std::filesystem::permissions(file, std::filesystem::perms::owner_read); - RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); - Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + if (::geteuid() != 0) { + RequireFailure(truncate_nid_postfix(path.c_str(), 1), GuestEacces); + Require(sceKernelTruncate_nid_postfix(path.c_str(), 1) == SceKernelErrorBase + GuestEacces); + } else { + std::fprintf(stderr, "running as root, which ignores file permissions: the read-only cases not tested\n"); + } std::filesystem::permissions(file, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write); #endif Require(Contents(file) == "locked"); diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 6756fbace..2f69a2201 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -68,6 +68,13 @@ int main() { Require(getsockopt_nid_postfix(sender, 0xffff, 0x20, &option, &optionSize) == 0 && option != 0); Require(setsockopt_nid_postfix(sender, 0xffff, 12345, &enabled, sizeof(enabled)) == -1); Require(*__error_nid_postfix() == 42); +#ifndef _WIN32 + const int unconnected = socket_nid_postfix(2, 2, 0); + Require(unconnected >= 0); + Require(sendto_nid_postfix(unconnected, "x", 1, 0, nullptr, 0) == -1); + Require(*__error_nid_postfix() == 39); + Require(close_nid_postfix(unconnected) == 0); +#endif const int unsized = socket_nid_postfix(2, 2, 0); std::array unsizedAddress{0, 2, 0, 0, 127, 0, 0, 1}; Require(unsized >= 0 && bind_nid_postfix(unsized, unsizedAddress.data(), unsizedAddress.size()) == 0); diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index 26f5e126c..fdf347e64 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -22,6 +22,14 @@ int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uin int APS5_VABI _sceUltSemaphoreCreate(void*, const char*, std::int32_t, void*, const void*, std::uint32_t); int APS5_VABI sceUltSemaphoreAcquire(void*, std::int32_t); int APS5_VABI sceUltSemaphoreRelease(void*, std::int32_t); +int APS5_VABI _sceUltMutexOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI _sceUltQueueDataResourcePoolCreate(void*, const char*, std::uint32_t, std::uint64_t, std::uint32_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI _sceUltQueueCreate(void*, const char*, std::uint64_t, void*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltQueuePush(void*, const void*); +int APS5_VABI sceUltQueuePop(void*, void*); } static constexpr int Ok = 0; @@ -150,6 +158,25 @@ static void UnderscoredCreate() { CHECK(_sceUltSemaphoreCreate(semaphore, "semaphore", 2, pool, nullptr, 0) == State); CHECK(sceUltSemaphoreAcquire(semaphore, 2) == Ok); CHECK(sceUltSemaphoreRelease(semaphore, 2) == Ok); + alignas(8) static std::uint8_t mutexOptParam[128]; + alignas(8) static std::uint8_t mutex[256]; + std::memset(mutexOptParam, 0xFF, sizeof(mutexOptParam)); + CHECK(_sceUltMutexOptParamInitialize(nullptr, 0) == Null); + CHECK(_sceUltMutexOptParamInitialize(mutexOptParam, 0) == Ok); + CHECK(mutexOptParam[8] == 0 && mutexOptParam[15] == 0); + CHECK(_sceUltMutexCreate(nullptr, "mutex", pool, mutexOptParam, 0) == Null); + CHECK(_sceUltMutexCreate(mutex, "mutex", pool, mutexOptParam, 0) == Ok); + CHECK(sceUltMutexLock(mutex) == Ok); + CHECK(sceUltMutexUnlock(mutex) == Ok); + alignas(8) static std::uint8_t dataPool[512]; + alignas(8) static std::uint8_t queue[512]; + CHECK(_sceUltQueueDataResourcePoolCreate(nullptr, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Null); + CHECK(_sceUltQueueDataResourcePoolCreate(dataPool, "data", 1, 8, 1, pool, nullptr, nullptr, 0) == Ok); + CHECK(_sceUltQueueCreate(nullptr, "queue", 8, pool, dataPool, nullptr, 0) == Null); + CHECK(_sceUltQueueCreate(queue, "queue", 8, pool, dataPool, nullptr, 0) == Ok); + std::uint64_t value = 0x1122334455667788ull, output = 0; + CHECK(sceUltQueuePush(queue, &value) == Ok); + CHECK(sceUltQueuePop(queue, &output) == Ok && output == value); } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index d67250976..48ec61d50 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,7 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; static constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -211,6 +212,63 @@ static void WakeWithoutWaitersSucceeds() { Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, INT_MAX) == 0); } +static void NwakeWakesEveryWaiterOnEachListedAddress() { + std::uint32_t first = 1; + std::uint64_t second = 1; + std::uint32_t unlisted = 1; + std::atomic started{0}; + std::atomic finished{0}; + std::atomic unlistedFinished{0}; + std::vector results(4, -2); + int unlistedResult = -2; + + std::vector waiters; + for (int i = 0; i < 3; ++i) { + waiters.emplace_back([&, i] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + ++finished; + }); + } + waiters.emplace_back([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + results[3] = Umtx(&second, UMTX_OP_WAIT, 1, nullptr, &failsafe); + ++finished; + }); + std::thread unlistedWaiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + ++unlistedFinished; + }); + AwaitCount(started, 5); + std::this_thread::sleep_for(SETTLE); + + void* addresses[] = {&first, nullptr, &second}; + Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 3) == 0); + AwaitCount(finished, 4); + std::this_thread::sleep_for(SETTLE); + Require(unlistedFinished.load() == 0); + + Require(Umtx(&unlisted, UMTX_OP_WAKE_PRIVATE, 1) == 0); + for (auto& waiter : waiters) waiter.join(); + unlistedWaiter.join(); + for (const int result : results) Require(result == 0); + Require(unlistedResult == 0); +} + +static void NwakeReadsOnlyTheCountedEntries() { + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffff00000000ULL) == 0); + Require(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 0xffffffffULL) == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_NWAKE_PRIVATE, 1), GUEST_EFAULT)); + std::uint32_t word = 0; + void* addresses[] = {&word}; + Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -244,5 +302,7 @@ int main() { WakeReleasesAWaiterWithoutAValueChange(); WakeHonoursTheCountAndTheAddress(); WakeWithoutWaitersSucceeds(); + NwakeWakesEveryWaiterOnEachListedAddress(); + NwakeReadsOnlyTheCountedEntries(); RejectsBadObjectsAndOperations(); } diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 086487a48..e91e1da8c 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -21,7 +21,25 @@ void APS5_VABI sceUsbdExit(); std::int64_t APS5_VABI sceUsbdGetDeviceList(void*** list); void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices); std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout); -int APS5_VABI sceUsbdOpen(); +std::int32_t APS5_VABI sceUsbdOpen(void* device, void** deviceHandle); +void APS5_VABI sceUsbdClose(void* deviceHandle); +void* APS5_VABI sceUsbdRefDevice(void* device); +void APS5_VABI sceUsbdUnrefDevice(void* device); +std::uint8_t APS5_VABI sceUsbdGetBusNumber(void* device); +std::uint8_t APS5_VABI sceUsbdGetDeviceAddress(void* device); +std::int32_t APS5_VABI sceUsbdGetDeviceDescriptor(void* device, void* descriptor); +std::int32_t APS5_VABI sceUsbdGetConfigDescriptor(void* device, std::uint8_t configIndex, void** config); +std::int32_t APS5_VABI sceUsbdGetActiveConfigDescriptor(void* device, void** config); +std::int32_t APS5_VABI sceUsbdClaimInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdReleaseInterface(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdSetConfiguration(void* deviceHandle, std::int32_t configuration); +std::int32_t APS5_VABI sceUsbdResetDevice(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdCheckConnected(void* deviceHandle); +std::int32_t APS5_VABI sceUsbdKernelDriverActive(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdAttachKernelDriver(void* deviceHandle, std::int32_t interfaceNumber); +std::int32_t APS5_VABI sceUsbdControlTransfer(void* deviceHandle, std::uint8_t requestType, std::uint8_t request, std::uint16_t value, std::uint16_t index, std::uint8_t* data, std::uint16_t length, std::uint32_t timeout); +std::int32_t APS5_VABI sceUsbdGetStringDescriptor(void* deviceHandle, std::uint8_t descriptorIndex, std::uint16_t languageId, std::uint8_t* data, std::int32_t length); +std::int32_t APS5_VABI sceUsbdSubmitTransfer(void* transfer); void* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets); void APS5_VABI sceUsbdFreeTransfer(void* transfer); void APS5_VABI sceUsbdFillInterruptTransfer(void* transfer, void* deviceHandle, std::uint8_t endpoint, std::uint8_t* buffer, std::int32_t length, void* callback, void* userData, std::uint32_t timeout); @@ -41,6 +59,16 @@ void Require(bool condition, const char* message) { } } +template +bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + } int main() { @@ -57,13 +85,37 @@ int main() { const auto start = std::chrono::steady_clock::now(); Require(sceUsbdHandleEventsTimeout(&timeout) == 0, "event handling failed"); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(45), "event handling returned before the timeout"); - bool threw = false; - try { - sceUsbdOpen(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "opening a device did not throw"); + int foreign = 0; + void* handle = &foreign; + Require(sceUsbdOpen(&foreign, nullptr) == invalidArgument, "null handle output accepted"); + Require(Throws([&] { sceUsbdOpen(nullptr, &handle); }), "opening a null device did not throw"); + Require(Throws([&] { sceUsbdOpen(&foreign, &handle); }), "opening a device that was never listed did not throw"); + sceUsbdClose(nullptr); + Require(Throws([&] { sceUsbdClose(&foreign); }), "closing a handle that was never opened did not throw"); + Require(sceUsbdRefDevice(nullptr) == nullptr, "null device was referenced"); + sceUsbdUnrefDevice(nullptr); + Require(Throws([&] { sceUsbdRefDevice(&foreign); }), "referencing a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdUnrefDevice(&foreign); }), "unreferencing a device that was never listed did not throw"); + Require(sceUsbdGetBusNumber(nullptr) == 0 && sceUsbdGetDeviceAddress(nullptr) == 0, "null device reported a bus or address"); + Require(Throws([&] { sceUsbdGetBusNumber(&foreign); }), "bus number of a device that was never listed did not throw"); + std::uint8_t descriptor[18] = {}; + void* config = &foreign; + Require(sceUsbdGetDeviceDescriptor(nullptr, descriptor) == invalidArgument, "null device descriptor accepted"); + Require(sceUsbdGetDeviceDescriptor(&foreign, nullptr) == invalidArgument, "null descriptor output accepted"); + Require(sceUsbdGetConfigDescriptor(nullptr, 0, &config) == invalidArgument, "null config device accepted"); + Require(sceUsbdGetConfigDescriptor(&foreign, 0, nullptr) == invalidArgument, "null config output accepted"); + Require(Throws([&] { sceUsbdGetConfigDescriptor(&foreign, 0, &config); }), "config of a device that was never listed did not throw"); + Require(Throws([&] { sceUsbdGetActiveConfigDescriptor(nullptr, &config); }), "active config of a null device did not throw"); + Require(sceUsbdClaimInterface(nullptr, 0) == invalidArgument && sceUsbdReleaseInterface(nullptr, 0) == invalidArgument, "null handle interface call accepted"); + Require(sceUsbdSetConfiguration(nullptr, 1) == invalidArgument && sceUsbdResetDevice(nullptr) == invalidArgument, "null handle configuration or reset accepted"); + Require(sceUsbdCheckConnected(nullptr) == invalidArgument, "null handle connection check accepted"); + Require(sceUsbdKernelDriverActive(nullptr, 0) == invalidArgument && sceUsbdAttachKernelDriver(nullptr, 0) == invalidArgument, "null handle kernel driver call accepted"); + Require(sceUsbdControlTransfer(nullptr, 0x80, 6, 0x100, 0, descriptor, 18, 1000) == invalidArgument, "null handle control transfer accepted"); + Require(sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, nullptr, 18, 1000) == invalidArgument, "control transfer without data accepted"); + Require(Throws([&] { sceUsbdControlTransfer(&foreign, 0x80, 6, 0x100, 0, descriptor, 18, 1000); }), "control transfer on a handle that was never opened did not throw"); + Require(sceUsbdGetStringDescriptor(nullptr, 1, 0x409, descriptor, 18) == invalidArgument, "null handle string descriptor accepted"); + Require(sceUsbdGetStringDescriptor(&foreign, 1, 0x409, descriptor, 0) == invalidArgument, "empty string descriptor buffer accepted"); + Require(Throws([&] { sceUsbdClaimInterface(&foreign, 0); }), "claiming on a handle that was never opened did not throw"); auto* transfer = static_cast(sceUsbdAllocTransfer(2)); Require(transfer != nullptr, "transfer allocation failed"); Require(*reinterpret_cast(transfer + 56) == 2, "iso packet count not stored"); @@ -75,6 +127,10 @@ int main() { Require(*reinterpret_cast(transfer + 12) == 250 && *reinterpret_cast(transfer + 20) == 8, "timeout or length not filled"); Require(*reinterpret_cast(transfer + 40) == &user && *reinterpret_cast(transfer + 48) == buffer, "user data or buffer not filled"); Require(sceUsbdCancelTransfer(transfer) == static_cast(0x80240005), "cancelling an unsubmitted transfer did not report not found"); + Require(sceUsbdSubmitTransfer(nullptr) == invalidArgument, "null transfer submitted"); + Require(sceUsbdSubmitTransfer(transfer) == invalidArgument, "transfer without a handle submitted"); + sceUsbdFillInterruptTransfer(transfer, &foreign, 0x81, buffer, 8, reinterpret_cast(&Require), &user, 250); + Require(Throws([&] { sceUsbdSubmitTransfer(transfer); }), "submitting on a handle that was never opened did not throw"); sceUsbdFreeTransfer(transfer); sceUsbdFreeTransfer(nullptr); Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index bf8b3467c..44aa99634 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -5,6 +5,7 @@ extern "C" { int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +int APS5_VABI sceVoiceQoSEnd(); } static void Require(bool value) { if (!value) std::abort(); } @@ -13,8 +14,10 @@ int main(int argc, char** argv) { Require(argc == 2); constexpr int argumentInvalid = static_cast(0x804E0902); constexpr int initialized = static_cast(0x804E0905); + constexpr int notInitialized = static_cast(0x804E0901); const auto appType = static_cast(std::strtoul(argv[1], nullptr, 16)); std::vector memory(0x40000, 0xAA); + Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); Require(sceVoiceQoSInit(nullptr, 0, appType) == argumentInvalid); for (std::uint32_t size : {0u, 1u, 0x100u, 0x3FFFFu, 0x40001u, 0x80000u, 0xFFFFFFFFu}) Require(sceVoiceQoSInit(memory.data(), size, appType) == argumentInvalid); @@ -25,4 +28,11 @@ int main(int argc, char** argv) { Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); Require(sceVoiceQoSInit(nullptr, 0, 0) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); + Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); + Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == initialized); + Require(sceVoiceQoSEnd() == 0); + Require(sceVoiceQoSEnd() == notInitialized); } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 2e01b4d33..64dd0afc3 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -19,6 +19,7 @@ const char16_t* APS5_VABI wcsrchr_nid_postfix(const char16_t* s, char16_t c); const char16_t* APS5_VABI wcsstr_nid_postfix(const char16_t* haystack, const char16_t* needle); const char16_t* APS5_VABI wcspbrk_nid_postfix(const char16_t* s, const char16_t* accept); std::size_t APS5_VABI wcsspn_nid_postfix(const char16_t* s, const char16_t* accept); +char16_t* APS5_VABI wcstok_nid_postfix(char16_t* text, const char16_t* delimiters, char16_t** last); char16_t* APS5_VABI wmemset_nid_postfix(char16_t* s, char16_t c, std::size_t n); double APS5_VABI wcstod_nid_postfix(const char16_t* str, char16_t** endptr); float APS5_VABI wcstof_nid_postfix(const char16_t* str, char16_t** endptr); @@ -96,6 +97,37 @@ int main() { require(wcsspn_nid_postfix(u"aabbc", u"ab") == 4); require(wcsspn_nid_postfix(u"abc", u"") == 0); + char16_t tokens[] = u",,one,,two;three,"; + char16_t* last = tokens; + char16_t* token = wcstok_nid_postfix(tokens, u",", &last); + require(token == tokens + 2 && same(token, u"one", 4) && last == tokens + 6); + token = wcstok_nid_postfix(nullptr, u",;", &last); + require(token == tokens + 7 && same(token, u"two", 4) && last == tokens + 11); + token = wcstok_nid_postfix(nullptr, u",", &last); + require(token == tokens + 11 && same(token, u"three", 6) && last == tokens + 17); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + require(wcstok_nid_postfix(nullptr, u",", &last) == nullptr && last == nullptr); + + char16_t tail[] = u"a b"; + require(wcstok_nid_postfix(tail, u" ", &last) == tail && last == tail + 2); + require(wcstok_nid_postfix(nullptr, u" ", &last) == tail + 2 && same(tail, u"a\0b", 4) && last == nullptr); + + char16_t onlyDelimiters[] = u";;"; + last = onlyDelimiters; + require(wcstok_nid_postfix(onlyDelimiters, u";", &last) == nullptr && last == nullptr && same(onlyDelimiters, u";;", 3)); + + char16_t empty[] = u""; + last = empty; + require(wcstok_nid_postfix(empty, u";", &last) == nullptr && last == nullptr); + + char16_t whole[] = u"word"; + require(wcstok_nid_postfix(whole, u"", &last) == whole && last == nullptr && same(whole, u"word", 5)); + + char16_t wide[] = u"\u4e00\u3001\xd83d\xde00\u3001"; + require(wcstok_nid_postfix(wide, u"\u3001", &last) == wide && same(wide, u"\u4e00\0", 2) && last == wide + 2); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == wide + 2 && wide[2] == 0xd83d && wide[3] == 0xde00 && wide[4] == 0 && last == wide + 5); + require(wcstok_nid_postfix(nullptr, u"\u3001", &last) == nullptr && last == nullptr); + const char16_t units[] = {u'x', 0, u'￿', u'y'}; require(wmemchr_nid_postfix(units, u'￿', 4) == units + 2); require(wmemchr_nid_postfix(units, u'y', 3) == nullptr); diff --git a/core/libs/tests/LogMacros.cpp b/core/libs/tests/LogMacros.cpp new file mode 100644 index 000000000..7106aa212 --- /dev/null +++ b/core/libs/tests/LogMacros.cpp @@ -0,0 +1,89 @@ +#include "prx/libc/include/general/LogMacros.hpp" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#endif + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "log macros test failed: %s\n", what); + std::exit(1); +} + +template +std::vector CaptureWrites(TAction action) { + std::vector messages; +#ifdef _WIN32 + const std::string name = R"(\\.\pipe\anyps5-log-macros-)" + std::to_string(GetCurrentProcessId()); + HANDLE server = CreateNamedPipeA(name.c_str(), PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS, 1, 0, 1 << 20, 0, nullptr); + Require(server != INVALID_HANDLE_VALUE, "message pipe"); + HANDLE client = CreateFileA(name.c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); + Require(client != INVALID_HANDLE_VALUE, "message pipe client"); + std::FILE* stream = _fdopen(_open_osfhandle(reinterpret_cast(client), _O_WRONLY | _O_BINARY), "wb"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + DWORD read = 0; + while (ReadFile(server, buffer.data(), static_cast(buffer.size()), &read, nullptr)) messages.emplace_back(buffer.data(), read); + }); +#else + int sockets[2]; + Require(socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) == 0, "message socket pair"); + std::FILE* stream = fdopen(sockets[0], "w"); + std::thread reader([&] { + std::string buffer(1 << 20, '\0'); + ssize_t read = 0; + while ((read = recv(sockets[1], buffer.data(), buffer.size(), 0)) > 0) messages.emplace_back(buffer.data(), static_cast(read)); + }); +#endif + Require(stream != nullptr, "message stream"); + Require(std::setvbuf(stream, nullptr, _IONBF, 0) == 0, "unbuffered message stream"); + action(stream); + std::fclose(stream); + reader.join(); +#ifdef _WIN32 + CloseHandle(server); +#else + close(sockets[1]); +#endif + return messages; +} + +std::string Join(const std::vector& messages) { + std::string text; + for (const auto& message : messages) text += message; + return text; +} + +bool HasLine(const std::string& text, const std::string& tail) { + const std::string head = "[tests/LogMacros.cpp:"; + return text.size() > head.size() + tail.size() && text.compare(0, head.size(), head) == 0 && text.compare(text.size() - tail.size(), tail.size(), tail) == 0 && text.find('\n') == text.size() - 1; +} + +} + +int main() { + const auto shortLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "value %d of %s", 7, "seven") == 0, "a short line was not flushed"); }); + Require(HasLine(Join(shortLine), "] value 7 of seven\n"), "a short line differs"); + Require(shortLine.size() <= 2, "a short line took more than two writes"); + + const std::string text(20000, 'r'); + const auto longLine = CaptureWrites([&](std::FILE* stream) { Require(_APS5_LOG_IMPL(stream, "%s", text.c_str()) == 0, "a long line was not flushed"); }); + Require(HasLine(Join(longLine), "] " + text + "\n"), "a long line differs"); + Require(longLine.size() <= 2, "a 20000-character line took more than two writes"); + + const auto plainLine = CaptureWrites([](std::FILE* stream) { Require(_APS5_LOG_IMPL_NF(stream, "plain") == 0, "a line without arguments was not flushed"); }); + Require(HasLine(Join(plainLine), "] plain\n"), "a line without arguments differs"); + Require(plainLine.size() <= 2, "a line without arguments took more than two writes"); + return 0; +} diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2d61f1c56..dc3d03d87 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -34,7 +34,33 @@ struct MallocManagedSize { static void Require(bool value) { if (!value) { std::fputs("Mspace check failed\n", stderr); std::abort(); } } +static void CheckAlignment() { + alignas(32) static std::array storage{}; + void* arena = sceLibcMspaceCreate_nid_postfix("aligned", storage.data() + 16, 65536, 0); + Require(arena != nullptr); + const auto aligned = [](const void* pointer) { return pointer && (reinterpret_cast(pointer) & 31u) == 0; }; + void* blocks[64] = {}; + for (std::size_t index = 0; index < 64; ++index) { + blocks[index] = sceLibcMspaceMalloc_nid_postfix(arena, index + 1); + Require(aligned(blocks[index])); + std::memset(blocks[index], 0xa5, index + 1); + } + void* zeroed = sceLibcMspaceCalloc_nid_postfix(arena, 3, 13); + Require(aligned(zeroed)); + blocks[0] = sceLibcMspaceRealloc_nid_postfix(arena, blocks[0], 600); + Require(aligned(blocks[0])); + void* small = sceLibcMspaceMemalign_nid_postfix(arena, 16, 40); + Require(aligned(small)); + void* moved = sceLibcMspaceReallocalign_nid_postfix(arena, nullptr, 40, 8); + Require(aligned(moved)); + void* posix = nullptr; + Require(sceLibcMspacePosixMemalign_nid_postfix(arena, &posix, 8, 40) == 0 && aligned(posix)); + for (void* block : blocks) sceLibcMspaceFree_nid_postfix(arena, block); + for (void* block : {zeroed, small, moved, posix}) sceLibcMspaceFree_nid_postfix(arena, block); + Require(sceLibcMspaceDestroy_nid_postfix(arena) == 0); +} int main() { + CheckAlignment(); alignas(16) std::array storage{}; void* arena = sceLibcMspaceCreate_nid_postfix("test", storage.data(), storage.size(), 0); Require(arena != nullptr); diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index 653fb6745..67d3e54ab 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -90,11 +90,34 @@ static void CheckNumberSyntax(const std::filesystem::path& path) { } } +static void CheckUtf8(const std::filesystem::path& path) { + const std::string prefix = R"({"titleId":"PPSA00000","localizedParameters":{"en-US":{"titleName":")"; + const std::string suffix = R"("}}})"; + for (const auto& title : {std::string("ASCII"), std::string("\xC2\x80"), std::string("\xE3\x82\xB2\xE3\x83\xBC\xE3\x83\xA0"), + std::string("\xF0\x9F\x8E\xAE"), std::string("\xF4\x8F\xBF\xBF")}) { + Require(ParseText(path, prefix + title + suffix).title == title, "Valid UTF-8 title changed"); + } + Require(ParseText(path, prefix + R"(\uD83C\uDFAE)" + suffix).title == "\xF0\x9F\x8E\xAE", "Escaped title changed"); + for (const auto& title : {std::string("\x80"), std::string("\xC0\xAF"), std::string("\xC1\xBF"), std::string("\xC2"), + std::string("\xE0\x80\xAF"), std::string("\xED\xA0\x80"), std::string("\xE3\x82"), + std::string("\xF0\x80\x80\xAF"), std::string("\xF4\x90\x80\x80"), std::string("\xF5\x80\x80\x80"), + std::string("\xFE"), std::string("\xFF"), std::string("\xC2x"), std::string("\xE3\x82x")}) { + bool rejected = false; + try { ParseText(path, prefix + title + suffix); } catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 title"); + } + bool rejected = false; + try { ParseText(path, std::string("{\"") + "\x80" + R"(":0,)" + prefix.substr(1) + "Valid" + suffix); } + catch (const std::exception&) { rejected = true; } + Require(rejected, "Accepted malformed UTF-8 object key"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); int result = 0; try { + CheckUtf8(path); CheckNumberSyntax(path); const std::pair sizes[] = { {"", 0}, {"0", 0}, {"-0", 0}, {"0.25", 0}, {"0e3", 0}, {"0E+3", 0}, {"-0e-3", 0}, {"4096", 4096}, diff --git a/core/libs/tests/VectorFusedMultiplyAdd.cpp b/core/libs/tests/VectorFusedMultiplyAdd.cpp index f17e7f083..3f2926517 100644 --- a/core/libs/tests/VectorFusedMultiplyAdd.cpp +++ b/core/libs/tests/VectorFusedMultiplyAdd.cpp @@ -105,7 +105,10 @@ void CheckMixHalf(const std::string& name, const std::array& w std::uint32_t exact = 0u; for (const auto* value : block.Instructions()) { Require(value->Opcode() != IrOpcode::FPFma32 && value->Opcode() != IrOpcode::FPMad32, name + " rounds to f32 before rounding to f16"); - if (value->Opcode() == IrOpcode::FPInterpolateF16) ++exact; + if (value->Opcode() != IrOpcode::FPInterpolateF16) continue; + ++exact; + const std::uint32_t rounding = (value->Argument(3)->ImmediateU32() >> InterpolationRound16Shift) & 3u; + Require(rounding == ((floatMode >> 2u) & 3u), name + " rounds in f16 mode " + std::to_string(rounding)); } Require(exact == 1u, name + " emits " + std::to_string(exact) + " product-sums rounded once to f16"); } @@ -146,7 +149,8 @@ int main() { CheckMixHalf("v_fma_mixlo_f16 op_sel_hi:[1,1,1]", {0xcc214005u, 0x1c220f06u}, RdnaOpcode::VMadMixloF16, 0xc0u); CheckMixHalf("v_fma_mixlo_f16 with f32 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xf3u); RejectMixHalf("v_fma_mixlo_f16 with f16 denormals flushed", {0xcc210005u, 0x04220f06u}, 0x30u); - RejectMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, 0xf8u); + CheckMixHalf("v_fma_mixhi_f16 with f16 round toward -inf", {0xcc220005u, 0x04220f06u}, RdnaOpcode::VMadMixhiF16, 0xf8u); + CheckMixHalf("v_fma_mixlo_f16 with f16 round toward zero", {0xcc210005u, 0x04220f06u}, RdnaOpcode::VMadMixloF16, 0xccu); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp new file mode 100644 index 000000000..a5d93edc4 --- /dev/null +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -0,0 +1,12 @@ +#include +#include +#include "../prx/libc/include/general/VabiMacros.hpp" + +extern "C" int* APS5_VABI __error_nid_postfix(); + +int main() { + if (__error_nid_postfix() == nullptr) return 1; + _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); + std::fputs("aborting\n", stderr); + std::abort(); +} diff --git a/core/libs/tests/WindowsExceptions.cpp b/core/libs/tests/WindowsExceptions.cpp index 3b5bc6116..d214000e5 100644 --- a/core/libs/tests/WindowsExceptions.cpp +++ b/core/libs/tests/WindowsExceptions.cpp @@ -1,14 +1,18 @@ #include #include #include +#include #include #include +#include +#include #include #include #include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" extern "C" { @@ -55,6 +59,73 @@ static void TestTypeInfoVtables() { extern "C" void NotImplemented_nid_no_patch(const char*); +static void RequireHandle(bool value) { + if (!value) throw std::runtime_error("child process setup failed"); +} + +[[gnu::noinline, noreturn]] static void ThrowUncaughtProbe() { + throw std::runtime_error("unhandled site probe"); +} + +static int RunThrowChild() { + std::fprintf(stderr, "probe %p\n", reinterpret_cast(&ThrowUncaughtProbe)); + std::fflush(stderr); + ThrowUncaughtProbe(); + return 100; +} + +static void testUnhandledThrowSite() { + SECURITY_ATTRIBUTES attributes{}; + attributes.nLength = sizeof(attributes); + attributes.bInheritHandle = TRUE; + HANDLE readPipe = nullptr; + HANDLE writePipe = nullptr; + RequireHandle(CreatePipe(&readPipe, &writePipe, &attributes, 0) != 0); + RequireHandle(SetHandleInformation(readPipe, HANDLE_FLAG_INHERIT, 0) != 0); + std::array image{}; + RequireHandle(GetModuleFileNameW(nullptr, image.data(), static_cast(image.size())) != 0); + const wchar_t* argument = L"\" throw-uncaught"; + std::array command{}; + const std::size_t imageLength = std::wcslen(image.data()); + const std::size_t argumentLength = std::wcslen(argument); + RequireHandle(1 + imageLength + argumentLength + 1 < command.size()); + command[0] = L'"'; + std::memcpy(command.data() + 1, image.data(), imageLength * sizeof(wchar_t)); + std::memcpy(command.data() + 1 + imageLength, argument, (argumentLength + 1) * sizeof(wchar_t)); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = writePipe; + startup.hStdError = writePipe; + PROCESS_INFORMATION child{}; + RequireHandle(CreateProcessW(image.data(), command.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &child) != 0); + CloseHandle(writePipe); + CloseHandle(child.hThread); + std::array output{}; + std::size_t outputLength = 0; + DWORD chunk = 0; + while (outputLength < output.size() && ReadFile(readPipe, output.data() + outputLength, static_cast(output.size() - outputLength), &chunk, nullptr) != 0 && chunk != 0) { + outputLength += chunk; + } + CloseHandle(readPipe); + RequireHandle(WaitForSingleObject(child.hProcess, 20000) == WAIT_OBJECT_0); + DWORD exitCode = 0; + RequireHandle(GetExitCodeProcess(child.hProcess, &exitCode) != 0); + CloseHandle(child.hProcess); + output[outputLength < output.size() ? outputLength : output.size() - 1] = 0; + RequireHandle(exitCode == 0xC0000409); + const char* probeMarker = std::strstr(output.data(), "probe "); + RequireHandle(probeMarker != nullptr); + const auto probe = static_cast(std::strtoull(probeMarker + 6, nullptr, 16)); + RequireHandle(probe != 0); + const char* marker = std::strstr(output.data(), "thrown from "); + RequireHandle(marker != nullptr); + const auto logged = static_cast(std::strtoull(marker + 13, nullptr, 16)); + RequireHandle(logged != 0); + if (logged <= probe || logged - probe >= 0x1000) throw std::runtime_error("unhandled exception named the wrapper instead of the throw site"); +} + static int destroyed; struct Guard { ~Guard() { ++destroyed; } }; static void ThrowNested() { @@ -156,35 +227,39 @@ static void testUncaughtExceptions() { if (seen != 1) throw std::runtime_error("unwinding did not count its exception"); } -static int onceDestroyed; -struct OnceGuard { ~OnceGuard() { ++onceDestroyed; } }; -static void ThrowThroughOnce(int depth) { - OnceGuard guard; - if (depth) { ThrowThroughOnce(depth - 1); return; } - std::once_flag flag; - std::call_once(flag, [] { throw std::runtime_error("through runtime frames"); }); +static int guestFrameDestroyed; +struct GuestFrameGuard { ~GuestFrameGuard() { ++guestFrameDestroyed; } }; +static void ThrowThroughGuestFrames(int depth) { + GuestFrameGuard guard; + if (depth) { ThrowThroughGuestFrames(depth - 1); return; } + throw std::runtime_error("through guest frames"); } -static void testUnwindThroughRuntimeModules() { +static void testUnwindThroughGuestFrames() { for (int depth : {0, 1, 7, 64}) { - onceDestroyed = 0; + guestFrameDestroyed = 0; try { - ThrowThroughOnce(depth); - throw std::logic_error("call_once did not propagate"); + ThrowThroughGuestFrames(depth); + throw std::logic_error("guest frame did not propagate"); } catch (const std::runtime_error& error) { - if (std::strcmp(error.what(), "through runtime frames")) throw; + if (std::strcmp(error.what(), "through guest frames")) throw; } - if (onceDestroyed != depth + 1) throw std::runtime_error("unwinding through runtime DLL frames skipped a destructor"); + if (guestFrameDestroyed != depth + 1) throw std::runtime_error("unwinding through guest frames skipped a destructor"); } } -int main() { +int main(int argc, char** argv) { + if (argc > 1) { + if (std::strcmp(argv[1], "throw-uncaught") == 0) return RunThrowChild(); + return 100; + } TestTypeInfoVtables(); + testUnhandledThrowSite(); testExceptionPointer(); testFutureException(); testMadeExceptionPointer(); testUncaughtExceptions(); - testUnwindThroughRuntimeModules(); + testUnwindThroughGuestFrames(); try { Rethrow(); return 1; diff --git a/core/libs/tests/WriteWatch.cpp b/core/libs/tests/WriteWatch.cpp index 378c67050..045b6c10b 100644 --- a/core/libs/tests/WriteWatch.cpp +++ b/core/libs/tests/WriteWatch.cpp @@ -41,13 +41,14 @@ struct Region { std::uintptr_t Address(std::size_t index) const { return reinterpret_cast(base) + index * page; } - std::set Collect() { + using Collector = bool (*)(std::uintptr_t, std::size_t, void (*)(void*, std::uintptr_t, std::uintptr_t), void*); + std::set Collect(Collector collector = &GuestWriteWatchCollect_nid_postfix, std::size_t first = 0, std::size_t count = 0) { std::set pages; struct Sink { Region* region; std::set* pages; } sink{this, &pages}; - const bool complete = GuestWriteWatchCollect_nid_postfix(Address(0), bytes, [](void* context, std::uintptr_t begin, std::uintptr_t end) { + const bool complete = collector(Address(first), count == 0 ? bytes - first * page : count * page, [](void* context, std::uintptr_t begin, std::uintptr_t end) { auto& target = *static_cast(context); for (auto at = begin; at < end; at += target.region->page) target.pages->insert((at - target.region->Address(0)) / target.region->page); }, &sink); @@ -83,6 +84,29 @@ void CheckCollect() { Require(region.Collect() == std::set({0, 1}), "a write across a page boundary is not reported on both pages"); } +void CheckFresh() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix) == std::set({4, 5, 6, 7}), "a fresh collection does not report exactly the never collected pages"); + Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix).empty(), "a fresh collection reports pages again"); + region.base[6 * region.page] = 1; + Require(region.Collect() == std::set({1, 6}), "a fresh collection does not arm the pages it reports"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + +void CheckArmed() { + Region region(8); + GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); + Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full"); + region.base[region.page] = 1; + region.base[6 * region.page] = 1; + Require(region.Collect(&GuestWriteWatchCollectArmed_nid_postfix) == std::set({1}), "an armed collection does not report exactly the written armed pages"); + Require(region.Collect() == std::set({4, 5, 6, 7}), "an armed collection takes never collected pages"); + Require(region.Collect().empty(), "collected writes are reported again"); +} + void CheckRegisterAgain() { Region region(4); GuestWriteWatchRegister_nid_postfix(region.base, region.bytes); @@ -208,9 +232,11 @@ void CheckThreads() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected collect, register, mapped, host, protection or threads"); + Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads"); const std::string_view mode(argv[1]); if (mode == "collect") CheckCollect(); + else if (mode == "fresh") CheckFresh(); + else if (mode == "armed") CheckArmed(); else if (mode == "register") CheckRegisterAgain(); else if (mode == "mapped") CheckRegisterMapped(); else if (mode == "host") CheckHostWrite(); diff --git a/core/libs/tests/shared_memory/CMakeLists.txt b/core/libs/tests/shared_memory/CMakeLists.txt new file mode 100644 index 000000000..f9e85f8de --- /dev/null +++ b/core/libs/tests/shared_memory/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.22) +project(SharedMemoryTests LANGUAGES CXX) +enable_testing() +if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux") + message(FATAL_ERROR "Standalone shared-memory tests require Linux") +endif() +set(LIBS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../..") +add_executable(guest_named_shm_open_tests + ../GuestNamedShmOpen.cpp + Support.cpp + ${LIBS_DIR}/prx/libkernel/File/src/SharedMemory.cpp + ${LIBS_DIR}/prx/libc/src/CreationMask.cpp + ${LIBS_DIR}/nid/src/Sha1.cpp) +target_include_directories(guest_named_shm_open_tests PRIVATE ${LIBS_DIR} ${LIBS_DIR}/nid/include) +target_compile_features(guest_named_shm_open_tests PRIVATE cxx_std_20) +target_link_libraries(guest_named_shm_open_tests PRIVATE rt) +add_test(NAME guest_named_shm_open COMMAND guest_named_shm_open_tests) +set_tests_properties(guest_named_shm_open PROPERTIES TIMEOUT 20) diff --git a/core/libs/tests/shared_memory/Support.cpp b/core/libs/tests/shared_memory/Support.cpp new file mode 100644 index 000000000..03f202e4e --- /dev/null +++ b/core/libs/tests/shared_memory/Support.cpp @@ -0,0 +1,11 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" int* APS5_VABI __error_nid_postfix() { + static thread_local int error; + return &error; +} + +extern "C" void NotImplemented_nid_no_patch(const char* name) { + throw std::runtime_error(name); +} diff --git a/core/libs/tests/write_watch/CMakeLists.txt b/core/libs/tests/write_watch/CMakeLists.txt index f98461a10..0824ee500 100644 --- a/core/libs/tests/write_watch/CMakeLists.txt +++ b/core/libs/tests/write_watch/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(write_watch_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode collect register mapped host protection threads) +foreach(mode collect fresh armed register mapped host protection threads) add_test(NAME write_watch_${mode} COMMAND write_watch_tests ${mode}) set_tests_properties(write_watch_${mode} PROPERTIES TIMEOUT 30) endforeach() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4561113eb..4e8e1a4ad 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -81,6 +81,20 @@ target_include_directories(relinker PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs ) +if(BUILD_TESTING AND WIN32) + get_target_property(windowsAliasTestSources relinker SOURCES) + list(REMOVE_ITEM windowsAliasTestSources + ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp) + add_test_executable(windows_guest_export_alias_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/WindowsExportAliasTests.cpp + ${windowsAliasTestSources}) + get_target_property(windowsAliasIncludes relinker INCLUDE_DIRECTORIES) + target_include_directories(windows_guest_export_alias_tests PRIVATE ${windowsAliasIncludes}) + add_test(NAME windows_guest_export_aliases COMMAND windows_guest_export_alias_tests) +endif() + if(WIN32) target_compile_options(relinker PRIVATE -municode) target_link_options(relinker PRIVATE -municode) @@ -137,6 +151,32 @@ endif() if(BUILD_TESTING) + add_test_executable(elf_reader_bounds_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ElfReaderBoundsTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/parsing/ElfReader.cpp) + target_include_directories(elf_reader_bounds_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include) + add_test(NAME elf_reader_bounds COMMAND elf_reader_bounds_tests) + set_tests_properties(elf_reader_bounds PROPERTIES TIMEOUT 20) + + add_test_executable(call_site_index_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/CallSiteIndexTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/src/analysis/CallSiteResolver.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(call_site_index_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME call_site_index COMMAND call_site_index_tests) + set_tests_properties(call_site_index PROPERTIES TIMEOUT 20) + add_test_executable(autorun_exit_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/AutorunExitTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/Autorun.cpp @@ -144,6 +184,22 @@ if(BUILD_TESTING) target_include_directories(autorun_exit_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/cli/include ${CMAKE_CURRENT_SOURCE_DIR}/io/include ${CMAKE_CURRENT_SOURCE_DIR}/domain/include) add_test(NAME autorun_exit_status COMMAND autorun_exit_tests) set_tests_properties(autorun_exit_status PROPERTIES TIMEOUT 20) + add_test_executable(rewrite_boundaries_tests + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/RewriteBoundaryTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionRewriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp + ) + target_include_directories(rewrite_boundaries_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME rewrite_boundaries COMMAND rewrite_boundaries_tests) + set_tests_properties(rewrite_boundaries PROPERTIES TIMEOUT 20) + add_test_executable(cli_args_tests ${CMAKE_CURRENT_SOURCE_DIR}/cli/tests/CliArgsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/cli/src/CliArgs.cpp @@ -154,6 +210,21 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/domain/include ) add_test(NAME cli_args COMMAND cli_args_tests) + add_test_executable(program_layout_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/ProgramLayoutRangeTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ) + target_include_directories(program_layout_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME program_layout_ranges COMMAND program_layout_ranges_tests) + set_tests_properties(program_layout_ranges PROPERTIES TIMEOUT 20) + add_test_executable(buffer_bounds_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/BufferBoundsTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp @@ -163,6 +234,13 @@ if(BUILD_TESTING) target_include_directories(buffer_bounds_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/io/include) add_test(NAME buffer_bounds COMMAND buffer_bounds_tests) + add_test_executable(param_json_utf8_tests + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/tests/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp) + target_include_directories(param_json_utf8_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../libs) + add_test(NAME param_json_utf8 COMMAND param_json_utf8_tests) + set_tests_properties(param_json_utf8 PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(file_writer_tests ${CMAKE_CURRENT_SOURCE_DIR}/io/tests/FileWriterTests.cpp @@ -191,6 +269,22 @@ if(BUILD_TESTING) add_test(NAME linux_desktop_entry COMMAND linux_desktop_entry_tests) endif() + add_test_executable(section_header_ranges_tests + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/SectionRangeTests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/src/general/SectionHeaderTableBuilder.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteWriter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp + ) + target_include_directories(section_header_ranges_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include + ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include + ${CMAKE_CURRENT_SOURCE_DIR}/io/include + ) + add_test(NAME section_header_ranges COMMAND section_header_ranges_tests) + set_tests_properties(section_header_ranges PROPERTIES TIMEOUT 20) + add_test_executable(amd64_only_converter_tests ${CMAKE_CURRENT_SOURCE_DIR}/codegen/tests/Amd64OnlyConverterTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp @@ -245,6 +339,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME rela_size COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_rela_size.py $) + add_test(NAME none_relocations COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_none_relocations.py $) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} @@ -297,6 +393,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_desktop_entry_output.py $) add_test(NAME guest_intel_trampolines COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) + add_test(NAME guest_syscall_trap COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_syscall_trap.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/cli/include/Cli.hpp b/core/relinker/cli/include/Cli.hpp index 6f7985c1c..7faa5353b 100644 --- a/core/relinker/cli/include/Cli.hpp +++ b/core/relinker/cli/include/Cli.hpp @@ -11,6 +11,7 @@ namespace Cli { struct Args { bool showHelp = false; bool skipSyscallCheck = false; + bool trapSdkSyscallStubs = false; bool skipSceModule = false; bool toIntel = false; bool toRosetta = false; diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 0528e8419..eef10daad 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -8,7 +8,7 @@ namespace Cli { const char* Usage() { - return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" + return "Usage: relinker [--help] [--windows | --macos] [--windows-diagnostics] [--windows-gui] [--skip-syscall-check | --trap-sdk-syscall-stubs] [--skip-sce-module | --sce-module-path ] [--exclude-sce-module ]... [--module-dir ]... [--to-intel] [--to-rosetta] [unused-filter=0|1|2] [--registry] [--rpath ] [--lazy-binding] [--autorun] \n" "Example: relinker input.elf output.elf"; } @@ -23,6 +23,8 @@ Args ParseArgs(int argc, char* argv[]) { args.showHelp = true; } else if (arg == "--skip-syscall-check") { args.skipSyscallCheck = true; + } else if (arg == "--trap-sdk-syscall-stubs") { + args.trapSdkSyscallStubs = true; } else if (arg == "--skip-sce-module") { args.skipSceModule = true; } else if (arg == "--sce-module-path") { @@ -80,6 +82,8 @@ Args ParseArgs(int argc, char* argv[]) { if (args.skipSceModule && sceModulePathSpecified) throw std::runtime_error("--sce-module-path conflicts with --skip-sce-module"); + if (args.skipSyscallCheck && args.trapSdkSyscallStubs) throw std::runtime_error("--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); + if (args.skipSceModule && !args.excludedSceModules.empty()) throw std::runtime_error("--exclude-sce-module conflicts with --skip-sce-module"); diff --git a/core/relinker/cli/tests/CliArgsTests.cpp b/core/relinker/cli/tests/CliArgsTests.cpp index 98f3b2997..ec27e771a 100644 --- a/core/relinker/cli/tests/CliArgsTests.cpp +++ b/core/relinker/cli/tests/CliArgsTests.cpp @@ -52,7 +52,7 @@ void defaults() { const auto args = parse({"input.elf", "output.elf"}); require(args.inputPath == "input.elf", "input path was not captured"); require(args.outputPath == "output.elf", "output path was not captured"); - require(!args.skipSyscallCheck && !args.skipSceModule && !args.toIntel && !args.writeRegistry && + require(!args.skipSyscallCheck && !args.trapSdkSyscallStubs && !args.skipSceModule && !args.toIntel && !args.writeRegistry && !args.toWindows && !args.lazyBinding && !args.autorun && !args.windowsDiagnostics && !args.windowsGui, "a flag defaulted to true"); require(args.unusedFilterLevel == 0u, "unused filter level did not default to 0"); @@ -76,6 +76,13 @@ void booleanFlags() { require(args.inputPath == "input.elf" && args.outputPath == "output.elf", "flags consumed the positional paths"); } +void trapSdkSyscallStubs() { + const auto args = parse({"--trap-sdk-syscall-stubs", "input.elf", "output.elf"}); + require(args.trapSdkSyscallStubs, "--trap-sdk-syscall-stubs was ignored"); + expectThrows({"--skip-syscall-check", "--trap-sdk-syscall-stubs", "input.elf", "output.elf"}, + "--skip-syscall-check conflicts with --trap-sdk-syscall-stubs"); +} + void positionalsInterleaveWithFlags() { const auto args = parse({"--windows", "input.elf", "--registry", "output.elf", "--autorun"}); require(args.toWindows && args.writeRegistry && args.autorun, "a flag after a positional was ignored"); @@ -189,6 +196,7 @@ int main() { try { defaults(); booleanFlags(); + trapSdkSyscallStubs(); positionalsInterleaveWithFlags(); singleDashIsPositional(); rpath(); diff --git a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp index a3899c3dd..212bdaee2 100644 --- a/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp +++ b/core/relinker/codegen/src/x86/X64InstructionRewriter.cpp @@ -74,6 +74,18 @@ RewriteResult X64InstructionRewriter::Rewrite( throw CodegenException("Rewrite offset is out of bounds", request.Offset); } + const auto scanner = MakeInstructionScanner(); + const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); + bool instructionStart = false; + for (const auto& match : matches) { + if (match.Offset == request.Offset) { + instructionStart = true; + break; + } + } + if (!instructionStart) + throw CodegenException("Rewrite offset is not an instruction boundary", request.Offset); + const X64InstructionDecoder decoder; const std::size_t decodedLength = decoder.Decode(codeSection.data() + offset, codeSection.size() - offset); @@ -90,9 +102,6 @@ RewriteResult X64InstructionRewriter::Rewrite( rewritten.insert(rewritten.end(), request.NewBytes.begin(), request.NewBytes.end()); rewritten.insert(rewritten.end(), codeSection.begin() + offset + decodedLength, codeSection.end()); - const auto scanner = MakeInstructionScanner(); - const auto matches = scanner->ScanCodeSection(codeSection, 0, codeSection.size()); - std::vector adjustments; for (const auto& match : matches) { diff --git a/core/relinker/codegen/tests/RewriteBoundaryTests.cpp b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp new file mode 100644 index 000000000..fa9ee0055 --- /dev/null +++ b/core/relinker/codegen/tests/RewriteBoundaryTests.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Invalid rewrite boundary accepted"); } + +int main() { + try { + const Codegen::X64InstructionRewriter rewriter; + for (const auto& code : {std::vector{0xB8, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0x48, 0x8B, 0x05, 0x90, 0x90, 0x90, 0x90, 0xC3}, + std::vector{0xE8, 0x90, 0x90, 0x90, 0x90, 0xC3}}) { + const auto original = code; + for (std::size_t offset = 1; offset + 1 < code.size(); ++offset) { + bool rejected = false; + try { (void)rewriter.Rewrite(code, {offset, {0x90}}); } + catch (const std::exception&) { rejected = true; } + Require(rejected && code == original); + } + const auto result = rewriter.Rewrite(code, {0, {0x90}}); + Require(result.Bytes == std::vector{0x90, 0xC3}); + } + const auto result = rewriter.Rewrite({0x90, 0x90, 0xC3}, {1, {0x90, 0x90}}); + Require(result.Bytes == std::vector{0x90, 0x90, 0x90, 0xC3}); + std::cout << "18 rewrite boundary cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp index d70c3385d..cd7ca36df 100644 --- a/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/ProgramHeaderLayoutBuilder.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -111,6 +112,8 @@ std::uint64_t ProgramHeaderLayoutBuilder::ComputeExtraBlockVaddr( if (ph.Type != PT_LOAD) continue; foundLoad = true; + if (ph.MemorySize > std::numeric_limits::max() - ph.MappedAddress) + throw Domain::RelinkerException("Load segment virtual range overflowed"); const std::uint64_t vaddrEnd = ph.MappedAddress + ph.MemorySize; if (vaddrEnd > highestVaddrEnd) highestVaddrEnd = vaddrEnd; @@ -156,22 +159,36 @@ std::uint16_t ProgramHeaderLayoutBuilder::WriteLayout( "Not enough program header slots: need " + std::to_string(neededPh) + ", available " + std::to_string(request.PhNum)); - const std::uint64_t headerBlockSize = request.PhOff + static_cast(neededPh) * request.PhEntSize; + const auto checkedEnd = [](std::uint64_t start, std::uint64_t size) { + if (size > std::numeric_limits::max() - start) + throw Domain::RelinkerException("Program header layout range overflowed"); + return start + size; + }; + if (request.PhEntSize < 56) + throw Domain::RelinkerException("Program header entry is too small"); + const std::uint64_t headerBlockSize = checkedEnd(request.PhOff, static_cast(neededPh) * request.PhEntSize); + if (headerBlockSize > buf.size()) + throw Domain::RelinkerException("Program header table exceeds the output file"); + if (request.ExtraBlockOffset > buf.size() || request.ExtraBlockSize > buf.size() - request.ExtraBlockOffset) + throw Domain::RelinkerException("Extra block exceeds the output file"); std::uint64_t headerBlockAlign = kDefaultLoadAlignment; for (const auto& ph : request.OriginalHeaders) { if (ph.Type == PT_LOAD && !_segmentFilter->ShouldSkip(ph) && ph.Alignment > headerBlockAlign && (ph.Alignment & (ph.Alignment - 1)) == 0) headerBlockAlign = ph.Alignment; } const std::uint64_t headerBlockVaddr = - (request.ExtraBlockVaddr + request.ExtraBlockSize + headerBlockAlign - 1) & ~(headerBlockAlign - 1); + checkedEnd(checkedEnd(request.ExtraBlockVaddr, request.ExtraBlockSize), headerBlockAlign - 1) & ~(headerBlockAlign - 1); + checkedEnd(headerBlockVaddr, headerBlockSize); if (request.DynamicSegmentOffset < request.ExtraBlockOffset) throw Domain::RelinkerException("Dynamic segment offset lies before the extra block"); - if (request.DynamicSegmentOffset + request.DynamicSegmentSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.DynamicSegmentOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + request.DynamicSegmentSize > request.ExtraBlockSize - (request.DynamicSegmentOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Dynamic segment does not fit within the extra block"); if (request.InterpOffset < request.ExtraBlockOffset) throw Domain::RelinkerException("Interp offset lies before the extra block"); - if (request.InterpOffset + request.InterpSize > request.ExtraBlockOffset + request.ExtraBlockSize) + if (request.InterpOffset - request.ExtraBlockOffset > request.ExtraBlockSize || + request.InterpSize > request.ExtraBlockSize - (request.InterpOffset - request.ExtraBlockOffset)) throw Domain::RelinkerException("Interp data does not fit within the extra block"); const std::uint64_t dynamicSegmentVaddr = request.ExtraBlockVaddr + (request.DynamicSegmentOffset - request.ExtraBlockOffset); diff --git a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp index e9ee612df..916a1575e 100644 --- a/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp +++ b/core/relinker/elfpatcher/src/general/SectionHeaderTableBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include namespace Elfpatcher { @@ -43,18 +44,33 @@ void SectionHeaderTableBuilder::WriteTable( const auto phOffset = Io::ReadU64(buf, kEhdrPhOffOffset); const auto phCount = Io::ReadU16(buf, kEhdrPhNumOffset); const auto phStride = Io::ReadU16(buf, kEhdrPhEntSizeOffset); - const auto mappedAddress = [&](std::uint64_t offset) { + if (phStride < 56 || phOffset > buf.size() || phCount > (buf.size() - phOffset) / phStride) + throw std::runtime_error("Output program header table is out of bounds"); + const auto mappedAddress = [&](std::uint64_t offset, std::uint64_t sectionSize) { + if (offset > buf.size() || sectionSize > buf.size() - offset) + throw std::runtime_error("Allocated output section exceeds file"); for (std::uint16_t index = 0; index < phCount; ++index) { const auto header = phOffset + index * phStride; if (Io::ReadU32(buf, header) != PT_LOAD) continue; const auto start = Io::ReadU64(buf, header + 8); const auto size = Io::ReadU64(buf, header + 32); - if (offset >= start && offset - start < size) - return Io::ReadU64(buf, header + 16) + offset - start; + if (offset >= start && offset - start <= size && sectionSize <= size - (offset - start)) { + const auto relative = offset - start; + const auto address = Io::ReadU64(buf, header + 16); + if (relative > std::numeric_limits::max() - address || + sectionSize > std::numeric_limits::max() - (address + relative)) + throw std::runtime_error("Allocated output section address overflowed"); + return address + relative; + } } throw std::runtime_error("Allocated output section is outside load segments"); }; + const auto dynStrAddress = mappedAddress(request.DynStrOffset, request.DynStrSize); + const auto dynSymAddress = mappedAddress(request.DynSymOffset, request.DynSymSize); + const auto dynamicAddress = mappedAddress(request.DynamicSegmentOffset, request.DynamicSegmentSize); + const auto stubAddress = mappedAddress(request.StubOffset, request.StubSize); + const auto shStrTabOff = static_cast(buf.size()); constexpr std::uint64_t shStrTabSize = sizeof(kShStrTabBlob); for (const char i : kShStrTabBlob) @@ -65,10 +81,10 @@ void SectionHeaderTableBuilder::WriteTable( _writeSectionHeader(buf, shOff + kShdrEntrySize * 0, 0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0); _writeSectionHeader(buf, shOff + kShdrEntrySize * 1, kShStrTabNameOffset, SHT_STRTAB, 0, shStrTabOff, shStrTabOff, shStrTabSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, mappedAddress(request.DynStrOffset), request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, mappedAddress(request.DynSymOffset), request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, mappedAddress(request.DynamicSegmentOffset), request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); - _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, mappedAddress(request.StubOffset), request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 2, kDynStrNameOffset, SHT_STRTAB, SHF_ALLOC, dynStrAddress, request.DynStrOffset, request.DynStrSize, 0, 0, kNoSpecialAlignment, 0); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 3, kDynSymNameOffset, SHT_DYNSYM, SHF_ALLOC, dynSymAddress, request.DynSymOffset, request.DynSymSize, kDynSymLinkToStrTab, kDynSymInfoFirstGlobal, kDynSymAlign, kSymEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 4, kDynamicNameOffset, SHT_DYNAMIC, SHF_ALLOC | SHF_WRITE, dynamicAddress, request.DynamicSegmentOffset, request.DynamicSegmentSize, kDynamicLinkToStrTab, 0, kDynSymAlign, kDynEntrySize); + _writeSectionHeader(buf, shOff + kShdrEntrySize * 5, kTextNameOffset, SHT_PROGBITS, SHF_ALLOC | SHF_EXECINSTR, stubAddress, request.StubOffset, request.StubSize, 0, 0, kNoSpecialAlignment, 0); _byteWriter->WriteU64(buf, kEhdrShOffOffset, shOff); _byteWriter->WriteU16(buf, kEhdrShEntSizeOffset, static_cast(kShdrEntrySize)); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index f7ca50fa2..bcc3469dd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -120,9 +120,16 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI return false; }; std::map exports; + std::map exportSymbols; PeSection tlsExports{".tlsrefs", nextRva, SectionRead | 0x40u, {}}; for (const auto& symbol : guest.Symbols) { - if (symbol.Section == 0 || symbol.Section == Relinker::AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; + const auto [previous, inserted] = exportSymbols.emplace(symbol.Name, &symbol); + if (!inserted) { + if (!Relinker::IdenticalOrdinaryFunctionExport(*previous->second, symbol)) throw Domain::RelinkerException("Duplicate guest export: " + symbol.Name); + continue; + } + if (symbol.Section == Relinker::AbsoluteSection) continue; if (symbol.Section >= 0xff00) throw Domain::RelinkerException("Unsupported guest export section: " + symbol.Name); std::uint32_t rva = 0; if ((symbol.Info & 15) == 6) { diff --git a/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp new file mode 100644 index 000000000..3cf152c37 --- /dev/null +++ b/core/relinker/elfpatcher/tests/ProgramLayoutRangeTests.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include +#include + +class KeepAll : public Elfpatcher::ISegmentFilter { +public: + bool ShouldSkip(const Domain::ProgramHeader&) const override { return false; } +}; + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Program layout range validation failed"); } + +int main() { + try { + Elfpatcher::ProgramHeaderLayoutBuilder builder(std::make_shared(), std::make_shared()); + constexpr auto maximum = std::numeric_limits::max(); + Domain::ProgramHeader load{}; + load.Type = 1; load.MappedAddress = maximum - 3; load.MemorySize = 8; + bool rejected = false; + try { builder.ComputeExtraBlockVaddr({load}, 0x1000); } catch (const std::exception&) { rejected = true; } + Require(rejected); + load.MappedAddress = 0x1000; load.MemorySize = 0x1000; + Require(builder.ComputeExtraBlockVaddr({load}, 0x1003) == 0x2003); + Elfpatcher::ProgramHeaderLayoutRequest valid{64, 56, 6, {load}, 0x1000, 0x2000, 0x100, 0x1000, 16, 0x1010, 8}; + std::vector output(0x1100, 0xA5); + Require(builder.WriteLayout(output, valid) == 6); + for (int kind = 0; kind < 11; ++kind) { + auto request = valid; + switch (kind) { + case 0: request.PhEntSize = 55; break; + case 1: request.PhOff = maximum - 10; break; + case 2: request.PhOff = 0x10F0; break; + case 3: request.ExtraBlockVaddr = maximum - 10; break; + case 4: request.DynamicSegmentSize = maximum; break; + case 5: request.InterpSize = maximum; break; + case 6: request.DynamicSegmentOffset = maximum; break; + case 7: request.InterpOffset = maximum; break; + case 8: request.ExtraBlockOffset = maximum; break; + case 9: request.ExtraBlockSize = 0x1000; break; + case 10: request.PhOff = 0xF00; request.ExtraBlockVaddr = maximum - 0x1fff; break; + } + std::vector bytes(0x1100, 0xA5); + const auto before = bytes; + rejected = false; + try { builder.WriteLayout(bytes, request); } catch (const std::exception&) { rejected = true; } + Require(rejected && bytes == before); + } + std::cout << "14 program layout cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/elfpatcher/tests/SectionRangeTests.cpp b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp new file mode 100644 index 000000000..5332f80e5 --- /dev/null +++ b/core/relinker/elfpatcher/tests/SectionRangeTests.cpp @@ -0,0 +1,47 @@ +#include +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Section range validation failed"); } + +static std::vector Fixture() { + std::vector bytes(512, 0); + Io::WriteU64(bytes, 32, 64); Io::WriteU16(bytes, 54, 56); Io::WriteU16(bytes, 56, 1); + Io::WriteU32(bytes, 64, 1); Io::WriteU64(bytes, 72, 128); + Io::WriteU64(bytes, 80, 0x1000); Io::WriteU64(bytes, 96, 256); + return bytes; +} + +int main() { + try { + Elfpatcher::SectionHeaderTableBuilder builder(std::make_shared()); + const Elfpatcher::SectionHeaderTableRequest valid{128, 8, 136, 24, 160, 16, 176, 16}; + auto bytes = Fixture(); builder.WriteTable(bytes, valid); + const auto table = Io::ReadU64(bytes, 40); + Require(Io::ReadU64(bytes, table + 2 * 64 + 16) == 0x1000); + auto empty = valid; empty.StubOffset = 384; empty.StubSize = 0; + bytes = Fixture(); builder.WriteTable(bytes, empty); + Require(Io::ReadU64(bytes, Io::ReadU64(bytes, 40) + 5 * 64 + 16) == 0x1100); + for (int kind = 0; kind < 8; ++kind) { + bytes = Fixture(); auto request = valid; + switch (kind) { + case 0: request.DynStrSize = 257; break; + case 1: request.DynSymSize = std::numeric_limits::max(); break; + case 2: request.DynamicSegmentOffset = 383; request.DynamicSegmentSize = 2; break; + case 3: request.StubOffset = 384; request.StubSize = 1; break; + case 4: Io::WriteU16(bytes, 54, 1); break; + case 5: Io::WriteU64(bytes, 32, std::numeric_limits::max()); break; + case 6: Io::WriteU64(bytes, 80, std::numeric_limits::max() - 4); break; + case 7: Io::WriteU16(bytes, 56, 20); break; + } + const auto before = bytes; + bool rejected = false; + try { builder.WriteTable(bytes, request); } catch (const std::exception&) { rejected = true; } + Require(rejected && bytes == before); + } + std::cout << "10 section range cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index f1b4a5b64..0e747853d 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -85,7 +85,7 @@ int Run(const int argc, char* argv[]) { } auto elfReader = std::make_shared(sourceBytes); - const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(); + const std::shared_ptr syscallScanner = args.skipSyscallCheck ? Relinker::MakeNullSyscallScanner() : Relinker::MakeSyscallScanner(args.trapSdkSyscallStubs); const auto pipeline = std::make_shared( elfReader, @@ -115,6 +115,19 @@ int Run(const int argc, char* argv[]) { const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { + if (!std::filesystem::exists(path)) return; + if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); + for (const auto& artifact : guestArtifacts) + if (std::filesystem::equivalent(artifact.SourcePath, path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite an input module: " + Io::Utf8Path(path)); + }; + if (desktopEntry) { + checkOutputInputs(desktopEntry->IconPath, "Icon"); + checkOutputInputs(desktopEntry->EntryPath, "Desktop entry"); + } + if (args.writeRegistry) { const std::filesystem::path outFsPath = Io::NativePath(absPath); std::vector registryPaths{ @@ -122,14 +135,7 @@ int Run(const int argc, char* argv[]) { }; for (const auto& artifact : guestArtifacts) registryPaths.push_back(outFsPath.parent_path() / Io::NativePath(Io::Utf8Path(outFsPath.stem()) + "." + Io::Utf8Path(artifact.Path.filename()) + ".registry.json")); - for (const auto& path : registryPaths) { - if (!std::filesystem::exists(path)) continue; - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), path)) - throw Domain::RelinkerException("Registry output would overwrite the input executable: " + Io::Utf8Path(path)); - for (const auto& artifact : guestArtifacts) - if (std::filesystem::equivalent(artifact.SourcePath, path)) - throw Domain::RelinkerException("Registry output would overwrite an input module: " + Io::Utf8Path(path)); - } + for (const auto& path : registryPaths) checkOutputInputs(path, "Registry"); const auto registryWriter = std::make_shared(); fileWriter.Write(Io::Utf8Path(registryPaths.front()), registryWriter->WriteCallRegistry(result.RegistryEntries)); for (std::size_t index = 0; index < guestArtifacts.size(); ++index) diff --git a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp index 2e4551de7..29e14cf09 100644 --- a/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/analysis/SyscallScanner.hpp @@ -6,7 +6,7 @@ namespace Relinker { -std::unique_ptr MakeSyscallScanner(); +std::unique_ptr MakeSyscallScanner(bool trapSdkStubs = false); std::unique_ptr MakeNullSyscallScanner(); } diff --git a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp index 3986e965e..021938a31 100644 --- a/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp +++ b/core/relinker/relinker/include/relinker/domain/ISyscallScanner.hpp @@ -2,6 +2,7 @@ #define RELINKER_DOMAIN_ISYSCALLSCANNER_HPP #include +#include #include namespace Relinker { @@ -10,7 +11,7 @@ class ISyscallScanner { public: virtual ~ISyscallScanner() = default; - virtual void ScanCodeSectionForSyscalls( + virtual std::vector ScanCodeSectionForSyscalls( const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index fcc35ac65..d566a152c 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -25,8 +25,16 @@ struct GuestSymbol { std::uint64_t Value; std::uint64_t Size; std::string Library; + std::string QualifiedName; }; +inline bool IdenticalOrdinaryFunctionExport(const GuestSymbol& left, const GuestSymbol& right) { + return left.Section != 0 && left.Section < 0xff00 && (left.Info >> 4) != 0 && + left.Visibility != 1 && left.Visibility != 2 && (left.Info & 15) == 2 && + left.Info == right.Info && left.Visibility == right.Visibility && + left.Section == right.Section && left.Value == right.Value && left.Size == right.Size; +} + struct GuestImage { std::filesystem::path SourcePath; std::string OutputName; @@ -49,7 +57,7 @@ struct GuestImage { class GuestImageReader { public: std::vector ReadModuleNames(const std::vector& bytes) const; - GuestImage Read(const std::filesystem::path& path, std::vector bytes) const; + GuestImage Read(const std::filesystem::path& path, std::vector bytes, bool windows = false) const; }; struct GuestArtifact { diff --git a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp index 6f08ab26f..0c698568e 100644 --- a/core/relinker/relinker/src/analysis/CallSiteResolver.cpp +++ b/core/relinker/relinker/src/analysis/CallSiteResolver.cpp @@ -4,7 +4,8 @@ #include #include #include -#include +#include +#include namespace Relinker { @@ -70,7 +71,7 @@ public: std::vector ResolveCallSites(const std::vector& textSection, FileByteOffset textSectionVAddr, VirtualAddress targetGotOrPltAddress, ByteCount targetGotOrPltSize) override; private: const std::vector* _cachedTextPtr = nullptr; - std::multimap _cachedTargetToInstr; + std::vector> _cachedTargetToInstr; FileByteOffset _cachedTextVAddr = 0; }; @@ -82,6 +83,10 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector auto scanner = Codegen::MakeInstructionScanner(); auto instructions = scanner->ScanCodeSection(textSection, 0, textSection.size()); _cachedTargetToInstr.clear(); + const auto referenceCount = std::count_if(instructions.begin(), instructions.end(), [&](const auto& match) { + return isRipRelativeFF(textSection, match) || isRipRelativeMov64(textSection, match); + }); + _cachedTargetToInstr.reserve(static_cast(referenceCount)); for (const auto& m : instructions) { std::size_t dispOff = 0; if (isRipRelativeFF(textSection, m)) @@ -94,15 +99,20 @@ std::vector CallSiteResolver::ResolveCallSites(const std::vector std::int32_t disp = readDisp32(textSection, dispOff); VirtualAddress instrEnd = textSectionVAddr + m.Offset + static_cast(m.Length); VirtualAddress target = static_cast(static_cast(instrEnd) + disp); - _cachedTargetToInstr.emplace(target, m.Offset); + _cachedTargetToInstr.emplace_back(target, m.Offset); } + std::sort(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end()); _cachedTextPtr = &textSection; _cachedTextVAddr = textSectionVAddr; } std::vector sites; - auto lower = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress); - auto upper = _cachedTargetToInstr.lower_bound(targetGotOrPltAddress + targetGotOrPltSize); + const auto findTarget = [&](VirtualAddress target) { + return std::lower_bound(_cachedTargetToInstr.begin(), _cachedTargetToInstr.end(), + std::pair{target, 0}); + }; + auto lower = findTarget(targetGotOrPltAddress); + auto upper = findTarget(targetGotOrPltAddress + targetGotOrPltSize); for (auto it = lower; it != upper; ++it) sites.push_back(textSectionVAddr + it->second); return sites; diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 45fa513c8..a7e80bcf0 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -2,7 +2,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -19,6 +22,28 @@ static constexpr std::uint8_t SYSRET_BYTE0 = 0x0F; static constexpr std::uint8_t SYSRET_BYTE1 = 0x07; static constexpr std::size_t SYSCALL_NUMBER_LOOKBACK = 8; +static constexpr std::array SDK_GENERIC_SYSCALL_STUB{ + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, +}; + +static bool IsSdkSyscallStub(const std::vector& code, const std::size_t syscallOffset) { + constexpr std::array numberLoad{0x48, 0xC7, 0xC0}; + constexpr std::array argumentShuffle{0x49, 0x89, 0xCA}; + if (syscallOffset <= code.size() && syscallOffset >= SDK_GENERIC_SYSCALL_STUB.size() && code.size() - syscallOffset >= 3u) { + const auto begin = code.begin() + static_cast(syscallOffset - SDK_GENERIC_SYSCALL_STUB.size()); + if (std::equal(SDK_GENERIC_SYSCALL_STUB.begin(), SDK_GENERIC_SYSCALL_STUB.end(), begin) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3) + return true; + } + if (syscallOffset > code.size() || syscallOffset < 10u || code.size() - syscallOffset < 3u) + return false; + const auto begin = code.begin() + static_cast(syscallOffset - 10u); + return std::equal(numberLoad.begin(), numberLoad.end(), begin) + && std::equal(argumentShuffle.begin(), argumentShuffle.end(), begin + 7) + && code[syscallOffset] == 0x0F && code[syscallOffset + 1] == 0x05 && code[syscallOffset + 2] == 0xC3; +} + enum class ForbiddenInstruction { None, Invocation, @@ -100,13 +125,18 @@ static std::optional ConstantSyscallNumber( class SyscallScanner : public ISyscallScanner { public: - void ScanCodeSectionForSyscalls( + explicit SyscallScanner(const bool trapSdkStubs) : _trapSdkStubs(trapSdkStubs) {} + + std::vector ScanCodeSectionForSyscalls( const std::vector& codeSection, FileByteOffset codeSectionOffset, FileByteOffset codeSectionSize) override; + +private: + bool _trapSdkStubs; }; -void SyscallScanner::ScanCodeSectionForSyscalls( +std::vector SyscallScanner::ScanCodeSectionForSyscalls( const std::vector& codeSection, const FileByteOffset codeSectionOffset, const FileByteOffset codeSectionSize) { @@ -115,6 +145,7 @@ void SyscallScanner::ScanCodeSectionForSyscalls( const auto scanner = Codegen::MakeInstructionScanner(); const auto matches = scanner->ScanCodeSection(codeSection, codeSectionOffset, codeSectionSize); + std::vector trapped; for (std::size_t index = 0; index < matches.size(); ++index) { const auto& match = matches[index]; const std::size_t start = static_cast(match.Offset - codeSectionOffset); @@ -127,6 +158,12 @@ void SyscallScanner::ScanCodeSectionForSyscalls( if (kind == ForbiddenInstruction::None) continue; + if (_trapSdkStubs && kind == ForbiddenInstruction::Invocation && IsSdkSyscallStub(codeSection, opcode)) { + trapped.push_back(opcode); + std::cout << "Trapping SDK syscall instruction at code offset 0x" << std::hex << match.Offset << std::dec << '\n'; + continue; + } + std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; @@ -136,19 +173,20 @@ void SyscallScanner::ScanCodeSectionForSyscalls( throw RelinkerException(msg.str(), match.Offset); } + return trapped; } -std::unique_ptr MakeSyscallScanner() { - return std::make_unique(); +std::unique_ptr MakeSyscallScanner(const bool trapSdkStubs) { + return std::make_unique(trapSdkStubs); } class NullSyscallScanner : public ISyscallScanner { public: - void ScanCodeSectionForSyscalls( + std::vector ScanCodeSectionForSyscalls( const std::vector&, FileByteOffset, FileByteOffset - ) override {} + ) override { return {}; } }; std::unique_ptr MakeNullSyscallScanner() { diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index ac7caaf6b..e6c3545a7 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -87,7 +87,7 @@ std::vector GuestImageReader::ReadModuleNames(const std::vector bytes) const { +GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector bytes, const bool windows) const { const auto fail = [&](const std::string& message) { throw Domain::RelinkerException(path.string() + ": " + message); }; const auto range = [&](std::uint64_t offset, std::uint64_t size) { if (offset > bytes.size() || size > bytes.size() - offset) { @@ -204,19 +204,21 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); std::vector neededLibraries; for (const auto offset : needed) neededLibraries.push_back(string(offset)); - std::set exports; + std::map exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; if (Io::ReadU16(bytes, symOffset + offset + 6) == 0 && library.empty()) { if (auto declared = Domain::DeclaredImportModule(name, importModules); !declared.empty()) image.Dynamic.UnmatchedImportModules.insert(std::move(declared)); } + const auto qualifiedName = name; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; const auto section = Io::ReadU16(bytes, symOffset + offset + 6); const auto value = Io::ReadU64(bytes, symOffset + offset + 8); const auto size = Io::ReadU64(bytes, symOffset + offset + 16); + const GuestSymbol symbol{name, info, visibility, section, value, size, library, qualifiedName}; if (offset == 0 && (info != 0 || visibility != 0 || section != 0 || value != 0 || size != 0 || !name.empty())) fail("Invalid null symbol"); if (offset == 0) { image.Symbols.push_back({{}, 0, 0, 0, 0, 0}); @@ -224,7 +226,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector } if (offset != 0 && ((info >> 4) > 2 || (visibility & ~3u) != 0 || ((info & 15) != 0 && (info & 15) != 1 && (info & 15) != 2 && (info & 15) != 6))) fail("Unsupported symbol attributes: " + name); if (section != 0 && (info >> 4) != 0 && visibility != 1 && visibility != 2) { - if (name.empty() || !exports.insert(name).second) fail("Duplicate or empty export after stripping #: " + name); + const auto [found, inserted] = exports.emplace(name, image.Symbols.size()); + if (name.empty() || (!inserted && (!windows || !IdenticalOrdinaryFunctionExport(image.Symbols[found->second], symbol)))) fail("Duplicate or empty export after stripping #: " + name); } if ((info & 15) == 6 && section != 0 && (tls == nullptr || value > tls->MemorySize || size > tls->MemorySize - value)) fail("TLS symbol exceeds TLS segment: " + name); if (section != 0 && section != AbsoluteSection && (info & 15) != 6) { @@ -234,7 +237,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size, library}); + image.Symbols.push_back(symbol); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 689f4dafd..d51c4c160 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -70,10 +70,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (hasPrx) directories.push_back(prx); std::vector runtimeDirectories; for (const auto& name : moduleDirectories) { - const auto relative = Io::NativePath(name); - if (relative.empty() || relative.is_absolute() || relative.lexically_normal().generic_string().starts_with("..")) + const auto relative = Io::NativePath(name).lexically_normal(); + if (relative.empty() || relative.has_root_path() || *relative.begin() == "..") throw Domain::RelinkerException("--module-dir must be a directory inside the guest module parent directory: " + name); - const auto directory = (root / relative).lexically_normal(); + auto directory = (root / relative).lexically_normal(); + if (!directory.has_filename()) directory = directory.parent_path(); if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + Io::Utf8Path(directory)); if (std::find(directories.begin(), directories.end(), directory) != directories.end()) throw Domain::RelinkerException("Duplicate guest module directory: " + Io::Utf8Path(directory)); directories.push_back(directory); @@ -122,7 +123,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path return name == identity || name == identity + ".prx" || name == identity + ".sprx" || name == identity + ".suprx"; }); }; - for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)))); + for (const auto& path : paths) discovered.emplace(path, GuestImageReader().Read(path, reader.Read(Io::Utf8Path(path)), windows)); if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); paths.erase(std::unique(paths.begin(), paths.end()), paths.end()); @@ -342,7 +343,12 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& header : codeHeaders) { const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize)) { + if (offset > code.size() || code.size() - offset < 2u) + throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); + image.Bytes[header.Offset + offset] = 0x0F; + image.Bytes[header.Offset + offset + 1] = 0x0B; + } } const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); const auto destination = outputDirectory / relativeDirectory; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index a92ef6238..c6af1ec10 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include namespace Relinker { @@ -132,6 +133,16 @@ std::vector ElfReader::ReadProgramHeaders() const { std::vector ElfReader::ReadSectionHeaders() const { const ElfHeader header = ReadHeader(); + if (header.SectionHeaderCount != 0 && header.SectionHeaderEntrySize != kShdrEntrySize) { + throw RelinkerException("Invalid ELF section header entry size: expected 64 bytes", 0x3a); + } + const ByteCount tableSize = static_cast(header.SectionHeaderCount) * header.SectionHeaderEntrySize; + if (!_rangeFits(header.SectionHeaderOffset, tableSize, _fileBuffer.size())) { + throw RelinkerException("Section header table out of bounds", header.SectionHeaderOffset); + } + if (header.SectionHeaderStringIndex != 0 && header.SectionHeaderStringIndex >= header.SectionHeaderCount) { + throw RelinkerException("Section header string table index out of bounds", 0x3e); + } std::vector headers; FileByteOffset offset = header.SectionHeaderOffset; @@ -146,7 +157,7 @@ std::vector ElfReader::ReadSectionHeaders() const { sh.SectionSize = _readU64At(offset + 0x20); sh.Link = _readU32At(offset + 0x28); sh.Info = _readU32At(offset + 0x2c); - sh.EntrySize = _readU64At(offset + 0x30); + sh.EntrySize = _readU64At(offset + kShdrEntSizeOffset); sh.Name = _resolveShdrName(nameOffset, header); @@ -165,17 +176,27 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade FileByteOffset shstrOffset = header.SectionHeaderOffset + (header.SectionHeaderStringIndex * header.SectionHeaderEntrySize); - const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); - - std::string name; - FileByteOffset currentPos = strTableOffset + nameOffset; - - while (currentPos < _fileBuffer.size() && _fileBuffer[currentPos] != '\0') { - name += static_cast(_fileBuffer[currentPos]); - currentPos++; + if (_readU32At(shstrOffset + kShdrTypeOffset) != SHT_STRTAB) { + throw RelinkerException("Section names do not reference a string table", shstrOffset); } - - return name; + const FileByteOffset strTableOffset = _readU64At(shstrOffset + 0x18); + const ByteCount strTableSize = _readU64At(shstrOffset + kShdrSizeOffset); + if (!_rangeFits(strTableOffset, strTableSize, _fileBuffer.size())) { + throw RelinkerException("Section name string table out of bounds", strTableOffset); + } + if (strTableSize == 0 && nameOffset == 0) { + return ""; + } + if (nameOffset >= strTableSize) { + throw RelinkerException("Section name offset out of bounds", nameOffset); + } + const auto first = _fileBuffer.begin() + strTableOffset + nameOffset; + const auto end = _fileBuffer.begin() + strTableOffset + strTableSize; + const auto terminator = std::find(first, end, 0); + if (terminator == end) { + throw RelinkerException("Unterminated section name", strTableOffset + nameOffset); + } + return std::string(first, terminator); } std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const { @@ -207,21 +228,14 @@ std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicH } FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { - const ElfHeader header = ReadHeader(); - - FileByteOffset offset = header.ProgramHeaderOffset; - - for (std::uint16_t i = 0; i < header.ProgramHeaderCount; ++i) { - const std::uint32_t type = _readU32At(offset); - const FileByteOffset segOffset = _readU64At(offset + 0x08); - const VirtualAddress segVAddr = _readU64At(offset + 0x10); - const ByteCount segFileSize = _readU64At(offset + 0x20); - - if (type == PT_LOAD && address >= segVAddr && address < segVAddr + segFileSize) { - return segOffset + (address - segVAddr); + for (const auto& segment : ReadProgramHeaders()) { + if (segment.Type == PT_LOAD && address >= segment.MappedAddress && + address - segment.MappedAddress < segment.FileSize) { + if (!_rangeFits(segment.Offset, segment.FileSize, _fileBuffer.size())) { + throw RelinkerException("PT_LOAD file mapping out of bounds", segment.Offset); + } + return segment.Offset + (address - segment.MappedAddress); } - - offset += header.ProgramHeaderEntrySize; } throw RelinkerException("Virtual address not mapped by any PT_LOAD segment", address); diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 5b28eb708..a486d3d64 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include namespace Relinker { @@ -46,7 +47,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::vector textSection; VirtualAddress textVAddr = 0; - std::vector, VirtualAddress>> executableSegments; + std::vector, VirtualAddress, FileByteOffset>> executableSegments; VirtualAddress gotVAddr = 0; ByteCount gotSize = 0; @@ -58,7 +59,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf textSection = segment; textVAddr = ph.MappedAddress; } - executableSegments.emplace_back(std::move(segment), ph.MappedAddress); + executableSegments.emplace_back(std::move(segment), ph.MappedAddress, ph.Offset); } } } @@ -231,6 +232,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); + if (relType == R_X86_64_NONE) + continue; + if (relType == 8) { if (symIdx != 0) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); @@ -257,7 +261,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { const auto functions = UnusedNidFilter::ReadExceptionFunctions(raw, programHeaders, {}, {}); - for (auto& [segment, segmentVAddr] : executableSegments) { + for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + static_cast(segmentFileOffset); VirtualAddress codeEnd = 0; for (const auto& function : functions) if (function.Begin >= segmentVAddr && function.Begin - segmentVAddr < segment.size()) @@ -292,8 +297,16 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } } - for (const auto& [segment, segmentVAddr] : executableSegments) - _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size()); + std::vector syscallPatches; + for (auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + for (const auto offset : _syscallScanner->ScanCodeSectionForSyscalls(segment, segmentVAddr, segment.size())) { + if (offset > segment.size() || segment.size() - offset < 2u) + throw RelinkerException("Syscall trap patch exceeds executable segment", segmentVAddr + offset); + segment[offset] = 0x0F; + segment[offset + 1] = 0x0B; + syscallPatches.push_back({segmentFileOffset + offset, {0x0F, 0x0B}}); + } + } _validationPolicy->ValidateSyscallAbsence(); @@ -416,7 +429,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf entries.push_back(std::move(entry)); } - for (const auto& [segment, segmentVAddr] : executableSegments) { + for (const auto& [segment, segmentVAddr, segmentFileOffset] : executableSegments) { + static_cast(segmentFileOffset); for (auto& entry : entries) { auto segmentSites = _callSiteResolver->ResolveCallSites(segment, segmentVAddr, entry.TargetOffset, 8); entry.CallSites.insert(entry.CallSites.end(), segmentSites.begin(), segmentSites.end()); @@ -424,6 +438,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } } + patches.insert(patches.end(), std::make_move_iterator(syscallPatches.begin()), std::make_move_iterator(syscallPatches.end())); return RelinkResult{std::move(entries), std::move(programHeaders), std::move(dynSection), gotVAddr, std::move(patches)}; } diff --git a/core/relinker/relinker/tests/CallSiteIndexTests.cpp b/core/relinker/relinker/tests/CallSiteIndexTests.cpp new file mode 100644 index 000000000..fceff1c26 --- /dev/null +++ b/core/relinker/relinker/tests/CallSiteIndexTests.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Incorrect call-site index"); } + +int main() { + try { + std::vector code; + const std::vector targets{0x2000, 0x1000, 0x2000, 0x3000, 0x1008}; + for (const auto target : targets) { + const auto offset = code.size(); + code.insert(code.end(), {0xFF, 0x15, 0, 0, 0, 0}); + const auto disp = static_cast(target - (0x100 + offset + 6)); + std::memcpy(code.data() + offset + 2, &disp, 4); + } + auto resolver = Relinker::MakeCallSiteResolver(); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 8) == std::vector{0x106}); + Require(resolver->ResolveCallSites(code, 0x100, 0x1000, 9) == std::vector{0x106, 0x118}); + Require(resolver->ResolveCallSites(code, 0x100, 0x2000, 0).empty()); + Require(resolver->ResolveCallSites(code, 0x100, 0x4000, 8).empty()); + auto copy = code; + Require(resolver->ResolveCallSites(copy, 0x100, 0x2000, 8) == std::vector{0x100, 0x10C}); + Require(resolver->ResolveCallSites(copy, 0x200, 0x2100, 8) == std::vector{0x200, 0x20C}); + std::vector mov{0x48, 0x8B, 0x05, 0xF9, 0x0F, 0, 0}; + Require(resolver->ResolveCallSites(mov, 0x1000, 0x2000, 8) == std::vector{0x1000}); + Require(resolver->ResolveCallSites({}, 0, 0, 1).empty()); + bool rejected = false; + try { resolver->ResolveCallSites(code, 0, std::numeric_limits::max(), 1); } + catch (const std::exception&) { rejected = true; } + Require(rejected); + std::cout << "10 call-site index cases passed\n"; + } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; } +} diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp new file mode 100644 index 000000000..065688d6d --- /dev/null +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Bytes = std::vector; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("Invalid fixture offset"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +Bytes Fixture() { + Bytes bytes(0x300); + const std::uint8_t magic[] = {0x7f, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(bytes.data(), magic, sizeof(magic)); + Write(bytes, 16, 3); + Write(bytes, 18, 62); + Write(bytes, 20, 1); + Write(bytes, 32, 64); + Write(bytes, 40, 128); + Write(bytes, 52, 64); + Write(bytes, 54, 56); + Write(bytes, 56, 1); + Write(bytes, 58, 64); + Write(bytes, 60, 3); + Write(bytes, 62, 2); + Write(bytes, 64, 1); + Write(bytes, 72, 0x240); + Write(bytes, 80, 0x1000); + Write(bytes, 96, 32); + Write(bytes, 104, 64); + Write(bytes, 192, 1); + Write(bytes, 196, 1); + Write(bytes, 216, 0x240); + Write(bytes, 224, 32); + Write(bytes, 240, 8); + Write(bytes, 248, 24); + Write(bytes, 256, 7); + Write(bytes, 260, 3); + Write(bytes, 280, 0x200); + const char strings[] = "\0.text\0.shstrtab"; + Write(bytes, 288, sizeof(strings)); + std::memcpy(bytes.data() + 0x200, strings, sizeof(strings)); + return bytes; +} + +void Require(const bool condition) { + if (!condition) throw std::runtime_error("Unexpected parsed value"); +} + +void RejectSections(Bytes bytes) { + try { + Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid section metadata was accepted"); +} + +void RejectAddress(Bytes bytes, const std::uint64_t address) { + try { + Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(address); + } catch (const Relinker::RelinkerException&) { + return; + } + throw std::runtime_error("Invalid file mapping was accepted"); +} + +} + +int main() { + const std::vector>> cases = { + {"section entry size field", [] { + const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); + Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); + }}, + {"section header stride", [] { + for (const std::uint16_t stride : {0, 56, 65}) { + auto bytes = Fixture(); + Write(bytes, 58, stride); + RejectSections(std::move(bytes)); + } + }}, + {"section name table index", [] { + auto bytes = Fixture(); + Write(bytes, 62, 3); + RejectSections(std::move(bytes)); + }}, + {"section name table type", [] { + auto bytes = Fixture(); + Write(bytes, 260, 1); + RejectSections(std::move(bytes)); + }}, + {"section name table range", [] { + auto bytes = Fixture(); + Write(bytes, 288, 0x200); + RejectSections(std::move(bytes)); + }}, + {"section name offset", [] { + auto bytes = Fixture(); + Write(bytes, 192, 64); + bytes[0x240] = 'X'; + RejectSections(std::move(bytes)); + }}, + {"section name terminator", [] { + auto bytes = Fixture(); + Write(bytes, 288, 5); + RejectSections(std::move(bytes)); + }}, + {"section header table truncation", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + bytes.resize(128 + 3 * 64 - 1); + RejectSections(std::move(bytes)); + }}, + {"translation program header stride", [] { + for (const std::uint16_t stride : {0, 48, 64}) { + auto bytes = Fixture(); + Write(bytes, 54, stride); + RejectAddress(std::move(bytes), 0x1008); + } + }}, + {"translation virtual boundary", [] { + auto bytes = Fixture(); + const auto start = std::numeric_limits::max() - 31; + Write(bytes, 80, start); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(start + 16) == 0x250); + }}, + {"translation overflowing file offset", [] { + auto bytes = Fixture(); + Write(bytes, 72, std::numeric_limits::max() - 3); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation file mapping range", [] { + auto bytes = Fixture(); + Write(bytes, 72, 0x2f0); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"empty and absent name tables", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + const auto unnamed = Relinker::ElfReader(bytes).ReadSectionHeaders(); + Require(unnamed.size() == 3 && unnamed[1].Name.empty()); + Write(bytes, 62, 2); + Write(bytes, 288, 0); + Write(bytes, 192, 0); + Write(bytes, 256, 0); + const auto empty = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(empty.size() == 3 && empty[1].Name.empty()); + }}, + {"section name at table boundary", [] { + auto bytes = Fixture(); + Write(bytes, 192, 17); + RejectSections(std::move(bytes)); + }}, + {"section name table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 280, std::numeric_limits::max() - 3); + RejectSections(std::move(bytes)); + }}, + {"section table offset overflow", [] { + auto bytes = Fixture(); + Write(bytes, 62, 0); + Write(bytes, 40, std::numeric_limits::max() - 31); + RejectSections(std::move(bytes)); + }}, + {"valid string table suffix and final null", [] { + auto bytes = Fixture(); + Write(bytes, 192, 2); + Write(bytes, 256, 16); + const auto sections = Relinker::ElfReader(std::move(bytes)).ReadSectionHeaders(); + Require(sections[1].Name == "text" && sections[2].Name.empty()); + }}, + {"ELF without section or program tables", [] { + auto bytes = Fixture(); + Write(bytes, 32, 0); + Write(bytes, 40, 0); + Write(bytes, 54, 0); + Write(bytes, 56, 0); + Write(bytes, 58, 0); + Write(bytes, 60, 0); + Write(bytes, 62, 0); + const Relinker::ElfReader reader(bytes); + Require(reader.ReadProgramHeaders().empty() && reader.ReadSectionHeaders().empty()); + RejectAddress(std::move(bytes), 0); + }}, + {"translation truncated program header table", [] { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x2c8, bytes.data() + 64, 56); + Write(bytes, 32, 0x2c8); + Write(bytes, 56, 2); + RejectAddress(std::move(bytes), 0x1008); + }}, + {"translation below segment and at virtual maximum", [] { + RejectAddress(Fixture(), 0x0fff); + auto bytes = Fixture(); + const auto maximum = std::numeric_limits::max(); + Write(bytes, 80, maximum - 31); + Require(Relinker::ElfReader(std::move(bytes)).TranslateVirtualAddress(maximum) == 0x25f); + }}, + {"ordinary translations and zero fill", [] { + const Relinker::ElfReader reader(Fixture()); + Require(reader.TranslateVirtualAddress(0x1000) == 0x240); + Require(reader.TranslateVirtualAddress(0x101f) == 0x25f); + RejectAddress(Fixture(), 0x1020); + }}, + }; + std::size_t failures = 0; + for (const auto& [name, test] : cases) { + try { + test(); + std::cout << "PASS: " << name << '\n'; + } catch (const std::exception& error) { + ++failures; + std::cerr << "FAIL: " << name << ": " << error.what() << '\n'; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/relinker/relinker/tests/SyscallScannerTests.cpp b/core/relinker/relinker/tests/SyscallScannerTests.cpp index 3b7846a8e..65901a6f3 100644 --- a/core/relinker/relinker/tests/SyscallScannerTests.cpp +++ b/core/relinker/relinker/tests/SyscallScannerTests.cpp @@ -70,6 +70,12 @@ const Bytes kPrefixedClobberedStub = { 0xC3, }; +const Bytes kSdkGenericStub = { + 0x48, 0x89, 0xF8, 0x48, 0x89, 0xF7, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCA, + 0x4D, 0x89, 0xC2, 0x4D, 0x89, 0xC8, 0x4C, 0x8B, 0x4C, 0x24, 0x08, + 0x0F, 0x05, 0xC3, +}; + std::string RunScanner(const std::string& binary, const Bytes& code) { const TempDirectory directory; const auto input = directory.Path() / "input.elf"; @@ -96,6 +102,35 @@ void RequireNoNumber(const std::string& binary, const Bytes& code, const std::st "Relinker reported a syscall number for the " + description + ":\n" + output); } +void RequireSdkStubsAreTrapped(const std::string& binary) { + Bytes code = kConstantStub; + const auto numberedOffset = code.size() - 3u; + code.insert(code.end(), kSdkGenericStub.begin(), kSdkGenericStub.end()); + const auto genericOffset = kConstantStub.size() + kSdkGenericStub.size() - 3u; + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(code)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker did not trap known SDK syscall stubs:\n" + run.Output); + const auto relinked = ReadFile(output); + require(Read(relinked, kCodeOffset + numberedOffset) == 0x0B0Fu, + "Relinker did not replace the numbered SDK syscall with UD2"); + require(Read(relinked, kCodeOffset + genericOffset) == 0x0B0Fu, + "Relinker did not replace the generic SDK syscall with UD2"); +} + +void RequireUnknownSyscallsStillFail(const std::string& binary) { + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kRegisterStub)); + const auto run = RunRelinker(binary, {"--skip-sce-module", "--trap-sdk-syscall-stubs", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode != 0, "Relinker accepted an unknown syscall in SDK-stub trap mode:\n" + run.Output); + require(run.Output.find("Forbidden syscall instruction at code offset 0x") != std::string::npos, + "Relinker did not report an unknown syscall in SDK-stub trap mode:\n" + run.Output); +} + } int main(const int argc, char** argv) { @@ -115,6 +150,9 @@ int main(const int argc, char** argv) { RequireNumber(binary, kPrefixedConstantStub, "syscall number 9", "prefixed constant stub"); RequireNoNumber(binary, kPrefixedClobberedStub, "prefixed stub whose intervening instruction writes rax"); + RequireSdkStubsAreTrapped(binary); + RequireUnknownSyscallsStillFail(binary); + std::cout << "syscall scanner tests passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/relinker/relinker/tests/WindowsExportAliasTests.cpp b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp new file mode 100644 index 000000000..78cc146c8 --- /dev/null +++ b/core/relinker/relinker/tests/WindowsExportAliasTests.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Bytes = std::vector; +static void Check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); } +static void Ph(Bytes& b, int i, unsigned type, unsigned flags, unsigned off, unsigned va, unsigned size) { + const auto p=64+i*56; Io::WriteU32(b,p,type); Io::WriteU32(b,p+4,flags); + Io::WriteU64(b,p+8,off);Io::WriteU64(b,p+16,va);Io::WriteU64(b,p+24,va); + Io::WriteU64(b,p+32,size);Io::WriteU64(b,p+40,size);Io::WriteU64(b,p+48,8); +} +static Bytes Fixture(bool reverse=false) { + Bytes b(0x4000);std::memcpy(b.data(),"\x7f" "ELF\x02\x01\x01",7); + Io::WriteU16(b,16,3);Io::WriteU16(b,18,62);Io::WriteU32(b,20,1); + Io::WriteU64(b,32,64);Io::WriteU16(b,52,64);Io::WriteU16(b,54,56);Io::WriteU16(b,56,3); + Ph(b,0,1,5,0x1000,0,0x1000);Ph(b,1,1,6,0x2000,0x2000,0x1000); + b[0x1020]=0xc3;b[0x1028]=0xc3; + std::string strings(1,'\0');auto str=[&](const char* s){auto o=strings.size();strings+=s;strings+='\0';return o;}; + const auto a=str(reverse?"alias#B#A":"alias#A#A"),c=str(reverse?"alias#A#A":"alias#B#A"); + const auto imp=str("imported#A#B"),host=str("fixtureHost.prx"),module=str("fixtureModule"); + std::copy(strings.begin(),strings.end(),b.begin()+0x2200); + const std::pair tags[]={{5,0x2200},{10,strings.size()},{6,0x2400},{11,24},{4,0x2600},{7,0x2700},{8,48},{9,24},{1,host},{0x61000043,module},{0x61000045,(1ull<<48)|host},{0,0}}; + Ph(b,2,2,6,0x2000,0x2000,sizeof(tags)); + for(unsigned i=0;i=start&&rva-start static void Reject(F&& f,const char* expected) { + bool rejected=false;try{f();}catch(const Domain::RelinkerException& e){rejected=std::string(e.what()).find(expected)!=std::string::npos;} + Check(rejected,expected); +} +static void Positive(bool reverse) { + const auto bytes=Fixture(reverse);const auto g=Relinker::GuestImageReader().Read("generic.prx",bytes,true); + Check(g.Symbols.size()==4&&g.Bytes==bytes,"input symbols/bytes changed"); + Check(g.Symbols[1].Name=="alias"&&g.Symbols[2].Name=="alias","stripped lookup names"); + Check(g.Symbols[1].QualifiedName==(reverse?"alias#B#A":"alias#A#A")&&g.Symbols[2].QualifiedName==(reverse?"alias#A#A":"alias#B#A"),"qualified names lost"); + Check(g.Symbols[1].Info==18&&g.Symbols[2].Info==18&&g.Symbols[1].Visibility==3&&g.Symbols[2].Visibility==3&&g.Symbols[1].Section==1&&g.Symbols[2].Section==1&&g.Symbols[1].Value==0x20&&g.Symbols[2].Value==0x20&&g.Symbols[1].Size==1&&g.Symbols[2].Size==1,"raw tuple changed"); + Check((Io::ReadU64(g.Dynamic.RelaData,8)>>32)==1&&(Io::ReadU64(g.Dynamic.RelaData,32)>>32)==2,"alias relocation indices changed"); + Check(g.Symbols[3].QualifiedName=="imported#A#B"&&g.Symbols[3].Library=="fixtureHost.prx","qualified import changed"); + Domain::GuestRuntime runtime;const auto pe=Elfpatcher::GuestModuleWriter().WriteWindows(g,runtime); + const auto directory=Offset(pe,Io::ReadU32(pe,Io::ReadU32(pe,0x3c)+24+112)); + Check(Io::ReadU32(pe,directory+20)==1&&Io::ReadU32(pe,directory+24)==1,"multiple PE exports"); + const auto function=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+28))); + const auto nameRva=Io::ReadU32(pe,Offset(pe,Io::ReadU32(pe,directory+32))); + Check(std::strcmp(reinterpret_cast(pe.data()+Offset(pe,nameRva)),"alias")==0,"wrong export name"); + Elfpatcher::Windows::WindowsLoadImage image(g.Bytes,g.Headers,false); + Check(function==image.GetRva(0x20,1),"wrong export RVA"); + for(auto target:{0x2800,0x2808})Check(Io::ReadU64(pe,Offset(pe,image.GetRva(target,8)))==Elfpatcher::Windows::ImageBase+function,"wrong alias relocation value"); +} +int main() { + try { + Positive(false);Positive(true); + Reject([]{Relinker::GuestImageReader().Read("generic.prx",Fixture());},"Duplicate or empty export"); + const std::array,7> changes={{{4,0x22},{4,0x11},{5,0},{6,2},{8,0x28},{16,2},{6,Relinker::AbsoluteSection}}}; + for(const auto& [field,value]:changes){auto b=Fixture();auto off=0x2400+48+field;if(field<6)b[off]=value;else if(field==6)Io::WriteU16(b,off,value);else Io::WriteU64(b,off,value);Reject([&]{Relinker::GuestImageReader().Read("generic.prx",b,true);},"Duplicate or empty export");} + auto good=Relinker::GuestImageReader().Read("generic.prx",Fixture(),true); + for(unsigned kind=0;kind<10;++kind){auto g=good;auto& s=g.Symbols[2];switch(kind){case 0:s.Value+=8;break;case 1:s.Size+=1;break;case 2:s.Info=0x22;break;case 3:s.Info=0x11;break;case 4:s.Visibility=0;break;case 5:s.Section=2;break;case 6:g.Symbols[1].Info=s.Info=0x11;break;case 7:g.Symbols[1].Section=s.Section=Relinker::AbsoluteSection;break;case 8:g.Symbols[1].Info=s.Info=0x16;g.Dynamic.RelaData.clear();break;case 9:g.Symbols[1].Info=s.Info=0x10;break;}Check(!Relinker::IdenticalOrdinaryFunctionExport(g.Symbols[1],s),"nonordinary/conflicting tuple accepted");Domain::GuestRuntime r;Reject([&]{Elfpatcher::GuestModuleWriter().WriteWindows(g,r);},kind==8?"TLS":"Duplicate guest export");} + Check(Domain::DeclaredImportModule("same#A#B",{{1,"one"},{2,"two"}})=="one"&&Domain::DeclaredImportModule("same#A#C",{{1,"one"},{2,"two"}})=="two","module-qualified IDs merged"); + std::cout<<"WINDOWS_EXPORT_ALIAS_CASES_PASSED\npreserved-indices reversed-order conflicting-fields strict-nonwindows qualified-import writer-guard\n"; + } catch(const std::exception& e){std::cerr< #include +#include #include #include #include @@ -314,6 +315,61 @@ bool reachesWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& lo return false; } +std::uint32_t joinWithinIteration(const ControlFlowGraph& graph, const NaturalLoop& loop, std::uint32_t trueTarget, std::uint32_t falseTarget, std::uint32_t globalMerge) { + std::vector reached; + std::vector pending = {trueTarget, falseTarget}; + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == globalMerge || contains(reached, blockId) || !isInsideLoopConstruct(graph, loop, blockId)) { + continue; + } + reached.push_back(blockId); + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + std::vector joining = {globalMerge}; + for (std::size_t index = 0; index < joining.size(); ++index) { + for (const auto predecessor : graph.FindBlock(joining[index]).predecessors) { + if (contains(reached, predecessor) && !contains(joining, predecessor)) { + joining.push_back(predecessor); + } + } + } + sortUnique(joining); + if (!contains(joining, trueTarget) || !contains(joining, falseTarget)) { + return globalMerge; + } + std::vector> postDominators(graph.blocks.size()); + for (const auto blockId : joining) { + postDominators[blockId] = blockId == globalMerge ? std::vector{globalMerge} : joining; + } + for (bool changed = true; changed;) { + changed = false; + for (const auto blockId : joining) { + if (blockId == globalMerge) { + continue; + } + auto next = joining; + for (const auto successor : graph.FindBlock(blockId).successors) { + if (contains(joining, successor)) { + next = intersectSorted(next, postDominators[successor]); + } + } + addSortedUnique(next, blockId); + if (next != postDominators[blockId]) { + postDominators[blockId] = std::move(next); + changed = true; + } + } + } + const auto common = intersectSorted(postDominators[trueTarget], postDominators[falseTarget]); + const auto merge = std::find_if(common.begin(), common.end(), [&](std::uint32_t candidate) { + return std::all_of(common.begin(), common.end(), [&](std::uint32_t other) { return contains(postDominators[candidate], other); }); + }); + return merge == common.end() ? globalMerge : *merge; +} + std::vector linearTail(const ControlFlowGraph& graph, std::uint32_t start) { std::vector tail; for (auto blockId = start; !contains(tail, blockId);) { @@ -428,6 +484,7 @@ std::uint32_t mergeBesideReturns(const ControlFlowGraph& graph, const BasicBlock std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const NaturalLoop& loop, const BasicBlock& header, std::uint32_t join) { const auto count = static_cast(graph.blocks.size()); + static const bool reuse = std::getenv("APS5_NO_PERF_ITERATION_SELECTION") == nullptr; const auto escapes = [&](std::uint32_t blockId) { return blockId == loop.continueBlock || blockId == loop.mergeBlock; }; std::vector joins(count, false); std::vector pending = {join}; @@ -455,16 +512,30 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur if (!joins[block.id] || block.id == join) { continue; } - auto next = allBlockIds(count); + bool seeded = !reuse; + auto next = seeded ? allBlockIds(count) : std::vector{}; for (const auto successor : block.successors) { if (joins[successor]) { - next = intersectSorted(next, postDominators[successor]); + if (!seeded) { + next = postDominators[successor]; + seeded = true; + } else if (reuse) { + intersectAnalysisSet(next, postDominators[successor]); + } else { + next = intersectSorted(next, postDominators[successor]); + } } } - addUnique(next, block.id); - sortUnique(next); + if (!seeded) next = allBlockIds(count); + if (reuse) { + addSortedUnique(next, block.id); + } else { + addUnique(next, block.id); + sortUnique(next); + } if (next != postDominators[block.id]) { - postDominators[block.id] = std::move(next); + if (reuse) replaceAnalysisSet(postDominators[block.id], std::move(next)); + else postDominators[block.id] = std::move(next); changed = true; } } @@ -479,7 +550,7 @@ std::uint32_t iterationSelectionMerge(const ControlFlowGraph& graph, const Natur return *merge; } -std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool routeLeavingExits = false, bool routeSharedExits = false) { const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); const auto* loop = findInnermostContainingLoop(graph, block.id); const auto trueTarget = block.terminator.trueBlock; @@ -511,6 +582,9 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock return globalMerge; } + if (routeSharedExits && reachesWithinIteration(graph, *loop, trueTarget, loop->continueBlock) && reachesWithinIteration(graph, *loop, falseTarget, loop->continueBlock)) { + return joinWithinIteration(graph, *loop, trueTarget, falseTarget, loop->continueBlock); + } if (isLoopControlGateway(graph, *loop, trueTarget) && graph.Dominates(block.id, trueTarget) && isInsideLoopConstruct(graph, *loop, falseTarget)) { return trueTarget; } @@ -575,7 +649,7 @@ bool isInnermostLoopControlConditional(const ControlFlowGraph& graph, const Basi return (isControlTarget(trueTarget) && (isControlTarget(falseTarget) || isInsideLoopConstruct(graph, *loop, falseTarget))) || (isControlTarget(falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)); } -std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block) { +std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { if (block.terminator.kind != TerminatorKind::ConditionalBranch) { return InvalidControlFlowId; } @@ -591,7 +665,7 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB } const auto body = trueInBody ? trueTarget : falseTarget; const auto tail = trueInBody ? falseTarget : trueTarget; - if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail)) { + if (tail == loop->mergeBlock || tail == loop->continueBlock || body == loop->continueBlock || !isInsideLoopConstruct(graph, *loop, tail) || !graph.Dominates(block.id, body) || !graph.Dominates(block.id, tail) || (privateTail && reachesWithinIteration(graph, *loop, body, tail))) { return InvalidControlFlowId; } std::vector pending{tail}; @@ -616,6 +690,20 @@ std::uint32_t exitTailSelectionMerge(const ControlFlowGraph& graph, const BasicB return body; } +bool isStructuredLoopExit(const ControlFlowGraph& graph, const BasicBlock& block, bool privateTail) { + if (!isInnermostLoopControlConditional(graph, block)) { + return false; + } + if (!privateTail) { + return true; + } + const auto* loop = findInnermostContainingLoop(graph, block.id); + const auto isControlTarget = [&](std::uint32_t target) { + return target == loop->mergeBlock || target == loop->continueBlock || (block.id == loop->continueBlock && target == loop->headerBlock); + }; + return isControlTarget(block.terminator.trueBlock) || isControlTarget(block.terminator.falseBlock) || exitTailSelectionMerge(graph, block, true) != InvalidControlFlowId; +} + bool mergeLeavesContainingLoop(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t merge) { for (const auto& loop : graph.naturalLoops) { if (loop.headerBlock != header && isInsideLoopConstruct(graph, loop, header) && !isInsideLoopConstruct(graph, loop, merge)) { @@ -1082,6 +1170,41 @@ std::uint32_t nextGotoVariable(const ControlFlowGraph& graph) { return next; } +bool hasSharedExitSelection(const ControlFlowGraph& graph, const NaturalLoop& loop) { + for (const auto id : loop.blocks) { + const auto& block = graph.FindBlock(id); + if (block.terminator.kind != TerminatorKind::ConditionalBranch) continue; + const auto globalMerge = graph.FindNearestCommonPostDominator(block.terminator.trueBlock, block.terminator.falseBlock); + if (globalMerge != InvalidControlFlowId && isInsideLoopConstruct(graph, loop, globalMerge)) continue; + for (const auto exit : loop.exitBlocks) { + const auto& forwarder = graph.FindBlock(exit); + if (forwarder.instructionBegin == forwarder.instructionEnd && forwarder.terminator.kind == TerminatorKind::Branch && forwarder.terminator.gotoVariable != InvalidControlFlowId && forwarder.terminator.trueBlock == loop.mergeBlock) continue; + if (exit != loop.mergeBlock && graph.Dominates(block.id, exit) && reachesWithinIteration(graph, loop, block.terminator.trueBlock, exit) && reachesWithinIteration(graph, loop, block.terminator.falseBlock, exit)) return true; + } + } + return false; +} + +bool exitsLeaveConstruct(const ControlFlowGraph& graph, const NaturalLoop& loop) { + std::vector visited(graph.blocks.size(), false); + std::vector pending; + pending.insert(pending.end(), loop.exitBlocks.begin(), loop.exitBlocks.end()); + while (!pending.empty()) { + const auto blockId = pending.back(); + pending.pop_back(); + if (blockId == loop.mergeBlock || visited[blockId]) { + continue; + } + if (!graph.Dominates(loop.headerBlock, blockId)) { + return true; + } + visited[blockId] = true; + const auto& successors = graph.FindBlock(blockId).successors; + pending.insert(pending.end(), successors.begin(), successors.end()); + } + return false; +} + std::optional routeLoopExits(const ControlFlowGraph& graph, const NaturalLoop& loop, const std::function& recompute, std::string& refusal) { std::vector exits; for (const auto member : loop.blocks) { @@ -1192,7 +1315,7 @@ bool structurizeEnteredRegion(ControlFlowGraph& graph, std::uint32_t header, std throw std::runtime_error("selection header block " + std::to_string(header) + " has externally entered region block " + std::to_string(entryEdges.front().second) + " (from block " + std::to_string(entryEdges.front().first) + "): " + cloneReason + "; " + junctionReason); } -bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute) { +bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const std::function& recompute, bool routeLeavingExits, bool routeSharedExits) { std::vector loopHeaders; loopHeaders.reserve(graph.naturalLoops.size()); for (const auto& loop : graph.naturalLoops) { @@ -1213,11 +1336,11 @@ bool splitOneSelectionMerge(ControlFlowGraph& graph, SplitBudget& budget, const for (const auto blockId : selectionHeaders) { const auto& block = graph.FindBlock(blockId); - if (isInnermostLoopControlConditional(graph, block)) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { continue; } @@ -1332,10 +1455,31 @@ void tarjanVisit(TarjanState& state, std::uint32_t blockId) { void Structurizer::Structurize(ControlFlowGraph& graph) const { const auto original = graph; try { - structurize(graph, false); + structurize(graph, false, false, false); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); } catch (const std::runtime_error&) { graph = original; - structurize(graph, true); + try { + structurize(graph, true, false, false); + if (!hasStructuredConstructs(graph)) throw std::runtime_error("CFG does not have structured constructs"); + } catch (const std::runtime_error&) { + const auto error = std::current_exception(); + graph = original; + try { + structurize(graph, false, true, false); + if (hasStructuredConstructs(graph)) return; + } catch (const std::runtime_error&) { + } + graph = original; + try { + structurize(graph, false, true, true); + } catch (const std::runtime_error&) { + std::rethrow_exception(error); + } + if (!hasStructuredConstructs(graph)) { + std::rethrow_exception(error); + } + } } } @@ -1350,14 +1494,14 @@ void Structurizer::privatizeSharedReturns(ControlFlowGraph& graph) const { } } -void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns) const { +void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns, bool routeLeavingExits, bool routeSharedExits) const { recomputeAnalyses(graph); verifyReducibility(graph); if (privatizeReturns) { privatizeSharedReturns(graph); } - canonicalizeNaturalLoops(graph); - splitSharedMergeBlocks(graph); + canonicalizeNaturalLoops(graph, routeLeavingExits, routeSharedExits); + splitSharedMergeBlocks(graph, routeLeavingExits, routeSharedExits); isolateSemanticLoopHeaders(graph); orderByDominance(graph); clearStructuredTerminators(graph); @@ -1389,14 +1533,14 @@ void Structurizer::structurize(ControlFlowGraph& graph, bool privatizeReturns) c if (block.terminator.kind != TerminatorKind::ConditionalBranch || block.terminator.loopHeader) { continue; } - if (isInnermostLoopControlConditional(graph, block)) { - if (const auto merge = exitTailSelectionMerge(graph, block); merge != InvalidControlFlowId) { + if (isStructuredLoopExit(graph, block, routeLeavingExits)) { + if (const auto merge = exitTailSelectionMerge(graph, block, routeLeavingExits); merge != InvalidControlFlowId) { exitTails.emplace_back(block.id, merge); } continue; } - const auto merge = findSelectionMerge(graph, block); + const auto merge = findSelectionMerge(graph, block, routeLeavingExits, routeSharedExits); if (merge == InvalidControlFlowId) { throw std::runtime_error("conditional block " + std::to_string(block.id) + " has no structured merge"); } @@ -1576,7 +1720,7 @@ void Structurizer::recomputeAnalyses(ControlFlowGraph& graph) const { computeStronglyConnectedComponents(graph); } -void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { +void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const { const auto rewriteBudget = graph.blocks.size() * 2u + 16u; for (std::size_t rewrite = 0; rewrite < rewriteBudget; ++rewrite) { bool changed = false; @@ -1587,7 +1731,10 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { addUnique(latches, edge.sourceBlock); } } - if (latches.size() <= 1u) { + const bool latchInInnerLoop = routeSharedExits && latches.size() == 1u && std::any_of(graph.naturalLoops.begin(), graph.naturalLoops.end(), [&](const NaturalLoop& inner) { + return inner.headerBlock != loop.headerBlock && contains(loop.blocks, inner.headerBlock) && contains(inner.blocks, latches.front()); + }); + if (latches.size() <= 1u && !latchInInnerLoop) { continue; } @@ -1627,7 +1774,7 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { const NaturalLoop* unmerged = nullptr; for (const auto& loop : graph.naturalLoops) { - if (loop.exitBlocks.size() > 1u && loop.mergeBlock == InvalidControlFlowId && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { + if (loop.exitBlocks.size() > 1u && (loop.mergeBlock == InvalidControlFlowId || (routeLeavingExits && (exitsLeaveConstruct(graph, loop) || (routeSharedExits && hasSharedExitSelection(graph, loop))))) && (unmerged == nullptr || loop.blocks.size() < unmerged->blocks.size())) { unmerged = &loop; } } @@ -1637,7 +1784,8 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { std::string refusal; auto routed = routeLoopExits(graph, *unmerged, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, refusal); if (!routed) { - throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits with no common post-dominator: " + refusal); + const auto exits = unmerged->mergeBlock == InvalidControlFlowId ? std::string("with no common post-dominator") : "that leave the loop before its merge block " + std::to_string(unmerged->mergeBlock); + throw std::runtime_error("loop at block " + std::to_string(unmerged->headerBlock) + " has exits " + exits + ": " + refusal); } graph = std::move(*routed); } @@ -1645,13 +1793,13 @@ void Structurizer::canonicalizeNaturalLoops(ControlFlowGraph& graph) const { throw std::runtime_error("CFG loop canonicalization exceeded rewrite budget"); } -void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph) const { +void Structurizer::splitSharedMergeBlocks(ControlFlowGraph& graph, bool routeLeavingExits, bool routeSharedExits) const { const auto originalBlockCount = static_cast(graph.blocks.size()); const auto splitBudget = std::max(16u, originalBlockCount * 4u); const auto programWords = estimatedSpirvWords(graph, allBlockIds(originalBlockCount)); SplitBudget budget{programWords, std::max(CloneWordLimit, programWords / CloneBudgetDivisor), std::max(CloneWordLimit, programWords), nextGotoVariable(graph)}; for (std::uint32_t splits = 0; splits < splitBudget; ++splits) { - if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }) && !splitOneReturnJoin(graph)) { + if (!splitOneLoopMerge(graph) && !splitOneSelectionMerge(graph, budget, [this](ControlFlowGraph& candidate) { recomputeAnalyses(candidate); }, routeLeavingExits, routeSharedExits) && !splitOneReturnJoin(graph)) { return; } rebuildPredecessors(graph); @@ -1715,4 +1863,98 @@ void Structurizer::verifyReducibility(const ControlFlowGraph& graph) const { } } +bool Structurizer::hasStructuredConstructs(const ControlFlowGraph& graph) const { + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + if (terminator.kind != TerminatorKind::ConditionalBranch || terminator.loopHeader || terminator.mergeBlock != InvalidControlFlowId || terminator.trueBlock == terminator.falseBlock) continue; + const BasicBlock* loop = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && graph.Dominates(header.id, block.id) && !graph.Dominates(header.terminator.mergeBlock, block.id) && (loop == nullptr || graph.Dominates(loop->id, header.id))) loop = &header; + } + if (loop == nullptr || (terminator.trueBlock != loop->terminator.mergeBlock && terminator.trueBlock != loop->terminator.continueBlock && terminator.falseBlock != loop->terminator.mergeBlock && terminator.falseBlock != loop->terminator.continueBlock)) return false; + } + auto structural = graph; + for (auto& block : structural.blocks) { + if (block.terminator.mergeBlock != InvalidControlFlowId) { + block.successors.push_back(block.terminator.mergeBlock); + } + if (block.terminator.loopHeader) { + block.successors.push_back(block.terminator.continueBlock); + } + } + rebuildPredecessors(structural); + computeDominatorTree(structural); + computePostDominators(structural); + const auto inRegion = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.id, blockId) && !structural.Dominates(header.terminator.mergeBlock, blockId); + }; + const auto inContinue = [&](const BasicBlock& header, std::uint32_t blockId) { + return structural.Dominates(header.terminator.continueBlock, blockId) && structural.PostDominates(header.terminator.continueBlock, blockId); + }; + const auto inConstruct = [&](const BasicBlock& header, std::uint32_t blockId) { + return inRegion(header, blockId) && !(header.terminator.loopHeader && inContinue(header, blockId)); + }; + const auto innermostLoop = [&](std::uint32_t blockId) { + const BasicBlock* innermost = nullptr; + for (const auto& header : graph.blocks) { + if (header.terminator.loopHeader && inRegion(header, blockId) && (innermost == nullptr || structural.Dominates(innermost->id, header.id))) { + innermost = &header; + } + } + return innermost; + }; + const auto exitsLoop = [](const BasicBlock* loop, std::uint32_t target) { + return loop != nullptr && (target == loop->terminator.mergeBlock || target == loop->terminator.continueBlock); + }; + for (const auto& block : graph.blocks) { + const auto& terminator = block.terminator; + for (const auto successor : block.successors) { + const auto& target = graph.FindBlock(successor); + if (structural.Dominates(successor, block.id) && (!target.terminator.loopHeader || target.terminator.continueBlock != block.id)) { + return false; + } + } + if (terminator.kind == TerminatorKind::ConditionalBranch && !terminator.loopHeader && terminator.mergeBlock == InvalidControlFlowId && terminator.trueBlock != terminator.falseBlock) { + const auto* loop = innermostLoop(block.id); + if (!exitsLoop(loop, terminator.trueBlock) && !exitsLoop(loop, terminator.falseBlock)) { + return false; + } + } + if (terminator.mergeBlock == InvalidControlFlowId) { + continue; + } + if (terminator.mergeBlock == block.id || !structural.Dominates(block.id, terminator.mergeBlock)) { + return false; + } + if (terminator.loopHeader && (terminator.continueBlock == terminator.mergeBlock || !structural.Dominates(block.id, terminator.continueBlock))) { + return false; + } + const auto* loop = terminator.loopHeader ? &block : innermostLoop(block.id); + for (const auto& member : graph.blocks) { + if (inConstruct(block, member.id)) { + if (member.id != block.id && member.terminator.mergeBlock != InvalidControlFlowId && !inConstruct(block, member.terminator.mergeBlock)) { + return false; + } + if (member.id != block.id && !std::all_of(member.predecessors.begin(), member.predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor); })) { + return false; + } + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inConstruct(block, successor) || successor == terminator.mergeBlock || exitsLoop(loop, successor); })) { + return false; + } + } else if (terminator.loopHeader && inContinue(block, member.id)) { + if (!std::all_of(member.successors.begin(), member.successors.end(), [&](std::uint32_t successor) { return inContinue(block, successor) || successor == block.id || successor == terminator.mergeBlock; })) { + return false; + } + } + } + if (terminator.loopHeader) { + const auto& predecessors = graph.FindBlock(terminator.continueBlock).predecessors; + if (!std::all_of(predecessors.begin(), predecessors.end(), [&](std::uint32_t predecessor) { return inConstruct(block, predecessor) || inContinue(block, predecessor); })) { + return false; + } + } + } + return true; +} + } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp index 99b5ef6fa..39c7c07c3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrBuilder.hpp @@ -88,6 +88,7 @@ public: } private: + IrValue* vectorWindowIndex(VectorReg reg); IrProgram& program; IrBlock* insertionPoint; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp index 9a6b86e8b..67eedc48c 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ProgramMetadata.hpp @@ -26,6 +26,8 @@ struct IrProgramMetadata { IrBindingLayout bindings; bool bindingLayoutComplete = false; std::uint32_t tessellationLocalStride = 0; + std::uint32_t vectorWindowBase = 0; + std::uint32_t vectorWindowSize = 0; std::uint32_t tessellationControlStride = 0; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index e580b1118..6ed27287a 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -93,6 +93,8 @@ enum class IrOpcode : std::uint16_t { SetScalarRegister, GetVectorRegister, SetVectorRegister, + LoadVectorWindow, + StoreVectorWindow, GetGotoVariable, SetGotoVariable, GetScc, @@ -141,6 +143,8 @@ enum class IrOpcode : std::uint16_t { CompositeExtractU32x4, PackHalf2x16, PackFloat2x16Rtz, + FPAdd16x2, + FPMul16x2, FPAbs32, FPNeg32, FPSaturate32, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp index 2a81dd39b..3f45934d4 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrBuilder/IrBuilderRegisters.cpp @@ -1,6 +1,7 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include "IntermediateRepresentation/IrBuilderInternal.hpp" #include +#include namespace ShaderRecompiler { @@ -106,12 +107,25 @@ void IrBuilder::SetScalarMaskTag(ScalarReg reg, IrValue& value) { (void)Emit(IrOpcode::SetScalarMaskTag, IrType::Void, {&operand, &value}); } +IrValue* IrBuilder::vectorWindowIndex(VectorReg reg) { + const auto& metadata = program.Metadata(); + const auto index = static_cast(RegIndex(reg)); + if (metadata.vectorWindowSize == 0u || index < metadata.vectorWindowBase) return nullptr; + if (index - metadata.vectorWindowBase >= metadata.vectorWindowSize) throw std::runtime_error("vector register " + std::to_string(index) + " lies past the relative-addressing window"); + return &Constant(index - metadata.vectorWindowBase); +} + IrValue& IrBuilder::GetVectorReg(VectorReg reg) { + if (IrValue* index = vectorWindowIndex(reg)) return Emit(IrOpcode::LoadVectorWindow, IrType::U32, {index}); IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); return Emit(IrOpcode::GetVectorRegister, IrType::U32, {&operand}); } void IrBuilder::SetVectorReg(VectorReg reg, IrValue& value) { + if (IrValue* index = vectorWindowIndex(reg)) { + (void)Emit(IrOpcode::StoreVectorWindow, IrType::Void, {index, &value}); + return; + } IrValue& operand = createRegisterOperand(program, RegisterBank::Vector, RegIndex(reg), IrType::VectorReg); (void)Emit(IrOpcode::SetVectorRegister, IrType::Void, {&operand, &value}); } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 7abf9208a..cdb2a2d88 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -134,6 +134,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("SetScalarRegister", Void, ScalarReg, U32), makeMeta("GetVectorRegister", U32, VectorReg), makeMeta("SetVectorRegister", Void, VectorReg, U32), + makeMeta("LoadVectorWindow", U32, U32), + makeMeta("StoreVectorWindow", Void, U32, U32), makeMeta("GetGotoVariable", U1, U32), makeMeta("SetGotoVariable", Void, U32, U1), makeMeta("GetScc", U1), @@ -182,6 +184,8 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("CompositeExtractU32x4", U32, U32x4, U32), makeMeta("PackHalf2x16", U32, F32x2), makeMeta("PackFloat2x16Rtz", U32, F32, F32), + makeMeta("FPAdd16x2", U32, U32, U32), + makeMeta("FPMul16x2", U32, U32, U32), makeMeta("FPAbs32", F32, F32), makeMeta("FPNeg32", F32, F32), makeMeta("FPSaturate32", F32, F32), @@ -890,6 +894,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::SetScalarMaskTag: case IrOpcode::SetScalarRegister: case IrOpcode::SetVectorRegister: + case IrOpcode::StoreVectorWindow: case IrOpcode::SetGotoVariable: case IrOpcode::SetScc: case IrOpcode::SetExec: diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 6bc946acd..32300b030 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -127,6 +127,7 @@ struct ShaderVertexInputInfo { int fetchBufferReg = 0; int buffersNum = 0; std::uint32_t scratchSizeDwords = 0; + std::uint32_t hostSubgroupSize = 64; std::uint32_t paClVsOutCntl = 0; ShaderClipSpaceTransform clipSpace; ShaderMeshInputInfo mesh; diff --git a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp index e500ead2a..51ec69506 100644 --- a/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/MaskedSelectEliminator.cpp @@ -60,6 +60,8 @@ bool isLaneLocal(IrOpcode opcode) { case IrOpcode::CompositeExtractU32x4: case IrOpcode::PackHalf2x16: case IrOpcode::PackFloat2x16Rtz: + case IrOpcode::FPAdd16x2: + case IrOpcode::FPMul16x2: case IrOpcode::BitFieldInsert: case IrOpcode::BitFieldUExtract: case IrOpcode::BitFieldSExtract: @@ -271,6 +273,64 @@ bool alwaysTrue(const IrValue* value, std::uint32_t waveSize, std::uint32_t dept return bit && isImmediate(bit->low, 0xffffffffu) && isImmediate(bit->high, 0xffffffffu); } +using BlockSet = std::unordered_set; + +BlockSet uniformEntryBlocks(const IrProgram& program) { + BlockSet result; + if (program.BlockOrder().empty()) return result; + const IrBlock* block = program.BlockOrder().front(); + while (block->Predecessors().empty() || (block->Predecessors().size() == 1u && result.contains(block->Predecessors().front()))) { + if (!result.insert(block).second || block->Successors().size() != 1u) break; + block = block->Successors().front(); + } + return result; +} + +bool sameCondition(const IrValue* condition, const IrValue* predicate, std::uint32_t depth) { + condition = condition->Resolve(); + predicate = predicate->Resolve(); + if (condition == predicate || (predicate->HasImmediate() && predicate->Type() == IrType::Bool && predicate->ImmediateBool())) return true; + if (depth > DepthLimit) return false; + return condition->Opcode() == IrOpcode::LogicalAnd && (sameCondition(condition->Argument(0), predicate, depth + 1u) || sameCondition(condition->Argument(1), predicate, depth + 1u)); +} + +bool withinBallotWord(const IrValue* condition, const IrValue* word, std::uint32_t half, const BlockSet& uniform, std::uint32_t depth) { + if (word->Opcode() != IrOpcode::CompositeExtractU32x4 || !isImmediate(word->Argument(1)->Resolve(), half)) return false; + const IrValue* ballot = word->Argument(0)->Resolve(); + return ballot->Opcode() == IrOpcode::Ballot && uniform.contains(ballot->Parent()) && sameCondition(condition, ballot->Argument(0), depth + 1u); +} + +bool withinWords(const IrValue* condition, const IrValue* low, const IrValue* high, std::uint32_t waveSize, const BlockSet& uniform, std::uint32_t depth) { + if (depth > DepthLimit) return false; + if (low->Opcode() == IrOpcode::CompositeExtractU32x4) { + return withinBallotWord(condition, low, 0u, uniform, depth) && (waveSize == 32u || withinBallotWord(condition, high, 1u, uniform, depth)); + } + if (low->Opcode() != IrOpcode::BitwiseAnd32) return false; + const IrValue* lowLeft = low->Argument(0)->Resolve(); + const IrValue* lowRight = low->Argument(1)->Resolve(); + if (waveSize == 32u) return withinWords(condition, lowLeft, high, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, high, waveSize, uniform, depth + 1u); + if (high->Opcode() != IrOpcode::BitwiseAnd32) return false; + const IrValue* highLeft = high->Argument(0)->Resolve(); + const IrValue* highRight = high->Argument(1)->Resolve(); + return (withinWords(condition, lowLeft, highLeft, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highRight, waveSize, uniform, depth + 1u)) || + (withinWords(condition, lowLeft, highRight, waveSize, uniform, depth + 1u) && withinWords(condition, lowRight, highLeft, waveSize, uniform, depth + 1u)); +} + +const IrValue* wholeQuadSource(const IrValue* mask, std::uint32_t waveSize) { + const auto bit = threadBit(mask, waveSize); + if (!bit || bit->low->Opcode() != IrOpcode::CompositeExtractU64 || !isImmediate(bit->low->Argument(1)->Resolve(), 0u)) return nullptr; + const IrValue* wqm = bit->low->Argument(0)->Resolve(); + if (waveSize != 32u && (bit->high->Opcode() != IrOpcode::CompositeExtractU64 || bit->high->Argument(0)->Resolve() != wqm || !isImmediate(bit->high->Argument(1)->Resolve(), 1u))) return nullptr; + if (wqm->Opcode() != IrOpcode::WqmU64) return nullptr; + const IrValue* source = wqm->Argument(0)->Resolve(); + return source->Opcode() == IrOpcode::CompositeConstructU64 ? source : nullptr; +} + +bool withinQuadsOf(const IrValue* condition, const IrValue* mask, std::uint32_t waveSize, const BlockSet& uniform) { + const IrValue* source = wholeQuadSource(mask, waveSize); + return source != nullptr && withinWords(condition, source->Argument(0)->Resolve(), source->Argument(1)->Resolve(), waveSize, uniform, 0u); +} + enum class Region : std::uint8_t { Unreached, Once, Loop }; Region regionOf(const std::vector& regions, const IrBlock* block) { @@ -297,7 +357,7 @@ struct ImplicationKeyHash { class Implications { public: - Implications(const std::vector& regions, std::uint32_t waveSize, std::size_t& work) : regions(regions), waveSize(waveSize), work(work) {} + Implications(const std::vector& regions, const BlockSet& uniform, std::uint32_t waveSize, std::size_t& work) : regions(regions), uniform(uniform), waveSize(waveSize), work(work) {} void Reset(const IrValue* writeMask, bool acrossLoops) { mask = writeMask; @@ -318,7 +378,9 @@ public: --work; const Attempt attempt = assume(key); bool result = false; - if (condition->Opcode() == IrOpcode::LogicalAnd) { + if (withinQuadsOf(condition, mask, waveSize, uniform)) { + result = true; + } else if (condition->Opcode() == IrOpcode::LogicalAnd) { result = Implies(condition->Argument(0), depth + 1u) || Implies(condition->Argument(1), depth + 1u); } else if (condition->IsPhi()) { result = merges(*condition); @@ -414,6 +476,7 @@ private: } const std::vector& regions; + const BlockSet& uniform; std::uint32_t waveSize; std::size_t& work; const IrValue* mask = nullptr; @@ -474,12 +537,13 @@ std::size_t walkLimit(const IrProgram& program) { class MaskedLaneWalk { public: - MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), limit(walkLimit(program)), implications(regions, program.WaveSize(), work) {} + MaskedLaneWalk(const IrProgram& program, const std::vector& regions) : program(program), regions(regions), uniform(uniformEntryBlocks(program)), limit(walkLimit(program)), implications(regions, uniform, program.WaveSize(), work) {} bool UnobservedWhereMasked(const IrValue& select, const IrValue* mask) { const bool acrossLoops = mask->HasImmediate() || regionOf(regions, mask->Parent()) == Region::Once; work = limit; implications.Reset(mask, acrossLoops); + const bool quadMasked = program.Resources().stage == IrShaderStage::Pixel && wholeQuadSource(mask, program.WaveSize()) != nullptr; startVisits(); pending.clear(); pending.push_back(&select); @@ -493,7 +557,7 @@ public: if (isSelect(user->Opcode()) && use.operand == 1u && implications.Implies(user->Argument(0))) continue; if (user->Opcode() == IrOpcode::LogicalAnd && use.operand < 2u && implications.Implies(user->Argument(1u - use.operand))) continue; if (readsOnlyWhereActive(user->Opcode()) && use.operand + 1u < user->ArgumentCount() && implications.Implies(user->Argument(user->ArgumentCount() - 1u))) continue; - if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user)) return false; + if (!user->IsPhi() && !isLaneLocal(user->Opcode()) && !isExplicitLodSample(program, *user) && !(quadMasked && user->Opcode() == IrOpcode::ImageSampleRaw)) return false; if (firstVisit(*user)) { if (work == 0) return false; --work; @@ -521,6 +585,7 @@ private: const IrProgram& program; const std::vector& regions; + BlockSet uniform; std::size_t limit; std::size_t work = 0; Implications implications; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 838e52932..53118b192 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -191,7 +191,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) throw std::runtime_error("selects a channel the converted image format does not have"); } } - decoded.srgbDecode = !storage && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; + decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (decoded.srgbDecode && !base.srgbDecodeCompatible) { throw std::runtime_error("samples or gathers an sRGB image the device cannot sample, which is not implemented"); } @@ -551,7 +551,8 @@ std::uint32_t emulatedCompareState(const ShaderInfo& info, const ResourceSnapsho if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; - if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u) { + const bool eightBitSrgb = format == IrBufferFormat::Format8Srgb || format == IrBufferFormat::Format8_8Srgb || format == IrBufferFormat::Format8_8_8_8Srgb; + if (type == ImageType::Cube && magFilter == 1u && ((words[0] >> 28u) & 1u) == 0u && !eightBitSrgb) { throw std::runtime_error("bilinear cube comparison requires DISABLE_CUBE_WRAP to avoid seamless face filtering"); } const auto addressMode = [](std::uint32_t clamp) { @@ -735,7 +736,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe modes.push_back(mode); } } - if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !storage && !image.depthCompare && !image.packed) { + if (image.srgbDecodeFormats != 0u && image.srgbDecodeCompatible && !image.depthCompare && !storage && !image.packed) { const auto count = modes.size(); for (std::size_t index = 0; index < count; ++index) { const auto& base = modes[index]; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 0f3a3a6af..6ed2a7fdc 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -86,6 +86,7 @@ bool IsRuntimeUniformOp(IrOpcode opcode) { case IrOpcode::SelectU1: case IrOpcode::SelectU32: case IrOpcode::SelectF32: + case IrOpcode::SLessThan32: case IrOpcode::ULessThan32: case IrOpcode::IEqual32: case IrOpcode::UGreaterThan32: diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index e3e0af422..4da4c6009 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -103,9 +103,7 @@ void PlanBuilder::PatchReads() { IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); block->RemoveInstruction(&flat); block->InsertInstructionBefore(patch.inst, &flat); - for (const auto& use : std::vector(patch.inst->OperandUses())) { - use.user->ReplaceArgument(use.operand, &flat); - } + patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { if (info.condition != nullptr && info.condition->Resolve() == patch.inst) { info.condition = ♭ diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index 0e6078d53..8cbadf9cc 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -529,6 +529,9 @@ constexpr VectorOpcodeInfo vop3Opcodes[] = { {0x311u, RdnaOpcode::VPackB32F16}, {0x314u, RdnaOpcode::VLshlrevB16}, {0x319u, RdnaOpcode::VSubrevI32}, + {0x200u, RdnaOpcode::VInterpP1F32}, + {0x201u, RdnaOpcode::VInterpP2F32}, + {0x202u, RdnaOpcode::VInterpMovF32}, {0x342u, RdnaOpcode::VInterpP1llF16}, {0x343u, RdnaOpcode::VInterpP1lvF16}, {0x344u, RdnaOpcode::VPermB32}, @@ -723,7 +726,6 @@ bool hasDppForm(RdnaOpcode opcode) { bool acceptsDpp8(RdnaOpcode opcode) { switch (opcode) { - case RdnaOpcode::VPkFmacF16: case RdnaOpcode::VMovrelsB32: case RdnaOpcode::VMovrelsdB32: case RdnaOpcode::VMovrelsd2B32: @@ -738,6 +740,10 @@ bool isNativeVop3F16TernaryOpcode(RdnaOpcode opcode) { opcode == RdnaOpcode::VFmaF16; } +bool isNativeVop3F16PackOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VPackB32F16 || opcode == RdnaOpcode::VCvtPknormI16F16 || opcode == RdnaOpcode::VCvtPknormU16F16; +} + bool isNativeVop3I16TernaryOpcode(RdnaOpcode opcode) { return opcode == RdnaOpcode::VMed3I16 || opcode == RdnaOpcode::VMed3U16 || opcode == RdnaOpcode::VMax3I16 || opcode == RdnaOpcode::VMax3U16 || opcode == RdnaOpcode::VMin3I16 || opcode == RdnaOpcode::VMin3U16 || @@ -1862,7 +1868,7 @@ void applyNativeVop3B16BinaryModifiers(RdnaInstruction& instruction, std::uint32 instruction.destination.sdwaSel = (opSel & 0x8u) != 0u ? 5u : 4u; } -void applyNativeVop3PackB32F16Modifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { +void applyNativeVop3F16PackModifiers(RdnaInstruction& instruction, std::uint32_t opSel, std::uint32_t abs, std::uint32_t neg) { instruction.source0.opSel = (opSel & 0x1u) != 0u; instruction.source1.opSel = (opSel & 0x2u) != 0u; instruction.source0.negate = (neg & 0x1u) != 0u; @@ -2016,7 +2022,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, if (carryInOut || scalarDst) { const bool floatCompare = scalarDst && isVopcFloatCompareOpcode(opcode); const bool divScale = opcode == RdnaOpcode::VDivScaleF32 || opcode == RdnaOpcode::VDivScaleF64; - if ((clamp != 0u && !isVop3BMadU64Opcode(opcode)) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { + if ((clamp != 0u && !isVop3BMadU64Opcode(opcode) && !floatCompare) || (omod != 0u && !divScale) || (neg != 0u && !floatCompare && !divScale)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; @@ -2051,7 +2057,7 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, } return; } - if (opcode == RdnaOpcode::VPackB32F16) { + if (isNativeVop3F16PackOpcode(opcode)) { if ((abs & ~0x3u) != 0u || (opSel & ~0x3u) != 0u || clamp != 0u || omod != 0u || (neg & ~0x3u) != 0u) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } @@ -2060,8 +2066,9 @@ void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, const bool sourceModifiers = supportsNativeVop3SourceModifiers(opcode); const bool resultModifiers = supportsNativeVop3ResultModifiers(opcode); const bool clampModifier = supportsNativeVop3Clamp(opcode); + const bool ignoresOutputModifiers = opcode == RdnaOpcode::VCvtI16F16; if (sourceModifiers) { - if (opSel != 0u || (omod != 0u && !resultModifiers) || (clamp != 0u && !clampModifier)) { + if (opSel != 0u || (omod != 0u && !resultModifiers && !ignoresOutputModifiers) || (clamp != 0u && !clampModifier && !ignoresOutputModifiers)) { throw std::invalid_argument("VOP3 source modifiers are not implemented"); } return; @@ -2114,6 +2121,47 @@ void decodeVop3F16Interpolation(RdnaInstruction& instruction, std::uint32_t vdst } } +bool isVop3F32InterpolationOpcode(RdnaOpcode opcode) { + return opcode == RdnaOpcode::VInterpP1F32 || opcode == RdnaOpcode::VInterpP2F32 || opcode == RdnaOpcode::VInterpMovF32; +} + +void decodeVop3F32Interpolation(RdnaInstruction& instruction, std::uint32_t vdst, std::uint32_t src0, std::uint32_t src1, std::uint32_t src2, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { + const bool moves = instruction.op == RdnaOpcode::VInterpMovF32; + const RdnaOperand thirdSource = DecodeRdnaScalarSource(src2, instruction.programCounter); + if (thirdSource.kind != RdnaOperandKind::IntegerInlineConstant || thirdSource.value != 0u) { + throw std::invalid_argument("VOP3 interpolation third source is not the inline constant 0"); + } + if ((abs & 0x5u) != 0u || (neg & 0x5u) != 0u || opSel != 0u || (src0 >> 8u) != 0u || (moves && (abs | neg) != 0u)) { + throw std::invalid_argument("VOP3 interpolation modifiers are not implemented"); + } + if (moves) { + if (src1 >= 3u) { + throw std::invalid_argument("VOP3 interpolation source is not an interpolation parameter"); + } + } else if (src1 < 256u) { + throw std::invalid_argument("VOP3 interpolation source is not a vector register"); + } + instruction.destination = DecodeRdnaVectorGpr(vdst); + instruction.destination.clamp = clamp != 0u; + instruction.destination.omod = omod; + if (moves) { + instruction.source0.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source0.value = src1; + instruction.source0.signedVal = static_cast(src1); + } else { + instruction.source0 = DecodeRdnaVectorGpr(src1 - 256u); + instruction.source0.absolute = (abs & 0x2u) != 0u; + instruction.source0.negate = (neg & 0x2u) != 0u; + } + instruction.source1.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source1.value = src0 & 0x3fu; + instruction.source1.signedVal = static_cast(instruction.source1.value); + instruction.source2.kind = RdnaOperandKind::IntegerInlineConstant; + instruction.source2.value = (src0 >> 6u) & 0x3u; + instruction.source2.signedVal = static_cast(instruction.source2.value); + instruction.sourceCount = 3; +} + void applyNativeVop3SourceModifiers(RdnaInstruction& instruction, std::uint32_t abs, std::uint32_t neg) { RdnaOperand* sources[] = {&instruction.source0, &instruction.source1, &instruction.source2}; for (std::uint32_t i = 0; i < instruction.sourceCount && i < 3u; ++i) { @@ -2278,6 +2326,10 @@ RdnaInstruction DecodeRdnaVop3(std::uint32_t programCounter, std::spanscratchDwords : 0u; translateOptions.sharedMemoryBytes = DeviceMemoryLdsBytes(request); translateOptions.fragmentShaderBarycentricEnabled = request.target.fragmentShaderBarycentricEnabled; + const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast(capability)) != request.target.supportedCapabilities.end(); }; + translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.floatMode = request.context.floatMode; translateOptions.inputInfo = inputInfo; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index deca904d9..85f9689e8 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -176,6 +176,7 @@ struct ShaderFloatMode { inline constexpr std::uint32_t InterpolationQuiet = 1u; inline constexpr std::uint32_t InterpolationFlush32 = 2u; inline constexpr std::uint32_t InterpolationFlush16 = 4u; +inline constexpr std::uint32_t InterpolationRound16Shift = 3u; struct GuestContext { std::uint32_t waveSize; diff --git a/core/shader/recompiler/ShaderCacheDirectory.hpp b/core/shader/recompiler/ShaderCacheDirectory.hpp index 229506482..db8e2f8fe 100644 --- a/core/shader/recompiler/ShaderCacheDirectory.hpp +++ b/core/shader/recompiler/ShaderCacheDirectory.hpp @@ -4,16 +4,29 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include #include #include #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif namespace ShaderRecompiler { @@ -26,6 +39,19 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span{}(std::this_thread::get_id()) ^ static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); auto temporary = path; temporary += ".tmp." + std::to_string(unique) + "." + std::to_string(serial.fetch_add(1, std::memory_order_relaxed)); +#ifdef _WIN32 + struct RenameInfo { DWORD flags; HANDLE root; DWORD nameBytes; WCHAR name[1]; }; + static_assert(sizeof(RenameInfo) == sizeof(FILE_RENAME_INFO) && offsetof(RenameInfo, name) == offsetof(FILE_RENAME_INFO, FileName)); + const auto destination = std::filesystem::absolute(path, error); + if (error || destination.native().size() > (MAXDWORD - sizeof(RenameInfo)) / sizeof(wchar_t)) return false; + const auto nameBytes = destination.native().size() * sizeof(wchar_t); + std::vector renameBuffer(sizeof(RenameInfo) + nameBytes); + auto* rename = reinterpret_cast(renameBuffer.data()); + constexpr DWORD replaceExisting = 0x00000001, posixSemantics = 0x00000002; + rename->flags = replaceExisting | posixSemantics; + rename->nameBytes = static_cast(nameBytes); + std::memcpy(rename->name, destination.c_str(), nameBytes); +#endif bool written = false; { std::ofstream file(temporary, std::ios::binary | std::ios::trunc); @@ -38,6 +64,25 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span(22); + if (!SetFileInformationByHandle(handle, renameInfoEx, rename, static_cast(renameBuffer.size()))) renameError = GetLastError(); + } + if (renameError == ERROR_SUCCESS) return true; + if (renameError != ERROR_INVALID_PARAMETER && renameError != ERROR_INVALID_FUNCTION && renameError != ERROR_NOT_SUPPORTED) { + std::filesystem::remove(temporary, error); + return false; + } +#endif std::filesystem::rename(temporary, path, error); if (error) { std::filesystem::remove(temporary, error); @@ -48,17 +93,27 @@ inline bool WriteFileAtomically(const std::filesystem::path& path, std::span& bytes) { bytes.clear(); - std::ifstream file(path, std::ios::binary | std::ios::ate); - if (!file) return false; - const auto size = static_cast(file.tellg()); - if (size < 0) return false; - file.seekg(0, std::ios::beg); - bytes.resize(static_cast(size)); - if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { + try { + std::ifstream file(path, std::ios::binary | std::ios::ate); + if (!file) return false; + const auto size = static_cast(file.tellg()); + if (size < 0 || static_cast(size) > bytes.max_size() || + static_cast(size) > static_cast(std::numeric_limits::max())) + return false; + if (!file.seekg(0, std::ios::beg)) return false; + bytes.resize(static_cast(size)); + if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { + bytes.clear(); + return false; + } + return true; + } catch (const std::bad_alloc&) { + bytes.clear(); + return false; + } catch (const std::length_error&) { bytes.clear(); return false; } - return true; } inline std::uint64_t HashBytes(std::span bytes, std::uint64_t seed = 0) { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bd05440bc..203770688 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -671,6 +671,7 @@ constexpr std::string_view NeutralSwitches[] = { "APS5_NO_FAILURE_MEMO", "APS5_NO_PERF_FRONTEND_PAIR", "APS5_NO_PERF_DOM_INTERSECTION", + "APS5_NO_PERF_ITERATION_SELECTION", "APS5_NO_RESULT_MEMO", }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 97071e7ca..3c5550ac8 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -13,6 +13,8 @@ std::uint32_t EmitConvertU32F32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF32S32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitCompositeExtractU64(SpirvEmitterState& state, std::uint32_t arg0, const IrValue* arg1); std::uint32_t EmitPackFloat2x16Rtz(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitFPSaturate32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitIAdd64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); std::uint32_t EmitISub64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); @@ -388,6 +390,8 @@ void EmitGetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitSetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetScc(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 23288d4c7..a876a4062 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -147,6 +147,7 @@ struct SpirvEmitterState { std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; + std::array vectorWindowVariable {}; std::array imageVariables {}; std::uint32_t samplerVariable = 0; std::uint32_t mainFunc = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index b9a4a629f..241522188 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -223,6 +223,23 @@ std::uint32_t RoundToPrecision(SpirvEmitterState& state, std::uint32_t value, st return ToF64(state, Join(state, {rounded.low, Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseAnd, u32, rounded.high, ConstantU32(state, 0x7fffffffu)), sign)})); } +std::uint32_t RoundToPrecisionDirected(SpirvEmitterState& state, std::uint32_t value, std::uint32_t fractionBits, std::uint32_t smallestStep, std::uint32_t rounding) { + const auto bits = Split(state, FromF64(state, value)); + const auto exponent = Unary(state, spv::OpBitcast, TypeI32(state), ExponentField(state, bits.high)); + const auto step = Glsl(state, TypeI32(state), GLSLstd450SMax, Binary(state, spv::OpISub, TypeI32(state), exponent, ConstantI32(state, static_cast(fractionBits))), ConstantI32(state, static_cast(smallestStep))); + const auto unbiased = Binary(state, spv::OpISub, TypeI32(state), step, ConstantI32(state, 1023)); + const auto scaled = Exact(state, spv::OpFMul, value, PowerOfTwo(state, Unary(state, spv::OpSNegate, TypeI32(state), unbiased))); + std::uint32_t integral = 0; + if (rounding == 3u) { + integral = Trunc(state, FromF64(state, scaled)); + } else if (rounding == 2u) { + integral = Floor(state, FromF64(state, scaled)); + } else { + integral = Unary(state, spv::OpFNegate, TypeF64(state), Floor(state, FromF64(state, Unary(state, spv::OpFNegate, TypeF64(state), scaled)))); + } + return Exact(state, spv::OpFMul, integral, PowerOfTwo(state, unbiased)); +} + std::uint32_t F32BitsOfRounded(SpirvEmitterState& state, std::uint32_t value) { const auto u32 = TypeU32(state); const auto bits = Split(state, FromF64(state, value)); @@ -265,12 +282,32 @@ std::uint32_t Interpolate(SpirvEmitterState& state, std::uint32_t delta, std::ui base = halfResult ? FlushDenormal32(state, base, flush32, 0x00800000u) : FlushDenormal32(state, base, flush16, 0x38800000u); const auto product = Exact(state, spv::OpFMul, FromF32Exact(state, delta), FromF32Exact(state, coordinate)); const auto sum = AddRoundToOdd(state, product, FromF32Exact(state, base)); - auto rounded = halfResult ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecision(state, sum, 23u, 874u); + const std::uint32_t rounding16 = halfResult ? (mode >> InterpolationRound16Shift) & 3u : 0u; + auto rounded = halfResult ? (rounding16 == 0u ? RoundToPrecision(state, sum, 10u, 999u) : RoundToPrecisionDirected(state, sum, 10u, 999u, rounding16)) : RoundToPrecision(state, sum, 23u, 874u); if (halfResult) { const auto bits = Split(state, FromF64(state, rounded)); const auto sign = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x80000000u)); - const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)), ConstantU32(state, 0x40f00000u)); - rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + const auto magnitude = Binary(state, spv::OpBitwiseAnd, u32, bits.high, ConstantU32(state, 0x7fffffffu)); + if (rounding16 == 0u) { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40f00000u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, ConstantU32(state, 0x40f00000u))}, bits))); + } else { + const auto overflow = Binary(state, spv::OpUGreaterThan, TypeBool(state), magnitude, ConstantU32(state, 0x40effc00u)); + const auto negative = Binary(state, spv::OpINotEqual, TypeBool(state), sign, ConstantU32(state, 0u)); + auto limit = ConstantU32(state, 0x40effc00u); + if (rounding16 == 1u) limit = Select(state, u32, negative, ConstantU32(state, 0x40effc00u), ConstantU32(state, 0x40f00000u)); + if (rounding16 == 2u) limit = Select(state, u32, negative, ConstantU32(state, 0x40f00000u), ConstantU32(state, 0x40effc00u)); + rounded = ToF64(state, Join(state, SelectBits(state, overflow, {ConstantU32(state, 0u), Binary(state, spv::OpBitwiseOr, u32, sign, limit)}, bits))); + if (rounding16 == 2u) { + const auto boolean = TypeBool(state); + const auto zeroBits = [&](F64Bits value, std::uint32_t highMask) { + return Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, Binary(state, spv::OpBitwiseAnd, u32, value.high, ConstantU32(state, highMask)), ConstantU32(state, 0u)), Binary(state, spv::OpIEqual, boolean, value.low, ConstantU32(state, 0u))); + }; + const auto positiveZeros = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, product)), 0xffffffffu), zeroBits(Split(state, FromF64(state, FromF32Exact(state, base))), 0xffffffffu)); + const auto negativeZero = Binary(state, spv::OpLogicalAnd, boolean, zeroBits(Split(state, FromF64(state, sum)), 0x7fffffffu), Unary(state, spv::OpLogicalNot, boolean, positiveZeros)); + rounded = ToF64(state, Join(state, SelectBits(state, negativeZero, {ConstantU32(state, 0u), ConstantU32(state, 0x80000000u)}, Split(state, FromF64(state, rounded))))); + } + } } const auto finite = F32BitsOfRounded(state, rounded); const auto inputs = std::array{Unary(state, spv::OpBitcast, u32, delta), Unary(state, spv::OpBitcast, u32, coordinate), Unary(state, spv::OpBitcast, u32, base)}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index dc3fa2a70..ec55881e3 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -276,9 +276,13 @@ bool AllLanesBit(const IrValue* bit) { return words && AllOnesWord((*words)[0]) && AllOnesWord((*words)[1]); } +bool EveryInvocationEnters(IrShaderStage stage) { + return stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation; +} + class EntryLaneWalk { public: - explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(stage == IrShaderStage::Vertex || stage == IrShaderStage::Local || stage == IrShaderStage::TessellationControl || stage == IrShaderStage::TessellationEvaluation) {} + explicit EntryLaneWalk(IrShaderStage stage) : everyInvocationEnters(EveryInvocationEnters(stage)) {} bool ZeroBit(const IrValue* bit) { bit = Resolved(bit); @@ -351,9 +355,107 @@ private: std::uint32_t budget = 512u; }; -bool RowShiftFlags(const IrValue* value, std::uint32_t control) { +bool EveryLaneBit(const IrValue* bit, bool constantTrue) { + const auto* resolved = Resolved(bit); + if (constantTrue && resolved != nullptr && resolved->HasImmediate()) return resolved->Type() == IrType::Bool && resolved->ImmediateBool(); + return AllLanesBit(resolved); +} + +bool WritesEveryLane(const IrValue& update) { + const auto flags = update.Flags(); + return flags.boundControl && flags.rowMask == 0xfu && flags.bankMask == 0xfu && EveryLaneBit(update.Argument(2), true); +} + +bool KeepsNoOldValue(const IrValue& permlane) { + const auto flags = permlane.Flags(); + return flags.fetchInactive || flags.boundControl || EveryLaneBit(permlane.Argument(3), true); +} + +bool UpperLaneTest(const IrValue* bit) { + const auto* compare = Resolved(bit); + return Is(compare, IrOpcode::UGreaterThanEqual32) && Is(Resolved(compare->Argument(0)), IrOpcode::LaneId) && IsU32(compare->Argument(1), 32u); +} + +class UpperLaneWalk { +public: + explicit UpperLaneWalk(IrShaderStage stage) : enters(EveryInvocationEnters(stage)), laneIndexed(enters && stage != IrShaderStage::TessellationControl) {} + + bool ZeroValue(const IrValue* value) { + value = Resolved(value); + if (!enters || value == nullptr || !spend()) return false; + if (value->HasImmediate()) return IsU32(value, 0u); + if (zeroValues.contains(value)) return true; + bool zero = false; + switch (value->Opcode()) { + case IrOpcode::SelectU32: zero = ZeroValue(value->Argument(2)) && (zeroBit(value->Argument(0)) || ZeroValue(value->Argument(1))); break; + case IrOpcode::BitwiseOr32: zero = ZeroValue(value->Argument(0)) && ZeroValue(value->Argument(1)); break; + case IrOpcode::DppMoveU32: zero = ZeroValue(value->Argument(0)); break; + case IrOpcode::DppUpdateU32: zero = ZeroValue(value->Argument(0)) && (WritesEveryLane(*value) || ZeroValue(value->Argument(1))); break; + case IrOpcode::Permlane16U32: zero = ZeroValue(value->Argument(0)) && (KeepsNoOldValue(*value) || ZeroValue(value->Argument(4))); break; + case IrOpcode::Phi: zero = incoming(value, [this](const IrValue* source) { return ZeroValue(source); }); break; + default: break; + } + if (zero && phis.empty()) zeroValues.insert(value); + return zero; + } + +private: + bool spend() { + if (budget == 0u) return false; + --budget; + return true; + } + + bool zeroBit(const IrValue* bit) { + bit = Resolved(bit); + if (bit == nullptr || !spend()) return false; + if (bit->HasImmediate()) return bit->Type() == IrType::Bool && !bit->ImmediateBool(); + switch (bit->Opcode()) { + case IrOpcode::LogicalAnd: return zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1)); + case IrOpcode::SelectU1: return zeroBit(bit->Argument(2)) && (zeroBit(bit->Argument(0)) || zeroBit(bit->Argument(1))); + case IrOpcode::Phi: return incoming(bit, [this](const IrValue* source) { return zeroBit(source); }); + case IrOpcode::INotEqual32: return EitherOrder(bit, [this](const IrValue* value, const IrValue* zero) { return IsU32(zero, 0u) && ZeroValue(value); }); + case IrOpcode::ULessThan32: return belowUpperLanes(bit->Argument(0), bit->Argument(1)); + case IrOpcode::UGreaterThan32: return belowUpperLanes(bit->Argument(1), bit->Argument(0)); + default: return false; + } + } + + bool belowUpperLanes(const IrValue* index, const IrValue* bound) { + const auto* limit = Resolved(bound); + return limit != nullptr && limit->HasImmediate() && limit->Type() == IrType::U32 && limit->ImmediateU32() <= 32u && upperLaneIndex(index); + } + + bool upperLaneIndex(const IrValue* index) { + index = Resolved(index); + if (!laneIndexed || index == nullptr || !spend()) return false; + if (Is(index, IrOpcode::LaneId)) return true; + if (Is(index, IrOpcode::BitCount32)) { + const auto* mask = Resolved(index->Argument(0)); + return Is(mask, IrOpcode::SelectU32) && IsU32(mask->Argument(1), 0xffffffffu) && UpperLaneTest(mask->Argument(0)); + } + return Is(index, IrOpcode::IAdd32) && EitherOrder(index, [this](const IrValue* counted, const IrValue* added) { return Is(added, IrOpcode::BitCount32) && upperLaneIndex(counted); }); + } + + template + bool incoming(const IrValue* phi, TCheck&& check) { + if (!phis.insert(phi).second) return true; + bool result = phi->ArgumentCount() != 0u; + for (std::size_t index = 0; result && index < phi->ArgumentCount(); ++index) result = check(phi->Argument(index)); + phis.erase(phi); + return result; + } + + bool enters; + bool laneIndexed; + std::unordered_set zeroValues; + std::unordered_set phis; + std::uint32_t budget = 1024u; +}; + +bool RowShiftFlags(const IrValue* value, std::uint32_t control, bool zeroFill) { const auto flags = value->Flags(); - return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && !flags.boundControl && !flags.fetchInactive; + return flags.control == control && flags.rowMask == 0xfu && flags.bankMask == 0xfu && (!flags.boundControl || zeroFill) && !flags.fetchInactive; } const IrValue* Unflushed(const IrValue* value) { @@ -383,47 +485,55 @@ std::optional> ReductionOperands(const IrValue* co return operands; } -const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control) { - if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control) || !AllLanesBit(step->Argument(2))) return nullptr; - const auto* previous = Resolved(step->Argument(1)); +const IrValue* RowScanStepInput(const IrValue* step, const WaveReduction& reduction, std::uint32_t control, bool narrow) { + const bool zeroFill = narrow && reduction.identity == 0u && !reduction.floating; + if (!Is(step, IrOpcode::DppUpdateU32) || !RowShiftFlags(step, control, zeroFill) || !EveryLaneBit(step->Argument(2), narrow)) return nullptr; + const auto* previous = zeroFill && WritesEveryLane(*step) ? nullptr : Resolved(step->Argument(1)); + const IrValue* input = nullptr; const auto operands = ReductionOperands(Resolved(step->Argument(0)), reduction); const bool matched = operands && EitherOrder(*operands, [&](const IrValue* moved, const IrValue* own) { - return own == previous && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control) && Resolved(moved->Argument(0)) == previous && AllLanesBit(moved->Argument(1)); + input = own; + return (previous == nullptr || own == previous) && Is(moved, IrOpcode::DppMoveU32) && RowShiftFlags(moved, control, zeroFill) && Resolved(moved->Argument(0)) == own && EveryLaneBit(moved->Argument(1), narrow); }); - return matched ? previous : nullptr; + return matched ? input : nullptr; } -const IrValue* Unmasked(const IrValue* value) { - while (Is(value, IrOpcode::SelectU32) && AllLanesBit(value->Argument(0))) value = Resolved(value->Argument(1)); +const IrValue* Unmasked(const IrValue* value, bool narrow) { + while (Is(value, IrOpcode::SelectU32) && EveryLaneBit(value->Argument(0), narrow)) value = Resolved(value->Argument(1)); return value; } -bool CrossesRowsOf(const IrValue* value, const IrValue* scan) { - const auto* permlane = Unmasked(value); +bool CrossesRowsOf(const IrValue* value, const IrValue* scan, bool narrow) { + const auto* permlane = Unmasked(value, narrow); if (!Is(permlane, IrOpcode::Permlane16U32)) return false; const auto flags = permlane->Flags(); - return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && AllLanesBit(permlane->Argument(3)); + return flags.x16 && !flags.fetchInactive && Resolved(permlane->Argument(0)) == scan && IsU32(permlane->Argument(1), 0xffffffffu) && IsU32(permlane->Argument(2), 0xffffffffu) && EveryLaneBit(permlane->Argument(3), narrow); } -std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStage stage) { - const auto* combined = Unmasked(Resolved(value)); +std::optional MatchHalfWaveScan(const IrValue* value, IrShaderStage stage, bool narrow) { + const auto* combined = Unmasked(Resolved(value), narrow); std::optional> operands; const auto reduction = std::find_if(WaveReductions.begin(), WaveReductions.end(), [&](const WaveReduction& candidate) { return (operands = ReductionOperands(combined, candidate)).has_value(); }); const IrValue* scan = nullptr; - if (reduction == WaveReductions.end() || !EitherOrder(*operands, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own); })) return std::nullopt; + if (reduction == WaveReductions.end() || !EitherOrder(*operands, [&](const IrValue* own, const IrValue* crossed) { scan = own; return CrossesRowsOf(crossed, own, narrow); })) return std::nullopt; for (const auto control : {0x118u, 0x114u, 0x112u, 0x111u}) { - scan = RowScanStepInput(scan, *reduction, control); + scan = RowScanStepInput(scan, *reduction, control, narrow); if (scan == nullptr) return std::nullopt; } - const auto* source = Unmasked(scan); - if (!Is(source, IrOpcode::SelectU32) || !IsU32(source->Argument(2), reduction->identity) || !EntryLaneWalk{stage}.ZeroBit(source->Argument(0))) return std::nullopt; + const auto* source = Unmasked(scan, narrow); + const bool entryKeys = Is(source, IrOpcode::SelectU32) && IsU32(source->Argument(2), reduction->identity) && EntryLaneWalk{stage}.ZeroBit(source->Argument(0)); + const bool upperKeys = narrow && reduction->identity == 0u && !reduction->floating && UpperLaneWalk{stage}.ZeroValue(source); + if (!entryKeys && !upperKeys) return std::nullopt; return HalfWaveScan{&*reduction, source}; } std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t lane) { auto& state = ctx.state; if (state.program.WaveSize() != 64u || (lane & 31u) != 31u) return std::nullopt; - const auto scan = MatchHalfWaveScan(inst.Argument(0), state.program.Resources().stage); + const auto stage = state.program.Resources().stage; + auto scan = MatchHalfWaveScan(inst.Argument(0), stage, false); + const bool narrow = !scan && HostSubgroupNarrowerThanWave(state) && state.hostSubgroupSize == 32u; + if (narrow) scan = MatchHalfWaveScan(inst.Argument(0), stage, true); if (!scan) return std::nullopt; if (scan->reduction->floating) { if (lane < 32u || state.hostSubgroupSize > 32u) return std::nullopt; @@ -431,7 +541,10 @@ std::optional EmitHalfWaveReduction(SpirvValueEmitContext& ctx, c } const auto read = "v_readlane_b32 of lane " + std::to_string(lane) + " of a wave64 half-wave reduction scan"; const auto& capabilities = state.supportedCapabilities; - if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityGroupNonUniformArithmetic)) == capabilities.end()) { + if (narrow) return lane >= 32u ? std::optional(ConstantU32(state, scan->reduction->identity)) : std::nullopt; + ctx.Fail(inst, (read + " needs subgroup arithmetic, which the device lacks").c_str()); + } if (state.hostSubgroupSize > 64u) ctx.Fail(inst, (read + " on a " + std::to_string(state.hostSubgroupSize) + "-lane host subgroup, which is wider than the wave").c_str()); const auto identity = ConstantU32(state, scan->reduction->identity); if (lane >= 32u && state.hostSubgroupSize <= 32u) return identity; @@ -746,6 +859,31 @@ std::uint32_t EmitFPLog2(SpirvEmitterState& state, std::uint32_t arg0) { return Select(state, TypeF32(state), Binary(state, spv::OpLogicalAnd, TypeBool(state), near, far), series, value); } +std::uint32_t SinTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0xc17183a8u); + for (const std::uint32_t coefficient : {0x42283c1au, 0xc2996966u, 0x42a335e3u, 0xc2255de7u, 0x40c90fdbu}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, polynomial)); +} + +std::uint32_t CosTurnSeries(SpirvEmitterState& state, std::uint32_t turns) { + const auto square = Exact(state, Binary(state, spv::OpFMul, TypeF32(state), turns, turns)); + auto polynomial = ConstantF32(state, 0x40fce9c5u); + for (const std::uint32_t coefficient : {0xc1d368f9u, 0x4270fa83u, 0xc2aae9e4u, 0x4281e0f8u, 0xc19de9e6u, 0x3f800000u}) { + polynomial = Exact(state, EmitExt(state, TypeF32(state), GLSLstd450Fma, {polynomial, square, ConstantF32(state, coefficient)})); + } + return polynomial; +} + +std::uint32_t SeriesWhenFar(SpirvEmitterState& state, std::uint32_t value, std::uint32_t series) { + const auto valueBits = Unary(state, spv::OpBitcast, TypeU32(state), value); + const auto seriesBits = Unary(state, spv::OpBitcast, TypeU32(state), series); + const auto distance = EmitExt(state, TypeU32(state), GLSLstd450UMin, {Binary(state, spv::OpISub, TypeU32(state), valueBits, seriesBits), Binary(state, spv::OpISub, TypeU32(state), seriesBits, valueBits)}); + return Select(state, TypeF32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), distance, ConstantU32(state, 4u)), series, value); +} + std::uint32_t TrigLinearNearZero(SpirvEmitterState& state, std::uint32_t value, std::uint32_t distance, std::uint32_t linearConstant) { const auto linear = Binary(state, spv::OpFMul, TypeF32(state), distance, linearConstant); const auto magnitude = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), distance), ConstantU32(state, 0x7fffffffu)); @@ -757,9 +895,11 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, true); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); const auto signBits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), cycle), ConstantU32(state, 0x80000000u)); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); const auto nearHalf = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e800000u)); - const auto distance = Select(state, TypeF32(state), nearHalf, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute), absolute); + const auto distance = Select(state, TypeF32(state), nearHalf, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)), absolute); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), distance, ConstantF32(state, 0x3e000000u)); + const auto series = Select(state, TypeF32(state), pastEighth, CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), distance))), SinTurnSeries(state, distance)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Sin, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); const auto linearConstant = Select(state, TypeF32(state), nearHalf, ConstantF32(state, 0x40c90fdbu), ConstantF32(state, 0x40c90fd5u)); const auto result = TrigLinearNearZero(state, value, distance, linearConstant); return Unary(state, spv::OpBitcast, TypeF32(state), Binary(state, spv::OpBitwiseOr, TypeU32(state), Unary(state, spv::OpBitcast, TypeU32(state), result), signBits)); @@ -768,8 +908,14 @@ std::uint32_t EmitFPSin(SpirvEmitterState& state, std::uint32_t arg0) { std::uint32_t EmitFPCos(SpirvEmitterState& state, std::uint32_t arg0) { const auto cycle = EmitTrigCycleF32(state, arg0, false); const auto absolute = EmitExt(state, TypeF32(state), GLSLstd450FAbs, {cycle}); - const auto value = EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}); - const auto distance = Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute); + const auto distance = Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3e800000u), absolute)); + const auto low = CosTurnSeries(state, absolute); + const auto middle = SinTurnSeries(state, distance); + const auto high = Unary(state, spv::OpFNegate, TypeF32(state), CosTurnSeries(state, Exact(state, Binary(state, spv::OpFSub, TypeF32(state), ConstantF32(state, 0x3f000000u), absolute)))); + const auto pastEighth = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3e000000u)); + const auto pastThreeEighths = Binary(state, spv::OpFOrdGreaterThan, TypeBool(state), absolute, ConstantF32(state, 0x3ec00000u)); + const auto series = Select(state, TypeF32(state), pastThreeEighths, high, Select(state, TypeF32(state), pastEighth, middle, low)); + const auto value = SeriesWhenFar(state, EmitExt(state, TypeF32(state), GLSLstd450Cos, {Binary(state, spv::OpFMul, TypeF32(state), absolute, ConstantF32(state, 0x40c90fdbu))}), series); return TrigLinearNearZero(state, value, distance, ConstantF32(state, 0x40c90fdbu)); } @@ -851,7 +997,10 @@ std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { if (selector != nullptr && selector->HasImmediate() && state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u) { const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); if (const auto reduced = EmitHalfWaveReduction(ctx, inst, index)) return *reduced; - if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + if (HostSubgroupNarrowerThanWave(state) && index >= state.hostSubgroupSize) { + if (index >= 32u && UpperLaneWalk{state.program.Resources().stage}.ZeroValue(inst.Argument(0))) return ConstantU32(state, 0u); + FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + } } const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, state.program.WaveSize() - 1u)); return ctx.Shuffle(inst, 0, lane); @@ -1117,13 +1266,16 @@ std::uint32_t EmitFPUnordEqual32(SpirvEmitterState& state, std::uint32_t arg0, s return EmitNative(state, arg0, arg1); } -std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { +static std::uint32_t BothOrderedBits32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { const auto ordered = [&](std::uint32_t value) { const auto magnitude = EmitNative(state, EmitNative(state, value), ConstantU32(state, 0x7fffffffu)); return EmitNative(state, magnitude, ConstantU32(state, 0x7f800000u)); }; - const auto bothOrdered = EmitNative(state, ordered(arg0), ordered(arg1)); - return EmitNative(state, EmitNative(state, arg0, arg1), bothOrdered); + return EmitNative(state, ordered(arg0), ordered(arg1)); +} + +std::uint32_t EmitFPOrdNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1131,7 +1283,7 @@ std::uint32_t EmitFPUnordNotEqual32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordLessThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1139,7 +1291,7 @@ std::uint32_t EmitFPUnordLessThan32(SpirvEmitterState& state, std::uint32_t arg0 } std::uint32_t EmitFPOrdGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { - return EmitNative(state, arg0, arg1); + return EmitNative(state, EmitNative(state, arg0, arg1), BothOrderedBits32(state, arg0, arg1)); } std::uint32_t EmitFPUnordGreaterThan32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { @@ -1229,7 +1381,7 @@ static bool NativeF16Rte(const SpirvEmitterState& state) { return supported(spv::CapabilityFloat16) && supported(spv::CapabilityRoundingModeRTE) && supported(spv::CapabilityDenormPreserve); } -static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { +static void EmitNativeF16Modes(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityFloat16); state.module.EmitCapability(spv::CapabilityRoundingModeRTE); state.module.EmitCapability(spv::CapabilityDenormPreserve); @@ -1239,6 +1391,10 @@ static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::ui state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 16u); state.nativeF16ModesEmitted = true; } +} + +static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { + EmitNativeF16Modes(state); const auto u32 = TypeU32(state); const auto f16 = state.module.Type(spv::OpTypeFloat, 16u); const auto half = Unary(state, spv::OpFConvert, f16, value); @@ -1254,6 +1410,24 @@ static std::uint32_t EmitNativeF32ToF16BitsRte(SpirvEmitterState& state, std::ui return Binary(state, spv::OpBitwiseOr, u32, sign, result); } +static std::uint32_t TypeF16x2(SpirvEmitterState& state) { + if (!NativeF16Rte(state)) FailEmit("packed f16 math needs Float16, RoundingModeRTE and DenormPreserve"); + EmitNativeF16Modes(state); + return state.module.Type(spv::OpTypeVector, state.module.Type(spv::OpTypeFloat, 16u), 2u); +} + +std::uint32_t EmitFPAdd16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto sum = Binary(state, spv::OpFAdd, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, sum)); +} + +std::uint32_t EmitFPMul16x2(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1) { + const auto f16x2 = TypeF16x2(state); + const auto product = Binary(state, spv::OpFMul, f16x2, Unary(state, spv::OpBitcast, f16x2, arg0), Unary(state, spv::OpBitcast, f16x2, arg1)); + return Unary(state, spv::OpBitcast, TypeU32(state), Exact(state, product)); +} + std::uint32_t EmitF32ToF16BitsRte(SpirvEmitterState& state, std::uint32_t value) { if (NativeF16Rte(state)) return EmitNativeF32ToF16BitsRte(state, value); const auto u32 = TypeU32(state); @@ -1320,7 +1494,13 @@ std::uint32_t EmitFPTrunc32(SpirvEmitterState& state, std::uint32_t arg0) { } std::uint32_t EmitFPFract32(SpirvEmitterState& state, std::uint32_t arg0) { - return EmitGlsl(state, arg0); + const auto magnitude = EmitGlsl(state, arg0); + const auto fraction = EmitGlsl(state, magnitude); + const auto complement = EmitFPSub32(state, ConstantF32(state, 0x3f800000u), fraction); + const auto negative = Binary(state, spv::OpFOrdLessThan, TypeBool(state), arg0, ConstantF32(state, 0u)); + const auto partial = Binary(state, spv::OpFOrdNotEqual, TypeBool(state), fraction, ConstantF32(state, 0u)); + const auto below = Binary(state, spv::OpLogicalAnd, TypeBool(state), negative, partial); + return Select(state, TypeF32(state), below, complement, fraction); } std::uint32_t EmitBitCastF16U16(SpirvEmitterState& state, std::uint32_t arg0) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp index d46db4461..f24270d40 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvAluEmitter.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvEmitterInstructions.hpp" #include @@ -44,6 +45,23 @@ void EmitSetVectorRegister(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } +std::uint32_t VectorWindowElement(SpirvValueEmitContext& ctx, const IrValue& inst) { + auto& state = ctx.state; + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), pointer, state.vectorWindowVariable.at(state.laneHalf), ctx.Arg(inst, 0u)); + return pointer; +} + +void EmitLoadVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { + const auto value = ctx.state.module.AllocateId(); + ctx.state.module.AddFunction(spv::OpLoad, TypeU32(ctx.state), value, VectorWindowElement(ctx, inst)); + ctx.Define(inst, value); +} + +void EmitStoreVectorWindow(SpirvValueEmitContext& ctx, const IrValue& inst) { + ctx.state.module.AddFunction(spv::OpStore, VectorWindowElement(ctx, inst), ctx.Arg(inst, 1u)); +} + void EmitGetGotoVariable(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitUnreachable(ctx, inst); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 0014b0bfb..4742b649a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -48,6 +48,7 @@ bool LaneSource(const IrProgram& program, const IrValue& value) { case IrOpcode::WriteLane: case IrOpcode::DataAppend: case IrOpcode::DataConsume: + case IrOpcode::LoadVectorWindow: return true; case IrOpcode::GetBuiltin: { const auto* kind = value.Argument(0)->Resolve(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 59a389eb2..5401643be 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -308,6 +308,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::SetScalarRegister: return Invoke(EmitSetScalarRegister, ctx, inst); case IrOpcode::GetVectorRegister: return Invoke(EmitGetVectorRegister, ctx, inst); case IrOpcode::SetVectorRegister: return Invoke(EmitSetVectorRegister, ctx, inst); + case IrOpcode::LoadVectorWindow: return Invoke(EmitLoadVectorWindow, ctx, inst); + case IrOpcode::StoreVectorWindow: return Invoke(EmitStoreVectorWindow, ctx, inst); case IrOpcode::GetGotoVariable: return Invoke(EmitGetGotoVariable, ctx, inst); case IrOpcode::SetGotoVariable: return Invoke(EmitSetGotoVariable, ctx, inst); case IrOpcode::GetScc: return Invoke(EmitGetScc, ctx, inst); @@ -356,6 +358,8 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::CompositeExtractU32x4: return Invoke(EmitCompositeExtractU32x4, ctx, inst); case IrOpcode::PackHalf2x16: return Invoke(EmitPackHalf2x16, ctx, inst); case IrOpcode::PackFloat2x16Rtz: return Invoke(EmitPackFloat2x16Rtz, ctx, inst); + case IrOpcode::FPAdd16x2: return Invoke(EmitFPAdd16x2, ctx, inst); + case IrOpcode::FPMul16x2: return Invoke(EmitFPMul16x2, ctx, inst); case IrOpcode::FPAbs32: return Invoke(EmitFPAbs32, ctx, inst); case IrOpcode::FPNeg32: return Invoke(EmitFPNeg32, ctx, inst); case IrOpcode::FPSaturate32: return Invoke(EmitFPSaturate32, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index db279fdf5..699290232 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -55,6 +55,11 @@ void DefineModule(SpirvEmitterState& state) { state.scratchVariable.at(half) = state.module.AllocateId(); } } + if (state.program.Metadata().vectorWindowSize != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.vectorWindowVariable.at(half) = state.module.AllocateId(); + } + } state.mainFunc = state.module.AllocateId(); if (StageOf(state) == IrShaderStage::Mesh) { const auto& mesh = VertexInfo(state).mesh; @@ -204,6 +209,11 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddName(state.scratchVariable.at(half), "scratch_dwords"); } } + if (state.program.Metadata().vectorWindowSize != 0u) { + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddName(state.vectorWindowVariable.at(half), "vector_window"); + } + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 5884af305..6124580ce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -999,6 +999,13 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, state.program.Info().scratchDwords), state.scratchVariable.at(half), spv::StorageClassFunction); } } + if (const auto window = state.program.Metadata().vectorWindowSize; window != 0u) { + const auto array = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, window)); + const auto zero = state.module.Constant(spv::OpConstantNull, array); + for (std::uint32_t half = 0; half < state.laneCount; half++) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, array), state.vectorWindowVariable.at(half), spv::StorageClassFunction, zero); + } + } if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 188cae8f5..6d8d41477 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -417,6 +417,12 @@ private: } return true; }; + std::set loopHeaders; + for (const auto& [label, indices] : blocks) { + for (const auto index : indices) { + if (Opcode(instructions[index]) == spv::OpLoopMerge) loopHeaders.insert(label); + } + } std::map> edges; for (const auto& [label, indices] : blocks) { auto& terminal = instructions[indices.back()]; @@ -425,7 +431,7 @@ private: bool loop = false; for (const auto index : indices) loop |= Opcode(instructions[index]) == spv::OpLoopMerge; if (!loop && op == spv::OpBranchConditional) { - if (const auto condition = value(terminal.at(1))) target = terminal.at(*condition != 0u ? 2u : 3u); + if (const auto condition = value(terminal.at(1)); condition.has_value() && !loopHeaders.contains(terminal.at(*condition != 0u ? 3u : 2u))) target = terminal.at(*condition != 0u ? 2u : 3u); } else if (!loop && op == spv::OpSwitch) { if (const auto selector = value(terminal.at(1))) { target = terminal.at(2); diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index 4f5674034..c1aa7723a 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -33,6 +33,7 @@ struct TranslateOptions { std::uint32_t sharedMemoryBytes = 0; std::uint64_t shaderHash = 0; bool fragmentShaderBarycentricEnabled = false; + bool nativeFloat16 = false; std::optional floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 7a4d392a4..7a9d6fc8f 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -17,6 +17,7 @@ public: void TranslateInstruction(const RdnaInstruction& instruction); void SetPixelInput(const ShaderPixelInputInfo* info, bool barycentricEnabled) { pixelInput = info; fragmentShaderBarycentricEnabled = barycentricEnabled; } void SetFloatMode(const std::optional& mode) { floatMode = mode; ieeeMode = mode.has_value() && mode->ieeeMode; } + void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); @@ -145,6 +146,7 @@ private: bool dsBpermuteB32(const RdnaInstruction& inst); bool dsPermuteB32(const RdnaInstruction& inst); IrF32 selectF32(IrU1 condition, IrF32 trueValue, IrF32 falseValue); + IrU32 minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs); IrU32 convertF32ToU32Saturated(IrF32 value, float upperBound, float safeUpper, std::uint32_t highResult); IrU32 convertF32ToI32Saturated(IrF32 value, float lowerBound, float upperBound, float safeUpper, std::uint32_t lowerResult, std::uint32_t upperResult); IrU32 convertFlooredF32ToI32(IrF32 source, IrF32 floored); @@ -319,7 +321,7 @@ private: void vMovrelsdB32(const RdnaInstruction& inst, bool split, bool swap); void vSwapB32(const RdnaInstruction& inst); IrU32 readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name); - void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name); + void writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable = nullptr); void vReadfirstlaneB32(const RdnaInstruction& inst); void vReadlaneB32(const RdnaInstruction& inst); void vWritelaneB32(const RdnaInstruction& inst); @@ -343,6 +345,7 @@ private: bool fragmentShaderBarycentricEnabled = false; std::optional floatMode; bool ieeeMode = false; + bool nativeFloat16 = false; IrBuilder ir; IrBlock& block; IrU1 instructionBranchCondition; diff --git a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp index 838346dc4..305327832 100644 --- a/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp @@ -13,7 +13,40 @@ void TranslateControlFlowInstruction(IrBuilder& builder, const RdnaInstruction& } void TranslationContext::sSubvectorLoop(const RdnaInstruction& inst, bool begin) { - throw std::runtime_error("GNM subvector loop instructions are not supported by this translator"); + if (program.WaveSize() != 64u) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " in a wave" + std::to_string(program.WaveSize()) + " shader is not measured, only wave64 subvector loops are supported"); + } + if (inst.destination.kind != RdnaOperandKind::ScalarRegister) { + throw std::runtime_error(std::string(begin ? "s_subvector_loop_begin" : "s_subvector_loop_end") + " with a destination that is not an SGPR is not measured"); + } + const IrU32 execLo(ir.GetExecLo()); + const IrU32 execHi(ir.GetExecHi()); + const IrU32 counter = readRawU32(inst.destination); + const IrU32 zero(ir.Constant(0u)); + IrU32 newExecLo = execLo; + IrU32 newExecHi = execHi; + IrU32 newCounter = counter; + if (begin) { + const IrU1 lowZero(ir.IEqual(execLo.Value(), zero.Value())); + const IrU1 highZero(ir.IEqual(execHi.Value(), zero.Value())); + const IrU1 execZero(ir.LogicalAnd(lowZero.Value(), highZero.Value())); + const IrU32 entered(ir.Select(lowZero.Value(), execLo.Value(), execHi.Value())); + newCounter = IrU32(ir.Select(execZero.Value(), counter.Value(), entered.Value())); + newExecHi = IrU32(ir.Select(ir.LogicalOr(execZero.Value(), lowZero.Value()), execHi.Value(), zero.Value())); + instructionBranchCondition = execZero; + } else { + const IrU1 highSet(ir.INotEqual(execHi.Value(), zero.Value())); + const IrU1 counterZero(ir.IEqual(counter.Value(), zero.Value())); + const IrU1 stay(ir.LogicalOr(highSet.Value(), counterZero.Value())); + newExecLo = IrU32(ir.Select(highSet.Value(), counter.Value(), ir.Select(counterZero.Value(), execLo.Value(), zero.Value()))); + newExecHi = IrU32(ir.Select(stay.Value(), execHi.Value(), counter.Value())); + newCounter = IrU32(ir.Select(stay.Value(), counter.Value(), execLo.Value())); + instructionBranchCondition = IrU1(ir.LogicalNot(stay.Value())); + } + writeRawU32(inst.destination, newCounter); + ir.SetExecLo(newExecLo.Value()); + ir.SetExecHi(newExecHi.Value()); + ir.SetExec(threadBit({newExecLo, newExecHi}).Value()); } // s_*_saveexec reads its source before it writes the old exec into the destination, so @@ -293,31 +326,45 @@ void TranslationContext::sWqm(const RdnaInstruction& inst, bool wide) { ir.SetScc(ir.INotEqual(low.Value(), ir.Constant(0u))); } -// M0 holds a uniform register offset. Vector registers are SSA values with fixed indices, so an indexed -// access becomes a select over every register the program references at or past the base. +struct RelativeWindowAccess { + IrValue* index = nullptr; + IrValue* inRange = nullptr; +}; + +RelativeWindowAccess relativeWindowAccess(IrBuilder& ir, const IrProgram& program, std::uint32_t reg, IrValue& offset, const char* name) { + const auto& metadata = program.Metadata(); + if (metadata.vectorWindowSize == 0u || reg < metadata.vectorWindowBase) throw std::runtime_error(std::string(name) + " addresses a register outside the relative-addressing window"); + const auto first = reg - metadata.vectorWindowBase; + IrValue& index = ir.Emit(IrOpcode::UMin32, IrType::U32, {&ir.IAdd(ir.Constant(first), offset), &ir.Constant(metadata.vectorWindowSize - 1u)}); + return {&index, &ir.ULessThan(offset, ir.Constant(metadata.vectorWindowSize - first))}; +} + IrU32 TranslationContext::readRelativeVector(const RdnaOperand& base, IrValue& offset, const char* name) { if (base.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " source is not a vector register"); } - IrU32 result(ir.GetVectorReg(static_cast(base.reg))); - for (std::uint32_t reg = base.reg + 1u; reg < currentVectorLimit; ++reg) { - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base.reg)); - result = IrU32(ir.Select(hit, ir.GetVectorReg(static_cast(reg)), result.Value())); - } - return applyBitSourceModifiers(base, result); + const auto access = relativeWindowAccess(ir, program, base.reg, offset, name); + IrValue& loaded = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + return applyBitSourceModifiers(base, IrU32(ir.Select(*access.inRange, loaded, ir.GetVectorReg(static_cast(0))))); } -void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name) { +void TranslationContext::writeRelativeVector(const RdnaOperand& base, IrValue& offset, IrU32 value, const char* name, IrValue* enable) { const RdnaOperand& destination = base; if (destination.kind != RdnaOperandKind::VectorRegister) { throw std::runtime_error(std::string(name) + " destination is not a vector register"); } - for (std::uint32_t reg = destination.reg; reg < currentVectorLimit; ++reg) { - RdnaOperand target = destination; - target.reg = reg; - IrValue& hit = ir.IEqual(offset, ir.Constant(reg - destination.reg)); - writeRawU32(target, IrU32(ir.Select(hit, value.Value(), ir.GetVectorReg(static_cast(reg))))); + const auto access = relativeWindowAccess(ir, program, destination.reg, offset, name); + IrValue& previous = ir.Emit(IrOpcode::LoadVectorWindow, IrType::U32, {access.index}); + IrValue* merged = nullptr; + if (destination.dpp) { + const DppMoveFlags flags{destination.dpp8 ? DppMoveFlags::Lanes8 | destination.dppCtrl : destination.dppCtrl, static_cast(destination.dppRowMask), static_cast(destination.dppBankMask), destination.dppFetchInactive, destination.dppBoundCtrl}; + merged = &ir.Emit(IrOpcode::DppUpdateU32, IrType::U32, {&value.Value(), &previous, &ir.GetExec()}, flags); + } else { + merged = &ir.Select(ir.GetExec(), value.Value(), previous); } + IrValue* written = access.inRange; + if (enable != nullptr) written = &ir.LogicalAnd(*written, *enable); + (void)ir.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {access.index, &ir.Select(*written, *merged, previous)}); } void TranslationContext::vMovrelsB32(const RdnaInstruction& inst) { @@ -340,8 +387,9 @@ void TranslationContext::vMovrelsdB32(const RdnaInstruction& inst, bool split, b return; } const IrU32 previous = readRelativeVector(plainOperand(inst.destination), destinationOffset, name); - writeRelativeVector(inst.destination, destinationOffset, value, name); - writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name); + IrValue& bothInRange = ir.LogicalAnd(*relativeWindowAccess(ir, program, sourceAt(inst, 0u).reg, sourceOffset, name).inRange, *relativeWindowAccess(ir, program, inst.destination.reg, destinationOffset, name).inRange); + writeRelativeVector(inst.destination, destinationOffset, value, name, &bothInRange); + writeRelativeVector(sourceAt(inst, 0u), sourceOffset, previous, name, &bothInRange); } void TranslationContext::vSwapB32(const RdnaInstruction& inst) { diff --git a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp index 7bc692922..1e2e2d5fc 100644 --- a/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp +++ b/core/shader/recompiler/Translation/src/EmbeddedVertexFetch.cpp @@ -342,6 +342,8 @@ bool writesExec(const RdnaInstruction& inst) { case RdnaOpcode::SOrn1SaveexecB64: case RdnaOpcode::SAndn1WrexecB64: case RdnaOpcode::SAndn2WrexecB64: + case RdnaOpcode::SSubvectorLoopBegin: + case RdnaOpcode::SSubvectorLoopEnd: return true; default: return false; diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 2ca7925c1..b19d72c94 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -31,16 +31,24 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc } return applyF32ResultModifiers(inst.destination, clampF16Overflow(IrF32(ir.Emit(opcode, IrType::F32, {&lhs.Value(), &rhs.Value()})), {&lhs.Value(), &rhs.Value()})); }; - IrU32 result = packHalf2x16(translateLane(false), translateLane(true)); - if (!inst.destination.clamp || !dx10Clamp() || quietSnan) { - const auto laneBits = [&](const RdnaOperand& operand, bool high) { - const IrU32 source = readF16SourceBits(operand); - const bool selectHigh = high ? operand.opSelHi : operand.opSel; - IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); - if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); - if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); - return bits; - }; + const auto laneBits = [&](const RdnaOperand& operand, bool high) { + const IrU32 source = readF16SourceBits(operand); + const bool selectHigh = high ? operand.opSelHi : operand.opSel; + IrU32 bits(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&source.Value(), &ir.Constant(selectHigh ? 16u : 0u), &ir.Constant(16u)})); + if (operand.absolute) bits = IrU32(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu))); + if (high ? operand.negateHi : operand.negate) bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); + return bits; + }; + const auto minMaxLane = [&](bool high) { return minMaxF16Bits(opcode == IrOpcode::FPMax32, laneBits(sourceAt(inst, 0u), high), laneBits(sourceAt(inst, 1u), high)); }; + const bool native = nativeFloat16 && inst.destination.omod == 0u && (opcode == IrOpcode::FPAdd32 || opcode == IrOpcode::FPMul32); + const auto nativeResult = [&]() { + const auto packed = [&](const RdnaOperand& operand) { return packU16Lanes(laneBits(operand, false), laneBits(operand, true)); }; + const IrU32 lhs = packed(sourceAt(inst, 0u)); + const IrU32 rhs = packed(sourceAt(inst, 1u)); + return IrU32(ir.Emit(opcode == IrOpcode::FPAdd32 ? IrOpcode::FPAdd16x2 : IrOpcode::FPMul16x2, IrType::U32, {&lhs.Value(), &rhs.Value()})); + }; + IrU32 result = quietSnan ? packU16Lanes(minMaxLane(false), minMaxLane(true)) : native ? nativeResult() : packHalf2x16(translateLane(false), translateLane(true)); + if (native || !inst.destination.clamp || !dx10Clamp() || quietSnan) { const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); }; @@ -62,6 +70,14 @@ bool TranslationContext::packedFloat16(const RdnaInstruction& inst, IrOpcode opc bits[index] = laneBits(accumulator && index == 2u ? accumulatorOperand(inst) : sourceAt(inst, index), high); } IrU32 lane(high ? ir.ShiftRightLogical(result.Value(), ir.Constant(16u)) : ir.BitwiseAnd(result.Value(), ir.Constant(0xffffu))); + if (native && inst.destination.clamp && dx10Clamp()) return lane; + if (native && fp16Overflow()) { + IrU1 overflow(ir.IEqual(magnitude(lane).Value(), ir.Constant(0x7c00u))); + for (std::uint32_t index = 0u; index < count; ++index) { + overflow = IrU1(ir.LogicalAnd(overflow.Value(), ir.INotEqual(magnitude(bits[index]).Value(), ir.Constant(0x7c00u)))); + } + lane = IrU32(ir.Select(overflow.Value(), ir.BitwiseOr(ir.BitwiseAnd(lane.Value(), ir.Constant(0x8000u)), ir.Constant(0x7bffu)), lane.Value())); + } if (quietSnan) { lane = IrU32(ir.Select(isNan(bits[0]).Value(), bits[1].Value(), lane.Value())); lane = IrU32(ir.Select(isNan(bits[1]).Value(), bits[0].Value(), lane.Value())); @@ -230,8 +246,8 @@ bool TranslationContext::float16Binary(const RdnaInstruction& inst, IrOpcode opc } bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode opcode, bool accumulator, bool mix) { - if (mix && floatMode.has_value() && (floatMode->floatMode & 0xccu) != 0xc0u) { - throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept or rounding not to nearest even) is not implemented"); + if (mix && floatMode.has_value() && (floatMode->floatMode & 0xc0u) != 0xc0u) { + throw std::runtime_error("v_fma_mixlo/mixhi_f16 at pc " + std::to_string(inst.programCounter) + " in FLOAT_MODE " + std::to_string(floatMode->floatMode) + " (f16 denormals not kept) is not implemented"); } std::array args{}; for (std::uint32_t index = 0u; index < args.size(); ++index) { @@ -243,7 +259,8 @@ bool TranslationContext::float16Ternary(const RdnaInstruction& inst, IrOpcode op return true; } if (mix) { - writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(0u)})), {args[0], args[1], args[2]}); + const std::uint32_t rounding = floatMode.has_value() ? ((floatMode->floatMode >> 2u) & 3u) << InterpolationRound16Shift : 0u; + writeF16(inst.destination, IrF32(ir.Emit(IrOpcode::FPInterpolateF16, IrType::F32, {args[0], args[1], args[2], &ir.Constant(rounding)})), {args[0], args[1], args[2]}); return true; } writeF16(inst.destination, IrF32(ir.Emit(opcode, IrType::F32, {args[0], args[1], args[2]})), {args[0], args[1], args[2]}); @@ -615,11 +632,19 @@ bool TranslationContext::ieeeMinMaxF32(const RdnaInstruction& inst, IrOpcode opc return true; } -bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { - struct Half { - IrValue* value; - IrU32 bits; +IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { + const auto order = [&](const IrU32& bits) -> IrValue& { + IrValue& value = ir.Emit(IrOpcode::BitFieldSExtract, IrType::U32, {&bits.Value(), &ir.Constant(0u), &ir.Constant(16u)}); + return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; + const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); + const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); + const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); + const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; + return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); +} + +bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { const bool ternary = opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32 || opcode == IrOpcode::FPMedTri32; const auto isNan = [&](const IrU32& bits) { return IrU1(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x7c00u))); @@ -627,39 +652,34 @@ bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) const auto isSnan = [&](const IrU32& bits) { return IrU1(ir.LogicalAnd(isNan(bits).Value(), ir.IEqual(ir.BitwiseAnd(bits.Value(), ir.Constant(0x0200u)), ir.Constant(0u)))); }; - const auto toBits = [&](IrValue& value) { return packHalf2x16(IrF32(value), IrF32(ir.ConstantF32(0.0f))); }; - std::array args{}; + std::array args{}; for (std::uint32_t index = 0u; index < (ternary ? 3u : 2u); ++index) { - const RdnaOperand& operand = sourceAt(inst, index); - args[index] = Half{&readF16AsF32(operand).Value(), readF16Bits(operand)}; + args[index] = readF16Bits(sourceAt(inst, index)); if (floatMode.has_value() && (floatMode->floatMode & 0x40u) == 0u) { - const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(args[index].bits.Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); - const IrU32 sign(ir.BitwiseAnd(args[index].bits.Value(), ir.Constant(0x8000u))); - args[index].bits = IrU32(ir.Select(tiny.Value(), sign.Value(), args[index].bits.Value())); - args[index].value = &ir.Emit(IrOpcode::SelectF32, IrType::F32, {&tiny.Value(), &ir.BitCastF32(ir.ShiftLeftLogical(sign.Value(), ir.Constant(16u))), args[index].value}); + const IrU1 tiny(ir.ULessThan(ir.BitwiseAnd(args[index].Value(), ir.Constant(0x7fffu)), ir.Constant(0x0400u))); + args[index] = IrU32(ir.Select(tiny.Value(), ir.BitwiseAnd(args[index].Value(), ir.Constant(0x8000u)), args[index].Value())); } } - const auto pair = [&](IrOpcode pairOpcode, const Half& lhs, const Half& rhs) { - IrValue& value = ir.Emit(pairOpcode, IrType::F32, {lhs.value, rhs.value}); - IrU32 bits(ir.Select(isNan(lhs.bits).Value(), rhs.bits.Value(), toBits(value).Value())); - bits = IrU32(ir.Select(isNan(rhs.bits).Value(), lhs.bits.Value(), bits.Value())); + const auto pair = [&](IrOpcode pairOpcode, const IrU32& lhs, const IrU32& rhs) { + IrU32 bits(ir.Select(isNan(lhs).Value(), rhs.Value(), minMaxF16Bits(pairOpcode == IrOpcode::FPMax32, lhs, rhs).Value())); + bits = IrU32(ir.Select(isNan(rhs).Value(), lhs.Value(), bits.Value())); if (ieeeMode) { - bits = IrU32(ir.Select(isSnan(rhs.bits).Value(), quietNan16(rhs.bits).Value(), bits.Value())); - bits = IrU32(ir.Select(isSnan(lhs.bits).Value(), quietNan16(lhs.bits).Value(), bits.Value())); + bits = IrU32(ir.Select(isSnan(rhs).Value(), quietNan16(rhs).Value(), bits.Value())); + bits = IrU32(ir.Select(isSnan(lhs).Value(), quietNan16(lhs).Value(), bits.Value())); } - return Half{&value, bits}; + return bits; }; IrU32 result; if (opcode == IrOpcode::FPMin32 || opcode == IrOpcode::FPMax32) { - result = pair(opcode, args[0], args[1]).bits; + result = pair(opcode, args[0], args[1]); } else if (opcode == IrOpcode::FPMinTri32 || opcode == IrOpcode::FPMaxTri32) { const auto pairOpcode = opcode == IrOpcode::FPMinTri32 ? IrOpcode::FPMin32 : IrOpcode::FPMax32; - result = pair(pairOpcode, pair(pairOpcode, args[0], args[1]), args[2]).bits; + result = pair(pairOpcode, pair(pairOpcode, args[0], args[1]), args[2]); } else { - const IrU1 anyNan(ir.LogicalOr(ir.LogicalOr(isNan(args[0].bits).Value(), isNan(args[1].bits).Value()), isNan(args[2].bits).Value())); - const Half minimum = pair(IrOpcode::FPMin32, pair(IrOpcode::FPMin32, args[0], args[1]), args[2]); - IrValue& median = ir.Emit(IrOpcode::FPMedTri32, IrType::F32, {args[0].value, args[1].value, args[2].value}); - result = IrU32(ir.Select(anyNan.Value(), minimum.bits.Value(), toBits(median).Value())); + const IrU1 anyNan(ir.LogicalOr(ir.LogicalOr(isNan(args[0]).Value(), isNan(args[1]).Value()), isNan(args[2]).Value())); + const IrU32 minimum = pair(IrOpcode::FPMin32, pair(IrOpcode::FPMin32, args[0], args[1]), args[2]); + const IrU32 median = minMaxF16Bits(true, minMaxF16Bits(false, args[0], args[1]), minMaxF16Bits(false, minMaxF16Bits(true, args[0], args[1]), args[2])); + result = IrU32(ir.Select(anyNan.Value(), minimum.Value(), median.Value())); } write16Bits(inst.destination, clampF16Bits(inst.destination, result)); return true; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 421ea460d..f99b87af0 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -365,7 +365,8 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO } } else if (options.stage == ShaderStageKind::Vertex) { if (options.userDataBaseRegister >= 8u) { - entryIr.SetScalarReg(static_cast(3), entryIr.Constant(options.waveSize | (options.waveSize << 8u))); + const std::uint32_t lanes = options.waveSize == 64u && options.inputInfo.vertex->hostSubgroupSize == 32u ? 32u : options.waveSize; + entryIr.SetScalarReg(static_cast(3), entryIr.Constant(lanes | (lanes << 8u))); } entryIr.SetVectorReg(static_cast(5), drawIndex(StageInputKind::VertexIndex)); entryIr.SetVectorReg(static_cast(8), drawIndex(StageInputKind::InstanceIndex)); @@ -396,6 +397,32 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con IrProgram program; program.SetWaveSize(options.waveSize); + std::uint32_t windowBase = NumVectorRegs; + for (const auto& cfgBlock : cfg.blocks) { + for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { + const auto& instruction = decoded.instructions[index]; + if (isCodeTableLoad(cfg, instruction.programCounter)) continue; + switch (instruction.op) { + case RdnaOpcode::VMovrelsB32: + windowBase = std::min(windowBase, instruction.source0.reg); + break; + case RdnaOpcode::VMovreldB32: + windowBase = std::min(windowBase, instruction.destination.reg); + break; + case RdnaOpcode::VMovrelsdB32: + case RdnaOpcode::VMovrelsd2B32: + case RdnaOpcode::VSwaprelB32: + windowBase = std::min({windowBase, instruction.source0.reg, instruction.destination.reg}); + break; + default: + break; + } + } + } + if (windowBase < vectorLimit) { + program.Metadata().vectorWindowBase = windowBase; + program.Metadata().vectorWindowSize = vectorLimit - windowBase; + } program.Resources().stage = toIrShaderStage(options.stage); program.Resources().shaderHash = options.shaderHash; program.Resources().userDataBase = options.userDataBaseRegister; @@ -461,6 +488,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con TranslationContext context(program, *blocks[typedIndex], vectorLimit); context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); + context.SetNativeFloat16(options.nativeFloat16); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; if (isCodeTableLoad(cfg, instruction.programCounter)) { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 18d89e989..79791905b 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -160,6 +160,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues auto& vertexStorage = HostThreadLocal(); vertexStorage = ShaderVertexInputInfo{}; vertexStorage.logicalStage = _toIrShaderStage(stage); + vertexStorage.hostSubgroupSize = hostSubgroupSize; vertexStorage.fetchEmbedded = vertex.fetchEmbedded; vertexStorage.fetchExternal = false; vertexStorage.fetchAttribReg = static_cast(vertex.fetchAttribReg); diff --git a/core/shader/recompiler/tests/CacheFileReadTests.cpp b/core/shader/recompiler/tests/CacheFileReadTests.cpp new file mode 100644 index 000000000..29f5e61b3 --- /dev/null +++ b/core/shader/recompiler/tests/CacheFileReadTests.cpp @@ -0,0 +1,44 @@ +#include "ShaderCacheDirectory.hpp" +#include +#include +#include + +static bool FailLargeAllocation = false; + +void* operator new(std::size_t size) { + if (FailLargeAllocation && size > 1024 * 1024) throw std::bad_alloc(); + if (auto* memory = std::malloc(size == 0 ? 1 : size)) return memory; + throw std::bad_alloc(); +} +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } +void* operator new[](std::size_t size) { return ::operator new(size); } +void operator delete[](void* memory) noexcept { ::operator delete(memory); } +void operator delete[](void* memory, std::size_t) noexcept { ::operator delete(memory); } + +static void Require(bool ok) { if (!ok) throw std::runtime_error("Cache file read failed"); } + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-cache-read-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + int status = 0; + try { + std::vector bytes{std::byte{1}}; + Require(!ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + { std::ofstream file(path, std::ios::binary); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.empty()); + const std::vector payload(2 * 1024 * 1024, 'S'); + { std::ofstream file(path, std::ios::binary | std::ios::trunc); file.write(payload.data(), payload.size()); Require(static_cast(file)); } + Require(ShaderRecompiler::ReadWholeFile(path, bytes) && bytes.size() == payload.size()); + Require(bytes.front() == std::byte{'S'} && bytes.back() == std::byte{'S'}); + std::vector failed{std::byte{1}}; + FailLargeAllocation = true; + const auto read = ShaderRecompiler::ReadWholeFile(path, failed); + FailLargeAllocation = false; + Require(!read && failed.empty()); + std::cout << "5 cache file read cases passed\n"; + } catch (const std::exception& error) { FailLargeAllocation = false; std::cerr << error.what() << '\n'; status = 1; } + std::error_code error; + std::filesystem::remove(path, error); + return status; +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f72550b6..203602f2b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -57,6 +57,14 @@ The reference contract and constants come from FreeBSD libthr, pinned at [`5ed7e [`GuestPthreadCancelRecovery.cpp`](../../core/libs/tests/GuestPthreadCancelRecovery.cpp) checks pending requests before ordinary and timed semaphore acquisition, cleanup reentrancy and destructor ordering, enabled and disabled timed condition waits, canceled join output, and subsequent resource and thread reuse. Host atomic gates establish pending requests; acquiring the condition mutex after the worker releases it establishes wait entry without a scheduling sleep. Both `guest_pthread_cancel_recovery` and `guest_pthread_cancel_recovery_coarse` retain checks in Release and have a 20-second timeout, shorter than the 60-second guest waits. The latter sets `APS5_COARSE_TIMED_WAITS=1` to exercise the Windows fallback; Linux uses that condition-variable path by default. +## APR and AMM command buffers + +[`Apr.cpp`](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) executes APR and AMM command buffers. A buffer runs on the submitting thread until a `WaitOnAddress` or `WaitOnCounter` that is not yet satisfied; the rest of it, and every buffer submitted after it, is copied and runs on a worker thread, so a submitter that satisfies the wait after the submit returns does not hang. Buffers without such a wait still complete before the submit returns. + +APR and AMM each have their own queue and worker thread. Order is kept inside a queue; a wait in one does not hold up the other, because AMM `WriteAddress` commands advance the counters that APR buffers wait on. Submission ids are per queue, and `sceKernelAprWaitCommandBuffer` and `sceAmprAmmWaitCommandBufferCompletion` block until the queue has completed that id. A command that throws on a worker aborts the process. + +[`GuestAmpr.cpp`](../../core/libs/tests/GuestAmpr.cpp) checks both cases: a submit whose wait is satisfied afterwards returns and keeps buffer order, and an AMM buffer submitted after a waiting APR buffer still completes. + ## Graphics ```mermaid @@ -86,6 +94,7 @@ flowchart LR - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. ### Shader MODE register access diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 50e7023fe..d8c4b9d63 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -27,7 +27,7 @@ git submodule update --init --recursive ## Requirements - x86-64, Git, CMake 3.22.1 or newer, Ninja, C++20. -- Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). +- 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. - Windows antivirus software can quarantine the executables that the relinker tests create in the temp directory, which fails `optional_plt`, `empty_tls`, `windows_address_space` and `windows_icon`. Set `TEMP` and `TMP` to a directory the scanner excludes before running `ctest`. @@ -42,9 +42,11 @@ cmake --build build --target libs --parallel ``` `libs` is a custom target: every library under [core/libs/prx](../../core/libs/prx) is built with -`EXCLUDE_FROM_ALL`, so the first command alone does not produce them. Titles load the patched `.prx` -files from `build/core/libs/libs`, which only that second step refreshes. Running a title after a -library change without it therefore tests the previous binaries and can show no effect at all. +`EXCLUDE_FROM_ALL`. With the default `BUILD_TESTING=OFF`, the default build does not produce them, +so the explicit `--target libs` step is required. With `BUILD_TESTING=ON`, the default full build +also builds `libs` through the `patched_libraries_tests` dependency. Titles load the patched `.prx` +files from `build/core/libs/libs`; running a title after a library change without rebuilding `libs` +therefore tests the previous binaries and can show no effect at all. [Relinker usage and runtime layout](../user/USAGE.md). @@ -57,6 +59,7 @@ Project switches accept `ON` or `OFF`: | `-DBUILD_TESTING=ON` | `OFF` | Build and register tests. | | `-DANYPS5_RELINKER_ONLY=ON` | `OFF` | Build only the relinker and its tests, without third-party dependencies. | | `-DANYPS5_ENABLE_SPIRV_TOOLS=ON` | `OFF` | Enable SPIR-V validation and optimization. | +| `-DANYPS5_ENABLE_ASAN=ON` | `OFF` | Build with AddressSanitizer on Linux and move the guest arena out of ASan's shadow gap. | | `-DANYPS5_USE_SYSTEM_DEPS=ON` | `OFF` | Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the submodules. | | `-DAPS5_ENABLE_TIMING_LOG=ON` | `OFF` | Compile frame timing logging. | | `-DAPS5_AGC_CREATE_LOG=OFF` | `ON` | Disable successful `sceAgcCreateShader` logging. | @@ -89,6 +92,24 @@ ctest --test-dir build/write-watch --output-on-failure With `CMAKE_OSX_ARCHITECTURES=x86_64` Apple silicon runs the tests through Rosetta, which is how relinked titles run; without it they run natively. The full `BUILD_TESTING` build on macOS also registers them. +## Compute pipeline replay + +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. + +Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. + +```sh +cmake --build build --target agc_compute_pipeline_replay +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-out pipeline.cache --output fresh.json +build/core/libs/prx/libSceAgcDriver/agc_compute_pipeline_replay capture.aps5cp --cache-in pipeline.cache --output imported.json +``` + +On Windows use `agc_compute_pipeline_replay.exe` and prepend `build/core/libs/libs/unpatched` to the process `PATH`. `ANYPS5_GPU` selects a device by name as for the driver. The tool uses the driver's compute pipeline creation path, without recording commands, dispatching work or importing guest memory. Compile-only devices also enable supported partially-bound, variable-count and runtime-array descriptor features to replay captured layouts; ordinary runtime devices are unchanged. Binding flags are preserved independently of update-after-bind. Replay accepts the unconditional Vulkan 1.0 capabilities listed in the pinned [Vulkan registry](https://github.com/KhronosGroup/Vulkan-Headers/blob/2fa203425eb4af9dfc6b03f97ef72b0b5bcb8350/registry/vk.xml); other capabilities must be enabled by the device target. Non-Vulkan capabilities such as Kernel and unknown capabilities fail before pipeline creation. Unsupported features, non-highest variable bindings and counts exceeding layout support fail explicitly. Unsupported inputs and mismatched cache vendor, device or UUID fail explicitly. Output files must not already exist. + +Each invocation creates one pipeline. With no `--cache-in`, its application pipeline cache starts empty; `--cache-in` imports a raw cache previously written by `--cache-out` (at most 64 MiB), and `--cache-out` exports the resulting cache. The title's wrapped `vk-pipelines-*.bin` format is not accepted. It does not load or save the title's usual pipeline cache. NVIDIA and other driver caches remain uncontrolled: a fresh application cache is not a cold driver cache. JSON reports the capture identity, GPU/driver identifiers, word count, cache policy and separate wall times for shader module, layouts, pipeline creation and cache work. Device initialization is outside these timings. Pipeline statistics captures and `APS5_PIPELINE_STATS` are refused because they change compilation cost. + +`--fixture` compiles a tiny synthetic compute shader with specialization, sparse descriptor binding and push constant metadata; `--write-fixture DIRECTORY` saves it without opening Vulkan. It performs no GPU work and does not reproduce a title's compile workload. With `BUILD_TESTING=ON`, `agc_compute_pipeline_capture` checks serialization and rejected inputs. With SPIRV-Tools also enabled, `agc_compute_pipeline_replay` compiles the fixture with fresh and imported application caches, verifies cache rejection, and checks required subgroup support. `agc_compute_pipeline_capture_run` verifies the existing branch-past-endpgm compute fixture's output with capture disabled and enabled, then replays the captured pipelines and checks their identities and metadata. Each test has a 30-second timeout. Runtime tests skip when Vulkan is absent; set `ANYPS5_REQUIRE_VULKAN=1` to require execution. There is no performance pass threshold. + ## Pipeline statistics Set `APS5_PIPELINE_STATS=1` to capture and print driver statistics for each newly created graphics or compute pipeline. This requires `VK_KHR_pipeline_executable_properties` and `pipelineExecutableInfo`; an unsupported device fails with an error. Statistic names and units are driver-specific. Capturing statistics can increase pipeline compilation cost. The setting is disabled by default. diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index dec716d9f..ca20cad09 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -4,7 +4,7 @@ ## Library counts -The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Other definitions are counted as implemented. Braces inside string literals, character literals and comments do not delimit bodies. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). +The scanner reads `APS5_VABI` function definitions under `core/libs/prx//`, excluding `tests` subdirectories and their descendants. Test callbacks do not contribute to library counts. A definition is pending when its body calls `NotImplemented_nid_no_patch`, directly or through a recognized local stub wrapper. Call matching uses complete identifiers and accepts whitespace or comments before `(`. Other definitions are counted as implemented. Braces inside string literals, character literals and comments do not delimit bodies. These counts describe declared functions, not every firmware export, verified ABI compatibility or playable games. Silent behavior and unverified assumptions remain in [Technical debt](TechnicalDebt.md). A library with no scanned definitions can include a sibling library's source builder. The scanner recognizes literal `include(${CMAKE_CURRENT_SOURCE_DIR}/..//.cmake)` references, including quoted paths. It does not evaluate arbitrary CMake expressions. It also recognizes a top-level `.cpp` that includes another library's source with `#include "prx//.cpp"`. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8fd34f89c..16f534547 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -24,8 +24,9 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) has no PS VR2 runtime: `sceHmd2Initialize` fails, and every other export it has (and every [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) export) throws. PPSA01317 skips its whole VR set-up when initialization fails - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) implements only the no-device path: initialization and termination succeed, no device events are reported and device queries return no entries - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. +- [libSceAcm](../../core/libs/prx/libSceAcm/Export.cpp) batches execute as silent no-ops, so `sceAcm_ConvReverb_SharedInput`, `sceAcm_FFT` and `sceAcm_Panner` return success without encoding audio commands. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The layout and error codes are unverified on PS5 +- [libSceUsbd](../../core/libs/prx/libSceUsbd/Export.cpp) implements only the no-device path: `sceUsbdInit` and `sceUsbdExit` succeed without a host USB context, the device list is always empty and event handling waits for its timeout; calls that need a device throw. Transfers follow the `libusb_transfer` layout from [libusb.h](https://github.com/libusb/libusb/blob/master/libusb/libusb.h), which [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.h) passes to titles as `SceUsbdTransfer`: `sceUsbdAllocTransfer` zero-allocates one with its iso packet count (a negative count returns null), `sceUsbdFillInterruptTransfer` fills the fields `libusb_fill_interrupt_transfer` sets, and `sceUsbdCancelTransfer` reports `LIBUSB_ERROR_NOT_FOUND` mapped to `0x80240005` by shadPS4's `0x80240000 - error` rule, since no transfer can be submitted. `sceUsbdEventHandlingOk` returns 1 because no other thread handles events, and `sceUsbdFreeConfigDescriptor` accepts only null because no descriptor is ever returned. The device and handle calls take the argument lists of FreeBSD 11.4 libusb ([libusb.h](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h)) and return `0x80240002` for the null arguments it rejects with `LIBUSB_ERROR_INVALID_PARAM` ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)); a null device or handle in the calls it treats as null-safe returns 0, null or nothing, as there. `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which libusb dereferences. Any other device or handle was never returned by this library, so those calls throw. The layout, the argument lists and the error codes are unverified on PS5 - [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer`, `sceSystemServiceReenableMusicPlayer`, `sceSystemServiceDisableMediaPlay` and `sceSystemServiceReenableMediaPlay` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify, no system music player to pause or resume and no system media playback to disable. PPSA01521 calls `sceSystemServiceDisableMediaPlay` during start-up, and its wrappers for both pass no arguments - [sceSystemServiceLaunchWebBrowser](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - AnyPS5 launches no web browser, so the call fails and nothing is shown. RommPS PPSA76677 then tells the player to scan its QR code instead - [libSceBluetoothHid](../../core/libs/prx/libSceBluetoothHid/Export.cpp) implements only the no-device path: initialization and callback and device registration succeed, the callback is never called because no device connects, and calls that need a connected device throw @@ -34,25 +35,28 @@ Throughout the project, every function at every stage either **does exactly what - [sceVideoOutAddVrrStatusFlagsPrivilege](../../core/libs/prx/libSceVideoOut/src/Output.cpp) returns 0 and does nothing: the host output has no VRR state to report. The name comes from an assert message in a title that requires `SCE_VIDEO_OUT_OK`; the NID hashes to it, the arguments are unknown (it is called without any) - [sceVideoOutVrrPegToFixedRate, sceVideoOutVrrUnpegFromFixedRate](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA05684 passes the handle to both, two further zero arguments to `sceVideoOutVrrPegToFixedRate`, and ignores the results. The meaning of those two arguments is unknown, so only zeros are accepted. VRR is not modelled and the output always runs at a fixed rate, so both calls only validate the handle and return 0 - [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output -- [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` fails and the other exports throw. PPSA01317 skips its narration set-up when initialization fails +- [libSceTextToSpeech2](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) has no speech engine: `sceTextToSpeech2Initialize` and the other exports return `EOPNOTSUPP`. PPSA01317 skips its narration set-up when initialization fails - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. -- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and returns success for any `id`, like `sceHttp2SslEnableOption` (prosper returns the invalid-id error for an unknown one) +- [libSceHttp2](../../core/libs/prx/libSceHttp2/Export.cpp) - no request reaches the network, so `sceHttp2SetMinSslVersion` has no TLS connection to restrict and its value is ignored, as for `sceHttp2SslEnableOption` and `sceHttp2SslDisableOption`. The three setters validate `id` against the live handle registry and return `SCE_HTTP2_ERROR_INVALID_ID` (`0x817B1100`) for a handle that was never allocated, was deleted, or belonged to a terminated context, following [prosper](https://github.com/mattias800/prosper). Whether the console restricts the object kind these setters accept, and what it does with an id from a previous init/term cycle, is unverified, so any live handle kind is accepted and ids are never reused. `sceHttp2Term`, `sceHttp2DeleteTemplate`, `sceHttp2DeleteRequest`, `sceHttp2CreateTemplate`, `sceHttp2CreateRequestWithURL`, `sceHttp2CreateCookieBox` and `sceHttp2SendRequestAsync` also return the invalid-id error for a handle of the wrong kind or no longer live, following prosper (not measured on a console) - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, `sceNpTrophy2UnregisterUnlockCallback` only returns success, and `sceNpTrophy2ShowTrophyList` returns success without showing a list (there is no system trophy UI; seen on PPSA15552). - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. +- [sceRudpProcessEvents and sceRudpNetFlush](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host, and every `sceRudpWrite`, `sceRudpRead` and `sceRudpGetStatus` call here reports a reset or idle context without queueing data, so no context ever holds pending data to process or send; both calls return success after `sceRudpInit` and `RUDP_ERROR_NOT_INITIALIZED` before it, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) returns OK from its stubs. PPSA03490 calls `sceRudpProcessEvents(0)` followed by `sceRudpNetFlush()` from its P2P service thread and ignores the first result; whether the real calls take parameters is unknown - [sceRudpActivate](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - there is no reliable-UDP stack on the host to activate, so it returns success without doing anything, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp) does for its own `sceRudpActivate`. PPSA01325 calls it with no arguments during start-up; what the console activates, and whether it reports an error before `sceRudpInit`, is unverified - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does -- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication +- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit, `sceHttpSetRecvBlockSize` (`(int id, u32 blockSize)` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h)) no response body to read in blocks and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0; `sceHttpSetAuthInfoCallback` accepts a callback that is never called, as no server asks for authentication; `sceHttpsGetCaList` and `sceHttpsFreeCaList` report an empty CA list (`certs` null, `certsNum` 0) and return 0, as no CA store is kept, and a null list returns `ERROR_INVALID_VALUE`; unlike shadPS4 they do not check that `http_ctx_id` is a live context nor that the library is initialised, and `sceHttpsFreeCaList` also nulls `certs`, which shadPS4 leaves untouched. The signatures and the `{ void* certs; int certsNum; }` layout follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.h) (PS4), not confirmed on a PS5 title - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` - [sceAudioInInit](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - returns 0 without library-wide setup, as `sceAudioOutInit` does; each `sceAudioInOpen` opens its own SDL capture device +- [sceRazorCpuPushMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 ### Unknown function info +- [sceHttpUriBuild](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) retains `//` for a non-opaque URI independently of the selected fields, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a8a70da595da2a96e0410309472ba4a7eb558144/src/core/libraries/network/http.cpp#L3551-L3558). Black Myth: Wukong builds its `Scheme` header with option 1 and expects `http://` or `https://`. Other partial field combinations follow the PS4 reference and remain unverified on PS5 hardware. - [sceCoredumpGetThreadContextInfo](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel, NID `ShChva57wIM`) - declared as a throwing stub. The pointer and size arguments and integer return type follow [SharpProspero's PS5 bindings](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs). The context structure and console behavior are unverified; the stub does not read or write caller memory. PPSA01736 imports it - [sceNpWebApi2PushEventAbortHandle](../../core/libs/prx/libSceNpWebApi2/Export.cpp) (libSceNpWebApi2, NID `1OLgvahaSco`) is a throwing stub. The two signed 32-bit handle arguments and return type follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/np/np_web_api2/np_web_api2.cpp) (PS4); the PS5 signature and abort behavior are unverified. PPSA01736 imports it. - [sceNpGetSandboxInfo](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager, NID `ZVZ++68nNI8`) - the name hashes to the imported NID (found with nidhunt, confirmed with `tools/nid_names.py`). STAR WARS: Galactic Racer (PPSA28416) imports it; its OnlineSubsystemPS5 calls it with one pointer to a 0x50-byte stack buffer and reads the buffer as a string (discussions/3086). The returned string, the exact parameter struct and console errors are unknown, so every call throws without reading or writing caller memory. A working implementation belongs with that title's bring-up. - [sceKernelGetSystemSwVersion](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000). The 0x28-byte layout (`uint64_t size` at 0, `versionString` at 8 and `version` at 0x24) matches [ProsperoStore's caller](https://github.com/blackbearreloaded/ProsperoStore/blob/20584011/src/diag/trace.cpp#L20-L26), [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d1/src/core/libraries/kernel/kernel.h#L45-L49), [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6/kernel/ps4_libkernel.pas#L502-L506) and [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4/include/orbis/_types/kernel.h#L256-L260). These sources support the structure layout, but not the reported version value: the console firmware version remains unverified. The checks are unverified too: a null pointer returns `SCE_KERNEL_ERROR_EFAULT` and a `size` other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL`, while shadPS4 returns success for null and never reads `size`. - [kern.rng_pseudo](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` fills the whole output buffer with bytes from the host's `std::random_device` and leaves the length unchanged; a write returns -1 with `EPERM` and a call without an output buffer throws. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator, its per-call maximum and its answer to a size query are unknown -- PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate and _sceUltSemaphoreCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to `sceUltWaitingQueueResourcePoolCreate` and `sceUltSemaphoreCreate`; that their argument lists match is inferred from the other underscore Ult exports, not measured. +- PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured. - [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory. - [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws. - [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it @@ -64,14 +68,15 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontSetScriptLanguage, sceFontGetScriptLanguage, sceFontSetTypographicDesign, sceFontGetTypographicDesign](../../core/libs/prx/libSceFont/src/Style.cpp) (libSceFont) - signatures follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.cs), not confirmed for the PS5. The handle keeps the language set for each script and the feature set for each typographic design and returns them, with 0 for one never set; the console default and its valid ranges are unknown, so any value is accepted. Neither setting changes shaping or rendering yet - [Packed pixel ancillary word](../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) (shader recompiler) - the sample index at bits 8-11 and the render target index at bits 16-28 follow #295, and their equality with the SampleId and Layer builtins is inferred, not measured. A value passed through a select, move or phi is rebuilt from those two fields with bits 0-7, 12-15 and 29-31 zero. That is exact only because every read of the word must be an extract inside one modelled field; any other read throws. The other bits are unmodelled and their RDNA 2 layout is unknown: the RDNA 3.5 ISA is reported to give prim type at bits 0-1, VRS rates at bits 2-5 and eye id at bit 7, which was not checked against RDNA 2 - [sceAudioPropagationSourceSetAudioPath](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation, NID `tKSmk2JsMAA`) - `(uint64_t path, const void* data, float gain)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioPropagation.cs) and SAROS's call, which passes a handle from `sceAudioPropagationSourceGetAudioPath`, path data and gain 1.0. Path updates are unsupported and every call throws without reading or writing caller memory. The no-propagation model yields no path handles; path allocation, ownership, rendering and console error codes remain unknown. -- [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters and bilinear cube comparisons without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. +- [Color comparison descriptor forms](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp): emulated color comparison supports UNORM, SNORM and float formats for 2D, 2D-array and cube views. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255. Anisotropic filters are refused. Bilinear cube comparisons of float, unorm and snorm formats without `DISABLE_CUBE_WRAP` are refused; on that hardware a 4x4 R32 float cube blends across faces (46 of 90 edge rows differ from a per-face clamp; face 0, ref 0.1, u=3.9 v=2.5 is 0.6016 and u=1.5 v=0.1 is 0.3984), and with `DISABLE_CUBE_WRAP` the per-face clamp matches. An 8-bit sRGB bilinear cube without that bit returns 0, since every texel comparison is already 0 for every compare function and the selected face cannot change the result. Cube-edge sampling behavior and console coverage on PS5's gfx1013 remain unverified. - [sceNpEntitlementAccessGetEntitlementKey](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - the three-argument signature, 16-byte key and errors `0x817D0002`/`0x817D0007` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp); PPSA01521 passes a service label, entitlement label and output pointer. Only the no-key path is implemented: local entitlement labels carry no keys, and the output is untouched on failure. Console error precedence and key provisioning are unverified -- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfoList](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified +- [sceNpEntitlementAccess{Request,Poll}{Unified,Service}EntitlementInfo{,List}](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp) - signatures unknown; no reference implements them, so they are declared without parameters. The single-item variants copy the behaviour of the list ones, with no source of their own. Like the consume requests in the same library, they report the signed-out NP state with `SCE_NP_ERROR_SIGNED_OUT` (`0x80550006`) and write nothing. PPSA15657 calls the unified request during start-up and continues past the error. Whether the console fails at the request or at the poll when signed out is unverified - [sceRudpGetStatus](../../core/libs/prx/libSceRudp/Export.cpp) - PPSA01521 passes an output pointer and byte count `0xF8`. Measured on a PS5 (firmware 13.60): the status is 0xF8 bytes; before `sceRudpInit` every call returns `RUDP_ERROR_NOT_INITIALIZED`, and after it a null pointer or a byte count of 0 or above 0xF8 returns `RUDP_ERROR_INVALID_ARGUMENT`. The layout matches `SceRudpStatus` in [Vita3K](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/master/vita3k/modules/SceRudp/SceLibRudp.cpp) with `currentContexts` at 0x64 and `allocs`, `frees`, `memCurrent` and `memPeak` at 0xC8 to 0xD4. Only `currentContexts` is reported: the traffic counters stay 0 because nothing is sent, and the allocator counters stay 0 because the library does not allocate from the title's pool (the console reports 6 allocations and 0x1260 bytes right after `sceRudpInit`, and 4 more allocations and 0x3F0 bytes per context). The console also writes `currentContexts` and the allocator counters when the byte count is smaller than their offset; only the requested bytes are written here. - [sceHttpSetCookieEnabled](../../core/libs/prx/libSceHttp/Export.cpp) - the two-argument signature and 0/1 enable values follow [VitaSDK](https://github.com/vitasdk/vita-headers/blob/master/include/psp2/net/http.h) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp); PPSA01521 passes 0 to disable cookies. Disabling succeeds because the offline HTTP backend has no cookie engine; enabling throws. Invalid enable values use the same error as `sceHttpSetInflateGZIPEnabled`. Like the existing offline HTTP setters, the handle is not validated; requests still fail with the network error. Console behavior is unverified - [sceHmd2Initialize](../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the error the console returns when the PS VR2 runtime is unavailable is unknown; it returns libSceHmd's `UNSUPPORTED_FEATURE` (0x81110016). PPSA01317 passes a pointer to 16 zeroed bytes and only tests the result for non-zero - PPSA01317 imports declared without parameters, signatures unknown: the remaining [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) and [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list +- PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list - [libSceMbus](../../core/libs/prx/libSceMbus/Export.cpp) (`libSceDeviceService`) - signatures taken from PPSA01317, which calls `sceDeviceServiceInitialize(3, 0)`, polls `sceDeviceServiceGetEventState(1)` and queries again when bit 0 or 1 is set, and calls `sceDeviceServiceQueryDeviceInfo_(0x7001, 0, 0, infos, 1, &count, &unknown, 0x70)`, treating a negative result as an error and `count` as the number of entries. The host has no devices: the event state is 0 and a query returns 0 with `count` 0 - [NGS2 ATRAC9 waveform loops](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) accepts one integer forward `smpl` loop. Inclusive RIFF endpoints and encoder-delay adjustment follow [vgmstream riff.c](https://github.com/vgmstream/vgmstream/blob/7dc938fa2f210943b37c7b6511852b516ef432ab/src/meta/riff.c). PPSA03747 supplies a 60-byte sampler chunk with one loop, `samplerData` 24, start 256, inclusive end 299224 and delay 256 for 298969 audible samples. The chunk size and loop count bound the records; `samplerData` is not interpreted. Output positions are assumed to use audible samples and an exclusive end. Blocks split into an optional intro, a repeated range and an optional tail; zero play count means infinite repeats, positive play count includes the first pass. ATRAC9 ranges include a preceding codec frame for decoder history. The output block layout, repeat-count mapping and error codes are unverified on hardware. Multiple, reverse, alternating and fractional loops throw. - [NGS2 submixer UserFx](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (control `0x20000004`) invokes the title's callback after input mixing on every playing grain, including silence. The 40-byte control and 48-byte process context follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/ngs2/ngs2_submixer.h). PPSA03747 installs a peak-meter callback; its code confirms the channel pointers, user data 0, channel count, grain size and sample-rate offsets and returns 0. Other user data words are preserved. Flags are assumed to be 1 after registration/restart and 0 thereafter, following UserFx2's first-process convention; this callback does not inspect them. Null handlers disable the effect, setup clears it, and nonzero callback results throw. These lifecycle and error semantics are unverified on console hardware. @@ -79,22 +84,26 @@ Throughout the project, every function at every stage either **does exactly what - PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp). Name from the shadPS4 aerolib NID list - [sceAppContentDownloadDataGetAvailableSpaceKb](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - a title whose `param.json` declares `downloadDataSize` 0 gets 0 KB and OK; the console's behaviour for a title without a download data area is unverified (it may return an error instead). KytyPS5 does not special-case it, and shadPS4 stubs 1048576 KB - [sceNpEAAccessInitialize, sceNpEAAccessTerminate](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - names and the no-argument signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_partner.cpp); imported by PPSA23566, not confirmed on a PS5 title +- [sceSigninDialogClose, sceSigninDialogGetResult](../../core/libs/prx/libSceSigninDialog/Export.cpp) (libSceSigninDialog) - SceSigninDialogResult layout (int32 result and 3 reserved int32 words, zeroed) follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/dialog.cpp) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/SigninDialog/SigninDialog.cs); error codes follow libSceLoginDialog's 0x80B8xxxx common-dialog codes rather than KytyPS5's 0x8135xxxx, and offline calls finish as user cancelled without opening a dialog. Neither choice is verified on hardware - [libSceBluetoothHid](../../core/libs/prx/libSceBluetoothHid/Export.cpp) (libSceBluetoothHid) - signatures taken from PPSA01317: `sceBluetoothHidInit()` and `sceBluetoothHidUnregisterCallback()` without arguments, `sceBluetoothHidRegisterCallback(callback, 0, param)` with a 16-byte param, and `sceBluetoothHidRegisterDevice` / `sceBluetoothHidUnregisterDevice(vendorId, productId)`; the report and device name exports are declared without parameters - [sceAgcWaitRegMemPatchMask](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws - [scePsmlMfsr2CreateContext, scePsmlMfsr2CreateSharedResources, scePsmlMfsr2GetContextInitRequirement, scePsmlMfsr2GetDispatchPackets, scePsmlMfsr2GetDispatchPacketsSizeInDwords, scePsmlMfsr2GetSharedResourcesInitRequirement, scePsmlMfsr2Init, scePsmlMfsr2ReleaseContext, scePsmlMfsr2ReleaseSharedResources](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown signatures, imported by PPSA23566; declared without parameters - [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws -- [scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - only special ports (type 2) are handled, and they return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller; whether the console opens a handle and reports the device as disconnected later is unknown. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw +- [scePadOpen and scePadOpenExt](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - special ports (type 2) return `DEVICE_NOT_CONNECTED` (0x80920007) because the host has no special controller, and `scePadGetHandle` has no handle for them; whether the console opens a handle and reports the device as disconnected later is unknown. scePadOpenExt handles only special ports. PPSA01317 passes a 16-byte param that names the device by vendor and product id (`b7 0e 04 0e ...` for 0x0eb7:0x0e04) and skips the user on any negative result. Other port types throw - [scePadVrControllerReadState](../../core/libs/prx/libScePad/Export.cpp) (libScePad, NID `iA-DdobUen8`) - the name hashes to the imported NID; the `(int handle, void* state)` signature follows [PS5VR](https://github.com/theghostonline/PS5VR/blob/0e408909ce7a39891a2e06eee3438d07a09e11e3/app/src/app_vr.c#L326-L369) (PPSA99177), which passes a PS VR2 Sense controller handle from `scePadOpen` type 3 or 4 and a 0x68-byte state it reads as buttons at 0, stick bytes at 4 and a connected byte at 0x40. The title calls it only after `sceHmd2Initialize` succeeds, which it does not here, so every call throws; the state layout is otherwise unknown +- [scePadSetVibrationMode](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - modes 0 to 3 are accepted and stored; the meaning of mode 3 is unknown (PPSA21480 calls it with 3 right after `scePadOpen` and aborts on failure; [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Pad/Pad.cs) names only 1 and 2). The stored mode has no other effect - PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - PPSA28416 imports declared without parameters, signatures unknown: [sceAudioOut2EnableChat](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), imported by its bundled `party-ps5-shipping.prx`. Name from the shadPS4 aerolib NID list - PPSA08427 imports declared without parameters, signatures unknown: [sceNpEntitlementAccessGetGameTrialsFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), imported by its bundled `pros.sdk.Prospero.prx` and not called before the main menu. Name from the aerolib NID list +- PPSA06888 imports declared without parameters, signatures unknown: [sceRudpEnableInternalIOThread2, sceRudpGetOption, sceRudpGetRemoteInfo](../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp), imported by its bundled `Media/Plugins/SceRudp.prx` and not called during start-up. Names from the NID table of [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/rudp/rudp.cpp), which declares them without parameters too - [sceVoiceChatRequestCreateVoiceChatGroup, sceVoiceChatRequestJoinVoiceChatGroup, sceVoiceChatRequestLeaveVoiceChatGroup, sceVoiceChatRequestDeleteVoiceChatGroup](../../core/libs/prx/libSceVoiceChat/Export.cpp) (libSceVoiceChat) - assumed to take a request id and two pointers like the other `sceVoiceChatRequest*` calls; offline, every argument is ignored and `SIGNED_OUT` is returned, as for those - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown -- [sceTextToSpeech2Initialize](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters +- [sceTextToSpeech2Initialize](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - the console's error for an unavailable speech engine is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D). PPSA01317 passes a param that starts with 0x2000000 and only tests the result for non-zero; the other exports it imports are declared without parameters and return the same error - [sceSystemServiceLaunchWebBrowser](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the console's error for an unavailable web browser is unknown; it returns the kernel's `EOPNOTSUPP` (0x8002002D) for any arguments. The (uri, param) signature follows [RommPS](https://github.com/s0liton/RommPS/blob/754d4d60f25938a5136b9e2efcd074313c4386c5/platform/ps5/app/rommps/examples/rommps/kit/credits.cpp#L86-L95), which passes a URL and a null param and only tests the result for zero; the param layout and the console's argument checks are unknown - [Command buffer cursors](../../core/libs/prx/libSceAgc/Command/src/Packet.cpp) (libSceAgc) - a null `cursor_down` is taken as a buffer without downward allocations, whose limit is `top`. PPSA16064 builds one-packet buffers of 8 dwords that way; the console's handling is unconfirmed - [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 - [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it +- [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 - [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 @@ -105,6 +114,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceAudiodecDecode](../../core/libs/prx/libSceAudiodec/src/Codecs.cpp) (libSceAudiodec) - a raw M4AAC stream carries no channel configuration, so the decoder tries the mono/stereo configuration and then configurations 3 to 7 within `ui_max_channels` until one decodes the first access unit, and keeps that one. No reference documents how the library picks the layout (shadPS4 maps the channel count straight to the configuration; SharpProspero only describes the maximum). With the stereo configuration a mono raw stream decodes as 2 channels, so 2 is reported, not 1 - [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [s_sethalt](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) (core/shader/recompiler) - with SIMM16[0] clear it is a no-op: the RDNA 2 ISA guide describes that value as resuming a halted wave, and a wave that executes it is not halted; measured on a gfx1035 (RDNA 2) wave: `s_sethalt 0` followed by a VALU op completes and gives the normal result. Only SIMM16[0] is read; with it set the instruction throws +- [s_subvector_loop_begin and s_subvector_loop_end](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (core/shader/recompiler) - follow AMD's RDNA 2 instruction set reference and a gfx1036 (wave64) measurement, pinned in [SubvectorLoop.cpp](../../core/libs/prx/libSceAgcDriver/tests/execution/SubvectorLoop.cpp); `S0` in the end instruction's `elif(S0 == 0)` is read as its SGPR operand. A wave32 shader and a destination other than an SGPR throw because neither is measured. Measured on PC RDNA2 (Radeon 610M), not on a PS5. The [specialization pass](../../core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp) keeps a constant-condition branch whose untaken target is a loop header, since folding it removes the loop's back edge, which SPIR-V requires exactly once per loop header (SPIRV-Tools rejects the module and the NVIDIA driver 615.78.08 crashes compiling it); a loop whose back-edge block becomes unreachable in another way is not handled - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR object layout and compact gathers](../../core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp) - the 24-byte object and offsets (type 0, cursor 4, count 8, capacity 12, pointer 16) follow [ampr_emu assertions](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr_emu_command_buffer_types.h#L138-L143) and its [C declaration](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/include/ampr.h#L41-L47). AnyPS5 represents the 32-bit type word as low 16-bit type and high 16-bit recording flags. Command encoding is AnyPS5 internal: opcode 29 and three 32-bit words encode the high offset byte, length minus one and low offset word. The 12-byte size reduces HFW's 1 MiB buffer pressure; it is not a claim about console opcode encoding. Existing 40-bit offsets and lengths 1 through 2^32 are retained; console behavior outside those limits is unverified. @@ -123,13 +133,16 @@ Throughout the project, every function at every stage either **does exactly what - [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` - [sceFontGetFontResolution](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title; the outputs are inferred from the names. The resolution is the face's units per EM and the scale pixel the driver's face scale (units per EM / 64 for FreeType), the pixel scale a font has when opened. Either output may be null, as in `sceFontGetResolutionDpi` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title. The 64-bit writing attribute is read as the PS4 11.00 `libSceFont.sprx` reads it: low byte `0xFA`, embedding level in bits 8-15 (0 to 126), bit 30 and bit 31 marking space before and after the run; the PS5 library was not measured, and the names of the two bits are ours. PPSA01325 builds its attribute the same way (`0xFA | level << 8`, bits 30 and 31) and passes a null metrics pointer when its run has no metrics object. Attribute 0 is accepted and treated as `0xFA` with level 0 and no spacing bits, as before this reading; the PS4 library rejects it and what the PS5 library does with it is unknown. Any other attribute, a level above 126 and a null orderer throw: the PS4 library returns `0x80460002` for them, not confirmed for the PS5 +- [sceFontWritingRefersRenderStepCharacter](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the second argument is an out pointer to the writing's letter step, as in [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1882-L1896) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings. Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): exactly 8 bytes are written, the address of a step stored inside the writing, and a null writing writes null; shadPS4 and prosper treat the argument as a struct, which the measurement contradicts. The letter step lives in the writing, so the pointer stays valid as long as the writing does, and its contents are rebuilt by each call of this function. On the PS4 library repeated calls on one step walk the characters it covers and then return null; here every call returns the step's one character, so the second call differs. Assumed: each step covers one character and one glyph. PPSA06699 (Hot Shots Golf 2) dereferences the returned pointer for its character counts. - [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - the four outputs are written in the order the PS4 11.00 `libSceFont.sprx` writes them (line advance, leading space, space between spaced runs, trailing space); the parameter names in the [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings order them differently (head, inline, tail, advance), and the PS5 library was not measured. PPSA01325's only call uses them in the PS4 order: it computes first - second - fourth - 0.25 * third and compares that, divided by a scale, with its width limit. The PS4 library writes the trailing space through its pointer without a null check whenever the leading-space pointer is set; that call throws here - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits -- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataConvert, sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceCoredumpWriteUserString](../../core/libs/prx/libkernel/System/src/Coredump.cpp) and [sceSaveDataGetConvertProgress](../../core/libs/prx/libSceSaveData.native/Export.cpp) - imported by PPSA16064, not called during its start-up and added without an implementation; unknown signatures +- [sceSaveDataConvert](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the parameter block (user id at `+0x00`, `SceSaveDataDirName` pointer at `+0x08`, 48 opaque bytes after it) is read from PPSA28416's single call, not confirmed by KytyPS5, shadPS4, fpPS4 or SharpProspero, which have no body for it. Host saves have no size limit, so an existing save returns `SCE_OK` without touching its files and no event is queued; what the console returns for a save that is mounted, already converted or needs no conversion is unknown. The directory name is validated as `sceSaveDataDelete` does - [sceSaveDataCancel](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA23126, which fails to load without it; added without an implementation because its signature is unknown. Every save data call here finishes before it returns, so nothing is ever in flight to cancel, but what the console returns then is unknown - [sceAudioOutExClose, sceAudioOutExConfigureOutput, sceAudioOutExGetMonitorInfo, sceAudioOutExOpen](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99505, which fails to load without them; added without an implementation and declared without parameters. PPSA99505 calls them only for its HDMI bitstream output, an option that is off by default, to read the formats the TV or receiver lists, switch the HDMI output mode and open a port for IEC 61937 bursts ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L110-L179)); [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp) also calls `sceAudioOutExOpen`, `sceAudioOutExConfigureOutput` and `sceAudioOutExClose` for HDMI bitstream output. The two declare the arguments of `sceAudioOutExConfigureOutput` differently and neither is confirmed on a console. Audio reaches the host device through SDL as PCM, so there is no encoded HDMI output to switch to - [sceOpusCeltDecCreateEx, sceOpusCeltDecDecode, sceOpusCeltDecDestroy, sceOpusCeltDecGetSize, sceOpusCeltDecInitialize, sceOpusCeltDecTerminate](../../core/libs/prx/libSceOpusCeltDec/Export.cpp) (libSceOpusCeltDec) - imported by PPSA99001, which fails to load without the module; added without an implementation. Their signatures match libSceOpusDec's in [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). PPSA99001 opens this decoder only as a fallback when libSceOpusDec reports a retryable error for a CELT packet, which [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) here never returns, so the fallback is not reached -- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete), [sceContentExportGetProgress, sceContentExportCancel, sceContentExportFromDataWithThumbnail](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sceContentDeleteById](../../core/libs/prx/libSceContentDelete/Export.cpp) (libSceContentDelete) and [sceContentSearchGetContentLastUpdateId, sceContentSearchOpenMetadataByContentId, sceContentSearchGetTotalContentSize, sceContentSearchGetNumOfContent](../../core/libs/prx/libSceContentSearch/Export.cpp) (libSceContentSearch) - imported by PPSA06465, which fails to load without them; added without an implementation because their signatures are unknown +- [sce::Json::Parser::parse with a read callback](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - the callback's contract is taken from PPSA04540's: it returns 0 with the next character, and at the end of its data writes `'\0'` and returns -1. Characters are read until the callback returns non-zero or gives `'\0'`, and the text read is then parsed as by the `(text, size)` overload, so trailing characters after the value are rejected as there. Whether the console stops reading at the end of the top-level value, and what it returns when the callback fails before the end, are unknown - [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown - [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` - [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) @@ -143,18 +156,20 @@ Throughout the project, every function at every stage either **does exactly what - [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws -- [Vertex entry merged wave info](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0). Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave. +- [Vertex entry merged wave info](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0), or to `32 | 32 << 8` for a wave64 program on a 32-lane host subgroup, which runs it at one lane per invocation and holds no lanes 32-63. Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave, and so does a host wave with idle invocations (an RTX 5070 ran every vertex warp of a non-indexed triangle list with 30 of its 32 invocations). - [NGG vertex programs on the mesh path](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [s_barrier in vertex-path programs](../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks. - [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. +- [sceAgcAcbAtomicGds_0900](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - signature, control/mode bit mapping and the 11-dword ATOMIC_GDS (0x1d) layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, which derives them from the library's disassembly; KytyPS5 and shadPS4 have no body for it. Not confirmed on hardware or a title (PPSA07286 imports it, nothing was run), and the driver has no ATOMIC_GDS handler (opcode 0x1d is not executed as an atomic GDS operation) - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown -- [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature +- [sceAgcDcbAtomicGds_0900](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - name from its NID (`zARR5aCmkoY`), the DCB counterpart of `sceAgcAcbAtomicGds_0900`, signature unknown - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `sceAgcDcbContextStateOp_0100` writes; KytyPS5 returns 0 for an operation above 3, here it throws - [sceAgcBranchPatchSetThenTarget_0300, sceAgcBranchPatchSetElseTarget_0300](../../core/libs/prx/libSceAgc/Patch/src/Branch.cpp) (libSceAgc) - names from their NIDs (`7Wa3aeJgeVU`, `rP5xLdOf26k`); the `(packet, cachePolicy, target, sizeInDwords)` signature is inferred from PPSA28416's calls (a branch packet, 0, an aligned target, 4), with the policy stored where `sceAgcCbBranch` puts it - [sceAgcDcbSet{Cx,Sh,Uc}RegistersIndirectGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - the count argument is ignored: PPSA28416 calls them with leftover register contents (0xf5f5039c), so the real functions likely take none; the size is 20 bytes either way - [sceAgcUpdateInterpolantMapping_0100](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - name from its NID (`k0E7vkgqAuE`); the `(regs, vs, ps)` arguments are the declared ones, behaviour unknown +- [sceFontCharacterGetSyllableStringState](../../core/libs/prx/libSceFont/src/Text.cpp) (libSceFont) - the signature `(textCharacter, int* syllableStringState)` follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs). Measured on PS4 firmware 11.00 `libSceFont.sprx` (not on a PS5): the state is 0 for Latin, CJK, Hangul, Arabic, Hebrew, Devanagari and the other scripts tried, and the library sets 1, 3 or 4 (never 2 on real text) for Thai while it creates the string; a null character or a null result returns `SCE_FONT_ERROR_INVALID_PARAMETER`, and a null character writes 0 to a non-null result. This returns 0 for every character outside the Thai block (U+0E00 to U+0E7F), as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/d2413fc2ebd91d1b7234197c2dd5bd48e1cb6a51/src/libs/libFont.cpp#L1805-L1815) does, and throws for Thai characters, whose cluster states are not modelled. SharpProspero's `SceFontSyllableString` names 0 NonDetection, 1 Starting, 2 Following, 3 Depending and 4 Connecting. PPSA06699 (Hot Shots Golf 2) calls it. - [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping_0100`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function @@ -168,8 +183,9 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetModuleList](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - lists the main program and every loaded `.prx` image in load order; host runtime libraries are left out. The handles are the ids `sceKernelGetModuleInfoFromAddr` reports, not the console's. The `(int32_t* handles, size_t count, size_t* actual)` signature and returning `ENOMEM` without writing `actual` after filling the first `count` handles when more images are loaded follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/kernel/process.cpp#L244-L264); whether the console writes any handles in that case is unknown - [sceKernelGetModuleInfo](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified +- [sceKernelAvailableFlexibleMemorySize](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the 448 MiB flexible budget while they are loaded, for this size and for the flexible maps, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165) maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits -- [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE` and `UMTX_OP_WAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one +- [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded @@ -185,16 +201,19 @@ Throughout the project, every function at every stage either **does exactly what - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [malloc_usable_size](../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` +- [reallocf](../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (an exhausted guest arena, an unknown pointer) throw, as they do for `realloc` - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [sceImeGetPanelPositionAndForm](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) - the output is taken to be the `PositionAndForm` that `sceImeDialogGetPanelPositionAndForm` fills; it is never written, since no panel can be open. That a closed panel is reported before a null output is assumed by analogy with `sceImeSetCaret`, not confirmed on a PS5 title -- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does, and the lookup is Linux only, so on Windows the call throws when the thread has destructors +- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - behaviour follows `sceLibcForceTlsDestructor(int)` in a title's `libc.prx` (disassembly): the calling thread's destructors whose dso belongs to the module run newest first, only if the dso's module is loaded and the dso points to itself, in up to 4 passes. The return type is unknown (that function returns nothing), 0 is returned. The module of each dso is looked up when the destructors are forced, not when they are registered as `libc.prx` does - [acosl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) +- [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception - [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [Dinkumware mutexes and conditions](../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. - [random, srandom, initstate, setstate](../../core/libs/prx/libc/src/Random.cpp) (libc) follow FreeBSD 11 [random.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdlib/random.c): the additive feedback generator on a default 128-byte `TYPE_3` table, `good_rand` seeding, state sizes rounded down to 8, 32, 64, 128 or 256 bytes, and `setstate` returning the previous state or null for a rear position past the degree. `initstate` below 8 bytes returns null without setting errno, as that file and its manual do (glibc sets `EINVAL`). FreeBSD does not lock the generator; here every call is serialised by one lock, so concurrent callers draw the serial sequence between them. A null state throws, where FreeBSD faults. That the console's `libSceLibcInternal` uses this FreeBSD code and not another generator is unverified - [tcgetattr, tcsetattr](../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Other descriptors that are not libSceNet sockets throw. Dungeons (PPSA16064) selects on descriptor 0 +- [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - a descriptor the socket poller reports as unknown that is below `GuestSockets::FirstDescriptor` is polled on the host instead of throwing, except on Windows where it still throws; an invalid host descriptor fails with `EBADF`, and mixed host and socket sets are polled in short alternating slices so neither side waits out the full slice alone. Wobbly Life (PPSA29413) selects on file descriptors while streaming its bundles - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it - [seteuid, setegid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. The ids and privileges a title has on a console are unverified @@ -202,7 +221,7 @@ Throughout the project, every function at every stage either **does exactly what - [pthread_getname_np](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature -- [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature +- [sceAudioInIsPadEmbeddedMic](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted. Whether the console reports an error for a repeated `sceAudioOut2MasteringInit` or for `sceAudioOut2MasteringTerm` without a matching initialization is unknown; both return 0. PPSA01289 creates the audio device of its next world before it destroys the previous one, so it calls Init twice before Term twice, and only logs a non-zero result - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted @@ -221,7 +240,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. The app type and the block are not used, as no endpoint is implemented - AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [sceNpSessionSignalingGetConnectionFromPeerAddress](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - imported by PPSA01289 and added without an implementation; unknown signature -- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; the `(regs, gs, ps)` arguments of `sceAgcCreateInterpolantMapping_0100` come from OpenAGC only - [scePadIsRemoteController](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the name hashes to NID `fCWdlnmB1Ks`; the signature `(int handle, bool* remote)` comes from PPSA16064, which zeroes a byte, passes its address after `scePadGetControllerInformation` and ignores the result. The error codes follow the other pad calls. Controllers are local devices, so the answer is always false - [scePadSetAngularVelocityBiasCorrectionState](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw @@ -278,13 +297,15 @@ Throughout the project, every function at every stage either **does exactly what - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw - [sceSystemServiceParamGetInt](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format, time format, time zone, summer time and enter button stay fixed +- [sceNgs2ReportRegisterHandler, sceNgs2ReportUnregisterHandler](../../core/libs/prx/libSceNgs2.native/src/Unimplemented.cpp) (libSceNgs2.native, NIDs `uBIN24Tv2MI`, `nPzb7Ly-VjE`) are throwing stubs. The signatures (report type, handler, user data, out handle) and (report handle) follow [SharpProspero](https://github.com/SvenGDK/SharpProspero) and [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_report.h`. No report is produced yet, so registering a handler cannot be honoured. PPSA04540 imports both and registers a CPU load handler (type 5) - [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` - [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero - [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw - [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet - [sceNgs2ParseWaveformFile](../../core/libs/prx/libSceNgs2.native/src/Atrac9.cpp) (libSceNgs2.native) - the signature `(path, offset, info)` is taken from the call site of PPSA21402, which sets only three argument registers; the file is read from `offset` to its end, the data and block offsets are taken to be relative to the start of the file, not of `offset`, and an unreadable file throws. The audio unit fields of a 16-bit PCM waveform are assumed to be one sample frame per unit and per frame; `INVALID_WAVEFORM_DATA` for a `data` chunk that runs past the buffer or ends inside a frame is assumed, and looped PCM and other PCM formats throw +- [sceSaveDataDirNameSearch](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - the sort keys (0 directory name, 1 user parameter, 2 blocks, 3 modification time, 5 free blocks) and orders (0 ascending, 1 descending) follow shadPS4, not a hardware run; saves with equal keys keep directory-name order before a descending reverse. Every native save reports `SAVE_DATA_BLOCKS_MAX` blocks, as `sceSaveDataGetMountInfo` does, so the two block keys keep directory-name order. Keys other than 0, 1, 2, 3 and 5 (including 4) and orders above 1 return `SCE_SAVE_DATA_ERROR_PARAMETER`; shadPS4 and KytyPS5 only reject keys above 5, so key 4 is rejected here because no source names it - [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown -- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature +- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature; returns `EOPNOTSUPP` like `sceTextToSpeech2Initialize` - [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature - [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title @@ -322,8 +343,10 @@ Throughout the project, every function at every stage either **does exactly what - [sceSharePlayInitialize](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - the library keeps no state, so the heap is not used. A null heap is accepted: PPSA11386 passes a null heap and 0x1800 bytes and stops on any non-zero result. The error for a zero size is unknown, so it throws - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) - [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) follows [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c) only, not confirmed on hardware: the `dbOlWdppb4o` transform without the identity tail, so it writes one register per pixel shader input and none for a pixel shader without inputs, and a null pixel shader throws. For an `is_f16` 1 or 3 input whose `default_value` and `default_value_hi` differ it throws: OpenAGC takes `DEFAULT_VAL` from `default_value_hi`, while KytyPS5, sharpemu and `dbOlWdppb4o` here take it from `default_value` - [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) uses the existing unavailable-registration state and leaves caller memory untouched. Its four arguments (uint32 handle output, uint32 owner, descriptor pointer, uint32 count) follow the original PPSA34502 / 01.010.000 codzm call. The caller builds 40-byte descriptors and explicitly accepts 0x8A6C9018 with an invalid output handle. Enabled registration and console argument-validation precedence are unverified. - [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown +- [NGG vertex workgroups](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - type-2 PRIMGEN programs using DS instructions or `s_barrier` are prepared with the draw's routing and primitive registers on the existing mesh path. The workgroup requirement follows [Mesa's NGG lowering](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_ngg.c#L1135); the mesh path supplies LDS and two guest lanes per host invocation for wave64 on a 32-lane host subgroup. Console routing has not been measured. Passthrough and tessellation keep their existing paths; unsupported mesh assembly and subgroup budgets throw. - [Shader header copies](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a header bound for a draw or dispatch at another address than the registered one is accepted when its fixed fields match the registered header except `user_data`; the registered header's metadata is used. PPSA21567 binds such a copy of a vertex shader header with `user_data` set to null. Whether the console reads the copy's `user_data` is unknown - [HtileFillCovers](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a fill is taken to cover a depth surface's whole HTILE when it spans 32 KiB per 1024x512 pixel block from DB_HTILE_DATA_BASE, the pipe-aligned SW_64KB_Z_X metadata extent AMD PAL's gfx10 addrlib gives for GB_ADDR_CONFIG 0x00100044 (1-8 samples, D16/D32); other metadata layouts are not decoded. A partial fill is ignored (the surface keeps its contents); not measured on the console. - [Depth fast clear through HTILE](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - a surface cleared by an HTILE fill is cleared to DB_DEPTH_CLEAR, also when it is next sampled. For a depth-only HTILE the clear word also carries a value (radv writes `minz = maxz = round(depth * 0x3fff)`, `HTILE_Z_CLEAR_REG` in Mesa's `src/amd/common/ac_descriptors.h`, and allows TC-compatible fast clears only to 0.0 or 1.0); which of the two the texture unit returns for a cleared tile was not measured. @@ -335,41 +358,58 @@ Throughout the project, every function at every stage either **does exactly what - [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does - [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty - [scePlayerReviewDialogOpen, scePlayerReviewDialogGetResult](../../core/libs/prx/libSceCdlgPlayerReview/Export.cpp) (libSceCdlgPlayerReview) - the PS5-only dialog has no public reference: the parameter is not read and the result is assumed to start with the 32-bit common dialog result, as `sceWebBrowserDialogGetResult` assumes -- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so the call waits for the timeout and returns 0 events, and a wait without limit throws +- [sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the timeout is assumed to be in microseconds and a negative timeout to wait without limit; the invalid-value error for a null handle or event array or a non-positive event count is assumed, not measured. No request is ever pending, so a wait returns 0 events when its timeout passes - [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 - [sched_get_priority_max, sched_get_priority_min](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `SCHED_OTHER` (2) reports the same 256 to 767 range as `SCHED_FIFO` and `SCHED_RR`; the console's answer is unverified. FreeBSD accepts `SCHED_OTHER` with its own range, and shadPS4 rejects it with `EINVAL`. `pthread_setschedparam` ignores the policy, so a priority taken from this range applies +- [inet_pton, inet_ntop](../../core/libs/prx/libkernel/Socket/src/AddressConversion.cpp) (libkernel) - the text forms are assumed to be those of FreeBSD 11.4 and are unverified on a console: `inet_pton` accepts only four decimal parts without leading zeros for `AF_INET` and the RFC 4291 forms for `AF_INET6` (one `::` standing for at least one group, a dotted-quad tail only as the last 32 bits, no zone id), and `inet_ntop` prints lowercase hex with the first longest run of two or more zero groups compressed, and a dotted tail only for `::a.b.c.d` and `::ffff:a.b.c.d`. They no longer depend on the host resolver library - [gethostbyname](../../core/libs/prx/libScePosixForWebKit/Resolver.cpp) (libScePosixForWebKit) - numeric IPv4 forms, trailing whitespace and 64-bit accumulation follow FreeBSD 11.4 [inet_aton](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/inet/inet_addr.c); clearing the resolver error on numeric success follows [fakeaddr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/net/gethostnamadr.c). Exact PS5 behavior is unverified; hostname lookup still uses the host resolver - [sceAppContentGetAddcontDownloadProgress](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) - imported by PPSA08755, which fails to load without it; added without an implementation because its behavior is unverified. [SharpProspero](https://github.com/SvenGDK/SharpProspero) lists `(uint32_t serviceLabel, entitlementLabel, SceAppContentAddcontDownloadProgress*)`, but no PS5 reference gives the progress layout or the error codes +- [_sceFiberGetThreadFramePointerAddress](../../core/libs/prx/libSceFiber/Export.cpp) (libSceFiber, NID `0dy4JtMUcMQ`) forwards to `sceFiberGetThreadFramePointerAddress`; that the argument list matches is inferred from the export without the underscore, as for the underscore Ult exports, not measured + ### Functional +- [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. + +- [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. +- [ftrylockfile](../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it + - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [arc4random](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) returns four bytes from the same thread-local host `std::random_device` as `arc4random_buf`, uniform over the 32-bit range like FreeBSD 11.4's RC4 keystream in [arc4random.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/arc4random.c). The console's generator and its reseeding are unknown; `arc4random_uniform`, `arc4random_stir` and `arc4random_addrandom` are not in the module's export set and are absent +- [fnmatch](../../core/libs/prx/libScePosixForWebKit/Export.cpp) (libScePosixForWebKit) follows FreeBSD 11 [fnmatch.c](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/gen/fnmatch.c) in the C locale: each byte is one character, `FNM_CASEFOLD` folds ASCII letters only, and bracket ranges compare byte values. `[:class:]` names are not character classes: as in FreeBSD 11 through 14, `[[:digit:]]` is the set of `[`, `:`, `d`, `i`, `g`, `t` followed by a literal `]` (classes were added to `fnmatch.c` after 14). Multibyte characters and locale collation are not modelled. A null pattern or string throws - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning +- [_Cnd_signal, _Cnd_init_with_name](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known - [libSceZlib](../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [Windows file identity](../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails - On Linux, [direct memory](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. +- [Partial rect-list primitives](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. +- [Unexported pixel inputs](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Compute LDS beyond the device's shared-memory limit](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. +- [AGC buffer allocations](../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline - [sceAvPlayerGetVideoData](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) in sync mode `SCE_AVPLAYER_AV_SYNC_MODE_NONE` skips queued frames that the audio clock has already passed while the title pulls audio, so a title that requests video slower than the film's frame rate keeps the picture with the sound. Whether the console hands out every frame in this mode is unknown. PPSA12544 requests 7-19 frames per second for a 30 fps cinematic - [`APS5_THREAD_PRIORITY=rt`](../../core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp) (libSceAgcDriver) lowers the process's hard `RLIMIT_RTTIME` when it is above `APS5_THREAD_RTTIME_US` or rtkit's `RTTimeUSecMax`, because [rtkit](https://github.com/heftig/rtkit/blob/c295fa849f52b487be6433e69e08b46251950399/rtkit-daemon.c#L592-L637) (`verify_process_rttime`) reads the hard limit from `/proc//limits` and refuses real-time scheduling when it is higher. An unprivileged process cannot raise the hard limit again, so it stays lowered for the process and its threads even when rtkit then refuses the request (burst limit, user or start-time checks) and the thread throws. rtkit's `MaxRealtimePriority` and `RTTimeUSecMax` are read first, so an unreachable rtkit or a priority above its maximum leaves the limit untouched. The lowered limit only matters for threads that later run under a real-time policy - On a device without VK_EXT_robustness2's `nullDescriptor`, such as MoltenVK, a null image descriptor in a typed heap is a [placeholder image](../../core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp) of that heap's type, 1×1 and cleared to zero, which every null descriptor of the heap shares. Reads give zeros, as `nullDescriptor` allows, but stores and atomics to it are kept, so a later read of a null storage image can return them, and a depth comparison compares against depth 0. A heap whose placeholder the device cannot create, for example 1D or 3D depth, 64-bit atomics without 64-bit image atomics, or multisampled storage without `shaderStorageImageMultisample`, throws when a null descriptor reaches it. - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw +- [sceHttpAbortWaitRequest, sceHttpWaitRequest](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - the abort takes only the epoll handle, as in [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); behaviour and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4), not confirmed on a PS5 title or hardware. An abort stays set: every later wait on the handle, a poll included, returns `SCE_HTTP_ERROR_ABORTED` (0x80431080) until the handle is destroyed. A wait woken by `sceHttpDestroyEpoll`, or by the `sceHttpTerm` that ends the last context, returns 0 events. The epoll calls return `BEFORE_INIT` (0x80431001) while no `sceHttpInit` context is alive and `INVALID_ID` (0x80431100) for an unknown context or handle, and `sceHttpTerm` returns the same errors instead of succeeding for any id. Handles are pointers, so a stale handle whose address a newer epoll reuses is taken as that epoll - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [nan and nanf](../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. +- [Draw input snapshots](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): `PrepareDrawBindings` caps read-only input buffer snapshots at 64 MiB (overridable via `APS5_DRAW_SNAPSHOT_MAX_MIB`). Larger inputs (streaming heaps, bindless pools) or draws exceeding the cumulative budget skip the per-draw snapshot and bind in place, avoiding multi-gigabyte CPU copy stalls (such as Ratchet & Clank: Rift Apart PPSA01474 stalling at frame ~380 copying 36 GB every 5 s). If the title rewrites those large ranges on the CPU before GPU completion, the in-place binding observes the modification, as verified by `drawSnapshotCapTests`. - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: @@ -379,13 +419,15 @@ Throughout the project, every function at every stage either **does exactly what - [Pending storage-image results](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) are stored when something reads their memory, not when the GPU work that wrote them completes. A title that waits for that work, frees the image and reuses its memory can have the new data overwritten by the late store (#1970). `APS5_STORE_AT_FLIP=1` stores every pending result at each flip; storing them at the title's GPU waits (labels, fences) is unimplemented. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. - [Storage-image units under a Linux import window](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) that the window stamped as a possible CPU store (`BlockMaybeWritten`) are compared with the bytes the image was uploaded from or written back as, and are unchanged when those still match. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Images tracked by layer instead of by 64 KiB unit still read the window as a change and upload again. -- [shm_open](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) creates only anonymous objects (`SHM_ANON`), each backed by a host anonymous file: a `memfd` on Linux, a delete-on-close temporary file on Windows. The flag checks follow FreeBSD 11's `kern_shm_open`; the mode is ignored. Named objects throw. The descriptor cannot be mapped yet: `mmap` accepts only private anonymous mappings and fails a descriptor mapping with `EOPNOTSUPP`, so the known caller (the bundled Mesa in PPSA59322, which truncates the object and maps it `MAP_SHARED`) gets that failure +- [Shared memory](../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. - [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the socket's send buffer size (`SO_SNDBUF`) in `data`, not the free space left in it, as Windows has no query for that space. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries +- [Content export sessions](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport): `sceContentExportStart` hands out positive session ids, at most 10 live, which `sceContentExportFinish` closes. The signatures follow [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) and the limit `MaxExecution` [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Content/ContentExport.cs); the parameter layouts follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/443f022450f6857256e780daa3d5a14029a748f7/src/core/libraries/content_export/content_export.h). The host has no media library, so `sceContentExportFromDataWithThumbnail` throws once the session is checked, before calling either data callback, and no export ever runs. `sceContentExportGetProgress` and `sceContentExportCancel` throw for a live session, as their result with no running export is unknown. An eleventh `sceContentExportStart` and `sceContentExportTerm` with live sessions throw: whether the console answers `EXECUTION_MAX` or `BUSY` there, or ends the sessions, is unverified. Session id values and error precedence are unverified - [Runtime-loaded modules](../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console +- [sceKernelAioSetParam](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way @@ -395,6 +437,7 @@ Throughout the project, every function at every stage either **does exactly what - [makecontext](../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) on Windows pins its stack writable until a later `makecontext` stack overlaps it, since nothing tells the library when the guest frees the stack - [setcontext](../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection +- The Windows libc exception unwinder reads the project's `.ehmeta` and `.ehfram` tables, not PE `.pdata`/`.xdata`. WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll` have empty `.eh_frame` sections, so guest exceptions cannot currently cross their frames (for example, the `std::call_once` path through `pthread_once`); `RtlVirtualUnwind` stepping and Windows cleanup handlers are not implemented - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask @@ -406,13 +449,17 @@ Throughout the project, every function at every stage either **does exactly what - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. - [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) follows FreeBSD libthr (states and types 0/1 and 0/2, `PTHREAD_CANCELED` 1, the `_pthread_cleanup_info` node layout), not measured on the console. A request is acted on only in `scePthreadTestcancel`, the condition variable, `sem` and `scePthreadSem` waits, `scePthreadJoin` and the sleeps, and their POSIX names; file and socket calls are not cancellation points. A sleep is not interrupted: the thread exits when it ends. The asynchronous type acts like the deferred one, except at the two set calls. - [cpuset_setaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. Bits outside `sceKernelGetAvailableCpumask` are dropped, and a mask with none of those bits fails with `EDEADLK`, as FreeBSD does when the new set has no CPU in common with the parent set +- [cpuset_getaffinity](../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - only the calling thread or the calling process is handled. For the process the mask is the union of the affinities of the live guest threads, as [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/kern_cpuset.c#L1150-L1156) ORs the mask of every thread in the process; threads the guest did not create through libkernel are not included, and the console result is unverified - [dup and dup2](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) throw for the combinations they do not model: `dup2` between a guest socket and a host descriptor in either direction, and on Windows duplicating a directory descriptor. Guest sockets share one socket object between the duplicates and the descriptor numbers come from the guest socket counter, not the lowest free number as in FreeBSD `kern_dup`. The errno values for descriptor exhaustion are not verified against the console. - [Cancellation coverage](ARCHITECTURE.md#thread-cancellation) checks deferred request handling and recovery, not full asynchronous support or console ABI compatibility. Cancellation during file/socket I/O, sleep interruption, cancellation versus exit/detach, condition signal conservation under cancellation, and safe rejection of non-null invalid or reclaimed thread handles remain unverified. No console title or homebrew establishes the cleanup NIDs or buffer layout. Accepted asynchronous mode currently has no diagnostic for its deferred behavior. - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. - [Packed horizontal gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. - [waitpid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) returns -1 with `ECHILD` (`EINVAL` for unknown options): nothing here creates a child process; child processes would need a real `waitpid`. +- [execv](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported, the same as `execvp`. PPSA97358 imports it and fails to load without it. +- [kern.proc.pathname](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. +- [machdep.cacheline_size](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. - [execvp](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: replacing the process image is not supported. PPSA85300 imports it and fails to load without it; its `fork` and `vfork` fail with `ENOSYS` ([libc_missing.c](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/ps5/compat/libc_missing.c#L290-L292)), so the child process that would call it is never created. -- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, but files are still created with the host process's mask +- [fork](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [MIMG BY2/BY4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. - [2D stores to 1D storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). @@ -447,7 +494,7 @@ Throughout the project, every function at every stage either **does exactly what - [EXEC read as a value](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. - Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. -- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1); other such reads throw. +- On a host subgroup narrower than the wave, [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) of a constant lane outside it builds only as lane 63 of a half-wave scan (DPP row_shr 1/2/4/8, `v_permlanex16_b32`, EXEC -1) or, in a program whose entry EXEC is every lane (vertex, local and tessellation stages), as 0 when the value is 0 in lanes 32-63; other such reads throw. A value is 0 there when it is built from 0 through selects under a lane index (lane id or `v_mbcnt` of -1) below at most 32, `v_cmp_ne_u32` against 0, OR, DPP and permlanes; the counts in `s3` give such a bound on a 32-lane host. On a 32-lane host a half-wave scan may also run under `s_mov_b64 exec, -1`, use bound_ctrl:1 steps for an OR, add or unsigned max, and take keys that are 0 in lanes 32-63; its lane 31 is then the subgroup reduction of the host's invocations (a shuffle of the scan on a device without subgroup arithmetic): idle invocations add the identity, where the console would add the keys of the lanes `s3` claims. - [NGG mesh entry](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) uses the guest wave size (32 or 64) for wave indices, per-wave vertex and primitive counts, and the workgroup wave count in `s3`. The four-bit wave fields follow Mesa's [get_wave_id_in_tg, get_tgsize and get_thread_id_in_tg](https://chromium.googlesource.com/chromiumos/third_party/mesa/+/refs/heads/stabilize-13982.70.B-chromeos-amd/src/gallium/drivers/radeonsi/gfx10_shader_ngg.c); partial waves and groups exceeding 15 waves throw. Indexed and non-indexed wave32 mesh rendering with 32, 64 and 128 threads was checked by pixel readback on Windows with an NVIDIA RTX 4070 Laptop GPU. The guest register values have not been compared with console hardware. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric` and throw without it, as fixed-function interpolation cannot split a 32-bit attribute. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64, so these instructions need the Float64 capability. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the arithmetic in every mode against the hardware rows on any device with Float64. @@ -461,6 +508,7 @@ Throughout the project, every function at every stage either **does exactly what - A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. +- [WRITE_DATA with DST_SEL 3](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. - The [shader float mode](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request (#981). `IEEE_MODE` is read by `v_min_f32`, `v_max_f32`, `v_min3_f32`, `v_max3_f32` and `v_med3_f32`, their f16 forms, `v_min_f64` and `v_max_f64`, `IEEE_MODE` and the `FLOAT_MODE` output denormal bits by the output modifier, `DX10_CLAMP` by the clamp modifier, `FP16_OVFL` by every instruction with an f16 result (see its entry), and the whole mode by the 16-bit interpolation instructions (see their entries). With IEEE=1 a signaling NaN operand returns that NaN quieted (the first operand checked first, min3/max3 as two nested pairs, med3 as min3 when any operand is NaN), measured on RDNA2 (gfx1035) over every combination of ±0, ±1, ±inf, denormals, quiet and signaling NaNs with f32 denormals kept and flushed. IEEE=0 and unknown modes keep the previous f32 lowering, which matches IEEE=0. The f16 forms choose their NaN results on the bits: a NaN operand returns the other operand unchanged (src0 when both are NaN), and with IEEE=1 a signaling NaN operand returns that NaN quieted, in the same order as f32. `v_min_f64` and `v_max_f64` quiet a signaling NaN operand the same way with IEEE=1; with IEEE=0 they return src0 unchanged when src1 is NaN, otherwise src1 when src0 is NaN. Both were measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f32 denormals kept and flushed and f16/f64 denormals kept; RDNA2 has no f64 min3, max3 or med3. Unknown modes follow IEEE=0. `v_min_f16` and `v_max_f16` flush each denormal input to its signed zero when bit 0 of the f16/f64 denormal field is clear (0 and 2); fields 1 and 3 preserve inputs, even when output flushing is enabled. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1, including signed zeros, NaNs and neg/abs modifiers. Unknown modes keep f16 denormals. With f16/f64 denormals flushed the hardware also flushes denormal inputs of f64 min/max, which those lowerings still do not. NaN quieting in f16 arithmetic (the unary instructions and the f32 arithmetic follow it, see below) and `FLOAT_MODE` (f32 denormals flushed before the min/max compare in Astro's Playroom's 0xc0) still follow a single mode, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none. - [v_ldexp_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f16 denormal field: bit 0 preserves the half input and bit 1 the half result. Otherwise a denormal input is flushed to its signed zero, and a result whose exact scaled value is below the minimum normal (2^-14) is flushed to its signed zero before rounding, so `v_ldexp_f16 0x07ff, -1` gives +0 instead of 0x0400. Measured on RDNA2 (gfx1036) with round to nearest, IEEE 0 and 1 and denorm16 0..3, over every half magnitude 0x0000..0x0fff of both signs scaled by 2^-1 and 2^0 (16384 rows per mode): no mismatch. NaN and infinity handling and destination high-half preservation are unchanged. Shaders without a known float mode keep the previous lowering. @@ -468,21 +516,21 @@ Throughout the project, every function at every stage either **does exactly what - The [dispatches that follow a compute queue's wait](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchAhead.cpp) (libSceAgcDriver), up to the next wait that memory does not satisfy, a packet that writes guest memory other than a label, or a conditional packet, are resolved (memory captured, shader recompiled) when the wait passes, as the CP reads them right after it; the graphics queue is exempt (`APS5_GROUP_CAPTURE_ALL_QUEUES=1` includes it). The device still reads guest memory when the dispatch is issued; when that memory no longer matches the capture, the dispatch is resolved again from current memory. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted only with IEEE=1, as measured at IEEE=0 too; a positive denormal src2 counts as positive); with f32 denormals flushed it follows the f32 denormals entry below, and denormal modes 1 and 2 throw. - [v_mad_legacy_f32 and v_mac_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140, VOP2 0x06) round the product to f32 before the add, give a +0 product for a zero factor, and flush their f32 denormal sources, their product and their result to a zero of the same sign in every f32 denormal mode, an exception to the `FLOAT_MODE` model in [f32DenormalFlushFor](../../core/shader/recompiler/Translation/src/VectorInstructions.cpp). Measured on gfx1010 (RX 5700 XT) with `tools/hw-oracle/template.s`, assembled by the AMD Windows driver's `amd_comgr_2.dll` and launched through its `amdhip64_6.dll` instead of `oracle.c` and ROCr, over the 32 rows of the five `FmaLegacy.cpp` and the four `MacLegacy.cpp` encodings: identical in wave32 and wave64 at IEEE 0 and 1 with f32 denormal modes 0 to 3, except that the two signaling NaN rows are quieted only with IEEE=1 (#1281, #1290). A sum of two zeros takes its sign on the bits (+0 unless both are -0), as lavapipe returned -0 for +0 + -0. The PS5's gfx1013 is not measured; LLVM decodes the encoding as `v_mad_legacy_f32` there, as on gfx1010. RDNA2 differs: on gfx1031 (RX 6700 XT, measured by pandather in #1281) the encoding is `v_fma_legacy_f32` and fused, with the same zero rule and denormal-source flush, so an RDNA2 measurement is not evidence for the console. -- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. Only f16 denormals kept with round to nearest even is translated; another f16 mode throws, as for packed f16. `FP16_OVFL` set is not measured. +- [v_fma_mixlo_f16 and v_fma_mixhi_f16](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) round the exact product-sum once to f16 with the f64 arithmetic of v_interp_p2_f16, so they need the Float64 capability. On RDNA2 (gfx1036) they follow the `FLOAT_MODE` f16 round and denormal fields, not the f32 round field: -1.5 * 2^-24 + 2^-126 from f32 sources (0xbfc00000, 0x33800000, 0x00800000) gives 0x8001 with f16 denormals kept and round to nearest even, 0x8000 with f16 denormals flushed and 0x8002 with f16 round toward -infinity. With f16 denormals kept, every f16 rounding mode is translated: the exact product-sum is rounded once in the mode's direction. All four modes match RDNA2 (gfx1036) on the 2,233 rows of the #2631 sweep, including `FLOAT_MODE` 0xcc (toward zero, used by ps5-opengl) and the exact cancellations toward -infinity; the PS5's gfx1013 is not measured. A result beyond 65504 becomes infinity or 65504 as IEEE 754 gives for the mode. Flushed f16 denormals still throw, and packed f16 still throws in every mode but round to nearest even with f16 denormals kept. `FP16_OVFL` set is not measured. - [Signaling NaN inputs](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) of the f32 and f16 unary instructions (`v_sqrt/rcp/rsq/log/exp/sin/cos/fract/floor/ceil/trunc/rndne/rcp_iflag/frexp_mant`), `v_ldexp_f32/f16`, `v_frexp_mant_f16`, `v_div_fixup_f32/f16` and `v_mullit_f32` are returned quieted only with IEEE=1 and unchanged with IEEE=0, measured on RDNA2 (gfx1035) in both modes, and so are those of the f64 forms (`v_sqrt/rcp/rsq/fract/floor/ceil/trunc/rndne/frexp_mant/ldexp/div_fixup_f64`) and `v_cvt_f64_f32`, measured on RDNA2 (gfx1036) in both modes; shaders without a known float mode follow IEEE=0. `v_pack_b32_f16` quiets each signaling NaN half by the same rule, measured on RDNA2 (gfx1036) in both modes. A signaling NaN result of `v_cubesc/cubetc/cubema_f32` is quieted by the same rule and keeps the sign the instruction gives it, measured on RDNA2 (gfx1036) in both modes. `v_add/sub/subrev/mul/fma/fmac/fmamk/fmaak/mul_legacy_f32` and `v_fma_mix_f32` with f32 sources choose their NaN result on the bits, measured over 1000 operand triples in both modes: the first NaN source in operand order after its modifiers (the subtrahend negated, as the hardware adds its negation), quieted only with IEEE=1, or the default NaN 0xffc00000 for an invalid operation; in the fma forms an invalid product (inf * 0) gives the default NaN even when src2 is a NaN, and in the legacy forms a zero factor makes the product 0 even when the other factor is a NaN (a NaN src2 still wins). `v_mad_legacy_f32` and `v_mac_legacy_f32` follow the same rule with their separately rounded product, as measured on gfx1010 (#1281, #1290): a NaN source quieted only with IEEE=1, inf - inf giving 0xffc00000, the zero rule hiding a NaN factor and a NaN src2 winning. The f16 and f64 arithmetic, `v_fma_mix_f32` with f16 sources and the separately rounded mad/mac forms (not measurable on RDNA2) follow the same rule on hardware but their NaN bits are left to the driver. `v_cvt_f16_f32`, `v_cvt_f32_f64`, `v_dot2_f32_f16`, `v_fma_mixlo_f16` and `v_fma_mixhi_f16` always quiet and `v_div_scale_f32` never does: the f32 signaling NaN 0x7f812345 as src0 or src2 gives 0x7e09 from mixlo and mixhi at IEEE=0 and IEEE=1 on RDNA2 (gfx1036), where `v_fma_mix_f32` gives 0x7f812345 at IEEE=0 and 0x7fc12345 at IEEE=1. The payload of an f16 NaN source of mixlo and mixhi is left to the driver's f16 to f32 conversion, which NVIDIA replaces. - [Scalar f16 add, sub, subrev and mul denormals](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field at nearest-even: bit 0 preserves inputs and bit 1 preserves the result; otherwise a denormal is flushed to its signed zero. Measured on RDNA2 (gfx1036) with IEEE=0/1 and FP16_OVFL=0/1, over 32 operand pairs in all four denormal modes, including signed zeros, denormals, minimum normals, infinities and NaNs. Multiply detects tininess after rounding to 11 significant bits with an unbounded exponent, before gradual half packing: `0x3bff * 0x0400` flushes, while `0x3c01 * 0x03ff` rounds to `0x0400` and is kept. Directed rounding and ternary arithmetic keep their previous denormal lowering. Shaders without a known float mode keep the previous lowering. Scalar `v_sub_f16`/`v_subrev_f16` share `float16Binary` with add/mul and were measured on gfx1036 over the same operand set as add (1156 non-NaN-input rows per mode); NaN result bits still depend on the driver as recorded above. - [Packed f16 NaN results](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding or that flushes f16 denormals throws. -- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. +- [f32 denormals](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; f16 inputs retain their previous conversion and denormal handling. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` is the exception: it preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. - [Overlay output bus](../../core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp) (libSceVideoOut) - flips on a port opened with `SCE_VIDEO_OUT_BUS_TYPE_OVERLAY` complete and signal their events like main-bus flips, but their buffers are not shown: the overlay is not composited over the main output. Presenting them as whole frames made the window alternate between the two buses - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. - [10_11_11 unorm color targets](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) uses Vulkan's last-vertex mode for native primitives. Bit 19 follows [opengnm's `flatshade_first` mapping](https://github.com/blackbearreloaded/ps5-opengl/blob/db752da4c89380af228497b5b6852163a4e61069/src/gallium/ps5/ps5_screen.c#L2244); vertex selection follows [Vulkan](https://docs.vulkan.org/spec/latest/chapters/drawing.html#drawing-primitive-topologies), without console measurements. Last mode throws without `VK_EXT_provoking_vertex`, for generated rectangle, mesh or tessellation primitives, explicit per-vertex flat inputs, and triangle strips with per-vertex inputs, whose [ordering depends on the device](https://docs.vulkan.org/refpages/latest/refpages/source/VkPhysicalDeviceFragmentShaderBarycentricPropertiesKHR.html). -- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_log_f32 and v_log_f16](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Dual-source blending](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. -- [Ordered not-equal compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual` with both operands checked for NaN on their bits instead of `OpFOrdNotEqual`; the two agree for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of the compare feeds a ballot on a 32-wide subgroup (#825) +- [Ordered not-equal, less-than and greater-than compares](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [sce::Job::JobManager](../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Only CPU sequences exist: `initialize` and `calculateRequiredMemorySize` with compute sequences, and both compute dispatches, are not implemented, and `createCompute` and `destroyCompute` return the errors the original returns when no compute sequence was configured. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. @@ -500,6 +548,10 @@ Throughout the project, every function at every stage either **does exactly what - [HTILE depth fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. - [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Multisampled CMASK](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. +- [Multisampled CMASK state](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. +- [Multisampled targets in CB passes](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve is modeled. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. +- [Multisampled target guest access](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw. - [Storage image write-back](../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. - [16-bit float display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. @@ -530,7 +582,7 @@ Throughout the project, every function at every stage either **does exactly what - The [clamp modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 only when f16 denormals are preserved (bits 7:6 = 3), measured on RDNA2 (gfx1036). With f16 denormals flushed (bits 7:6 = 0), or flushed on one side (modes 1 and 2, not measured), the hardware also flushes the f16 result and the directed path does not model that, so those shaders keep round-to-nearest-even. The `clamp` modifier with directed rounding is not measured. -- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255; that measurement also returns 0 for 8-bit UNORM, which this path still compares. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D, 2D array and cube views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. 8-bit sRGB (`Format8Srgb`, `Format8_8Srgb`, `Format8_8_8_8Srgb`) returns 0 for every compare function, measured with `image_sample_c_lz` on gfx1035 over references -0.5..1.5 and texel codes 0..255; that measurement also returns 0 for 8-bit UNORM, which this path still compares. A bilinear cube of those 8-bit sRGB formats without `DISABLE_CUBE_WRAP` returns 0 for the same reason; float, unorm and snorm bilinear cubes without that bit still throw. Bilinear weights are full float, not the hardware's fixed-point subtexel weights - [Negative color-texture comparison wrap indices](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. - [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. @@ -549,13 +601,15 @@ Throughout the project, every function at every stage either **does exactly what - [Buffer loads without GLC or DLC](../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) are not coherent in a program that has GLC or DLC buffer accesses and no `buffer_gl0_inv`, `buffer_gl1_inv`, `s_gl1_inv` or `s_dcache_inv`: only its GLC and DLC loads, stores and atomics use the `Coherent` buffer view, so a plain load can return a value cached before another work-group's write. This follows the [LLVM AMDGPU memory model for GFX10](https://llvm.org/docs/AMDGPUUsage.html#memory-model-gfx10-gfx11), in which a load-acquire is a GLC DLC load followed by `buffer_gl1_inv` and `buffer_gl0_inv`; it is not measured on the console or on RDNA hardware. - [DS src2 operations](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) (`ds_*_src2_*`, GFX10.1 only) read `DS[B]`, with `B = A + 4 * sext(offset1[6:0]:offset0)`, and then update `DS[A]` with the matching 32-bit atomic, so the read of `B` is not atomic with the update of `A`. The form that takes the offset from the address (`offset1[7]` set), the 64-bit forms and GDS throw: whether the LDS address keeps `A[31:17]` in that form, and whether the 64-bit forms scale the offset by 4 or 8, is not documented. `ds_min_src2_f32`, `ds_max_src2_f32` and `ds_add_src2_f32` throw: on RDNA2 (gfx1035, the oracle's hardware, which has no src2 opcodes) `ds_min_f32`, `ds_max_f32` and `ds_add_f32` ignore `IEEE_MODE` but depend on whether f32 denormals are flushed (28, 28 and 12 of 196 special rows differ), and that is not modelled for LDS float atomics yet (#981). - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached -- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); positional arguments, the `'` flag and `%m` throw when a format requires the guest formatter +- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, so a narrow `%p` there still follows the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`); `%m` and argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter - [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [The scanf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) hands `%i` to glibc on Linux, which since 2.38 reads the C23 `0b`/`0B` binary prefix when the library is built as GNU C++: `sscanf("0b101", "%i", &value)` stores 5 where C17 stores 0 and stops at the `b`. The strtol family stops at the prefix as C17 does - [The scanf family](../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close` +- [UDP recv/recvfrom](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified. - [read, write and vector I/O](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, stdio) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [Socket error numbers](../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [msync](../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. @@ -571,30 +625,41 @@ Throughout the project, every function at every stage either **does exactly what - [Running application ID](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `sceSystemServiceGetAppIdOfRunningBigApp` reports 1 as the running title's app ID. The value is AnyPS5's own: the console's shell assigns app IDs and their values are not known here, so other calls that take an app ID must use the same one. `sceSystemServiceKillApp` with that ID and the arguments -1, 0 and 0, which [SwanStationPS5](https://github.com/darkxex/SwanStationPS5/blob/4cdc981d1a64ffb7f2af73f7a27a9e91a3cb5eb4/src/main.c#L1050-L1058) passes to close itself and [ps5-payload-dev's hbldr](https://github.com/ps5-payload-dev/websrv/blob/46374edee6627885abac717de24da7a1d73af6f1/src/ps5/hbldr.c#L425-L435) passes to close the running game, ends the title as its own `exit(0)` would: the guest's shutdown and `atexit` handlers run and the process exits with status 0. On the console the call returns and the shell then ends the title, and whether those handlers run there is unknown; here there is no shell, so the call does not return. Another app ID or other arguments throw. - [sceNpCheckNpAvailability](../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests - [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw. -- [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. +- [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation. - [localtime, mktime](../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does, and passes every other conversion to the host's `strftime`. On Windows the conversions the CRT lacks (`%P` and the `%-`, `%_` and `%0` padding flags) still produce nothing, and the padding flags are not supported on the five new conversions at all (`%-k`, `%_k` and `%0k` go to the host), an unknown conversion, a doubled `E` or `O` modifier (`%EE`) or a trailing lone `%` returns 0 where FreeBSD prints `%` and the character, and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null. - [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it - [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct. - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. +- [Relative vector addressing](../../core/shader/recompiler/Translation/src/ControlFlowInstructions.cpp) (`v_movrels_b32`, `v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) keeps every vector register from the lowest base register the program addresses relatively up to the last register the program names in a zero-initialised function-scope array per invocation (`LoadVectorWindow`/`StoreVectorWindow`, one element per register), so an indexed access is one load or store instead of a compare-and-select chain over the window; the host compiler decides whether that array stays in registers or spills to scratch, which is not measured. A register in the window the program never wrote reads 0, as an unwritten SSA register does; the console's value is undefined. M0 is unsigned: an index at or past the window reads `v0` and drops its write, and `v_swaprel_b32` writes neither side when either is out of range, as measured on RDNA2 (gfx1035, `agc_driver_relative_indexing`). The window ends at the last register the program names rather than at the shader's VGPR allocation, which the recompiler does not receive, so a relative write past the named registers but inside the allocation is dropped where the hardware keeps it for a later relative read. Killzone: Liberation's PSP vertex program (PPSA11142, 3176 ISA words, 104 relative accesses over 154 registers) went from 649,515 to 392,333 SPIR-V words with this. - [User-data callees](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped; in the shaders seen they are metadata, and a caller that reads its program counter, which could address them, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. - [DB_RENDER_CONTROL resummarize draws](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing; any other resummarize, copy or decompress draw throws - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Measured on RDNA2 (gfx1036) in wave32 and wave64 with SIMM16 = 3: the link pair receives `PC + 4`, the branch goes to `PC + 4 + signext(SIMM16) * 4`, and `s_setpc_b64` of the link returns to the instruction after the call. Execution on the PS5 (gfx1013) and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported -- [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `openlog`, `setlogmask` and `closelog` are not implemented, so the identity, options and mask they set are not applied +- [syslog](../../core/libs/prx/libc/src/Syslog.cpp) (libc) writes each message to `stderr` as `[syslog:] `, with a newline added when missing, instead of sending it to the console's system log. Priority and facility handling follow FreeBSD's `vsyslog`: `%m` expands to the guest errno's message, unknown bits are reported (as priority 0x23, since FreeBSD passes `LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID` as the priority) and masked, a missing facility becomes `LOG_USER`, and errno is preserved. `setlogmask` follows [FreeBSD 11](https://github.com/freebsd/freebsd-src/blob/8d2708de5d6112a492744d367cfdfb8fd219227a/lib/libc/gen/syslog.c): the process-wide mask defaults to 0xff, zero queries it, and nonzero values replace it; masked messages are skipped before formatting, including the unknown-priority diagnostic. The console's mask behaviour is unverified. `openlog` and `closelog` are not implemented, so their identity and options are not applied - [daemon](../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [sigtimedwait](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE` +- [pause, sigwait, sigwaitinfo](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throw, as `sigtimedwait` does: guest signals are never left pending, so `sigwait` and `sigwaitinfo` have nothing to take, and `pause` would wait for a handler that `raise`, `kill` and `pthread_kill` only run on the calling thread. PPSA68091, PPSA99169, PPSA99420 and PPSA97358 import them and fail to load without them. PPSA68091 calls `pause` at one site (0x8F7266), in a loop that waits while a global holds another thread's `errno` address. PPSA99169 only stores the address of `pause` in a table of symbol addresses (0x30DB1). PPSA99420 calls `pause` at 0x3BD80F6 and 0x3C9E220, `sigwait` at 0x3C9E4C7 and `sigwaitinfo` at 0x3C9E588. PPSA97358 calls `sigwait` from QEMU's `sigwait_compat` thread ([compatfd.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/compatfd.c#L28-L53)), which `qemu_signal_init` ([main-loop.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/main-loop.c#L110-L147)) starts for `SIGIO`, `SIGALRM` and `SIGBUS` when the main loop is created, so it reaches the throw at start-up, and from the vCPU thread of the dummy accelerator ([dummy-cpus.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/accel/dummy-cpus.c#L48-L56)) +- [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console - [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. - [AJM](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp), [APR](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) and [NGS2](../../core/libs/prx/libSceNgs2.native/src/System.cpp) state is allocated with `new` and never freed on purpose: title threads still call into these libraries while `std::exit` runs static destructors, so ordinary statics would be destroyed under them. Keep them leaked; `guest_ajm_teardown` covers the AJM case on Unix. - PPSA30601 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [six sce::Json::Object::Pair, sce::Json::Object and sce::Json::Array::iterator members](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceAmprAmmGetUsageStatsData](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr), [sceMouseResetAbsolutePosition](../../core/libs/prx/libSceMouse/Export.cpp) (libSceMouse) and [sceSaveDataDirNameSearch2](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) +- PPSA07411 imports declared without parameters, signatures unknown, added without an implementation because the title fails to load without them: [sceSslCreateConnection, sceSslDeleteConnection, sceSslEnableVerifyOption, sceSslConnect, sceSslRead, sceSslWrite](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl), [four sce::Json::Value members (setElementAccessFailureHandler, setSpecialFloatHandler, setTypeMismatchHandler, operator bool)](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [sceConvertKeycodeGetCharacter](../../core/libs/prx/libSceConvertKeycode/Export.cpp) (libSceConvertKeycode) and [sceNpSessionSignalingGetGroupFromSessionId](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - [Comparison-sampled R32 float and R16 unorm textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. +- [sce::Json::InitParameter2 base-object constructor](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - forwards to the complete-object constructor and uses its inferred layout and default field values; constructor equivalence and initialization bytes remain unverified on the console - [_fstatfs](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) fills the FreeBSD 11 `struct statfs` of the PS5 payload SDK with the host volume that holds the file: block size, transfer size, total, free and available blocks, maximum name length and the read-only, `nosuid` and `noexec` flags (on Windows only read-only, and `f_files` and `f_ffree` are 0, as NTFS has no fixed number of files). The filesystem type number and name, mount paths, owner, filesystem id and I/O counters are left 0 or empty, since they would describe a host mount, not a console one. Sockets and pipes return `EINVAL`, as FreeBSD's `getvnode` does; a Windows descriptor that is neither a file nor a pipe throws. Stremio Plus (PPSA74126) reads the free space before a download and only logs the other fields +- [fstatvfs](../../core/libs/prx/libSceLibcInternal/FilesystemStatistics.cpp) (libSceLibcInternal) converts `_fstatfs` to the 88-byte `struct statvfs` as FreeBSD 9.2's `sfs2svfs` does (`f_bsize` from the transfer size, `f_frsize` from the block size, `f_favail` equal to `f_ffree`, `f_fsid` 0, `ST_RDONLY` and `ST_NOSUID` only). Differences: `f_namemax` is the `statfs` value, not `fpathconf(_PC_NAME_MAX)`; negative `f_bavail` and `f_ffree` wrap to unsigned without `EOVERFLOW`, as the same-width copy in FreeBSD does; inode counts are 0 on Windows, inherited from `_fstatfs`; a null output returns `EFAULT` after the descriptor is validated, where FreeBSD would fault. Layout and conversion are unverified against the PS5 library (PS5MC imports `fstatvfs` but was not run) +- [sceVoiceQoSEnd](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0901` (not initialized) before any initialization was measured on a PS5 (firmware 13.60, #1407); ending an initialized library was not, because a valid `sceVoiceQoSInit` never returns inside a homebrew payload. PPSA14632 calls it at shutdown and PPSA26344 after a nonzero `sceVoiceQoSInit`. After an initialization it returns 0 and clears the init flag, so the library can be initialized again. Whether the console also ends the libSceVoice instance that its `sceVoiceQoSInit` starts ([prosper](https://github.com/mattias800/prosper)'s reading of the module), and what it answers while endpoints exist, is unknown; no endpoint is implemented - [_WStoul](../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../core/libs/prx/libc/src/RuneLocale.cpp) and [swscanf_s](../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [sceSharePlayGetCurrentConnectionInfoA](../../core/libs/prx/libSceSharePlay/Export.cpp) (libSceSharePlay) - reports no Share Play session, as the library keeps none: status 0 (DORMANT), mode 0, empty online IDs, account IDs 0 and both user IDs -1 (`SCE_USER_SERVICE_USER_ID_INVALID`). A null pointer returns `SCE_SHARE_PLAY_ERROR_INVALID_ARGS` (0x810E0001). The 72-byte `SceSharePlayConnectionInfoA` layout and the error code follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libNet.cpp) and [fpPS4](https://github.com/red-prig/fpPS4/blob/trunk/src/ps4_libsceshareplay.pas), which agree on both; KytyPS5 sets the user IDs to -1 and fpPS4 leaves them 0, and which one the console returns is not verified. KytyPS5 also returns `SCE_SHARE_PLAY_ERROR_NOT_INITIALIZED` before `sceSharePlayInitialize`; this library keeps no initialization state and does not. +- [sceAgcCreatePrimState](../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well +- [pthread_spin_init, pthread_spin_destroy, pthread_spin_lock, pthread_spin_trylock, pthread_spin_unlock](../../core/libs/prx/libkernel/Pthread/Posix/Spinlock.cpp) (libkernel) follow FreeBSD 11.4 libthr [`thr_pspinlock.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_pspinlock.c) and the owner checks of its [`thr_umtx.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_umtx.h#L80-L175), unverified on the console: `EINVAL` for a null or destroyed lock and a `pshared` other than 0 or 1, `EBUSY` from `trylock` while any thread holds it, `EPERM` from `unlock` by a thread that does not hold it. `PTHREAD_PROCESS_SHARED` allocates a private lock, as guest code runs in one process. libthr spins forever when the holder locks again; here that call throws +- [pthread_cond_reltimedwait_np](../../core/libs/prx/libkernel/Pthread/Posix/Cond.cpp) (libkernel) takes `(cond, mutex, usec)`, a relative timeout in microseconds, and is `scePthreadCondTimedwait` with POSIX error numbers. FreeBSD 11.4 has no such function; the signature comes only from shadPS4, which registers one function for this NID and for `scePthreadCondTimedwait` ([`condvar.cpp`](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/kernel/threads/condvar.cpp#L229-L234)). shadPS4 reads a 64-bit count; here it is the 32-bit `SceKernelUseconds` of the SCE twin, as for `sem_reltimedwait_np`. Unverified on the console diff --git a/docs/user/COMPATIBILITY.md b/docs/user/COMPATIBILITY.md index 2e667fa59..a6eb265f4 100644 --- a/docs/user/COMPATIBILITY.md +++ b/docs/user/COMPATIBILITY.md @@ -3,13 +3,14 @@ | Game | ID | Windows | Linux | GTX 1050 Ti / i5-7500 3.4GHz | Intel(R) HD Graphic 620 / i5-7200 2.5GHz | |----------------|-----------|-------------------|-------|------------------------------|------------------------------------------| | Dreaming Sarah | PPSA02929 | In game, playable | ? | 60 FPS | 36 FPS | -| DOOM Shareware (homebrew) | PPSA99666 | In game (E1M1 and later maps) | In game (E1M1), software Vulkan only | ? | ? | +| DOOM Shareware (homebrew) | PPSA99666 | In game (E1M1 and later maps) | In game (E1M1) | ? | ? | DOOM Shareware is a public homebrew, [lowbit/ps5-doom](https://github.com/lowbit/ps5-doom), release v1.1.1 (content version 01.001.001). - Windows 11, RTX 4070 (version 01.001.000, an integration build before [#553](https://github.com/boykopovar/AnyPS5/pull/553), [#683](https://github.com/boykopovar/AnyPS5/pull/683), [#684](https://github.com/boykopovar/AnyPS5/pull/684) and [#685](https://github.com/boykopovar/AnyPS5/pull/685) were merged): movement and shooting in E1M1. [In-game screenshots](https://github.com/boykopovar/AnyPS5/pull/685#issuecomment-5997547697). - Windows 11, RTX 4070 SUPER (v1.1.1, `main` `2edaf7f4` plus open PRs, reported in [#690](https://github.com/boykopovar/AnyPS5/pull/690)): further maps, weapon switching, new games, level select, save/load round trip and audible audio. - Linux, Ubuntu 24.04 container, Mesa llvmpipe, no hardware GPU (v1.1.1, `main` `0e173a46` plus [#1278](https://github.com/boykopovar/AnyPS5/pull/1278), [#1279](https://github.com/boykopovar/AnyPS5/pull/1279) and [#1307](https://github.com/boykopovar/AnyPS5/pull/1307), relinked with `--exclude-sce-module libc.prx`, see [#1497](https://github.com/boykopovar/AnyPS5/issues/1497)): E1M1, 35 FPS in game; audio and extended gameplay not tested. The `/download0` directory must exist beside the executable. +- Linux, CachyOS, NVIDIA RTX 3080 Ti, driver 610.57.04, KDE Plasma Wayland (v1.1.1, `main` `a11c8f7f7` without extra PRs; `eboot.bin` and `sce_module/libc.prx` unwrapped from fSELF before relinking, see [#2599](https://github.com/boykopovar/AnyPS5/issues/2599); the title's `libc.prx` converted as a guest module, no `--exclude-sce-module`): launcher at 60 FPS, E1M1 at 35 FPS in game; new game from the menu, movement, turning, firing, an item pickup and the automap checked with controller input from `APS5_PAD_INPUT_FILE`. Audio output opened at 48000 Hz but was not listened to; later maps and save/load not tested. The `/download0` directory did not need to exist before the first run. - On the first run without a shader cache, the presentation kernel is compiled at its first dispatch, frame 1 does not complete in time and the title switches to the CPU scaler. Later runs with the cached kernel stay on AGC compute presentation (measured on Windows 11, RTX 3080, `main` `d70b8998`). -Not verified: Linux with a hardware GPU, current `main` without extra PRs. +Not verified: Windows on current `main` without extra PRs, Linux with AMD or Intel GPUs. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2aa4daa4a..3ff968d17 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -35,7 +35,7 @@ relinker --macos source/input.elf eboot Add `--to-intel` for Intel hosts, or `--to-rosetta` for Rosetta; a title that runs under Rosetta on Apple silicon is relinked with `--macos --to-rosetta`. The output format defaults to Linux ELF regardless of the filename; `.exe` alone does not select Windows. -The executable output must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. An existing output file can be replaced if it is separate from those inputs. +The executable, registry, icon and desktop entry output files must not refer to the input executable or a bundled module being converted, including through a hard link or symbolic link. These collisions fail before any output is written. An existing output file can be replaced if it is separate from those inputs. ## Options @@ -60,12 +60,15 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--skip-sce-module` | Deprecated. Disable bundled module processing and module directory checks. Conflicts with explicit `--sce-module-path`. | | `--exclude-sce-module ` | Deprecated. Exclude a bundled module by exact filename, not path. Repeat for multiple files; a missing filename is an error. Conflicts with `--skip-sce-module`. | | `--skip-syscall-check` | Deprecated. Disable syscall scanning in the executable and bundled modules. | +| `--trap-sdk-syscall-stubs` | Replace the exact ps5-payload-dev generic and numbered syscall wrapper stubs with `UD2`, so unused wrappers do not block relinking. Other syscall instructions still cause an error. If a trapped wrapper runs, it raises an illegal-instruction exception. For debugging only. | | `--lazy-binding` | Deprecated. Enable lazy symbol binding instead of eager binding. Incompatible with bundled ELF modules. | Specify `unused-filter=0|1|2` without `--`, at most once. Unknown options and extra positional arguments are errors. There is no `--help` flag; invoking `relinker` without arguments prints the usage syntax and exits with an error. The `--skip-sce-module`, `--exclude-sce-module `, `--skip-syscall-check`, and `--lazy-binding` flags are deprecated. If the application runs with these flags enabled, it will be extremely unstable and unsuitable for general use. These flags are only for debugging. +`--trap-sdk-syscall-stubs` is also for debugging. It preserves a runtime failure if a syscall wrapper is called; it does not implement FreeBSD syscalls or make these applications generally safe to run. + ## Runtime layout Paths are relative to the output executable: @@ -128,6 +131,15 @@ Games ask for the console's language with `sceSystemServiceParamGetInt` and show The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. +On Windows, the OpenCL, OpenGL, and Vulkan Compatibility Pack can install Mesa Dozen, which exposes D3D12 devices through Vulkan. Some D3D12 drivers can crash while the Vulkan loader enumerates adapters, even when AnyPS5 would select a native Vulkan device. If this happens and the system has another Vulkan driver, disable Dozen for the current PowerShell session before launching AnyPS5: + +```powershell +$env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' +.\app.exe +``` + +Do not disable Dozen if it is the only Vulkan driver available; the application would have no device to use. `VK_LOADER_DRIVERS_DISABLE` matches Vulkan driver manifest filenames and requires a Vulkan loader built with headers 1.3.234 or later ([loader documentation](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md#filter-environment-variables)). + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. diff --git a/tools/check_conventions.py b/tools/check_conventions.py index 8254975e8..961b82c97 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -7,6 +7,7 @@ import subprocess import sys from collections import defaultdict from pathlib import PurePosixPath +from urllib.parse import unquote RULES = { "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md in the same pull request; #endif and namespace ends are always allowed"), @@ -55,21 +56,26 @@ class Check: self.added = defaultdict(list) self.status = {} self.numstat = {} - self.tree = set(git("ls-tree", "-r", "--name-only", head).splitlines()) + entries = [entry.split("\t", 1) for entry in git("ls-tree", "-rz", head).split("\0") if entry] + self.modes = {path: metadata.split()[0] for metadata, path in entries} + self.tree = set(self.modes) self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} - for line in git("diff", "--name-status", "--no-renames", self.base, head).splitlines(): - status, path = line.split("\t", 1) + statuses = git("diff", "--name-status", "--no-renames", "-z", self.base, head).split("\0") + for status, path in zip(statuses[::2], statuses[1::2]): self.status[path] = status - for line in git("diff", "--numstat", "--no-renames", self.base, head).splitlines(): + for line in git("diff", "--numstat", "--no-renames", "-z", self.base, head).split("\0"): + if not line: + continue added, deleted, path = line.split("\t", 2) self.numstat[path] = (added, deleted) - path = None - for line in git("diff", "-U0", "--no-color", "--no-renames", "--no-prefix", self.base, head).splitlines(): - if line.startswith("+++ "): - path = None if line == "+++ /dev/null" else line[4:].rstrip("\t") + paths = iter(self.status) + path, number = None, None + for line in git("diff", "-U0", "--no-color", "--no-renames", self.base, head).splitlines(): + if line.startswith("diff --git "): + path, number = next(paths), None elif line.startswith("@@"): number = int(re.search(r"\+(\d+)", line).group(1)) - elif line.startswith("+") and path: + elif path and number is not None and line.startswith("+"): self.added[path].append((number, line[1:])) number += 1 @@ -141,6 +147,9 @@ class Check: for path, (added, deleted) in self.numstat.items(): if path.startswith("3rdparty/"): continue + if self.modes.get(path) == "160000": + self.report("system-dependency", path, 0, "submodule outside 3rdparty/") + continue suffix = PurePosixPath(path).suffix.lower() if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(git("show", f"{self.head}:{path}", text=False))): self.report("binary", path, 0) @@ -188,10 +197,11 @@ def utf8(data): def links(path, text): result = [] for target in LINK.findall(text): - if re.match(r"[a-z]+:|#", target): + if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue target = target.split("#", 1)[0].split("?", 1)[0] - result.append((target, posixpath.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)))) + decoded = unquote(target) + result.append((target, posixpath.normpath(decoded.lstrip("/") if decoded.startswith("/") else str(PurePosixPath(path).parent / decoded)))) return result @@ -256,7 +266,8 @@ def output(check, repo): errors += level == "error" text = f"{message}" + (f": {detail}" if detail else "") if actions: - location = f" file={path},line={max(line, 1)}," if path else " " + escaped_path = path.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A").replace(":", "%3A").replace(",", "%2C") + location = f" file={escaped_path},line={max(line, 1)}," if path else " " print(f"::{level}{location}title={rule}::" + text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")) else: print(f"{path or '(pull request)'}{f':{line}' if line else ''}: {level}: [{rule}] {text}") diff --git a/tools/check_skips.py b/tools/check_skips.py index afe85d4bb..0afad4f27 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -12,13 +12,15 @@ WINDOWS_PREFIX = "agc" def skips(path): for case in ET.parse(path).getroot().iter("testcase"): - if case.find("skipped") is None: + skipped = case.find("skipped") + if skipped is None: continue name = case.get("name") if not name: - continue - output = case.find("system-out") - yield name, (output.text or "") if output is not None else "" + raise ValueError(f"{path}: skipped testcase has no name") + output = ["skipped, " + reason for reason in (skipped.get("message", ""), skipped.text or "") if reason] + output += [case.findtext(channel) or "" for channel in ("system-out", "system-err")] + yield name, "\n".join(output) if __name__ == "__main__": @@ -27,7 +29,10 @@ if __name__ == "__main__": parser.add_argument("--job", choices=("linux", "windows"), default="linux") parser.add_argument("--list", action="store_true", help="print the skipped tests and why the file says they skipped") args = parser.parse_args() - found = sorted(skips(args.junit)) + try: + found = sorted(skips(args.junit)) + except (OSError, ET.ParseError, ValueError) as error: + parser.error(str(error)) if args.list: for name, output in found: reason = next((line.strip() for line in output.splitlines() if line.startswith("skipped")), "") diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 991cdb59c..83311cb20 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -4,6 +4,8 @@ import subprocess from pathlib import Path TEST_OUTPUT = r"tests[\\/]" +COMMAND = re.compile(r"^\s*(add_test|set_tests_properties)\s*\(", re.M | re.I) +ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) KNOWN_UNRUN = set() @@ -16,10 +18,10 @@ def built(build): path, _, rule = line.partition(": ") if "EXECUTABLE" not in rule: continue - found = re.match(rf"^{TEST_OUTPUT}([A-Za-z0-9_.]+)$", path) + found = re.match(rf"^{TEST_OUTPUT}(?:.*[\\/])?([^\\/]+)$", path) if not found: continue - name = found.group(1).rstrip(".") + name = found.group(1) if name.endswith(".exe"): name = name[:-4] if name: @@ -27,16 +29,58 @@ def built(build): return names +def commands(content): + parsed_until = 0 + for command in COMMAND.finditer(content): + if command.start() < parsed_until: + continue + position, arguments = command.end(), [] + while position < len(content): + if content[position].isspace(): + position += 1 + continue + if content[position] == ")": + parsed_until = position + 1 + yield command.group(1).lower(), arguments + break + if content[position] == "#": + position = content.find("\n", position) + if position < 0: + break + continue + token = ARGUMENT.match(content, position) + if token is None: + raise ValueError(f"invalid CTest argument at position {position}") + argument = token.group(0) + if argument.startswith('"'): + argument = re.sub(r"\\(.)", lambda match: {"n": "\n", "r": "\r", "t": "\t"}.get(match[1], match[1]), argument[1:-1], flags=re.S) + elif token.group("equals") is not None: + width = len(token.group("equals")) + 2 + argument = argument[width:-width] + arguments.append(argument) + position = token.end() + + def registered(build): names = set() for file in Path(build).rglob("CTestTestfile.cmake"): - for line in file.read_text(errors="replace").splitlines(): - found = re.match(r"\s*add_test\s*\((.*)\)\s*$", line) - if not found: + tests, disabled = {}, {} + for command, arguments in commands(file.read_text(encoding="utf-8", errors="replace")): + if command == "add_test" and arguments: + tests[arguments[0]] = arguments[1:] + elif command == "set_tests_properties" and "PROPERTIES" in arguments: + properties = arguments.index("PROPERTIES") + values = arguments[properties + 1:] + for key, value in zip(values[::2], values[1::2]): + if key == "DISABLED": + for name in arguments[:properties]: + disabled[name] = value.upper() in ("1", "ON", "YES", "TRUE", "Y") + for test, arguments in tests.items(): + if disabled.get(test, False): continue - tokens = re.findall(r'"([^"]*)"|(\S+)', found.group(1)) - for token in tokens[1:]: - argument = token[0] or token[1] + for index, argument in enumerate(arguments): + if index and (argument.startswith("-") or re.match(r"^[A-Za-z_]\w*=", argument)): + continue base = re.split(r"[\\/]", argument)[-1] if base.endswith(".exe"): base = base[:-4] diff --git a/tools/import_audit.py b/tools/import_audit.py index c7ebf9dc8..a6c0f6b0d 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -1,4 +1,5 @@ import argparse +import hashlib import json import re import struct @@ -33,14 +34,14 @@ def unpack(layout, data, offset, path, what): return struct.unpack(layout, take(data, offset, struct.calcsize(layout), path, what)) -def cstring(data, offset, limit, path, what): +def cstring(data, offset, limit, path, what, encoding="utf-8", errors="surrogateescape"): end = data.find(b"\0", offset, limit) if offset < 0 or offset >= limit or end < 0: raise AuditError(f"{path}: {what} at 0x{offset:x} is not a NUL-terminated string inside 0x{limit:x} bytes") try: - return data[offset:end].decode("utf-8") + return data[offset:end].decode(encoding, errors=errors) except UnicodeDecodeError as error: - raise AuditError(f"{path}: {what} at 0x{offset:x} is not UTF-8: {error}") from error + raise AuditError(f"{path}: {what} at 0x{offset:x} is not {encoding}: {error}") from error def elf_exports(path, data): @@ -48,9 +49,14 @@ def elf_exports(path, data): ident, section_offset, section_size, section_count = header[0], header[6], header[11], header[12] if ident[4] != 2 or ident[5] != 1 or section_size != 64: raise AuditError(f"{path}: only little-endian ELF64 with 64-byte section headers is supported (class {ident[4]}, data {ident[5]}, shentsize {section_size})") + if not section_offset: + raise AuditError(f"{path}: ELF exports without a section header table are not supported") + if not section_count: + section_count = unpack(" 1: + raise AuditError(f"{path}: multiple extended section index tables for section {section_index}") for position in range(0, size, 24): name, info, other, index, _, _ = unpack("= len(sections): + raise AuditError(f"{path}: extended symbol section index {index} lies outside the section table") + elif 0 < index < 0xff00 and index >= len(sections): + raise AuditError(f"{path}: symbol section index {index} lies outside the section table") binding, visibility = info >> 4, other & 3 - if index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): + if name == 0 or index == 0 or binding not in (1, 2, 10) or visibility not in (0, 3): continue names.add(cstring(data, strings_offset + name, strings_offset + strings_size, path, "export name")) return names @@ -79,12 +96,19 @@ def pe_exports(path, data): directory = PE_DIRECTORY_OFFSET[magic] if optional_size < directory + 8: raise AuditError(f"{path}: PE optional header of {optional_size} bytes has no export directory") + take(data, optional, optional_size, path, "PE optional header") + if unpack("= raw_size or length > raw_size - relative: continue take(data, raw_offset + relative, length, path, f"data at RVA 0x{address:x}") - return raw_offset + relative + return raw_offset + relative, min(raw_offset + raw_size, len(data)) raise AuditError(f"{path}: RVA 0x{address:x} is not inside any section") - table = unpack("= function_count: + raise AuditError(f"{path}: export ordinal {ordinal} lies outside the address table ({function_count} entries)") + function_rva = unpack("//[^\n]*|/\*[\s\S]*?\*/)' + r'|(?P(?:u8|u|U|L)?R"(?P[^\s()\\]{0,16})\([\s\S]*?\)(?P=delimiter)")' + r'|(?P(?:u8|u|U|L)?"(?:\\[\s\S]|[^"\\])*")' + r"|(?P(?:u8|u|U|L)?'(?:\\[\s\S]|[^'\\])*')" + r"|(?P(?:[0-9]|\.[0-9])[A-Za-z0-9_.']*|[A-Za-z_][A-Za-z0-9_]*|\S)") DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" @@ -49,35 +54,58 @@ def load_db(path): finally: temporary.unlink(missing_ok=True) db = {} - with open(path, errors="replace") as handle: + with open(path, encoding="utf-8-sig", errors="replace") as handle: for line in handle: parts = line.strip().split() - if len(parts) >= 2: - db.setdefault(parts[0], parts[1]) + if len(parts) >= 2 and not parts[0].startswith("#"): + nid, name = (part.split("#", 1)[0] for part in parts[:2]) + if nid and name: + db.setdefault(nid, name) return db +def source_files(root): + return (path for path in sorted(root.rglob("*.cpp")) + if not any(part.lower() in {"test", "tests"} + for part in path.relative_to(root).parts[:-1])) + + +def source_aliases(text): + tokens = [match.group() for match in CPP_TOKEN.finditer(text.replace("\\\n", "").replace("\\\r\n", "")) + if match.group("comment") is None] + for index, token in enumerate(tokens): + if token != "APS5_EXPORT" or index + 5 >= len(tokens): + continue + opening, literal, comma, name, closing = tokens[index + 1:index + 6] + if opening != "(" or comma != "," or closing != ")": + continue + if re.fullmatch(r'"[A-Za-z0-9+\-]{11}"', literal) is None: + continue + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) and "unknown" in name.lower(): + yield literal[1:-1], name + + +def source_definitions(text): + def mask(match): + return match.group() if match.group("code") is not None else re.sub(r"[^\n]", " ", match.group()) + + return DEFINITION.finditer(CPP_TOKEN.sub(mask, text.replace("\\\n", "").replace("\\\r\n", ""))) + + def collect_unknowns(): found = {} - for path in sorted(PRX.rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in ALIAS.finditer(text): - found.setdefault(match.group(1), "%s:%s" % ( - path.relative_to(ROOT), match.group(2))) + for path in source_files(PRX): + text = path.read_text(encoding="utf-8") + for nid, name in source_aliases(text): + found.setdefault(nid, "%s:%s" % (path.relative_to(ROOT).as_posix(), name)) return found def collect_real_names(): names = set() - for path in sorted((ROOT / "core" / "libs").rglob("*.cpp")): - try: - text = path.read_text(errors="replace") - except OSError: - continue - for match in DEFINITION.finditer(text): + for path in source_files(ROOT / "core" / "libs"): + text = path.read_text(encoding="utf-8") + for match in source_definitions(text): name = match.group(1) names.add(name) if name.endswith(NID_POSTFIX): @@ -118,26 +146,30 @@ def nid_arguments(arguments, options): return normalized -def main(): +def main(argv=None): parser = argparse.ArgumentParser( description="Suggest real names for Unknown NID stubs") parser.add_argument("--db", default=os.environ.get("ANYPS5_NID_DB"), help="NID database CSV (downloaded to a cache dir by default)") - parser.add_argument("--nid", action="append", default=[], + parser.add_argument("--nid", action="append", default=None, help="resolve only these NIDs instead of scanning the tree") parser.add_argument("--json", action="store_true", help="emit machine-readable JSON") options = [option for action in parser._actions for option in action.option_strings] - args = parser.parse_args(nid_arguments(sys.argv[1:], options)) + arguments = sys.argv[1:] if argv is None else argv + args = parser.parse_args(nid_arguments(arguments, options)) + requested_nids = any(option_name(argument, options) == "--nid" for argument in arguments) db_path = Path(args.db) if args.db else DEFAULT_CACHE - db = load_db(db_path) - - if args.nid: - unknowns = {nid: "" for nid in args.nid} - else: - unknowns = collect_unknowns() - real_names = collect_real_names() + try: + db = load_db(db_path) + if requested_nids: + unknowns = {nid: "" for nid in (args.nid or [])} + else: + unknowns = collect_unknowns() + real_names = collect_real_names() if unknowns else set() + except (OSError, UnicodeError) as error: + parser.error(str(error)) rows = [] failures = 0 diff --git a/tools/package_release.py b/tools/package_release.py index 53add7365..476314e0e 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -2,15 +2,18 @@ import argparse import re import shutil import tarfile +import tempfile import zipfile from pathlib import Path +ROOT = Path(__file__).resolve().parent.parent + def package(platform, build, output, version): if not re.fullmatch(r"v[0-9A-Za-z][0-9A-Za-z._-]*", version) or version.endswith("."): raise ValueError(f"Invalid release tag for asset filenames: {version}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) - expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} + expected = {f"{directory.name}.prx" for directory in (ROOT / "core/libs/prx").iterdir() if directory.is_dir() and not directory.name.startswith(".")} missing = expected - {library.name for library in libraries} if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") @@ -24,25 +27,29 @@ def package(platform, build, output, version): if not file.is_file() or file.stat().st_size == 0: raise RuntimeError(f"Missing or empty release file: {file}") output.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(output / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: - for file in files: - archive.write(file, arcname=f"libs/{file.name}") - with tarfile.open(output / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9) as archive: - for file in files: - archive.add(file, arcname=f"libs/{file.name}") asset = f"relinker-{version}.exe" if platform == "windows" else f"relinker-{version}" - shutil.copy2(binary, output / asset) + with tempfile.TemporaryDirectory(prefix=".release-", dir=output) as temporary: + staging = Path(temporary) + with zipfile.ZipFile(staging / f"prx-{platform}-{version}.zip", "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for file in files: + archive.write(file, arcname=f"libs/{file.name}") + with tarfile.open(staging / f"prx-{platform}-{version}.tar.gz", "w:gz", compresslevel=9, dereference=True) as archive: + for file in files: + archive.add(file, arcname=f"libs/{file.name}") + shutil.copy2(binary, staging / asset) + for file in sorted(staging.iterdir()): + file.replace(output / file.name) def collect_docs(source, output): - documents = sorted(source.rglob("*.md")) + documents = sorted(document for document in source.rglob("*.md") if document.is_file()) if not documents: raise RuntimeError(f"No Markdown documents found in {source}") - names = set() + names = {file.name.casefold() for file in output.iterdir()} if output.exists() else set() for document in documents: - if document.name in names or (output / document.name).exists(): + if document.name.casefold() in names: raise RuntimeError(f"Duplicate release asset name: {document.name}") - names.add(document.name) + names.add(document.name.casefold()) output.mkdir(parents=True, exist_ok=True) for document in documents: shutil.copy2(document, output / document.name) diff --git a/tools/pr_overlap.py b/tools/pr_overlap.py index e84ee058d..279006dba 100644 --- a/tools/pr_overlap.py +++ b/tools/pr_overlap.py @@ -9,7 +9,7 @@ from urllib.parse import urlencode PARENT = 'if .parent then "\\(.parent.owner.login)/\\(.parent.name)" else .nameWithOwner end' EXPORT = re.compile(r"^\+.*\bAPS5_VABI\s+(\w+)\s*\(") -DEPENDS = re.compile(r'Depends on:([^\r\n]*(?:\r?\n[ \t]*[-*][ \t]*#\d+[^\r\n]*)*)') +DEPENDS = re.compile(r'Depends on:[ \t]*((?:#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*|none\b)?(?:\r?\n[ \t]*[-*][ \t]*#\d+\b[^\r\n]*)*)') PLAIN_DEPENDS = re.compile(r'Depends on[ \t]+(#\d+\b(?:(?:[ \t]*,[ \t]*(?:and[ \t]+)?|[ \t]+and[ \t]+)#\d+\b)*)') HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))?", re.M) MARKER = "" diff --git a/tools/progress.py b/tools/progress.py index 6f37f6cce..e3b281bb8 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -13,6 +13,7 @@ ISA = Path(__file__).resolve().parent / "rdna_isa.txt" SOURCE = f'https://github.com/{os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5")}/blob/main' DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") STUB = "NotImplemented_nid_no_patch" +CALL = re.compile(r"\b(\w+)\s*\(") CMAKE_SHARED = re.compile(r'^\s*include\(\s*"?\$\{CMAKE_CURRENT_SOURCE_DIR\}/\.\./([\w.-]+)/[\w.-]+\.cmake"?\s*\)', re.M) SOURCE_SHARED = re.compile(r'^\s*#\s*include\s*"prx/([\w.-]+)/[\w./-]+\.cpp"', re.M) STUB_WRAPPER = re.compile(r"\bstatic\s+(?:\[\[noreturn\]\]\s+)?[\w:<>,\s*&]+?\s+(\w+)\s*\([^;{]*\)\s*\{") @@ -91,10 +92,11 @@ def body_end(text, start): def stub_calls(text): - calls = [STUB] + calls = {STUB} for match in STUB_WRAPPER.finditer(text): - if STUB in text[match.end() - 1:body_end(text, match.end() - 1)]: - calls.append(match.group(1) + "(") + body = text[match.end() - 1:body_end(text, match.end() - 1)] + if any(call.group(1) == STUB for call in CALL.finditer(body)): + calls.add(match.group(1)) return calls @@ -156,7 +158,7 @@ def scan_library(path): if name.endswith("_nid_no_patch"): continue body = text[match.end() - 1:body_end(text, match.end() - 1)] - if any(call in body for call in calls): + if any(call.group(1) in calls for call in CALL.finditer(body)): todo.add(name) continue done.add(name) diff --git a/tools/stubgen.py b/tools/stubgen.py index 5a3f47480..e12520f43 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import argparse +import os import re import sys +import tempfile from pathlib import Path try: @@ -27,10 +29,12 @@ def parse_nids(path): nids = [] seen = set() with open(path, "r", encoding="utf-8-sig", errors="replace") as f: - for line in f: + for number, line in enumerate(f, 1): nid = strip_nid_token(line) if not nid: continue + if re.fullmatch(r"[A-Za-z0-9+\-]{11}", nid) is None: + raise ValueError(f"{path}:{number}: invalid NID {nid!r}") if nid in seen: continue seen.add(nid) @@ -167,6 +171,35 @@ def generate(module, nids, db, td_path=None, td_lib=None): return (code, td_text) +def same_file(left, right): + left, right = Path(left), Path(right) + return left.resolve() == right.resolve() or (left.exists() and right.exists() and left.samefile(right)) + + +def write_outputs(outputs): + staged = [] + umask = os.umask(0) + os.umask(umask) + try: + for filename, content in outputs: + path = Path(filename).resolve() + if path.is_dir(): + raise IsADirectoryError(str(path)) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=path.parent, prefix=path.name + ".", suffix=".tmp", delete=False) as handle: + temporary = Path(handle.name) + staged.append((temporary, path)) + handle.write(content) + if path.exists(): + temporary.chmod(path.stat().st_mode & 0o7777) + else: + temporary.chmod(0o666 & ~umask) + for temporary, path in staged: + temporary.replace(path) + finally: + for temporary, path in staged: + temporary.unlink(missing_ok=True) + + def main(argv=None): ap = argparse.ArgumentParser(description="Generate AnyPS5 Export.cpp skeletons and TechnicalDebt lines") ap.add_argument("--module", required=True, help="target module name, e.g. libSceAmpr, used for UnknownNN prefix and default TechnicalDebt path core/libs/prx/MODULE/Export.cpp") @@ -175,36 +208,36 @@ def main(argv=None): ap.add_argument("--out", required=False, default=None, help="output file for code, default stdout") ap.add_argument("--td-path", required=False, default=None, help="override file path used inside TechnicalDebt lines, default derived from module, relative to docs/dev") ap.add_argument("--td-lib", required=False, default=None, help="override lib label used inside TechnicalDebt lines, default derived from module") - ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stdout") + ap.add_argument("--td-out", required=False, default=None, help="output file for TechnicalDebt lines, default stderr when code uses stdout, otherwise stdout") ap.add_argument("--td-only", action="store_true", help="print only TechnicalDebt lines, no code") args = ap.parse_args(argv) - nids = parse_nids(args.nids) - db = {} - if args.db: - db = parse_db(args.db) - code, td = generate(args.module, nids, db, args.td_path, args.td_lib) - if args.td_only: + try: + if not args.td_only and args.out and args.td_out and same_file(args.out, args.td_out): + ap.error("--out and --td-out must name different files") + destinations = [args.td_out] if args.td_only else [args.out, args.td_out] + for destination in filter(None, destinations): + for source in filter(None, [args.nids, args.db]): + if same_file(destination, source): + ap.error(f"output {destination!r} would overwrite input {source!r}") + nids = parse_nids(args.nids) + db = parse_db(args.db) if args.db else {} + code, td = generate(args.module, nids, db, args.td_path, args.td_lib) + outputs = [] + if not args.td_only and args.out: + outputs.append((args.out, code)) if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: + outputs.append((args.td_out, td)) + write_outputs(outputs) + except (OSError, ValueError) as error: + ap.error(str(error)) + if args.td_only: + if not args.td_out: sys.stdout.write(td) return 0 - if args.out: - with open(args.out, "w", encoding="utf-8", newline="\n") as f: - f.write(code) - else: + if not args.out: sys.stdout.write(code) - if args.td_out: - with open(args.td_out, "w", encoding="utf-8", newline="\n") as f: - f.write(td) - else: - if args.out: - sys.stdout.write(td) - else: - if td: - sys.stdout.write("\n") - sys.stdout.write(td) + if not args.td_out: + (sys.stdout if args.out else sys.stderr).write(td) return 0 diff --git a/tools/tests/test_check_conventions_comments.py b/tools/tests/test_check_conventions_comments.py index 9c943fe7b..89df82af5 100644 --- a/tools/tests/test_check_conventions_comments.py +++ b/tools/tests/test_check_conventions_comments.py @@ -1,3 +1,5 @@ +import contextlib +import io import os import subprocess import sys @@ -6,7 +8,7 @@ import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body +from check_conventions import STUB_BODY, SYSTEM_DEPENDENCY, Check, function_body, links, output class FunctionBodyTests(unittest.TestCase): @@ -270,5 +272,103 @@ class SystemDependencyTests(unittest.TestCase): self.assertIsNone(SYSTEM_DEPENDENCY.search(line)) +class RepositoryChecksTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.root = Path(self.directory.name) + self.original = Path.cwd() + os.chdir(self.root) + self.git("init", "-q") + (self.root / "README.md").write_text("Project\n", encoding="utf-8") + self.commit() + + def tearDown(self): + os.chdir(self.original) + self.directory.cleanup() + + def git(self, *args): + return subprocess.run(["git", "-c", "user.name=Conventions Test", "-c", + "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], + capture_output=True, text=True, check=True).stdout.strip() + + def commit(self): + self.git("add", ".") + self.git("commit", "-qm", "test: add convention fixture") + + def test_quoted_utf8_paths_are_checked(self): + name = "docs/dev/測試guide.md" if os.name == "nt" else "docs/dev/測試\tguide.md" + path = self.root / name + path.parent.mkdir(parents=True) + path.write_text("[missing](absent.md)\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.repository() + check.docs() + self.assertEqual(check.findings, [("doc-link", name, 1, "absent.md")]) + + def test_mixed_patch_types_preserve_file_order(self): + name = "a b.cpp" if os.name == "nt" else "a\nb.cpp" + (self.root / "README.md").unlink() + (self.root / "a.bin").write_bytes(b"binary\x00data") + (self.root / name).write_text("// debt\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + check.repository() + self.assertIn(("comment", name, 1, ""), check.findings) + self.assertIn(("binary", "a.bin", 0, ""), check.findings) + self.assertNotIn("README.md", check.added) + + def test_increment_line_is_not_a_patch_file_header(self): + path = self.root / "core/example.cpp" + path.parent.mkdir() + path.write_text("void example() {\n++ counter;\n// debt\n}\n", encoding="utf-8") + self.commit() + check = Check("HEAD~1", "HEAD") + check.code() + self.assertIn(("comment", "core/example.cpp", 3, ""), check.findings) + + def test_submodule_outside_thirdparty_reports_without_crashing(self): + head = self.git("rev-parse", "HEAD") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "vendor") + self.git("commit", "-qm", "build: add invalid submodule") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertIn(("system-dependency", "vendor", 0, "submodule outside 3rdparty/"), check.findings) + + def test_thirdparty_submodule_remains_allowed(self): + head = self.git("rev-parse", "HEAD") + self.git("update-index", "--add", "--cacheinfo", "160000", head, "3rdparty/vendor") + self.git("commit", "-qm", "build: add allowed submodule") + check = Check("HEAD~1", "HEAD") + check.repository() + self.assertEqual(check.findings, []) + + def test_relative_link_url_decoding(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./space%20name.md#details)"), + [("./space%20name.md", "docs/dev/space name.md")]) + self.assertEqual(links("docs/dev/index.md", "[reference](HTTPS://example.com/x)"), []) + self.assertEqual(links("docs/dev/index.md", "[reference](//example.com/x)"), []) + + def test_percent_encoded_fragment_is_part_of_filename(self): + self.assertEqual(links("docs/dev/index.md", "[guide](./part%23one.md#details)"), + [("./part%23one.md", "docs/dev/part#one.md")]) + + def test_annotation_filename_properties_are_escaped(self): + check = type("Findings", (), {"findings": [("doc-link", "docs/name,with:colon%\n.md", 2, "missing") ]})() + original = os.environ.get("GITHUB_ACTIONS") + os.environ["GITHUB_ACTIONS"] = "true" + try: + with contextlib.redirect_stdout(io.StringIO()) as captured: + output(check, "owner/repo") + finally: + if original is None: + os.environ.pop("GITHUB_ACTIONS", None) + else: + os.environ["GITHUB_ACTIONS"] = original + annotation = captured.getvalue().splitlines()[0] + self.assertIn("file=docs/name%2Cwith%3Acolon%25%0A.md,line=2", annotation) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index c668ca75d..02ab77f59 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -59,5 +59,41 @@ class CheckSkipsTests(unittest.TestCase): self.assertNotIn("error: geometry skipped", result.stdout) + + +class SkipReportTests(unittest.TestCase): + def skipped(self, content): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "ctest.xml" + path.write_text(content, encoding="utf-8") + return list(check_skips.skips(path)) + + def test_skip_message_preserves_the_device_reason(self): + found = self.skipped('') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_stderr_preserves_the_device_reason(self): + found = self.skipped('skipped, no display') + self.assertEqual(len(found), 1) + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_unnamed_skipped_test_is_an_error(self): + with self.assertRaises(ValueError): + self.skipped('') + + def test_unexpected_reason_stays_unexpected(self): + found = self.skipped('') + self.assertIsNone(check_skips.DEVICE_LIMITATION.search(found[0][1])) + + def test_ctest_stdout_is_retained(self): + found = self.skipped('skipped, the device reports too few descriptors') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + def test_message_and_element_text_are_both_retained(self): + found = self.skipped('the device has no required extension') + self.assertRegex(found[0][1], check_skips.DEVICE_LIMITATION) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 6118d21bc..98084bf52 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -47,21 +47,26 @@ def exporting(*names): def build_pe(names): section_rva, raw_offset = 0x1000, 0x200 strings, rvas = b"", [] - start = 40 + 4 * len(names) + start = 40 + 10 * len(names) for name in names: rvas.append(section_rva + start + len(strings)) strings += name.encode() + b"\0" - raw = struct.pack("