diff --git a/CMakeLists.txt b/CMakeLists.txt index 5670ea8ce30..7fa95eaf663 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,10 @@ if(BUILD_TESTING) set_tests_properties(request_serializer PROPERTIES TIMEOUT 20) find_package(Python3 QUIET COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) + add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py + ${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND}) + set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20) add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py) add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE} @@ -115,6 +119,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py) add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py) + add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py) add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py) add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE} diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2b49528bdd1..8385138261f 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -58,6 +58,21 @@ if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") endforeach() + + if(NOT EXISTS "${CMAKE_RANLIB}") + message(FATAL_ERROR "Stable import libraries require ranlib") + endif() + list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount) + string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib, " implibOption) + if(NOT linkCommandCount EQUAL 1 OR implibOption EQUAL -1) + message(FATAL_ERROR "Expected one shared library link command that writes -Wl,--out-implib,: ${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + endif() + string(REPLACE "-Wl,--out-implib, " "-Wl,--out-implib,.new " linkCommand "${CMAKE_CXX_CREATE_SHARED_LIBRARY}") + set(CMAKE_CXX_CREATE_SHARED_LIBRARY + "\"${CMAKE_COMMAND}\" -E env SOURCE_DATE_EPOCH=0 ${linkCommand}" + "\"${CMAKE_RANLIB}\" -D .new" + "\"${CMAKE_COMMAND}\" -E copy_if_different .new " + ) endif() set(LIBC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc) @@ -87,6 +102,14 @@ set_target_properties(libc PROPERTIES set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx) set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx) +set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports) + +add_custom_command( + OUTPUT ${LIBC_EXPORT_LIST} + COMMAND $ --write-export-list ${LIBC_UNPATCHED} ${LIBC_EXPORT_LIST} + DEPENDS nid_patcher libc ${LIBC_UNPATCHED} + COMMENT "Listing libc exports" +) add_custom_command( OUTPUT ${LIBC_PATCHED} @@ -146,8 +169,8 @@ foreach(LIB_DIR ${LIBS_DIRS}) add_custom_command( OUTPUT ${PATCHED_FILE} COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE} - COMMAND $ ${LIB_NAME} --preserve-exports "${LIBC_UNPATCHED}" "${PATCHED_FILE}" - DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_UNPATCHED} + COMMAND $ ${LIB_NAME} --preserve-export-list "${LIBC_EXPORT_LIST}" "${PATCHED_FILE}" + DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_EXPORT_LIST} COMMENT "Patching NIDs for ${LIB_NAME}" ) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 9ddb8391f49..0ebfa792f7c 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1750,23 +1750,29 @@ struct JpegEncOutputInfo { }; struct JpegDecCreateParam { - std::uint32_t size; + std::uint32_t this_size; std::uint32_t attribute; + std::uint32_t max_image_width; }; struct JpegDecParseParam { const void* jpeg_mem_addr; std::uint32_t jpeg_mem_size; - std::uint32_t reserved0; + std::uint16_t decode_mode; + std::uint16_t down_scale; }; struct JpegDecDecodeParam { const void* jpeg_mem_addr; void* image_mem_addr; + void* coefficient_mem_addr; std::uint32_t jpeg_mem_size; std::uint32_t image_mem_size; + std::uint32_t coefficient_mem_size; + std::uint16_t decode_mode; + std::uint16_t down_scale; std::uint16_t pixel_format; - std::uint16_t reserved0; + std::uint16_t alpha_value; std::uint32_t image_pitch; }; @@ -1774,8 +1780,13 @@ struct JpegDecImageInfo { std::uint32_t image_width; std::uint32_t image_height; std::uint16_t color_space; - std::uint16_t bit_depth; - std::uint32_t image_flag; + std::uint16_t component_number; + std::uint8_t component_id[4]; + std::uint8_t sampling_factor[4]; + std::uint32_t coefficient_mem_size; + std::uint32_t suitable_csc_attribute; + std::uint32_t output_image_width; + std::uint32_t output_image_height; }; struct PlayGoInitParams { diff --git a/core/libs/nid/include/nid/ExportExclusions.hpp b/core/libs/nid/include/nid/ExportExclusions.hpp index 93442d156e7..ae41e169c54 100644 --- a/core/libs/nid/include/nid/ExportExclusions.hpp +++ b/core/libs/nid/include/nid/ExportExclusions.hpp @@ -8,6 +8,8 @@ namespace Nid { std::string NormalizeExportName(const std::string& name); std::unordered_set ReadExportExclusions(const std::string& path); +std::unordered_set ReadExportList(const std::string& path); +bool WriteExportList(const std::string& referencePath, const std::string& path); } diff --git a/core/libs/nid/src/ExportExclusions.cpp b/core/libs/nid/src/ExportExclusions.cpp index 366e918cb55..273f517d3b7 100644 --- a/core/libs/nid/src/ExportExclusions.cpp +++ b/core/libs/nid/src/ExportExclusions.cpp @@ -4,10 +4,13 @@ #include #include #include +#include #include #include #include +#include #include +#include namespace Nid { @@ -113,6 +116,24 @@ std::unordered_set ReadElfExports(const std::vector& return result; } +std::string ReadText(const std::string& path, std::ifstream& input) { + std::string text{std::istreambuf_iterator(input), {}}; + if (input.bad()) throw std::runtime_error("cannot read export list: " + path); + return text; +} + +std::string FormatExportList(const std::unordered_set& exports) { + std::vector names(exports.begin(), exports.end()); + std::ranges::sort(names); + std::string text; + for (const auto& name : names) { + if (name.find('\n') != std::string::npos) throw std::runtime_error("export name contains a line break: " + name); + text += name; + text += '\n'; + } + return text; +} + } std::string NormalizeExportName(const std::string& name) { @@ -153,4 +174,31 @@ std::unordered_set ReadExportExclusions(const std::string& path) { return result; } +std::unordered_set ReadExportList(const std::string& path) { + std::ifstream input(path, std::ios::binary); + if (!input) throw std::runtime_error("cannot open export list: " + path); + const auto text = ReadText(path, input); + if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path); + std::unordered_set result; + for (std::size_t start = 0; start < text.size();) { + const auto end = text.find('\n', start); + if (end == start) throw std::runtime_error("empty name in export list: " + path); + auto name = text.substr(start, end - start); + if (!result.insert(name).second) throw std::runtime_error("duplicate name in export list: " + name); + start = end + 1; + } + return result; +} + +bool WriteExportList(const std::string& referencePath, const std::string& path) { + const auto text = FormatExportList(ReadExportExclusions(referencePath)); + if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false; + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) throw std::runtime_error("cannot write export list: " + path); + output.write(text.data(), static_cast(text.size())); + output.close(); + if (!output) throw std::runtime_error("cannot write export list: " + path); + return true; +} + } diff --git a/core/libs/nid/src/main.cpp b/core/libs/nid/src/main.cpp index c07a4e8bfaa..7e459dc99d1 100644 --- a/core/libs/nid/src/main.cpp +++ b/core/libs/nid/src/main.cpp @@ -25,10 +25,25 @@ void _writeFile(const std::string& path, const std::vector& data) int Run(int argc, char* argv[]) { if (argc < 3) { - std::cerr << "Usage: nid_patcher [--preserve-exports ] [file2 ...]\n"; + std::cerr << "Usage: nid_patcher [--preserve-exports | --preserve-export-list ] [file2 ...]\n" + " nid_patcher --write-export-list \n"; return 1; } + if (std::string(argv[1]) == "--write-export-list") { + if (argc != 4) { + std::cerr << "FAIL: --write-export-list requires a reference library and an export list\n"; + return 2; + } + try { + std::cout << (Nid::WriteExportList(argv[2], argv[3]) ? "WROTE: " : "UNCHANGED: ") << argv[3] << "\n"; + } catch (const std::exception& e) { + std::cerr << "FAIL: " << argv[3] << ": " << e.what() << "\n"; + return 2; + } + return 0; + } + const std::string libraryName = argv[1]; std::unordered_set excludedExports; std::vector paths; @@ -36,10 +51,10 @@ int Run(int argc, char* argv[]) { bool hasReference = false; for (int i = 2; i < argc; ++i) { const std::string argument = argv[i]; - if (argument == "--preserve-exports") { - if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library"); + if (argument == "--preserve-exports" || argument == "--preserve-export-list") { + if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference"); hasReference = true; - excludedExports = Nid::ReadExportExclusions(argv[++i]); + excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]); } else { if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument); paths.push_back(argument); diff --git a/core/libs/nid/tests/test_macho_nids.py b/core/libs/nid/tests/test_macho_nids.py index 1d5c118ced3..a4a239fcbc2 100644 --- a/core/libs/nid/tests/test_macho_nids.py +++ b/core/libs/nid/tests/test_macho_nids.py @@ -1,6 +1,7 @@ """Check nid_patcher on small byte-built Mach-O dylibs: the export trie, the chained imports and what it refuses.""" import hashlib +import os import struct import subprocess import sys @@ -211,6 +212,53 @@ def check_preserved(patcher, directory): assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) +def write_export_list(patcher, reference, listing): + return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30) + + +def check_export_list(patcher, directory): + reference = Path(directory) / "listed.dylib" + listing = Path(directory) / "listed.exports" + reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes() + + os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000)) + reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten" + + reference.write_bytes(build_dylib([("_puts", 0, 0x100)], [])) + result = write_export_list(patcher, reference, listing) + assert result.returncode == 0, (result.stdout, result.stderr) + assert listing.read_bytes() == b"puts\n", listing.read_bytes() + assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time" + + path = Path(directory) / "listed-preserve.dylib" + path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (result.stdout, result.stderr) + assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes()) + + malformed_lists = [ + ("unterminated", b"puts", "malformed export list"), + ("blank", b"puts\n\nexit\n", "empty name in export list"), + ("twice", b"puts\nputs\n", "duplicate name in export list"), + ("empty", b"", "malformed export list"), + ] + for name, content, message in malformed_lists: + malformed = Path(directory) / (name + ".exports") + malformed.write_bytes(content) + target = Path(directory) / (name + ".dylib") + target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], [])) + result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr) + result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30) + assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr) + + def check_refusals(patcher, directory): growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)] result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True)) @@ -227,6 +275,7 @@ def main(): check_shrinking(patcher, directory) check_growing(patcher, directory) check_preserved(patcher, directory) + check_export_list(patcher, directory) check_refusals(patcher, directory) print("NID patcher Mach-O tests passed") diff --git a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp index 27f4ec9738b..f3acef34c52 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Init.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Init.cpp @@ -7,21 +7,18 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - extern "C" { -int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { - Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); - Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version"); - return 0; -} - int APS5_VABI sceAgcInit(std::uint32_t version) { - if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT; + if (version >= 14) return 0; Agc::Command::GetRegisterDefaults(version, false, __func__); Agc::Command::GetRegisterDefaults(version, true, __func__); return 0; } +int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) { + Agc::Command::CheckAddress(reinterpret_cast(state), alignof(std::uint32_t), __func__); + return sceAgcInit(version); +} + } diff --git a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp index 608c859ad89..1f3c3ac3c67 100644 --- a/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp @@ -72,7 +72,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32"); } - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps}); std::array values{}; auto* output = regs; regs = values.data(); diff --git a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp index 1044b410f44..59a43354c8b 100644 --- a/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/LinkShaders.cpp @@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist // The simple vertex/pixel pipeline combines two primitive context registers // and 32 interpolant registers. Publish only after both helpers succeed. - AgcDriver::ShaderPreparationScope transaction; + AgcDriver::ShaderPreparationScope transaction({vertex, pixel}); std::array contextValues{}; std::array primitiveValues{}; sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType); diff --git a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp index 587ae15a320..b62666315c9 100644 --- a/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp +++ b/core/libs/prx/libSceAgc/Shader/src/PrimState.cpp @@ -8,6 +8,7 @@ #include "SceShaders.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" #include "prx/libSceAgc/Shader/include/ShaderUtils.hpp" #include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" @@ -56,7 +57,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_ const std::array stages{hs, gs}; const bool patch = prim_type == static_cast(ShaderRegs::PrimitiveType::Patch); if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) { + AgcDriver::ShaderPreparationScope transaction({hs, gs}); AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues); + transaction.Commit(); } if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs); if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs); diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 6fc0d679e7e..7c90e826440 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -52,8 +52,6 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, namespace { -constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast(0x80890001); - void check(bool condition, const char* message) { if (!condition) { throw std::runtime_error(message); @@ -624,12 +622,12 @@ void testDefaults() { check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state"); check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state"); expectFailure([] { sceAgcInit_0090(nullptr, 8); }); - expectFailure([&] { sceAgcInit_0090(&state, 14); }); + check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state"); check(sceAgcInit(8) == 0, "AGC version initialization failed"); check(sceAgcInit(13) == 0, "AGC version 13 initialization failed"); - check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument"); - check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument"); - check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument"); + check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success"); + check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success"); + check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success"); for (std::uint32_t version = 0; version < 14; ++version) { for (const bool internal : {false, true}) { auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__); diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index dca69100435..7b24fd83a06 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -814,6 +814,11 @@ target_link_libraries(agc_formatted_buffer_once_tests PRIVATE shader_recompiler_ set_target_properties(agc_formatted_buffer_once_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_formatted_buffer_once_tests) +add_test_executable(agc_fma_mix_once_tests EXCLUDE_FROM_ALL tests/FmaMixOnce.cpp) +target_link_libraries(agc_fma_mix_once_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_fma_mix_once_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_fma_mix_once_tests) + add_test_executable(agc_rect_list_position_invariant_tests EXCLUDE_FROM_ALL tests/RectListPositionInvariant.cpp) target_link_libraries(agc_rect_list_position_invariant_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_rect_list_position_invariant_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1024,6 +1029,12 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_storage_kept_unit_settle PROPERTIES ENVIRONMENT_MODIFICATION + "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_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only) set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77) @@ -1322,8 +1333,12 @@ endforeach() target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber) +target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc) + target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $) +target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $) + if(BUILD_TESTING) add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state) set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES @@ -1426,7 +1441,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests 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_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_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 agc_formatted_buffer_byte_stores_tests ${agcExecutionTests}) + foreach(agcTest IN ITEMS agc_vertex_input_tests 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_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_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 agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) 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() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index fc2f6d1a7d2..7ae8f979ca4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -62,6 +62,7 @@ class Driver { void ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive); void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); + void SettleShaderPreparations(std::span stages); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request); 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 5ba74600788..9aa51c4f40e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -113,6 +113,7 @@ std::shared_ptr RegisteredProgram(const ShaderRegistry& re void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task, const PreparedShaders* prepared = nullptr); void PrioritizeRegistrationPreparation(PreparedShaders& prepared); +void AwaitRegistrationPreparation(); void SubmitLibraryPrebuild(std::function task); void CloseRegistrationPreparation(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp index 435a8ef4908..2a5314f9ffa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp @@ -162,6 +162,9 @@ class TraceThread { std::span slices; std::vector totals; bool dropped = false; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; }; inline static const std::atomic AlwaysBuffering{true}; @@ -195,7 +198,10 @@ class TraceThread { if (scope == nullptr) continue; snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)}); } - snapshot.dropped = dropped.load(std::memory_order_relaxed); + snapshot.eventsLost = lost.load(std::memory_order_acquire); + if (snapshot.eventsLost != 0) snapshot.eventsFullAt = Clock::time_point(Clock::duration(firstLost.load(std::memory_order_relaxed))); + snapshot.totalsFull = totalsFull.load(std::memory_order_relaxed); + snapshot.dropped = snapshot.eventsLost != 0 || snapshot.totalsFull; return snapshot; } @@ -210,7 +216,9 @@ class TraceThread { void append(const Slice& slice) { const auto index = used.load(std::memory_order_relaxed); if (index == capacity) { - dropped.store(true, std::memory_order_relaxed); + const auto count = lost.load(std::memory_order_relaxed); + if (count == 0) firstLost.store(slice.begin.time_since_epoch().count(), std::memory_order_relaxed); + lost.store(count + 1, std::memory_order_release); return; } slices[index] = slice; @@ -233,7 +241,7 @@ class TraceThread { entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); return; } - dropped.store(true, std::memory_order_relaxed); + totalsFull.store(true, std::memory_order_relaxed); } const std::string name; @@ -242,7 +250,9 @@ class TraceThread { const std::unique_ptr slices; const std::size_t capacity; std::atomic used{0}; - std::atomic dropped{false}; + std::atomic lost{0}; + std::atomic firstLost{0}; + std::atomic totalsFull{false}; std::array entries; bool sampled = false; std::uint64_t sampledPresent = 0; @@ -344,6 +354,10 @@ class TraceFormat { } output += text; } + if (thread.eventsFullAt) { + event("trace buffer full", "i", *thread.eventsFullAt, tid); + output += ",\"s\":\"t\",\"args\":{\"events_lost\":" + std::to_string(thread.eventsLost) + "}}"; + } } for (std::size_t index = 0; index + 1 < presents.size(); ++index) { event("present", "i", presents[index], presenter + 1); @@ -423,8 +437,22 @@ class TraceFormat { bool firstWorker = true; std::map phases; bool dropped = false; + struct DroppedThread { + std::string name; + std::uint64_t eventsLost = 0; + std::optional eventsFullAt; + bool totalsFull = false; + }; + std::vector droppedList; for (const auto& thread : threads) { dropped = dropped || thread.dropped; + if (thread.eventsLost != 0 || thread.totalsFull) { + auto entry = std::find_if(droppedList.begin(), droppedList.end(), [&](const DroppedThread& candidate) { return candidate.name == thread.name; }); + if (entry == droppedList.end()) entry = droppedList.insert(droppedList.end(), DroppedThread{thread.name}); + entry->eventsLost += thread.eventsLost; + if (thread.eventsLost != 0 && thread.eventsFullAt && (!entry->eventsFullAt || *thread.eventsFullAt < *entry->eventsFullAt)) entry->eventsFullAt = thread.eventsFullAt; + entry->totalsFull = entry->totalsFull || thread.totalsFull; + } Clock::duration busy{}; for (const auto& total : thread.totals) { const std::string_view scope = total.scope; @@ -447,6 +475,20 @@ class TraceFormat { } output += "},\"trace_dropped\":"; output += dropped ? "true" : "false"; + if (!droppedList.empty()) { + std::string droppedThreads; + for (const auto& entry : droppedList) { + if (!droppedThreads.empty()) droppedThreads += ','; + AppendString(droppedThreads, entry.name); + droppedThreads += ":{"; + if (entry.eventsLost != 0) { + if (entry.eventsFullAt && !presents.empty() && *entry.eventsFullAt >= presents.front() && *entry.eventsFullAt <= presents.back()) droppedThreads += "\"events_full_after_s\":" + number(std::chrono::duration(*entry.eventsFullAt - presents.front()).count()) + ","; + droppedThreads += "\"events_lost\":" + std::to_string(entry.eventsLost) + ","; + } + droppedThreads += std::string("\"totals_full\":") + (entry.totalsFull ? "true" : "false") + "}"; + } + output += ",\"trace_dropped_threads\":{" + droppedThreads + "}"; + } output += ",\"trace\":"; AppendString(output, summary.trace); output += "}\n"; @@ -483,7 +525,7 @@ class PerformanceTrace { if (!buffering.load(std::memory_order_relaxed)) return; std::lock_guard lock(gpuMutex); if (gpuRanges.size() == gpuRanges.capacity()) { - gpuDropped = true; + ++gpuLost; return; } gpuRanges.push_back({name, beginNs, endNs, batch}); @@ -509,7 +551,7 @@ class PerformanceTrace { inline static thread_local TraceThread* local = nullptr; inline static std::mutex gpuMutex; inline static std::vector gpuRanges; - inline static bool gpuDropped = false; + inline static std::uint64_t gpuLost = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp index 0c314803071..30fcf5b8f43 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp @@ -1,11 +1,16 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP +#include +#include + +struct Shader; + namespace AgcDriver::DriverDetail { class ShaderPreparationTransaction; } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(); +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count); extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction); extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept; @@ -13,7 +18,7 @@ namespace AgcDriver { class ShaderPreparationScope { public: - ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {} + explicit ShaderPreparationScope(std::initializer_list shaders) : transaction(AgcDriverBeginShaderPreparation_nid_postfix(shaders.begin(), shaders.size())) {} ~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); } ShaderPreparationScope(const ShaderPreparationScope&) = delete; ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 374129a92bf..9a23e42c8a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -75,7 +75,10 @@ void Driver::dispatch(QueueState& queue, std::span packet, if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; if (!partialLastGroup && indirectArguments == 0 && matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return; } else { - if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) Graphics::NoteHtileDepthClear(htile); + if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) { + const auto threads = (packet[4] & 0x20u) != 0 ? static_cast(packet[1]) : static_cast(packet[1]) * compute.numThreads[0]; + Graphics::NoteHtileDepthClear(htile, static_cast(threads * 16u)); + } if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) { if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill); } 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 8858b58f2c1..1121e825174 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -173,7 +173,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe std::vector recompiled(programs.size(), false); bool drawHit = false; bool verifyHit = false; - lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, useDrawEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); + const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u); + lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs); if (registerKey) { @@ -231,7 +232,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe pushCursorBytes += static_cast(result.pushConstants.size()); } - cacheDrawStages(useDrawEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { @@ -325,6 +326,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex); patch(startInstanceWord, arguments.firstInstance); patch(drawIndexWord, record); + if (graphics.stages.tessellation) { + for (std::size_t i = 0; i < programs.size(); ++i) { + if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i); + } + } Pm4::DrawParameters direct{0, arguments.count, 0, arguments.instances, drawParameters.flags, drawParameters.indexed, 0, 0}; if (drawParameters.indexed) { @@ -343,7 +349,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe auto& result = results[resultIndex[programIndex]]; const auto pushBytes = result.pushConstants.size(); decodeVertexInfo(programIndex); - result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); + result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected); if (!rejected.empty()) return DrawVerdict::Rejected; require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout"); } 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 4f3eb618917..3609990f909 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -192,6 +192,12 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } + if (header == FlipPacketHeader) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + static_cast(Graphics::StorageTexture::StoreSmallPending("flip")); + } + static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader) : (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip))); 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 db3f64fa8d4..3d428973f27 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -298,6 +298,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now(); waitForFlipRoom(submission); holdFlipBehindWorker(submission); + AwaitRegistrationPreparation(); if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now(); static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr; if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast(descriptor.addr)); 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 9a4a6e63eb7..84d7e036cfb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -894,8 +895,10 @@ class RegistrationPreparePool { { std::unique_lock lock(mutex); if (threads != 0 && !closed) { - if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared}); - else tasks.push_back({std::move(task), prepared}); + const auto sequence = ++submitted; + if (prepared != nullptr && prepared->demanded.load()) tasks.push_front({std::move(task), prepared, sequence}); + else tasks.push_back({std::move(task), prepared, sequence}); + unfinished.insert(sequence); lock.unlock(); ready.notify_one(); return; @@ -912,6 +915,12 @@ class RegistrationPreparePool { std::rotate(tasks.begin(), found, std::next(found)); } + void AwaitSubmitted() { + std::unique_lock lock(mutex); + const auto ticket = submitted; + idle.wait(lock, [&] { return unfinished.empty() || *unfinished.begin() > ticket; }); + } + void Close() { std::unique_lock lock(mutex); closed = true; @@ -945,14 +954,16 @@ class RegistrationPreparePool { pool.tasks.pop_front(); ++pool.running; } + const auto sequence = task.sequence; task.function(); task = {}; bool drained = false; { std::lock_guard lock(pool.mutex); --pool.running; + pool.unfinished.erase(sequence); drained = pool.tasks.empty() && pool.running == 0; - if (drained) pool.idle.notify_all(); + pool.idle.notify_all(); } #ifdef __GLIBC__ if (drained) trimFreedMemory(); @@ -964,6 +975,7 @@ class RegistrationPreparePool { struct Task { std::function function; const PreparedShaders* prepared = nullptr; + std::uint64_t sequence = 0; }; std::mutex mutex; @@ -972,6 +984,8 @@ class RegistrationPreparePool { std::deque tasks; unsigned threads = 0; unsigned running = 0; + std::uint64_t submitted = 0; + std::set unfinished; bool closed = false; }; @@ -1017,6 +1031,10 @@ void PrioritizeRegistrationPreparation(PreparedShaders& prepared) { if (auto* pool = RegistrationPreparePool::Existing()) pool->Prioritize(prepared); } +void AwaitRegistrationPreparation() { + if (auto* pool = RegistrationPreparePool::Existing()) pool->AwaitSubmitted(); +} + void SubmitLibraryPrebuild(std::function task) { RegistrationPreparePool::Prebuild().Submit(std::move(task)); } @@ -1271,6 +1289,22 @@ void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std:: transaction.Commit(); } +void Driver::SettleShaderPreparations(std::span stages) { + std::vector> snapshots; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return; + for (const auto* shader : stages) { + if (shader == nullptr) continue; + if (auto snapshot = RegisteredHeader(*shaders, shader)) snapshots.push_back(std::move(snapshot)); + } + } + for (const auto& snapshot : snapshots) { + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + } +} + void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) { ShaderPreparationTransaction transaction; require(fragment.get() != &front, "rectangle stages refer to the same shader"); @@ -1441,7 +1475,8 @@ void Driver::RegisterShader(const Shader* shader) { } -extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() { +extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix(const Shader* const* shaders, std::size_t count) { + AgcDriver::DriverDetail::Driver::Get().SettleShaderPreparations({shaders, count}); return new AgcDriver::DriverDetail::ShaderPreparationTransaction(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp index 862c10d8486..c913b43155e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceTrace.cpp @@ -221,6 +221,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const { std::lock_guard gpu(gpuMutex); gpuRanges.clear(); + gpuLost = 0; gpuRanges.reserve(GpuRangeCapacity); } state.presents.push_back(now); @@ -256,7 +257,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const if (state.gpuClock && device != nullptr && device->CalibrateClock(clock.gpuNs, clock.cpu)) { std::lock_guard gpu(gpuMutex); gpuSlices = TraceFormat::GpuSlices(gpuRanges, *state.gpuClock, clock, state.presents.front()); - snapshots.push_back({"gpu", gpuSlices, {}, gpuDropped}); + snapshots.push_back({"gpu", gpuSlices, {}, gpuLost != 0, gpuLost}); } const auto process = processId(); auto summary = identity(width, height, device); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp index 063205cbee2..7dc7a4640ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderDeviceProfile.cpp @@ -30,6 +30,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const auto* barycentric = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR); const auto* mesh = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT); const auto* clock = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR); + const auto* drawParameters = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES); const auto* maintenance8 = findFeatures(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR); Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI"); Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64"); @@ -64,6 +65,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE; checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric"); checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader"); + checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters"); checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock"); Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics"); Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c8830d353d9..eefe712a194 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1411,6 +1411,14 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : enabled.tessellationShader = available.tessellationShader; state->tessellationShader = enabled.tessellationShader == VK_TRUE; if (state->tessellationShader) state->capabilities.push_back(3); + VkPhysicalDeviceShaderDrawParametersFeatures drawParametersFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES}; + { + VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &drawParametersFeatures}; + state->InstanceFunction("vkGetPhysicalDeviceFeatures2")(selected, &features); + } + const bool drawParameters = drawParametersFeatures.shaderDrawParameters == VK_TRUE; + drawParametersFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES, nullptr, VK_TRUE}; + if (drawParameters) state->capabilities.push_back(spv::CapabilityDrawParameters); APS5_LOG_OUT("Vulkan features tessellationAvailable=%u mesh=%u depthClip=%u depthRangeUnrestricted=%u pipelineLibrary=%u", static_cast(state->tessellationShader), static_cast(state->meshShader), static_cast(state->depthClipControl), static_cast(state->depthRangeUnrestricted), static_cast(state->graphicsPipelineLibrary)); require(available.samplerAnisotropy && available.textureCompressionBC, "device lacks sampler anisotropy or BC texture compression support required for texture sampling"); enabled.samplerAnisotropy = VK_TRUE; @@ -1603,6 +1611,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : descriptorIndexingFeatures.pNext = byteFeatures.pNext; byteFeatures.pNext = &descriptorIndexingFeatures; } + if (drawParameters) { + drawParametersFeatures.pNext = byteFeatures.pNext; + byteFeatures.pNext = &drawParametersFeatures; + } // Timeline semaphores let a queue worker wait for recorded batches without holding the GPU mutex // (see Recorder::WaitSerial). The instance is 1.1, so the KHR extension is used even on 1.2+ // devices. Debug aid: APS5_NO_TIMELINE=1 leaves it off (drains wait under the mutex as before). diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 052e452a625..1a8f3fa00c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -22,7 +22,7 @@ void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); bool DepthSurfaceAt(const Context& context, std::span words, const GuestTextureResource& resource); std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads); -void NoteHtileDepthClear(std::uint64_t htileAddress); +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes); struct MemoryValueFill { std::uint64_t address; std::size_t bytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 997e9dc7e74..e7447ef57b1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -58,7 +58,8 @@ enum class TextureDimension { k2DArray, kCube, k3D, - k1DArray + k1DArray, + k2DMsaa }; struct GuestTextureResource { @@ -86,10 +87,12 @@ struct GuestTextureResource { bool dccPipeAligned = false; std::uint32_t minLod = 0; std::uint32_t allocatedMipCount = 0; + std::uint32_t samples = 1; }; float EffectiveMinLod(const GuestTextureResource& resource); +bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); bool DescriptorSingleLevel(std::span words); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index bfcc8fd5347..62832614458 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -6,12 +6,15 @@ #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include #include #include namespace AgcDriver::Graphics { class StorageTexture; +class Texture; +struct GuestTextureResource; class MultisampledCmask { public: @@ -49,6 +52,8 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target); bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear); bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 493436dcbc0..818b4e2926d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -218,6 +218,7 @@ class ShaderResources { // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); const std::vector>& SampledTextures() const { return textures; } bool ChecksFaults() const { return bda != nullptr; } @@ -479,6 +480,7 @@ class ShaderResources { std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; bool usesBda = false; + std::vector multisampleSources; bool usesFaultBuffer = false; bool faultChecks = false; VkDescriptorSetLayout _layout = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index df145d6d236..172ca2f31c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -141,13 +141,13 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); -ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); +ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); GuestTextureResource SurfaceForTarget(const ColorTarget& color); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress, Resolve }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve, FmaskDecompress }; Mode mode; std::vector targets; std::optional source; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f9a0bdd6b69..d88555ba86e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -61,7 +61,7 @@ class Texture { // compute pass writing it and the next pass sampling it share one image and copy nothing. // CanCopyFrom says whether the two descriptors address the same surface compatibly. Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components); - Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); + Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); static VkFormat DepthCopyFormat(VkFormat colorFormat); @@ -194,6 +194,8 @@ class StorageTexture : public std::enable_shared_from_this { 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 void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes); + static constexpr std::uint64_t SmallStoreBytes = 64 * 1024; + static std::size_t StoreSmallPending(const char* reason); static bool AnyPending(); static void FlushDumpSelected(const char* reason); static bool StoreAtFlipRequested(const char* value); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index f5d31678408..9d3bc9a9ac6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -213,7 +213,7 @@ class DepthSurface { if (plane.storage != nullptr) plane.replaced.push_back(plane.storage); plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); } - copyPlane(plane, stencil, true); + copyPlane(plane, plane.storage, stencil, true); return plane.storage; } @@ -221,7 +221,7 @@ class DepthSurface { for (const bool stencil : {false, true}) { auto& plane = stencil ? stencilPlane : depthPlane; if (plane.storage.get() == &candidate) { - copyPlane(plane, stencil, false); + copyPlane(plane, plane.storage, stencil, false); written = ++depthWrites; return true; } @@ -233,6 +233,18 @@ class DepthSurface { return false; } + bool StoreBackCovering(const StorageTexture& candidate) { + for (const bool stencil : {false, true}) { + if (!CoversPlane(candidate, stencil)) continue; + auto& plane = stencil ? stencilPlane : depthPlane; + const auto source = std::const_pointer_cast(candidate.shared_from_this()); + copyPlane(plane, source, stencil, false); + written = ++depthWrites; + return true; + } + return false; + } + const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -250,20 +262,30 @@ class DepthSurface { PlaneStorage depthPlane; PlaneStorage stencilPlane; - void copyPlane(PlaneStorage& plane, bool stencil, bool toStorage) { + bool CoversPlane(const StorageTexture& storage, bool stencil) const { + const auto& resource = storage.Descriptor(); + const auto address = stencil ? target.stencilAddress : target.address; + if (address == 0 || resource.baseAddress != address || resource.width != target.extent.width || resource.height != target.extent.height || resource.baseLevel != 0 || resource.mipCount != 1) return false; + if (resource.dimension != TextureDimension::k2D && (resource.dimension != TextureDimension::k2DArray || resource.baseArray != 0 || resource.depthOrLastArray != 0)) return false; + if (target.samples != 1 || (!stencil && hostFormat != target.format)) return false; + const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; + return ResolveTextureFormat(resource.format) == (stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT); + } + + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage) { auto* recorder = Recorder::Active(); Require(recorder != nullptr, "depth surface storage copies need an active recorder"); const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytes = static_cast(target.extent.width) * target.extent.height * (stencil ? 1u : d16 ? 2u : 4u); if (plane.staging == nullptr) plane.staging = std::make_unique(context, bytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); const auto commands = recorder->Commands(); - recorder->Keep(plane.storage); + recorder->Keep(storage); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); VkBufferImageCopy planeRegion{0, 0, 0, {stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : VK_IMAGE_ASPECT_DEPTH_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}}; VkBufferImageCopy colorRegion = planeRegion; colorRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - const auto sourceImage = toStorage ? image : plane.storage->Image(); - const auto destinationImage = toStorage ? plane.storage->Image() : image; + const auto sourceImage = toStorage ? image : storage->Image(); + const auto destinationImage = toStorage ? storage->Image() : image; RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); context.Function("vkCmdCopyImageToBuffer")(commands, sourceImage, VK_IMAGE_LAYOUT_GENERAL, plane.staging->Handle(), 1, toStorage ? &planeRegion : &colorRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); @@ -316,7 +338,8 @@ class DepthPlaneCopy { allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory depth plane copy"); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory depth plane copy"); - staging = std::make_unique(context, static_cast(sliceBytes()) * layers, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = static_cast(sliceBytes()) * layers; + staging = std::make_unique(context, stagingSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } catch (...) { release(); throw; @@ -326,10 +349,21 @@ class DepthPlaneCopy { DepthPlaneCopy(const DepthPlaneCopy&) = delete; DepthPlaneCopy& operator=(const DepthPlaneCopy&) = delete; - std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) { + std::shared_ptr Refresh(std::span slices, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) { Require(slices.size() == layers, "depth plane copy slices do not match its layers"); const auto geometry = DescribeSurface(resource); - Require(geometry.layers == layers && geometry.sliceLinearBytes == sliceBytes() && !geometry.mips.empty(), "depth plane copy geometry does not match its layers"); + const bool guestFill = std::any_of(slices.begin(), slices.end(), [](const auto& slice) { return slice.image == VK_NULL_HANDLE; }); + if (guestFill && (geometry.layers != layers || geometry.sliceLinearBytes < sliceBytes() || geometry.mips.empty())) { + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: depth plane copy geometry does not match its layers (copy %ux%u, %u layers, %llu bytes a slice; texture %ux%u guest format %u, dimension %d, tile mode %u, %u layers, %llu bytes a slice, %zu mips)", extent.width, extent.height, layers, static_cast(sliceBytes()), resource.width, resource.height, resource.format, static_cast(resource.dimension), resource.tileMode, geometry.layers, static_cast(geometry.sliceLinearBytes), geometry.mips.size()); + throw std::runtime_error(text); + } + const auto stagingBytes = static_cast(geometry.sliceLinearBytes) * layers; + if (stagingBytes > stagingSize) { + retiredStaging.push_back(std::move(staging)); + staging = std::make_unique(context, stagingBytes, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + stagingSize = stagingBytes; + } std::vector> uploads; std::vector uploadLayers; std::vector copiedLayers; @@ -356,10 +390,10 @@ class DepthPlaneCopy { held = current; } if (!copiedLayers.empty() || !uploads.empty()) record(slices, resource, geometry, copiedLayers, uploads, uploadLayers); - const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType)}; + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a), static_cast(viewType), baseLayer}; auto& texture = textures[key]; if (texture == nullptr) { - texture = std::make_shared(context, image, format, aspect(), components, viewType); + texture = std::make_shared(context, image, format, aspect(), components, viewType, baseLayer); texture->MarkRefreshedPerUse(); } return texture; @@ -382,7 +416,9 @@ class DepthPlaneCopy { VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; std::unique_ptr staging; - std::map, std::shared_ptr> textures; + std::size_t stagingSize = 0; + std::vector> retiredStaging; + std::map, std::shared_ptr> textures; std::vector guestLayers; std::vector surfaceLayers; bool filled = false; @@ -399,9 +435,11 @@ class DepthPlaneCopy { const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier"); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::vector regions; + const auto& mip = geometry.mips.front(); const auto region = [&](std::uint32_t layer, VkImageAspectFlags aspectMask, std::uint32_t imageLayer) { VkBufferImageCopy copy{}; - copy.bufferOffset = sliceBytes() * layer; + copy.bufferOffset = geometry.LinearLayerOffset(layer) + mip.linearOffset; + copy.bufferRowLength = mip.pitchBytes / (format == VK_FORMAT_D32_SFLOAT ? 4u : 2u); copy.imageSubresource = {aspectMask, 0, imageLayer, 1}; copy.imageExtent = {extent.width, extent.height, 1}; return copy; @@ -617,11 +655,20 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -std::set& clearedHtiles() { - static auto* cleared = new std::set(); +std::map& clearedHtiles() { + static auto* cleared = new std::map(); return *cleared; } +bool takeClearedHtile(const DepthTarget& target) { + if (target.htileAddress == 0) return false; + auto& cleared = clearedHtiles(); + const auto found = std::find_if(cleared.begin(), cleared.end(), [&](const auto& entry) { return HtileFillCovers(target.htileAddress, target.extent, entry.first, entry.second); }); + if (found == cleared.end()) return false; + cleared.erase(found); + return true; +} + const DepthSurface* drawnDepth = nullptr; std::atomic surfacesSerial{0}; @@ -678,7 +725,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); - const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; + const bool cleared = takeClearedHtile(target); DepthSurface* surface = nullptr; for (const auto& candidate : surfaces()) { if (candidate->context.device == context.device && sameSurface(candidate->target, target)) { @@ -736,15 +783,15 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) { NoteDepthMetadataFill(fill.address, fill.bytes, value); } -void NoteHtileDepthClear(std::uint64_t htileAddress) { +void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes) { std::lock_guard lock(surfacesMutex()); bool existing = false; for (const auto& surface : surfaces()) { - if (surface->target.htileAddress != htileAddress) continue; + if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue; surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT; existing = true; } - if (!existing) clearedHtiles().insert(htileAddress); + if (!existing) clearedHtiles()[htileAddress] = bytes; } void ClearDepthSurfaces(VkDevice device) { @@ -786,14 +833,15 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::spanhostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented"); const auto copyFormat = integer ? VK_FORMAT_R16_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u; - if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || resource.baseArray != 0 || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { + if ((format != copyFormat && !depthBits) || resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0 || resource.lastLevel != 0 || (resource.baseArray != 0 && (cube || !layered || resource.baseArray > resource.depthOrLastArray)) || (cube && resource.depthOrLastArray != 0 && resource.depthOrLastArray != 5) || (resource.dimension != TextureDimension::k2D && resource.dimension != TextureDimension::k2DArray && !cube)) { char text[320]; std::snprintf(text, sizeof(text), "AGC graphics: sampling the depth planes of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), dimension %d, %u slices, levels %u-%u, base slice %u is not implemented", static_cast(base.address), base.extent.width, base.extent.height, static_cast(base.format), resource.width, resource.height, resource.format, static_cast(format), static_cast(resource.dimension), layers, resource.baseLevel, resource.lastLevel, resource.baseArray); throw std::runtime_error(text); } std::vector slices; + const auto geometry = DescribeSurface(resource); for (std::uint32_t layer = 0; layer < layers; ++layer) { - const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, d16 ? 2u : 4u); + const auto address = base.address + geometry.GuestLayerOffset(layer); auto* slice = depthSlice(context.device, address, base); if (slice != nullptr) slice->ApplyFastClear(); if (slice == nullptr) slices.push_back({}); @@ -804,7 +852,7 @@ std::shared_ptr DepthSurfaceTexture(const Context& context, std::span(context, base.extent, imageFormat, layers); } - return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D); + return copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u); } bool DepthSurfaceHolds(const Context& context, const GuestTextureResource& resource, const Texture* texture) { @@ -917,6 +965,9 @@ bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage for (const auto& surface : surfaces()) { if (surface->context.device == context.device && surface->StoreBack(storage)) return true; } + for (auto surface = surfaces().rbegin(); surface != surfaces().rend(); ++surface) { + if ((*surface)->context.device == context.device && (*surface)->StoreBackCovering(storage)) return true; + } return false; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index ee37b74a072..6b1fd6f8e5d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2105,6 +2105,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& 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"); + for (const auto& binding : targets) { + if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + } 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(); @@ -2570,6 +2573,10 @@ void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); return; } + if (pass.mode == ColorMetadataPass::Mode::FmaskDecompress) { + for (const auto& color : pass.targets) MaterializeMultisampleTargetClear(context, color); + 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/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 55ecd30e305..87c57c12788 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -44,12 +44,20 @@ TextureDimension resolveDimension(std::uint32_t raw) { case 11: return TextureDimension::kCube; case 12: return TextureDimension::k1DArray; case 13: return TextureDimension::k2DArray; + case 14: return TextureDimension::k2DMsaa; default: throw std::runtime_error("AGC graphics: guest texture descriptor uses an unsupported image type " + std::to_string(raw)); } } } +bool IsNullTextureResource(std::span words) { + Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); + const auto base40 = (static_cast(words[0]) | (static_cast(words[1]) << 32u)) & 0xffffffffffull; + const auto type = (words[3] >> 28u) & 0xfu; + return base40 == 0 || type == 0; +} + GuestTextureResource DecodeTextureResource(std::span words) { Require(words.size() == 8, "guest texture descriptor must contain 8 dwords"); @@ -76,7 +84,7 @@ GuestTextureResource DecodeTextureResource(std::span words) const auto baseArray = (words[4] >> 16u) & 0x1fffu; const auto arrayPitch = (words[5] >> 0u) & 0xfu; - const auto maxMip = (words[5] >> 4u) & 0xfu; + auto maxMip = (words[5] >> 4u) & 0xfu; const auto minLodWarn = (words[5] >> 8u) & 0xfffu; const auto cornerSample = ((words[5] >> 23u) & 0x1u) != 0; const auto mipStatsCntEn = ((words[5] >> 25u) & 0x1u) != 0; @@ -138,6 +146,13 @@ GuestTextureResource DecodeTextureResource(std::span words) } catch (const std::exception& error) { throw std::runtime_error(std::string(error.what()) + describe()); } + std::uint32_t samples = 1; + if (dimension == TextureDimension::k2DMsaa) { + Require(baseLevel == 0 && lastLevel >= 1u && lastLevel <= 4u && maxMip == lastLevel, "guest multisampled texture descriptor does not hold the same sample count of 2 to 16 in its last level and its last mip, which is not implemented" + describe()); + samples = 1u << lastLevel; + lastLevel = 0; + maxMip = 0; + } // Views may name levels past MAX_MIP (a 512x512 view through 1x1 over a 9-level surface); the // hardware never addresses them, so the view ends at the surface's last level. if (baseLevel <= maxMip) lastLevel = std::min(lastLevel, maxMip); @@ -161,6 +176,9 @@ GuestTextureResource DecodeTextureResource(std::span words) } Require(depth == 0 && baseArray == 0, "guest 2D texture descriptor has a nonzero depth or base array"); break; + case TextureDimension::k2DMsaa: + Require(depth == 0 && baseArray == 0, "guest multisampled 2D texture descriptor has a nonzero depth or base array"); + break; case TextureDimension::k3D: // word4 DEPTH holds the depth minus one; BASE_ARRAY has no meaning for volumes. Require(baseArray == 0, "guest 3D texture descriptor has a nonzero base array"); @@ -207,6 +225,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.dccAlphaOnMsb = dccAlphaPos; result.dccPipeAligned = metaPipeAligned; result.minLod = minLod; + result.samples = samples; if (baseLevel > maxMip) { Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe()); result.mipCount = lastLevel + 1u; @@ -231,6 +250,7 @@ bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, Texture case ShaderRecompiler::DescriptorImageShape::ImageCube: return dimension == TextureDimension::kCube; case ShaderRecompiler::DescriptorImageShape::Image3D: return dimension == TextureDimension::k3D; case ShaderRecompiler::DescriptorImageShape::Image1DArray: return dimension == TextureDimension::k1DArray; + case ShaderRecompiler::DescriptorImageShape::Image2DMultisample: return dimension == TextureDimension::k2DMsaa; } throw std::runtime_error("AGC graphics: MatchesGuestDimension encountered an unknown descriptor image shape"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 792b1395a02..87b9158eb2a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -54,8 +56,10 @@ class MultisampleTarget { 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; + VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; VkImageFormatProperties supported{}; + sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0; + if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT; 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 { @@ -120,6 +124,23 @@ class MultisampleTarget { }); } + std::shared_ptr Sampled(VkComponentMapping components) { + Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count"); + Require(cmask.Refusal().empty(), cmask.Refusal()); + Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented"); + Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches"); + ApplyFastClear(target); + recordCommands(context, [&](VkCommandBuffer commands, Recorder*) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + Recorder::CountBarriers(Recorder::CommandClass::Draw, 1); + }); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}; + if (const auto found = textures.find(key); found != textures.end()) return found->second; + auto texture = std::make_shared(context, image, target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + textures.emplace(key, texture); + return texture; + } + VkRenderPass ResolvePass() { if (resolvePass != VK_NULL_HANDLE) return resolvePass; std::array attachments{}; @@ -164,7 +185,11 @@ class MultisampleTarget { MultisampledColorFill colorFill; private: + bool sampleable = false; + std::map, std::shared_ptr> textures; + void release() noexcept { + textures.clear(); 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); @@ -380,6 +405,28 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar return acquire(context, target).view; } +std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components); + other = target.get(); + } + char text[320]; + if (other != nullptr) std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); + else std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to, is not implemented", resource.width, resource.height, static_cast(resource.baseAddress), resource.samples); + throw std::runtime_error(text); +} + +void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + acquire(context, target); +} + void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { std::lock_guard lock(targetsMutex()); auto& target = acquire(context, source); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e89fa2b31da..2c120f5cd81 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -524,6 +524,11 @@ 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)); + if (resource.dimension == TextureDimension::k2DMsaa) { + auto target = MultisampleTargetTexture(context, resource, components); + logLookup({target.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); + return target; + } 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)) { @@ -868,6 +873,7 @@ 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) { + Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented"); RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth; static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; @@ -2003,7 +2009,7 @@ void ShaderResources::noteReusable() { captureValidation(); reusable = false; directRegions.clear(); - if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || (bda != nullptr && LoopGuardEnabled())) return; + if (guestMemory.HasCopiedWrites() || HoldsLease() || usesBda || !multisampleSources.empty() || (bda != nullptr && LoopGuardEnabled())) return; if (std::any_of(textures.begin(), textures.end(), [](const std::shared_ptr& texture) { return texture != nullptr && texture->RefreshedPerUse(); })) return; if (TemplateDataRefresh() && std::any_of(allocations.begin(), allocations.end(), [](const Allocation& allocation) { return allocation.buffer != nullptr && !allocation.guest && allocation.role != ShaderRecompiler::DescriptorRole::ImageTableMap && allocation.size > MaxRefreshBytes; })) return; const auto regions = guestMemory.DirectRegions(); @@ -2671,7 +2677,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (textureIndex >= textures.size()) return OwnRefreshFallback::Uncached; const auto index = textureIndex++; const auto overlap = listed(index, false); @@ -2681,7 +2687,7 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return OwnRefreshFallback::Uncached; const auto index = storageIndex++; if (listed(index, true) == overlapping.end()) continue; @@ -2769,7 +2775,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto elementWords = binding.count != 0 ? binding.guestDescriptor.size() / binding.count : 0; for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; const auto resource = DecodeTextureResource(words); const VkComponentMapping components = ViewComponents(resource); if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false; @@ -2778,7 +2784,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } else if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) { for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8u, 8u); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + if (IsNullTextureResource(words)) continue; if (storageIndex >= storageTextures.size()) return false; std::shared_ptr expected; if (SameAsPreviousStorageElement(binding, element) && StorageDedupeEnabled()) expected = storageTextures[storageIndex - 1]; @@ -3754,7 +3760,7 @@ bool ShaderResources::precollectImages() { } ImageRecord record; record.sampled = sampled; - if (words[0] != 0u || (words[1] & 0xffu) != 0u) { + if (!IsNullTextureResource(words)) { const auto* memo = words.size() == 8 ? &decodedImage(context.device, words) : nullptr; record.resource = memo != nullptr ? memo->resource : DecodeTextureResource(words); record.guestBytes = memo != nullptr ? memo->guestBytes : DescribeSurface(record.resource).guestBytes; @@ -3863,7 +3869,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedSampledImages; continue; @@ -3885,6 +3891,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare); + if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) { const auto range = texture->SampledViewRange(firstLayer); @@ -3932,7 +3939,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind if (sameAsPrevious) ++mipOffset; else mipOffset = 0; const auto* record = nextRecord(); - if (words[0] == 0u && (words[1] & 0xffu) == 0u) { + if (IsNullTextureResource(words)) { item.imageAllocations.push_back(std::numeric_limits::max()); --plannedStorageImages; continue; @@ -4267,7 +4274,7 @@ std::vector> ShaderResources::PresyncSur // Stage A has not run (an address-based build, or the pass is off): decoded from the shader. if (deferredCompute.program == nullptr) return surfaces; forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span words) { - if (words[0] == 0u && (words[1] & 0xffu) == 0u) return; + if (IsNullTextureResource(words)) return; const auto resource = DecodeTextureResource(words); consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage); }); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 5e16fd73f0a..73a321a2d40 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -140,7 +140,7 @@ struct Module { } const auto signature = Signature(type); if (vertex && storage == spv::StorageClassInput) { - Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex) && signature == "i32", "unsupported vertex built-in input"); + Require((value == spv::BuiltInVertexIndex || value == spv::BuiltInInstanceIndex || value == spv::BuiltInBaseInstance) && signature == "i32", "unsupported vertex built-in input"); } else if (vertex && storage == spv::StorageClassOutput) { if (value == spv::BuiltInLayer && layerOutput && (signature == "u32" || signature == "i32")) return; Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); @@ -263,6 +263,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityImageGatherExtended || capability == spv::CapabilityImageQuery || ((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) || + (vertex && capability == spv::CapabilityDrawParameters) || (fragment && geometryShader && capability == spv::CapabilityGeometry) || (fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) || capability == spv::CapabilityImageMSArray || diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 973a5a6fe6a..8811ce32e89 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -292,7 +292,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx depth.clearDepth = readFloat(cx, 0x00b); depth.clearStencil = static_cast(read(cx, 0x00a) & 0xffu); if (const auto htile = find(cx, 0x005); zFormat != 0 && htile != cx.end() && htile->second != 0 && (read(cx, 0x010) & (1u << 29u)) != 0) { - depth.htileAddress = base(0x005, 0x01e); + depth.htileAddress = base(0x005, 0x01e) + static_cast(view & 0x1fffu) * HtileMetadataBytes(depth.extent); depth.htileStencil = stencil && (read(cx, 0x011) & (1u << 29u)) == 0; } result.depth = depth; @@ -1107,7 +1107,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) { return surface; } -ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { +ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allowMultisampleDcc) { const auto stride = slot * 0xfu; ColorTarget color{}; color.slot = slot; @@ -1123,7 +1123,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { 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"); + Require(allowMultisampleDcc || 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 | 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"); @@ -1248,8 +1248,9 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto sourceAttrib = find(cx, 0x31d); const bool sourceSamples = sourceAttrib != cx.end() && ((sourceAttrib->second >> 12u) & 7u) != 0; const bool resolve = mode == 3u && (rasterizerSamples || sourceSamples); - 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}; + const bool fmaskDecompress = mode == 5u; + if (mode != 2u && mode != 6u && !resolve && !fmaskDecompress) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : fmaskDecompress ? ColorMetadataPass::Mode::FmaskDecompress : 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"); const auto value = [&](std::uint32_t offset) { const auto found = find(cx, offset); @@ -1299,8 +1300,13 @@ std::optional DecodeColorMetadataPass(const QueueState& queue 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)"); + const auto target = DecodeColorBuffer(cx, slot, fmaskDecompress); + if (fmaskDecompress) { + Require(target.samples > 1, "CB metadata pass over a single-sample target"); + Require(target.cmaskAddress != 0, "FMASK decompress of a target without a CMASK address"); + } else { + 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a57623bdb1c..673b90f1bb3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -211,6 +211,7 @@ VkImageViewType ViewTypeFor(TextureDimension dimension, [[maybe_unused]] std::ui case TextureDimension::kCube: return VK_IMAGE_VIEW_TYPE_2D_ARRAY; case TextureDimension::k3D: return VK_IMAGE_VIEW_TYPE_3D; case TextureDimension::k1DArray: return VK_IMAGE_VIEW_TYPE_1D_ARRAY; + case TextureDimension::k2DMsaa: break; } throw std::runtime_error("AGC graphics: Texture encountered an unknown guest texture dimension"); } @@ -779,7 +780,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } -Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType) : context(context) { +Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType, std::uint32_t baseLayer) : context(context) { layout = VK_IMAGE_LAYOUT_GENERAL; VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = depthImage; @@ -787,7 +788,7 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma viewInfo.format = depthFormat; viewFormat = depthFormat; viewInfo.components = components; - viewInfo.subresourceRange = {aspect, 0, 1, 0, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; + viewInfo.subresourceRange = {aspect, 0, 1, baseLayer, viewType == VK_IMAGE_VIEW_TYPE_2D ? 1u : VK_REMAINING_ARRAY_LAYERS}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane"); viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; if (viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return; @@ -3323,6 +3324,26 @@ void StorageTexture::FlushAllPending(const char* reason) { static_cast(FlushPending(0, std::numeric_limits::max(), nullptr, reason)); } +std::size_t StorageTexture::StoreSmallPending(const char* reason) { + std::vector> small; + { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (texture->guestBytes > SmallStoreBytes) continue; + if (auto alive = texture->weak_from_this().lock()) small.push_back(std::move(alive)); + } + } + std::size_t stored = 0; + for (const auto& texture : small) { + const auto address = texture->descriptor.baseAddress; + const auto bytes = static_cast(texture->guestBytes); + if (!HostImportCovers(texture->context, address, bytes)) continue; + if (FlushPending(address, bytes, nullptr, reason)) ++stored; + } + return stored; +} + bool StorageTexture::AnyPending() { auto& pending = Pending(); std::lock_guard lock(pending.mutex); @@ -4450,18 +4471,16 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { if (layers[layer]) layerPending[layer] = false; } - const bool allKept = std::none_of(skippedLayer.begin(), skippedLayer.end(), [](bool kept) { return kept; }); - if (allKept || !adjacent.empty()) { - const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); - for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { - if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; - } - refreshGeneration(); - advanceAdjacent(adjacent, now, firstBlock, lastBlock); - if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); - } else if (!shadowed.empty()) { - MarkShadowed(*shadowImport, shadowed, GuestMemory::TrackerGeneration()); + bool advances = false; + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) advances = advances || (layers[layer] && !skippedLayer[layer]); + if (!advances && adjacent.empty() && shadowed.empty()) return; + const auto now = memoizedCollect ? GuestMemory::CollectWrites(firstStored, static_cast(lastStored - firstStored)) : GuestMemory::CollectWritesUncached(firstStored, static_cast(lastStored - firstStored)); + for (std::uint32_t layer = 0; layer < trackedLayers; ++layer) { + if (layers[layer] && !skippedLayer[layer]) layerGeneration[layer] = now; } + refreshGeneration(); + advanceAdjacent(adjacent, now, firstBlock, lastBlock); + if (!shadowed.empty()) MarkShadowed(*shadowImport, shadowed, now); }; if (keep.empty()) { // Every selected block was written by the CPU since the layer's generation: nothing to @@ -4605,7 +4624,24 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { // the round trip through the CPU below would store stale texels over whatever the memory // holds now. Its selected units are dropped instead. APS5_NO_UNREGISTERED_DROP=1 stores. static const bool unregisteredDrop = std::getenv("APS5_NO_UNREGISTERED_DROP") == nullptr; + bool overhang = false; if (unregisteredDrop && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { + const auto allocationEnd = RegisteredReadableEnd(descriptor.baseAddress); + const auto& level = mips.front(); + const auto rowBytes = std::uint64_t{level.height} * level.pitchBytes; + const auto lastRowTail = mips.size() == 1 && arrayLayers == 1 && !level.tail && level.tiledSize > rowBytes ? level.tiledSize - rowBytes : 0; + const auto end = descriptor.baseAddress + guestBytes; + if (allocationEnd > descriptor.baseAddress && end - allocationEnd <= lastRowTail) { + auto covered = allocationEnd; + while (covered < end) { + const auto next = RegisteredReadableEnd(covered); + if (next <= covered) break; + covered = next; + } + overhang = covered >= end; + } + } + if (unregisteredDrop && !overhang && !RegisteredReadableCovers(descriptor.baseAddress, static_cast(guestBytes))) { static std::atomic reports{0}; if (reports.fetch_add(1) < 4) std::fprintf(stderr, "[gpu] storage image 0x%llx+0x%llx: memory no longer registered; %zu pending ranges dropped\n", static_cast(descriptor.baseAddress), static_cast(guestBytes), keep.size()); unregisteredDropped.fetch_add(1, std::memory_order_relaxed); diff --git a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp index deec634f74a..066f2eff6f5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp @@ -39,11 +39,14 @@ THandle newHandle() { VkFormat lastImageFormat = VK_FORMAT_UNDEFINED; std::vector createdImages; std::vector> imageCopies; +VkImage lastImage = VK_NULL_HANDLE; +std::vector clearedImages; VKAPI_ATTR VkResult VKAPI_CALL createImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* image) { lastImageFormat = info->format; *image = newHandle(); createdImages.push_back(*image); + lastImage = *image; return VK_SUCCESS; } @@ -101,7 +104,9 @@ VKAPI_ATTR void VKAPI_CALL destroyImageView(VkDevice, VkImageView, const VkAlloc VKAPI_ATTR void VKAPI_CALL destroyImage(VkDevice, VkImage, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL freeMemory(VkDevice, VkDeviceMemory, const VkAllocationCallbacks*) {} VKAPI_ATTR void VKAPI_CALL cmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, std::uint32_t, const VkMemoryBarrier*, std::uint32_t, const VkBufferMemoryBarrier*, std::uint32_t, const VkImageMemoryBarrier*) {} -VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) {} +VKAPI_ATTR void VKAPI_CALL cmdClearDepthStencilImage(VkCommandBuffer, VkImage image, VkImageLayout, const VkClearDepthStencilValue*, std::uint32_t, const VkImageSubresourceRange*) { + clearedImages.push_back(image); +} VKAPI_ATTR void VKAPI_CALL cmdCopyImage(VkCommandBuffer, VkImage source, VkImageLayout, VkImage destination, VkImageLayout, std::uint32_t count, const VkImageCopy* regions) { for (std::uint32_t i = 0; i < count; ++i) imageCopies.emplace_back(source, destination, regions[i].dstSubresource.aspectMask); } @@ -307,6 +312,29 @@ void RunDepthSurfaceReuseTests() { imageCopies.clear(); const auto otherWithA = pairing(otherDepth, stencilA); Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over"); + { + constexpr std::uint64_t arrayBase = 0x80000000; + constexpr std::uint64_t htile = 0x81000000; + constexpr VkExtent2D extent{1024, 512}; + const auto sliceHtile = HtileMetadataBytes(extent); + DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + first.htileAddress = htile; + DepthTarget second{arrayBase + DepthSliceBytes(extent, 4u), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0}; + second.htileAddress = htile + sliceHtile; + DepthSurfaceView(context, first); + const auto firstImage = lastImage; + DepthSurfaceView(context, second); + const auto secondImage = lastImage; + const auto clearsAfter = [&](std::size_t bytes) { + clearedImages.clear(); + NoteHtileDepthClear(htile, static_cast(bytes)); + DepthSurfaceView(context, first); + DepthSurfaceView(context, second); + return clearedImages; + }; + Require(clearsAfter(sliceHtile) == std::vector{firstImage}, "an HTILE clear of the first slice must clear that slice alone"); + Require(clearsAfter(2u * sliceHtile) == std::vector{firstImage, secondImage}, "an HTILE clear over both slices must clear both"); + } ClearDepthSurfaces(context.device); } diff --git a/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp new file mode 100644 index 00000000000..aa48f4f1e7f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FmaMixOnce.cpp @@ -0,0 +1,93 @@ +#include "BdaAbi.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void Check(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +SpirvTarget Target() { + static constexpr std::array capabilities{ + spv::CapabilityInt64, spv::CapabilityFloat64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityClipDistance, + }; + static constexpr std::array extensions{ + "SPV_EXT_descriptor_indexing", "SPV_KHR_8bit_storage", "SPV_KHR_float_controls", "SPV_KHR_physical_storage_buffer", + }; + return {0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions, false, + {1024, 1024, 64}, 1024, 32768, {}, {}}; +} + +struct Counts { + std::size_t words = 0; + std::size_t functions = 0; +}; + +Counts Count(std::span words) { + Check(words.size() >= 5 && words[0] == spv::MagicNumber, "fma mix once: missing SPIR-V module"); + Counts counts{words.size()}; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> spv::WordCountShift; + Check(count != 0 && count <= words.size() - offset, "fma mix once: malformed SPIR-V instruction"); + if ((words[offset] & spv::OpCodeMask) == spv::OpFunction) ++counts.functions; + offset += count; + } + return counts; +} + +Counts Compile(std::uint32_t uses) { + std::vector code; + for (std::uint32_t load = 0; load < 3u; ++load) code.insert(code.end(), {0xe0300000u | (load * 4u), 0x80010000u | ((1u + load) << 8u)}); + code.push_back(0xbf8c3f70u); + std::uint32_t result = 1u; + for (std::uint32_t use = 0; use < uses; ++use) { + const std::uint32_t opcode = use % 2u == 0u ? 0x21u : 0x22u; + code.insert(code.end(), {0xcc000000u | (opcode << 16u) | 10u, (256u + result) | ((256u + 2u) << 9u) | ((256u + 3u) << 18u)}); + result = 10u; + } + code.insert(code.end(), {0xe0700000u, 0x80010000u | (result << 8u)}); + code.push_back(0xbf810000u); + const std::span words(code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; + const std::array userData{0u, 0u, 0u, 0u, 0x10000000u, 0u, 0x1000u, 0x31016facu}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, reinterpret_cast(code.data()), words, 0, {}}; + request.target = Target(); + request.layout = {0, 0, 0, 128}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.memory = memory; + request.useCache = false; + return Count(Recompile(request).spirv.Words()); +} + +} + +int main() { + try { + const auto none = Compile(0); + const auto one = Compile(1); + const auto three = Compile(3); + Check(three.words - one.words < one.words - none.words, "fma mix once: 2 more v_fma_mixlo/mixhi_f16 added " + std::to_string(three.words - one.words) + " SPIR-V words, as many as the first one with its shared emulation (" + std::to_string(one.words - none.words) + ")"); + Check(one.functions == none.functions + 1u, "fma mix once: one v_fma_mixlo_f16 emitted " + std::to_string(one.functions - none.functions) + " functions more than none, expected one shared emulation function"); + Check(three.functions == one.functions, "fma mix once: two more v_fma_mixlo/mixhi_f16 in the same float mode emitted " + std::to_string(three.functions - one.functions) + " more functions, expected calls to the shared one"); + std::printf("fma mix once tests passed (%zu words without v_fma_mix, %zu for 1, %zu for 3)\n", none.words, one.words, three.words); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index edb679bc598..ce44e04d969 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1484,6 +1484,13 @@ void DepthStencilTests() { AgcDriver::Graphics::RegisterReadLog() = nullptr; for (const auto read : log) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks an HTILE register the decoder reads: " + std::to_string(read.offset)); Require(state.depth->htileAddress == (0x30000ull | (1ull << 40u)), "the HTILE address was not decoded from DB_HTILE_DATA_BASE and its high byte"); + { + const auto view = queue.context[0x002]; + queue.context[0x002] = 2u | (2u << 13u); + const auto slice = AgcDriver::Graphics::DecodeState(queue); + Require(slice.depth->htileAddress == (0x30000ull | (1ull << 40u)) + 2u * AgcDriver::Graphics::HtileMetadataBytes(slice.depth->extent), "slice 2 of a depth array must use the third slice of its HTILE"); + queue.context[0x002] = view; + } queue.context[0x010] &= ~0x20000000u; state = AgcDriver::Graphics::DecodeState(queue); Require(state.depth->htileAddress == 0, "a surface without TILE_SURFACE_ENABLE decoded an HTILE address"); @@ -2259,6 +2266,7 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorBuffer; using AgcDriver::Graphics::DecodeColorMetadataPass; using AgcDriver::Graphics::DecodeState; const auto multisampled = [] { @@ -2475,6 +2483,10 @@ void multisampleTests() { queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeState(queue); }, "DCC over a multisampled color target is unsupported"); + expectFailure([&] { DecodeColorBuffer(queue.context, 0); }, "DCC over a multisampled color target is unsupported"); + queue = multisampled(); queue.context[0x31c] |= 0x2000; setCmaskAddress(queue); std::vector reads; @@ -2505,6 +2517,9 @@ void multisampleTests() { queue = rasterizedResolve; queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = rasterizedResolve; + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); queue = multisampled(); queue.context[0x0] = 0; queue.context[0x202] = 0xcc0030; @@ -2520,6 +2535,32 @@ void multisampleTests() { queue.context[0x0] = 0; queue.context[0x202] = 0xcc0020; expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + queue.context[0x31c] |= 0x10000000u; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "DCC over a multisampled color target is unsupported"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x1002e028; + queue.context[0x31d] = 0x00012000; + queue.context[0x325] = 0x2000; + queue.context[0x323] = 0x3f800000; + queue.context[0x324] = 0x3f800000; + setCmaskAddress(queue); + auto fmaskPass = DecodeColorMetadataPass(queue); + Require(fmaskPass.has_value() && fmaskPass->mode == ColorMetadataPass::Mode::FmaskDecompress && fmaskPass->targets.size() == 1 && fmaskPass->targets[0].samples == 4 && fmaskPass->targets[0].cmaskAddress == cmaskAddress && fmaskPass->targets[0].dccAddress != 0, "an FMASK decompress of a 4x multisampled target with CMASK and DCC did not decode"); + Require(fmaskPass->targets[0].clearWords[0] == 0x3f800000 && fmaskPass->targets[0].clearWords[1] == 0x3f800000, "fast clear words changed"); + queue = makeState(); + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "over a single-sample target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0x00cc0050; + queue.context[0x31c] = 0x00008028; + queue.context[0x31d] = 0x00012000; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "FMASK decompress of a target without a CMASK address"); using AgcDriver::Graphics::DccKeys; using AgcDriver::Graphics::MultisampledCmask; @@ -2539,6 +2580,7 @@ void multisampleTests() { MultisampledCmask seeded(0x30000, 0x1000); seeded.SeedKeys(DccKeys::Clear0000); Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + Require(!seeded.TakeClear(), "a subsequent use applied the clear again"); MultisampledCmask mixedSeed(0x30000, 0x1000); mixedSeed.SeedKeys(DccKeys::Mixed); expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index bd5e36614f3..ee2531011aa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -96,6 +96,14 @@ void rejectFields(const Fields& f, std::string_view reason) { void RunGuestTextureResourceTests() { Fields base; + const auto baseWords = pack(base); + Require(!IsNullTextureResource(baseWords), "a nonzero 2D texture was classified as null"); + Fields nullBase = base; + nullBase.base40 = 0; + Require(IsNullTextureResource(pack(nullBase)), "a zero-base texture was not classified as null"); + Fields nullType = base; + nullType.typeRaw = 0; + Require(IsNullTextureResource(pack(nullType)), "a type-0 texture with a nonzero base was not classified as null"); auto result = DecodeTextureResource(pack(base)); Require(result.baseAddress == (base.base40 << 8u), "decoded base address changed"); Require(result.width == 16 && result.height == 16, "decoded width or height changed"); @@ -244,6 +252,37 @@ void RunGuestTextureResourceTests() { cubeNotMultiple.depth = 4; rejectFields(cubeNotMultiple, "multiple of 6"); + Fields multisampled = base; + multisampled.typeRaw = 14; + multisampled.width = 297; + multisampled.height = 297; + multisampled.tileModeRaw = 0x1b; + multisampled.lastLevel = 2; + multisampled.maxMip = 2; + result = DecodeTextureResource(pack(multisampled)); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.width == 297 && result.height == 297, "4x multisampled descriptor decoded incorrectly"); + Require(result.mipCount == 1 && result.allocatedMipCount == 1 && result.baseLevel == 0 && result.lastLevel == 0, "a multisampled descriptor's sample count was decoded as mip levels"); + Require(DecodeTextureResource(pack(base)).samples == 1, "a single-sample descriptor decoded with several samples"); + const std::array canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; + result = DecodeTextureResource(canvas); + Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Fields fewerFragments = multisampled; + fewerFragments.maxMip = 1; + rejectFields(fewerFragments, "same sample count"); + Fields singleSampleType = multisampled; + singleSampleType.lastLevel = 0; + singleSampleType.maxMip = 0; + rejectFields(singleSampleType, "same sample count"); + Fields offsetLevel = multisampled; + offsetLevel.baseLevel = 1; + rejectFields(offsetLevel, "same sample count"); + Fields multisampledSlice = multisampled; + multisampledSlice.depth = 1; + rejectFields(multisampledSlice, "nonzero depth or base array"); + Fields multisampledArray = multisampled; + multisampledArray.typeRaw = 15; + rejectFields(multisampledArray, "unsupported image type"); + Fields badType = base; badType.typeRaw = 0; rejectFields(badType, "unsupported image type"); @@ -438,6 +477,9 @@ void RunGuestTextureResourceTests() { Require(MatchesGuestDimension(Shape::Image2D, TextureDimension::k2D), "2D shape must match 2D dimension"); Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DArray), "2D shape must not match 2D array dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::k2DArray), "2D array shape must match 2D array dimension"); + Require(MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2DMsaa), "multisampled shape must match the multisampled dimension"); + Require(!MatchesGuestDimension(Shape::Image2DMultisample, TextureDimension::k2D), "multisampled shape must not match 2D dimension"); + Require(!MatchesGuestDimension(Shape::Image2D, TextureDimension::k2DMsaa), "2D shape must not match the multisampled dimension"); Require(MatchesGuestDimension(Shape::Image2DArray, TextureDimension::kCube), "a cube must be readable as a 2D array of its faces"); Require(MatchesGuestDimension(Shape::ImageCube, TextureDimension::kCube), "cube shape must match cube dimension"); Require(!MatchesGuestDimension(Shape::ImageCube, TextureDimension::k1D), "cube shape must not match 1D dimension"); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp index 3f16b22de47..41824053d68 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceTrace.cpp @@ -57,9 +57,31 @@ void BufferCap() { for (int index = 0; index < 3; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); const auto snapshot = thread.Collect(); Check(snapshot.slices.size() == 2 && snapshot.dropped, "a full buffer did not stop and set the dropped flag"); + Check(snapshot.eventsLost == 1 && snapshot.eventsFullAt == origin + milliseconds(2) && !snapshot.totalsFull, "a full buffer did not report the lost slice and its time apart from the totals"); Check(snapshot.totals.size() == 1 && snapshot.totals[0].count == 3 && snapshot.totals[0].length == microseconds(90), "totals missed a slice past the cap"); } +void LostEventsCount() { + std::atomic presents{1}; + TraceThread thread("queue worker 0x0", presents, 2); + const auto origin = Clock::now(); + Check(!thread.Collect().eventsFullAt && thread.Collect().eventsLost == 0, "a thread that lost nothing reported a loss"); + for (int index = 0; index < 10; ++index) thread.Add("Driver.Packet", nullptr, origin + milliseconds(index), origin + milliseconds(index) + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.eventsLost == 8 && snapshot.eventsFullAt == origin + milliseconds(2), "the lost slices were not counted from the first one"); +} + +void TotalsOverflow() { + static char scopes[600]; + std::atomic presents{1}; + TraceThread thread("presenter", presents, 4096); + const auto origin = Clock::now(); + for (const auto& scope : scopes) thread.Add(&scope, nullptr, origin, origin + microseconds(30)); + const auto snapshot = thread.Collect(); + Check(snapshot.totalsFull && snapshot.dropped && snapshot.eventsLost == 0 && !snapshot.eventsFullAt, "a full totals table was not reported apart from the event buffer"); + Check(snapshot.totals.size() == 512 && snapshot.slices.size() == 600, "a full totals table stopped the event buffer or kept more than its entries"); +} + void CapacitySettings() { Check(TraceThread::ParseCapacity(nullptr) == TraceThread::DefaultCapacity, "the default event capacity changed"); Check(TraceThread::ParseCapacity("1") == 1, "the smallest event capacity was rejected"); @@ -152,6 +174,22 @@ void BatchIds() { Check(TraceFormat::GpuSlices(ranges, first, last, origin)[0].batch == 3, "a GPU range lost its batch id"); } +void FullBufferEvents() { + const auto origin = Clock::now(); + const std::vector slices{{"Driver.Packet", nullptr, origin + milliseconds(1), microseconds(30)}}; + TraceThread::Snapshot snapshot{"queue worker 0x0", slices, {}, true, 7, origin + milliseconds(3), false}; + const std::vector threads{snapshot}; + const std::vector presents{origin, origin + milliseconds(16)}; + const auto json = TraceFormat::Events(7, origin, threads, 0, presents, 5); + const std::string expected = + "{\"name\":\"trace buffer full\",\"ph\":\"i\",\"ts\":3000.000,\"pid\":7,\"tid\":1,\"s\":\"t\",\"args\":{\"events_lost\":7}},\n" + "{\"name\":\"present\""; + Check(json.find(expected) != std::string::npos, "the cut-off instant event is missing or has the wrong shape"); + snapshot.eventsLost = 0; + snapshot.eventsFullAt.reset(); + Check(TraceFormat::Events(7, origin, std::vector{snapshot}, 0, presents, 5).find("trace buffer full") == std::string::npos, "a thread that lost nothing got a cut-off event"); +} + void SummaryFields() { const auto origin = Clock::now(); AgcDriver::TraceSummary summary; @@ -184,6 +222,24 @@ void SummaryFields() { auto dropped = threads; dropped[1].dropped = true; Check(TraceFormat::Summary(summary, dropped, presents).find("\"trace_dropped\":true") != std::string::npos, "a dropped thread did not mark the summary"); + Check(line.find("trace_dropped_threads") == std::string::npos, "a summary without drops listed threads"); + dropped[0].dropped = true; + dropped[0].eventsLost = 2240000; + dropped[0].eventsFullAt = presents.front() + milliseconds(32); + dropped[1].totalsFull = true; + const auto detailed = TraceFormat::Summary(summary, dropped, presents); + Check(detailed.find("\"trace_dropped\":true,\"trace_dropped_threads\":{\"queue worker 0x0\":{\"events_full_after_s\":0.032,\"events_lost\":2240000,\"totals_full\":false},\"presenter\":{\"totals_full\":true}},\"trace\":") != std::string::npos, "the dropped threads were not listed with their reasons"); + auto shared = threads; + shared.push_back({"shader prepare", {}, {}, true}); + shared.push_back({"shader prepare", {}, {}, true}); + shared[2].eventsLost = 5; + shared[2].eventsFullAt = presents.front() + milliseconds(30); + shared[3].eventsLost = 7; + shared[3].eventsFullAt = presents.front() + milliseconds(10); + shared[3].totalsFull = true; + const auto merged = TraceFormat::Summary(summary, shared, presents); + Check(merged.find("\"trace_dropped_threads\":{\"shader prepare\":{\"events_full_after_s\":0.010,\"events_lost\":12,\"totals_full\":true}},\"trace\":") != std::string::npos, "threads with one name were not merged into one entry"); + Check(merged.find("shader prepare") == merged.rfind("shader prepare"), "threads with one name were listed twice"); } void GpuClock() { @@ -271,6 +327,25 @@ void IntervalSummaryFields() { Check(line == expected, "interval summary line changed"); } +void IntervalDroppedThreads() { + const auto origin = Clock::now(); + AgcDriver::TraceSummary summary; + summary.interval = std::make_pair(30.0, 60.0); + std::vector threads{{"queue worker 0x0", {}, {}, true, 5}}; + const std::vector presents{origin + milliseconds(100), origin + milliseconds(116)}; + threads[0].eventsFullAt = origin + milliseconds(40); + auto line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos, "a loss before the interval printed its time"); + threads[0].eventsFullAt = origin + milliseconds(104); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("{\"events_full_after_s\":0.004,\"events_lost\":5,") != std::string::npos, "a loss inside the interval lost its time"); + threads[0].eventsFullAt = origin + milliseconds(120); + line = TraceFormat::Summary(summary, threads, presents); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "a loss after the last present printed its time"); + line = TraceFormat::Summary(summary, threads, {}); + Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time"); +} + void GzipFile() { std::string text; for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n"; @@ -323,7 +398,10 @@ int main() { WindowBySeconds(); WindowRejects(); BufferCap(); + LostEventsCount(); + TotalsOverflow(); CapacitySettings(); + FullBufferEvents(); FoldShortSlices(); EscapeStrings(); EventsShape(); @@ -334,6 +412,7 @@ int main() { SummaryOnlyThread(); TotalsSince(); IntervalSummaryFields(); + IntervalDroppedThreads(); GzipFile(); AppendLines(); std::cout << "performance trace tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 916b9ca313d..c445f888d9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4469,6 +4469,135 @@ void unchangedCpuStampTests(const Device& device, Recorder& recorder) { std::cout << "unchanged CPU stamps: ok\n"; } +void keptUnitSettleTests(const Device& device, Recorder& recorder) { + Require(AgcDriver::GuestMemory::WriteWatched(), "(k) kept-unit settle tests require host write watching"); + const auto& base = device.GetContext(); + constexpr std::uint32_t width = 1024; + constexpr std::uint32_t height = 512; + constexpr std::size_t surfaceBytes = std::size_t{width} * height * 4; + constexpr std::size_t unitBytes = 65536; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + for (const bool direct : {false, true}) { + const std::string what = direct ? "GPU-mapped direct memory" : "watched memory"; + if (direct && base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + struct Backing { + Recorder& recorder; + bool direct; + void* block = nullptr; + std::int64_t physical = -1; + ~Backing() { + try { + recorder.Sync(); + } catch (const std::exception& error) { + std::cerr << "(k) the recorded work did not finish, the test memory stays mapped: " << error.what() << '\n'; + return; + } + if (direct) { + if (block != nullptr) sceKernelMunmap(block, surfaceBytes); + if (physical >= 0) sceKernelReleaseDirectMemory(physical, surfaceBytes); + return; + } + if (block == nullptr) return; + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, surfaceBytes); + } + } backing{recorder, direct}; + if (direct) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, surfaceBytes, unitBytes, 0, &backing.physical) == 0, "(k) allocate the direct memory"); + Require(sceKernelMapDirectMemory(&backing.block, surfaceBytes, 0x33, 0, backing.physical, unitBytes) == 0, "(k) map the direct memory for the GPU"); + } else { + backing.block = AllocateWatched(surfaceBytes, unitBytes); + Require(backing.block != nullptr, "(k) no watched block although write watching is on"); + GuestAllocations::Mutation mutation; + mutation.Add(backing.block, surfaceBytes, true, true, true); + } + auto* memory = static_cast(backing.block); + std::memset(memory, 0x5a, surfaceBytes); + const auto address = reinterpret_cast(backing.block); + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + TextureDetiler detiler(base); + auto context = base; + if (!direct) context.hostImportAlignment = 0; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 56; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + Require(DescribeSurface(resource).guestBytes == surfaceBytes, "(k) unexpected surface size"); + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "GPU-mapped direct memory is not write-watched here: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(AgcDriver::GuestMemory::Watched(address, surfaceBytes), "(k) " + what + " is not watched"); + if (direct && HostImportFor(context, address, surfaceBytes) == nullptr) { + std::cout << "host import of the direct memory refused: units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + if (direct && !AgcDriver::GuestMemory::Watched(address, surfaceBytes)) { + std::cout << "the host import of the direct memory took it out of the write watch (its imports are compared): units stored next to a unit kept for the CPU in " << what << " not tested\n"; + continue; + } + Require(direct || HostImportFor(context, address, surfaceBytes) == nullptr, "(k) watched memory was imported although host imports are off for it"); + { + auto image = StorageTexture::Create(context, detiler, resource, 0); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto clear = [&](VkClearColorValue value) { + const auto commands = recorder.Commands(); + recorder.Keep(image); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdClearColorImage")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, &value, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); + image->MarkDirty(); + }; + const auto store = [&] { + image->WriteBack(); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + }; + const auto unitHolds = [&](std::size_t unit, const std::array& texel) { + std::vector read(unitBytes); + AgcDriver::GuestMemory::Read(address + unit * unitBytes, read); + for (std::size_t i = 0; i < unitBytes; ++i) { + if (std::to_integer(read[i]) != texel[i % 4]) return false; + } + return true; + }; + clear({{1.0f, 0.0f, 0.0f, 1.0f}}); + memory[16] = 0x77; + AgcDriver::GuestMemory::CollectWritesUncached(address, surfaceBytes); + store(); + std::vector head(unitBytes); + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the CPU store into unit 0 was overwritten by results it was newer than"); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) Require(unitHolds(unit, red), "(k) " + what + ": the first write-back did not store unit " + std::to_string(unit)); + clear({{0.0f, 0.0f, 1.0f, 1.0f}}); + store(); + for (std::size_t unit = 1; unit < surfaceBytes / unitBytes; ++unit) { + Require(unitHolds(unit, blue), "(k) " + what + ": unit " + std::to_string(unit) + " kept the results of the earlier write-back; that write-back's own store was taken for a CPU write because a neighbouring unit was kept for the CPU"); + } + AgcDriver::GuestMemory::Read(address, head); + Require(std::to_integer(head[16]) == 0x77, "(k) " + what + ": the second write-back stored over unit 0, which the CPU wrote after that unit's generation"); + } + std::cout << "units stored next to a unit kept for the CPU in " << what << ": ok\n"; + } +} + void settledBaselineTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); #ifdef _WIN32 @@ -5686,6 +5815,58 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); + const auto multisampledWords = [&](std::uint32_t width, std::uint32_t sampleShift, std::uint64_t base) { + return std::vector{ + static_cast(base >> 8u), + static_cast((base >> 40u) & 0xffu) | (56u << 20u) | (((width - 1u) & 3u) << 30u), + ((width - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (14u << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; + }; + const auto sampledProgram = [&](ShaderRecompiler::DescriptorKind kind, std::vector words) { + ShaderRecompiler::RecompileResult result; + auto element = binding; + element.kind = kind; + if (kind == ShaderRecompiler::DescriptorKind::StorageImage) element.binding = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + element.imageShape = ShaderRecompiler::DescriptorImageShape::Image2DMultisample; + element.guestDescriptor = std::move(words); + result.bindings.push_back(std::move(element)); + return result; + }; + const auto refusedSample = [&](ShaderRecompiler::DescriptorKind kind, std::vector words, const std::string& expected, const std::string& what) { + const auto refusedProgram = sampledProgram(kind, std::move(words)); + const CompiledShader refusedCompute{ShaderRecompiler::ShaderStage::Compute, &refusedProgram, 0}; + std::string message; + try { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, refusedCompute); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos, what + " did not throw: " + message); + }; + { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; + std::lock_guard gpu(GpuMutex()); + ShaderResources first(context, sampledCompute); + ShaderResources second(context, sampledCompute); + Require(first.SampledTextures().size() == 1 && first.SampledTextures().front() != nullptr, "the multisampled target was not bound as a sampled texture"); + Require(first.SampledTextures().front()->Layout() == VK_IMAGE_LAYOUT_GENERAL && first.SampledTextures().front()->ViewFormat() == target.format, "the sampled multisampled target is not a view of the target's image in its layout"); + Require(second.SampledTextures().front() == first.SampledTextures().front(), "a second lookup of the multisampled target made another texture"); + Require(first.MultisampleSources() == std::vector{address} && !first.Reusable(), "resources sampling a multisampled target are kept for reuse without the barrier their lookup records"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 3u, address), "as a 64x64 texture of 8 samples", "sampling a 4x target as an 8x texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side / 2u, 2u, address), "as a 32x64 texture of 4 samples", "sampling a 4x target as a narrower texture"); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address + 4u * 65536u), "which no draw rendered to", "sampling a multisampled texture no draw rendered to"); + refusedSample(ShaderRecompiler::DescriptorKind::StorageImage, multisampledWords(side, 2u, address), "storage image access to a multisampled texture", "storage access to a multisampled texture"); 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); clearTargets(); @@ -5735,12 +5916,19 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { target.surfaceExtent = target.extent; target.samples = 4; VkImageView previous = VK_NULL_HANDLE; + std::shared_ptr previousTexture; for (const bool removeAllocation : {false, true}) { VkImageView view = VK_NULL_HANDLE; + std::shared_ptr texture; { + const auto sampled = sampledProgram(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address)); + const CompiledShader sampledCompute{ShaderRecompiler::ShaderStage::Compute, &sampled, 0}; std::lock_guard gpu(GpuMutex()); view = MultisampleTargetView(context, target); Require(view != VK_NULL_HANDLE && view != previous, "a remapped allocation reused its retired multisampled image"); + ShaderResources resources(context, sampledCompute); + texture = resources.SampledTextures().front(); + Require(texture != nullptr && texture != previousTexture, "a remapped allocation sampled its retired multisampled image"); recorder.Submit(); device.WaitQueue(); recorder.Sync(); @@ -5767,12 +5955,114 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { } RequireNoMultisampleTarget(address, bytes, "a texture read"); AgcDriver::GuestMemory::Read(address + 256u, texels); + refusedSample(ShaderRecompiler::DescriptorKind::SampledImage, multisampledWords(side, 2u, address), "which no draw rendered to", "sampling a multisampled target whose allocation was unmapped"); previous = view; + previousTexture = texture; } clearTargets(); std::cout << "Multisampled target guest access tests passed\n"; } +void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || base.hostImportAlignment == 0) { + std::cout << "4x color targets or host imports unavailable: FMASK decompress not tested\n"; + return; + } + constexpr std::uint32_t side = 128; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t cmaskBytes = 4096u; + constexpr std::size_t destBytes = targetBytes; + constexpr std::size_t bytes = 5u * 65536u + cmaskBytes + 2u * destBytes; +#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 FMASK decompress block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + const auto cmaskAddress = address + 5u * 65536u; + const auto destAddress = cmaskAddress + cmaskBytes; + const auto destAddress2 = destAddress + destBytes; + { + 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; + ColorTarget source{}; + source.address = address; + source.surfaceAddress = address; + source.extent = {side, side}; + source.surfaceExtent = source.extent; + source.format = VK_FORMAT_R8G8B8A8_UNORM; + source.elementBytes = 4; + source.bytes = targetBytes; + source.samples = 4; + source.cmaskAddress = cmaskAddress; + source.cmaskBytes = cmaskBytes; + source.cmaskFastClear = true; + source.clearWords = {0x11223344u, 0}; + ColorTarget destination{}; + destination.address = destAddress; + destination.surfaceAddress = destAddress; + destination.extent = {side, side}; + destination.surfaceExtent = destination.extent; + destination.format = VK_FORMAT_R8G8B8A8_UNORM; + destination.elementBytes = 4; + destination.bytes = destBytes; + destination.samples = 1; + destination.tileMode = ColorTileMode::RenderTarget; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + auto recolored = source; + recolored.clearWords = {0x55667788u, 0}; + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination}, recolored}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress, std::as_writable_bytes(std::span(resolved))); + Require(std::ranges::all_of(resolved, [](auto word) { return word == 0x11223344u; }), "FMASK decompress did not materialize the seeded clear to the host multisampled image"); + source.clearWords = {0x55667788u, 0}; + ColorTarget destination2 = destination; + destination2.address = destAddress2; + destination2.surfaceAddress = destAddress2; + { + std::lock_guard gpu(GpuMutex()); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::FmaskDecompress, {source}}); + RunColorMetadataPass(context, {ColorMetadataPass::Mode::Resolve, {destination2}, source}); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::vector resolved2(destBytes / 4); + AgcDriver::GuestMemory::Read(destAddress2, std::as_writable_bytes(std::span(resolved2))); + Require(std::ranges::all_of(resolved2, [](auto word) { return word == 0x11223344u; }), "subsequent FMASK decompress re-cleared the multisampled target"); + std::cout << "FMASK decompress MSAA clear and resolve tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -7094,6 +7384,10 @@ int main(int argc, char** argv) { unchangedCpuStampTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--kept-unit-settle-only") { + keptUnitSettleTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -7182,6 +7476,7 @@ int main(int argc, char** argv) { } if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); return 0; } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { @@ -7224,6 +7519,7 @@ int main(int argc, char** argv) { readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); multisampleGuestAccessTests(device, recorder); + fmaskDecompressMsaaTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); @@ -7254,6 +7550,7 @@ int main(int argc, char** argv) { singlePassTests(device, recorder, false); singlePassTests(device, recorder, true); unchangedCpuStampTests(device, recorder); + keptUnitSettleTests(device, recorder); settledBaselineTests(device, recorder); importWatchTests(device); staleGenerationTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 59cf1d71b5c..b61f0b8e290 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1648,6 +1648,57 @@ void verifyPointerImageTable() { #endif } +void verifyPointerTableOffsetCarry() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + constexpr std::size_t Low = 0x10000; + constexpr std::uint64_t High = 0xffff0000ull; + constexpr std::size_t HighBytes = 0x20000; + constexpr std::uint64_t Reserved = High + HighBytes; + static TableTexture textures[2]; + void* block = nullptr; +#ifdef _WIN32 + block = VirtualAlloc(nullptr, static_cast(Reserved), MEM_RESERVE, PAGE_NOACCESS); + require(block != nullptr && VirtualAlloc(block, Low, MEM_COMMIT, PAGE_READWRITE) == block && VirtualAlloc(static_cast(block) + High, HighBytes, MEM_COMMIT, PAGE_READWRITE) == static_cast(block) + High, "pointer table offset carry: cannot map the root"); +#else + block = mmap(nullptr, Reserved, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + require(block != MAP_FAILED && mprotect(block, Low, PROT_READ | PROT_WRITE) == 0 && mprotect(static_cast(block) + High, HighBytes, PROT_READ | PROT_WRITE) == 0, "pointer table offset carry: cannot map the root"); +#endif + auto* root = static_cast(block); + const auto base = static_cast(reinterpret_cast(root)); + static std::array keys{}; + static std::array output{}; + const auto keysV = TableV(keys.data(), 0u, static_cast(sizeof(keys))); + const auto outputV = TableV(output.data(), 0u, static_cast(sizeof(output))); + std::copy(keysV.begin(), keysV.end(), root); + std::copy(outputV.begin(), outputV.end(), root + 4); + const auto at = [&](std::uint64_t offset, std::array words) { + std::copy(words.begin(), words.end(), root + offset / 4u); + }; + at(0x100000010ull, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D)); + at(0x50u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D)); + const std::array userData{static_cast(base), static_cast(base >> 32u)}; + const std::vector code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u, + 0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4080900u, 0x26000040u, 0xf4080a00u, + 0x28000040u, 0xf09c0f08u, 0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u}; + auto request = TableRequest(code, userData); + request.shader.codeAddress = 0x36000u; + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "pointer table offset carry: a T# read by two loads of its record was not planned as a table"); + const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn; + require(column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "pointer table offset carry: the column is wrong"); + const auto compiled = CompileTable(request); + const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0); + require(snapshot.keys == 256u && snapshot.codes[7] == Abi::PoisonCode(static_cast(Abi::PoisonReason::OffsetCarry)), "pointer table offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison"); + require((snapshot.codes[6] & Abi::PoisonFlag) == 0u && (snapshot.codes[8] & Abi::PoisonFlag) == 0u && compiled.capture->snapshot.tables.words.at(snapshot.codes[6]).dwords[0] == root[0x100000010ull / 4u] && compiled.capture->snapshot.tables.words.at(snapshot.codes[8]).dwords[0] == root[0x50u / 4u] && root[0x100000010ull / 4u] != root[0x50u / 4u], "pointer table offset carry: the records on either side of the carry were not resolved"); + require(!TableTouched(compiled.regions, root + 0x100000030ull / 4u, 0x20u), "pointer table offset carry: the record whose second load wraps was read"); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + munmap(block, Reserved); +#endif +} + void verifyDescriptorPhis() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -2508,16 +2559,31 @@ void verifyMeshConfiguration() { RecompileCacheKey::Build(primitiveGrouped, key); require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the primitive group size, which rebuilds their relative patch index"); } - auto shared = hull; - shared.graphics->tessellation->breakAtInstance = false; + auto instanced = hull; + instanced.graphics->tessellation->breakAtInstance = false; + instanced.graphics->draw.instanceCount = 2u; + instanced.graphics->draw.indexCount = 12u; + auto longer = instanced; + longer.graphics->draw.indexCount = 16u; + auto breaking = instanced; + breaking.graphics->tessellation->breakAtInstance = true; for (const auto stage : {ShaderStage::Local, ShaderStage::TessellationControl, ShaderStage::TessellationEvaluation}) { hull.shader.stage = stage; - shared.shader.stage = stage; + instanced.shader.stage = stage; + longer.shader.stage = stage; + breaking.shader.stage = stage; RecompileCacheKey::Build(hull, key); - const auto patchKey = key; - RecompileCacheKey::Build(shared, key); - require((key != patchKey) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on BREAK_WAVE_AT_EOI, which decides whether their relative patch index can be rebuilt"); + const auto single = key; + RecompileCacheKey::Build(instanced, key); + const auto shared = key; + RecompileCacheKey::Build(longer, key); + require((shared != single && key != shared) == (stage != ShaderStage::Local), "only the hull and domain cache keys may depend on the patches per instance of an instanced draw"); + RecompileCacheKey::Build(breaking, key); + require(key == single, "an instanced draw whose instances start hull threadgroups (BREAK_WAVE_AT_EOI) changed the cache key"); } + breaking.shader.stage = ShaderStage::TessellationControl; + const auto breakingReplay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(breaking)); + require(breakingReplay.request.graphics && breakingReplay.request.graphics->tessellation && breakingReplay.request.graphics->tessellation->breakAtInstance, "BREAK_WAVE_AT_EOI was lost in serialization"); const std::array users{}; request.context.waveSize = 32u; request.context.userData = users; @@ -4802,6 +4868,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } @@ -4825,6 +4892,7 @@ int main(int argc, char** argv) { verifyImageTableRegistration(); verifyScalarAddressRule(); verifyPointerImageTable(); + verifyPointerTableOffsetCarry(); verifyDescriptorPhis(); verifyUniformTableImage(); verifyLaneSelectedDescriptors(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index b9c39114062..79616003ec4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -166,6 +166,8 @@ void testNarrowConversions(bool optimizationEnabled) { check(opcodeCount(normalized, spv::OpStore) == (vector ? 1u : 2u), label + ": optimizer lost buffer stores"); if (!optimizationEnabled) { check(output == input, label + ": none mode changed the shader"); + const auto specialized = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false, true, true); + check(specialized == input, label + ": none mode changed the specialized shader"); } else { check(opcodeCount(normalized, spv::OpIAdd) == 0, label + ": optimizer did not remove dead arithmetic"); if (arithmetic && !type.floating) { @@ -378,6 +380,77 @@ OpFunctionEnd } } +void testLongControlFlow(bool optimizationEnabled, bool large) { + const char* mode = std::getenv("APS5_SPIRV_OPT"); + const bool full = mode != nullptr && std::string(mode) == "full"; + const std::size_t blocks = large && optimizationEnabled && !full ? 512 : 8; + std::string source = R"(OpCapability Shader +OpCapability StorageBuffer8BitAccess +OpExtension "SPV_KHR_8bit_storage" +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" +OpExecutionMode %main LocalSize 1 1 1 +OpDecorate %array ArrayStride 4 +OpMemberDecorate %buffer 0 Offset 0 +OpDecorate %buffer Block +OpDecorate %data DescriptorSet 0 +OpDecorate %data Binding 0 +OpDecorate %byteArray ArrayStride 1 +OpMemberDecorate %byteBuffer 0 Offset 0 +OpDecorate %byteBuffer Block +OpDecorate %bytes DescriptorSet 0 +OpDecorate %bytes Binding 1 +%void = OpTypeVoid +%uint = OpTypeInt 32 0 +%boolean = OpTypeBool +%byte = OpTypeInt 8 0 +%byteArray = OpTypeRuntimeArray %byte +%byteBuffer = OpTypeStruct %byteArray +%byteBufferPointer = OpTypePointer StorageBuffer %byteBuffer +%bytePointer = OpTypePointer StorageBuffer %byte +%array = OpTypeRuntimeArray %uint +%buffer = OpTypeStruct %array +%bufferPointer = OpTypePointer StorageBuffer %buffer +%valuePointer = OpTypePointer StorageBuffer %uint +%function = OpTypeFunction %void +%data = OpVariable %bufferPointer StorageBuffer +%bytes = OpVariable %byteBufferPointer StorageBuffer +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%two = OpConstant %uint 2 +)"; + for (std::size_t i = 0; i < blocks * 4; ++i) source += "%index" + std::to_string(i) + " = OpConstant %uint " + std::to_string(i + 1) + "\n"; + source += "%main = OpFunction %void None %function\n%entry = OpLabel\n%input = OpAccessChain %valuePointer %data %zero %zero\n%seed = OpLoad %uint %input Volatile\n%condition = OpINotEqual %boolean %seed %zero\nOpBranch %block0\n"; + for (std::size_t i = 0; i < blocks; ++i) { + const auto number = std::to_string(i); + const auto previous = i == 0 ? "%seed" : "%next" + std::to_string(i - 1); + source += "%block" + number + " = OpLabel\n%sum" + number + " = OpIAdd %uint " + previous + " %one\n"; + source += "%duplicate" + number + " = OpIAdd %uint " + previous + " %one\n%shared" + number + " = OpIAdd %uint %seed %one\n"; + for (std::size_t j = 0; j < 3; ++j) { + const auto index = std::to_string(i * 4 + j); + source += "%slot" + index + " = OpAccessChain %valuePointer %data %zero %index" + index + "\n"; + source += "OpStore %slot" + index + " %" + (j == 0 ? "sum" : j == 1 ? "duplicate" : "shared") + number + " Volatile\n"; + } + const auto branchIndex = std::to_string(i * 4 + 3); + source += "%branchSlot" + number + " = OpAccessChain %valuePointer %data %zero %index" + branchIndex + "\n"; + source += "OpSelectionMerge %join" + number + " None\nOpBranchConditional %condition %left" + number + " %right" + number + "\n"; + source += "%left" + number + " = OpLabel\n%leftValue" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %leftValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%right" + number + " = OpLabel\n%rightCopy" + number + " = OpIAdd %uint %sum" + number + " %one\nOpStore %branchSlot" + number + " %rightCopy" + number + " Volatile\n"; + source += "%rightValue" + number + " = OpIAdd %uint %sum" + number + " %two\nOpStore %branchSlot" + number + " %rightValue" + number + " Volatile\nOpBranch %join" + number + "\n"; + source += "%join" + number + " = OpLabel\n%next" + number + " = OpPhi %uint %leftValue" + number + " %left" + number + " %rightValue" + number + " %right" + number + "\n"; + if (i + 1 == blocks) source += "%byteSlot = OpAccessChain %bytePointer %bytes %zero %zero\n%truncated = OpUConvert %byte %next" + number + "\nOpStore %byteSlot %truncated Volatile\nOpReturn\n"; + else source += "OpBranch %block" + std::to_string(i + 1) + "\n"; + } + source += "OpFunctionEnd\n"; + const auto input = assemble(source); + const auto output = ShaderRecompiler::ValidateAndOptimizeSpirv(input, Vulkan11, Spirv13, false); + const auto stores = opcodeCount(output, spv::OpStore); + check(stores >= blocks * 4 + 1 && stores <= blocks * 6 + 1, "long control-flow optimization discarded observable stores"); + const auto adds = opcodeCount(output, spv::OpIAdd); + if (!optimizationEnabled) check(output == input, "disabled optimization changed the long control-flow module"); + else check(adds >= blocks && adds <= blocks * 4, "control-flow optimization lost arithmetic or retained redundant values"); +} + } int main() { @@ -388,6 +461,8 @@ int main() { testInvalidInputIsRejected(); testSpecializedSelectionExit(); testSpecializedDeadContinue(); + testLongControlFlow(optimizationEnabled, false); + testLongControlFlow(optimizationEnabled, true); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp index 797f0862870..cdb94c5363b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneGuestLayers.cpp @@ -313,6 +313,76 @@ void RecordedUploads(const Context& context, const WatchedBlock& block) { recorder.Sync(); } +void RunCubeGather(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + GuestTextureResource resource{}; + resource.width = Side; + resource.height = Side; + resource.depthOrLastArray = 5; + resource.mipCount = 2; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + Require(geometry.layerBytes != LayerBytes, "the cube fixture's per-layer footprint must not match one host depth slice"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, {Side, Side}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a cube over six depth surfaces at the descriptor's layer offsets was not sampled through their depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + +void RunCubeTiledSlice(const Context& base, const WatchedBlock& block) { + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + constexpr VkExtent2D extent{64, 64}; + GuestTextureResource resource{}; + resource.width = extent.width; + resource.height = extent.height; + resource.depthOrLastArray = 5; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::kCube; + resource.format = Format32Float; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + const auto geometry = DescribeSurface(resource); + const auto slice = DepthSliceBytes(extent, 4u); + Require(geometry.layerBytes == slice && geometry.sliceLinearBytes != slice, "the tiled cube fixture must have a layer footprint equal to a depth slice but a different linear footprint"); + const auto address = block.Address(); + resource.baseAddress = address; + ClearDepthSurfaces(context.device); + for (std::uint32_t layer = 0; layer < 6; ++layer) { + DepthSurfaceView(context, DepthTarget{address + geometry.GuestLayerOffset(layer), 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0}); + } + imageToBufferCopies = 0; + bufferToImageCopies = 0; + const auto texture = DepthSurfaceTexture(context, words, resource, components); + Require(texture != nullptr, "a tiled cube whose linear layer footprint differs from a depth slice was not sampled through its depth planes"); + if (imageToBufferCopies.load() != 6 || bufferToImageCopies.load() != 1) { + throw std::runtime_error("sampling a tiled cube over six depth surfaces recorded " + std::to_string(imageToBufferCopies.load()) + " depth surface copies and " + std::to_string(bufferToImageCopies.load()) + " plane copy fills, expected 6 and 1"); + } + ClearDepthSurfaces(context.device); +} + } int main() { @@ -337,6 +407,8 @@ int main() { Require(AgcDriver::GuestMemory::Watched(block.Address(), BlockBytes), "depth plane guest layers: the guest block is not write-watched"); Run(context, block); RecordedUploads(context, block); + RunCubeGather(context, block); + RunCubeTiledSlice(context, block); } device.reset(); std::puts("depth plane guest layer tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp index 2cdf03f8897..9b74cf6f3a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneSampling.cpp @@ -1,5 +1,8 @@ +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" @@ -26,6 +29,14 @@ #include #include +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + namespace { using AgcDriver::Graphics::Require; @@ -51,6 +62,8 @@ constexpr std::size_t IntegerOffset = 0xa0000; constexpr std::size_t DepthBitsOffset = 0xb0000; constexpr std::size_t OneSliceOffset = 0xd0000; constexpr std::size_t FirstSliceOffset = 0xe0000; +constexpr std::size_t PaddedCubeOffset = 0x120000; +constexpr std::size_t SmallCubeOffset = 0x180000; constexpr float DrawnDepth = 0.25f; constexpr float DrawnD16Depth = 0.2f; constexpr std::uint32_t DrawnD16Bits = 13107; @@ -196,7 +209,7 @@ void DrawDepth(AgcDriver::VulkanDevice& device, std::uint64_t address, VkExtent2 }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, static_cast(extent.width) * extent.height * 4u, 0xe4u}; + state.color = {reinterpret_cast(Pixels.data()), extent, VK_FORMAT_R8G8B8A8_UNORM, AgcDriver::Graphics::ColorTargetLayout(extent.width, extent.height, AgcDriver::Graphics::ColorTileMode::Linear).Bytes(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{address, 0, extent, format, 1.0f, 0}; @@ -303,6 +316,13 @@ void DepthBitsArrayCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std: // A 2D shader image over a 2D-array T# reads the array's first slice: a one-slice array, which samples the depth // image itself, and a 3-slice array, which samples the plane copy. +void DepthBitsBaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + Shape shape{Format16Uint, 256, 128, Type2DArray, 1, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, block.At(DepthBitsOffset), shape, 0, output); + Expect(output, 0x1234u, "D16 depth-bits array from base slice 1"); +} + void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { auto* one = block.At(OneSliceOffset); DrawDepth(device, reinterpret_cast(one), {128, 128}, VK_FORMAT_D32_SFLOAT, DrawnDepth); @@ -316,20 +336,58 @@ void FirstSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::spa Expect(output, Bits(DrawnDepth), "3-slice D32 2D array through a 2D shader image"); } -// A plane array from a later base slice is still refused (its slice offsets are not modeled here). -void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { - bool refused = false; +void PaddedCubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(PaddedCubeOffset); + DrawDepth(device, reinterpret_cast(base), {128, 128}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1000u + face, 2u); + const Shape shape{Format16Uint, 128, 128, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1000u + face, "D16 cube with padded rows, face " + std::to_string(face)); + } +} + +void SmallD16CubeCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(SmallCubeOffset); + DrawDepth(device, reinterpret_cast(base), {32, 32}, VK_FORMAT_D16_UNORM, DrawnD16Depth); + for (std::uint32_t face = 1; face < 6u; ++face) FillSlice(base + face * SliceBytes, SliceBytes, 0x1230u + face, 2u); + const Shape shape{Format16Uint, 32, 32, TypeCube, 0, Swizzle64KbZX}; + for (std::uint32_t face = 0; face < 6u; ++face) { + Load(device, base, shape, face, output); + Expect(output, face == 0 ? DrawnD16Bits : 0x1230u + face, "small D16 cube face " + std::to_string(face)); + } +} + +void BaseSliceCase(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span output) { + auto* base = block.At(ArrayOffset); + Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; + shape.baseArray = 1; + Load(device, base, shape, 0, output); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1, slice 0"); + Load(device, base, shape, 1, output); + Expect(output, Bits(0.75f), "D32 2D array from base slice 1, slice 1"); + Load(device, base, shape, 0, output, true); + Expect(output, Bits(0.5f), "D32 2D array from base slice 1 through a 2D shader image"); +} + +void ExpectRefusal(AgcDriver::VulkanDevice& device, GuestBlock& block, std::size_t offset, const Shape& shape, std::uint32_t baseArray, const std::string& expected, const std::string& what) { + const auto words = TextureDescriptor(block.At(offset), shape); + auto resource = AgcDriver::Graphics::DecodeTextureResource(words); + resource.baseArray = baseArray; try { - Shape shape{Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}; - shape.baseArray = 1; - Load(device, block.At(ArrayOffset), shape, 0, output); + static_cast(AgcDriver::Graphics::DepthSurfaceTexture(AgcDriver::VulkanDeviceTestAccess::ContextOf(device), words, resource, {})); } catch (const std::runtime_error& error) { - refused = std::string(error.what()).find("sampling the depth planes of depth surface") != std::string::npos; - if (!refused) throw; + Require(std::string(error.what()).find(expected) != std::string::npos, what + " was refused with an unexpected message: " + error.what()); + return; } - Require(refused, "a depth plane array from base slice 1 was not refused"); + Require(false, what + " was not refused"); } +void RefusalCase(AgcDriver::VulkanDevice& device, GuestBlock& block) { + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2DArray, 2, Swizzle64KbZX}, 3, "base slice 3 is not implemented", "a depth plane array whose base slice is past its last slice"); + ExpectRefusal(device, block, CubeOffset, {Format32Float, 128, 128, TypeCube, 5, Swizzle64KbZX}, 1, "base slice 1 is not implemented", "a depth plane cube from base slice 1"); + ExpectRefusal(device, block, ArrayOffset, {Format32Float, 128, 128, Type2D, 0, Swizzle64KbZX}, 1, "slice 1 is not implemented", "a 2D depth plane view with a base slice"); +} } int main() { @@ -342,8 +400,12 @@ int main() { CubeCase(*device, block, output); IntegerCase(*device, block, output); DepthBitsArrayCase(*device, block, output); + DepthBitsBaseSliceCase(*device, block, output); FirstSliceCase(*device, block, output); - RefusalCase(*device, block, output); + PaddedCubeCase(*device, block, output); + SmallD16CubeCase(*device, block, output); + BaseSliceCase(*device, block, output); + RefusalCase(*device, block); std::puts("depth plane sampling tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 0739bd6b861..f83e70edb50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -246,6 +246,52 @@ void Run(Context context, std::uint8_t* block) { ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); layeredDepth.baseArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); + const std::array eighth{std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f)}; + Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *own), "a storage image over the depth plane that the depth surface did not hand out was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); +} + +void RunPairings(Context context, std::uint8_t* block) { + const auto address = reinterpret_cast(block); + const auto submit = [&](Recorder& recorder) { + recorder.Submit(); + Check(context.Function("vkQueueWaitIdle")(context.queue), "vkQueueWaitIdle"); + }; + Recorder recorder(context); + recorder.Activate(); + const auto readBack = [&](StorageTexture& texture) { + texture.MarkDirty(); + texture.WriteBack(); + submit(recorder); + }; + const DepthTarget first{address, address + BlockBytes / 2, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7}; + const DepthTarget second{address, address + BlockBytes / 2 + 8192, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.5f, 3}; + Require(DepthSurfaceView(context, first) != VK_NULL_HANDLE && DepthSurfaceView(context, second) != VK_NULL_HANDLE, "the two pairings of the depth plane were not created"); + const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(depth != nullptr, "the depth plane has no storage image"); + submit(recorder); + const std::array pattern{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + const char* refusal = nullptr; + Require(depth->FillClear(pattern, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); + Require(DepthSurfaceStoreBack(context, *depth), "the newest pairing's storage image was not stored back"); + submit(recorder); + const auto reread = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); + Require(reread != nullptr, "the depth plane has no storage image"); + submit(recorder); + readBack(*reread); + Require(CountFloats(block, 0.75f) == Side * Side, "a storage image stored back went into an older pairing instead of the pairing that handed it out"); + auto layered = PlaneResource(address, Format32Float); + layered.dimension = TextureDimension::k2DArray; + Require(DepthSurfaceStorage(context, {}, layered, 0) != nullptr, "a one-layer 2D array view of the depth plane was not taken"); + ExpectFailure([&] { static_cast(DepthSurfaceStoreBack(context, *depth)); }, "through a view that another view of the plane", "a store back through a replaced view was taken by an older pairing that covers the plane"); } } @@ -269,6 +315,8 @@ int main() { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); Run(context, block); ClearDepthSurfaces(context.device); + RunPairings(context, block); + ClearDepthSurfaces(context.device); } GuestAllocations::Mutation().Remove(block); std::cout << "depth surface storage access tests passed\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp new file mode 100644 index 00000000000..bbac636c998 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FlipStoresSmallResults.cpp @@ -0,0 +1,155 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.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 +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +constexpr std::uint32_t Threads = 8; +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t OutputHandle = 7; +constexpr std::size_t BlockBytes = 0x20000; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Written = 0x12345678u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Written, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(4) std::atomic Gate = 0; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::uint32_t Low(const void* data) { + return static_cast(AddressOf(data)); +} + +std::uint32_t High(const void* data) { + return static_cast(AddressOf(data) >> 32u); +} + +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 Frame(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 std::array program{static_cast(AddressOf(StoreCode.data()) >> 8u), static_cast(AddressOf(StoreCode.data()) >> 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}); + words.insert(words.end(), {AgcDriver::FlipPacketHeader, OutputHandle, 0xfffffffeu, 1, 0, 0}); + words.insert(words.end(), {0xc0053c00u, 0x13u, Low(&Gate), High(&Gate), 1u, 0xffffffffu, 0x19u}); + return words; +} + +class Request final : public AgcDriver::IFlipRequest { +public: + explicit Request(std::atomic& ready) : ready(ready) {} + void GpuReady(const std::shared_ptr&) override { ++ready; } + void Fail(std::exception_ptr) noexcept override {} +private: + std::atomic& ready; +}; + +class Output final : public AgcDriver::IVideoOutput { +public: + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return std::make_shared(ready); } + void Fail(std::exception_ptr) noexcept override {} + std::atomic ready = 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; + Require(bytes <= AgcDriver::Graphics::StorageTexture::SmallStoreBytes, "the test image is larger than an image the flip stores"); + const volatile std::uint32_t* cpu = texels; + const auto stored = [&] { + for (std::size_t index = 0; index < bytes / 4u; ++index) { + if (cpu[index] == Written) return true; + } + return false; + }; + auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(OutputHandle, output); + + auto words = Frame(texels); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + while ((output->ready.load() == 0 || !stored()) && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + const bool flipped = output->ready.load() == 1; + const bool seen = stored(); + Gate.store(1, std::memory_order_release); + + AgcDriverWaitIdle_nid_postfix(); + AgcDriverUnregisterVideoOutput_nid_postfix(OutputHandle, output); + AgcDriverShutdown_nid_postfix(); + { + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + } + Require(flipped, "the flip did not reach the GPU"); + Require(seen, "a small image written before a flip did not reach guest memory while the queue kept working"); + std::puts("flip stores small 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/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d5eb6a23e54..43f1799617f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -237,6 +237,22 @@ alignas(256) constexpr std::array ReadPointerCode{ 0xbf810000u, }; +alignas(256) constexpr std::array SplitRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0x8f108202u, 0xf4200444u, + 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0xf4280a02u, 0x24000020u, 0x360200bfu, 0x7e020d01u, + 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, 0x01090401u, 0x7e120202u, + 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) constexpr std::array IndirectRecordCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, + 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf4240b02u, 0x24000008u, 0xf40c0916u, 0xfa000020u, + 0x360200bfu, 0x7e020d01u, 0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c0f08u, + 0x01090401u, 0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u, +}; + +alignas(256) std::array Indirect{}; + constexpr std::array PointClamp{0x92u, (4u * 256u) << 12u, 0u, 0u}; constexpr std::array LinearWrap{0u, (4u * 256u) << 12u, (1u << 20u) | (1u << 22u), 0u}; constexpr std::array PointWrap{0u, (4u * 256u) << 12u, 1u << 24u, 0u}; @@ -407,6 +423,26 @@ Outcome RunPointer(AgcDriver::VulkanDevice& device, const std::array +Outcome RunIndirectRecords(AgcDriver::VulkanDevice& device, const std::array& code, std::uint32_t groups) { + const auto table = Buffer(Table.data(), static_cast(sizeof(Table))); + const auto keys = Buffer(Keys.data(), static_cast(sizeof(Keys))); + const auto output = Buffer(Output.data(), static_cast(sizeof(Output))); + std::copy(table.begin(), table.end(), Srt.begin()); + std::copy(keys.begin(), keys.end(), Srt.begin() + 4); + std::copy(output.begin(), output.end(), Srt.begin() + 8); + std::copy(PointClamp.begin(), PointClamp.end(), Srt.begin() + 12); + const std::span span(code); + const std::array memory{{ + {reinterpret_cast(span.data()), std::as_bytes(span)}, + {reinterpret_cast(Srt.data()), std::as_bytes(std::span(Srt))}, + {reinterpret_cast(Table.data()), std::as_bytes(std::span(Table))}, + {reinterpret_cast(Keys.data()), std::as_bytes(std::span(Keys))}, + {reinterpret_cast(Indirect.data()), std::as_bytes(std::span(Indirect))}, + }}; + return Execute(device, span, Srt.data(), memory, groups, Lanes, 64); +} + void SetEntry(std::uint32_t entry, const std::array& image) { auto* words = Root.data() + (PaletteOffset + entry * 32u) / 4u; std::fill(words, words + 8, 0u); @@ -1035,6 +1071,64 @@ void RunPointerTests(AgcDriver::VulkanDevice& device) { RequireRed(reads, 4, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "pointer image table read: key 0x1ff masked to entry 255"); } +void RunPointerRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + Indirect = {}; + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + for (std::uint32_t record = 0; record < 3; ++record) { + const std::uint32_t entry = 2u - record; + std::copy(images[record].begin(), images[record].end(), Indirect.data() + entry * 16u + 8u); + const auto pointer = static_cast(reinterpret_cast(Indirect.data() + entry * 16u)); + Table[record * (Stride / 4u) + 2u] = static_cast(pointer); + Table[record * (Stride / 4u) + 3u] = static_cast(pointer >> 32u); + } + const auto indirect = RunIndirectRecords(device, IndirectRecordCode, 3); + Require(!Faulted(indirect), "image table: a T# behind a pointer in its record faulted:\n" + indirect.log); + Require(indirect.imageTableCount == 3u, "image table: T#s behind pointers in their records do not hold one element per distinct T# (" + std::to_string(indirect.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(indirect, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# behind a pointer in its record"); + + const auto entry = Indirect.data() + 16u; + std::copy(floatImage.begin(), floatImage.end(), entry + 8u); + const auto changed = RunIndirectRecords(device, IndirectRecordCode, 3); + RequireRed(changed, 1, Lanes, texels[0], "float texels after the T# behind record 1's pointer changed"); +} + +void RunSplitRecordTests(AgcDriver::VulkanDevice& device) { + Fill(); + const auto floatImage = Image(FloatTexels.data(), Format32Float); + const auto uintImage = Image(UintTexels.data(), Format32UInt); + const auto sintImage = Image(SintTexels.data(), Format32SInt); + const std::array, 3> images{floatImage, uintImage, sintImage}; + const std::array, 3> texels{ + [](std::uint32_t x) { return std::bit_cast(FloatTexels[x]); }, + [](std::uint32_t x) { return UintTexels[x]; }, + [](std::uint32_t x) { return static_cast(SintTexels[x]); }, + }; + const std::array names{"float", "uint", "sint"}; + + Table = {}; + for (std::uint32_t record = 0; record < 3; ++record) SetRecord(record, PointClamp, images[record]); + Keys = {}; + for (std::uint32_t group = 0; group < 3; ++group) Keys[group] = 2u - group; + const auto split = Run(device, SplitRecordCode, 3, Lanes, 64); + Require(!Faulted(split), "image table: a T# read by two loads of its record faulted:\n" + split.log); + Require(split.imageTableCount == 3u, "image table: a T# read by two loads of its record does not hold one element per distinct T# (" + std::to_string(split.imageTableCount) + ")"); + for (std::uint32_t group = 0; group < 3; ++group) RequireRed(split, group, Lanes, texels[2u - group], std::string(names[2u - group]) + " texels of a T# read by two loads of its record"); +} + void RunPointerStorageWriteTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array records{}; alignas(256) static std::array otherRecords{}; @@ -1234,6 +1328,8 @@ int main() { RunModeTests(*device); RunStoreTests(*device); RunPointerTests(*device); + RunPointerRecordTests(*device); + RunSplitRecordTests(*device); RunPointerStorageWriteTests(*device); RunGraphicsTests(*device); std::puts("image table tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp new file mode 100644 index 00000000000..2233d80ba37 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparationScopeSettle.cpp @@ -0,0 +1,150 @@ +#include "VulkanTestDevice.hpp" +#include "SceShaders.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps); + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) const std::array VertexCode{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; +alignas(256) const std::array PixelCode{0xbf810000u}; + +struct VertexHeader { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + ShaderUserData users{}; + std::array context{{{0x2d5, 0x2000}}}; + + VertexHeader() { + const auto address = reinterpret_cast(VertexCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(VertexHeader); + shader.shader_size = sizeof(VertexCode); + shader.code = VertexCode.data(); + shader.type = 2; + registers = {{{0xc8, static_cast(address >> 8u)}, {0xc9, static_cast(address >> 40u)}, {0x8b, 0}, {0x8a, 0}, {0xca, 0}, {0xcb, 0}, {0xcc, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + shader.specials = &specials; + shader.user_data = &users; + specials.dispatch_modifier = 0x8000; + } +}; + +struct PixelHeader { + Shader shader{}; + std::array registers{}; + std::array context{{{0x1b3, 2}, {0x1b4, 2}, {0x1b6, 1}}}; + + PixelHeader() { + const auto address = reinterpret_cast(PixelCode.data()); + shader.file_header = 0x34333231u; + shader.version = 0x18; + shader.header_size = sizeof(PixelHeader); + shader.shader_size = sizeof(PixelCode); + shader.code = PixelCode.data(); + shader.type = 1; + registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00a, 0}, {0x00b, 0}}}; + shader.sh_registers = registers.data(); + shader.num_sh_registers = registers.size(); + shader.cx_registers = context.data(); + shader.num_cx_registers = context.size(); + } +}; + +class HeldPreparationThreads { +public: + HeldPreparationThreads() { + for (unsigned index = 0; index < std::thread::hardware_concurrency() + 2u; ++index) { + AgcDriver::DriverDetail::SubmitRegistrationPreparation([state = state] { + std::unique_lock lock(state->mutex); + state->changed.wait(lock, [&] { return state->released; }); + }); + } + } + + ~HeldPreparationThreads() { + Release(); + } + + void Release() { + { + std::lock_guard lock(state->mutex); + state->released = true; + } + state->changed.notify_all(); + } + +private: + struct State { + std::mutex mutex; + std::condition_variable changed; + bool released = false; + }; + std::shared_ptr state = std::make_shared(); +}; + +void RegistrationBesidePendingLink() { + VertexHeader vertex; + VertexHeader other; + PixelHeader pixel; + AgcDriverRegisterShader_nid_postfix(&pixel.shader); + std::future link; + std::future registration; + bool registeredBeside = false; + std::array mapping{}; + { + HeldPreparationThreads held; + AgcDriverRegisterShader_nid_postfix(&vertex.shader); + link = std::async(std::launch::async, [&] { return sceAgcCreateInterpolantMapping_0100(mapping.data(), &vertex.shader, &pixel.shader); }); + std::this_thread::sleep_for(std::chrono::seconds(1)); + Require(link.wait_for(std::chrono::seconds(0)) == std::future_status::timeout, "the link did not wait for the vertex shader's registration preparation"); + registration = std::async(std::launch::async, [&] { AgcDriverRegisterShader_nid_postfix(&other.shader); }); + registeredBeside = registration.wait_for(std::chrono::seconds(5)) == std::future_status::ready; + held.Release(); + } + registration.get(); + Require(link.get() == 0, "the interpolant mapping failed"); + Require(registeredBeside, "a link waiting for a registration preparation blocked the registration of another shader"); + for (std::uint32_t index = 0; index < mapping.size(); ++index) Require(mapping[index].offset == 0x191u + index, "the interpolant mapping wrote another register"); +} + +} + +int main() { + try { + if (!OpenVulkanTestDevice()) return VulkanTestSkipped; + RegistrationBesidePendingLink(); + AgcDriverShutdown_nid_postfix(); + std::cout << "preparation scope settle tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& shutdownError) { + std::cerr << shutdownError.what() << '\n'; + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index c4fb6eba1f0..777dd733f47 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -304,6 +304,31 @@ std::exception_ptr InjectedFailure() { } } +void AwaitedRegistrationPreparation() { + std::atomic ran{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + ran.store(true, std::memory_order_release); + }); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(ran.load(std::memory_order_acquire), "a submission did not wait for the registration preparation queued before it"); + std::atomic slow{false}; + std::atomic fast{false}; + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + slow.store(true, std::memory_order_release); + }); + auto waiter = std::async(std::launch::async, [&] { + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + return slow.load(std::memory_order_acquire); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + AgcDriver::DriverDetail::SubmitRegistrationPreparation([&] { fast.store(true, std::memory_order_release); }); + Require(waiter.get(), "a submission stopped waiting for an earlier preparation once a later one finished"); + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); + Require(fast.load(std::memory_order_acquire), "the later preparation did not run"); +} + void PendingRegistrationPreparation(AgcDriver::VulkanDevice& device) { alignas(256) const std::array code{0xbf810000u}; std::array users{}; @@ -673,6 +698,7 @@ int main(int argc, char** argv) { RegisteredVertexWorkgroup(device->Target()); FailureCapture(*device); PendingRegistrationPreparation(*device); + AwaitedRegistrationPreparation(); Require(argc != 2 || std::string_view(argv[1]) != "--fail-before-registration", "injected failure before registration"); PrepareMultisampledStorage(*device); device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp new file mode 100644 index 00000000000..da0ad032b76 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -0,0 +1,333 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t SampleShift = 2; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Float16x4Info = 0x0730u; +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Format16161616Float = 71; +constexpr std::uint32_t Type2DMsaa = 14; +constexpr std::uint32_t TileRenderTarget = 0x1b; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr std::array Drawn{0.25f, 0.5f, 0.125f, 1.0f}; +constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::uint32_t FillOnes = 0x3c003c00u; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0802ff, 0x3e800000, 0x7e0a02ff, 0x3f000000, 0x7e0c02ff, 0x3e000000, 0x7e0e02ff, 0x3f800000, 0xf800180f, 0x07060504, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {0.83f, -1.0f, 0.5f, 1.0f}, {-1.0f, 0.71f, 0.5f, 1.0f} +}}; + +alignas(256) std::array Buffer{}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Float16x4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16G16B16A16_SFLOAT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 RGBA16F surface of 4 samples"); + return color; +} + +AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = 0xf; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, 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}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = Samples; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + 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 codeAddress = reinterpret_cast(PixelCode.data()); + const std::span pixelCode(PixelCode); + 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, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::array TargetDescriptor(const Block& block, std::uint32_t sampleShift) { + const auto address = block.Address(); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16161616Float << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (sampleShift << 16u) | (TileRenderTarget << 20u) | (Type2DMsaa << 28u), + 0u, + sampleShift << 4u, + 0u, + 0u, + }; +} + +ShaderRecompiler::RecompileResult CompileLoad(AgcDriver::VulkanDevice& device, const std::array& texture, std::vector& userData) { + const auto address = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + userData.assign(16, 0u); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::span code(LoadCode); + 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; + return ShaderRecompiler::Recompile(request); +} + +std::vector> LoadSamples(AgcDriver::VulkanDevice& device, const Block& block) { + std::vector userData; + const auto program = CompileLoad(device, TargetDescriptor(block, SampleShift), userData); + std::vector> loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) loaded[first + tid][component] = std::bit_cast(Buffer[tid * Words + LoadResult + component]); + } + } + return loaded; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Triangle[from]); + const auto b = pixels(Triangle[(from + 1u) % Triangle.size()]); + const auto c = pixels(Triangle[(from + 2u) % Triangle.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +void CheckSamples(const AgcDriver::Graphics::State& state, const std::vector>& loaded) { + const auto locations = AgcDriver::Graphics::SampleLocations(state); + std::array pixelsByCoverage{}; + std::array coveredBySample{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto& texel = loaded[(y * Side + x) * Samples + sample]; + const bool drawn = texel == Drawn; + const bool empty = texel == std::array{}; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) loaded (%g, %g, %g, %g)", sample, x, y, texel[0], texel[1], texel[2], texel[3]); + Require(drawn || empty, std::string(where) + ", neither the drawn color nor the cleared target"); + covered += drawn ? 1u : 0u; + coveredBySample[sample] += drawn ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require(drawn == (distance > 0), std::string(where) + (distance > 0 ? ", which the triangle covers" : ", which the triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0, "the triangle covers every pixel or none"); + Require(pixelsByCoverage[1] + pixelsByCoverage[2] + pixelsByCoverage[3] != 0, "no pixel is partly covered, so the samples of a pixel were not told apart"); + Require(std::adjacent_find(coveredBySample.begin(), coveredBySample.end(), std::not_equal_to<>()) != coveredBySample.end(), "every sample index has the same coverage, so the sample index was not told apart"); +} + +void CheckFilled(const std::vector>& loaded) { + for (std::size_t index = 0; index < loaded.size(); ++index) { + const auto& texel = loaded[index]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %zu of pixel (%zu, %zu) loaded (%g, %g, %g, %g)", index % Samples, index / Samples % Side, index / Samples / Side, texel[0], texel[1], texel[2], texel[3]); + Require(texel == std::array{1.0f, 1.0f, 1.0f, 1.0f}, std::string(where) + " after a fill of the target's color memory with half-float ones"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, const std::array& texture, const std::string& expected, const std::string& what) { + std::string refusal; + try { + std::vector userData; + const auto program = CompileLoad(device, texture, userData); + Buffer.fill(0u); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block block(4u * BlockBytes); + Block other(BlockBytes); + const auto color = DecodeTarget(block); + const auto state = TargetState(color); + Draw(*device, state); + CheckSamples(state, LoadSamples(*device, block)); + const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); + Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); + AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); + CheckFilled(LoadSamples(*device, block)); + RequireRefused(*device, TargetDescriptor(block, 3u), "texture of 8 samples", "loading a 4x target as an 8x texture"); + RequireRefused(*device, TargetDescriptor(other, SampleShift), "which no draw rendered to", "loading a multisampled texture no draw rendered to"); + std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp new file mode 100644 index 00000000000..4f5f7f8c0bd --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageWriteBackOverhang.cpp @@ -0,0 +1,170 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.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 "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 128; +constexpr std::uint32_t Height = 120; +constexpr std::uint32_t FormatRgba16Float = 71; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Standard64KB = 0x09; +constexpr std::size_t SurfaceBytes = 0x20000; +constexpr std::size_t AllocationBytes = std::size_t{Width} * Height * 8u; +constexpr std::uint8_t Sentinel = 0xa5; + +alignas(256) constexpr std::array WriteCode{0xd7460004u, 0x0401060eu, 0xd7460005u, 0x0405060fu, 0x7e100d04u, 0x7e120d05u, 0x7e140c0du, 0x7e1602f2u, 0xf0200f08u, 0x00000804u, 0xbf810000u}; + +std::array TextureDescriptor(const void* texels) { + const auto address = reinterpret_cast(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (FormatRgba16Float << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Standard64KB << 20u) | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void Write(std::uint8_t* texels) { + std::vector data; + const auto texture = TextureDescriptor(texels); + data.insert(data.end(), texture.begin(), texture.end()); + data.resize(14, 0u); + data[13] = 7u; + std::vector words; + const auto registers = [&](std::uint32_t first, 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{8, 8, 1}; + registers(0x207, shape); + const auto code = reinterpret_cast(WriteCode.data()); + const std::array program{static_cast(code >> 8u), static_cast(code >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (7u << 7u) | (1u << 11u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, Width / 8u, Height / 8u, 1u, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::Graphics::StorageTexture::FlushAllPending("test"); + AgcDriverWaitIdle_nid_postfix(); +} + +class GuestSurface { +public: + explicit GuestSurface(bool split) { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Alignment)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Alignment - 1) & ~(static_cast(Alignment) - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + Alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Alignment - aligned); + block = reinterpret_cast(aligned); + if (AgcDriver::GuestMemory::WriteWatched()) GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, SurfaceBytes); + } +#endif + Require(block != nullptr, "storage write-back overhang: cannot allocate the guest surface"); + std::memset(block, 0, AllocationBytes); + std::memset(block + AllocationBytes, Sentinel, SurfaceBytes - AllocationBytes); + GuestAllocations::Mutation mutation; + if (split) { + mutation.Add(block, AllocationBytes, true, true, true); + mutation.Add(block + AllocationBytes, SurfaceBytes - AllocationBytes, true, true, true); + } else { + mutation.Add(block, SurfaceBytes, true, true, true); + } + this->split = split; + } + + ~GuestSurface() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + if (split) mutation.Remove(block + AllocationBytes); + } +#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 + } + + GuestSurface(const GuestSurface&) = delete; + GuestSurface& operator=(const GuestSurface&) = delete; + + std::uint8_t* Data() { return block; } + +private: + static constexpr std::size_t Alignment = 65536; + std::uint8_t* block = nullptr; + bool split = false; +}; + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + GuestSurface whole(false); + GuestSurface split(true); + Write(whole.Data()); + Write(split.Data()); + const auto* expected = whole.Data(); + const auto* stored = split.Data(); + const auto texel = [](const std::uint8_t* at) { return !std::all_of(at, at + 8, [](std::uint8_t byte) { return byte == 0 || byte == Sentinel; }); }; + std::size_t written = 0; + std::size_t past = 0; + for (std::size_t at = 0; at < SurfaceBytes; at += 8) { + written += texel(expected + at); + past += at >= AllocationBytes && texel(expected + at); + } + Require(written != 0, "storage write-back overhang: the fully registered surface received no texels"); + Require(past != 0, "storage write-back overhang: no texel of the fully registered surface lies past pitch x height"); + const auto differs = std::mismatch(expected, expected + SurfaceBytes, stored); + Require(differs.first == expected + SurfaceBytes, "storage write-back overhang: byte 0x" + [&] { char text[32]; std::snprintf(text, sizeof(text), "%zx", static_cast(differs.first - expected)); return std::string(text); }() + " differs from the fully registered surface (" + std::to_string(written) + " texels, " + std::to_string(past) + " past pitch x height)"); + Require(std::any_of(stored + AllocationBytes, stored + SurfaceBytes, [](std::uint8_t byte) { return byte == Sentinel; }), "storage write-back overhang: the padding past the image's rows was overwritten"); + AgcDriverShutdown_nid_postfix(); + std::puts("storage write-back overhang tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index c016c7423a0..2946f45e8ba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -23,6 +23,7 @@ #include #include #include +#include namespace { @@ -359,6 +360,8 @@ struct PatchDraw { std::span indices; float minLevel = 1.0f; float maxLevel = 64.0f; + std::uint32_t instances = 1; + std::uint32_t firstInstance = 0; }; alignas(256) constexpr std::array NextInputIndices{ @@ -394,7 +397,7 @@ ShaderRecompiler::RecompileResult Compile(const ShaderRecompiler::SpirvTarget& t pixel = info; } else { vertex = ShaderRecompiler::ShaderVertexStageInfo{}; - graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1}}; + graphics = ShaderRecompiler::GraphicsCompileContext{hull ? 0u : 8u, {}, std::nullopt, patches.configuration, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances}}; } auto layout = stage == ShaderStage::Fragment ? PixelPushLayout(pushCursor, target) : ShaderRecompiler::BindingLayout{0, 0, pushCursor, AgcDriver::Graphics::PipelinePushSlotBytes - pushCursor}; if (layout.pushConstantOffsetBytes < AgcDriver::Graphics::PipelinePushSlotBytes) layout.pushConstantSizeBytes = ShaderRecompiler::TessellationLevelPushOffsetBytes - layout.pushConstantOffsetBytes; @@ -477,7 +480,7 @@ void Draw(AgcDriver::VulkanDevice& device, const Stages& stages, VkCullModeFlags state.blend.colorWriteMask = 15; state.blends = {state.blend}; state.blendConstants = {}; - device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, 1, 0, true}, shaders); + device.Draw(state, {reinterpret_cast(patches.indices.data()), static_cast(patches.indices.size()), 2, patches.instances, 0, true, 0, patches.firstInstance}, shaders); device.WaitIdle(); } @@ -621,6 +624,33 @@ void RequireRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompi } } +void RequireInstancedRelativePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target) { + const auto center = Row(-0.5f + 1.0f / 3.0f); + TessellationConfiguration restarting{3u, 3u, 1u, 2u, 2u, 4u, 8u}; + restarting.breakAtInstance = true; + for (const auto& [patches, first, last] : {std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}, 128, 64}, std::tuple{PatchDraw{{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}, std::tuple{PatchDraw{restarting, Indices, 1.0f, 64.0f, 2u, 1u}, 32, 96}}) { + std::uint32_t cursor = 0; + const Stages stages{ + Compile(target, ShaderStage::Local, LocalCode, cursor, patches), + Compile(target, ShaderStage::TessellationControl, HullRelativeData, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, DomainPatchRead, cursor, patches), + Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), + }; + Draw(device, stages, VK_CULL_MODE_NONE, VK_FRONT_FACE_COUNTER_CLOCKWISE, patches); + for (const auto& [patch, expected] : {std::pair{0u, first}, std::pair{2u, last}}) { + const auto pixel = PixelAt(Column(patch, 0.25f), center); + Require(pixel[0] == 255 && std::abs(pixel[2] - expected) <= 1, "the patch in column " + std::to_string(patch) + " of an instanced draw with " + std::to_string(patches.configuration.patchesPerGroup) + " patches per group and " + std::to_string(patches.configuration.primitiveGroupSize) + " primitives per group" + (patches.configuration.breakAtInstance ? " and BREAK_WAVE_AT_EOI" : "") + " did not take its blue channel from the hull's relative patch index across instances, " + PixelText(pixel)); + } + } + const PatchDraw shared{{3u, 3u, 1u, 2u, 2u, 4u, 8u}, Indices, 1.0f, 64.0f, 2u, 1u}; + ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "domain program uses its relative patch index as data in an instanced draw", shared); + std::vector capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()); + std::erase(capabilities, static_cast(spv::CapabilityDrawParameters)); + auto withoutDrawParameters = target; + withoutDrawParameters.supportedCapabilities = capabilities; + ExpectRefusal(withoutDrawParameters, ShaderStage::TessellationControl, HullRelativeData, "shaderDrawParameters", shared); +} + class GuestBlock { public: GuestBlock() { @@ -802,8 +832,6 @@ int main() { ExpectRefusal(target, ShaderStage::Local, LocalIndexAsData, "outside a tessellation ring or LDS address"); ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.PRIM_GRP_SIZE is 0", RecordPatches); - ExpectRefusal(target, ShaderStage::TessellationControl, HullRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); - ExpectRefusal(target, ShaderStage::TessellationEvaluation, DomainRelativeData, "GE_CNTL.BREAK_WAVE_AT_EOI is clear", {{3u, 3u, 1u, 2u, 2u, 3u, 3u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullCode, "outside its patch's quad factors", {{3u, 3u, 2u, 2u, 2u}, Indices}); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine"); ExpectRefusal(target, ShaderStage::TessellationControl, HullDescendingRecords, "not affine", {{3u, 3u, 1u, 2u, 2u, 158u}, Indices}); @@ -822,6 +850,7 @@ int main() { RequireQuads(*device, target); RequirePatchConstants(*device, target); RequireRelativePatches(*device, target); + RequireInstancedRelativePatches(*device, target); RequireHullBdaReads(*device, target); RequireCompareBranches(target); device.reset(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp new file mode 100644 index 00000000000..79d60e331fb --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/TgSizeWaveId.cpp @@ -0,0 +1,94 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Threads = 256; + +alignas(256) constexpr std::array TgSizeWaveIdCode{ + 0x9384ff03, 0x00060006, 0x34020082, 0x7e040204, 0xdc708000, 0x00000201, 0xbf810000, 0xbf810000, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "TG_SIZE wave id: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { GuestAllocations::Mutation().Remove(block); } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize) { + std::memset(guest.Data(), 0xff, BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const std::span code(TgSizeWaveIdCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {true, false, false}, true, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 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(); + std::array waves{}; + std::memcpy(waves.data(), guest.Data(), sizeof(waves)); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + if (waves[thread] == thread / waveSize) continue; + Require(false, "wave" + std::to_string(waveSize) + " thread " + std::to_string(thread) + " read wave " + std::to_string(waves[thread]) + " from TG_SIZE[11:6], expected " + std::to_string(thread / waveSize)); + } + std::printf("wave%u: TG_SIZE[11:6] holds the wave index for all %u threads\n", waveSize, Threads); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + Run(*device, guest, 64); + Run(*device, guest, 32); + std::puts("TG_SIZE wave id 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/VCmpF32Denormals.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp index 54e132b6c6e..22568a2f9ad 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCmpF32Denormals.cpp @@ -95,17 +95,9 @@ std::string Hex(std::uint32_t value) { return text; } -std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture, std::uint32_t group) { +ShaderRecompiler::RecompileResult Compile(const AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode mode, std::uint32_t compare, std::uint32_t capture) { Code[6] = compare; Code[7] = capture; - for (std::uint32_t lane = 0; lane < Threads; ++lane) { - const std::uint32_t pair = group * Threads + lane; - Input[lane * Inputs] = Values[pair / ValueCount]; - Input[lane * Inputs + 1u] = Values[pair % ValueCount]; - Input[lane * Inputs + 2u] = 0u; - Input[lane * Inputs + 3u] = 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)); @@ -122,8 +114,19 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat }; request.useCache = false; request.context.floatMode = mode; - const auto result = ShaderRecompiler::Recompile(request); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + return ShaderRecompiler::Recompile(request); +} + +std::uint32_t Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& program, std::uint32_t group) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const std::uint32_t pair = group * Threads + lane; + Input[lane * Inputs] = Values[pair / ValueCount]; + Input[lane * Inputs + 1u] = Values[pair % ValueCount]; + Input[lane * Inputs + 2u] = 0u; + Input[lane * Inputs + 3u] = 0u; + } + Output.fill(0xdeadbeefu); + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(Code.data())); device.WaitIdle(); std::uint32_t mask = 0u; for (std::uint32_t lane = 0; lane < Threads; ++lane) { @@ -135,8 +138,9 @@ std::uint32_t Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloat void Check(AgcDriver::VulkanDevice& device, const char* modeName, ShaderRecompiler::ShaderFloatMode mode, const Row& row, bool exec, const std::uint32_t* expected) { const std::string name = std::string(exec ? "v_cmpx_" : "v_cmp_") + row.condition + "_f32"; + const auto program = Compile(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc); for (std::uint32_t group = 0; group < Groups; ++group) { - const std::uint32_t actual = Run(device, mode, exec ? row.cmpx : row.cmp, exec ? CaptureExec : CaptureVcc, group); + const std::uint32_t actual = Run(device, program, group); if (actual == expected[group]) continue; const std::uint32_t diff = actual ^ expected[group]; for (std::uint32_t lane = 0; lane < Threads; ++lane) { diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index 6b0f691ad58..b29adfce52e 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -117,6 +117,15 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_port_buffers PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(audio_out2_device_state_tests tests/AudioOut2DeviceState.cpp) + target_include_directories(audio_out2_device_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out2_device_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out2_device_state_tests) + add_test(NAME audio_out2_device_state COMMAND audio_out2_device_state_tests) + if(WIN32) + set_tests_properties(audio_out2_device_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(audio_out2_timing_tests tests/AudioOut2Timing.cpp) target_include_directories(audio_out2_timing_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(audio_out2_timing_tests PRIVATE libSceAudioOut libc) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index fcd7d359f82..206466563d1 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -23,6 +23,7 @@ static constexpr int PORT_TYPE_BGM = 1; static constexpr int PORT_TYPE_VOICE = 2; static constexpr int PORT_TYPE_PERSONAL = 3; static constexpr int PORT_TYPE_PADSPK = 4; +static constexpr int PORT_TYPE_EX = 6; static constexpr int PORT_TYPE_VIBRATION = 10; static constexpr int PORT_TYPE_AUDIO3D = 126; static constexpr int PORT_TYPE_AUX = 127; @@ -37,6 +38,12 @@ static constexpr std::uint64_t DRAIN_SLEEP_US = 1000; static constexpr int MONITOR_TYPE_HDMI = 1; static constexpr std::uint32_t HDMI_MONITOR_INFO_SIZE = 0x180; static constexpr std::uint32_t EX_MONITOR_INFO_MIN_SIZE = 0xA0; +static constexpr std::uint32_t EX_OPEN_MODE_MAX = 10; +static constexpr std::uint32_t EX_OPEN_MODES = 0x61F; +static constexpr std::uint32_t EX_CONFIGURE_MODE_DEFAULT = 0xFF; +static constexpr std::uint32_t EX_CONFIGURE_TARGET_DEFAULT = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = -2144993259; +static constexpr int ERROR_EX_INVALID_CONTROL = -2144960503; enum class Format { Unknown, @@ -297,6 +304,34 @@ static Port* getPort(int handle) { return &g_ports[idx]; } +static int allocatePort(int type, std::uint32_t len, std::uint32_t freq, Format format) { + std::lock_guard lock(g_mutex); + for (int i = 0; i < PORTS_MAX; i++) { + if (!g_ports[i].used) { + Port& port = g_ports[i]; + port.used = true; + port.type = type; + port.samplesNum = len; + port.freq = freq; + port.format = format; + port.channels = channelsForFormat(format); + port.lastDataOutputTime = 0; + port.virtualQueueEnd = sceKernelGetProcessTime(); + port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; + for (int c = 0; c < port.channels; c++) { + port.volume[c] = DEFAULT_VOLUME; + } + if (type != PORT_TYPE_VIBRATION && type != PORT_TYPE_EX) { + openDevice(port); + } + static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; + if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); + return i + 1; + } + } + return -2144993275; +} + extern "C" { int APS5_VABI sceAudioOutInit() { @@ -333,31 +368,7 @@ int APS5_VABI sceAudioOutOpen(int userId, int type, int index, std::uint32_t len throw std::runtime_error("sceAudioOutOpen: unknown format param"); } - std::lock_guard lock(g_mutex); - for (int i = 0; i < PORTS_MAX; i++) { - if (!g_ports[i].used) { - Port& port = g_ports[i]; - port.used = true; - port.type = type; - port.samplesNum = len; - port.freq = freq; - port.format = format; - port.channels = channelsForFormat(format); - port.lastDataOutputTime = 0; - port.virtualQueueEnd = sceKernelGetProcessTime(); - port.mixLevel = type == PORT_TYPE_PADSPK ? DEFAULT_PADSPK_MIX_LEVEL : DEFAULT_VOLUME; - for (int c = 0; c < port.channels; c++) { - port.volume[c] = DEFAULT_VOLUME; - } - if (type != PORT_TYPE_VIBRATION) { - openDevice(port); - } - static const bool trace = std::getenv("APS5_TRACE_AUDIOOUT") != nullptr; - if (trace) std::fprintf(stderr, "[audioout] port %d: type %d, %u samples at %u Hz, format %d, device %u\n", i + 1, type, len, freq, static_cast(format), static_cast(port.device)); - return i + 1; - } - } - return -2144993275; + return allocatePort(type, len, freq, format); } int APS5_VABI sceAudioOutClose(int handle) { @@ -554,13 +565,30 @@ int APS5_VABI sceAudioOutSetMixLevelPadSpk(int handle, int mixLevel) { return 0; } -int APS5_VABI sceAudioOutExClose() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExClose(int handle) { + std::lock_guard outputLock(g_outputMutex); + std::lock_guard lock(g_mutex); + Port* port = getPort(handle); + if (port == nullptr || port->type != PORT_TYPE_EX) { + throw std::runtime_error("sceAudioOutExClose: handle " + std::to_string(handle) + " is not an open Ex port"); + } + *port = Port{}; return 0; } -int APS5_VABI sceAudioOutExConfigureOutput() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option) { + if (control != 0) { + return ERROR_EX_INVALID_CONTROL; + } + if (flags != 0 || option != 0) { + throw std::runtime_error("sceAudioOutExConfigureOutput: flags " + std::to_string(flags) + " and option " + std::to_string(option) + " not supported"); + } + if (mode > EX_OPEN_MODE_MAX && mode != EX_CONFIGURE_MODE_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: mode " + std::to_string(mode) + " not supported"); + } + if ((target < 1 || target > 3) && target != EX_CONFIGURE_TARGET_DEFAULT) { + throw std::runtime_error("sceAudioOutExConfigureOutput: target " + std::to_string(target) + " not supported"); + } return 0; } @@ -578,9 +606,13 @@ int APS5_VABI sceAudioOutExGetMonitorInfo(int type, int index, void* info, std:: return 0; } -int APS5_VABI sceAudioOutExOpen() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { + (void)userId; + if (mode > EX_OPEN_MODE_MAX || ((EX_OPEN_MODES >> mode) & 1) == 0) { + return ERROR_EX_INVALID_MODE; + } + const bool highRate = mode == 3 || mode == 4 || mode == 10; + return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp index 96ca5c4273a..081850a76cf 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp @@ -17,17 +17,11 @@ #include "prx/libSceAudioOut/include/AudioOutSdl.hpp" #include "prx/libScePad/include/PadState.hpp" -// An AudioOut2 context is the hardware output queue: every push appends a grain (num_grains samples) -// to a queue of queue_depth grains that plays in real time. Titles pace their mixer on that queue, so -// the level must follow the output clock. With an SDL device open, a grain's port buffers are read at -// the next push, the device callback plays the mixed grains behind a cushion and the level counts the -// grains beyond that cushion; otherwise each push is mixed at once and a wall-clock model of the -// queue stands in. - using Clock = std::chrono::steady_clock; static constexpr std::size_t CONTEXT_MEMORY = 0x10000; static constexpr std::uint32_t DEFAULT_MAX_PORTS = 16; +static constexpr std::size_t ADVANCED_GRAINS_MAX = 16; // Pushes, polls and advances traced individually before the trace falls back to the per-second summary. static constexpr std::uint64_t CALL_TRACE_FULL = 16; // Grains the device waits for before it starts playing (and again whenever it ran dry), so the @@ -262,12 +256,24 @@ static void TraceSummary(AudioOut2Context& context, Clock::time_point now) { extern "C" { -// The title's per-tick step between setting the ports' data and pushing. A push records the buffers the -// ports point at and the next push reads them, and the push paces the clock, so nothing is due here. int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle ctx) { auto* context = FromHandle(ctx); if (!context) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; std::lock_guard lock(context->lock); + if (context->device != 0) { + if (context->advancedGrains.size() >= ADVANCED_GRAINS_MAX) throw std::runtime_error("AudioOut2: more than 16 completed grains await a push"); + if (!context->advancedMode) { + while (!context->pendingGrains.empty()) { + Render(*context, context->pendingGrains.front()); + context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); + context->pendingGrains.pop_front(); + context->pendingSince.pop_front(); + } + context->advancedMode = true; + } + context->advancedGrains.push_back(AudioOut2CaptureGrain(*context, true)); + context->grainReady.notify_one(); + } context->advances++; context->summaryAdvances++; if (context->advances <= CALL_TRACE_FULL) AUDIOOUT2_TRACE("t=%.3f Advance ctx %p (pushes so far %llu)\n", AudioOut2TraceSeconds(), static_cast(context), static_cast(context->pushes)); @@ -336,13 +342,24 @@ int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle ctx, uint32_t block lock.lock(); now = Clock::now(); } + if (context->advancedMode && context->advancedGrains.empty()) { + if (!blocking) throw std::runtime_error("AudioOut2: a non-blocking push without a rendered grain is not implemented"); + context->grainReady.wait(lock, [&] { return !context->advancedGrains.empty(); }); + now = Clock::now(); + Drain(*context, now); + } context->queued++; context->playHead += GrainDuration(*context); UpdatePadDevice(*context, now); - auto grain = AudioOut2CaptureGrain(*context); + auto grain = context->advancedMode ? std::move(context->advancedGrains.front()) : AudioOut2CaptureGrain(*context); + if (context->advancedMode) context->advancedGrains.pop_front(); const auto mixed = static_cast(grain.size()); if (context->device == 0) { Render(*context, grain); + } else if (context->advancedMode) { + Render(*context, grain); + if (PendingMs(*context) > MAX_QUEUED_MS) context->dropped++; + else context->output.insert(context->output.end(), context->mix.begin(), context->mix.end()); } else { context->pendingGrains.push_back(std::move(grain)); context->pendingSince.push_back(Clock::now()); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp index 1a6f8cc7e40..8832eb873a3 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Internal.hpp @@ -1,6 +1,8 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOOUT_SRC_AUDIOOUT2INTERNAL_HPP +#include +#include #include #include #include @@ -47,6 +49,8 @@ struct AudioOut2CapturedPort { std::size_t index = 0; std::uint64_t generation = 0; const void* data = nullptr; + std::vector pcm; + std::array volume{}; }; using AudioOut2Grain = std::vector; @@ -86,6 +90,9 @@ struct AudioOut2Context { // Mixed stereo frames the device has not played yet; it plays silence until the cushion is mixed. std::deque output; bool priming = true; + bool advancedMode = false; + std::condition_variable grainReady; + std::deque advancedGrains; std::deque pendingGrains; std::deque pendingSince; SDL_AudioDeviceID padDevice = 0; @@ -112,7 +119,7 @@ struct AudioOut2Context { // Mixes every port of the context that carries PCM data into out (stereo float, frames frames), summing // onto the zeroed buffer. Returns the number of ports mixed. std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames); -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context); +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot = false); bool AudioOut2HasPadPorts(const AudioOut2Context& context); // Forgets the ports of a context being destroyed. void AudioOut2ReleasePorts(const AudioOut2Context& context); diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp index bbb30e79688..cc72dbbc161 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -74,15 +75,15 @@ static void ReadFrame(const AudioOut2Port& port, const void* data, std::uint32_t } } -static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames) { +static void AccumulatePadPort(const AudioOut2Port& port, const void* data, AudioOut2Route route, float* out, std::uint32_t frames, const float* volume) { float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { ReadFrame(port, data, frame, in); - AudioOut2AccumulatePadFrame(route, in, port.channels, port.volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); + AudioOut2AccumulatePadFrame(route, in, port.channels, volume, out + static_cast(frame) * AUDIO_OUT2_PAD_CHANNELS); } } -static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames) { +static void AccumulatePort(const AudioOut2Port& port, const void* data, float* out, std::uint32_t frames, const float* volume) { const auto& fold = *port.fold; float in[AUDIO_OUT2_PORT_CHANNELS_MAX]; for (std::uint32_t frame = 0; frame < frames; frame++) { @@ -90,7 +91,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o float left = 0.0f; float right = 0.0f; for (std::uint32_t c = 0; c < port.channels; c++) { - const float sample = in[c] * port.volume[c]; + const float sample = in[c] * volume[c]; if (fold.left[c] != 0.0f) left += sample * fold.left[c]; if (fold.right[c] != 0.0f) right += sample * fold.right[c]; } @@ -99,7 +100,7 @@ static void AccumulatePort(const AudioOut2Port& port, const void* data, float* o } } -AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { +AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context, bool snapshot) { std::lock_guard lock(g_portsLock); AudioOut2Grain grain; for (std::size_t index = 0; index < g_ports.size(); index++) { @@ -110,7 +111,15 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { data = port.queuedData.front(); port.queuedData.pop_front(); } - if (data != nullptr) grain.push_back({index, port.generation, data}); + if (data != nullptr) { + AudioOut2CapturedPort captured{index, port.generation, data}; + if (snapshot) { + captured.pcm.resize(static_cast(context.grain) * port.channels * (port.int16 ? sizeof(std::int16_t) : sizeof(float))); + std::memcpy(captured.pcm.data(), data, captured.pcm.size()); + std::copy_n(port.volume, port.channels, captured.volume.begin()); + } + grain.push_back(std::move(captured)); + } } return grain; } @@ -118,13 +127,17 @@ AudioOut2Grain AudioOut2CaptureGrain(const AudioOut2Context& context) { std::uint32_t AudioOut2MixPorts(const AudioOut2Context& context, const AudioOut2Grain& grain, float* out, float* padOut, std::uint32_t frames) { std::lock_guard lock(g_portsLock); std::uint32_t mixed = 0; - for (const auto& [index, generation, data] : grain) { + for (const auto& captured : grain) { + const auto index = captured.index; + const auto generation = captured.generation; + const void* data = captured.pcm.empty() ? captured.data : captured.pcm.data(); if (index >= g_ports.size()) continue; const auto& port = g_ports[index]; if (!port.used || port.generation != generation || port.context != &context || port.channels == 0) continue; const auto route = AudioOut2RouteForPort(port.type, port.channels); - if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames); - else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames); + const float* volume = captured.pcm.empty() ? port.volume : captured.volume.data(); + if (route == AudioOut2Route::Main || (context.padDevice == 0 && route == AudioOut2Route::PadSpeaker)) AccumulatePort(port, data, out, frames, volume); + else if (padOut != nullptr) AccumulatePadPort(port, data, route, padOut, frames, volume); mixed++; } return mixed; @@ -219,13 +232,13 @@ int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle port) { int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortState* state) { std::lock_guard lock(g_portsLock); - if (!FromHandle(port)) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; + const auto* entry = FromHandle(port); + if (!entry) return SCE_AUDIO_OUT2_ERROR_INVALID_HANDLE; if (!state) return SCE_AUDIO_OUT2_ERROR_INVALID_ARGUMENT; + *state = AudioOut2PortState{}; state->output = OUTPUT_MAIN; - state->num_channels = AUDIO_OUT2_OUTPUT_CHANNELS; + state->num_channels = static_cast(entry->channels); state->volume = VOLUME_MAX; - state->reroute_counter = 0; - state->flags = 0; return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp new file mode 100644 index 00000000000..cfa5a7a0620 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2DeviceState.cpp @@ -0,0 +1,137 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); +int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); +int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); +int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); +int APS5_VABI sceAudioOut2PortGetState(AudioOut2PortHandle, AudioOut2PortState*); +int APS5_VABI sceAudioOut2GetSpeakerInfo(AudioOut2SpeakerInfo*, std::uint32_t); +int APS5_VABI sceAudioOut2GetSystemState(AudioOut2SystemState*); +} + +static void Require(bool value) { if (!value) std::abort(); } + +namespace { + +constexpr int errorInvalidArgument = static_cast(0x80260502); +constexpr int errorInvalidHandle = static_cast(0x80260503); +constexpr std::uint16_t portTypeMain = 0; +constexpr std::uint16_t outputMain = 1; +constexpr std::int16_t volumeMax = 127; +constexpr std::uint32_t formatFloat = 0; +constexpr std::uint32_t formatS16 = 1; +constexpr std::uint32_t frequency = 48000; + +std::uint32_t Format(std::uint32_t channels, std::uint32_t type) { + return channels << 8 | type; +} + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +AudioOut2PortState StateOfPort(AudioOut2ContextHandle context, std::uint32_t format) { + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = format; + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + AudioOut2PortState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2PortGetState(port, &state) == 0); + Require(sceAudioOut2PortDestroy(port) == 0); + return state; +} + +void PortStateFollowsTheDataFormat(AudioOut2ContextHandle context) { + struct Case { + std::uint32_t channels; + std::uint32_t sampleType; + }; + for (const Case entry : {Case{1, formatFloat}, Case{2, formatFloat}, Case{2, formatS16}, Case{8, formatFloat}, Case{12, formatS16}}) { + const auto state = StateOfPort(context, Format(entry.channels, entry.sampleType)); + Require(state.output == outputMain); + Require(state.num_channels == entry.channels); + Require(state.volume == volumeMax); + Require(state.reroute_counter == 0); + Require(state.flags == 0); + Require(state.pad1 == 0 && state.pad2 == 0); + for (const auto word : state.reserved) Require(word == 0); + } +} + +void PortStateRejectsBadArguments(AudioOut2ContextHandle context) { + AudioOut2PortState state{}; + Require(sceAudioOut2PortGetState(0, &state) == errorInvalidHandle); + Require(sceAudioOut2PortGetState(0x7fff, &state) == errorInvalidHandle); + + AudioOut2PortParam params{}; + params.port_type = portTypeMain; + params.data_format = Format(2, formatFloat); + params.sampling_freq = frequency; + AudioOut2PortHandle port = 0; + Require(sceAudioOut2PortCreate(context, ¶ms, &port) == 0); + Require(sceAudioOut2PortGetState(port, nullptr) == errorInvalidArgument); + Require(sceAudioOut2PortDestroy(port) == 0); + Require(sceAudioOut2PortGetState(port, &state) == errorInvalidHandle); +} + +void SpeakerInfoReportsTheFrontPair() { + AudioOut2SpeakerInfo info; + std::memset(&info, 0xAB, sizeof(info)); + Require(sceAudioOut2GetSpeakerInfo(&info, 0) == 0); + Require(info.type == 0); + Require(info.available_bits == 0x3); + Require(info.flags == 0); + Require(info.speaker_angle[0].azimuth == -30 && info.speaker_angle[0].elevation == 0); + Require(info.speaker_angle[1].azimuth == 30 && info.speaker_angle[1].elevation == 0); + for (int index = 2; index < 16; index++) Require(info.speaker_angle[index].azimuth == 0 && info.speaker_angle[index].elevation == 0); + Require(sceAudioOut2GetSpeakerInfo(nullptr, 0) == errorInvalidArgument); +} + +void SystemStateIsSilentAndRejectsNull() { + AudioOut2SystemState state; + std::memset(&state, 0xAB, sizeof(state)); + Require(sceAudioOut2GetSystemState(&state) == 0); + Require(state.loudness == 0.0f); + Require(sceAudioOut2GetSystemState(nullptr) == errorInvalidArgument); +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_device_state-" + std::to_string(std::random_device{}()) + ".raw"); + std::filesystem::remove(path); + SetEnvironment("SDL_DISKAUDIOFILE", path.string()); + + AudioOut2ContextParam params{}; + Require(sceAudioOut2ContextResetParam(¶ms) == 0); + params.num_grains = 256; + params.queue_depth = 1; + AudioOut2ContextHandle context = 0; + Require(sceAudioOut2ContextCreate(¶ms, nullptr, 0, &context) == 0); + + PortStateFollowsTheDataFormat(context); + PortStateRejectsBadArguments(context); + SpeakerInfoReportsTheFrontPair(); + SystemStateIsSilentAndRejectsNull(); + + Require(sceAudioOut2ContextDestroy(context) == 0); + std::filesystem::remove(path); + return 0; +} diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp index 54f467b3348..cac79482e34 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2PortBuffers.cpp @@ -1,5 +1,7 @@ #include "SceTypes.hpp" +#include +#include #include #include #include @@ -7,15 +9,18 @@ #include #include #include +#include #include #include #include +#include #include extern "C" { int APS5_VABI sceAudioOut2ContextResetParam(AudioOut2ContextParam*); int APS5_VABI sceAudioOut2ContextCreate(const AudioOut2ContextParam*, void*, std::size_t, AudioOut2ContextHandle*); int APS5_VABI sceAudioOut2ContextDestroy(AudioOut2ContextHandle); +int APS5_VABI sceAudioOut2ContextAdvance(AudioOut2ContextHandle); int APS5_VABI sceAudioOut2ContextPush(AudioOut2ContextHandle, std::uint32_t); int APS5_VABI sceAudioOut2PortCreate(AudioOut2ContextHandle, const AudioOut2PortParam*, AudioOut2PortHandle*); int APS5_VABI sceAudioOut2PortDestroy(AudioOut2PortHandle); @@ -49,7 +54,7 @@ void SetData(AudioOut2PortHandle port, const void* data) { Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); } -std::vector Play(const std::function& feed) { +std::vector Play(const std::function& feed, bool advanced = false) { const auto path = std::filesystem::temp_directory_path() / ("anyps5_audio_out2_port_buffers-" + std::to_string(std::random_device{}()) + ".raw"); std::filesystem::remove(path); SetEnvironment("SDL_DISKAUDIOFILE", path.string()); @@ -67,7 +72,13 @@ std::vector Play(const std::function volume{0.5f, 0.5f}; + const AudioOut2Attribute attribute{1, 0, volume.data(), sizeof(volume)}; + Require(sceAudioOut2PortSetAttributes(port, &attribute, 1) == 0); + SetData(port, buffer.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + }, true); + RequirePlayed(played, {&buffer, &reduced}); +} + +void TestPushingThreadWaitsForAdvance() { + const auto first = Grain(0.01f); + const auto second = Grain(0.02f); + const auto played = Play([&](AudioOut2ContextHandle context, AudioOut2PortHandle port) { + SetData(port, first.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(sceAudioOut2ContextPush(context, 1) == 0); + std::promise entered; + std::promise done; + auto completed = done.get_future(); + std::thread pusher([&] { + entered.set_value(); + done.set_value(sceAudioOut2ContextPush(context, 1)); + }); + entered.get_future().wait(); + const bool waited = completed.wait_for(std::chrono::milliseconds(10)) == std::future_status::timeout; + SetData(port, second.data()); + Require(sceAudioOut2ContextAdvance(context) == 0); + Require(completed.get() == 0); + pusher.join(); + Require(waited); + }, true); + RequirePlayed(played, {&first, &second}); +} + } int main() { SetEnvironment("SDL_AUDIODRIVER", "disk"); + TestAdvanceRetainsRewrittenBuffer(); + TestAdvanceRetainsVolume(); + TestPushingThreadWaitsForAdvance(); TestBuffersSetBeforeOnePushPlayInOrder(); TestBurstThenGapKeepsEveryBuffer(); TestAdvancingPortWithoutNewBufferPlaysSilence(); diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp index 22f70c91287..b96f9dd570f 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutEx.cpp @@ -1,32 +1,66 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include -#include +#include extern "C" { -int APS5_VABI sceAudioOutExClose(); -int APS5_VABI sceAudioOutExConfigureOutput(); -int APS5_VABI sceAudioOutExOpen(); +int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode); +int APS5_VABI sceAudioOutExConfigureOutput(int control, std::uint64_t flags, std::uint32_t mode, std::uint32_t target, std::uint64_t option); +int APS5_VABI sceAudioOutExClose(int handle); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); } -using ExFunction = int (APS5_VABI *)(); +static constexpr int SYSTEM_USER = 0xFF; +static constexpr int ERROR_EX_INVALID_MODE = static_cast(0x80260015u); +static constexpr int ERROR_EX_INVALID_CONTROL = static_cast(0x80268009u); -static bool ReportsNotImplemented(ExFunction function, const char* name) { +static bool failed = false; + +static void Require(bool condition, const char* what) { + if (!condition) { + std::fprintf(stderr, "failed: %s\n", what); + failed = true; + } +} + +template +static bool Throws(Call call) { try { - function(); - } catch (const std::runtime_error& error) { - if (std::string(error.what()) == std::string(name) + " not implemented") return true; - std::fprintf(stderr, "%s threw: %s\n", name, error.what()); - return false; + call(); + } catch (const std::runtime_error&) { + return true; } - std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name); return false; } int main() { - bool passed = ReportsNotImplemented(sceAudioOutExClose, "sceAudioOutExClose"); - passed = ReportsNotImplemented(sceAudioOutExConfigureOutput, "sceAudioOutExConfigureOutput") && passed; - passed = ReportsNotImplemented(sceAudioOutExOpen, "sceAudioOutExOpen") && passed; - return passed ? 0 : 1; + Require(sceAudioOutExOpen(SYSTEM_USER, 5) == ERROR_EX_INVALID_MODE, "mode 5 (Linear PCM) is refused"); + Require(sceAudioOutExOpen(SYSTEM_USER, 7) == ERROR_EX_INVALID_MODE, "mode 7 is outside mask 0x61F"); + Require(sceAudioOutExOpen(SYSTEM_USER, 11) == ERROR_EX_INVALID_MODE, "mode 11 is above 10"); + + const int ac3 = sceAudioOutExOpen(SYSTEM_USER, 0); + Require(ac3 > 0, "mode 0 opens a port"); + const std::vector burst(256 * 2, 0x1234); + Require(sceAudioOutOutput(ac3, burst.data()) == 256, "a mode 0 port takes 256 frames"); + Require(sceAudioOutOutput(ac3, nullptr) == 256, "a mode 0 port drains"); + + const int eac3 = sceAudioOutExOpen(SYSTEM_USER, 4); + Require(eac3 > 0 && eac3 != ac3, "mode 4 opens a second port"); + const std::vector wide(1024 * 2, 0); + Require(sceAudioOutOutput(eac3, wide.data()) == 1024, "a mode 4 port takes 1024 frames"); + + Require(sceAudioOutExConfigureOutput(1, 0, 0, 1, 0) == ERROR_EX_INVALID_CONTROL, "a nonzero first argument is refused"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 1, 0) == 0, "mode 0 to HDMI"); + Require(sceAudioOutExConfigureOutput(0, 0, 0, 0xFF, 0) == 0, "mode 0 to the default target"); + Require(sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0) == 0, "the reset"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 11, 1, 0); }), "an unknown mode throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 0, 0, 4, 0); }), "an unknown target throws"); + Require(Throws([] { sceAudioOutExConfigureOutput(0, 1, 0, 1, 0); }), "nonzero flags throw"); + + Require(sceAudioOutExClose(ac3) == 0, "close the mode 0 port"); + Require(Throws([ac3] { sceAudioOutExClose(ac3); }), "a closed port throws"); + Require(sceAudioOutExClose(eac3) == 0, "close the mode 4 port"); + return failed ? 1 : 0; } diff --git a/core/libs/prx/libSceFiber/CMakeLists.txt b/core/libs/prx/libSceFiber/CMakeLists.txt index d2ec7534eff..6aa8ddda2a7 100644 --- a/core/libs/prx/libSceFiber/CMakeLists.txt +++ b/core/libs/prx/libSceFiber/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceFiber SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceFiber PRIVATE ${LIBS_INCLUDE_DIR}) +target_compile_options(libSceFiber PRIVATE -fno-omit-frame-pointer) configure_windows_unwind(libSceFiber) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index d38764bf71f..0d92c485bb0 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -5,14 +5,15 @@ #include #include #include +#include #include #include "Decoder/Jpeg.hpp" #include "SceTypes.hpp" -static_assert(sizeof(JpegDecCreateParam) == 0x8); +static_assert(sizeof(JpegDecCreateParam) == 0xC); static_assert(sizeof(JpegDecParseParam) == 0x10); -static_assert(sizeof(JpegDecDecodeParam) == 0x20); -static_assert(sizeof(JpegDecImageInfo) == 0x10); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(sizeof(JpegDecImageInfo) == 0x24); namespace { @@ -24,10 +25,14 @@ constexpr std::int32_t SCE_JPEG_DEC_ERROR_INVALID_WORK_MEMORY = static_cast(0x80690010); constexpr std::int32_t SCE_JPEG_DEC_ERROR_DECODE_ERROR = static_cast(0x80690012); -constexpr std::uint32_t ATTRIBUTE_NONE = 0; +constexpr std::uint32_t ATTRIBUTE_CSC_ORDINARY = 1; +constexpr std::uint32_t ATTRIBUTE_CSC_FULL = 2; constexpr std::uint32_t MEMORY_SIZE = 0x20; constexpr std::uintptr_t HANDLE_ALIGNMENT = 8; +constexpr std::uint16_t DECODE_MODE_NORMAL = 0; +constexpr std::uint16_t DOWN_SCALE_NONE = 1; + constexpr std::uint16_t PIXEL_FORMAT_R8G8B8A8 = 0; constexpr std::uint16_t PIXEL_FORMAT_B8G8R8A8 = 1; @@ -45,11 +50,16 @@ static_assert(sizeof(Context) + HANDLE_ALIGNMENT - 1 <= MEMORY_SIZE); std::int32_t validateCreateParam(const JpegDecCreateParam* param) { if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (param->size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; - if (param->attribute != ATTRIBUTE_NONE) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + if (param->this_size != sizeof(JpegDecCreateParam)) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + if (param->attribute != ATTRIBUTE_CSC_ORDINARY && param->attribute != ATTRIBUTE_CSC_FULL) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; return 0; } +void requireNormalDecode(const char* function, std::uint16_t decodeMode, std::uint16_t downScale) { + if (decodeMode != DECODE_MODE_NORMAL) throw std::runtime_error(std::string(function) + ": decode mode " + std::to_string(decodeMode) + " is not implemented"); + if (downScale != DOWN_SCALE_NONE) throw std::runtime_error(std::string(function) + ": down scale " + std::to_string(downScale) + " is not implemented"); +} + Context* toContext(void* handle) { const auto address = reinterpret_cast(handle); if (address == 0 || address % HANDLE_ALIGNMENT != 0) return nullptr; @@ -58,11 +68,14 @@ Context* toContext(void* handle) { } void fillImageInfo(const Decoder::Jpeg::Header& header, JpegDecImageInfo* info) { + *info = {}; info->image_width = header.width; info->image_height = header.height; info->color_space = COLOR_SPACE_RGB; - info->bit_depth = 8; - info->image_flag = 0; + info->component_number = static_cast(header.channels); + info->suitable_csc_attribute = ATTRIBUTE_CSC_ORDINARY; + info->output_image_width = header.width; + info->output_image_height = header.height; } std::span toBytes(const void* data, std::uint32_t size) { @@ -95,6 +108,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0 || param->image_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; if (param->pixel_format != PIXEL_FORMAT_R8G8B8A8 && param->pixel_format != PIXEL_FORMAT_B8G8R8A8) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); const std::optional header = Decoder::Jpeg::ParseHeader(jpeg); @@ -109,6 +123,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!image || image->width != header->width || image->height != header->height) return SCE_JPEG_DEC_ERROR_DECODE_ERROR; const bool swapRedBlue = param->pixel_format == PIXEL_FORMAT_B8G8R8A8; + const auto alpha = static_cast(param->alpha_value); auto* output = static_cast(param->image_mem_addr); for (std::uint32_t y = 0; y < image->height; ++y) { std::uint8_t* out = output + y * pitch; @@ -125,7 +140,7 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param pixel[2] = in[2]; } if (swapRedBlue) std::swap(pixel[0], pixel[2]); - pixel[3] = 0xFF; + pixel[3] = alpha; } } @@ -145,6 +160,7 @@ int32_t APS5_VABI sceJpegDecParseHeader(const JpegDecParseParam* param, JpegDecI if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; if (!param->jpeg_mem_addr || !image_info) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; if (param->jpeg_mem_size == 0) return SCE_JPEG_DEC_ERROR_INVALID_SIZE; + requireNormalDecode(__func__, param->decode_mode, param->down_scale); const std::optional header = Decoder::Jpeg::ParseHeader(toBytes(param->jpeg_mem_addr, param->jpeg_mem_size)); if (!header) return SCE_JPEG_DEC_ERROR_INVALID_DATA; fillImageInfo(*header, image_info); diff --git a/core/libs/prx/libSceMouse/Export.cpp b/core/libs/prx/libSceMouse/Export.cpp index f41c9eaf484..0fd4352a9f8 100644 --- a/core/libs/prx/libSceMouse/Export.cpp +++ b/core/libs/prx/libSceMouse/Export.cpp @@ -19,7 +19,7 @@ int APS5_VABI sceMouseRead(std::int32_t handle, MouseData* data, std::int32_t nu return Mouse::Read(handle, data, num); } -void MousePublishInput_nid_postfix(const MouseInputEvent& event) { +void MousePublishInput_nid_no_patch(const MouseInputEvent& event) { Mouse::Publish(event); } @@ -28,7 +28,7 @@ int APS5_VABI sceMouseResetAbsolutePosition() { return 0; } -bool MouseIsOpen_nid_postfix() { +bool MouseIsOpen_nid_no_patch() { return Mouse::IsOpen(); } diff --git a/core/libs/prx/libSceMouse/include/MouseState.hpp b/core/libs/prx/libSceMouse/include/MouseState.hpp index 15c89831b68..5abc3c3e6a7 100644 --- a/core/libs/prx/libSceMouse/include/MouseState.hpp +++ b/core/libs/prx/libSceMouse/include/MouseState.hpp @@ -24,7 +24,7 @@ int Read(std::int32_t handle, MouseData* data, std::int32_t num); void Publish(const MouseInputEvent& event); } -extern "C" void MousePublishInput_nid_postfix(const MouseInputEvent& event); -extern "C" bool MouseIsOpen_nid_postfix(); +extern "C" void MousePublishInput_nid_no_patch(const MouseInputEvent& event); +extern "C" bool MouseIsOpen_nid_no_patch(); #endif diff --git a/core/libs/prx/libSceMouse/tests/Mouse.cpp b/core/libs/prx/libSceMouse/tests/Mouse.cpp index e01f3f9b4f4..2793ec01846 100644 --- a/core/libs/prx/libSceMouse/tests/Mouse.cpp +++ b/core/libs/prx/libSceMouse/tests/Mouse.cpp @@ -92,7 +92,7 @@ int main() { for (int i = 0; i < 70; ++i) { MouseInputEvent motion{}; motion.x = i; - MousePublishInput_nid_postfix(motion); + MousePublishInput_nid_no_patch(motion); } Require(sceMouseRead(MOUSE_HANDLE, data, 64) == 64); Require(data[0].x_axis == 6 && data[63].x_axis == 69); diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831d..95bb7c755ef 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -595,6 +595,41 @@ bool SetSocketOptionForKernel(int descriptor, int level, int option, const void* const bool g_socketOptionSetterRegistered = (KernelSetSocketOptionSetter_nid_no_patch(&SetSocketOptionForKernel), true); +constexpr int GuestFcntlGetStatus = 3; +constexpr int GuestFcntlSetStatus = 4; +constexpr int GuestReadWrite = 2; +constexpr int GuestNonblocking = 4; +constexpr int GuestStatusFlagsMask = 7; + +bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* result, int* error) { + bool nonblock = false; + { + std::lock_guard lk(g_mutex); + const auto it = g_socks.find(descriptor); + if (it == g_socks.end()) return false; + nonblock = it->second.nonblock; + } + *result = 0; + *error = 0; + if (command == GuestFcntlGetStatus) { + *result = GuestReadWrite | (nonblock ? GuestNonblocking : 0); + return true; + } + if (command != GuestFcntlSetStatus) { + *error = NET_EINVAL; + return true; + } + if ((flags & ~GuestStatusFlagsMask) != 0) { + *error = NET_EOPNOTSUPP; + return true; + } + const int enabled = (flags & GuestNonblocking) != 0 ? 1 : 0; + if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) *error = *errno_slot(); + return true; +} + +const bool g_socketStatusControlRegistered = (KernelSetSocketStatusControl_nid_no_patch(&ControlSocketStatusForKernel), true); + int APS5_VABI sceNetInit_nid_postfix(void) { std::lock_guard lk(g_mutex); g_net_inited = true; diff --git a/core/libs/prx/libSceNetCtl/CMakeLists.txt b/core/libs/prx/libSceNetCtl/CMakeLists.txt index f221f21932c..0083a712aff 100644 --- a/core/libs/prx/libSceNetCtl/CMakeLists.txt +++ b/core/libs/prx/libSceNetCtl/CMakeLists.txt @@ -1,3 +1,17 @@ add_library(libSceNetCtl SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNetCtl PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceNetCtl) + +if(BUILD_TESTING) + add_test_executable(guest_net_ctl_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetCtl.cpp) + target_include_directories(guest_net_ctl_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_ctl_tests PRIVATE libSceNetCtl libc) + configure_windows_unwind(guest_net_ctl_tests) + add_test(NAME guest_net_ctl COMMAND guest_net_ctl_tests) + set_tests_properties(guest_net_ctl PROPERTIES TIMEOUT 20 SKIP_RETURN_CODE 77) + if(WIN32) + target_link_libraries(guest_net_ctl_tests PRIVATE ws2_32) + set_tests_properties(guest_net_ctl PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index c6645e18b10..27bdd35a7c1 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -4,32 +4,58 @@ #include #include #else +#include #include #include +#include #include +#include +#include +#include #ifdef __linux__ #include #endif #endif #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include +#include #include +#include #include #include -#ifdef _WIN32 #include -#endif // Network-control state reflects host interface addressing, not PSN sign-in status. static constexpr int SCE_NET_CTL_ERROR_CALLBACK_MAX = static_cast(0x80412103); static constexpr int SCE_NET_CTL_ERROR_INVALID_ID = static_cast(0x80412105); +static constexpr int SCE_NET_CTL_ERROR_INVALID_CODE = static_cast(0x80412106); static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x80412107); static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_INFO_DEVICE = 1; +static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; +static constexpr int NET_CTL_INFO_MTU = 3; +static constexpr int NET_CTL_INFO_LINK = 4; +static constexpr int NET_CTL_INFO_IP_CONFIG = 11; +static constexpr int NET_CTL_INFO_IP_ADDRESS = 14; +static constexpr int NET_CTL_INFO_NETMASK = 15; +static constexpr int NET_CTL_INFO_DEFAULT_ROUTE = 16; +static constexpr int NET_CTL_INFO_PRIMARY_DNS = 17; +static constexpr int NET_CTL_INFO_SECONDARY_DNS = 18; +static constexpr int NET_CTL_INFO_HTTP_PROXY_CONFIG = 19; +static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr std::uint32_t NET_CTL_DEVICE_WIRED = 0; +static constexpr std::uint32_t NET_CTL_LINK_CONNECTED = 1; +static constexpr std::uint32_t NET_CTL_IP_DHCP = 0; +static constexpr std::uint32_t NET_CTL_HTTP_PROXY_OFF = 0; +static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; +static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; static std::mutex g_callbackLock; @@ -105,6 +131,157 @@ static bool HostHasAddress(int family) { #endif } +struct HostIpv4Interface { + in_addr address{}; + in_addr netmask{}; + in_addr gateway{}; + std::uint32_t mtu = 0; + std::vector dns; +}; + +static bool UsableIpv4(in_addr address) { + const auto host = ntohl(address.s_addr); + return host != 0 && (host >> 24) != 127; +} + +#ifdef _WIN32 +static const sockaddr_in* Ipv4Of(const SOCKET_ADDRESS& address) { + if (!address.lpSockaddr || address.lpSockaddr->sa_family != AF_INET) return nullptr; + return reinterpret_cast(address.lpSockaddr); +} + +static std::uint32_t PrefixMask(unsigned length) { + if (length == 0) return 0; + return htonl(length >= 32 ? 0xFFFFFFFFu : 0xFFFFFFFFu << (32 - length)); +} +#else +struct HostDefaultRoute { + std::string name; + in_addr gateway{}; + unsigned long metric = 0; +}; + +static std::vector HostDefaultRoutes() { + std::ifstream table("/proc/net/route"); + if (!table) throw std::runtime_error("sceNetCtlGetInfo: the host routing table is unavailable"); + std::vector routes; + std::string line; + std::getline(table, line); + while (std::getline(table, line)) { + std::istringstream fields(line); + HostDefaultRoute route; + unsigned long destination = 0; + unsigned long gateway = 0; + unsigned long flags = 0; + unsigned long references = 0; + unsigned long uses = 0; + unsigned long mask = 0; + if (!(fields >> route.name >> std::hex >> destination >> gateway >> flags >> std::dec >> references >> uses >> route.metric >> std::hex >> mask)) continue; + if (destination != 0 || mask != 0 || !(flags & RTF_UP) || (flags & RTF_REJECT)) continue; + route.gateway.s_addr = static_cast(gateway); + routes.push_back(route); + } + return routes; +} + +static std::uint32_t HostInterfaceMtu(const char* name) { + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + if (descriptor < 0) throw std::runtime_error("sceNetCtlGetInfo: no socket to read the host interface MTU"); + ifreq request{}; + std::strncpy(request.ifr_name, name, IFNAMSIZ - 1); + const int result = ioctl(descriptor, SIOCGIFMTU, &request); + close(descriptor); + if (result != 0) throw std::runtime_error(std::string("sceNetCtlGetInfo: no MTU for host interface ") + name); + return static_cast(request.ifr_mtu); +} + +static std::vector HostDnsServers() { + std::ifstream config("/etc/resolv.conf"); + std::vector servers; + std::string line; + while (std::getline(config, line)) { + std::istringstream fields(line); + std::string keyword; + std::string value; + in_addr server{}; + if (fields >> keyword >> value && keyword == "nameserver" && inet_pton(AF_INET, value.c_str(), &server) == 1) servers.push_back(server); + } + return servers; +} +#endif + +static bool FindHostIpv4Interface(HostIpv4Interface& found) { +#ifdef _WIN32 + ULONG size = 0; + const ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS; + if (GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size) != ERROR_BUFFER_OVERFLOW) return false; + std::vector storage(size); + auto* adapters = reinterpret_cast(storage.data()); + if (GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size) != NO_ERROR) return false; + bool chosen = false; + bool chosenHasGateway = false; + ULONG chosenMetric = 0; + for (auto* adapter = adapters; adapter; adapter = adapter->Next) { + if (adapter->OperStatus != IfOperStatusUp || adapter->IfType == IF_TYPE_SOFTWARE_LOOPBACK) continue; + auto* unicast = adapter->FirstUnicastAddress; + while (unicast && !(Ipv4Of(unicast->Address) && UsableIpv4(Ipv4Of(unicast->Address)->sin_addr))) unicast = unicast->Next; + if (!unicast) continue; + auto* gateway = adapter->FirstGatewayAddress; + while (gateway && !Ipv4Of(gateway->Address)) gateway = gateway->Next; + if (chosen && !(gateway && (!chosenHasGateway || adapter->Ipv4Metric < chosenMetric))) continue; + HostIpv4Interface candidate; + candidate.address = Ipv4Of(unicast->Address)->sin_addr; + candidate.netmask.s_addr = PrefixMask(unicast->OnLinkPrefixLength); + if (gateway) candidate.gateway = Ipv4Of(gateway->Address)->sin_addr; + candidate.mtu = adapter->Mtu; + for (auto* server = adapter->FirstDnsServerAddress; server; server = server->Next) { + if (Ipv4Of(server->Address)) candidate.dns.push_back(Ipv4Of(server->Address)->sin_addr); + } + found = std::move(candidate); + chosen = true; + chosenHasGateway = gateway != nullptr; + chosenMetric = adapter->Ipv4Metric; + } + return chosen; +#else + ifaddrs* list = nullptr; + if (getifaddrs(&list) != 0) return false; + const std::unique_ptr interfaces(list, freeifaddrs); + const auto routes = HostDefaultRoutes(); + const ifaddrs* chosen = nullptr; + const HostDefaultRoute* chosenRoute = nullptr; + for (auto* item = list; item; item = item->ifa_next) { + if (!item->ifa_addr || item->ifa_addr->sa_family != AF_INET || !(item->ifa_flags & IFF_UP) || (item->ifa_flags & IFF_LOOPBACK)) continue; + if (!UsableIpv4(reinterpret_cast(item->ifa_addr)->sin_addr)) continue; + const HostDefaultRoute* route = nullptr; + for (const auto& entry : routes) { + if (entry.name == item->ifa_name && (!route || entry.metric < route->metric)) route = &entry; + } + if (chosen && !(route && (!chosenRoute || route->metric < chosenRoute->metric))) continue; + chosen = item; + chosenRoute = route; + } + if (!chosen) return false; + found.address = reinterpret_cast(chosen->ifa_addr)->sin_addr; + if (chosen->ifa_netmask) found.netmask = reinterpret_cast(chosen->ifa_netmask)->sin_addr; + if (chosenRoute) found.gateway = chosenRoute->gateway; + found.mtu = HostInterfaceMtu(chosen->ifa_name); + found.dns = HostDnsServers(); + return true; +#endif +} + +static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { + std::memcpy(info->opaque, &value, sizeof(value)); +} + +static void WriteInfoAddress(NetCtlInfo* info, in_addr address) { + const auto* bytes = reinterpret_cast(&address.s_addr); + char text[NET_CTL_IPV4_ADDR_STR_LEN] = {}; + std::snprintf(text, sizeof(text), "%u.%u.%u.%u", static_cast(bytes[0]), static_cast(bytes[1]), static_cast(bytes[2]), static_cast(bytes[3])); + std::memcpy(info->opaque, text, sizeof(text)); +} + extern "C" std::uint64_t NetCtlReadByteCounter_nid_no_patch(const char* path) { std::ifstream file(path); std::uint64_t value = 0; @@ -156,9 +333,25 @@ int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat* stat) { } int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { - (void)code; - (void)info; - return SCE_NET_CTL_ERROR_NOT_CONNECTED; + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_DEVICE || code > NET_CTL_INFO_HTTP_PROXY_PORT) return SCE_NET_CTL_ERROR_INVALID_CODE; + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + switch (code) { + case NET_CTL_INFO_DEVICE: WriteInfoWord(info, NET_CTL_DEVICE_WIRED); break; + case NET_CTL_INFO_ETHER_ADDR: std::memcpy(info->opaque, NET_CTL_ETHER_ADDR, sizeof(NET_CTL_ETHER_ADDR)); break; + case NET_CTL_INFO_MTU: WriteInfoWord(info, host.mtu); break; + case NET_CTL_INFO_LINK: WriteInfoWord(info, NET_CTL_LINK_CONNECTED); break; + case NET_CTL_INFO_IP_CONFIG: WriteInfoWord(info, NET_CTL_IP_DHCP); break; + case NET_CTL_INFO_IP_ADDRESS: WriteInfoAddress(info, host.address); break; + case NET_CTL_INFO_NETMASK: WriteInfoAddress(info, host.netmask); break; + case NET_CTL_INFO_DEFAULT_ROUTE: WriteInfoAddress(info, host.gateway); break; + case NET_CTL_INFO_PRIMARY_DNS: WriteInfoAddress(info, host.dns.size() > 0 ? host.dns[0] : in_addr{}); break; + case NET_CTL_INFO_SECONDARY_DNS: WriteInfoAddress(info, host.dns.size() > 1 ? host.dns[1] : in_addr{}); break; + case NET_CTL_INFO_HTTP_PROXY_CONFIG: WriteInfoWord(info, NET_CTL_HTTP_PROXY_OFF); break; + default: throw std::runtime_error("sceNetCtlGetInfo: info code " + std::to_string(code) + " is not implemented"); + } + return 0; } int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index 2bf31d06e5e..f62edcdc2ac 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -102,9 +102,9 @@ 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 sceRudpEnableInternalIOThread2(uint32_t stackSize, uint32_t priority, KernelCpumask affinityMask) { + (void)affinityMask; + return sceRudpEnableInternalIOThread(stackSize, priority); } int APS5_VABI sceRudpSetEventHandler(GuestRudpEventHandler handler, void* arg) { diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 00529793b72..bb58612ea7b 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -893,9 +893,8 @@ int APS5_VABI sceSaveDataDirNameSearch2(const SaveDataDirNameSearchCond* cond, S return rc; } -APS5_EXPORT("RvID8BVeg+g", sceSaveDataUnknown_RvID8BVeg); -int APS5_VABI sceSaveDataUnknown_RvID8BVeg() { - NotImplemented_nid_no_patch("RvID8BVeg+g"); +int APS5_VABI sceSaveDataPrepareForTransferring() { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceShare.native/Export.cpp b/core/libs/prx/libSceShare.native/Export.cpp index 2d8f2cf3613..fb34206c211 100644 --- a/core/libs/prx/libSceShare.native/Export.cpp +++ b/core/libs/prx/libSceShare.native/Export.cpp @@ -3,86 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -// Capture and sharing are not emulated; policy and parameter calls are accepted. - extern "C" { -int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { - (void)feature_flags; - return 0; -} - -int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { - (void)heap_size; - (void)thread_priority; - (void)affinity_mask; - return 0; -} - -int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return 0; -} - -int APS5_VABI sceShareSetCaptureSource(uint32_t feature_flags, const void* tap_point) { - (void)feature_flags; - (void)tap_point; - return 0; -} - -int APS5_VABI sceShareSetContentParam(const char* content_param) { - (void)content_param; - return 0; -} - -int APS5_VABI sceShareSetScreenshotOverlayImage(const char* file_path, int32_t margin_x, int32_t margin_y, int32_t origin) { - (void)file_path; - (void)margin_x; - (void)margin_y; - (void)origin; - return 0; -} - -int APS5_VABI sceShareTerminate(void) { - return 0; -} - -int APS5_VABI sceShareUnregisterContentEventCallback(void* callback) { - (void)callback; - return 0; -} +APS5_DUMMY_FUN } diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e754..334984d1f71 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -214,13 +214,32 @@ std::uint8_t stickByte(std::int32_t axis) { return static_cast(((axis + 32768) * 255 + 32767) / 65535); } +std::filesystem::path pathFromUtf8(std::string_view text) { + return std::filesystem::path(std::u8string(reinterpret_cast(text.data()), text.size())); +} + +std::string pathToUtf8(const std::filesystem::path& path) { + const auto text = path.u8string(); + return std::string(reinterpret_cast(text.data()), text.size()); +} + +std::filesystem::path configuredPath() { +#ifdef _WIN32 + const wchar_t* configured = _wgetenv(L"ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? std::filesystem::path(configured) : std::filesystem::path{}; +#else + const char* configured = std::getenv("ANYPS5_INPUT_CONFIG"); + return configured != nullptr ? pathFromUtf8(configured) : std::filesystem::path{}; +#endif +} + [[noreturn]] void invalidLine(const std::filesystem::path& path, std::size_t line, const std::string& reason) { - throw std::runtime_error("Pad: invalid input mapping " + path.string() + ":" + std::to_string(line) + ": " + reason); + throw std::runtime_error("Pad: invalid input mapping " + pathToUtf8(path) + ":" + std::to_string(line) + ": " + reason); } std::filesystem::path defaultConfigPath() { std::unique_ptr basePath(SDL_GetBasePath(), SDL_free); - if (basePath) return std::filesystem::path(basePath.get()) / "anyps5-input.ini"; + if (basePath) return pathFromUtf8(basePath.get()) / "anyps5-input.ini"; return "anyps5-input.ini"; } @@ -244,13 +263,13 @@ std::array Pad::StickWithDeadzone(std::int16_t x, std::int16_t Pad::InputConfiguration Pad::LoadInputMapping() { InputConfiguration configuration{{InputMapping.begin(), InputMapping.end()}, {}}; auto& bindings = configuration.bindings; - const char* configuredPath = std::getenv("ANYPS5_INPUT_CONFIG"); - const bool explicitPath = configuredPath != nullptr && configuredPath[0] != '\0'; - const std::filesystem::path path = explicitPath ? configuredPath : defaultConfigPath(); + const auto configured = configuredPath(); + const bool explicitPath = !configured.empty(); + const std::filesystem::path path = explicitPath ? configured : defaultConfigPath(); std::ifstream file(path); if (!file) { if (explicitPath || std::filesystem::exists(path)) { - throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); } return configuration; } @@ -301,7 +320,7 @@ Pad::InputConfiguration Pad::LoadInputMapping() { invalidLine(path, lineNumber, error.what()); } } - if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + path.string() + "'"); + if (file.bad()) throw std::runtime_error("Pad: cannot read input mapping '" + pathToUtf8(path) + "'"); return configuration; } diff --git a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp index c00aef3ff31..42866332902 100644 --- a/core/libs/prx/libSceVideoOut/src/MouseInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/MouseInput.cpp @@ -54,7 +54,7 @@ void MouseInput::HandleEvent(const SDL_Event& event, unsigned windowId) { default: return; } - MousePublishInput_nid_postfix(input); + MousePublishInput_nid_no_patch(input); } void MouseInput::SetPresentation(std::uint32_t sourceWidth, std::uint32_t sourceHeight, std::uint32_t presentedWidth, std::uint32_t presentedHeight) { @@ -78,7 +78,7 @@ std::int32_t MouseInput::scaled(std::int32_t delta, double scale, double& remain } bool MouseInput::CaptureWanted(bool allowed) const { - return allowed && focused && MouseIsOpen_nid_postfix(); + return allowed && focused && MouseIsOpen_nid_no_patch(); } void MouseInput::UpdateCapture(bool allowed) { diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6f..1fe0f4b30b0 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -105,6 +105,48 @@ void mouseLookReject() { require(rejects("MouseDecayMs = 20\nmousedecayms = 30\n"), "a mouse setting given twice is rejected"); } +void setConfigPath(const std::filesystem::path& path) { +#ifdef _WIN32 + require(_wputenv_s(L"ANYPS5_INPUT_CONFIG", path.wstring().c_str()) == 0, "set ANYPS5_INPUT_CONFIG"); +#else + require(::setenv("ANYPS5_INPUT_CONFIG", path.string().c_str(), 1) == 0, "set ANYPS5_INPUT_CONFIG"); +#endif +} + +void nonAsciiPaths() { + const std::u8string name = u8"anyps5-input-\u00fc-\u65e5\u672c-" + std::u8string(root.filename().u8string()); + const auto directory = root / std::filesystem::path(name); + std::filesystem::create_directories(directory); + const auto file = directory / "anyps5-input.ini"; + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = 3\n"; + require(stream.good(), "write the input mapping in a non-ASCII directory"); + } + setConfigPath(file); + require(Pad::LoadInputMapping().settings.mouseSensitivity == 3.0, "ANYPS5_INPUT_CONFIG may name a path with non-ASCII characters"); + { + std::ofstream stream(file, std::ios::binary | std::ios::trunc); + stream << "MouseSensitivity = fast\n"; + } + std::string message; + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + const auto utf8Name = std::string(reinterpret_cast(name.data()), name.size()); + require(message.find(utf8Name) != std::string::npos, "an error names the path in UTF-8"); + setConfigPath(directory / "missing.ini"); + message.clear(); + try { + Pad::LoadInputMapping(); + } catch (const std::runtime_error& error) { + message = error.what(); + } + require(message.find(utf8Name) != std::string::npos, "a missing file names the path in UTF-8"); +} + void deadzoneOffMatchesRawSticks() { for (const std::int16_t axis : {std::int16_t{-32768}, std::int16_t{-20000}, std::int16_t{-1}, std::int16_t{0}, std::int16_t{1}, std::int16_t{1234}, std::int16_t{32767}}) { const auto stick = Pad::StickWithDeadzone(axis, axis, 0); @@ -141,6 +183,7 @@ int main() try { mouseLookDefault(); mouseLookParse(); mouseLookReject(); + nonAsciiPaths(); deadzoneOffMatchesRawSticks(); deadzoneShapesSticks(); std::filesystem::remove_all(root); diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index c9bb506b2c9..02162709a58 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -543,6 +543,11 @@ if(BUILD_TESTING) target_include_directories(guest_exit_order_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_exit_order_tests PRIVATE libc) add_test(NAME guest_exit_order COMMAND guest_exit_order_tests) + add_test_executable(guest_path_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathLifetime.cpp) + target_include_directories(guest_path_lifetime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_lifetime_tests PRIVATE libc) + add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) + set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8df63459a7a..56c8851fbe4 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -24,8 +24,8 @@ class WindowsMappings { enum class Collection { All, Armed, Fresh }; static WindowsMappings& Get() { - static WindowsMappings mappings; - return mappings; + static auto* mappings = new WindowsMappings; + return *mappings; } void* Reserve(void* address, std::size_t bytes) { @@ -334,6 +334,12 @@ class WindowsMappings { } } else if (clear && !collectable(address, end)) return false; auto position = views.lower_bound(address & ~(pageBytes - 1)); + std::uintptr_t cleanStart = 0; + std::uintptr_t cleanEnd = 0; + const auto flushClean = [&] { + if (cleanEnd > cleanStart) rememberClean(cleanStart, cleanEnd); + cleanStart = cleanEnd = 0; + }; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { @@ -346,6 +352,7 @@ class WindowsMappings { const auto base = cursor & ~(pageBytes - 1); const auto found = viewAt(base, position); if (found != views.end() && found->second.resident) { + flushClean(); if (!watchResident) return false; const auto available = capacity - *count; if (available == 0) return true; @@ -371,7 +378,10 @@ class WindowsMappings { } } } - if (view.protection == PAGE_NOACCESS) return false; + if (view.protection == PAGE_NOACCESS) { + flushClean(); + return false; + } const bool pinned = view.page->pins != 0; std::uint8_t settled = 0; auto settledEnd = cursor; @@ -414,24 +424,25 @@ class WindowsMappings { other.armed |= arming; } const auto tracked = static_cast(settled & ~untracked); - auto cleanFrom = cursor; - auto cleanTo = cursor; for (std::size_t sub = 0; sub < subpages; ++sub) { if ((tracked & (1u << sub)) == 0) continue; const auto from = std::max(cursor, base + sub * subpageBytes); const auto to = std::min(settledEnd, base + (sub + 1) * subpageBytes); if (from == base + sub * subpageBytes && to == base + (sub + 1) * subpageBytes) view.seen[sub] = view.page->generation[sub]; - if (from != cleanTo) { - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); - cleanFrom = from; + if (from != cleanEnd) { + flushClean(); + cleanStart = from; } - cleanTo = to; + cleanEnd = to; } - if (cleanFrom != cleanTo) rememberClean(cleanFrom, cleanTo); } - if (full) return true; + if (full) { + flushClean(); + return true; + } cursor = stop; } else { + flushClean(); if (collection == Collection::Fresh) { const auto next = views.lower_bound(cursor); cursor = next == views.end() ? end : std::min(end, next->first); @@ -450,6 +461,7 @@ class WindowsMappings { cursor = stop; } } + flushClean(); return true; } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 402da585ff6..9e5a938d36d 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -42,8 +42,8 @@ struct PathAliases { }; PathAliases& Aliases() { - static PathAliases aliases; - return aliases; + static auto* aliases = new PathAliases; + return *aliases; } std::string TrimSlashes(const char* path) { @@ -120,7 +120,10 @@ struct WorkingDirectory { const std::filesystem::path root = std::filesystem::canonical(std::filesystem::current_path()); std::filesystem::path current = root; }; -WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +WorkingDirectory& Directories() { + static auto* state = new WorkingDirectory; + 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(); diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index c7b286af879..910d6a8d885 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -41,8 +41,8 @@ struct Registry { }; Registry& registry() { - static Registry value; - return value; + static auto* value = new Registry; + return *value; } std::atomic generation{1}; diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 5387ae03903..df6cdd71f08 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -49,8 +49,8 @@ std::uintptr_t alignUp(std::uintptr_t value, std::size_t alignment) { class Arena { public: static Arena& Get() { - static Arena arena; - return arena; + static auto* arena = new Arena; + return *arena; } bool Available() const { diff --git a/core/libs/prx/libc/src/GuestWriteWatch.cpp b/core/libs/prx/libc/src/GuestWriteWatch.cpp index 55f991f047e..959d940c38e 100644 --- a/core/libs/prx/libc/src/GuestWriteWatch.cpp +++ b/core/libs/prx/libc/src/GuestWriteWatch.cpp @@ -47,8 +47,8 @@ constexpr std::uintptr_t PageBytes = 4096; class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { @@ -321,8 +321,8 @@ const bool g_opened = (Watch::Get(), true); class Watch { public: static Watch& Get() { - static Watch watch; - return watch; + static auto* watch = new Watch; + return *watch; } bool Available() const { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 3c678d19a7d..723888384ec 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/OpenFileLimit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/HostStreams.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -85,6 +86,16 @@ endif() configure_windows_unwind(libkernel) if(BUILD_TESTING) + if(NOT WIN32) + add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) + target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_memory_lifetime_module PRIVATE libkernel libc) + add_test_executable(guest_memory_lifetime_tests ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetime.cpp) + target_link_libraries(guest_memory_lifetime_tests PRIVATE ${CMAKE_DL_LIBS}) + add_dependencies(guest_memory_lifetime_tests guest_memory_lifetime_module) + add_test(NAME guest_memory_lifetime COMMAND guest_memory_lifetime_tests $) + set_tests_properties(guest_memory_lifetime PROPERTIES TIMEOUT 25) + endif() add_test_executable(download_data_mount_tests ${LIBS_INCLUDE_DIR}/tests/GuestDownloadDataMount.cpp) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) @@ -1148,6 +1159,16 @@ if(BUILD_TESTING) set_tests_properties(guest_socket_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_host_stream_tests ${LIBS_INCLUDE_DIR}/tests/GuestHostStreams.cpp) + target_include_directories(guest_host_stream_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_host_stream_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_host_stream_tests) + add_test(NAME guest_host_streams COMMAND guest_host_stream_tests) + set_tests_properties(guest_host_streams PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_host_streams PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_socket_event_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketEvents.cpp) target_include_directories(guest_socket_event_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_event_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index c68e05455be..6e62527600e 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -241,19 +241,19 @@ int PhysicalFile() { return file; } -std::vector g_physicalUsed(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +std::vector& PhysicalUsed(); void ZeroReusedPhysical(std::uint64_t start, std::size_t bytes) { const auto first = start / PS5_PAGE_SIZE; const auto count = bytes / PS5_PAGE_SIZE; for (std::size_t page = 0; page < count;) { - if (!g_physicalUsed[first + page]) { - g_physicalUsed[first + page] = true; + if (!PhysicalUsed()[first + page]) { + PhysicalUsed()[first + page] = true; ++page; continue; } std::size_t run = 0; - while (page + run < count && g_physicalUsed[first + page + run]) ++run; + while (page + run < count && PhysicalUsed()[first + page + run]) ++run; const auto offset = (first + page) * PS5_PAGE_SIZE; void* view = ::mmap(nullptr, run * PS5_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, PhysicalFile(), static_cast(offset)); if (view == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map reused direct memory"); @@ -337,9 +337,6 @@ struct PhysicalPage { std::uint64_t offset; }; -std::mutex g_directLock; -std::map g_physPages; - struct DirectMapping { std::uintptr_t end; std::uint64_t phys; @@ -347,13 +344,47 @@ struct DirectMapping { std::shared_ptr backing; }; -std::map g_directMappings; struct PhysicalSpan { std::uint64_t end; std::vector bases; }; -std::map g_physicalSpans; +struct ProtectedRange { + std::uintptr_t end; + int prot; +}; + +struct DirectMemoryState { +#if defined(__linux__) + std::vector physicalUsed = std::vector(DIRECT_MEMORY_SIZE / PS5_PAGE_SIZE); +#endif + std::map physicalSpans; + std::mutex directLock; + std::map physPages; + std::map directMappings; + std::mutex protectionLock; + std::map protections; + std::mutex reservationLock; + std::map reservations; +}; + +DirectMemoryState& MemoryState() { + static auto* state = new DirectMemoryState; + return *state; +} + +#if defined(__linux__) +std::vector& PhysicalUsed() { return MemoryState().physicalUsed; } +#endif + +auto& g_directLock = MemoryState().directLock; +auto& g_physPages = MemoryState().physPages; +auto& g_directMappings = MemoryState().directMappings; +auto& g_physicalSpans = MemoryState().physicalSpans; +auto& g_protectionLock = MemoryState().protectionLock; +auto& g_protections = MemoryState().protections; +auto& g_reservationLock = MemoryState().reservationLock; +auto& g_reservations = MemoryState().reservations; void SplitPhysicalSpan(std::uint64_t at) { auto span = g_physicalSpans.upper_bound(at); @@ -444,7 +475,6 @@ void ErasePhysMappings(std::uint64_t first, std::uint64_t last) { } } - void ValidatePhysicalRange(std::uint64_t phys, std::size_t len) { for (std::size_t offset = 0; offset < len; offset += PS5_PAGE_SIZE) { if (!g_physPages.contains(phys + offset)) { @@ -749,13 +779,6 @@ void* MapPlaced(void* addr, size_t len, int prot, int flags, size_t alignment) { return aligned; } -struct ProtectedRange { - std::uintptr_t end; - int prot; -}; -std::mutex g_protectionLock; -std::map g_protections; - void EraseProtections(std::uintptr_t start, std::uintptr_t end) { auto it = g_protections.lower_bound(start); if (it != g_protections.begin() && std::prev(it)->second.end > start) --it; @@ -768,9 +791,6 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { } } -std::mutex g_reservationLock; -std::map g_reservations; - void EraseReservations(const void* addr, size_t len) { const auto start = reinterpret_cast(addr); const auto end = start + len; diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 8fa22b5fbc8..fc082fb8e95 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -52,8 +52,8 @@ bool PageSpan(uint64_t start, size_t len, uint64_t& lo, uint64_t& hi) { struct PhysicalMemoryPool { static PhysicalMemoryPool& Instance() { - static PhysicalMemoryPool inst; - return inst; + static auto* inst = new PhysicalMemoryPool; + return *inst; } int Alloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut) { diff --git a/core/libs/prx/libkernel/File/include/HostStreams.hpp b/core/libs/prx/libkernel/File/include/HostStreams.hpp new file mode 100644 index 00000000000..d40bcb1600d --- /dev/null +++ b/core/libs/prx/libkernel/File/include/HostStreams.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_HOSTSTREAMS_HPP + +namespace File { + +bool IsHostStream(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp new file mode 100644 index 00000000000..849b88004e6 --- /dev/null +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -0,0 +1,58 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_WINDOWSSYMBOLICLINK_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace File { + +class WindowsSymbolicLink { + struct ReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; + }; + static constexpr ULONG relativeFlag = 1; + +public: + static bool ReadRelative(const std::filesystem::path& native, std::string& target, const char* operation) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), std::string(operation) + ": CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) + throw std::system_error(static_cast(code), std::system_category(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + ReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & relativeFlag) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) + throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + target = std::filesystem::path(name).generic_string(); + return true; + } +}; + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/HostStreams.cpp b/core/libs/prx/libkernel/File/src/HostStreams.cpp new file mode 100644 index 00000000000..0dbc0a53c97 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/HostStreams.cpp @@ -0,0 +1,40 @@ +#include "prx/libkernel/File/include/HostStreams.hpp" +#include +#include +#include +#ifdef _WIN32 +#include +#include +#else +#include +#endif + +namespace { + +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} +#endif + +bool OpenAtStart(int fd) { +#ifdef _WIN32 + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const bool open = _get_osfhandle(fd) != -1; + _set_thread_local_invalid_parameter_handler(previous); + return open; +#else + return ::fcntl(fd, F_GETFD) != -1; +#endif +} + +const std::array g_hostStreams = {OpenAtStart(0), OpenAtStart(1), OpenAtStart(2)}; + +} + +namespace File { + +bool IsHostStream(int fd) { + return fd >= 0 && fd < static_cast(g_hostStreams.size()) && g_hostStreams[static_cast(fd)]; +} + +} diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 5e6e304a73b..7950599933b 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -8,14 +8,17 @@ #ifdef _WIN32 #include "prx/libkernel/File/include/WindowsFileTime.hpp" +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #include #include #include +constexpr unsigned short SymbolicLinkMode = 0120777; struct NativeStat : __stat64 { FILE_BASIC_INFO times{}; BY_HANDLE_FILE_INFORMATION identity{}; bool hasTimes = false; bool hasIdentity = false; + bool isSymbolicLink = false; }; static int ReadHandleInfo(HANDLE handle, NativeStat* st) { if (GetFileType(handle) != FILE_TYPE_DISK) return 0; @@ -45,9 +48,20 @@ static int DoFstat(int fd, NativeStat* st) { } static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); - if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) - NotImplemented_nid_no_patch("lstat of a Windows symbolic link"); - return DoStat(p, st); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); + std::string target; + if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); + const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return File::WindowsFileTime::Failure(GetLastError()); + const int result = ReadHandleInfo(handle, st); + CloseHandle(handle); + if (result != 0) return result; + st->st_mode = SymbolicLinkMode; + st->st_size = static_cast<__int64>(target.size()); + st->isSymbolicLink = true; + return 0; } #else #include @@ -68,7 +82,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { sb->st_mode = static_cast(st.st_mode); sb->st_size = static_cast(st.st_size); #ifdef _WIN32 - if (st.hasIdentity) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); + if (st.hasIdentity && !st.isSymbolicLink) sb->st_size = static_cast((static_cast(st.identity.nFileSizeHigh) << 32u) | st.identity.nFileSizeLow); sb->st_dev = static_cast(st.st_dev); sb->st_ino = static_cast(st.st_ino); sb->st_nlink = static_cast(st.st_nlink); diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49899033360..0f90fb1694d 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -11,6 +11,7 @@ #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "SceTypes.hpp" @@ -233,6 +234,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { + if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); EqueueDescriptorClosed(d); File::ForgetRandomDevice(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaac..55cfd9736ba 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -16,6 +16,7 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libkernel/File/include/HostStreams.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" @@ -565,6 +566,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI close_nid_postfix(int d) { + if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 622a17bd20a..227cce5ba0a 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -18,6 +18,7 @@ struct Entry { using Poller = int (*)(Entry* entries, int count, int timeoutMilliseconds); using Closer = bool (*)(int descriptor, int* error); using OptionSetter = bool (*)(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +using StatusControl = bool (*)(int descriptor, int command, int flags, int* result, int* error); } extern "C" void KernelSetSocketPoller_nid_no_patch(KernelSocketPoll::Poller poller); @@ -25,5 +26,7 @@ extern "C" void KernelSetSocketCloser_nid_no_patch(KernelSocketPoll::Closer clos extern "C" bool KernelCloseSocket_nid_no_patch(int descriptor, int* error); extern "C" void KernelSetSocketOptionSetter_nid_no_patch(KernelSocketPoll::OptionSetter setter); extern "C" bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, const void* value, std::uint32_t length, int* error); +extern "C" void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control); +extern "C" bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error); extern "C" int KernelReserveSocketNumber_nid_no_patch(std::uintptr_t native); extern "C" void KernelReleaseSocketNumber_nid_no_patch(int number); diff --git a/core/libs/prx/libkernel/Socket/src/Socket.cpp b/core/libs/prx/libkernel/Socket/src/Socket.cpp index e01a8b7f34d..8e7907b77c0 100644 --- a/core/libs/prx/libkernel/Socket/src/Socket.cpp +++ b/core/libs/prx/libkernel/Socket/src/Socket.cpp @@ -35,6 +35,7 @@ struct GuestTimeval { std::atomic g_socketPoller{nullptr}; std::atomic g_socketCloser{nullptr}; std::atomic g_socketOptionSetter{nullptr}; +std::atomic g_socketStatusControl{nullptr}; bool DescriptorSet(const void* set, int descriptor) { return set != nullptr && ((static_cast(set)[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; @@ -153,6 +154,15 @@ bool KernelSetSocketOption_nid_no_patch(int descriptor, int level, int option, c return setter != nullptr && setter(descriptor, level, option, value, length, error); } +void KernelSetSocketStatusControl_nid_no_patch(KernelSocketPoll::StatusControl control) { + g_socketStatusControl.store(control); +} + +bool KernelControlSocketStatus_nid_no_patch(int descriptor, int command, int flags, int* result, int* error) { + const auto control = g_socketStatusControl.load(); + return control != nullptr && control(descriptor, command, flags, result, error); +} + int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout) { const auto* limit = static_cast(timeout); if (nfds < 0 || nfds > SelectDescriptorLimit || (limit != nullptr && (limit->seconds < 0 || limit->microseconds < 0 || limit->microseconds >= 1000000))) { diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index e33cc71239d..435d75c4d75 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -54,13 +54,29 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestPeek = 0x2; +constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; +#ifdef _WIN32 +constexpr int GuestSendFlags = GuestNoSignal; +constexpr int GuestReceiveFlags = GuestPeek; +#else +constexpr int GuestSendFlags = GuestNoSignal | GuestDontWait; +constexpr int GuestReceiveFlags = GuestPeek | GuestDontWait; +#endif int NativeSendFlags(int flags) { #ifdef _WIN32 static_cast(flags); return 0; #else - return flags & GuestNoSignal ? MSG_NOSIGNAL : 0; + return (flags & GuestNoSignal ? MSG_NOSIGNAL : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); +#endif +} +int NativeReceiveFlags(int flags) { +#ifdef _WIN32 + return flags & GuestPeek ? MSG_PEEK : 0; +#else + return (flags & GuestPeek ? MSG_PEEK : 0) | (flags & GuestDontWait ? MSG_DONTWAIT : 0); #endif } int NativeError() { @@ -739,6 +755,11 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { va_end(arguments); #endif } + int result = 0; + int netError = 0; + if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { + return netError == 0 ? result : Fail(netError); + } return File::Fcntl(descriptor, command, argument); } const auto socket = Lookup(descriptor); @@ -908,7 +929,7 @@ int APS5_VABI accept_nid_postfix(int descriptor, void* address, std::uint32_t* l std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const auto result = ::send(socket->value, static_cast(buffer), static_cast(length), NativeSendFlags(flags)); @@ -917,13 +938,13 @@ std::int64_t APS5_VABI send_nid_postfix(int descriptor, const void* buffer, std: std::int64_t APS5_VABI recv_nid_postfix(int descriptor, void* buffer, std::uint64_t length, int flags) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); if (!destination.Open()) return Fail(14); const auto result = ::recv(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0); + NativeReceiveFlags(flags)); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) return static_cast(length); #endif @@ -1005,7 +1026,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st int flags, const void* address, std::uint32_t addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~GuestNoSignal) != 0) return Fail(45); + if ((flags & ~GuestSendFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if (!buffer && length) return Fail(14); int result; @@ -1026,7 +1047,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u int flags, void* address, std::uint32_t* addressLength) { const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); if (length > INT_MAX) return Fail(40); if ((!buffer && length) || (address && !addressLength)) return Fail(14); const GuestArena::HostWrite destination(length == 0 ? nullptr : buffer, static_cast(length)); @@ -1034,7 +1055,7 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u sockaddr_storage native{}; socklen_t size = sizeof(native); auto result = ::recvfrom(socket->value, static_cast(buffer), static_cast(length), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); #ifdef _WIN32 if (result < 0 && socket->type == 2 && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(length); #endif @@ -1098,7 +1119,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (total < 0) return -1; const auto socket = Lookup(descriptor); if (!socket) return -1; - if ((flags & ~2) != 0) return Fail(45); + if ((flags & ~GuestReceiveFlags) != 0) return Fail(45); const bool records = socket->type == 2 || socket->type == 5; std::vector buffer; try { @@ -1109,7 +1130,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, sockaddr_storage native{}; socklen_t size = sizeof(native); std::int64_t received = ::recvfrom(socket->value, buffer.data(), static_cast(buffer.size()), - flags & 2 ? MSG_PEEK : 0, reinterpret_cast(&native), &size); + NativeReceiveFlags(flags), reinterpret_cast(&native), &size); if (received < 0) { #ifdef _WIN32 if (!records || WSAGetLastError() != WSAEMSGSIZE) return Fail(NativeError()); diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index dda99c2edfe..178c6459e0d 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -59,12 +59,26 @@ bool IsAlignedAddress(std::uintptr_t address) { return address != 0 && address % alignof(TValue) == 0; } +template +void DeferringDelivery(TFunction function) { +#ifdef _WIN32 + { + TimedWait::DeliveryDeferral deferral; + function(); + } + TimedWait::DeliverDeferred(); +#else + function(); +#endif +} + template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { const auto key = reinterpret_cast(address); std::unique_lock lock(g_waitersLock); if (std::atomic_ref(*address).load() != expected) { - return true; + woken = true; + return std::nullopt; } AddressWaiter waiter; @@ -79,15 +93,24 @@ bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional(std::chrono::steady_clock::now() - waitStart); - const bool woken = waiter.woken; + woken = waiter.woken; if (!woken) { queue.erase(position); if (queue.empty()) { g_waiters.erase(key); } } - lock.unlock(); - KernelTraceWait_nid_postfix("addr", caller, static_cast(waited.count()), !woken); + return waited; +} + +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + bool woken = false; + std::optional waited; + DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + if (waited) { + KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); + } return woken; } @@ -97,7 +120,7 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; } -void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { +void WakeAddressLocked(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { std::lock_guard lock(g_waitersLock); const auto entry = g_waiters.find(key); if (entry == g_waiters.end()) { @@ -115,6 +138,10 @@ void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { } } +void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + DeferringDelivery([&] { WakeAddressLocked(key, count, wakesAtLeastOne); }); +} + std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { if (seconds < 0) { return 0; diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index 8535253896c..c4ae764f64b 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -241,6 +241,18 @@ void Condition::NotifyAll() { } } +DeliveryDeferral::DeliveryDeferral() { + WaitState().bound->fetch_add(1, std::memory_order_seq_cst); +} + +DeliveryDeferral::~DeliveryDeferral() { + WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); +} + +void DeliverDeferred() { + DrainApcs(); +} + namespace { struct alignas(64) ParkBucket { @@ -256,11 +268,6 @@ ParkBucket& ParkBucketFor(const void* key) { return parkBuckets[(reinterpret_cast(key) * 0x9E3779B97F4A7C15ULL) >> 56]; } -struct DeliveryDeferral { - DeliveryDeferral() { WaitState().bound->fetch_add(1, std::memory_order_seq_cst); } - ~DeliveryDeferral() { WaitState().bound->fetch_sub(1, std::memory_order_seq_cst); } -}; - void UnlinkParked(ParkBucket& bucket, Waiter* waiter) { if (waiter->previous) waiter->previous->next = waiter->next; else bucket.head = waiter->next; diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 4f7ccc32b51..446b0c73f31 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -22,6 +22,15 @@ struct Waiter; #ifdef _WIN32 constexpr unsigned PARK_SPINS = 1000; +class DeliveryDeferral { +public: + DeliveryDeferral(); + ~DeliveryDeferral(); + DeliveryDeferral(const DeliveryDeferral&) = delete; + DeliveryDeferral& operator=(const DeliveryDeferral&) = delete; +}; + +void DeliverDeferred(); void SpinPause(); Waiter* BeginPark(const void* key); bool FinishPark(const void* key, Waiter* waiter, const std::uint64_t* deadlineNanos); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 67c48233462..99809d62129 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -30,6 +29,7 @@ #include #include #include +#include "prx/libkernel/File/include/WindowsSymbolicLink.hpp" #ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE #define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 #endif @@ -125,49 +125,12 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast(count); } -#ifdef _WIN32 -struct SymbolicLinkReparseData { - ULONG tag; - USHORT dataLength; - USHORT reserved; - USHORT substituteNameOffset; - USHORT substituteNameLength; - USHORT printNameOffset; - USHORT printNameLength; - ULONG flags; -}; -constexpr ULONG SymbolicLinkRelative = 1; - -bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { - const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, - OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); - if (handle == INVALID_HANDLE_VALUE) - throw std::system_error(static_cast(GetLastError()), std::system_category(), "readlink: CreateFileW"); - std::array data{}; - DWORD size = 0; - const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); - const DWORD code = GetLastError(); - CloseHandle(handle); - if (!read) throw std::system_error(static_cast(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); - SymbolicLinkReparseData header{}; - if (size < sizeof(header)) return false; - std::memcpy(&header, data.data(), sizeof(header)); - if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; - const std::size_t begin = sizeof(header) + header.substituteNameOffset; - if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); - std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); - std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); - target = std::filesystem::path(name).generic_string(); - return true; -} -#endif - std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!ReadRelativeSymbolicLink(native, target)) { + if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); return -1; } diff --git a/core/libs/tests/Fiber.cpp b/core/libs/tests/Fiber.cpp index 43ce3151639..248325f5690 100644 --- a/core/libs/tests/Fiber.cpp +++ b/core/libs/tests/Fiber.cpp @@ -172,6 +172,25 @@ void APS5_VABI HostCallEntry(std::uint64_t, std::uint64_t) { for (;;) sceFiberReturnToThread(0, &received); } +alignas(16) std::array g_chainContext; +FiberStorage g_chainStorage; +bool g_chainEnded = false; + +void APS5_VABI FrameChainEntry(std::uint64_t, std::uint64_t) { + auto* frame = static_cast(__builtin_frame_address(0)); + const auto low = reinterpret_cast(frame); + const auto high = low + (1u << 20); + for (int depth = 0; depth < 100 && frame != nullptr; ++depth) { + const auto address = reinterpret_cast(frame); + Require(address >= low && address + 2 * sizeof(std::uintptr_t) <= high, "frame chain stays on the fiber's stack"); + frame = reinterpret_cast(frame[0]); + Require(frame == nullptr || reinterpret_cast(frame) > address, "frame chain climbs the stack"); + } + g_chainEnded = frame == nullptr; + std::uint64_t received = 0; + for (;;) sceFiberReturnToThread(0, &received); +} + void RunHostCallOnSmallContext() { auto* fiber = reinterpret_cast(&g_hostCallStorage); auto* context = g_hostCallArea.data() + g_hostCallArea.size() - HostCallContextSize; @@ -184,6 +203,14 @@ void RunHostCallOnSmallContext() { } } +void RunFrameChain() { + auto* fiber = reinterpret_cast(&g_chainStorage); + Require(_sceFiberInitializeImpl_nid_postfix(fiber, "chain", FrameChainEntry, 0, g_chainContext.data(), g_chainContext.size(), nullptr, 0) == 0, "initialize chain"); + Require(sceFiberRun_nid_postfix(fiber, 0, nullptr) == 0, "run chain"); + Require(g_chainEnded, "frame chain of a fiber ends with a null frame"); + Require(sceFiberFinalize(fiber) == 0, "finalize chain"); +} + [[gnu::noinline]] std::int32_t RunFirst(std::uint64_t* returned) { g_threadFramePointer = reinterpret_cast(__builtin_frame_address(0)); const std::int32_t result = sceFiberRun_nid_postfix(g_first, 100, returned); @@ -231,4 +258,5 @@ int main() { Require(sceFiberStopContextSizeCheck() == 0, "stop size check"); RunHostCallOnSmallContext(); + RunFrameChain(); } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 854cf0fe330..2c17a9dc9c9 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -651,9 +651,25 @@ int main() { Require(std::filesystem::create_directory(targetDirectory)); const auto directoryLink = root / "directory-link"; const auto danglingLink = root / "dangling-link"; + const auto absoluteLink = root / "absolute-link"; Require(CreateSymbolicLinkW(directoryLink.c_str(), L"target-directory", SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); - Require(CreateSymbolicLinkW(danglingLink.c_str(), missing.c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(danglingLink.c_str(), L"missing.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + Require(CreateSymbolicLinkW(absoluteLink.c_str(), std::filesystem::absolute(present).c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + const auto checkRelativeLink = [](const std::filesystem::path& link, std::int64_t targetLength) { + FileStat own{}; + return lstat_nid_postfix(link.string().c_str(), &own) == 0 && (own.st_mode & 0170000) == 0120000 && + own.st_size == targetLength && own.st_nlink == 1; + }; + Require(checkRelativeLink(fileLink, 11)); + Require(checkRelativeLink(directoryLink, 16)); + Require(checkRelativeLink(danglingLink, 11)); + FileStat followed{}; + Require(lstat_nid_postfix(fileLink.string().c_str(), &linkStatus) == 0); + Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); + Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); + Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); const auto checkUnsupportedLink = [](const std::filesystem::path& link) { FileStat untouched; std::memset(&untouched, 0x5a, sizeof(untouched)); @@ -663,7 +679,7 @@ int main() { try { lstat_nid_postfix(link.string().c_str(), &untouched); } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows symbolic link not implemented") == 0; + rejected = std::strcmp(error.what(), "lstat of a Windows reparse point other than a relative symbolic link not implemented") == 0; } const bool errorUnchanged = *__error_nid_postfix() == sentinelError; const auto* bytes = reinterpret_cast(&untouched); @@ -671,11 +687,9 @@ int main() { return rejected && errorUnchanged && statusUnchanged; }; Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - Require(checkUnsupportedLink(fileLink)); - Require(checkUnsupportedLink(directoryLink)); - Require(checkUnsupportedLink(danglingLink)); - Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && - std::filesystem::remove(danglingLink) && std::filesystem::remove(targetDirectory)); + Require(checkUnsupportedLink(absoluteLink)); + Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && + std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } #endif FileStat identity{}; diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp new file mode 100644 index 00000000000..e31c1dad517 --- /dev/null +++ b/core/libs/tests/GuestHostStreams.cpp @@ -0,0 +1,38 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { +int APS5_VABI dup_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI close_nid_postfix(int); +int APS5_VABI sceKernelClose(int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEbadf = static_cast(0x80020009); + +static bool Open(int fd) { + const int duplicate = dup_nid_postfix(fd); + if (duplicate < 0) return false; + Require(close_nid_postfix(duplicate) == 0); + return true; +} + +int main() { + bool open[3]; + for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); + for (int fd = 0; fd < 3; ++fd) { + if (!open[fd]) continue; + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(Open(fd)); + } + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + for (const int fd : pipe) Require(fd >= 3 || !open[fd]); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + return 0; +} diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 1268bd57d22..0d9e9dcf430 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -1,12 +1,28 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include "Decoder/Jpeg.hpp" +#include #include #include #include +#include #include #include +static_assert(sizeof(JpegDecCreateParam) == 0xC); +static_assert(sizeof(JpegDecParseParam) == 0x10); +static_assert(offsetof(JpegDecParseParam, jpeg_mem_size) == 0x8 && offsetof(JpegDecParseParam, decode_mode) == 0xC && offsetof(JpegDecParseParam, down_scale) == 0xE); +static_assert(sizeof(JpegDecDecodeParam) == 0x30); +static_assert(offsetof(JpegDecDecodeParam, coefficient_mem_addr) == 0x10 && offsetof(JpegDecDecodeParam, jpeg_mem_size) == 0x18); +static_assert(offsetof(JpegDecDecodeParam, image_mem_size) == 0x1C && offsetof(JpegDecDecodeParam, coefficient_mem_size) == 0x20); +static_assert(offsetof(JpegDecDecodeParam, decode_mode) == 0x24 && offsetof(JpegDecDecodeParam, down_scale) == 0x26); +static_assert(offsetof(JpegDecDecodeParam, pixel_format) == 0x28 && offsetof(JpegDecDecodeParam, alpha_value) == 0x2A && offsetof(JpegDecDecodeParam, image_pitch) == 0x2C); +static_assert(sizeof(JpegDecImageInfo) == 0x24); +static_assert(offsetof(JpegDecImageInfo, color_space) == 0x8 && offsetof(JpegDecImageInfo, component_number) == 0xA); +static_assert(offsetof(JpegDecImageInfo, component_id) == 0xC && offsetof(JpegDecImageInfo, sampling_factor) == 0x10); +static_assert(offsetof(JpegDecImageInfo, coefficient_mem_size) == 0x14 && offsetof(JpegDecImageInfo, suitable_csc_attribute) == 0x18); +static_assert(offsetof(JpegDecImageInfo, output_image_width) == 0x1C && offsetof(JpegDecImageInfo, output_image_height) == 0x20); + extern "C" { std::int32_t APS5_VABI sceJpegDecQueryMemorySize(const JpegDecCreateParam*); std::int32_t APS5_VABI sceJpegDecCreate(const JpegDecCreateParam*, void*, std::uint32_t, void**); @@ -17,21 +33,35 @@ std::int32_t APS5_VABI sceJpegDecDecode(void*, const JpegDecDecodeParam*, JpegDe static void Require(bool value) { if (!value) std::abort(); } -static std::vector TestJpeg(std::uint32_t width, std::uint32_t height) { - std::vector rgb(static_cast(width) * height * 3); - for (std::size_t i = 0; i < rgb.size(); ++i) rgb[i] = static_cast(10 + i * 13); - return Decoder::Jpeg::Encode(rgb, width, height, 3, 90); +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +static std::vector TestJpeg(std::uint32_t width, std::uint32_t height, std::uint32_t channels) { + std::vector pixels(static_cast(width) * height * channels); + for (std::size_t i = 0; i < pixels.size(); ++i) pixels[i] = static_cast(10 + i * 13); + return Decoder::Jpeg::Encode(pixels, width, height, channels, 90); } -static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image) { +static JpegDecDecodeParam DecodeParam(const std::vector& jpeg, std::vector& image, std::uint32_t pitch) { JpegDecDecodeParam param{}; param.jpeg_mem_addr = jpeg.data(); param.image_mem_addr = image.data(); + param.coefficient_mem_addr = nullptr; param.jpeg_mem_size = static_cast(jpeg.size()); param.image_mem_size = static_cast(image.size()); + param.coefficient_mem_size = 0; + param.decode_mode = 0; + param.down_scale = 1; param.pixel_format = 0; - param.reserved0 = 0; - param.image_pitch = 0; + param.alpha_value = 0xFF; + param.image_pitch = pitch; return param; } @@ -43,62 +73,95 @@ int main() { constexpr std::int32_t invalidWorkMemory = static_cast(0x80690005); constexpr std::int32_t invalidData = static_cast(0x80690010); - const JpegDecCreateParam param{sizeof(JpegDecCreateParam), 0}; + const std::vector jpeg = TestJpeg(7, 5, 3); + JpegDecImageInfo info{}; + const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); + Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); + const JpegDecParseParam emptyParam{nullptr, 0, 0, 1}; + Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); + const JpegDecParseParam downScaledParse{jpeg.data(), static_cast(jpeg.size()), 0, 2}; + Require(ThrowsRuntimeError([&] { sceJpegDecParseHeader(&downScaledParse, &info); })); + const std::vector grayJpeg = TestJpeg(3, 2, 1); + const JpegDecParseParam grayParse{grayJpeg.data(), static_cast(grayJpeg.size()), 0, 1}; + Require(sceJpegDecParseHeader(&grayParse, &info) == 0); + Require(info.image_width == 3 && info.image_height == 2 && info.component_number == 1); + Require(sceJpegDecParseHeader(&parseParam, &info) == 0); + Require(info.image_width == 7 && info.image_height == 5 && info.component_number == 3); + Require(info.output_image_width == 7 && info.output_image_height == 5); + Require(info.coefficient_mem_size == 0 && info.suitable_csc_attribute == 1); + + const JpegDecCreateParam param{sizeof(JpegDecCreateParam), info.suitable_csc_attribute, info.image_width}; const std::int32_t memorySize = sceJpegDecQueryMemorySize(¶m); Require(memorySize > 0); + const JpegDecCreateParam fullParam{sizeof(JpegDecCreateParam), 2, info.image_width}; + Require(sceJpegDecQueryMemorySize(&fullParam) == memorySize); Require(sceJpegDecQueryMemorySize(nullptr) == invalidParam); - const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 1, 0}; + const JpegDecCreateParam badSize{sizeof(JpegDecCreateParam) - 4, 1, 7}; Require(sceJpegDecQueryMemorySize(&badSize) == invalidSize); - const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), 1}; - Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + for (const std::uint32_t attribute : {0u, 3u}) { + const JpegDecCreateParam badAttribute{sizeof(JpegDecCreateParam), attribute, 7}; + Require(sceJpegDecQueryMemorySize(&badAttribute) == invalidParam); + } std::vector memory(static_cast(memorySize) + 8); void* handle = nullptr; Require(sceJpegDecCreate(¶m, memory.data(), 0, &handle) == invalidWorkMemory); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), nullptr) == invalidAddr); - Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); + Require(sceJpegDecCreate(&fullParam, memory.data(), static_cast(memorySize), &handle) == 0); Require(handle != nullptr); Require(sceJpegDecDelete(nullptr) == invalidHandle); Require(sceJpegDecDelete(handle) == 0); Require(sceJpegDecCreate(¶m, memory.data(), static_cast(memorySize), &handle) == 0); - const std::vector jpeg = TestJpeg(7, 5); - JpegDecImageInfo info{}; - const JpegDecParseParam parseParam{jpeg.data(), static_cast(jpeg.size()), 0}; - Require(sceJpegDecParseHeader(nullptr, &info) == invalidParam); - Require(sceJpegDecParseHeader(&parseParam, nullptr) == invalidAddr); - const JpegDecParseParam emptyParam{nullptr, 0, 0}; - Require(sceJpegDecParseHeader(&emptyParam, &info) == invalidAddr); - Require(sceJpegDecParseHeader(&parseParam, &info) == 0); - Require(info.image_width == 7); - Require(info.image_height == 5); + const std::optional reference = Decoder::Jpeg::Decode(jpeg); + Require(reference && reference->width == 7 && reference->height == 5 && reference->channels == 3); std::vector image(7 * 5 * 4, 0xCC); - JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image); + JpegDecDecodeParam decodeParam = DecodeParam(jpeg, image, 7 * 4); Require(sceJpegDecDecode(nullptr, &decodeParam, &info) == invalidHandle); Require(sceJpegDecDecode(handle, nullptr, &info) == invalidParam); JpegDecDecodeParam badFormat = decodeParam; badFormat.pixel_format = 2; Require(sceJpegDecDecode(handle, &badFormat, &info) == invalidParam); - const std::int32_t packed = sceJpegDecDecode(handle, &decodeParam, &info); - Require(packed == static_cast(7 << 16 | 5)); - Require(info.image_width == 7); - Require(info.image_height == 5); - Require(image[3] == 0xFF); - - std::vector bgra(7 * 5 * 4, 0); - JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra); + JpegDecDecodeParam downScaled = decodeParam; + downScaled.down_scale = 2; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &downScaled, &info); })); + JpegDecDecodeParam otherMode = decodeParam; + otherMode.decode_mode = 1; + Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); + info = {}; + Require(sceJpegDecDecode(handle, &decodeParam, &info) == static_cast(7 << 16 | 5)); + Require(info.image_width == 7 && info.image_height == 5); + Require(info.output_image_width == 7 && info.output_image_height == 5); + for (std::size_t pixel = 0; pixel < 7 * 5; ++pixel) { + for (std::size_t c = 0; c < 3; ++c) Require(image[pixel * 4 + c] == reference->pixels[pixel * 3 + c]); + Require(image[pixel * 4 + 3] == 0xFF); + } + + constexpr std::uint32_t pitch = 7 * 4 + 8; + std::vector bgra(4 * pitch + 7 * 4, 0xEE); + JpegDecDecodeParam bgraParam = DecodeParam(jpeg, bgra, pitch); bgraParam.pixel_format = 1; + bgraParam.alpha_value = 0x77; Require(sceJpegDecDecode(handle, &bgraParam, nullptr) == static_cast(7 << 16 | 5)); - Require(bgra[0] == image[2]); - Require(bgra[2] == image[0]); + for (std::size_t y = 0; y < 5; ++y) { + for (std::size_t x = 0; x < 7; ++x) { + const std::uint8_t* pixel = &bgra[y * pitch + x * 4]; + const std::uint8_t* rgba = &image[(y * 7 + x) * 4]; + Require(pixel[0] == rgba[2] && pixel[1] == rgba[1] && pixel[2] == rgba[0] && pixel[3] == 0x77); + } + } + for (std::size_t y = 0; y < 4; ++y) { + for (std::size_t x = 7 * 4; x < pitch; ++x) Require(bgra[y * pitch + x] == 0xEE); + } const std::vector garbage{0x00, 0x01, 0x02, 0x03}; - JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0}; + const JpegDecParseParam garbageParam{garbage.data(), static_cast(garbage.size()), 0, 1}; Require(sceJpegDecParseHeader(&garbageParam, &info) == invalidData); std::vector garbageOut(64, 0); - JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut); + const JpegDecDecodeParam garbageDecode = DecodeParam(garbage, garbageOut, 4 * 4); Require(sceJpegDecDecode(handle, &garbageDecode, &info) == invalidData); Require(sceJpegDecDelete(handle) == 0); diff --git a/core/libs/tests/GuestMemoryLifetime.cpp b/core/libs/tests/GuestMemoryLifetime.cpp new file mode 100644 index 00000000000..80a0dec2727 --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetime.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +int fixtureDestroyed = 0; +using Exercise = int (*)(); +Exercise exercise = nullptr; + +void ProbeAfterStaticDestruction() { + if (fixtureDestroyed != 1 || exercise == nullptr) { + std::fputs("memory lifetime: fixture finalizer did not run before the exit probe\n", stderr); + std::_Exit(1); + } + if (exercise() != 0) std::_Exit(1); + std::fputs("memory lifetime: allocation, backing, mapping and explicit free survived native exit\n", stderr); +} + +int RunChild(const char* module) { + if (std::atexit(ProbeAfterStaticDestruction) != 0) return 2; + auto* handle = dlopen(module, RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) { + std::fprintf(stderr, "memory lifetime: load failed: %s\n", dlerror()); + std::_Exit(2); + } + const auto prepare = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimePrepare")); + exercise = reinterpret_cast(dlsym(handle, "GuestMemoryLifetimeExercise")); + if (prepare == nullptr || exercise == nullptr || prepare(&fixtureDestroyed) != 0) std::_Exit(2); + std::exit(0); +} + +} + +int main(int argc, char** argv) { + if (argc == 3 && std::strcmp(argv[1], "--child") == 0) return RunChild(argv[2]); + if (argc != 2) return 2; + const auto child = fork(); + if (child == -1) return 2; + if (child == 0) { + execl(argv[0], argv[0], "--child", argv[1], static_cast(nullptr)); + std::_Exit(2); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(20); + int status = 0; + for (;;) { + const auto result = waitpid(child, &status, WNOHANG); + if (result == child) break; + if (result < 0) { + if (errno == EINTR) continue; + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + return 2; + } + if (std::chrono::steady_clock::now() >= deadline) { + kill(child, SIGKILL); + while (waitpid(child, &status, 0) < 0 && errno == EINTR) {} + std::fputs("memory lifetime: exit probe timed out\n", stderr); + return 1; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + std::fprintf(stderr, "memory lifetime: child failed during native exit (status=%d)\n", status); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestMemoryLifetimeModule.cpp b/core/libs/tests/GuestMemoryLifetimeModule.cpp new file mode 100644 index 00000000000..173ac84e32e --- /dev/null +++ b/core/libs/tests/GuestMemoryLifetimeModule.cpp @@ -0,0 +1,126 @@ +#include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#include +#include +#include + +namespace { + +int* fixtureDestroyed = nullptr; +std::int64_t physical = -1; +void* mapping = nullptr; +bool watched = false; + +struct StaticExitMarker { + ~StaticExitMarker() { if (fixtureDestroyed != nullptr) *fixtureDestroyed = 1; } +} marker; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +void CheckPhysical(std::int64_t expected) { + std::int64_t start = -1, end = -1; + int type = -1; + Require(DirectMemoryFind(expected, false, &start, &end, &type), "physical pool lost a live allocation during exit"); + Require(start == expected && end == expected + static_cast(PS5_PAGE_SIZE) && type == 0, "physical pool range changed during exit"); +} + +void CheckMapping(void* address, std::int64_t expected) { + std::uintptr_t start = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(QueryDirectMapping(reinterpret_cast(address), &start, &end, &offset, &type), "direct backing map lost a live allocation during exit"); + Require(start == reinterpret_cast(address) && end == start + PS5_PAGE_SIZE && offset == static_cast(expected) && type == 0, "direct backing range changed during exit"); + GuestAllocations::Mutation mutation; + Require(mutation.Covers(address, PS5_PAGE_SIZE), "guest allocation registry lost a live mapping during exit"); + Require(mutation.Find(address).bytes == PS5_PAGE_SIZE, "guest allocation size changed during exit"); +} + +void CheckUnmapped(void* address) { + std::uintptr_t begin = 0, end = 0; + std::uint64_t offset = 0; + int type = -1; + Require(!QueryDirectMapping(reinterpret_cast(address), &begin, &end, &offset, &type), "explicit unmap did not remove the direct mapping"); + int protection = -1; + Require(!GuestProtection(reinterpret_cast(address), &protection), "explicit unmap did not remove protection metadata"); + GuestAllocations::Mutation mutation; + Require(!mutation.Overlaps(address, PS5_PAGE_SIZE), "explicit unmap did not remove the guest allocation"); +} + +void IgnoreWritten(void*, std::uintptr_t, std::uintptr_t) {} + +} + +extern "C" int GuestMemoryLifetimePrepare(int* destroyed) { + try { + fixtureDestroyed = destroyed; + Require(DirectMemoryAlloc(0, DIRECT_MEMORY_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &physical) == 0, "prepare physical allocation failed"); + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, physical, PS5_PAGE_SIZE) == 0, "prepare direct mapping failed"); + CheckPhysical(physical); + CheckMapping(mapping, physical); + std::memset(mapping, 0x6d, PS5_PAGE_SIZE); + watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); + if (watched) Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "prepare write watch missing"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime prepare: %s\n", error.what()); + return 1; + } +} + +extern "C" int GuestMemoryLifetimeExercise() { + const char* stage = "physical allocation query"; + try { + CheckPhysical(physical); + stage = "backing mapping query"; + CheckMapping(mapping, physical); + Require(static_cast(mapping)[0] == 0x6d, "physical data changed during exit"); + stage = "write watch"; + if (watched) { + Require(GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE), "write watch lost a live mapping during exit"); + Require(GuestWriteWatch::GuestWriteWatchCollect_nid_postfix(reinterpret_cast(mapping), PS5_PAGE_SIZE, IgnoreWritten, nullptr), "write watch collection failed during exit"); + static_cast(mapping)[0] = 0x2a; + } + stage = "mapping protection"; + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 1) == 0, "late read-only protection failed"); + int protection = -1; + Require(GuestProtection(reinterpret_cast(mapping), &protection) && protection == 1, "late protection registry failed"); + Require(DoMprotect(mapping, PS5_PAGE_SIZE, 3) == 0, "late read/write protection failed"); + stage = "explicit unmap and free"; + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late direct unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(physical, PS5_PAGE_SIZE); + std::int64_t start = -1, end = -1; + int type = -1; + Require(!DirectMemoryFind(physical, false, &start, &end, &type), "explicit physical free did not remove the allocation"); + + stage = "physical offset reuse"; + std::int64_t reused = -1; + Require(DirectMemoryAlloc(physical, physical + PS5_PAGE_SIZE, PS5_PAGE_SIZE, PS5_PAGE_SIZE, 0, &reused) == 0 && reused == physical, "late physical allocation failed"); + mapping = nullptr; + Require(DoMapDirect(&mapping, PS5_PAGE_SIZE, 3, 0, reused, PS5_PAGE_SIZE) == 0, "late direct mapping failed"); + CheckPhysical(reused); + CheckMapping(mapping, reused); + const auto* bytes = static_cast(mapping); + for (std::size_t i = 0; i < PS5_PAGE_SIZE; ++i) Require(bytes[i] == 0, "reused physical backing was not zeroed"); + std::memset(mapping, 0x17, PS5_PAGE_SIZE); + Require(DoMunmap(mapping, PS5_PAGE_SIZE) == 0, "late reused mapping unmap failed"); + CheckUnmapped(mapping); + DirectMemoryFree(reused, PS5_PAGE_SIZE); + + stage = "late arena reservation"; + void* reserved = nullptr; + Require(DoReserveVirtual(&reserved, PS5_PAGE_SIZE, 0, PS5_PAGE_SIZE) == 0, "late arena reservation failed"); + std::uintptr_t begin = 0, finish = 0; + Require(GuestReservation(reinterpret_cast(reserved), &begin, &finish) && begin == reinterpret_cast(reserved) && finish == begin + PS5_PAGE_SIZE, "late reservation registry failed"); + Require(DoMunmap(reserved, PS5_PAGE_SIZE) == 0, "late reserved mapping unmap failed"); + Require(!GuestReservation(reinterpret_cast(reserved), &begin, &finish), "explicit unmap did not remove the reservation"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "memory lifetime exit probe (%s): %s\n", stage, error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp new file mode 100644 index 00000000000..f0196de5a72 --- /dev/null +++ b/core/libs/tests/GuestNetCtl.cpp @@ -0,0 +1,136 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#else +#include +#include +#include +#include +#endif +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); +int APS5_VABI sceNetCtlGetState(int*); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kErrInvalidCode = static_cast(0x80412106); +constexpr int kErrInvalidAddr = static_cast(0x80412107); +constexpr int kErrNotConnected = static_cast(0x80412108); +constexpr int kStateIpObtained = 3; +constexpr std::uint8_t kFill = 0xa5; +constexpr std::size_t kAddressLength = 16; + +NetCtlInfo Query(int code) { + NetCtlInfo info; + std::memset(&info, kFill, sizeof(info)); + Require(sceNetCtlGetInfo(code, &info) == 0); + return info; +} + +void RequireUntouchedFrom(const NetCtlInfo& info, std::size_t offset) { + for (std::size_t i = offset; i < sizeof(info.opaque); ++i) Require(info.opaque[i] == kFill); +} + +std::uint32_t QueryWord(int code) { + const auto info = Query(code); + std::uint32_t value = 0; + std::memcpy(&value, info.opaque, sizeof(value)); + RequireUntouchedFrom(info, sizeof(value)); + return value; +} + +in_addr QueryAddress(int code) { + const auto info = Query(code); + char text[kAddressLength]; + std::memcpy(text, info.opaque, sizeof(text)); + Require(std::memchr(text, '\0', sizeof(text)) != nullptr); + RequireUntouchedFrom(info, kAddressLength); + in_addr address{}; + Require(inet_pton(AF_INET, text, &address) == 1); + return address; +} + +bool Binds(in_addr address) { + sockaddr_in local{}; + local.sin_family = AF_INET; + local.sin_addr = address; +#ifdef _WIN32 + const SOCKET descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor != INVALID_SOCKET); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + closesocket(descriptor); +#else + const int descriptor = socket(AF_INET, SOCK_DGRAM, 0); + Require(descriptor >= 0); + const bool bound = bind(descriptor, reinterpret_cast(&local), sizeof(local)) == 0; + close(descriptor); +#endif + return bound; +} + +} + +int main() { +#ifdef _WIN32 + WSADATA data{}; + Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); +#endif + NetCtlInfo info{}; + Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); + Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + + const int result = sceNetCtlGetInfo(14, &info); + if (result == kErrNotConnected) { + std::puts("skipped, the host has no IPv4 address"); + return 77; + } + Require(result == 0); + int state = -1; + Require(sceNetCtlGetState(&state) == 0 && state == kStateIpObtained); + + Require(QueryWord(1) == 0); + const auto ether = Query(2); + const std::uint8_t mac[6] = {0x02, 0x50, 0x53, 0x35, 0x00, 0x01}; + Require(std::memcmp(ether.opaque, mac, sizeof(mac)) == 0); + RequireUntouchedFrom(ether, sizeof(mac)); + const auto mtu = QueryWord(3); + Require(mtu >= 68 && mtu <= 65535); + Require(QueryWord(4) == 1); + Require(QueryWord(11) == 0); + Require(QueryWord(19) == 0); + + const auto address = QueryAddress(14); + const auto host = ntohl(address.s_addr); + Require(host != 0 && (host >> 24) != 127); + Require(Binds(address)); + const auto mask = ntohl(QueryAddress(15).s_addr); + Require(mask != 0 && (~mask & (~mask + 1)) == 0); + QueryAddress(16); + QueryAddress(17); + QueryAddress(18); + + for (const int code : {5, 6, 7, 8, 9, 10, 12, 13, 20, 21}) { + bool threw = false; + try { + sceNetCtlGetInfo(code, &info); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + } +} diff --git a/core/libs/tests/GuestPathLifetime.cpp b/core/libs/tests/GuestPathLifetime.cpp new file mode 100644 index 00000000000..44e8f59072d --- /dev/null +++ b/core/libs/tests/GuestPathLifetime.cpp @@ -0,0 +1,80 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char*, std::size_t); + +namespace { + +int events[2]; +char expectedRoot[4096]; + +void CheckPathsDuringExit() { + bool passed = false; + try { + std::thread worker([&] { + try { + const std::filesystem::path root(expectedRoot); + char cwd[32] = {}; + passed = getcwd_nid_postfix(cwd, sizeof(cwd)) == cwd && + std::strcmp(cwd, "/") == 0 && + ResolvePath_nid_no_patch(".").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/").lexically_relative(root) == "." && + ResolvePath_nid_no_patch("/lifetime_alias") == root; + RemovePathAlias_nid_no_patch("/lifetime_alias"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + passed = passed && ResolvePath_nid_no_patch("/lifetime_alias") == root; + } catch (const std::exception& error) { + std::fprintf(stderr, "path services during exit: %s\n", error.what()); + passed = false; + } catch (...) { passed = false; } + }); + worker.join(); + } catch (...) { passed = false; } + const char result = passed ? 'P' : 'F'; + static_cast(!write(events[1], &result, 1)); +} + +bool PathsSurviveExit() { + if (pipe(events) != 0) return false; + const pid_t child = fork(); + if (child < 0) { + close(events[0]); + close(events[1]); + return false; + } + if (child == 0) { + close(events[0]); + const auto root = std::filesystem::canonical(std::filesystem::current_path()).string(); + if (root.size() >= sizeof(expectedRoot)) std::_Exit(2); + std::memcpy(expectedRoot, root.c_str(), root.size() + 1); + if (std::atexit(CheckPathsDuringExit) != 0) std::_Exit(2); + ResolvePath_nid_no_patch("/"); + AddPathAlias_nid_no_patch("/lifetime_alias", expectedRoot); + LibcExit_nid_no_patch(0); + } + close(events[1]); + char result = 0; + const ssize_t count = read(events[0], &result, 1); + close(events[0]); + int status = 0; + return waitpid(child, &status, 0) == child && WIFEXITED(status) && + WEXITSTATUS(status) == 0 && count == 1 && result == 'P'; +} + +} + +int main() { + if (!PathsSurviveExit()) { + std::fputs("guest path services must remain available during process exit\n", stderr); + return 1; + } + return 0; +} diff --git a/core/libs/tests/GuestRaiseException.cpp b/core/libs/tests/GuestRaiseException.cpp index ee3bde77068..c1b6d0a2987 100644 --- a/core/libs/tests/GuestRaiseException.cpp +++ b/core/libs/tests/GuestRaiseException.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -33,6 +34,7 @@ int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); int APS5_VABI sceKernelSyncOnAddressWait(std::uint32_t* address, std::uint32_t expected, const KernelUseconds* timeout, const char* name); +int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count); } static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); @@ -402,6 +404,87 @@ static void NestedWaitKeepsOuterWake() { Require(sceKernelDeleteSema(sem) == 0); } +#ifdef _WIN32 +static constexpr int AddressRounds = 100; +static std::uint32_t addressWord = 0; +static std::uint32_t otherAddressWord = 0; +static std::atomic holdingEntered{false}; +static std::atomic holdingRelease{false}; +static std::atomic holdingLeft{false}; +static std::atomic holdingThread; + +static void APS5_VABI HoldingHandler(int signum, void*) { + Require(signum == SIGUSR1); + holdingThread.store(std::this_thread::get_id()); + holdingEntered.store(true); + while (!holdingRelease.load()) std::this_thread::yield(); + holdingLeft.store(true); +} + +static void* APS5_VABI AddressWaking(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) Require(sceKernelSyncOnAddressWake(&addressWord, 1) == 0); + return nullptr; +} + +static void* APS5_VABI AddressWaiting(void* arg) { + auto& worker = *static_cast(arg); + worker.id = std::this_thread::get_id(); + worker.started.store(true); + while (!worker.stop.load()) { + KernelUseconds timeout = 20; + const int result = sceKernelSyncOnAddressWait(&addressWord, 0, &timeout, "address waiting"); + Require(result == 0 || result == SCE_KERNEL_ERROR_ETIMEDOUT); + } + return nullptr; +} + +static bool AwaitWithin(const std::atomic& flag, std::chrono::milliseconds limit) { + const auto deadline = std::chrono::steady_clock::now() + limit; + while (!flag.load()) { + if (std::chrono::steady_clock::now() >= deadline) return false; + std::this_thread::yield(); + } + return true; +} + +static void RequireInRound(bool value, const char* name, int round, const char* check) { + if (value) return; + std::fprintf(stderr, "%s round %d: %s\n", name, round, check); + std::fflush(stderr); + std::abort(); +} + +static void HandlerLeavesAddressLock(PthreadEntry entry, const char* name) { + Worker worker; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, entry, &worker, name) == 0); + while (!worker.started.load()) std::this_thread::yield(); + for (int round = 0; round < AddressRounds; ++round) { + holdingEntered.store(false); + holdingLeft.store(false); + holdingRelease.store(false); + RequireInRound(sceKernelRaiseException(thread, SIGUSR1) == 0, name, round, "the raise failed"); + RequireInRound(AwaitWithin(holdingEntered, std::chrono::seconds(5)), name, round, "the handler did not run within 5 s"); + RequireInRound(holdingThread.load() == worker.id, name, round, "the handler ran on another thread"); + std::atomic woke{false}; + std::thread waker([&] { + Require(sceKernelSyncOnAddressWake(&otherAddressWord, 1) == 0); + woke.store(true); + }); + const bool returned = AwaitWithin(woke, std::chrono::milliseconds(500)); + holdingRelease.store(true); + waker.join(); + RequireInRound(returned, name, round, "a wake on another address blocked for 500 ms while the handler ran"); + RequireInRound(AwaitWithin(holdingLeft, std::chrono::seconds(5)), name, round, "the handler did not return within 5 s of its release"); + } + worker.stop.store(true); + Require(scePthreadJoin(thread, nullptr) == 0); +} +#endif + static void ExpectDelivery(int before, std::thread::id thread) { for (int attempt = 0; attempt < 5000 && calls.load() == before; ++attempt) std::this_thread::sleep_for(std::chrono::milliseconds(1)); Require(calls.load() == before + 1); @@ -660,4 +743,11 @@ int main() { NestedWaitKeepsLaterWaiters(); NestedWaitKeepsOuterWake(); Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); + +#ifdef _WIN32 + Require(sceKernelInstallExceptionHandler(SIGUSR1, reinterpret_cast(&HoldingHandler)) == 0); + HandlerLeavesAddressLock(AddressWaking, "address waking"); + HandlerLeavesAddressLock(AddressWaiting, "address waiting"); + Require(sceKernelRemoveExceptionHandler(SIGUSR1) == 0); +#endif } diff --git a/core/libs/tests/GuestRudpPump.cpp b/core/libs/tests/GuestRudpPump.cpp index 1e8ec7ab294..b941e47bfa2 100644 --- a/core/libs/tests/GuestRudpPump.cpp +++ b/core/libs/tests/GuestRudpPump.cpp @@ -13,6 +13,8 @@ int APS5_VABI sceRudpProcessEvents(); int APS5_VABI sceRudpNetFlush(); int APS5_VABI sceRudpGetMaxSegmentSize(std::uint16_t*); int APS5_VABI sceRudpNetReceived(); +int APS5_VABI sceRudpEnableInternalIOThread(std::uint32_t, std::uint32_t); +int APS5_VABI sceRudpEnableInternalIOThread2(std::uint32_t, std::uint32_t, std::uint64_t); } static void Require(bool value, int line) { @@ -32,6 +34,8 @@ int main() { CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); CHECK(segmentSize == 0x5a5a); CHECK(sceRudpGetMaxSegmentSize(nullptr) == notInitialized); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == notInitialized); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == notInitialized); alignas(8) static unsigned char pool[0xC58]; CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); @@ -43,7 +47,14 @@ int main() { std::uint16_t segment[3] = {0, 0, 0x5a5a}; CHECK(sceRudpGetMaxSegmentSize(&segment[1]) == 0); CHECK(segment[0] == 0 && segment[1] == 1410 && segment[2] == 0x5a5a); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == 0); CHECK(sceRudpEnd() == 0); + CHECK(sceRudpEnableInternalIOThread(0x4000, 700) == notInitialized); + + CHECK(sceRudpInit_nid_postfix(pool, sizeof(pool)) == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == 0); + CHECK(sceRudpEnd() == 0); + CHECK(sceRudpEnableInternalIOThread2(0x4000, 700, 0x1) == notInitialized); segmentSize = 0x5a5a; CHECK(sceRudpGetMaxSegmentSize(&segmentSize) == notInitialized); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c117778e49e..d4f635b46a2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -74,6 +74,7 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceNetInetPton(int, const char*, void*); const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); @@ -506,6 +507,26 @@ static void CheckKernelTimeouts() { Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelStatusFlags() { + const int descriptor = sceNetSocket("kernel-status", 2, 1, 6); + Require(descriptor >= 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + Require(fcntl_nid_postfix(descriptor, 4, 2 | 4) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == (2 | 4)); + int enabled = 0; + std::uint32_t length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 1); + Require(fcntl_nid_postfix(descriptor, 4, 2) == 0); + Require(fcntl_nid_postfix(descriptor, 3) == 2); + length = sizeof(enabled); + Require(sceNetGetsockopt(descriptor, 0xffff, 0x1200, &enabled, &length) == 0 && enabled == 0); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 4, 0x100) == -1 && *__error_nid_postfix() == 45); + *__error_nid_postfix() = 0; + Require(fcntl_nid_postfix(descriptor, 1) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(descriptor) == 0); +} + static void CheckAddressText(int family, const char* text) { std::array address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -844,6 +865,7 @@ int main() { Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); CheckSocketError(); + CheckKernelStatusFlags(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 2f712816f2b..65cd55de264 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -206,7 +206,7 @@ int main(int argc, char** argv) { for (const char* path : {"/app0/another.bin", "/data/eboot.bin", "/app0/../eboot.bin", "/app0//eboot.bin"}) { bool rejected = false; try { sceSystemServiceLoadExec(path, nullptr); } - catch (const std::runtime_error&) { rejected = true; } + catch (const std::runtime_error& error) { rejected = std::string(error.what()).find(path) != std::string::npos; } Require(rejected); } #ifdef _WIN32 diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index d9da3cf3495..e9ffebd9a75 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -19,6 +19,7 @@ int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); int APS5_VABI _ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); +std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, 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 recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*); int APS5_VABI close_nid_postfix(int); @@ -98,9 +99,50 @@ static void CheckAddressLengths(int family) { Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); } +static void CheckDontWait() { + const int receiver = socket_nid_postfix(2, 2, 0); + const int sender = socket_nid_postfix(2, 2, 0); + Require(receiver >= 0 && sender >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(receiver, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(receiver, address.data(), &size) == 0); + char buffer[16]{}; + Iovec vector{buffer, sizeof(buffer)}; + Msghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; +#ifdef _WIN32 + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 45); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 45); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 45); + Require(sendto_nid_postfix(sender, "x", 1, 0x80, address.data(), address.size()) == -1 && *__error_nid_postfix() == 45); +#else + const auto start = std::chrono::steady_clock::now(); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 0x80, nullptr, nullptr) == -1 && *__error_nid_postfix() == 35); + Require(recvmsg_nid_postfix(receiver, &message, 0x80) == -1 && *__error_nid_postfix() == 35); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == -1 && *__error_nid_postfix() == 35); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); + const char datagram[] = "nowait"; + Require(sendto_nid_postfix(sender, datagram, sizeof(datagram), 0x80, address.data(), address.size()) == sizeof(datagram)); + Require(recvfrom_nid_postfix(receiver, buffer, sizeof(buffer), 2, nullptr, nullptr) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x82) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + std::memset(buffer, 0, sizeof(buffer)); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(connect_nid_postfix(sender, address.data(), address.size()) == 0); + Require(send_nid_postfix(sender, datagram, sizeof(datagram), 0x80 | 0x20000) == sizeof(datagram)); + std::memset(buffer, 0, sizeof(buffer)); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(datagram) && std::strcmp(buffer, datagram) == 0); + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x80) == -1 && *__error_nid_postfix() == 35); +#endif + Require(recv_nid_postfix(receiver, buffer, sizeof(buffer), 0x1) == -1 && *__error_nid_postfix() == 45); + Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckDontWait(); const int receiver = socket_nid_postfix(2, 2, 0); const int sender = socket_nid_postfix(2, 2, 17); Require(receiver >= 0 && sender >= 0 && receiver != sender); diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 0b575709968..f8fd8039fe6 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -549,6 +549,10 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_magic.py $) add_test(NAME input_preservation COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_preservation.py $) + add_test(NAME output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_output_collisions.py $) + add_test(NAME secondary_output_collisions COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_secondary_output_collisions.py $) add_test(NAME input_file COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_input_file.py $) add_test(NAME null_symbol_relocations COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/io/src/FileWriter.cpp b/core/relinker/io/src/FileWriter.cpp index e80448f5036..49df1334111 100644 --- a/core/relinker/io/src/FileWriter.cpp +++ b/core/relinker/io/src/FileWriter.cpp @@ -1,28 +1,110 @@ #include #include #include +#include #include +#include +#ifndef _WIN32 +#include +#include +#include +#include +#include +#include +#endif namespace Io { -void FileWriter::Write(const std::string& path, const std::vector& data) { - std::ofstream f(NativePath(path), std::ios::binary); +namespace { + +bool writeFile(const std::filesystem::path& file, const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + std::ofstream f(file, mode); if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f.write(reinterpret_cast(data.data()), static_cast(data.size())); + return false; + f.write(data, static_cast(size)); f.close(); if (!f) throw Domain::RelinkerException("Failed to write file: " + path); + return true; } -void FileWriter::Write(const std::string& path, const std::string& content) { - std::ofstream f(NativePath(path)); - if (!f) - throw Domain::RelinkerException("Cannot open output file: " + path); - f << content; - f.close(); - if (!f) +#ifndef _WIN32 +int createTemporary(const std::filesystem::path& target, const mode_t mode, std::filesystem::path& temporary) { + constexpr std::string_view characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + std::random_device device; + for (int attempt = 0; attempt < 100; ++attempt) { + std::string suffix = "."; + for (int index = 0; index < 6; ++index) + suffix += characters[device() % characters.size()]; + temporary = target; + temporary += suffix; + const int descriptor = open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); + if (descriptor >= 0 || errno != EEXIST) + return descriptor; + } + return -1; +} + +bool writeAll(const int descriptor, const char* data, std::size_t size) { + while (size != 0) { + const auto written = write(descriptor, data, size); + if (written < 0 && errno == EINTR) + continue; + if (written <= 0) + return false; + data += written; + size -= static_cast(written); + } + return true; +} + +bool replaceFile(const std::filesystem::path& target, const std::string& path, const char* data, const std::size_t size) { + std::error_code error; + const auto previous = std::filesystem::symlink_status(target, error); + const bool regular = std::filesystem::is_regular_file(previous); + if (!regular && previous.type() != std::filesystem::file_type::not_found) + return false; + const auto mode = regular ? static_cast(previous.permissions() & std::filesystem::perms::all) : static_cast(0666); + std::filesystem::path temporary; + const int descriptor = createTemporary(target, mode, temporary); + if (descriptor < 0) + return false; + if (regular && fchmod(descriptor, mode) != 0) { + close(descriptor); + std::filesystem::remove(temporary, error); + return false; + } + const bool written = writeAll(descriptor, data, size); + if (close(descriptor) != 0 || !written) { + std::filesystem::remove(temporary, error); throw Domain::RelinkerException("Failed to write file: " + path); + } + std::filesystem::rename(temporary, target, error); + if (!error) + return true; + std::filesystem::remove(temporary, error); + return false; +} +#endif + +void writeOutput(const std::string& path, const std::ios::openmode mode, const char* data, const std::size_t size) { + const auto target = NativePath(path); +#ifndef _WIN32 + if (replaceFile(target, path, data, size)) + return; +#endif + if (!writeFile(target, path, mode, data, size)) + throw Domain::RelinkerException("Cannot open output file: " + path); +} + +} + +void FileWriter::Write(const std::string& path, const std::vector& data) { + writeOutput(path, std::ios::binary, reinterpret_cast(data.data()), data.size()); +} + +void FileWriter::Write(const std::string& path, const std::string& content) { + writeOutput(path, std::ios::out, content.data(), content.size()); } } diff --git a/core/relinker/io/tests/FileWriterTests.cpp b/core/relinker/io/tests/FileWriterTests.cpp index 8edc710d559..900518b16f3 100644 --- a/core/relinker/io/tests/FileWriterTests.cpp +++ b/core/relinker/io/tests/FileWriterTests.cpp @@ -1,9 +1,19 @@ #include #include +#include +#include +#include #include +#include #include #include +#include #include +#include +#include +#include +#include +#include namespace { @@ -21,6 +31,141 @@ void requireWriteFailure(const TValue& data) { throw std::runtime_error("Write to /dev/full reported success"); } +std::string readFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +std::vector siblings(const std::filesystem::path& output) { + std::vector names; + for (const auto& entry : std::filesystem::directory_iterator(output.parent_path())) { + const auto name = entry.path().filename().string(); + if (name.starts_with(output.filename().string() + ".")) + names.push_back(name); + } + return names; +} + +template +void requireRunningReplacement(const std::filesystem::path& directory, const TValue& data, const bool leftover) { + const auto output = directory / (leftover ? "leftover" : "running"); + if (leftover) + std::ofstream(directory / "leftover.tmp", std::ios::binary) << "killed"; + std::filesystem::copy_file("/bin/sleep", output); + const auto permissions = std::filesystem::perms::owner_all | std::filesystem::perms::group_all; + std::filesystem::permissions(output, permissions); + const auto previous = readFile(output); + const pid_t child = fork(); + if (child < 0) + throw std::runtime_error("Cannot start the previous executable"); + if (child == 0) { + execl(output.c_str(), "sleep", "30", nullptr); + _exit(127); + } + std::error_code error; + bool started = false; + for (int attempt = 0; attempt < 2000 && !started; ++attempt) { + started = std::filesystem::read_symlink("/proc/" + std::to_string(child) + "/exe", error) == output; + if (!started) + usleep(1000); + } + const int descriptor = open(output.c_str(), O_RDONLY); + std::string failure; + if (started) { + const auto mask = umask(077); + try { + Io::FileWriter().Write(output.string(), data); + } catch (const std::exception& e) { + failure = e.what(); + } + umask(mask); + } + std::string kept(previous.size(), '\0'); + const bool keptPrevious = pread(descriptor, kept.data(), kept.size(), 0) == static_cast(kept.size()) && kept == previous; + close(descriptor); + const bool running = waitpid(child, nullptr, WNOHANG) == 0; + if (running) { + kill(child, SIGKILL); + waitpid(child, nullptr, 0); + } + if (!started) + throw std::runtime_error("The previous executable did not start"); + if (!failure.empty()) + throw std::runtime_error("Replacing a running executable failed: " + failure); + if (readFile(output) != std::string(data.begin(), data.end())) + throw std::runtime_error("Running executable was not replaced"); + if ((std::filesystem::status(output).permissions() & std::filesystem::perms::all) != permissions) + throw std::runtime_error("Replacement did not keep the previous permissions"); + if (!keptPrevious) + throw std::runtime_error("An open descriptor no longer reads the previous file"); + if (!running) + throw std::runtime_error("Running executable stopped"); + if (siblings(output) != (leftover ? std::vector{"leftover.tmp"} : std::vector{})) + throw std::runtime_error("Temporary file left behind"); + if (leftover && readFile(directory / "leftover.tmp") != "killed") + throw std::runtime_error("A file left by an earlier run was changed"); +} + +template +void requireFailedReplacement(const std::filesystem::path& directory, const TValue& data) { + const auto output = directory / "limited"; + std::ofstream(output, std::ios::binary) << "previous"; + rlimit limit{}; + getrlimit(RLIMIT_FSIZE, &limit); + rlimit lowered = limit; + lowered.rlim_cur = 1; + const auto handler = std::signal(SIGXFSZ, SIG_IGN); + setrlimit(RLIMIT_FSIZE, &lowered); + std::string failure; + try { + Io::FileWriter().Write(output.string(), data); + } catch (const Domain::RelinkerException& e) { + failure = e.what(); + } + setrlimit(RLIMIT_FSIZE, &limit); + std::signal(SIGXFSZ, handler); + if (failure != "Failed to write file: " + output.string()) + throw std::runtime_error("Write past the file size limit did not fail: " + failure); + if (readFile(output) != "previous" || !siblings(output).empty()) + throw std::runtime_error("Failed write did not keep the previous file"); +} + +template +void requireInPlaceWrites(const std::filesystem::path& directory, const TValue& data) { + const std::string expected(data.begin(), data.end()); + const auto target = directory / "target"; + const auto link = directory / "link"; + std::ofstream(target, std::ios::binary) << "previous"; + std::filesystem::create_symlink(target, link); + Io::FileWriter().Write(link.string(), data); + if (!std::filesystem::is_symlink(link) || readFile(target) != expected) + throw std::runtime_error("Symbolic link output was not written through"); + const auto reserved = directory / "reserved"; + std::ofstream(reserved, std::ios::binary) << "previous"; + std::ofstream(directory / "reserved.tmp", std::ios::binary) << "unrelated"; + Io::FileWriter().Write(reserved.string(), data); + if (readFile(reserved) != expected || readFile(directory / "reserved.tmp") != "unrelated") + throw std::runtime_error("Existing temporary file name was not left alone"); +} + +template +void requireReplacement(const TValue& data) { + const auto directory = std::filesystem::canonical(std::filesystem::temp_directory_path()) / ("anyps5-file-writer-" + std::to_string(getpid())); + std::error_code error; + std::filesystem::remove_all(directory, error); + std::filesystem::create_directory(directory); + try { + requireRunningReplacement(directory, data, false); + requireRunningReplacement(directory, data, true); + requireFailedReplacement(directory, data); + requireInPlaceWrites(directory, data); + } catch (...) { + std::filesystem::remove_all(directory, error); + throw; + } + std::filesystem::remove_all(directory, error); +} + } int main(int argc, char* argv[]) { @@ -28,12 +173,17 @@ int main(int argc, char* argv[]) { if (argc != 2) throw std::runtime_error("Expected binary or text"); const std::string mode = argv[1]; for (const std::size_t size : {std::size_t{3}, std::size_t{65536}}) { - if (mode == "binary") - requireWriteFailure(std::vector(size, 0x41)); - else if (mode == "text") - requireWriteFailure(std::string(size, 'A')); - else + if (mode == "binary") { + const std::vector data(size, 0x41); + requireWriteFailure(data); + requireReplacement(data); + } else if (mode == "text") { + const std::string data(size, 'A'); + requireWriteFailure(data); + requireReplacement(data); + } else { throw std::runtime_error("Unknown test mode: " + mode); + } } } catch (const std::exception& e) { std::cerr << e.what() << '\n'; diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 557840d16a1..07f448aefce 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -69,6 +70,100 @@ ConvertedExecutable Convert(const Cli::Args& args, const std::string& inputPath, return {inputPath, absPath, std::move(converted.Analysis), std::move(converted.Modules), std::move(converted.Executable), std::move(converted.WindowsDiagnostics)}; } +#ifdef _WIN32 + +class OutputIdentityDirectory { +public: + OutputIdentityDirectory(const OutputIdentityDirectory&) = delete; + OutputIdentityDirectory& operator=(const OutputIdentityDirectory&) = delete; + + explicit OutputIdentityDirectory(const std::filesystem::path& parent) { + std::random_device random; + for (unsigned attempt = 0; attempt < 64; ++attempt) { + const auto candidate = parent / (".anyps5-output-identity-" + std::to_string(random()) + "-" + std::to_string(random())); + std::error_code error; + if (std::filesystem::create_directory(candidate, error)) { + directory = candidate; + return; + } + if (error && error != std::errc::file_exists) + throw std::filesystem::filesystem_error("Cannot create output identity directory", candidate, error); + } + throw Domain::RelinkerException("Cannot reserve an output identity directory in " + Io::Utf8Path(parent)); + } + + ~OutputIdentityDirectory() { + if (directory.empty()) return; + try { + Remove(); + } catch (const std::exception& error) { + std::cerr << "FAIL: Output identity cleanup: " << error.what() << '\n'; + } + } + + std::filesystem::path GetPath() const { return directory / "file"; } + const std::filesystem::path& GetDirectory() const { return directory; } + + void Remove() { + std::filesystem::remove(GetPath()); + std::filesystem::remove(directory); + directory.clear(); + } + +private: + std::filesystem::path directory; +}; + +#endif + +bool SameOutputFile(const std::filesystem::path& first, const std::filesystem::path& second) { + if (std::filesystem::equivalent(first, second)) return true; +#ifdef _WIN32 + if (std::filesystem::status(first).permissions() != std::filesystem::status(second).permissions() && + std::filesystem::hard_link_count(first) > 1 && std::filesystem::hard_link_count(second) > 1) { + for (const auto& path : {first, second}) { + if ((std::filesystem::status(path).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none) + throw Domain::RelinkerException("Cannot compare read-only output identities: " + Io::Utf8Path(path)); + } + OutputIdentityDirectory firstDirectory(first.parent_path()); + OutputIdentityDirectory secondDirectory(second.parent_path()); + std::filesystem::create_hard_link(first, firstDirectory.GetPath()); + std::filesystem::create_hard_link(second, secondDirectory.GetPath()); + const bool same = std::filesystem::equivalent(firstDirectory.GetPath(), secondDirectory.GetPath()); + secondDirectory.Remove(); + firstDirectory.Remove(); + return same; + } +#endif + return false; +} + +bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { + const auto firstPath = std::filesystem::weakly_canonical(first); + const auto secondPath = std::filesystem::weakly_canonical(second); + if (firstPath == secondPath) return true; + if (std::filesystem::exists(first) && std::filesystem::exists(second)) return SameOutputFile(first, second); +#ifdef _WIN32 + auto firstName = Io::Utf8Path(firstPath.filename()); + auto secondName = Io::Utf8Path(secondPath.filename()); + for (auto* name : {&firstName, &secondName}) + for (auto& character : *name) + if (character >= 'A' && character <= 'Z') character = static_cast(character + ('a' - 'A')); + if (firstName == secondName && std::filesystem::exists(firstPath.parent_path()) && + std::filesystem::exists(secondPath.parent_path()) && std::filesystem::equivalent(firstPath.parent_path(), secondPath.parent_path())) { + OutputIdentityDirectory probe(firstPath.parent_path()); + auto alternateName = Io::Utf8Path(probe.GetDirectory().filename()); + for (auto& character : alternateName) + if (character >= 'a' && character <= 'z') character = static_cast(character - ('a' - 'A')); + const auto alternate = firstPath.parent_path() / Io::NativePath(alternateName); + const bool same = std::filesystem::exists(alternate) && std::filesystem::equivalent(probe.GetDirectory(), alternate); + probe.Remove(); + return same; + } +#endif + return false; +} + int Run(const int argc, char* argv[]) { Cli::Args args; try { @@ -184,6 +279,24 @@ int Run(const int argc, char* argv[]) { checkOutputInputs(Io::NativePath(absPath), "Executable"); for (const auto& artifact : guestArtifacts) checkOutputInputs(artifact.Path, "Guest module"); + std::vector outputPaths; + for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); + for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); + outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + if (desktopEntry) { + outputPaths.push_back(desktopEntry->IconPath); + outputPaths.push_back(desktopEntry->EntryPath); + } + if (args.windowsDiagnostics) outputPaths.push_back(std::filesystem::absolute("windows-diagnostics-imports.txt")); + for (std::size_t index = 0; index < outputPaths.size(); ++index) { + for (std::size_t previous = 0; previous < index; ++previous) { + const auto& first = outputPaths[index]; + const auto& second = outputPaths[previous]; + if (SameOutputDestination(first, second)) + throw Domain::RelinkerException("Output paths collide: " + Io::Utf8Path(first) + " and " + Io::Utf8Path(second)); + } + } + if (args.windowsDiagnostics) fileWriter.Write("windows-diagnostics-imports.txt", executables.front().Diagnostics); const auto outputCount = guestArtifacts.size() + executables.size(); progress.Report({Domain::ProgressStage::Write, absPath, 0, outputCount}); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py new file mode 100644 index 00000000000..2026412310a --- /dev/null +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -0,0 +1,153 @@ +import itertools +import json +import os +from pathlib import Path +import stat +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import PLAIN_SITE, guest_fixture, main_fixture +from test_linux_desktop_entry_output import png + + +def prepare(case, windows): + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + modules = case / "sce_module" + modules.mkdir() + module = modules / "sample.prx" + module.write_bytes(guest_fixture(PLAIN_SITE)) + output = case / ("output.exe" if windows else "output.elf") + paths = { + "executable": output, + "module": case / "app0/sce_module/sample.prx.guest.prx", + "registry": case / "output.registry.json", + "module-registry": case / "output.sample.prx.guest.prx.registry.json", + } + if windows: + paths["diagnostics"] = case / "windows-diagnostics-imports.txt" + else: + (case / "sce_sys").mkdir() + (case / "sce_sys/icon0.png").write_bytes(png(1, 1)) + paths["icon"] = Path(f"{output}.png") + paths["desktop"] = Path(f"{output}.desktop") + for path in paths.values(): + path.parent.mkdir(parents=True, exist_ok=True) + options = ["--windows", "--windows-diagnostics"] if windows else [] + command = [*options, "--registry", str(source), str(output)] + return command, paths, {source: source.read_bytes(), module: module.read_bytes()} + + +def case_sensitive(directory): + probe = directory / "CaseProbe" + probe.write_bytes(b"probe") + sensitive = not (directory / "caseprobe").exists() + probe.unlink() + return sensitive + + +def main(): + relinker = Path(sys.argv[1]).resolve() + checked = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: + work = Path(directory) + for windows in (False, True): + command, paths, inputs = prepare(work / f"{windows}-control", windows) + links = paths["executable"].parent / "separate-links" + links.mkdir() + for name, path in paths.items(): + path.write_bytes(b"previous output") + os.link(path, links / name) + result = subprocess.run([str(relinker), *command], cwd=paths["executable"].parent, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for name in ("executable", "module"): + assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + for name in ("registry", "module-registry"): + json.loads(paths[name].read_text()) + for path, original in inputs.items(): + assert path.read_bytes() == original + assert not list(paths["executable"].parent.rglob(".anyps5-output-identity-*")) + for first, second in itertools.combinations(paths, 2): + case = work / f"{windows}-{first}-{second}" + command, outputs, inputs = prepare(case, windows) + previous = b"previous output" + outputs[first].write_bytes(previous) + os.link(outputs[first], outputs[second]) + for name, path in outputs.items(): + if name not in (first, second): + path.write_bytes(previous) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, first, second, result.returncode, result.stdout, result.stderr) + for path in outputs.values(): + assert path.read_bytes() == previous, path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + checked += 1 + names = ["windows-diagnostics-imports.txt"] + if os.name == "nt" and not case_sensitive(work): + names.append("WINDOWS-DIAGNOSTICS-IMPORTS.TXT") + for index, name in enumerate(names): + for existing in (False, True): + case = work / f"diagnostics-executable-{index}-{existing}" + case.mkdir() + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / name + if existing: + output.write_bytes(b"previous output") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, (result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" if existing else not output.exists() + assert source.read_bytes() == main_fixture() + assert not list(case.rglob(".anyps5-output-identity-*")) + for existing in (False, True): + case = work / f"case-sensitive-{existing}" + case.mkdir() + if not case_sensitive(case): + print("Distinct case-variant outputs skipped: case-sensitive directory unavailable") + break + source = case / "input.elf" + source.write_bytes(main_fixture()) + output = case / "WINDOWS-DIAGNOSTICS-IMPORTS.TXT" + diagnostics = case / "windows-diagnostics-imports.txt" + if existing: + output.write_bytes(b"previous executable") + diagnostics.write_bytes(b"previous diagnostics") + result = subprocess.run([str(relinker), "--windows", "--windows-diagnostics", "--skip-sce-module", + str(source), str(output)], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + assert output.read_bytes().startswith(b"MZ") and diagnostics.exists() + assert not os.path.samefile(output, diagnostics) + assert not list(case.rglob(".anyps5-output-identity-*")) + if os.name == "nt": + case = work / "readonly-identity" + command, paths, inputs = prepare(case, True) + output = paths["executable"] + output.write_bytes(b"previous output") + os.link(output, paths["module"]) + output.chmod(stat.S_IREAD) + try: + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Cannot compare read-only output identities" in result.stderr, ( + result.stdout, result.stderr) + assert output.read_bytes() == b"previous output" + assert paths["module"].read_bytes() == b"previous output" + assert not output.stat().st_mode & stat.S_IWRITE + assert not list(case.rglob(".anyps5-output-identity-*")) + finally: + output.chmod(stat.S_IWRITE) + print(f"Output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_secondary_output_collisions.py b/core/relinker/relinker/tests/test_secondary_output_collisions.py new file mode 100644 index 00000000000..a0db558788b --- /dev/null +++ b/core/relinker/relinker/tests/test_secondary_output_collisions.py @@ -0,0 +1,101 @@ +import itertools +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile + +from test_guest_intel_trampolines import main_fixture +from test_output_collisions import prepare + + +def prepare_secondary(case, windows): + command, paths, inputs = prepare(case, windows) + options = [] + for name in ("secondary-one", "secondary-two"): + source = case / f"{name}.bin" + source.write_bytes(main_fixture()) + inputs[source] = source.read_bytes() + options.extend(["--executable", source.name]) + paths[name] = case / f"{source.name}{paths['executable'].suffix}" + paths[f"{name}-registry"] = case / f"{source.name}.registry.json" + if not windows: + icon = case / "sce_sys/icon0.png" + inputs[icon] = icon.read_bytes() + return [*command[:-2], *options, *command[-2:]], paths, inputs + + +def main(): + relinker = Path(sys.argv[1]).resolve() + secondary = {"secondary-one", "secondary-two"} + checked = 0 + with tempfile.TemporaryDirectory(prefix="anyps5-secondary-collisions-") as directory: + work = Path(directory) + for windows in (False, True): + case = work / f"{windows}-control" + command, paths, inputs = prepare_secondary(case, windows) + links = case / "separate-links" + links.mkdir() + for name, path in paths.items(): + path.write_bytes(b"previous output") + os.link(path, links / name) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for name in ("executable", "module", *sorted(secondary)): + assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), paths[name] + for name, path in paths.items(): + if "registry" in name: + json.loads(path.read_text()) + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + + for first, second in itertools.combinations(paths, 2): + if not secondary.intersection((first, second)): + continue + case = work / f"{windows}-{first}-{second}" + command, outputs, inputs = prepare_secondary(case, windows) + previous = b"previous output" + outputs[first].write_bytes(previous) + os.link(outputs[first], outputs[second]) + for name, path in outputs.items(): + if name not in (first, second): + path.write_bytes(previous) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, first, second, result.returncode, result.stdout, result.stderr) + for path in outputs.values(): + assert path.read_bytes() == previous, path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + checked += 1 + + for existing in (False, True): + case = work / f"diagnostics-secondary-{existing}" + command, paths, inputs = prepare(case, True) + source = case / "windows-diagnostics-imports" + source.write_bytes(main_fixture()) + inputs[source] = source.read_bytes() + paths["executable"] = case / "output.txt" + paths["secondary-registry"] = case / "windows-diagnostics-imports.registry.json" + if existing: + for path in paths.values(): + path.write_bytes(b"previous output") + result = subprocess.run([str(relinker), *command[:-2], "--executable", source.name, + command[-2], str(paths["executable"])], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, (result.stdout, result.stderr) + for path in paths.values(): + assert path.read_bytes() == b"previous output" if existing else not path.exists(), path + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert not list(case.rglob(".anyps5-output-identity-*")) + print(f"Secondary output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + + +if __name__ == "__main__": + main() diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 3461dd0d214..3068aca764d 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -167,7 +167,7 @@ function(add_shader_recompiler target runtimeTarget) target_compile_features(${target} PUBLIC cxx_std_20) set(shaderCacheGeneratedDir "${CMAKE_CURRENT_BINARY_DIR}/${target}_generated") - set(shaderCacheVersionHeader "${shaderCacheGeneratedDir}/ShaderCacheVersion.hpp") + set(shaderCacheVersionSource "${shaderCacheGeneratedDir}/ShaderCacheVersion.cpp") file(GLOB_RECURSE shaderCacheRecompilerSources CONFIGURE_DEPENDS "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.cpp" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/*.hpp" @@ -282,21 +282,20 @@ function(add_shader_recompiler target runtimeTarget) set(shaderCacheListFile "${shaderCacheGeneratedDir}/sources.txt") file(CONFIGURE OUTPUT "${shaderCacheListFile}" CONTENT "${shaderCacheManifest}" @ONLY) add_custom_command( - OUTPUT "${shaderCacheVersionHeader}" + OUTPUT "${shaderCacheVersionSource}" COMMAND ${CMAKE_COMMAND} "-DROOT=${PROJECT_SOURCE_DIR}" "-DLIST_FILE=${shaderCacheListFile}" "-DRECOMPILER_DIR=${CMAKE_CURRENT_FUNCTION_LIST_DIR}" "-DGIT=${GIT_EXECUTABLE}" "-DEXTRA=${shaderCacheExtra}" - "-DOUTPUT=${shaderCacheVersionHeader}" + "-DOUTPUT=${shaderCacheVersionSource}" -P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" DEPENDS ${shaderCacheRecompilerSources} ${shaderCacheGuardSources} ${shaderCacheSubmoduleStamps} "${shaderCacheListFile}" "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/ShaderCacheVersion.cmake" COMMENT "Hashing the shader recompiler sources for the shader disk cache" VERBATIM ) - target_sources(${target} PRIVATE "${shaderCacheVersionHeader}") - target_include_directories(${target} PRIVATE "${shaderCacheGeneratedDir}") + target_sources(${target} PRIVATE "${shaderCacheVersionSource}") set_target_properties(${target} PROPERTIES POSITION_INDEPENDENT_CODE ON diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 33e899a8188..e0264eeae7a 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -109,7 +109,7 @@ class RecompileCacheKey { if (request.shader.stage == ShaderStage::Local) return; append(key, tessellation->patchesPerGroup); append(key, tessellation->primitiveGroupSize); - append(key, tessellation->breakAtInstance); + append(key, TessellationInstancePatches(request)); } template diff --git a/core/shader/recompiler/ImageTableAbi.hpp b/core/shader/recompiler/ImageTableAbi.hpp index 8a07ef7cdae..9b434141d8a 100644 --- a/core/shader/recompiler/ImageTableAbi.hpp +++ b/core/shader/recompiler/ImageTableAbi.hpp @@ -75,6 +75,7 @@ enum class PoisonReason : std::uint32_t { HeapWritten, DriverRejected, UnsupportedOperation, + OffsetCarry, Count }; @@ -101,6 +102,7 @@ constexpr std::string_view PoisonReasonName(PoisonReason reason) { case PoisonReason::HeapWritten: return "table written by its dispatch"; case PoisonReason::DriverRejected: return "rejected by the driver"; case PoisonReason::UnsupportedOperation: return "unsupported operation"; + case PoisonReason::OffsetCarry: return "record offset carries past 32 bits"; case PoisonReason::Count: break; } return "unknown"; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp index f361365f4b5..c402bbee7ea 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp @@ -39,9 +39,14 @@ struct TableColumn { bool address = false; std::vector keyDomains; bool unkeyed = false; + bool indirect = false; + std::uint32_t indirectOffset = 0; + std::uint32_t indirectImmediate = 0; + std::uint32_t foldedAddend = 0; + std::uint32_t foldedCarry = 0; bool operator==(const TableColumn& other) const { - return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address; + return heapSource == other.heapSource && stride == other.stride && addend == other.addend && offset == other.offset && dwordCount == other.dwordCount && sampler == other.sampler && address == other.address && indirect == other.indirect && indirectOffset == other.indirectOffset && indirectImmediate == other.indirectImmediate && foldedAddend == other.foldedAddend && foldedCarry == other.foldedCarry; } }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp index 96c7d515334..84ec2412d08 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/StageIO.hpp @@ -14,7 +14,9 @@ enum class TessellationAttribute { EvaluationInput, PatchOutput, Factor, - PatchInput + PatchInput, + InstanceOutput, + InstanceInput }; enum class TessellationBaseKind { @@ -46,6 +48,7 @@ enum class StageInputKind { HelperInvocation, RasterPrimitive, LayerExported, + BaseInstance, }; enum class StageOutputKind { diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 77e52d36de5..d23faa3437f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -115,6 +115,8 @@ struct ShaderTessellationInputInfo { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; struct ShaderVertexInputInfo { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 2a437bc6252..96fb7398e08 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -91,7 +91,10 @@ DescriptorRole RoleFor(DescriptorBindingKind kind) { DescriptorImageShape ImageShapeForResource(const ImageResource& image) { const RdnaImageDimensionInfo& info = RdnaImageDimensionInfoFor(image.dimension); if (info.multisampled != 0u) { - fail("DescriptorBindingBuilder::Populate multisampled image resources have no descriptor image shape"); + if (info.arrayed != 0u) { + fail("DescriptorBindingBuilder::Populate multisampled array image resources have no descriptor image shape"); + } + return DescriptorImageShape::Image2DMultisample; } if (info.spirvDimension == spv::Dim1D) { return info.arrayed != 0u ? DescriptorImageShape::Image1DArray : DescriptorImageShape::Image1D; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index e6387263976..7353276efbb 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -345,7 +345,7 @@ void reportTables() { static_cast(counters.narrowed.exchange(0, std::memory_order_relaxed)), static_cast(counters.narrowSkipped.exchange(0, std::memory_order_relaxed)), static_cast(counters.nanoseconds.exchange(0, std::memory_order_relaxed)) / 1e6); } -enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped }; +enum class RecordState : std::uint8_t { OutsideDomain, Read, Unmapped, Carried }; struct ColumnRecords { std::uint64_t base = 0; @@ -555,7 +555,7 @@ class TableSnapshotter { } records.base = heap.Base48(); records.size = heap.GetSize(); - records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset, records.size); + records.records = ImageTableAbi::ScalarBufferRecords(column.addend, column.stride, column.offset + (column.indirect ? 4u : 0u), records.size); if (records.size != 0u) tables.ranges.emplace_back(records.base, records.size); } records.records = std::min(records.records, ImageTableAbi::KeyRecords(column.maxKey, column.stride)); @@ -567,6 +567,38 @@ class TableSnapshotter { } records.words.assign(records.keys, 0u); const auto end = records.base + records.size; + if (column.indirect) { + for (std::uint32_t record = 0; record < records.keys; record++) { + if (records.states[record] != RecordState::Read) continue; + const auto offset = column.addend + record * column.stride; + const auto lowWord = ImageTableAbi::ScalarBufferDword(offset, column.offset, records.size); + const auto highWord = ImageTableAbi::ScalarBufferDword(offset, column.offset + 4u, records.size); + if (!lowWord.has_value() || !highWord.has_value() || !PageAccessible(records.base + *lowWord, records.base, end) || !PageAccessible(records.base + *highWord, records.base, end)) { + records.states[record] = RecordState::Unmapped; + continue; + } + const std::uint64_t pointer = (static_cast(Read(records.base + *highWord)) << 32u) | Read(records.base + *lowWord); + DescriptorValue value; + value.dwordCount = column.sampler ? 4u : 8u; + std::uint64_t first = std::numeric_limits::max(); + std::uint64_t past = 0; + for (std::uint32_t dword = 0; dword < column.dwordCount; dword++) { + auto address = ImageTableAbi::ScalarAddressDword(pointer, column.indirectOffset, column.indirectImmediate + dword * 4u); + if (address.has_value() && (*address & 3u) != 0u) address.reset(); + if (!address.has_value() || !AddressAccessible(*address)) { + records.states[record] = RecordState::Unmapped; + break; + } + value.dwords[dword] = Read(*address); + first = std::min(first, *address); + past = std::max(past, *address + sizeof(std::uint32_t)); + } + if (records.states[record] != RecordState::Read) continue; + records.words[record] = WordsIndex(value); + tables.ranges.emplace_back(first, past - first); + } + return records; + } constexpr auto Unread = std::numeric_limits::max(); std::vector whole(records.keys, Unread); std::vector>> split; @@ -611,6 +643,10 @@ class TableSnapshotter { for (std::uint32_t record = 0; record < records.keys; record++) { if (records.states[record] != RecordState::Read) continue; const auto offset = column.addend + record * column.stride; + if (offset >= column.foldedAddend && offset > 0xffffffffu - column.foldedCarry) { + records.states[record] = RecordState::Carried; + continue; + } const auto head = locate(offset, column.offset); const auto tail = locate(offset, column.offset + last); if (head.has_value() && tail.has_value() && (*head & 3u) == 0u && *tail > *head && *tail - *head == last && wholeAccessible(*head) && wholeAccessible(*tail)) { @@ -698,6 +734,10 @@ class TableSnapshotter { out.codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::Unmapped)); counters.poison[static_cast(PoisonReason::Unmapped)].fetch_add(1, std::memory_order_relaxed); break; + case RecordState::Carried: + out.codes[key] = ImageTableAbi::PoisonCode(static_cast(PoisonReason::OffsetCarry)); + counters.poison[static_cast(PoisonReason::OffsetCarry)].fetch_add(1, std::memory_order_relaxed); + break; } trace.poison++; } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 5209bd0cfba..6bce45abcad 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -539,6 +539,17 @@ class Tracker { return KeyDomain{sourceIndex, memory->offset, std::max(4u, 1u << zeros)}; } + static IrValue* SplitImmediateAddend(IrValue* value, std::uint32_t& addend) { + value = value->Resolve(); + addend = 0; + if (value->Opcode() == IrOpcode::IAdd32 && value->ArgumentCount() == 2u) { + if (immediateU32(value->Argument(0), addend)) return value->Argument(1)->Resolve(); + if (immediateU32(value->Argument(1), addend)) return value->Argument(0)->Resolve(); + } + addend = 0; + return value; + } + bool TryMakeTableColumn(IrValue& handle, bool sampler, bool r128, TableColumnPlan& plan) { const auto expected = sampler ? IrOpcode::GetSamplerResource : IrOpcode::GetImageResource; const std::uint32_t width = sampler ? 4u : 8u; @@ -558,6 +569,10 @@ class Tracker { } IrValue* heapHandle = nullptr; IrValue* offset = nullptr; + IrValue* base = nullptr; + std::uint32_t baseAddend = 0; + std::uint32_t carry = 0; + bool folded = false; std::uint32_t first = 0; bool address = false; for (std::uint32_t dword = 0; dword < words; dword++) { @@ -568,20 +583,30 @@ class Tracker { if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { return false; } + std::uint32_t readAddend = 0; + IrValue* readBase = SplitImmediateAddend(read->Argument(1), readAddend); if (dword == 0u) { first = memory->offset; address = addressRead; - } else if (addressRead != address || memory->offset != first + dword * static_cast(sizeof(std::uint32_t))) { + base = readBase; + baseAddend = readAddend; + } else if (addressRead != address || memory->offset + readAddend != first + baseAddend + dword * static_cast(sizeof(std::uint32_t))) { return false; + } else if (readAddend != baseAddend) { + if (readAddend < baseAddend || static_cast(memory->offset) + (readAddend - baseAddend) > 0x7fffffffu) { + return false; + } + folded = true; + carry = std::max(carry, readAddend - baseAddend); } IrValue* currentHandle = read->Argument(0)->Resolve(); - if (heapHandle != nullptr && currentHandle != heapHandle) { + if (heapHandle != nullptr && currentHandle != heapHandle && !EquivalentValue(m_program.Resources(), heapHandle, currentHandle)) { return false; } - heapHandle = currentHandle; + if (heapHandle == nullptr) heapHandle = currentHandle; if (dword == 0u) { offset = read->Argument(1)->Resolve(); - } else if (!EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + } else if (!EquivalentValue(m_program.Resources(), base, readBase)) { return false; } plan.memory.push_back(memoryIndex); @@ -599,6 +624,10 @@ class Tracker { std::uint32_t stride = 0; std::uint32_t addend = 0; bool masked = false; + std::uint32_t descriptorOffset = 0; + if (address && immediateU32(offset, descriptorOffset)) { + return TryMakeIndirectColumn(handle, *heapHandle, sampler, words, descriptorOffset, first, plan); + } if (!MatchAffineOffset(offset, key, stride, addend)) { if (!MatchMaskedOffset(offset, key, stride)) { return false; @@ -619,6 +648,8 @@ class Tracker { column.maxKey = possibleU32Bits(key); column.sampler = sampler; column.address = address; + column.foldedAddend = folded ? baseAddend : 0u; + column.foldedCarry = folded ? carry : 0u; const auto domain = masked ? std::optional{} : MatchKeyDomain(key); DescriptorSource columnSource = heapSource; columnSource.tableColumn = column; @@ -637,6 +668,68 @@ class Tracker { return true; } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& pointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { + if (pointer.Opcode() != IrOpcode::GetAddressResource || pointer.ArgumentCount() != 2u) { + return false; + } + IrValue* bufferHandle = nullptr; + IrValue* offset = nullptr; + std::uint32_t first = 0; + for (std::uint32_t dword = 0; dword < 2u; dword++) { + IrValue* read = pointer.Argument(dword)->Resolve(); + std::uint32_t memoryIndex = 0; + const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); + if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { + return false; + } + if (dword == 0u) { + first = memory->offset; + bufferHandle = read->Argument(0)->Resolve(); + offset = read->Argument(1)->Resolve(); + } else if (memory->offset != first + static_cast(sizeof(std::uint32_t)) || read->Argument(0)->Resolve() != bufferHandle || !EquivalentValue(m_program.Resources(), offset, read->Argument(1))) { + return false; + } + } + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (!MatchAffineOffset(offset, key, stride, addend)) { + return false; + } + DescriptorSource heapSource; + std::uint32_t heapIndex = 0; + if (!MakeRuntimeSource(*bufferHandle, IrOpcode::GetBufferResource, 4u, heapIndex, heapSource)) { + return false; + } + TableColumn column; + column.heapSource = heapIndex; + column.stride = stride; + column.addend = addend; + column.offset = first; + column.dwordCount = words; + column.maxKey = possibleU32Bits(key); + column.sampler = sampler; + column.indirect = true; + column.indirectOffset = descriptorOffset; + column.indirectImmediate = descriptorImmediate; + const auto domain = MatchKeyDomain(key); + DescriptorSource columnSource = heapSource; + columnSource.tableColumn = column; + plan.handle = &handle; + plan.source = InternSource(columnSource); + plan.keyDomain = domain; + auto& shared = *m_sources[plan.source].tableColumn; + shared.maxKey = std::max(shared.maxKey, column.maxKey); + if (!domain.has_value()) { + shared.unkeyed = true; + shared.keyDomains.clear(); + } else if (!shared.unkeyed && std::find(shared.keyDomains.begin(), shared.keyDomains.end(), *domain) == shared.keyDomains.end()) { + shared.keyDomains.push_back(*domain); + } + plan.offset = offset; + return true; + } + const TableColumnPlan* FindTableColumn(const IrValue& handle) const { const auto found = std::ranges::find_if(m_tableColumns, [&](const TableColumnPlan& plan) { return plan.handle == &handle; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 6ded41848d2..211f7b9a12b 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -151,6 +151,7 @@ void ValidateValueReferences(const IrProgram& program, ShaderStageInputInfo inpu break; case StageInputKind::VertexIndex: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::FrontFacing: @@ -343,6 +344,9 @@ void CollectBuiltinInputs(const IrProgram& program, ShaderInfo& info) { case StageInputKind::InstanceIndex: AddInput(info, kind, 0, 1, "gl_InstanceIndex"); break; + case StageInputKind::BaseInstance: + AddInput(info, kind, 0, 1, "gl_BaseInstance"); + break; case StageInputKind::InvocationId: AddInput(info, kind, 0, 1, "gl_InvocationID"); break; diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 5ca2dc0cc65..e1970d35c43 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -576,6 +576,7 @@ class Lowering { if (data) requireGroupedPatches(); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); + if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); program.Metadata().tessellationLocalStride = localStride; program.Metadata().tessellationControlStride = controlStride; program.Metadata().tessellationPatchStride = patchStride; @@ -773,7 +774,9 @@ class Lowering { void requireGroupedPatches() const { if (stage == IrShaderStage::Local) Fail("the relative patch or vertex index is used outside a tessellation ring or LDS address", 0u); if (tessellation.primitiveGroupSize == 0u) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed" : "the domain program uses its relative patch index as data, but GE_CNTL.PRIM_GRP_SIZE is 0 or not programmed", 0u); - if (!tessellation.breakAtInstance) Fail(stage == IrShaderStage::TessellationControl ? "the hull program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups" : "the domain program uses its relative patch index as data, but GE_CNTL.BREAK_WAVE_AT_EOI is clear, so the instances of a draw can share hull threadgroups", 0u); + if (tessellation.instancePatches == 0u || restartsPerInstance()) return; + if (stage == IrShaderStage::TessellationEvaluation) Fail("the domain program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups (GE_CNTL.BREAK_WAVE_AT_EOI clear)", 0u); + if (!tessellation.drawParameters) Fail("the hull program uses its relative patch index as data in an instanced draw whose instances share hull threadgroups, which needs the device's shaderDrawParameters", 0u); } IrValue& groupedPatch(IrValue& base) { @@ -787,8 +790,14 @@ class Lowering { IrValue& count = constant(divisor); return emit(IrOpcode::ISub32, {&value, &emit(IrOpcode::IMul32, {&emit(IrOpcode::UDiv32, {&value, &count}), &count})}); }; - IrValue& primitive = emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); - return remainder(remainder(primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); + IrValue* primitive = &emit(IrOpcode::GetBuiltin, {&constant(static_cast(StageInputKind::PrimitiveId)), &constant(0u)}); + if (tessellation.instancePatches != 0u && !restartsPerInstance()) { + IrValue& always = program.CreateValue(IrOpcode::Void, IrType::Bool); + always.SetImmediateBool(true); + IrValue& instance = emit(IrOpcode::GetTessellationAttribute, {&constant(static_cast(TessellationAttribute::InstanceInput)), &constant(0u), &always}); + primitive = &emit(IrOpcode::IAdd32, {&emit(IrOpcode::IMul32, {&instance, &constant(tessellation.instancePatches)}), primitive}); + } + return remainder(remainder(*primitive, tessellation.primitiveGroupSize), tessellation.patchesPerGroup); } void removeUnobservedCycles() { @@ -833,6 +842,33 @@ class Lowering { return result; } + [[nodiscard]] bool restartsPerInstance() const { + const auto patches = tessellation.instancePatches; + const auto group = tessellation.primitiveGroupSize; + return group != 0u && (patches % group == 0u || (group % tessellation.patchesPerGroup == 0u && patches % tessellation.patchesPerGroup == 0u)); + } + + void writeInstance() { + IrBlock* entry = program.BlockOrder().front(); + IrValue* first = entry->Instructions().empty() ? nullptr : entry->Instructions().front(); + const auto place = [&](IrOpcode opcode, IrType type, std::initializer_list arguments) -> IrValue& { + IrValue& value = program.CreateValue(opcode, type); + for (IrValue* argument : arguments) value.AddArgument(argument); + if (first != nullptr) { + entry->InsertInstructionBefore(first, &value); + } else { + entry->AppendInstruction(&value); + } + return value; + }; + IrValue& index = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::InstanceIndex)), &constant(0u)}); + IrValue& base = place(IrOpcode::GetBuiltin, IrType::U32, {&constant(static_cast(StageInputKind::BaseInstance)), &constant(0u)}); + IrValue& instance = place(IrOpcode::ISub32, IrType::U32, {&index, &base}); + IrValue& truth = program.CreateValue(IrOpcode::Void, IrType::Bool); + truth.SetImmediateBool(true); + static_cast(place(IrOpcode::SetTessellationAttribute, IrType::Void, {&constant(static_cast(TessellationAttribute::InstanceOutput)), &constant(0u), &instance, &truth})); + } + IrValue& insert(const Access& access, IrOpcode opcode, IrType type, std::initializer_list arguments) { IrValue& value = program.CreateValue(opcode, type); for (IrValue* argument : arguments) value.AddArgument(argument); diff --git a/core/shader/recompiler/PatchSpirvTools.cmake b/core/shader/recompiler/PatchSpirvTools.cmake index 2a6e17df0e8..69cb4bcfc79 100644 --- a/core/shader/recompiler/PatchSpirvTools.cmake +++ b/core/shader/recompiler/PatchSpirvTools.cmake @@ -3,31 +3,101 @@ set(spirvToolsRedundancySource "${spirvToolsSourceDir}/redundancy_elimination.cp set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsRedundancySource}") file(READ "${spirvToolsRedundancySource}" spirvToolsRedundancy) +set(spirvToolsLocalRedundancySource "${spirvToolsSourceDir}/local_redundancy_elimination.cpp") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${spirvToolsLocalRedundancySource}") +file(READ "${spirvToolsLocalRedundancySource}" spirvToolsLocalRedundancy) +set(spirvToolsExpectedElimination [=[ + auto func = [this, &vnTable, &modified, value_to_ids](Instruction* inst) { + if (inst->result_id() == 0) { + return; + } + + uint32_t value = vnTable.GetValueNumber(inst); + + if (value == 0) { + return; + } + + auto candidate = value_to_ids->insert({value, inst->result_id()}); + if (!candidate.second) { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }; + block->ForEachInst(func); +]=]) +string(FIND "${spirvToolsLocalRedundancy}" "${spirvToolsExpectedElimination}" spirvToolsEliminationPosition) +if(spirvToolsEliminationPosition EQUAL -1) + message(FATAL_ERROR "SPIRV-Tools value elimination has changed; review the local traversal fix") +endif() + set(spirvToolsOldTraversal [=[ + struct State { + DominatorTreeNode* node; + std::map value_to_id_map; + }; + std::vector todo; + todo.push_back({bb, std::map()}); + bool modified = false; + for (size_t next_node = 0; next_node < todo.size(); next_node++) { + modified |= EliminateRedundanciesInBB(todo[next_node].node->bb_, vnTable, + &todo[next_node].value_to_id_map); for (DominatorTreeNode* child : todo[next_node].node->children_) { todo.push_back({child, todo[next_node].value_to_id_map}); } + } ]=]) set(spirvToolsNewTraversal [=[ - const auto& children = todo[next_node].node->children_; - for (size_t child_index = 0; child_index < children.size(); ++child_index) { - if (child_index + 1 == children.size()) { - todo.push_back({children[child_index], - std::move(todo[next_node].value_to_id_map)}); - } else { - todo.push_back({children[child_index], todo[next_node].value_to_id_map}); - } + struct State { + DominatorTreeNode* node; + size_t next_child; + size_t added_begin; + bool entered; + }; + std::map value_to_id_map; + std::vector added_values; + std::vector todo; + todo.push_back({bb, 0, 0, false}); + bool modified = false; + while (!todo.empty()) { + auto& state = todo.back(); + if (!state.entered) { + state.entered = true; + state.node->bb_->ForEachInst([&](Instruction* inst) { + if (inst->result_id() == 0) return; + const uint32_t value = vnTable.GetValueNumber(inst); + if (value == 0) return; + auto candidate = value_to_id_map.insert({value, inst->result_id()}); + if (candidate.second) { + added_values.push_back(value); + } else { + context()->KillNamesAndDecorates(inst); + context()->ReplaceAllUsesWith(inst->result_id(), candidate.first->second); + context()->KillInst(inst); + modified = true; + } + }); + } + const auto& children = state.node->children_; + if (state.next_child != children.size()) { + auto* child = children[state.next_child++]; + todo.push_back({child, 0, added_values.size(), false}); + continue; + } + while (added_values.size() != state.added_begin) { + value_to_id_map.erase(added_values.back()); + added_values.pop_back(); } - todo[next_node].value_to_id_map.clear(); + todo.pop_back(); + } ]=]) string(FIND "${spirvToolsRedundancy}" "${spirvToolsOldTraversal}" spirvToolsTraversalPosition) if(spirvToolsTraversalPosition EQUAL -1) message(FATAL_ERROR "SPIRV-Tools redundancy traversal has changed; review the local memory fix") endif() string(REPLACE "${spirvToolsOldTraversal}" "${spirvToolsNewTraversal}" spirvToolsRedundancy "${spirvToolsRedundancy}") -string(REPLACE "#include \"source/opt/redundancy_elimination.h\"\n" - "#include \"source/opt/redundancy_elimination.h\"\n\n#include \n" - spirvToolsRedundancy "${spirvToolsRedundancy}") set(spirvToolsPatchedDir "${CMAKE_BINARY_DIR}/3rdparty/SPIRV-Tools/patched") set(spirvToolsPatchedSource "${spirvToolsPatchedDir}/redundancy_elimination.cpp") diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index d7574e7d416..68bf17d2934 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -108,7 +108,14 @@ RdnaProgram DecodeStageProgram(ShaderStageKind stage, std::spanmesh ? &*request.graphics->mesh : nullptr; - const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + std::optional tessellationCopy; + if (request.graphics && request.graphics->tessellation) { + tessellationCopy = *request.graphics->tessellation; + tessellationCopy->instancePatches = TessellationInstancePatches(request); + const auto& capabilities = request.target.supportedCapabilities; + tessellationCopy->drawParameters = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDrawParameters)) != capabilities.end(); + } + const auto* tessellation = tessellationCopy ? &*tessellationCopy : nullptr; return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 8e77c62bd0e..5b1b5591af1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -293,6 +293,8 @@ struct TessellationConfiguration { std::uint32_t patchesPerGroup = 256; std::uint32_t primitiveGroupSize = 0; bool breakAtInstance = false; + std::uint32_t instancePatches = 0; + bool drawParameters = false; }; inline constexpr std::uint32_t MeshDrawPushOffsetBytes = 104; @@ -333,6 +335,11 @@ struct RecompileRequest { bool useCache = true; }; +inline std::uint32_t TessellationInstancePatches(const RecompileRequest& request) { + if (!request.graphics || !request.graphics->tessellation || request.graphics->draw.instanceCount <= 1u || request.graphics->tessellation->breakAtInstance) return 0u; + return request.graphics->draw.indexCount / request.graphics->tessellation->inputControlPoints; +} + enum class DescriptorKind { UniformBuffer, StorageBuffer, @@ -349,7 +356,8 @@ enum class DescriptorImageShape { Image2DArray, ImageCube, Image3D, - Image1DArray + Image1DArray, + Image2DMultisample }; enum class DescriptorRole { diff --git a/core/shader/recompiler/ShaderCacheSourceVersion.hpp b/core/shader/recompiler/ShaderCacheSourceVersion.hpp new file mode 100644 index 00000000000..d73de7db7fa --- /dev/null +++ b/core/shader/recompiler/ShaderCacheSourceVersion.hpp @@ -0,0 +1,15 @@ +#ifndef CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP +#define CORE_SHADER_RECOMPILER_SHADERCACHESOURCEVERSION_HPP + +#include +#include +#include + +namespace ShaderRecompiler::ShaderDiskCache::Generated { + +extern const std::uint64_t SourceVersion; +extern const std::span RecompilerSwitches; + +} + +#endif diff --git a/core/shader/recompiler/ShaderCacheVersion.cmake b/core/shader/recompiler/ShaderCacheVersion.cmake index e952236aef8..81be546106a 100644 --- a/core/shader/recompiler/ShaderCacheVersion.cmake +++ b/core/shader/recompiler/ShaderCacheVersion.cmake @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.20) -file(STRINGS "${LIST_FILE}" entries) +file(STRINGS "${LIST_FILE}" entries ENCODING UTF-8) set(sources) set(guard) set(submodules) @@ -46,7 +46,7 @@ foreach(source IN LISTS sources) if(NOT source MATCHES "\\.(cpp|hpp|h)$") continue() endif() - file(STRINGS "${source}" lines REGEX "\"APS5_[A-Z0-9_]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "\"APS5_[A-Z0-9_]+\"") foreach(line IN LISTS lines) string(REGEX MATCHALL "\"APS5_[A-Z0-9_]+\"" names "${line}") list(APPEND switches ${names}) @@ -56,7 +56,7 @@ foreach(source IN LISTS sources) continue() endif() get_filename_component(sourceDirectory "${source}" DIRECTORY) - file(STRINGS "${source}" lines REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "^[ \t]*#[ \t]*include[ \t]*\"[^\"]+\"") foreach(line IN LISTS lines) string(REGEX REPLACE "^[ \t]*#[ \t]*include[ \t]*\"([^\"]+)\".*$" "\\1" included "${line}") set(resolved) @@ -109,7 +109,7 @@ foreach(source IN LISTS guard) continue() endif() file(RELATIVE_PATH relative "${ROOT}" "${source}") - file(STRINGS "${source}" lines REGEX "ShaderRecompiler|setenv|putenv") + file(STRINGS "${source}" lines ENCODING UTF-8 REGEX "ShaderRecompiler|setenv|putenv") foreach(line IN LISTS lines) if(line MATCHES "using[ \t]+namespace[ \t]+ShaderRecompiler" OR line MATCHES "namespace[ \t]+[A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*(::)?ShaderRecompiler[ \t]*;") list(APPEND failures "${relative} brings the ShaderRecompiler namespace in whole, which hides its calls from the shader disk cache key review: qualify them") @@ -140,23 +140,22 @@ list(REMOVE_DUPLICATES switches) list(SORT switches) list(JOIN switches ",\n " switchList) -set(content "#ifndef CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP -#define CORE_SHADER_RECOMPILER_SHADERCACHEVERSION_HPP - -#include -#include - +set(content "#include \"ShaderCacheSourceVersion.hpp\"\n") +string(APPEND content " namespace ShaderRecompiler::ShaderDiskCache::Generated { -inline constexpr std::uint64_t SourceVersion = 0x${version}ull; +namespace { -inline constexpr std::string_view RecompilerSwitches[] = { +constexpr std::string_view recompilerSwitches[] = { ${switchList} }; } -#endif +constinit const std::uint64_t SourceVersion = 0x${version}ull; +constinit const std::span RecompilerSwitches = recompilerSwitches; + +} ") if(EXISTS "${OUTPUT}") diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ede17b76e5e..f93a98ca637 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -20,7 +20,7 @@ #include #include #endif -#include "ShaderCacheVersion.hpp" +#include "ShaderCacheSourceVersion.hpp" #include #include #include @@ -107,8 +107,8 @@ static_assert(sizeof(IrResourcePlan) == 584, "IrResourcePlan changed: update Enc static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); static_assert(sizeof(GuestRegister) == 8, "GuestRegister changed: update the plan value encoder"); static_assert(sizeof(MemoryInfo) == 80, "MemoryInfo changed: update the plan encoder"); -static_assert(sizeof(DescriptorSource) == 144, "DescriptorSource changed: update the plan encoder"); -static_assert(sizeof(TableColumn) == 64, "TableColumn changed: update the plan encoder"); +static_assert(sizeof(DescriptorSource) == 160, "DescriptorSource changed: update the plan encoder"); +static_assert(sizeof(TableColumn) == 80, "TableColumn changed: update the plan encoder"); static_assert(sizeof(KeyDomain) == 16, "KeyDomain changed: update the plan encoder"); static_assert(sizeof(ResourceBlock) == 80, "ResourceBlock changed: update the plan encoder"); static_assert(sizeof(SrtRead) == 16, "SrtRead changed: update the plan encoder"); @@ -148,7 +148,7 @@ static_assert(fieldCount() == 21, "IrResourcePlan changed: updat static_assert(fieldCount() == 2, "GuestRegister changed: update the plan value encoder"); static_assert(fieldCount() == 30, "MemoryInfo changed: update the plan encoder"); static_assert(fieldCount() == 3, "DescriptorSource changed: update the plan encoder"); -static_assert(fieldCount() == 10, "TableColumn changed: update the plan encoder"); +static_assert(fieldCount() == 15, "TableColumn changed: update the plan encoder"); static_assert(fieldCount() == 4, "KeyDomain changed: update the plan encoder"); static_assert(fieldCount() == 4, "ResourceBlock changed: update the plan encoder"); static_assert(fieldCount() == 2, "SrtRead changed: update the plan encoder"); @@ -1019,6 +1019,11 @@ bool encodePlan(Writer& writer, const IrResourcePlan& plan) { domains.Value(domain.value); }); out.Value(column.unkeyed); + out.Value(column.indirect); + out.Value(column.indirectOffset); + out.Value(column.indirectImmediate); + out.Value(column.foldedAddend); + out.Value(column.foldedCarry); }); writer.List(plan.controlFlow, [&](Writer& out, const ResourceBlock& block) { out.Value(valueIndex(block.condition)); @@ -1169,6 +1174,11 @@ bool decodePlan(Reader& reader, IrResourcePlan& plan) { domains.Value(domain.value); }); in.Value(column.unkeyed); + in.Value(column.indirect); + in.Value(column.indirectOffset); + in.Value(column.indirectImmediate); + in.Value(column.foldedAddend); + in.Value(column.foldedCarry); source.tableColumn = table ? std::optional(std::move(column)) : std::nullopt; }); reader.List(plan.controlFlow, 28, [&](Reader& in, ResourceBlock& block) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 0612a023fe0..c76e2a1c258 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -93,7 +93,7 @@ struct SpirvEmitterState { SpirvModule module; const IrProgram& program; ShaderStageInputInfo inputInfo; - std::array tessVariables {}; + std::array tessVariables {}; std::uint32_t tessInnerVariable = 0; SpirvRequirements requirements; @@ -134,6 +134,7 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::map, std::uint32_t> interpolateFunctions; std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index 241522188cf..592cb6a6d1e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace ShaderRecompiler { namespace { @@ -806,12 +808,52 @@ std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std }); } +namespace { + +std::uint32_t InterpolateFunction(SpirvEmitterState& state, bool halfResult, std::uint32_t mode) { + const auto key = std::make_pair(halfResult, mode); + if (const auto found = state.interpolateFunctions.find(key); found != state.interpolateFunctions.end()) return found->second; + const auto f32 = TypeF32(state); + const auto id = state.module.AllocateId(); + struct Restore { + SpirvEmitterState& state; + std::uint32_t label; + ~Restore() { + state.module.EndHelperFunction(); + state.currentLabel = label; + } + }; + state.module.BeginHelperFunction(); + const Restore restore{state, state.currentLabel}; + state.module.AddName(id, std::string(halfResult ? "interpolate_f16_mode_" : "interpolate_f32_mode_") + std::to_string(mode)); + state.module.AddFunction(spv::OpFunction, f32, id, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, f32, f32, f32, f32)); + std::array parameters{}; + for (auto& parameter : parameters) { + parameter = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, f32, parameter); + } + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, Interpolate(state, parameters[0], parameters[1], parameters[2], halfResult, mode)); + state.module.AddFunction(spv::OpFunctionEnd); + state.interpolateFunctions.emplace(key, id); + return id; +} + +std::uint32_t CallInterpolate(SpirvEmitterState& state, std::uint32_t delta, std::uint32_t coordinate, std::uint32_t base, bool halfResult, std::uint32_t mode) { + const auto function = InterpolateFunction(state, halfResult, mode); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeF32(state), result, function, delta, coordinate, base); + return result; +} + +} + std::uint32_t EmitFPInterpolateF32(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, false, mode->ImmediateU32()); } std::uint32_t EmitFPInterpolateF16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2, const IrValue* mode) { - return Interpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); + return CallInterpolate(state, arg0, arg1, arg2, true, mode->ImmediateU32()); } std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index bd37156052f..5174516920a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -70,6 +70,7 @@ std::uint32_t BuiltInForInput(StageInputKind kind) { case StageInputKind::PrimitiveId: return spv::BuiltInPrimitiveId; case StageInputKind::TessCoord: return spv::BuiltInTessCoord; case StageInputKind::InstanceIndex: return spv::BuiltInInstanceIndex; + case StageInputKind::BaseInstance: return spv::BuiltInBaseInstance; case StageInputKind::FragCoord: return spv::BuiltInFragCoord; case StageInputKind::FrontFacing: return spv::BuiltInFrontFacing; case StageInputKind::HelperInvocation: return spv::BuiltInHelperInvocation; @@ -217,6 +218,7 @@ void DefineInputs(SpirvEmitterState& state) { case StageInputKind::InvocationId: case StageInputKind::PrimitiveId: case StageInputKind::InstanceIndex: + case StageInputKind::BaseInstance: case StageInputKind::Layer: case StageInputKind::SampleId: type = TypeI32(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index f4c1da3f2bf..48225d9ae25 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -137,9 +137,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilityGroupNonUniformShuffle); } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupQuadsKHR); + state.module.EmitCapability(spv::CapabilityComputeDerivativeGroupLinearKHR); state.module.EmitExtension("SPV_KHR_compute_shader_derivatives"); } + if (std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.kind == StageInputKind::BaseInstance; })) { + state.module.EmitCapability(spv::CapabilityDrawParameters); + } const bool fragmentBarycentric = StageOf(state) == IrShaderStage::Pixel && std::any_of(state.inputs.begin(), state.inputs.end(), [](const SpirvInputBinding& input) { return input.perVertex || input.kind == StageInputKind::BaryCoordSmooth || input.kind == StageInputKind::BaryCoordNoPerspective; }); @@ -170,10 +173,6 @@ void DefineModule(SpirvEmitterState& state) { localX = ((localX * localY * localZ + 63u) / 64u) * 32u; localY = 1u; localZ = 1u; - if (state.requirements.computeDerivatives) { - localY = localX / 2u; - localX = 2u; - } } state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeLocalSize, localX, localY, localZ); } @@ -210,7 +209,7 @@ void DefineModule(SpirvEmitterState& state) { } } if (state.requirements.computeDerivatives && StageOf(state) == IrShaderStage::Compute) { - state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupQuadsKHR); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); if (state.requirements.functionLds) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6ea..96c42edc4a9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -87,7 +87,7 @@ std::uint32_t EmitBuiltinU32(SpirvEmitterState& state, StageInputKind kind, std: state.module.AddFunction(spv::OpSelect, TypeU32(state), bits, value, ConstantU32(state, 1u), ConstantU32(state, 0u)); return bits; } - if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { + if (kind == StageInputKind::VertexIndex || kind == StageInputKind::InstanceIndex || kind == StageInputKind::BaseInstance || kind == StageInputKind::InvocationId || kind == StageInputKind::PrimitiveId || kind == StageInputKind::Layer || kind == StageInputKind::SampleId) { const auto value = state.module.AllocateId(); const auto bits = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeI32(state), value, variable); @@ -579,9 +579,16 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins if (variable == 0u) { throw std::runtime_error("tessellation attribute has no interface variable"); } - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + if (kind == TessellationAttribute::InstanceOutput) { + return variable; + } const auto pointer = state.module.AllocateId(); + if (kind == TessellationAttribute::InstanceInput) { + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, storage, TypeU32(state)), pointer, variable, ConstantU32(state, 0u)); + return pointer; + } if (kind == TessellationAttribute::Factor) { if (!inst.Argument(1)->HasImmediate()) { throw std::runtime_error("tessellation factor index must be immediate"); @@ -629,7 +636,7 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins } void DefineTessellationInterfaces(SpirvEmitterState& state) { - std::array used {}; + std::array used {}; for (const IrBlock* block : state.program.BlockOrder()) { for (const IrValue* inst : block->Instructions()) { if (inst->Opcode() != IrOpcode::GetTessellationAttribute && inst->Opcode() != IrOpcode::SetTessellationAttribute) { @@ -650,12 +657,15 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { continue; } const auto kind = static_cast(index); - const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput; + const bool input = kind == TessellationAttribute::ControlInput || kind == TessellationAttribute::EvaluationInput || kind == TessellationAttribute::PatchInput || kind == TessellationAttribute::InstanceInput; const bool patch = kind == TessellationAttribute::PatchOutput || kind == TessellationAttribute::PatchInput; const auto storage = input ? spv::StorageClassInput : spv::StorageClassOutput; + const bool instance = kind == TessellationAttribute::InstanceOutput || kind == TessellationAttribute::InstanceInput; std::uint32_t type; if (kind == TessellationAttribute::Factor) { type = array(TypeF32(state), 4u); + } else if (instance) { + type = kind == TessellationAttribute::InstanceInput ? array(TypeU32(state), tess.inputControlPoints) : TypeU32(state); } else if (patch) { type = array(TypeU32Vector(state, 4u), (state.program.Metadata().tessellationPatchStride + 15u) / 16u); } else { @@ -667,8 +677,10 @@ void DefineTessellationInterfaces(SpirvEmitterState& state) { } } auto& variable = state.tessVariables.at(index); - variable = DefineInterfaceVariable(state, type, storage, "tess_attributes"); - if (kind == TessellationAttribute::Factor) { + variable = DefineInterfaceVariable(state, type, storage, instance ? "tess_instance" : "tess_attributes"); + if (instance) { + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationLocation, (state.program.Metadata().tessellationLocalStride + 15u) / 16u); + } else if (kind == TessellationAttribute::Factor) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBuiltIn, spv::BuiltInTessLevelOuter); state.tessInnerVariable = DefineInterfaceVariable(state, array(TypeF32(state), 2u), storage, "tess_inner"); state.module.AddAnnotation(spv::OpDecorate, state.tessInnerVariable, spv::DecorationBuiltIn, spv::BuiltInTessLevelInner); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp index a1b5e245b21..89702e7b2bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp @@ -94,7 +94,7 @@ std::vector ValidateAndOptimizeSpirv(std::span(spirv.begin(), spirv.end()); + if (mode != nullptr && std::string(mode) == "none") return std::vector(spirv.begin(), spirv.end()); // The recompiler emits helpers (BDA lookup, fault reporting) as functions called from every memory // access. Exhaustive inlining multiplies module size by ~10x and optimization time by ~20x, so the // default pipeline keeps the performance passes that work per function and leaves inlining to the driver. diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 8bd53ac8db5..70c446d3c29 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -231,7 +231,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO const std::uint32_t waves = std::min((totalThreads + waveSize - 1u) / waveSize, 0x3fu); IrValue& localIndex = builtin(StageInputKind::LocalInvocationIndex); IrValue& waveId = entryIr.Emit(IrOpcode::UDiv32, IrOpcodeType(IrOpcode::UDiv32), {&localIndex, &entryIr.Constant(waveSize)}); - IrValue& waveBits = entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u)); + IrValue& waveBits = entryIr.BitwiseOr(entryIr.ShiftLeftLogical(waveId, entryIr.Constant(6u)), entryIr.ShiftLeftLogical(waveId, entryIr.Constant(20u))); IrValue& firstBit = entryIr.Select(entryIr.IEqual(waveId, entryIr.Constant(0u)), entryIr.Constant(0x80000000u), entryIr.Constant(0u)); entryIr.SetScalarReg(static_cast(cs->workgroupRegister + regOffset), entryIr.BitwiseOr(entryIr.BitwiseOr(waveBits, entryIr.Constant(waves)), firstBit)); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 3e1f574040e..7fc5176d482 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -195,6 +195,8 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.patchesPerGroup = tessellation->patchesPerGroup; target.primitiveGroupSize = tessellation->primitiveGroupSize; target.breakAtInstance = tessellation->breakAtInstance; + target.instancePatches = tessellation->instancePatches; + target.drawParameters = tessellation->drawParameters; } if (stage == ShaderStageKind::Mesh) { if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); diff --git a/core/shader/recompiler/tests/test_shader_cache_native_paths.py b/core/shader/recompiler/tests/test_shader_cache_native_paths.py new file mode 100644 index 00000000000..b2f1e8c57f3 --- /dev/null +++ b/core/shader/recompiler/tests/test_shader_cache_native_paths.py @@ -0,0 +1,78 @@ +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +SCRIPT = Path(sys.argv.pop(1)).resolve() +CMAKE = sys.argv.pop(1) + + +class ShaderCachePathTests(unittest.TestCase): + def fixture(self, root): + source = root / 'recompiler' / 'input.cpp' + source.parent.mkdir(parents=True) + source.write_text('const char* mode = "APS5_TEST_PATH";\n', encoding='utf-8') + listing = root / 'sources.txt' + listing.write_text(f'sources {source.as_posix()}\n', encoding='utf-8') + output = root / 'version.hpp' + return source, listing, output + + def run_script(self, root, listing, output): + return subprocess.run([CMAKE, f'-DROOT={root.as_posix()}', + f'-DRECOMPILER_DIR={(root / "recompiler").as_posix()}', + f'-DLIST_FILE={listing.as_posix()}', f'-DOUTPUT={output.as_posix()}', + '-P', str(SCRIPT)], capture_output=True, encoding='utf-8', errors='replace', timeout=10) + + def check_source_change(self, name): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / name + source, listing, output = self.fixture(root) + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + previous = output.read_bytes() + self.assertIn(b'APS5_TEST_PATH', previous) + source.write_text('const char* mode = "APS5_TEST_CHANGED";\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertNotEqual(previous, output.read_bytes()) + self.assertIn(b'APS5_TEST_CHANGED', output.read_bytes()) + unchanged = output.stat().st_mtime_ns + result = self.run_script(root, listing, output) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertEqual(output.stat().st_mtime_ns, unchanged) + + def test_ascii(self): + self.check_source_change('plain path') + + def test_cyrillic(self): + self.check_source_change('\u0441\u0431\u043e\u0440\u043a\u0430 \u0438\u0433\u0440\u044b') + + def test_cjk(self): + self.check_source_change('\u6e2c\u8a66 path') + + def test_unicode_guard_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / '\u0441\u0431\u043e\u0440\u043a\u0430' + source, listing, output = self.fixture(root) + (root / 'shared.hpp').write_text('int example();\n', encoding='utf-8') + source.write_text('#include "../shared.hpp"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + def test_unicode_header_rejects_unhashed_include(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) / 'plain path' + source, listing, output = self.fixture(root) + name = '\u0437\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a.hpp' + (root / name).write_text('int example();\n', encoding='utf-8') + source.write_text(f'#include "../{name}"\n', encoding='utf-8') + result = self.run_script(root, listing, output) + self.assertNotEqual(result.returncode, 0) + self.assertIn('does not hash', result.stdout + result.stderr) + self.assertFalse(output.exists()) + + +unittest.main() diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index e73a415d40a..15ca8b2ea4b 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -43,7 +43,7 @@ flowchart LR ``` - [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). -- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. +- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. Every other library keeps the names libc exports; it reads them from `libc.exports`, which `nid_patcher --write-export-list` rewrites only when that list changes. On Windows the link writes each import library with `SOURCE_DATE_EPOCH=0` and `ranlib -D` and replaces it only when its bytes change. A source edit that keeps the exports therefore relinks and re-patches only its own library. ## Windows deferred module initialization diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index a3bd5422831..12b552df7ce 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -30,7 +30,7 @@ git submodule update --init --recursive - Linux display refresh queries require SDL's XRandR support (`libxrandr-dev` on Debian/Ubuntu; `libXrandr-devel` on Fedora). Bundled SDL configuration fails without it. Keep `SDL_X11_XRANDR` enabled. - Linux: GCC, G++, binutils. SDL's X11 backend requires X11 and Xext development headers (`libx11-dev` and `libxext-dev` on Debian/Ubuntu). SDL's Wayland backend requires the Wayland, xkbcommon and EGL development files (`libwayland-dev`, `libxkbcommon-dev` and `libegl-dev` on Debian/Ubuntu; `wayland-devel`, `libxkbcommon-devel` and `mesa-libEGL-devel` on Fedora), and `libdecor-0-dev` (`libdecor-devel`) for window decorations on GNOME. SDL loads both backends at run time and prefers Wayland when `WAYLAND_DISPLAY` is set; `SDL_VIDEODRIVER=x11` selects X11 (XWayland). Titles that bundle their own SDL read `SDL_VIDEODRIVER` too, and some of them fail to start with `x11`. Without the Wayland files configuration still succeeds, and the build uses X11 only. 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: only MinGW-w64 GCC 15.2.0 (WinLibs `x86_64-ucrt-posix-seh`, release [`15.2.0posix-14.0.0-ucrt-r7`](https://github.com/brechtsanders/winlibs_mingw/releases/tag/15.2.0posix-14.0.0-ucrt-r7), archive `winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip`) 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`. - FFmpeg binaries are downloaded during configuration unless `FFMPEG_PREBUILT_DIR` is set. With the WinLibs CMake, the download fails with status 60 (`SSL peer certificate or SSH remote key was not OK`) unless `SSL_CERT_FILE` names a CA bundle, for example `C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt` from Git for Windows, as in CI. @@ -151,9 +151,9 @@ Set `APS5_TRACE_FRAMES=:` to record `` presents from presen Set `APS5_TRACE_EVENTS=` to change the queue-worker and presenter buffers for a frame or seconds window. The count is a whole number from 1 through 4,194,304; the default is 262,144. An invalid value fails when one of these threads first binds to the trace, before recording. Each buffer uses `count * sizeof(TraceThread::Slice)` bytes (currently 32 bytes per event): 8 MiB at the default, 96 MiB at `3145728`, and 128 MiB at the maximum, per thread. The buffers are allocated even while waiting for the window to open, and formatting the trace uses additional memory. Shader preparation keeps its explicitly sized 32,768-event buffers, and the GPU range limit stays 262,144. Summary-only sessions allocate no event buffers and do not read this setting. A larger buffer can still fill; use a shorter window or increase the count within the bound. -Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording and sets `trace_dropped`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. +Inside the window the queue workers and the presenter record every `PerformanceTimer` scope and stage as a complete event named after its frame timing key: `Driver.WorkerWait`, `Driver.Packet`, `Driver.Packet.draw`, `Graphics.Draw`, `Driver.Present`, `Driver.Present.inflight_wait`, `Vulkan.Retire` and so on. A slice shorter than 20 us is only counted: its time is in the thread's counter ` folded` (milliseconds per present) and in the summary. A thread writes a present's counter sample at its first slice after the next present, so the last present of the window has no sample; its time is still in the summary. By default, each queue worker and presenter allocates a buffer for 262,144 events when it starts; a full buffer stops that thread's recording, sets `trace_dropped` and puts an instant event `trace buffer full` on the thread's track at the start of the first lost slice (slices are appended when their outer scope ends, so that can precede the last recorded slice by up to one outer-scope length), with the number of lost slices in `args.events_lost`. When the window closes, the presenter formats the trace once and a `ProfileOutput` writer thread writes `aps5-trace--.json.gz` (Chrome trace-event JSON, opened by ui.perfetto.dev or chrome://tracing) and appends one line to `aps5-summary.jsonl`, both in `APS5_TRACE_DIR` or the working directory. A window that is still open when the title exits writes nothing. -The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped` and `trace`. A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. +The summary line has `commit` and `dirty` of the build, `env` (every `APS5_*` variable; a value containing `/` or `\` is written as ``), `gpu`, `driver`, `os`, `resolution` (the video output's), `window`, `presents`, `frame_ms` (p50, p90, p99 and max of the intervals between presents), `gpu_busy_ms_per_present` (the batch timestamps behind `[present]`), `worker_busy_pct` (share of the window each queue worker spends in `Driver.Packet`, `Driver.Suspend`, `Driver.EndSubmission`, `Driver.Rewind` and `Driver.Completion`), `top_phases_ms_per_frame` (the eight largest scopes and stages, without `Driver.WorkerWait`), `trace_dropped`, `trace_dropped_threads` and `trace`. `trace_dropped` is `true` when any thread lost a slice, any thread's totals table (512 scope and stage entries) was full, or the `gpu` track was full. `trace_dropped_threads` is present only then and has one entry per such thread name (threads that share a name, such as the shader prepare threads, share one entry: `events_lost` is summed, `events_full_after_s` is the earliest, `totals_full` is true if any is): `events_full_after_s` (seconds from the first present of the window to the start of the first lost slice; absent for `gpu` and, in every summary line (windowed `APS5_TRACE_FRAMES`/`APS5_TRACE_SECONDS` and `APS5_TRACE_SUMMARY_EVERY`), whenever the first lost slice starts outside the line's span of presents, including a slice whose scope began before the window opened or one lost after the window's last present), `events_lost` (slices not recorded; absent when 0; in `APS5_TRACE_SUMMARY_EVERY` lines it counts the thread buffer's losses since it started, not only those of the line's span) and `totals_full` (`true` when the thread's totals table overflowed, which makes the summary numbers incomplete; a thread with `events_lost` only has complete totals). A trace holds timings, scope names, counts and thread names; no guest memory, paths or strings of the title. The shader preparation threads are traced as well, `shader prepare`, with a buffer for 32,768 events each. A wait for the GPU mutex on a traced thread is a `GpuMutex.wait.` slice (`dispatch`, `draw`, `present`, `flush` and so on, as on the `[lock]` line). diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3e40f44bba2..2f721015f3f 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -4,7 +4,6 @@ Entries about one library or component are in [debt](debt/), one file per librar ### Build -- The bundled [SPIRV-Tools build](../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy that releases processed traversal maps. Remove this patch when the pinned revision includes [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). System-dependency builds need the fix in their installed SPIRV-Tools package. - Building on Windows requires a specific version of mingw - MinGW-w64 GCC 15.2.0 (`winlibs-gcc15`, `x86_64-ucrt-posix-seh`) - Even compiled prx libraries on Windows require nearby (static linking of these dependencies causes conflicts): - libgcc_s_seh-1.dll diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 2a413608079..81b3f5e7183 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -7,7 +7,7 @@ - [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 - [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 - [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 -- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - unsupported register-default versions return `0x80890001` following [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c); AnyPS5 keeps version 13 support because its register-default tables include it, while that reference accepts only versions 0–12. The upper version accepted by PS5 firmware is unverified +- [sceAgcInit](../../../core/libs/prx/libSceAgc/DcbState/src/Init.cpp) (libSceAgc) - versions 0–13 select their register-default tables and return 0; pointer-shaped or otherwise out-of-range values (>=14) return 0 without a register-default lookup. Console probe on firmware 13.60 ([#4808](https://github.com/boykopovar/AnyPS5/issues/4808#issuecomment-6149448503)): `sceAgcInit(&buf, 8)` (NID `kW3GLb7QfPg`) returns 0 and writes nothing; scalar versions 2–13 return `0x8A6C0004` there, while AnyPS5 still returns 0 for those scalars. The probe does not show that table 13 is selected for pointer-shaped calls. [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns `0x80890001` above 12 for `sceAgcInit`, and falls back to version 11 in `sceAgcGetRegisterDefaults2`. PPSA99011, PPSA99995, [ps5-vulkan](https://github.com/mpereiraesaa/ps5-vulkan) and [prospero-win](https://github.com/mpereiraesaa/prospero-win) call that NID with a pointer-shaped first argument. `sceAgcInit_0090` (NID `23LRUSvYu1M`) checks the state pointer and then calls `sceAgcInit`, as OpenAGC does - [sceAgcSetSubmitMode](../../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted - [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 - [sceAgcGetGsPrimPayload](../../../core/libs/prx/libSceAgc/Shader/src/GsPrimPayload.cpp) (libSceAgc) - the search does not stop at an `SPI_SHADER_IDX_FORMAT` (0x1C2) entry whose format is not `SPI_SHADER_2COMP`, as the analysis of the library declared in the pull request describes (its origin and firmware version are not identified), while [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1285-L1310) stops there; not verified on PS5 hardware. A null payload, a null shader or a register count with a null register array throws instead of faulting on the memory access diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 24455beaa32..0bdf741fc61 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -30,11 +30,12 @@ ### Functional - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. +- [Type-0 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 in dword 3) with a nonzero base are bound as null images, like a zero base. The behaviour comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault); it was not measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. - [Collapsed color array views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) preserve the selected slice in the XOR address equation when the view is represented as a shifted 2D surface. Native Vulkan coverage checks R_X draws, array reads, publication and re-upload for slices 1-3; Lost Soul Aside PPSA26358 validates the RGBA16F cube mip chain on Windows/NVIDIA. Other devices and console comparisons remain unverified. Full-array views retain their layer addressing. -- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. Planes with mips, a base slice other than 0, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). +- [Depth plane copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth plane sampled as a 2D array or a cube, or a D16 plane read as `R16_UINT`, is copied into an image of its own (`DepthPlaneCopy`), kept per base address, extent, format and slice count; a slice comes from the live depth surface when one is bound at the slice's address and from guest memory (detiled) otherwise. A draw or dispatch that binds the plane copies again only the slices that changed and records nothing when none did: a surface slice when its image or write serial differs from the one copied (the serial is bumped when the surface is created, when a draw binds it as its depth target, when a pending fast clear is applied and when a written storage view of the plane is copied back), a guest-memory slice when its bytes were written since they were last read (`GuestMemory::UnchangedSince`), a snapshot write overlaps them, or its address, size or tile mode changed. The surface of the latest depth draw is copied at every use until another depth target is bound, since that draw may still be recorded after the copy. A slice's address is the base plus a whole 64 KiB-block depth slice per layer (`DepthSliceBytes`), and the cube face order is the array order; both are inferred from the titles' use, not measured on hardware. A 2D-array T# whose `BASE_ARRAY` is past slice 0 reads slices `BASE_ARRAY` to `LAST_ARRAY` of the copy, which holds slices 0 to `LAST_ARRAY`, and a 2D binding of it reads slice `BASE_ARRAY`; that follows the colour 2D-array semantics and the titles that hit it (PPSA01556, PPSA03912), and was not measured on hardware for depth surfaces. Planes with mips, a cube or a 2D T# with a base slice other than 0, a `BASE_ARRAY` past `LAST_ARRAY`, a cube with other than 1 or 6 slices, and other views of the planes still throw, and so do these copies of a 16-bit plane with stencil that the device stores as D24 or D32 (16-bit depth with stencil, below). - [SW_64KB_Z_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the texture equations and 64 KiB block geometry of `SW_64KB_Z_X`, whose addresses are checked against [AMD addrlib GFX10](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) in the texture tiling tests. Any 256-byte pipe/bank XOR below the block size is accepted, as for `SW_64KB_R_X` targets and Z_X textures; which of those bits the console honours, and the render results, are not measured. - [SW_64KB_D_X color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ColorTargetLayout.cpp) (libSceAgcDriver) use the existing texture block/mip geometry and equations from [AMD addrlib GFX10 non-RB+](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp) for 16 pipes and 256-byte interleave. The address-reference tests use legal pipe/bank XOR bits for that configuration. Raw column bits outside the source configuration's pipe/bank mask 0xcf00 throw; their console meaning is unresolved. Console address equations and render results have not been measured. - [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. @@ -89,7 +90,7 @@ - [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 color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. -- [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 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 and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. 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) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. - [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off. - [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware. @@ -132,3 +133,4 @@ - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. +- [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 017fe07cf44..749777f7486 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -2,8 +2,10 @@ ### Unknown function info +- [sceAudioOut2ContextAdvance and sceAudioOut2ContextPush](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) - with an SDL device, Advance snapshots each port's PCM and gain; Push consumes completed grains in order and a blocking Push waits for Advance when none is ready. Inferred from PPSA26358, which rewrites the same 512-frame, 12-channel float buffer on a producer thread and pushes on another thread. Console buffer ownership, snapshot timing and empty-queue errors are unverified. More than 16 completed grains awaiting Push and a nonblocking Push without a completed grain throw; the limit and errors are runtime restrictions, not console measurements. Context destruction concurrent with active calls remains unsupported. Without an SDL device the existing wall-clock path remains. + - [sceAudioOut2UserGetSupportedAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2User.cpp) (libSceAudioOut) - the user handle and two uint32 output pointers follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp), not a console measurement. The masks describe this runtime: no context attributes and port attributes PCM (bit 0) and gain (bit 1). Null outputs and unknown or destroyed user handles throw; console validation order and error values are unknown. Users are tracked for this query, but port ownership and busy-user destruction are not implemented. The other PPSA23566 imports remain declared without parameters, with unknown signatures: [sceConvertKeycodeGetCharacterFromKeyboardData](../../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sce::Json::MemAllocator::notifyError](../../../core/libs/prx/libSceJson2/Export.cpp), [sceNpCommerceDialogOpen2](../../../core/libs/prx/libSceNpCommerce/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 -- [sceAudioOutExClose, sceAudioOutExConfigureOutput, 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 +- [sceAudioOutExOpen, sceAudioOutExConfigureOutput, sceAudioOutExClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the HDMI bitstream calls follow [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/docs/FINDINGS.md#L31-L80), which disassembles firmware 12.70's libSceAudioOut and logs the calls on a console. `sceAudioOutExOpen(user, mode)` returns `0x80260015` for a mode above 10 or outside mask `0x61F`; otherwise it opens a port of internal type 6 in S16 stereo, 256 frames at 48000 Hz for modes 0, 1, 2 and 9 and 1024 frames at 192000 Hz for modes 3, 4 and 10. `sceAudioOutExConfigureOutput(control, flags, mode, target, option)` returns `0x80268009` when `control` is not 0, takes modes 0 to 10 and 0xFF and targets 1 to 3 and 0xFF, and returned 0 on the console; `sceAudioOutExClose` returned 0. The argument registers match [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L26-L28) and PPSA99051's [bitstream_out.cpp](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/cf3c921d79bf00db4847f31b68a7bd95a8037cd7/sample-app/src/platform/ps5/bitstream_out.cpp#L29-L32). Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so an Ex port has no host device: `sceAudioOutOutput` paces it at its own rate and drops its IEC 61937 bursts, and `sceAudioOutExConfigureOutput` changes nothing. Nonzero `flags` or `option`, other modes or targets, `sceAudioOutExClose` of a handle that is not an Ex port, and `sceAudioOutGetPortState` on an Ex port throw, since the console's answers are unknown - [sceAudioOutExGetMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports a sink without audio descriptors: it zeroes the buffer and returns 0, as `sceAudioOutSysGetHdmiMonitorInfo` does, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(0, 0, info, size)` and the descriptor count as a 32-bit value at +0x98, with 8-byte descriptors from +0xA0, follow [Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/jelly5_bitstream.cpp#L62-L117) (PPSA99505), which passes a 0x400-byte buffer and read that layout from one TV; with no descriptor listed it decodes the audio itself instead of opening a bitstream port. The meaning of the first two arguments and the structure's size are unknown: nonzero first or second arguments and sizes below 0xA0, which cannot hold the count, throw, and every byte of the declared size is written as 0. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured - [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 @@ -16,10 +18,11 @@ - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - the stereo fold is assumed, not the console's downmix: C at -3 dB into both sides, Ls/Rs, Lb/Rb and Ltf/Rtf at -3 dB and Ltb/Rtb at -6 dB into their own side, LFE dropped - [sceAudioOut2PortSetAttributes](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - each PCM buffer set (attribute 0) is taken as one grain: buffers set between two pushes are queued and played one per grain, at most 16 queued. A grain with no new buffer repeats the last one only when the port has always been given that same buffer (rewritten in place); a port whose buffers move on plays silence instead. Inferred from PPSA12544, whose object port (type 6) is given one grain of a 64-grain ring per set, often two or three between pushes and then none for a few; the console behaviour for several sets per push and for a push without a new buffer is not documented - [sceAudioOutDeviceIdOpen](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA01317, which fails to load without it; declared without parameters, signature unknown. It opens a port on a given audio device; which device ids exist and how they map to host outputs is unknown, so it throws +- [sceAudioOut2PortGetState](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) and [sceAudioOut2GetSpeakerInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - the port state reports the channel count of the port's data format, output main, volume 127 and a zero reroute counter and flags; the speaker info reports a stereo front pair at -30 and +30 degrees. Taking the channel count from the data format follows [SharpEmu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Audio/AudioOut2Exports.cs), another emulator, and no console measurement exists for either call: the output of a pad port, the volume after `sceAudioOut2PortSetAttributes` and the reroute counter on a device change are unverified, as are the speaker types other than stereo. ASTRO BOT and ASTRO's PLAYROOM reportedly stop on an assertion that depends on these values (#4317), but which field matters has not been isolated ### Functional - [sceAudioOut2EnableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) takes no arguments and returns 0, as declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) ("Allows chat audio alongside the application's own"). The host mixes no chat audio and AudioOut2 has no chat port, so the call only records that chat audio is allowed. Whether the console fails it without a chat session, when repeated or before `sceAudioOut2Initialize` is unknown. [sceAudioOut2DisableChat](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) is the counterpart, imported by PPSA18216: it takes no arguments, returns 0 and records that chat audio is no longer allowed; its signature is not confirmed by a console measurement, and whether the console fails it without a chat session or when repeated is unknown. - [sceAudioOut2Set3DLatency](../../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit, sceAudioOut2MasteringTerm and sceAudioOut2MasteringSetParam](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. -- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. +- [AudioOut2 contexts](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) with an SDL device and no `sceAudioOut2ContextAdvance` calls read a grain's port buffers at the next push, at least 100 us after the grain was pushed, and play the mix behind a 40 ms cushion that the queue level leaves out. When the hardware reads the buffers is not known. The timing follows a title whose mixer fills a buffer after pushing it, reuses it two pushes later, and may still be writing it when the next push follows within microseconds. The last grain pushed before a title stops pushing is never read or played. - [sceAudioOutSysGetHdmiMonitorInfo](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) reports an HDMI sink without a monitor name or audio descriptors: it zeroes the 0x180 bytes and returns 0, since audio reaches the host through SDL as PCM and there is no EDID to describe. The arguments `(type, info, size)`, the required size 0x180 and the fields titles read (monitor name at +0x0B, 13 bytes; descriptor count as a 32-bit value at +0x94; 8-byte descriptors `[coding type, channels, rate mask, 0, extra, 0, 0, 0]` from +0x98) come from [PS5-Audio-Passthrough-Research](https://github.com/sainsaji/PS5-Audio-Passthrough-Research/blob/c3be9c2afcb8f7838784ead0fe1c12a2ceea965a/docs/FINDINGS.md) (firmware 12.70, one monitor). What a console without an HDMI audio sink returns, an error or no descriptors, is unknown. The other bytes, among them the header at +0x90 (`01 09 4f 00` on that monitor), have no known meaning and are written as 0. No LPCM descriptor is reported for the host device, as the rate mask and extra byte of one are not known. A null buffer returns `0x80260004` like the other AudioOut calls; the console's code is not measured. Monitor type 0x10, which the console also accepts, other types and sizes other than 0x180 throw: their results and error codes are unknown. diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md new file mode 100644 index 00000000000..342b2df1719 --- /dev/null +++ b/docs/dev/debt/libSceJpegDec.md @@ -0,0 +1,9 @@ +# libSceJpegDec technical debt + +### Unknown function info + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - structure layouts follow [OpenOrbis `jpeg.h`](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/_types/jpeg.h#L13-L72) and [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L20-L117); attributes 1 and 2 are the values [SharpProspero's `JpegImage.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Graphics/JpegImage.cs#L64-L74) passes. The error codes are libScePngDec's: no reference lists the JpegDec ones, and SharpProspero raises 0x80650020 on its JpegDec path where it raises libScePngDec's 0x80690020 on its PngDec path. Image info always reports color space 3, suitable CSC attribute 1 and zero component ids and sampling factors; the console's values, including which images it reports attribute 2 for, are unknown. The coefficient memory size is 0 because the host decoder needs no coefficient buffer; the size the console reports for progressive images is unknown. `max_image_width` is not checked, and the packed `width << 16 | height` decode result is libScePngDec's + +### Functional + +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1 throw diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md new file mode 100644 index 00000000000..4da9e097aac --- /dev/null +++ b/docs/dev/debt/libSceNetCtl.md @@ -0,0 +1,5 @@ +# libSceNetCtl technical debt + +### Unknown function info + +- [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The Wi-Fi codes (5-10), the DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server and port (20, 21) throw diff --git a/docs/dev/debt/libSceRudp.md b/docs/dev/debt/libSceRudp.md index f2e4d7383b8..ec0e81990a0 100644 --- a/docs/dev/debt/libSceRudp.md +++ b/docs/dev/debt/libSceRudp.md @@ -4,9 +4,10 @@ - [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 +- [sceRudpEnableInternalIOThread and sceRudpEnableInternalIOThread2](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - return `RUDP_ERROR_NOT_INITIALIZED` before `sceRudpInit` and success after it without starting a thread: no context ever holds pending data to process (see `sceRudpProcessEvents` above), so the stack size, priority and affinity are not applied and no argument is validated. A second enable within one `sceRudpInit`/`sceRudpEnd` cycle, by either call, is not modelled and also returns success: the [Vita3K fork](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/d8a78af6ff734f01becb0876083a1c5d7d2630f8/vita3k/modules/SceRudp/SceLibRudp.cpp#L1013-L1014) cited below returns `SCE_RUDP_ERROR_THREAD_IN_USE` ([`0x80770010`](https://github.com/AndnixSH/Vita3K-PS-Vita-OS-Fork/blob/d8a78af6ff734f01becb0876083a1c5d7d2630f8/vita3k/modules/SceRudp/SceLibRudp.cpp#L49)) from its `sceRudpEnableInternalIOThread` once its thread has started (its `sceRudpEnableInternalIOThread2` is unimplemented), and the console result is unknown. The second call's `(uint32_t stackSize, uint32_t priority, SceKernelCpumask affinityMask)` signature is the PS4 one from the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L1970) IDA type database; the PS5 signature is unverified ### Unknown function info - [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. - [sceRudpGetMaxSegmentSize](../../../core/libs/prx/libSceRudp/Export.cpp) (libSceRudp) - measured on a PS5 (firmware 13.60) in #3073: before `sceRudpInit` it returns `RUDP_ERROR_NOT_INITIALIZED` and leaves the value untouched, also for a null pointer; after it a null pointer returns `RUDP_ERROR_INVALID_ARGUMENT` and a valid one gets 1410 with result 0. The library keeps no configurable segment size (there is no `sceRudpSetMaxSegmentSize`), so 1410 is always reported. The `uint16_t*` parameter is inferred: the measurement does not show how many bytes the console writes, PPSA03490 passes a pointer to a 16-bit value and [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellRudp.cpp) declares the PS3 `cellRudpSetMaxSegmentSize(u16 mss)`, so only 16 bits are written -- 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 +- PPSA06888 imports declared without parameters, signatures unknown: [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 diff --git a/docs/dev/debt/libSceSaveData.native.md b/docs/dev/debt/libSceSaveData.native.md index 6759fd2a9e9..ca4be61ba75 100644 --- a/docs/dev/debt/libSceSaveData.native.md +++ b/docs/dev/debt/libSceSaveData.native.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceSaveDataUnknown_RvID8BVeg](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. No name in aerolib or among the PS4-migration variants tried with `tools/nid_names.py` hashes to it. +- [sceSaveDataPrepareForTransferring](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native, NID `RvID8BVeg+g`) - declared without parameters as a throwing stub; Minecraft (PPSA17221) imports it beside `sceSaveDataTransferringMountPs4` and `sceSaveDataDirNameSearchPs4` and fails to load without it. The name hashes to the NID but is not in aerolib; it was found by combining words from the libSceSaveData names in aerolib. Its arguments and behaviour are unknown. - [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 - [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 - [sceSaveDataGetConvertProgress](../../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - imported by PPSA16064; signature and error codes follow the [SDK 12.000 reference](https://github.com/peterpuppy/bm/blob/3f9d1ba6b896a53080460c903f5c15af8affb80d/features/ps5/document/output/psn_12/System/Save_Data/SaveData-Reference/sce-save-data-get-convert-progress.md) (`float* progress`). Save data conversion on host filesystems completes synchronously, so the progress query returns `SAVE_DATA_OK` with the last conversion's progress (`0.0f` before any conversion or when conversion fails, `1.0f` after successful conversion), `SAVE_DATA_ERROR_PARAMETER` if `progress` is null, or `SAVE_DATA_ERROR_NOT_INITIALIZED` when the library is uninitialized; intermediate asynchronous conversion progress on console is unmeasured diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6eaefa8c882..531d36a7644 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -99,6 +99,7 @@ - [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 - [sceKernelIsThreadSanitizerEnabled](../../../core/libs/prx/libkernel/System/src/Sanitizer.cpp) (libkernel, NID `LzoM-wVLJDE`) - no source gives the signature; `int (void)` is assumed from `sceKernelIsAddressSanitizerEnabled` and from PPSA23226's bundled `libSceJobManager_nosubmission.prx`, which passes no arguments and tests only the 32-bit result - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - throws: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and neither value is known for the console, where `getrlimit(RLIMIT_NOFILE)` throws too. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed ### Functional @@ -110,6 +111,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` 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 - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [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 +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target, a junction and any other reparse point throw, as for `readlink` - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - 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. - [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 @@ -133,7 +135,7 @@ - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [pthread_sigmask](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) shares the process-wide mask of `sigprocmask` and `_sigprocmask`, which keeps only the first 32 signals; FreeBSD keeps a mask per thread - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) reaches a thread that waits in an alertable `TimedWait` wait through an APC. A thread blocked anywhere else, including all waits under `APS5_COARSE_TIMED_WAITS` and host waits inside the emulator, is redirected and runs the handler only when that wait returns -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler wherever the target thread was interrupted. It waits only while the thread is inside winpthreads or restoring a context through `NtContinue`, and throws if that lasts 1 s, so a handler that calls into other host code the thread was interrupted in, such as the C runtime's heap or a lock of the emulator, can deadlock on it or reenter it. A thread that holds the address-wait lock of `sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake` and `_umtx_op`, or the park queue lock of the guest mutexes, gets the raise as an APC instead, which runs once it has released the lock or while it waits; the other locks of the emulator are not deferred this way - [sigaltstack](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records each thread's alternate signal stack and reports it back, following FreeBSD's `kern_sigaltstack` (`EINVAL` for flags other than `SS_DISABLE`, `ENOMEM` below `MINSIGSTKSZ` 2048), but handlers never run on it, so `SS_ONSTACK` and `EPERM` are never reported - [sigpending](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) reports the intersection of the process-wide pending set of `sigtimedwait` and the blocked mask of `_sigprocmask`, as FreeBSD's `sys_sigpending` intersects the pending list with `td_sigmask`, and returns -1 with `EFAULT` for a null set. Because `_sigprocmask` keeps only the first 32 signals, signals 33 to 128 are never blocked and never appear in the set, and setting `SIG_IGN` (or `SIG_DFL` on a signal whose default action is to ignore) does not clear a signal that was already marked pending. PPSA99420 imports it - [scePthreadJoin, scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - on a PS5 (firmware 13.60) joining a thread a second time returns `ESRCH` (`0x80020003`); here a joined thread is released, so a second join reads freed memory. A null entry crashed the payload and still throws. Joining the calling thread returns `EDEADLK` and a zeroed attribute creates a default thread, as on the console @@ -155,6 +157,7 @@ - [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. - [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. +- [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows these calls still fail with `EOPNOTSUPP` for it - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [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. `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 - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 92d3cea3b1f..af70f7d4102 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -4,6 +4,8 @@ - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. +- The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. + ### Unknown function info - [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. The sample index is the SampleId builtin, an equality inferred, not measured. The render target index is the Layer builtin when the draw exports it (a layered draw, `PA_CL_VS_OUT_CNTL` bit 18; the pipeline specialization constant `LayerExported`) and 0 otherwise, the first layer Vulkan renders when no pre-rasterization stage writes Layer. Reading the unwritten Layer builtin instead makes AMD's Windows driver drop the fragment's writes (#4800); with the constant at 0 the load is folded away. Mesa reads both fields from those bits, with the layer 14 bits wide only from GFX12 on ([ac_nir_lower_intrinsics_to_args.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/amd/common/nir/ac_nir_lower_intrinsics_to_args.c#L342-L368)). A value passed through a select, move or phi whose every read is an extract inside one of those fields is rebuilt from the two fields alone. Any other read the masked-select pass cannot drop gets the whole word: the primitive class at bits 0-1 (0 point, 1 line, 2 triangle, 3 rectangle), which the draw sets as a pipeline specialization constant ([RasterPrimitiveOf](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp)) in the shaders that read it, plus the sample field, with every other bit zero. That layout comes from the RDNA 3 and 3.5 ISA guides (prim type 1:0, VRS rates 3:2 and 5:4, eye id 7, sample 11:8, render target index 28:16). The GCN register guides give the same prim type and sample fields and put the render target index at 26:16, so the two sources agree for layers below 2048. AMD's LLPC reads the primitive class from bits 1:0 with the same encoding ([LowerGlCompatibility.cpp](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/llpc/lowering/LowerGlCompatibility.cpp#L830-L836)) and without a GFX-level check ([getPrimType](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/LowerInOut.cpp#L5307-L5315)). Nobody has checked it on gfx1013. In the whole word only bits 0-1 and 8-11 have a source. Bits 2-7, 12-15 and 29-31 are assumed zero, not measured: the VRS rates (2-5) and the eye id (7) because the driver supports neither variable-rate shading nor multiview, so a title that enables either would read other bits on the console, and bits 6, 12-15 and 29-31 because no source gives them a meaning. The sample field reads SampleId, which is always 0 here because the driver rejects MSAA and per-sample shading (`PA_SC_AA_CONFIG`, `PS_ITER_SAMPLE`). The whole word has the same 0 at bits 16-28. A change that makes draws export a layer must restore the Layer read in both places @@ -104,7 +106,7 @@ - [VGT_HOS_MIN/MAX_TESS_LEVEL](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) clamp positive tessellation factors (inner ones to at least 1); outer factors that are zero, negative or NaN still cull the patch and such inner factors become max(HOS_MIN, 1). The console's clamp and cull order is not measured. - [Patch constants](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull programs write and domain programs read are placed by their off-chip address modulo the record stride, as the two compile apart; a domain read of a field the hull never writes, or of only a neighbouring patch's record, is not detected. - [Integer partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 0) is drawn with Vulkan's equal spacing, which [radeonsi programs as `PART_INTEGER`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L539-540), with factors bounded at 64 instead of fractional-odd's 63; how the console rounds a clamped factor up to a whole number of segments is not measured. Power-of-two (1) and fractional-even (3) partitioning throw. -- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, as if each primitive group and each instance started new threadgroups; it throws when GE_CNTL.BREAK_WAVE_AT_EOI is clear. Unmeasured; PPSA26344 programs 64 primitives for 63 patches. +- The [relative patch index](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) that hull and domain programs use as data is (PrimitiveID mod PRIM_GRP_SIZE) mod NUM_PATCHES, with the draw-relative instance times the patches per instance added when GE_CNTL.BREAK_WAVE_AT_EOI is clear and the instances share hull threadgroups; a domain that uses it in such a draw, and a hull without shaderDrawParameters, throw. Unmeasured on hardware; PPSA26344 programs 64 primitives for 63 patches. - The [output modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the shader's float mode as measured on RDNA2 (gfx1035): the hardware applies it only with IEEE mode off and output denormals of the result's precision flushed (bit 1 of the `FLOAT_MODE` denormal field clear), and ignores it otherwise. On f32 results it is applied as the hardware does with f32 denormals flushed; a shader without a known float mode is translated with IEEE mode off and f32 denormals flushed, the mode titles use. On f16 and f64 results it is ignored, and it throws in the modes where the hardware applies it. - 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). diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a6..30325338665 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -1,8 +1,8 @@ # Controller, keyboard and mouse input -AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. +AnyPS5 accepts SDL-mapped game controllers as well as its built-in keyboard and mouse bindings. Xbox-style controllers and PlayStation controllers recognized by SDL use the standard PS button layout; sticks and analog triggers are passed through, and controllers can be connected or disconnected while the game is running. The first recognized controller is used. AnyPS5 uses its built-in keyboard and mouse bindings when no configuration file is present. To change selected keyboard and mouse bindings, create `anyps5-input.ini` beside the generated game executable. Set `ANYPS5_INPUT_CONFIG` to use a file at another path. Both the game directory and that path may contain any characters, such as `ü` or `日本`. -For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, the touchpad click button is forwarded; finger touch coordinates are not implemented. +For an Xbox-layout controller, A/B/X/Y map to Cross/Circle/Square/Triangle, LB/RB map to L1/R1, and LT/RT map to analog L2/R2. Start maps to Options. D-pad and stick-click buttons are supported. On a PlayStation controller, touchpad clicks and the positions of up to two fingers on the touchpad are forwarded. On other controllers, View (Back) presses the touchpad at its center. Each non-empty line has the form `Action = Type:Value`. Action names are case-insensitive. A `#` or `;` starts a comment. The first line for an action replaces its built-in bindings; later lines for the same action add alternate inputs. Duplicate entries for the same action are ignored. Actions omitted from the file keep their built-in bindings. UTF-8 files with or without a byte-order mark are supported. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 1e736e2820f..d21e304eb09 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -94,11 +94,12 @@ libs/ *.prx app0/ + eboot.bin sce_module/ ``` -Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. +Use `sce_modules/` or `prx/` instead of `sce_module/` if that is the input directory name. Relinker preserves each module's directory under `app0/` and prints its exact path. Place app resources in `app0/` separately, including the title's original `eboot.bin`: titles test `/app0/eboot.bin` to find their own directory, as it is always present on the console (ProsperoTV, ProsperoRadio, ProsperoLight and ProsperoLichess fall back to an empty `/data/homebrew/` and fail to load their fonts without it). Only the converted executable runs. Copy the built system libraries from `build/core/libs/libs/*.prx` into `libs/`; use libraries built for the target OS. A custom `--rpath` changes the system library location. Use the generated files printed as `Guest module:` for bundled title modules. `libs/` is for AnyPS5 system libraries, not the original PS5 `.prx` files. Placing an original PS5 module in `libs/` on Windows makes Windows try to load it as a DLL and can fail with error 193 (not a valid Win32 application). @@ -161,6 +162,8 @@ $env:VK_LOADER_DRIVERS_DISABLE = '*dzn*' 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)). +On Windows, NVIDIA driver 610.47 can crash in its shader compiler while pipelines are created: the log ends in `FATAL: unhandled exception 0xc0000005` with `nvgpucomp64.dll` in `rip`, on a driver thread. PPSA25344 on an RTX 3060 stopped this way in 3 of 3 New Game loads, also with `APS5_NO_PIPELINE_LTO=1`; with driver 617.42 the same build and scene ran for 40 minutes. Update the driver when the crash is in `nvgpucomp64.dll`. + ### HDR output HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. diff --git a/tools/check_conventions.py b/tools/check_conventions.py index ee3f7d05279..7bac33573a0 100644 --- a/tools/check_conventions.py +++ b/tools/check_conventions.py @@ -46,7 +46,9 @@ SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules\b|pkg_search_module\b|find_(library|path|program|file)\b)", re.I) TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") -LINK = re.compile(r"\]\(([^)\s]+)") +LINK = re.compile(r"\[[^\]]+\]\(([^)\s]+)") +FENCE = re.compile(r"^ {0,3}(```|~~~).*?^ {0,3}\1[ \t]*$|^ {0,3}(?:```|~~~).*\Z", re.S | re.M) +INLINE = re.compile(r"`[^`\n]*`") SECTION = re.compile(r"^###(?!#)\s*(.+?)\s*$", re.M) KEY = re.compile(r"^( *)([A-Za-z0-9_-]+|'[^']*'|\"[^\"]*\"):(?:\s+(.*?))?\s*$") @@ -205,10 +207,13 @@ def repository(self): def docs(self): for path, lines in self.files(lambda p: p.suffix == ".md"): + fenced = fenced_lines(self.show(path)) for number, text in lines: name = debt_name(text) if is_debt(path) and text.startswith("- ") else None if name and path != f"{DEBT_DIR}{name}.md": self.report("debt-file", path, number, f"{DEBT_DIR}{name}.md") + if number in fenced: + continue for target, resolved in links(path, text): if resolved not in self.tree and resolved not in self.dirs: self.report("doc-link", path, number, target) @@ -258,8 +263,17 @@ def utf8(data): return True +def fenced_lines(text): + inside = set() + for match in FENCE.finditer(text): + first = text.count("\n", 0, match.start()) + 1 + inside.update(range(first, first + match.group(0).count("\n") + 1)) + return inside + + def links(path, text): result = [] + text = INLINE.sub("", FENCE.sub("", text)) for target in LINK.findall(text): if re.match(r"[A-Za-z][A-Za-z0-9+.-]*:|//|#", target): continue diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7b00d94d3e1..a496b7eadaa 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -1,11 +1,14 @@ import argparse import re import subprocess +import sys from pathlib import Path TEST_OUTPUT = r"tests[\\/]" COMMAND = re.compile(r"^\s*(add_test|set_tests_properties|subdirs)\s*\(", re.M | re.I) ARGUMENT = re.compile(r'"(?:\\.|[^"\\])*"|\[(?P<equals>=*)\[.*?\](?P=equals)\]|[^\s()]+', re.S) +LEXICAL = re.compile(r'\\.|"(?:\\.|[^"\\])*"|\[(?P<literal_equals>=*)\[.*?\](?P=literal_equals)\]' + r'|(?P<comment>\#\[(?P<comment_equals>=*)\[.*?\](?P=comment_equals)\]|\#[^\n]*)', re.S) KNOWN_UNRUN = set() @@ -21,8 +24,16 @@ def built(build): raise ValueError(f"test registration audit requires a Ninja generator, got {generator!r}") if not program or program.endswith("-NOTFOUND"): raise FileNotFoundError(f"CMAKE_MAKE_PROGRAM is not set in {Path(build) / 'CMakeCache.txt'}") + encoding = "utf-8" + if sys.platform == "win32": + codepage = subprocess.run([program, "-t", "wincodepage"], capture_output=True, text=True, + encoding="ascii", check=True).stdout.strip() + encodings = {"Build file encoding: UTF-8": "utf-8", "Build file encoding: ANSI": "mbcs"} + if codepage not in encodings: + raise ValueError(f"unsupported Ninja build-file encoding: {codepage!r}") + encoding = encodings[codepage] listed = subprocess.run([program, "-C", str(build), "-t", "targets", "all"], - capture_output=True, text=True, check=True).stdout + capture_output=True, text=True, encoding=encoding, check=True).stdout names = set() for line in listed.splitlines(): path, _, rule = line.partition(": ") @@ -40,6 +51,8 @@ def built(build): def commands(content): + content = LEXICAL.sub(lambda match: re.sub(r"[^\n]", " ", match.group()) + if match.group("comment") is not None else match.group(), content) parsed_until = 0 for command in COMMAND.finditer(content): if command.start() < parsed_until: diff --git a/tools/progress_history.py b/tools/progress_history.py index 22818d0cd3a..83b9bc76a2d 100644 --- a/tools/progress_history.py +++ b/tools/progress_history.py @@ -35,7 +35,11 @@ def measure(sha): with tempfile.TemporaryDirectory() as directory: root = Path(directory) try: - tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))).extractall(root, filter="data") + archive = tarfile.open(fileobj=io.BytesIO(git("archive", sha, "--", *paths))) + if hasattr(tarfile, "data_filter"): + archive.extractall(root, filter="data") + else: + archive.extractall(root) except subprocess.CalledProcessError: return None saved = progress.PRX, progress.OPCODES diff --git a/tools/stubgen.py b/tools/stubgen.py index 2c296b9298e..6fd72e6cef7 100644 --- a/tools/stubgen.py +++ b/tools/stubgen.py @@ -197,7 +197,11 @@ def write_outputs(outputs): temporary.replace(path) finally: for temporary, path in staged: - temporary.unlink(missing_ok=True) + try: + temporary.unlink(missing_ok=True) + except PermissionError: + temporary.chmod(0o600) + temporary.unlink(missing_ok=True) def main(argv=None): diff --git a/tools/tests/test_check_conventions_doc_links.py b/tools/tests/test_check_conventions_doc_links.py new file mode 100644 index 00000000000..d230f88a30e --- /dev/null +++ b/tools/tests/test_check_conventions_doc_links.py @@ -0,0 +1,29 @@ +from pathlib import Path +import sys +import unittest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import check_conventions + +FENCE = "`" * 3 + + +class DocLinkTests(unittest.TestCase): + def test_code_blocks_and_inline_code_have_no_links(self): + text = f"{FENCE}cpp\nauto f = [](int a) {{ return a; }};\n{FENCE}\n~~~\nauto g = [](int b) {{}};\n~~~\nuse `[](auto)` here\n" + self.assertEqual(check_conventions.links("docs/x.md", text), []) + + def test_real_link_outside_block_is_kept(self): + text = f"{FENCE}\n[](int a)\n{FENCE}\nsee [guide](../README.md)\n" + self.assertEqual(check_conventions.links("docs/x.md", text), [("../README.md", "README.md")]) + + def test_empty_link_text_is_ignored(self): + self.assertEqual(check_conventions.links("docs/x.md", "![](img.png) and [](empty.md)"), []) + + def test_fenced_lines(self): + text = f"a\n{FENCE}\n[](int a)\n{FENCE}\nb\n" + self.assertEqual(check_conventions.fenced_lines(text), {2, 3, 4}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/tests/test_stubgen.py b/tools/tests/test_stubgen.py index 66ab9bd1b36..cfda701a6b4 100644 --- a/tools/tests/test_stubgen.py +++ b/tools/tests/test_stubgen.py @@ -1,6 +1,7 @@ import contextlib import io import os +import stat import tempfile from pathlib import Path import sys @@ -121,5 +122,43 @@ def test_valid_generation_keeps_the_code_and_debt(self): self.assertEqual(code.stat().st_mode & 0o777, 0o666 & ~mask) +class ReadOnlyOutputTests(unittest.TestCase): + def test_later_staging_failure_preserves_original_error_and_cleans_readonly_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + errors = io.StringIO() + arguments = ["--module", "libSceExample", "--nids", str(nids), "--out", str(output), + "--td-out", str(root / "absent" / "debt.txt")] + with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as error: + stubgen.main(arguments) + self.assertEqual(error.exception.code, 2) + self.assertIn("absent", errors.getvalue()) + self.assertNotIn("Export.cpp.", errors.getvalue()) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + @unittest.skipUnless(os.name == "nt", "Windows denies replacing read-only output files") + def test_denied_replace_preserves_readonly_output_without_leaking_temporary(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + nids, output = root / "nids.txt", root / "Export.cpp" + nids.write_text("AAAAAAAAAAA\n", encoding="utf-8") + original = "existing source\n" + output.write_text(original, encoding="utf-8") + output.chmod(0o444) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as error: + stubgen.main(["--module", "libSceExample", "--nids", str(nids), "--out", str(output)]) + self.assertEqual(error.exception.code, 2) + self.assertEqual(output.read_text(encoding="utf-8"), original) + self.assertFalse(output.stat().st_mode & stat.S_IWRITE) + self.assertEqual(sorted(path.name for path in root.iterdir()), ["Export.cpp", "nids.txt"]) + + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index e9862c2bc3d..59056d06dd2 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -18,6 +18,28 @@ def registered(self, content): (build / "CTestTestfile.cmake").write_text(content, encoding="utf-8") return check_test_registration.registered(build) + def test_bracket_comments_do_not_register_tests(self): + for equals in ("", "=", "==="): + with self.subTest(equals=equals): + content = (f"#[{equals}[\nadd_test(ghost /build/tests/missing_tests)\n]{equals}]\n" + 'add_test("real" "/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_does_not_disable_a_registered_test(self): + content = ('add_test("real" "/build/tests/real_tests")\n' + '#[=[\nset_tests_properties("real" PROPERTIES DISABLED TRUE)\n]=]\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_bracket_comment_inside_arguments_does_not_end_the_command(self): + content = ('add_test("real" #[=[ )\nadd_test(ghost /build/tests/missing_tests)\n]=]\n' + '"/build/tests/real_tests")\n') + self.assertEqual(self.registered(content), {"real_tests"}) + + def test_comment_markers_inside_literals_remain_arguments(self): + content = ('add_test("real" "/build/tests/real_tests" "#[[" [=[# comment]=] "\\\\#")\n') + self.assertEqual(list(check_test_registration.commands(content)), + [("add_test", ["real", "/build/tests/real_tests", "#[[", "# comment", "\\#"])]) + def test_test_name_does_not_register_executable(self): names = self.registered('add_test("missing_tests" "/usr/bin/python3" "unrelated.py")\n') self.assertNotIn("missing_tests", names) @@ -41,7 +63,8 @@ def test_hyphenated_executable_is_discovered(self): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer-bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -50,7 +73,8 @@ def test_nested_test_output_is_discovered(self): with tempfile.TemporaryDirectory() as directory: build = Path(directory) (build / "CMakeCache.txt").write_text("CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja Multi-Config\n", encoding="utf-8") - with patch.object(check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout=output)) as run: + with patch.object(check_test_registration.subprocess, "run", side_effect=lambda command, **kwargs: SimpleNamespace( + stdout="Build file encoding: UTF-8\n" if command[-1] == "wincodepage" else output)) as run: self.assertEqual(check_test_registration.built(build), {"buffer_bounds_tests"}) self.assertEqual(run.call_args[0][0][0], "/configured/ninja") @@ -151,5 +175,39 @@ def test_missing_make_program_never_runs_build_tool(self): check_test_registration.built(build) run.assert_not_called() + @unittest.skipIf(shutil.which("ninja") is None, "requires a local Ninja to exercise its real output encoding") + def test_ninja_output_encoding_with_a_cp1252_default(self): + program = shutil.which("ninja") + actual_run = check_test_registration.subprocess.run + for encoding in (("utf-8", "mbcs") if sys.platform == "win32" else ("utf-8",)): + with self.subTest(encoding=encoding), tempfile.TemporaryDirectory() as directory: + build = Path(directory) + name = "caf\u00e9_tests".encode(encoding, errors="ignore").decode(encoding) + (build / "CMakeCache.txt").write_text( + f"CMAKE_MAKE_PROGRAM:FILEPATH={program}\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + (build / "build.ninja").write_text( + "rule CXX_EXECUTABLE_LINKER\n command = true\n" + f"build tests/{name}: CXX_EXECUTABLE_LINKER\n", encoding=encoding) + + def run(command, **kwargs): + if command[-1] == "wincodepage": + return SimpleNamespace(stdout="Build file encoding: " + ("ANSI" if encoding == "mbcs" else "UTF-8")) + kwargs.setdefault("encoding", "cp1252") + return actual_run(command, **kwargs) + + with patch.object(check_test_registration.subprocess, "run", side_effect=run): + self.assertEqual(check_test_registration.built(build), {name}) + + def test_unknown_windows_ninja_encoding_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + build = Path(directory) + (build / "CMakeCache.txt").write_text( + "CMAKE_MAKE_PROGRAM:FILEPATH=/configured/ninja\nCMAKE_GENERATOR:INTERNAL=Ninja\n", encoding="utf-8") + with patch.object(check_test_registration.sys, "platform", "win32"), patch.object( + check_test_registration.subprocess, "run", return_value=SimpleNamespace(stdout="unknown")) as run: + with self.assertRaisesRegex(ValueError, "unsupported Ninja build-file encoding"): + check_test_registration.built(build) + run.assert_called_once() + if __name__ == "__main__": unittest.main()