diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 0427642d2b..d01372483d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -692,6 +692,15 @@ if(BUILD_TESTING) add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests) endif() +add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp) +target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) +set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_float32_flush_control_tests) +if(BUILD_TESTING) + set_target_properties(agc_float32_flush_control_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_float32_flush_control COMMAND agc_float32_flush_control_tests) +endif() + add_test_executable(agc_pipeline_link_optimization_tests EXCLUDE_FROM_ALL tests/PipelineLinkOptimization.cpp) target_include_directories(agc_pipeline_link_optimization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR}) set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -811,6 +820,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp) +target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f32_denorm_preserve_tests) +if(BUILD_TESTING) + set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -981,6 +1003,10 @@ add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1Con target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_vop1_control_decode_tests) +add_test_executable(agc_vertex_input_validation_tests EXCLUDE_FROM_ALL tests/VertexInputValidation.cpp) +target_link_libraries(agc_vertex_input_validation_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_vertex_input_validation_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_vertex_input_validation_tests) add_test_executable(agc_vop2_encoding_decode_tests EXCLUDE_FROM_ALL tests/Vop2EncodingDecode.cpp) target_link_libraries(agc_vop2_encoding_decode_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES CXX_EXTENSIONS OFF) @@ -1389,6 +1415,11 @@ if(BUILD_TESTING) set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only) + set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 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) @@ -1396,6 +1427,12 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only) + add_test(NAME agc_storage_clear_hand_over COMMAND agc_driver_recorder_tests --clear-hand-over-only) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_storage_clear_hand_over PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() 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) if(WIN32) @@ -1587,6 +1624,7 @@ target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_D target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc) set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_host_import_retire_tests) +target_sources(agc_host_import_retire_tests PRIVATE tests/HostImportFailure.cpp) if(BUILD_TESTING) set_target_properties(agc_host_import_retire_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_host_import_retire COMMAND agc_host_import_retire_tests) @@ -1796,6 +1834,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL tests/BdaShader.cpp tests/BdaExecution.cpp tests/BdaContracts.cpp + tests/HostImportFailure.cpp ) target_include_directories(agc_driver_bda_device_tests PRIVATE @@ -1807,6 +1846,15 @@ target_include_directories(agc_driver_bda_device_tests PRIVATE target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc) set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_driver_bda_device_tests) +if(BUILD_TESTING) + add_test(NAME agc_driver_host_import_failure_handles COMMAND agc_driver_bda_device_tests --host-import-failures) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_host_import_failure_handles PROPERTIES + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp) target_include_directories(agc_driver_mesh_tests PRIVATE @@ -1854,6 +1902,14 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution") endif() endforeach() +if(BUILD_TESTING) + add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() if(ANYPS5_ENABLE_SPIRV_TOOLS) target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1) target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static) @@ -2091,7 +2147,7 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_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_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_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 agc_f16_single_convert_tests) + foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_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_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_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 agc_f16_single_convert_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() @@ -2151,6 +2207,27 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(Python3_Interpreter_FOUND) + set(prebuiltDiscoveryModes plain relative plain-long) + if(WIN32) + list(APPEND prebuiltDiscoveryModes extended extended-forward device extended-long) + endif() + foreach(prebuiltDiscoveryMode IN LISTS prebuiltDiscoveryModes) + set(prebuiltDiscoveryTest agc_shader_prebuilt_discovery_${prebuiltDiscoveryMode}) + add_test(NAME ${prebuiltDiscoveryTest} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_prebuilt_discovery.py + $ ${prebuiltDiscoveryMode}) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() + endforeach() + endif() + add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() if(WIN32) add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint) set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30 @@ -2408,9 +2485,11 @@ if(BUILD_TESTING) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) - set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced) + add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed) + set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex @@ -2472,8 +2551,10 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1") + add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests) + set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp index 75efb72f56..5f3df6c538 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp @@ -45,6 +45,7 @@ struct DispatchVariant { std::vector runBases; std::uint64_t relocatableWords = 0; std::vector> shaderDataPatches; + std::uint64_t userDataMask = ~std::uint64_t{0}; std::shared_ptr vertexInfo; std::shared_ptr memory; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp index 0ad9ba5903..3e1f59e9f4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp @@ -126,6 +126,7 @@ struct StageCapture { std::uint64_t forgetSerial = 0; std::uint32_t pushOffset = 0; ShaderRecompiler::SrtUserDataUse userDataUse; + std::uint64_t userDataMask = ~std::uint64_t{0}; }; struct RelocatableUserWord { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp new file mode 100644 index 0000000000..0d93c2c2a4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP + +#include + +namespace AgcDriver { + +struct Float32FlushControl { + bool probe = false; + bool declared = false; +}; + +inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) { + const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE; + return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared}; +} + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 19b1b24e22..5012ff4ab4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -11,6 +11,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include #include #include #include @@ -45,6 +46,7 @@ public: // Starts the journal replay if the device has not (a headless device starts it at its first // draw or dispatch) and joins it (tests; the teardown joins it anyway). void WaitForPipelineJournalReplay(); + void SavePipelineCache(); // Compute pipelines this device built, by dispatches and by the journal replay (tests). std::uint64_t ComputePipelinesCreated() const; ~VulkanDevice(); @@ -274,7 +276,7 @@ public: int cpuReason; double argumentReadMs; }; - IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr); + IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr prepared = nullptr, std::shared_ptr* recipe = nullptr, const std::array& threadGroup = {}); // A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps. // PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's // device is this one, its template and pipeline objects still alive), the template's @@ -296,7 +298,7 @@ public: // with the recipe's, else the process aborts. `refreshByWords`: the driver's data-only hit // (`shader` carries live flat-SRT words the recipe's hash does not name), so the template's // data refresh is decided by the per-word compare instead of the hash. - RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false); + RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify = nullptr, bool refreshByWords = false, const std::array& threadGroup = {}); // Whether dispatch-cache hits use recipes (APS5_NO_DISPATCH_RECIPE unset) and whether every // hit is verified (APS5_VERIFY_RECIPE=1). static bool DispatchRecipes(); @@ -352,7 +354,7 @@ private: ShaderRecompiler::SpirvTarget buildTarget() const; Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. - IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe); + IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp index ec282a171f..4aa7625ba9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp @@ -5,6 +5,7 @@ namespace AgcDriver { // Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file. void StartWorkerSampler(); +void StartCommitterSampler(); void StopWorkerSampler(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp index 124d638998..22865df487 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp @@ -23,7 +23,11 @@ public: if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false; const auto first = (address - base) / BlockBytes; const auto last = (address - base + bytes - 1) / BlockBytes; - return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; }); + const auto firstPage = (address - base) % BlockBytes / PageBytes; + const auto lastPage = (address - base + bytes - 1) % BlockBytes / PageBytes; + if (first == last) return (excluded[first] & pageMask(firstPage, lastPage + 1)) == 0; + if ((excluded[first] & pageMask(firstPage, PagesPerBlock)) != 0 || (excluded[last] & pageMask(0, lastPage + 1)) != 0) return false; + return std::none_of(excluded.begin() + first + 1, excluded.begin() + last, [](auto mask) { return mask != 0; }); } bool ExcludedSpan(std::uint64_t address, std::size_t bytes, std::uint64_t* begin, std::uint64_t* end) const { @@ -40,10 +44,12 @@ public: if (!excludedBlocks(address, bytes, first, last)) return false; bool changed = false; for (auto block = first; block <= last; ++block) { - changed = changed || excluded[block] == 0; + const auto pages = (std::min(BlockBytes, size - block * BlockBytes) + PageBytes - 1) / PageBytes; + const auto mask = pageMask(0, pages); + changed = changed || excluded[block] != mask; auto& stamp = excludedGenerations[block]; stamp = std::max(stamp, std::max(generation, 1)); - excluded[block] = 1; + excluded[block] = mask; } return changed; } @@ -58,21 +64,28 @@ public: const auto offset = address - base; if (offset >= size) return false; bytes = static_cast(std::min(bytes, size - offset)); - const auto first = offset / BlockBytes + (offset % BlockBytes != 0); + const auto firstPage = offset / PageBytes + (offset % PageBytes != 0); const auto stop = offset + bytes; - const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0); + const auto endPage = stop / PageBytes + (stop == size && stop % PageBytes != 0); + if (firstPage >= endPage) return false; + const auto first = firstPage / PagesPerBlock; + const auto last = (endPage - 1) / PagesPerBlock; bool changed = false; - for (auto block = first; block < end; ++block) { - if (excluded[block] != 0 && excludedGenerations.at(block) < generation) { + for (auto block = first; block <= last; ++block) { + const auto mask = pageMask(block == first ? firstPage % PagesPerBlock : 0, block == last ? (endPage - 1) % PagesPerBlock + 1 : PagesPerBlock); + if ((excluded[block] & mask) != 0 && excludedGenerations.at(block) < generation) { changed = true; - excluded[block] = 0; - excludedGenerations.erase(block); + excluded[block] &= static_cast(~mask); + if (excluded[block] == 0) excludedGenerations.erase(block); } } return changed; } private: + static std::uint16_t pageMask(std::size_t first, std::size_t end) { + return static_cast(((1u << end) - 1u) & ~((1u << first) - 1u)); + } bool excludedBlocks(std::uint64_t address, std::size_t bytes, std::size_t& first, std::size_t& last) const { if (bytes == 0 || size == 0) return false; if (address < base) { @@ -88,9 +101,11 @@ private: return true; } static constexpr std::size_t BlockBytes = 65536; + static constexpr std::size_t PageBytes = 4096; + static constexpr std::size_t PagesPerBlock = BlockBytes / PageBytes; std::uint64_t base = 0; std::size_t size = 0; - std::vector excluded; + std::vector excluded; std::unordered_map excludedGenerations; }; 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 b840a0df06..7bba71169d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -147,11 +147,16 @@ void Driver::dispatch(QueueState& queue, std::span packet, } } if (partialLastGroup) compute.partialThreads = partialGroups; + std::array threadGroup{}; + if (indirectArguments != 0 && (packet[4] & 0x20u) != 0) { + for (std::uint32_t axis = 0; axis < 3; ++axis) threadGroup[axis] = std::max(readRegister(queue.shader, 0x207 + axis) & 0xffffu, 1u); + compute.partialThreads = {~0u, ~0u, ~0u}; + } ShaderRecompiler::RecompileRequest request{ {ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header}, {(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)}, localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u), - {0, 0, 0, 128} + {0, 0, 0, threadGroup != std::array{} ? 0u : 128u} }; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; static double captureMs = 0, keyMs = 0, recompileMs = 0, deviceMs = 0; @@ -572,7 +577,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, try { if (recipeHit != nullptr) { VulkanDevice::IndirectOutcome outcome{0, 0}; - const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit); + const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit, threadGroup); if (result == RecipeOutcome::Rebuild) { recipeHit = nullptr; @@ -581,7 +586,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, } if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else if (indirectArguments != 0) { - const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); + const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup); countIndirect(outcome.cpuReason, outcome.argumentReadMs); } else { localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp index cd6ee2d743..65057babfa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp @@ -48,7 +48,8 @@ void Driver::countIndirect(int path, double readMs) { int Driver::indirectArgumentsPath(std::span packet, std::uint64_t arguments) { static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr; if (!gpuIndirect) return IndirectDisabled; - if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions; + static const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + if ((packet.back() & 0x20u) != 0 && !gpuThreadDimensions) return IndirectThreadDimensions; if (arguments % 4 != 0) return IndirectMisaligned; return IndirectGpu; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index 884514bbb8..a79f05c76a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -106,6 +106,14 @@ std::shared_ptr Driver::materializeDraw } stageCapture.capture = capture; timing.Mark("materialize"); + const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase; + auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads; + if (vertexInfos[i]) { + for (const auto reg : {static_cast(vertexInfos[i]->fetchAttribReg), static_cast(vertexInfos[i]->fetchBufferReg)}) { + mask |= reg + 1u < 64u ? std::uint64_t{3} << reg : ~std::uint64_t{0}; + } + } + stageCapture.userDataMask = mask; phaseTiming.Phase(DrawRowRecompile); return stageCapture.compiled; } @@ -160,6 +168,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP variant->shader = programs[i].snapshot; variant->forgetSerial = stageCapture.forgetSerial; variant->pushOffset = stageCapture.pushOffset; + variant->userDataMask = stageCapture.userDataMask; if (vertexInfos[i]) variant->vertexInfo = std::make_shared(*vertexInfos[i]); std::vector regions(stageCapture.regions.begin(), stageCapture.regions.end()); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index b0ae591522..1b694befa3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -215,9 +215,16 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr sampling; for (std::size_t i = 0; i < programs.size(); ++i) { - if (roles[i] == Role::GeometryBack || programs[i].userData != dataEntry->decode->programs[i].userData) continue; + if (roles[i] == Role::GeometryBack || programs[i].userData.size() != dataEntry->decode->programs[i].userData.size()) continue; + const auto& now = programs[i].userData; + const auto& was = dataEntry->decode->programs[i].userData; auto& candidates = dataCandidates[i]; for (const auto& variant : dataEntry->stages[i]) { + bool usedEqual = true; + for (std::size_t w = 0; w < now.size() && usedEqual; ++w) { + if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false; + } + if (!usedEqual) continue; if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue; std::vector regions; appendEntryRegions(*variant, regions); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp index 78de291737..9ba7b4a1db 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp" +#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp" #include #include #include @@ -142,6 +143,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) { void DrawPipeline::run() { PerformanceTrace::BindThread("draw committer"); + StartCommitterSampler(); for (;;) { Commit* commit = nullptr; SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; }); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 1012187b38..04a1f72a02 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,11 +1,13 @@ #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include namespace AgcDriver { void Submit(const Packet* packet, std::uint32_t queue) try { + KernelNoteGpuSubmit_nid_no_patch(); DriverDetail::Driver::Get().Submit(packet, queue); } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 55bff4e2ac..71c479e41e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -183,6 +183,10 @@ Driver::Driver() { InstallAsyncLog_nid_no_patch(); static const bool driverMemory = (MapDriverMemory(), true); static_cast(driverMemory); + LibcSetExitDrain_nid_no_patch([] { + const auto current = Driver::Get().device.Load(); + if (current) current->SavePipelineCache(); + }); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 36d57da9c9..6e7a43a575 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -925,7 +925,7 @@ struct WriteTracker { if (first >= stop) return; bool started = false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (!coverage.Covers(blockBegin(block), 1)) continue; + if (!coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) continue; if (!started) ++generation; started = true; stampHint(block); @@ -935,7 +935,7 @@ struct WriteTracker { bool hinted(std::uint64_t first, std::uint64_t stop) const { if (first >= stop || first < base || stop > base + size) return false; for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) { - if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), 1)) return false; + if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), static_cast(std::min(WriteBlockBytes, base + size - blockBegin(block))))) return false; } return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast(first), static_cast(stop - first)); } @@ -1204,6 +1204,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_ ++tracker.generation; tracker.noteCpuStore(address, address + bytes, tracker.generation); for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { + if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0; if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max(found->second.dropped, tracker.generation); tracker.stamp(block, tracker.generation, StampKind::Cpu); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp index d18386b591..63748fa18c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp" #include #include +#include #include namespace AgcDriver { @@ -24,7 +25,7 @@ bool SameDisplay(const HostDisplayHdr& a, const HostDisplayHdr& b) { void Publish(HostHdrStatus& status, const HostDisplayHdr& display, bool surfaceHdr10) { const bool displayChanged = !SameDisplay(status.display, display); - const bool available = display.hdr && surfaceHdr10; + const bool available = display.hdr && surfaceHdr10 && std::getenv("APS5_FORCE_SDR") == nullptr; const bool availabilityChanged = available != status.available; status.display = display; status.surfaceHdr10 = surfaceHdr10; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index e21ddf0599..ca74a794d9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -538,14 +538,8 @@ PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::si layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex"); - moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV); - moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment"); + vertex = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_VERT_SPV, sizeof(PERFORMANCE_OVERLAY_VERT_SPV), "vkCreateShaderModule performance overlay vertex"); + fragment = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_FRAG_SPV, sizeof(PERFORMANCE_OVERLAY_FRAG_SPV), "vkCreateShaderModule performance overlay fragment"); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp index ecaa69ff9c..e42e4e2b03 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationPass.cpp @@ -70,14 +70,8 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t tableWrites[i].pBufferInfo = &tableInfo; } context.Function("vkUpdateDescriptorSets")(context.device, static_cast(tableWrites.size()), tableWrites.data(), 0, nullptr); - const auto createModule = context.Function("vkCreateShaderModule"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV); - moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex"); - moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV); - moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV; - Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment"); + vertex = Graphics::CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule presentation vertex"); + fragment = Graphics::CreateShaderModule(context, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV), "vkCreateShaderModule presentation fragment"); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9361c6b459..226c792301 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp" #include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp" +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" #include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp" #include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp" #include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp" @@ -321,6 +322,7 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool float32FlushToZeroCandidate = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool probingFloatControls = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; @@ -916,10 +918,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip check(state->DeviceFunction("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay"); result.cacheMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = capture.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay"); + objects.module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule replay"); result.moduleMilliseconds = elapsed(phase); phase = std::chrono::steady_clock::now(); std::array, 2> descriptorCounts{}; @@ -1163,10 +1162,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : std::vector availableExtensions(extensionCount); check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties"); const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); }; + VkDriverId driverId{}; if (hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) { VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); + driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); @@ -1329,9 +1330,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; - state->float32FlushToZeroCandidate = floatControls.shaderDenormFlushToZeroFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; - if (state->float32FlushToZeroCandidate) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); - if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); + state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; + if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); + state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr; + if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -2121,6 +2124,10 @@ void VulkanDevice::WaitForPipelineJournalReplay() { if (state->journalReplay != nullptr) state->journalReplay->Wait(); } +void VulkanDevice::SavePipelineCache() { + if (state->pipelineCache) state->pipelineCache->SaveNow(); +} + std::uint64_t VulkanDevice::ComputePipelinesCreated() const { return state->computePipelinesCreated.load(std::memory_order_relaxed); } @@ -3640,6 +3647,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; + target.float32DenormPreserve = state->float32DenormPreserve; return target; } @@ -4358,6 +4366,8 @@ struct RecordedDispatch { std::shared_ptr bindings{}; std::uint64_t cpuArguments = 0; int cpuReason = 0; + std::array threadGroup{}; + std::array threadLimit{}; }; VkDevice VulkanDevice::Device() const { @@ -4614,8 +4624,8 @@ void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std static_cast(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe)); } -VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe) { - return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe); +VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup) { + return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup); } void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) { @@ -4623,6 +4633,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out auto& timer = *record.timer; const auto& context = *record.context; const auto arguments = record.arguments; + const bool threadDimensions = record.threadGroup != std::array{}; // Decided here, after the resource build (which may retire imports and wait for recorded // work) and before the dispatch itself is recorded, so the import cannot be dropped before // the record. The build already recorded the copy-ins of its misaligned and staged regions @@ -4649,7 +4660,8 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out outcome.cpuReason = 8; } else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) { outcome.cpuReason = 2; - } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) { + } else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr || (threadDimensions && record.argumentImport->address == 0)) { + record.argumentImport = nullptr; // Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held // here, by every Dispatch caller) and Keeps a retired VkBuffer while the recorder is busy. outcome.cpuReason = 3; @@ -4666,6 +4678,10 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out Graphics::FinishLeaseWork(); std::array groups{}; GuestMemory::Read(arguments, std::as_writable_bytes(std::span(groups)), 4); + if (threadDimensions) { + record.threadLimit = groups; + for (std::size_t axis = 0; axis < groups.size(); ++axis) groups[axis] = groups[axis] / record.threadGroup[axis] + (groups[axis] % record.threadGroup[axis] != 0 ? 1u : 0u); + } outcome.argumentReadMs = std::chrono::duration(std::chrono::steady_clock::now() - readStart).count(); record.x = groups[0]; record.y = groups[1]; @@ -4742,6 +4758,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } recordStep(PhaseRecordDataRefresh); } + std::shared_ptr threadArguments; + if (record.threadGroup != std::array{}) { + const auto data = resources.shaderDataBuffer(*record.shader); + const auto limitDword = record.shader->program->imageMetadataDword; + Graphics::Require(data != VK_NULL_HANDLE && limitDword >= 3, "Vulkan dispatch: a thread-dimension dispatch has no ShaderData thread limit"); + const VkDeviceSize limitByte = static_cast(limitDword - 3) * sizeof(std::uint32_t); + Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + if (argumentImport != nullptr) { + threadArguments = std::make_shared(context, 24, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + Graphics::Require(recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), threadArguments->DeviceAddress(), &record.threadGroup), "Vulkan dispatch: thread-dimension indirect arguments need the argument kernel"); + const VkBufferCopy limit{12, limitByte, 12}; + context.Function("vkCmdCopyBuffer")(commands, threadArguments->Handle(), data, 1, &limit); + } else { + context.Resolved(&Graphics::DeviceFunctions::cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, data, limitByte, sizeof(record.threadLimit), record.threadLimit.data()); + } + resources.forgetShaderDataWords(*record.shader); + covered = 0; + } using CommandClass = Graphics::Recorder::CommandClass; if (Graphics::Recorder::BarrierValidate()) { if (argumentImport != nullptr) { @@ -4775,7 +4809,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { } VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE; VkDeviceSize indirectOffset = argumentImport != nullptr ? Graphics::HostImportOffset(*argumentImport, arguments) : 0; - if (argumentImport != nullptr && argumentImport->address != 0) { + if (threadArguments != nullptr) { + indirectBuffer = threadArguments->Handle(); + indirectOffset = 0; + recorder.Keep(std::move(threadArguments)); + } else if (argumentImport != nullptr && argumentImport->address != 0) { auto checked = std::make_shared(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) { indirectBuffer = checked->Handle(); @@ -4876,10 +4914,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons objects->destroyModule = state->DeviceFunction("vkDestroyShaderModule"); objects->destroyLayout = state->DeviceFunction("vkDestroyPipelineLayout"); objects->destroyPipeline = state->DeviceFunction("vkDestroyPipeline"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule"); + objects->module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); objects->resourcesLayout = setLayout; const std::array setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE}; const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes}; @@ -4939,11 +4974,7 @@ VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context, std::array offsets{}; for (std::uint32_t component = 0; component < offsets.size(); ++component) offsets[component] = {ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetBase + component, base[component]}; const Graphics::PipelineSpecialization specialization(shader, offsets); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - moduleInfo.pCode = shader.spirv.data(); - VkShaderModule module = VK_NULL_HANDLE; - check(state->DeviceFunction("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule"); VkPipeline pipeline = VK_NULL_HANDLE; try { CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization); @@ -4994,8 +5025,19 @@ std::shared_ptr VulkanDevice::computePipeline(const Grap return finish(std::move(objects), nullptr); } -VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut) { +VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipeOut, const std::array& threadGroup) { startJournalReplay(); + for (const auto& binding : shader.bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; + for (std::size_t at = 0; at + 4u <= binding.guestDescriptor.size(); at += 4u) { + const auto* words = binding.guestDescriptor.data() + at; + const auto base = static_cast(words[0]) | (static_cast(words[1] & 0xffffu) << 32u); + const auto stride = (words[1] >> 16u) & 0x3fffu; + const auto records = static_cast(words[2]); + const auto bytes = stride != 0 ? records * stride : records; + if (base != 0 && bytes != 0) static_cast(Graphics::FlushDepthPlanes(base, static_cast(bytes))); + } + } state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.Dispatch"); @@ -5132,6 +5174,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec Graphics::ShaderResources::MoveProof proof; auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof); moves.checked.fetch_add(1, std::memory_order_relaxed); + if (moved.has_value() && threadGroup != std::array{}) { + const auto data = cached->shaderDataAllocation(shaders[0]); + if (data.has_value() && std::ranges::any_of(*moved, [&](const Graphics::ShaderResources::MovedBuffer& entry) { return entry.allocation == *data; })) moved.reset(); + } if (moved.has_value() && !moved->empty()) { overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true); if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset(); @@ -5234,6 +5280,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer}; record.resourceKeyMatched = fromCache; record.bindings = overlay; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); // The indirect hold total below still covers the whole call of a CPU-resolved one. const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; @@ -5308,7 +5355,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec return outcome; } -RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords) { +RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr& hit, IndirectOutcome& outcome, const std::shared_ptr& verify, bool refreshByWords, const std::array& threadGroup) { state->recorder->BoundKeptBytes(); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion); PerformanceTimer timing("Vulkan.DispatchRecipe"); @@ -5404,6 +5451,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed); RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer}; record.resourceKeyMatched = verify != nullptr; + record.threadGroup = threadGroup; if (arguments != 0) decideIndirect(record, outcome, groupsText); const bool indirect = record.arguments != 0 || outcome.cpuReason != 0; const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp index 8b3ad92f49..7bbca06017 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp @@ -27,6 +27,7 @@ namespace { std::jthread processSamplerThread; std::jthread workerSamplerThread; +std::jthread committerSamplerThread; // Debug aid: APS5_SAMPLE_WORKER= samples the calling thread every millisecond and, every 20 s, // writes "module+offset self inclusive" lines to for offline symbolization with nm. @@ -285,11 +286,7 @@ void StartProcessSampler() { }); } -} - -void StartWorkerSampler() { - StartProcessSampler(); - const char* path = std::getenv("APS5_SAMPLE_WORKER"); +void StartCallingThreadSampler(const char* path, std::jthread& slot) { if (path == nullptr) return; auto sampler = std::make_unique(); sampler->path = path; @@ -302,7 +299,7 @@ void StartWorkerSampler() { sampler->stackHigh = reinterpret_cast(block->StackBase); } if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle"); - workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) { + slot = std::jthread([sampler = std::move(sampler)](std::stop_token token) { auto flushed = std::chrono::steady_clock::now(); while (!token.stop_requested()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); @@ -316,14 +313,28 @@ void StartWorkerSampler() { }); } +} + +void StartWorkerSampler() { + StartProcessSampler(); + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread); +} + +void StartCommitterSampler() { + StartCallingThreadSampler(std::getenv("APS5_SAMPLE_COMMITTER"), committerSamplerThread); +} + void StopWorkerSampler() { processSamplerThread.request_stop(); workerSamplerThread.request_stop(); + committerSamplerThread.request_stop(); if (processSamplerThread.joinable()) processSamplerThread.join(); if (workerSamplerThread.joinable()) workerSamplerThread.join(); + if (committerSamplerThread.joinable()) committerSamplerThread.join(); } #else void StartWorkerSampler() {} +void StartCommitterSampler() {} void StopWorkerSampler() {} #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp index 1c8ee60301..580236b120 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BdaResources.hpp @@ -25,6 +25,11 @@ public: std::vector messages; }; void SetImageTables(std::vector tables, std::vector> ranges); + struct SrtGuardFaults { + std::uint32_t stage = 0; + std::vector flattenedSrt; + }; + void SetSrtGuards(std::vector guards); // APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative), // and how many recent tables the cache holds right now. struct TableCacheStats { @@ -52,11 +57,13 @@ private: // same device addresses share one buffer while one of them is alive (see the table cache in // BdaResources.cpp, which refers to it weakly). void markWrittenPages() const; + void checkSrtGuard() const; std::shared_ptr table; std::unique_ptr fault; std::size_t tableBytes = 0; std::vector imageTables; std::vector> imageTableRanges; + std::vector srtGuards; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index beec495e48..657c5351c4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -2,8 +2,10 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include #include #include +#include #include #include #include @@ -24,6 +26,7 @@ struct BufferAllocation { VkDeviceSize offset = 0; bool slab = false; std::size_t bufferBytes = 0; + std::uint32_t memoryType = 0; }; struct SlabSlot { @@ -63,6 +66,10 @@ public: static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; + std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); @@ -90,16 +97,18 @@ private: struct Slot { BufferAllocation allocation; std::uint64_t lastUse; + std::list::iterator order; }; struct SlotKey { std::size_t bytes; VkBufferUsageFlags usage; VkMemoryPropertyFlags properties; + std::uint32_t memoryType = 0; bool operator==(const SlotKey&) const = default; }; struct SlotKeyHash { std::size_t operator()(const SlotKey& key) const noexcept { - return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u); + return std::hash{}(key.bytes) ^ (static_cast(key.usage) << 32u) ^ (static_cast(key.properties) << 48u) ^ (static_cast(key.memoryType) << 56u); } }; // One retention tier: its slots by key (each list oldest first), their count and bytes, the @@ -127,6 +136,9 @@ private: // caller destroys those after releasing the mutex, so builds taking buffers on other threads // do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow. void evictOldest(Tier& tier, std::vector& evicted); + bool evictOldestImage(std::vector& evicted); + bool imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const; + bool heapOverBudget(); // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; @@ -137,14 +149,23 @@ private: PFN_vkFreeMemory freeMemory; PFN_vkAllocateMemory allocateMemory; PFN_vkMapMemory mapMemory; - VkPhysicalDevice physical; - VkPhysicalDeviceMemoryProperties memoryProperties; - PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2; + VkPhysicalDevice physical = VK_NULL_HANDLE; + VkPhysicalDeviceMemoryProperties memoryProperties{}; + PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; std::mutex mutex; // Not `small`/`large`: (via ) defines `small` as a macro. Tier smallTier; Tier largeTier; Tier deviceTier; + std::chrono::steady_clock::time_point heapRead{}; + bool heapOver = false; + VkDeviceSize imageBudget = 0; + VkDeviceSize imageBytes = 0; + std::size_t imageSlots = 0; + std::uint64_t imageHits = 0; + std::uint64_t imageMisses = 0; + std::uint64_t imageEvictions = 0; + std::list imageOrder; std::uint64_t clock = 0; std::mutex slabMutex; std::unordered_map slabs; @@ -160,7 +181,8 @@ private: }; std::shared_ptr GetBufferPool(const Context& context); -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index c73dcc10bb..b4c113d749 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -20,6 +20,7 @@ namespace AgcDriver::Graphics { + class TextureDetiler; class GpuColorTransfer; class BufferPool; @@ -71,6 +72,17 @@ inline void Check(VkResult result, const char* operation) { if (result != VK_SUCCESS) throw std::runtime_error(std::string("AGC graphics: ") + operation + ": Vulkan result " + std::to_string(result)); } +inline VkShaderModule CreateShaderModule(PFN_vkCreateShaderModule create, VkDevice device, bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes, const char* operation) { + std::vector keyed(code, code + bytes / sizeof(std::uint32_t)); + if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u; + VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + info.codeSize = keyed.size() * sizeof(std::uint32_t); + info.pCode = keyed.data(); + VkShaderModule module = VK_NULL_HANDLE; + Check(create(device, &info, nullptr, &module), operation); + return module; +} + inline void Require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("AGC graphics: ") + reason); } @@ -337,6 +349,10 @@ struct Context { } }; +inline VkShaderModule CreateShaderModule(const Context& context, const std::uint32_t* code, std::size_t bytes, const char* operation) { + return CreateShaderModule(context.Function("vkCreateShaderModule"), context.device, context.graphicsPipelineLibrary, code, bytes, operation); +} + inline void EndRenderPass(const Context& context, VkCommandBuffer commands) { if (context.graphicsPipelineLibrary) context.Resolved(&DeviceFunctions::cmdEndRendering, "vkCmdEndRenderingKHR")(commands); else context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index 8b2267cda4..726701cb8c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -9,13 +9,16 @@ #include #include #include +#include namespace AgcDriver::Graphics { class Texture; class StorageTexture; -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true); +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes); +bool DepthPlanesStale(); bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSurfacesSerial(); std::uint64_t DepthSurfacesReleased(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 169dcabeb6..6ef4b5cf08 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -202,7 +202,7 @@ public: // ever submitted); WaitSerial(serial) then covers all recorded work. std::uint64_t SubmitAndEpoch(); bool HasTimeline() const { return timeline != VK_NULL_HANDLE; } - bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments); + bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup = nullptr); // Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex: // it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires // HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one. @@ -233,6 +233,7 @@ public: std::uint64_t end; bool computed; }; + static std::vector QueuedWriteOrder(std::span writes); static std::vector QueuedWriteGroups(std::span writes); // Waits only for the batches up to the newest one that writes the range (submitting the open // batch when it is that one); later batches stay in flight. Fences of one queue signal in @@ -808,6 +809,8 @@ private: VkSemaphore timeline = VK_NULL_HANDLE; VkSemaphore submitHold = VK_NULL_HANDLE; std::uint64_t submitHoldValue = 0; + struct ProgressWatcher; + std::unique_ptr progressWatcher; // Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the // recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer. std::uint64_t id = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 9999703dc6..c0d65d1f74 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -307,7 +307,7 @@ public: bool DataWordsDiffer(const CompiledShader& shader) const; // Why the fast proof of a Revalidate left the object to the full walk (the [rescache] // revalidate line's reasons); Count: it did not. - enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count }; + enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count }; // Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see // refreshOwnObjects); Count: it was not. enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count }; @@ -336,6 +336,9 @@ public: private: friend class AgcDriver::VulkanDevice; bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder); + std::optional shaderDataAllocation(const CompiledShader& shader) const; + VkBuffer shaderDataBuffer(const CompiledShader& shader) const; + void forgetShaderDataWords(const CompiledShader& shader); struct DescribedRange { const char* kind; std::uint64_t address; @@ -543,6 +546,7 @@ private: GuestBufferMemory guestMemory; std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes(); std::unique_ptr bda; + std::vector srtGuardFaults; bool usesBda = false; bool bdaReadOnly = true; std::vector multisampleSources; @@ -592,6 +596,7 @@ private: // Guest buffer elements bound read-only: each use of this object skips that many pending-write // notes (counted in MarkGpuWrites for the [buffers] line). std::size_t readOnlyBuffers = 0; + std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now(); std::vector> writeOnlyRanges; bool drawBuild = false; std::uint64_t dispatchThreads = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f54483fcbd..20a1905725 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -46,13 +46,13 @@ struct OwnedImage { OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {} OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; - ~OwnedImage() { - if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); - } + ~OwnedImage(); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); Context context; VkImage image; VkDeviceMemory memory; + VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; }; class Texture { @@ -197,6 +197,7 @@ public: void WriteBack(); // Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead). void MarkDirty(); + void MarkWholeWritten(); void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1); void Flush(); bool Dirty() const; @@ -366,6 +367,9 @@ public: static std::uint64_t SinglePassMoves(); static std::uint64_t SlabRetiles(); static std::uint64_t RefreshesProved(); + static std::uint64_t ClearHandOvers(); + static std::uint64_t HandedOverSuperseded(); + static void SetClearHandOver(bool enabled); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; std::uint64_t BudgetBytes() const; @@ -505,6 +509,8 @@ private: // stays responsible for storing them until this image's own results supersede them // (markLayersPending). APS5_NO_ALIAS_BORROW=1 stores and re-reads as before. std::shared_ptr pendingAlias() const; + std::optional clearHandOver(); + void supersedeHandedOver(std::uint32_t first, std::uint32_t count); // The pending images FlushPending(address, bytes) would store (not the one a Refresh validates, // a pending unit inside the range), kept alive across the caller's use. static std::vector> overlappingPending(std::uint64_t address, std::size_t bytes); @@ -585,6 +591,8 @@ private: std::weak_ptr borrowedFrom; std::uint64_t borrowedVersion = 0; std::vector borrowedUnits; + std::vector, std::uint64_t>> handedOver; + std::vector handedUnits; // Units of this image were borrowed since it was last written: the borrower's proofs // (the resource cache's serial memo) hold only while this image's version does, so the next // write here bumps the pending serial although the image was pending already. @@ -592,6 +600,7 @@ private: VkImage image = VK_NULL_HANDLE; VkDeviceMemory memory = VK_NULL_HANDLE; VkDeviceSize memoryBytes = 0; + std::uint32_t memoryType = 0; VkImageView view = VK_NULL_HANDLE; std::uint32_t defaultMip = 0; std::map extraViews; @@ -610,6 +619,7 @@ private: // Results are on the GPU only (guarded by the pending-write registry lock). bool dirty = false; std::uint64_t version = 0; + std::uint64_t writes = 0; // `original` holds the guest bytes; false after a GPU-side clear, which never read them. std::vector layerOriginal; std::map keptAtUnwatch; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp index 5c71753057..a8d93a9a85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp @@ -363,23 +363,36 @@ inline std::optional ClampDrawIndices(std::span indice return highest; } +template +std::optional HighestDrawIndexOf(std::span indices, bool skipRestart) { + constexpr auto restartIndex = std::numeric_limits::max(); + const auto count = indices.size() / sizeof(TIndex); + const auto* data = indices.data(); + TIndex highest = 0; + if (!skipRestart) { + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + highest = std::max(highest, index); + } + if (count == 0) return std::nullopt; + return highest; + } + TIndex kept = 0; + for (std::size_t i = 0; i < count; ++i) { + TIndex index; + std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex)); + const TIndex valid = index == restartIndex ? TIndex{0} : TIndex(~TIndex{0}); + highest = std::max(highest, static_cast(index & valid)); + kept |= valid; + } + if (kept == 0) return std::nullopt; + return highest; +} + inline std::optional HighestDrawIndex(std::span indices, std::uint32_t indexSize, bool skipRestart) { Require(indexSize == 2 || indexSize == 4, "unsupported index size"); - const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu; - std::optional highest; - for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) { - std::uint32_t index = 0; - if (indexSize == 2) { - std::uint16_t value = 0; - std::memcpy(&value, indices.data() + offset, sizeof(value)); - index = value; - } else { - std::memcpy(&index, indices.data() + offset, sizeof(index)); - } - if (skipRestart && index == restartIndex) continue; - highest = std::max(highest.value_or(0u), index); - } - return highest; + return indexSize == 2 ? HighestDrawIndexOf(indices, skipRestart) : HighestDrawIndexOf(indices, skipRestart); } inline std::vector MeshRestartTable(std::span indices, std::uint32_t indexSize) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp index ed0b252561..2ebd51c40e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/DispatchArguments.comp @@ -9,13 +9,29 @@ layout(push_constant) uniform Parameters { uint maxX; uint maxY; uint maxZ; + uint threadDimensions; + uint groupX; + uint groupY; + uint groupZ; uint unused; } parameters; +uint groups(uint threads, uint group) { + return threads / group + (threads % group != 0u ? 1u : 0u); +} + void main() { uint x = parameters.source.words[0]; uint y = parameters.source.words[1]; uint z = parameters.source.words[2]; + if (parameters.threadDimensions != 0u) { + parameters.arguments.words[3] = x; + parameters.arguments.words[4] = y; + parameters.arguments.words[5] = z; + x = groups(x, parameters.groupX); + y = groups(y, parameters.groupY); + z = groups(z, parameters.groupZ); + } bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ; parameters.arguments.words[0] = valid ? x : 0u; parameters.arguments.words[1] = valid ? y : 0u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 6e2537248b..1da043f87f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -127,6 +127,7 @@ BdaResources::BdaResources(const Context& context) { Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit"); fault = std::make_unique(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t)); + std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t)); } BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) { @@ -245,6 +246,7 @@ void BdaResources::CheckFault() const { std::memcpy(&report, fault->Bytes().data(), sizeof(report)); if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) { Require(static_cast(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication"); + checkSrtGuard(); return; } Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record"); @@ -309,6 +311,27 @@ void BdaResources::SetImageTables(std::vector tables, std::vec imageTableRanges = std::move(ranges); } +void BdaResources::SetSrtGuards(std::vector guards) { + srtGuards = std::move(guards); +} + +void BdaResources::checkSrtGuard() const { + namespace Abi = ShaderRecompiler::BdaAbi; + std::uint32_t guard = 0; + std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard)); + if (guard == 0) return; + const auto stage = guard >> Abi::SrtGuardStageShift; + const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u); + const auto found = std::find_if(srtGuards.begin(), srtGuards.end(), [&](const SrtGuardFaults& candidate) { return candidate.stage == stage; }); + const auto* guards = found != srtGuards.end() ? &*found : srtGuards.size() == 1 ? &srtGuards.front() : nullptr; + Require(guards != nullptr && record + 2u < guards->flattenedSrt.size(), "a guarded SRT read faulted without its poison record"); + const auto& words = guards->flattenedSrt; + const auto address = static_cast(words[record + 1u]) | (static_cast(words[record + 2u]) << 32u); + std::ostringstream message; + message << "BDA access failed: address=0x" << std::hex << address << " instruction=0x" << words[record] << std::dec << " bytes=4 stage=" << stage << " reason=" << static_cast(Abi::FaultReason::Unmapped); + throw std::runtime_error(message.str()); +} + void BdaResources::markWrittenPages() const { namespace Abi = ShaderRecompiler::BdaAbi; auto* words = reinterpret_cast(fault->Bytes().data()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 0fe017487d..128970a898 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -42,6 +42,10 @@ BufferPool::BufferPool(const Context& context) : maxSlots(std::max( smallTier.budget = smallBudget; largeTier.budget = budget; deviceTier.budget = deviceBudget(context); + memoryProperties = context.memory; + physical = context.physical; + memoryProperties2 = context.memoryProperties2; + imageBudget = ImageBudget(deviceTier.budget); } BufferPool::~BufferPool() { @@ -61,7 +65,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation unsigned attempts = 1; VkDeviceSize reclaimed = 0; VkDeviceSize released = 0; - const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; }; + const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY || value == VK_ERROR_TOO_MANY_OBJECTS; }; if (exhausted(result)) { reclaimed = trim(); if (reclaimed != 0) { @@ -73,6 +77,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation if (exhausted(result)) { if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); if (released != 0) { + reclaimed += trim(); memory = VK_NULL_HANDLE; result = allocateMemory(device, &allocation, nullptr, &memory); ++attempts; @@ -115,6 +120,102 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation return VK_NULL_HANDLE; } +VkDeviceSize BufferPool::ImageBudget(VkDeviceSize deviceBudget) { + static const char* value = std::getenv("APS5_IMAGE_POOL_MIB"); + if (value != nullptr) return static_cast(std::strtoull(value, nullptr, 10)) << 20u; + return deviceBudget / 4u; +} + +bool BufferPool::imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const { + return imageBudget != 0 && bytes != 0 && bytes <= imageBudget && bytes <= deviceTier.budget && memoryType < memoryProperties.memoryTypeCount && DeviceTiered(memoryProperties.memoryTypes[memoryType].propertyFlags); +} + +bool BufferPool::heapOverBudget() { + if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return false; + const auto now = std::chrono::steady_clock::now(); + if (heapRead != std::chrono::steady_clock::time_point{} && now - heapRead < std::chrono::milliseconds(100)) return heapOver; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + memoryProperties2(physical, &properties); + heapRead = now; + heapOver = false; + for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) { + if ((memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && reported.heapBudget[heap] != 0 && reported.heapUsage[heap] > reported.heapBudget[heap]) heapOver = true; + } + return heapOver; +} + +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { + const auto type = allocation.memoryTypeIndex; + if (imagePooled(allocation.allocationSize, type)) { + std::lock_guard lock(mutex); + ++clock; + const auto found = deviceTier.free.find({static_cast(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type}); + if (found != deviceTier.free.end() && !found->second.empty()) { + auto& slots = found->second; + const auto memory = slots.back().allocation.memory; + imageOrder.erase(slots.back().order); + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + deviceTier.retainedBytes -= allocation.allocationSize; + --imageSlots; + imageBytes -= allocation.allocationSize; + ++imageHits; + if (allocatedType != nullptr) *allocatedType = type; + return memory; + } + ++imageMisses; + } + return AllocateMemory(allocation, operation, hostType, allocatedType); +} + +void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (!imagePooled(bytes, memoryType)) { + freeMemory(device, memory, nullptr); + return; + } + BufferAllocation allocation{VK_NULL_HANDLE, memory, nullptr, 0, bytes, static_cast(bytes), 0, memoryProperties.memoryTypes[memoryType].propertyFlags}; + allocation.memoryType = memoryType; + std::vector evicted; + try { + std::lock_guard lock(mutex); + if (heapOverBudget()) { + while (imageSlots != 0 && evictOldestImage(evicted)) { + } + evicted.push_back(allocation); + } else { + while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) { + } + while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted); + const SlotKey imageKey{allocation.bytes, 0, allocation.properties, memoryType}; + auto& slots = deviceTier.free[imageKey]; + slots.push_back({allocation, ++clock, {}}); + try { + slots.back().order = imageOrder.insert(imageOrder.end(), &slots.back()); + } catch (...) { + slots.pop_back(); + if (slots.empty()) deviceTier.free.erase(imageKey); + throw; + } + ++deviceTier.slots; + deviceTier.retainedBytes += bytes; + ++imageSlots; + imageBytes += bytes; + } + } catch (...) { + destroy(allocation); + } + for (const auto& gone : evicted) destroy(gone); +} + +std::size_t BufferPool::RetainedImages() { + std::lock_guard lock(mutex); + return imageSlots; +} + VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { return bytes + std::max(bytes, VkDeviceSize{256} << 20u); } @@ -256,7 +357,7 @@ VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept { } // Device-local allocations (see DeviceBuffer) are never mapped. if (allocation.mapping != nullptr) unmap(device, allocation.memory); - destroyBuffer(device, allocation.buffer, nullptr); + if (allocation.buffer != VK_NULL_HANDLE) destroyBuffer(device, allocation.buffer, nullptr); freeMemory(device, allocation.memory, nullptr); return allocation.allocationBytes; } @@ -297,7 +398,7 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag const auto now = std::chrono::steady_clock::now(); if (now - lastReport > std::chrono::seconds(10)) { lastReport = now; - AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u)); + AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu); images: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast(smallTier.hits), static_cast(smallTier.misses), static_cast(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast(largeTier.hits), static_cast(largeTier.misses), static_cast(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast(deviceTier.hits), static_cast(deviceTier.misses), static_cast(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast(deviceTier.budget >> 20u), static_cast(imageHits), static_cast(imageMisses), static_cast(imageEvictions), imageSlots, imageBytes / 1048576.0, static_cast(imageBudget >> 20u)); } } auto& tier = tierFor(capacity, properties); @@ -322,12 +423,37 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) // First: a throw here leaves the slot retained and counted. evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; + if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) { + imageOrder.erase(oldest->second.front().order); + imageBytes -= oldest->second.front().allocation.allocationBytes; + --imageSlots; + ++imageEvictions; + } oldest->second.pop_front(); if (oldest->second.empty()) tier.free.erase(oldest); --tier.slots; ++tier.evictions; } +bool BufferPool::evictOldestImage(std::vector& evicted) { + if (imageOrder.empty()) return false; + const Slot* oldest = imageOrder.front(); + const SlotKey key{oldest->allocation.bytes, 0, oldest->allocation.properties, oldest->allocation.memoryType}; + const auto found = deviceTier.free.find(key); + evicted.push_back(oldest->allocation); + imageOrder.pop_front(); + const auto bytes = oldest->allocation.allocationBytes; + deviceTier.retainedBytes -= bytes; + imageBytes -= bytes; + found->second.pop_front(); + if (found->second.empty()) deviceTier.free.erase(found); + --deviceTier.slots; + --imageSlots; + ++deviceTier.evictions; + ++imageEvictions; + return true; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS= bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. @@ -351,6 +477,10 @@ VkDeviceSize BufferPool::trim() { tier->retainedBytes = 0; tier->slots = 0; } + imageEvictions += imageSlots; + imageBytes = 0; + imageSlots = 0; + imageOrder.clear(); } VkDeviceSize bytes = 0; for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) { @@ -388,7 +518,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept { evicted.push_back(allocation); } else { while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted); - tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock}); + tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}}); ++tier.slots; tier.retainedBytes += allocation.allocationBytes; } @@ -403,8 +533,17 @@ std::shared_ptr GetBufferPool(const Context& context) { return context.bufferPool; } -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { - return GetBufferPool(context)->AllocateMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex)); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +} + +void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { + if (memory == VK_NULL_HANDLE) return; + if (context.bufferPool) { + context.bufferPool->PutImageMemory(memory, bytes, memoryType); + return; + } + context.Function("vkFreeMemory")(context.device, memory, nullptr); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index fe4c7ef50e..7df4340010 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -33,6 +33,7 @@ namespace AgcDriver::Graphics { namespace { std::uint64_t depthWrites = 0; +std::uint64_t mirrorEpoch = 0; std::pair DepthBlockExtent(std::uint32_t bytesPerTexel) { return {bytesPerTexel == 4 ? 128u : 256u, bytesPerTexel == 1 ? 256u : 128u}; @@ -697,6 +698,43 @@ public: else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); } + std::shared_ptr StoreDepthPlane(const GuestTextureResource& resource) { + ApplyFastClear(); + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + copyPlane(mirrorPlane, mirrorPlane.storage, false, true, true); + return mirrorPlane.storage; + } + + bool SyncFromMemory(const GuestTextureResource& resource) { + const auto bytes = static_cast(DescribeSurface(resource).guestBytes); + GuestMemory::CollectWrites(resource.baseAddress, bytes); + if (syncedGeneration == 0 || (pendingClear & VK_IMAGE_ASPECT_DEPTH_BIT) != 0 || !GuestMemory::WrittenSince(resource.baseAddress, bytes, syncedGeneration)) { + syncedGeneration = GuestMemory::TrackerGeneration(); + return false; + } + memoryStale = false; + if (mirrorPlane.storage == nullptr) { + mirrorPlane.storage = StorageTexture::Create(context, *context.detiler, resource, 0); + ++mirrorEpoch; + } + else static_cast(mirrorPlane.storage->Refresh()); + syncedGeneration = GuestMemory::TrackerGeneration(); + copyPlane(mirrorPlane, mirrorPlane.storage, false, false, true); + written = ++depthWrites; + return true; + } + + void DropMirror() { + mirrorPlane = {}; + } + + bool memoryStale = false; + std::uint64_t scannedEpoch = 0; + std::uint64_t syncedGeneration = 0; + const Context context; const DepthTarget target; const VkFormat hostFormat; @@ -893,6 +931,7 @@ private: std::uint64_t stencilBytes() const { return DepthSliceBytes(target.extent, 1u); } + PlaneStorage mirrorPlane; bool CoversPlane(const StorageTexture& storage, bool stencil) const { const auto& resource = storage.Descriptor(); @@ -904,15 +943,17 @@ private: 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) { + void copyPlane(PlaneStorage& plane, const std::shared_ptr& storage, bool stencil, bool toStorage, bool ownBatch = false) { auto* recorder = Recorder::Active(); - Require(recorder != nullptr, "depth surface storage copies need an active recorder"); + Require(recorder != nullptr || ownBatch, "depth surface storage copies need an active recorder"); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); 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(storage); - const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); + const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle(); + if (recorder != nullptr) recorder->Keep(storage); + const std::uint32_t timing = recorder != nullptr ? recorder->BeginGpuTiming(Recorder::CommandClass::Copy) : 0u; 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; @@ -923,6 +964,10 @@ private: 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); context.Function("vkCmdCopyBufferToImage")(commands, plane.staging->Handle(), destinationImage, VK_IMAGE_LAYOUT_GENERAL, 1, toStorage ? &colorRegion : &planeRegion); RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + if (batch) { + batch->SubmitAndWait(); + return; + } Recorder::CountBarriers(Recorder::CommandClass::Copy, 3); recorder->EndGpuTiming(timing, bytes); } @@ -931,6 +976,7 @@ private: depthPlane = {}; stencilPlane = {}; releaseStencilCopy(); + mirrorPlane = {}; textures.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (owned != nullptr) owned.reset(); @@ -1223,8 +1269,8 @@ bool sameSurface(const DepthTarget& a, const DepthTarget& b) { return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples && (a.samples == 1u || (a.htileAddress == b.htileAddress && a.htileStencil == b.htileStencil && a.depthSwizzle == b.depthSwizzle && a.pipeBankXor == b.pipeBankXor && a.firstSlice == b.firstSlice && a.lastSlice == b.lastSlice && a.iterate256 == b.iterate256 && a.htilePipeAligned == b.htilePipeAligned && a.htileVrsEncoding == b.htileVrsEncoding && a.htileDepthOnly == b.htileDepthOnly)); } -std::mutex& surfacesMutex() { - static std::mutex mutex; +std::recursive_mutex& surfacesMutex() { + static std::recursive_mutex mutex; return mutex; } @@ -1296,13 +1342,6 @@ DepthSurface* planeSurface(VkDevice device, std::uint64_t address) { return found == list.rend() ? nullptr : found->get(); } -void retire(DepthSurface& surface) { - surface.retired = true; - for (auto* owners : {&depthOwners(), &stencilOwners()}) { - std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); - } -} - std::map& clearedHtiles() { static auto* cleared = new std::map(); return *cleared; @@ -1470,6 +1509,84 @@ VkImageAspectFlags storeBackClearedHtile(const Context& context, const DepthTarg htileReads()[target.htileAddress] = generation; return cleared; } + +struct StalePlanes { + std::mutex mutex; + std::vector> ranges; + std::atomic begin{std::numeric_limits::max()}; + std::atomic end{0}; +}; + +StalePlanes& stalePlanes() { + static auto* planes = new StalePlanes(); + return *planes; +} + +void publishStaleHull(StalePlanes& planes) { + auto begin = std::numeric_limits::max(); + std::uint64_t end = 0; + for (const auto& range : planes.ranges) { + begin = std::min(begin, range.first); + end = std::max(end, range.second); + } + planes.begin.store(begin, std::memory_order_release); + planes.end.store(end, std::memory_order_release); +} + +void noteStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + if (std::find(planes.ranges.begin(), planes.ranges.end(), std::pair{begin, end}) != planes.ranges.end()) return; + planes.ranges.emplace_back(begin, end); + publishStaleHull(planes); +} + +bool overlapsStalePlane(std::uint64_t begin, std::uint64_t end) { + auto& planes = stalePlanes(); + if (begin >= planes.end.load(std::memory_order_acquire) || planes.begin.load(std::memory_order_acquire) >= end) return false; + std::lock_guard lock(planes.mutex); + return std::any_of(planes.ranges.begin(), planes.ranges.end(), [&](const auto& range) { return begin < range.second && range.first < end; }); +} + +bool mirrorable(const DepthTarget& target) { + return (target.format == VK_FORMAT_D32_SFLOAT || target.format == VK_FORMAT_D32_SFLOAT_S8_UINT) && target.samples == 1 && target.depthSwizzle == XorSwizzleMode(TextureTileMode::kZ64KBX); +} + +GuestTextureResource mirrorResource(const DepthTarget& target) { + GuestTextureResource resource{}; + resource.baseAddress = target.address; + resource.width = target.extent.width; + resource.height = target.extent.height; + resource.mipCount = 1; + resource.allocatedMipCount = 1; + resource.tileMode = TextureTileMode::kZ64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = 22; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + return resource; +} + +std::pair stalePlaneRange(const DepthSurface& surface) { + return {surface.target.address, surface.target.address + DescribeSurface(mirrorResource(surface.target)).guestBytes}; +} + +void retire(DepthSurface& surface) { + surface.retired = true; + for (auto* owners : {&depthOwners(), &stencilOwners()}) { + std::erase_if(*owners, [&](const auto& entry) { return entry.second.surface == &surface; }); + } + if (!surface.memoryStale) return; + surface.memoryStale = false; + const auto plane = stalePlaneRange(surface); + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& other) { return other->memoryStale && stalePlaneRange(*other) == plane; })) return; + auto& planes = stalePlanes(); + std::lock_guard lock(planes.mutex); + std::erase(planes.ranges, plane); + publishStaleHull(planes); +} } std::uint64_t DepthSurfacesSerial() { @@ -1574,7 +1691,7 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const return constant * 256.0f; } -VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { +VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten) { const auto htileCleared = RoutesToNativeDepthOwner(target) ? VkImageAspectFlags{0} : storeBackClearedHtile(context, target); std::lock_guard gpu(GuestMemory::GpuMutex()); std::unique_lock lock(surfacesMutex()); @@ -1609,6 +1726,7 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) releaseRetired(context); surfacesSerial.fetch_add(1, std::memory_order_release); surfaces().push_back(std::make_unique(context, target, std::move(sampling))); + ++mirrorEpoch; auto& fresh = *surfaces().back(); if (target.samples != 1u && fresh.sampleable && fresh.sampling.refusal.empty()) { std::unique_lock guardLock(depthGuardMutex()); @@ -1631,14 +1749,82 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) if (!created) surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT); } if (!created) surface->NoteHtileClear(htileCleared); + if (mirrorable(target)) static_cast(surface->SyncFromMemory(mirrorResource(target))); surface->ApplyFastClear(); if (drawnDepth != nullptr && drawnDepth != surface) drawnDepth->written = ++depthWrites; surface->written = ++depthWrites; surface->NoteMemory(); + if (mirrorable(target) && surface->scannedEpoch != mirrorEpoch) { + surface->scannedEpoch = mirrorEpoch; + const auto plane = stalePlaneRange(*surface); + for (const auto& other : surfaces()) { + if (other.get() == surface || other->context.device != context.device || other->written >= surface->written || !mirrorable(other->target)) continue; + const auto otherPlane = stalePlaneRange(*other); + if (otherPlane == plane || otherPlane.first >= plane.second || plane.first >= otherPlane.second) continue; + other->memoryStale = false; + other->DropMirror(); + } + } + if ((depthWritten || cleared) && mirrorable(target)) { + surface->memoryStale = true; + const auto plane = target.address; + noteStalePlane(plane, plane + DescribeSurface(mirrorResource(target)).guestBytes); + } drawnDepth = surface; return surface->view; } +bool DepthPlanesStale() { + auto& planes = stalePlanes(); + return planes.begin.load(std::memory_order_acquire) < planes.end.load(std::memory_order_acquire); +} + +std::vector> FlushDepthPlanes(std::uint64_t address, std::size_t bytes) { + static thread_local bool flushing = false; + const auto end = bytes > std::numeric_limits::max() - address ? std::numeric_limits::max() : address + bytes; + if (flushing || !overlapsStalePlane(address, end)) return {}; + flushing = true; + struct Reset { + ~Reset() { flushing = false; } + } reset; + std::vector>> stored; + std::vector> ranges; + { + std::lock_guard gpu(GuestMemory::GpuMutex()); + std::lock_guard lock(surfacesMutex()); + std::vector> remaining; + for (const auto& surface : surfaces()) { + if (!surface->memoryStale) continue; + const auto resource = mirrorResource(surface->target); + const auto plane = surface->target.address; + const auto planeBytes = DescribeSurface(resource).guestBytes; + if (plane >= end || address >= plane + planeBytes) { + remaining.emplace_back(plane, plane + planeBytes); + continue; + } + surface->memoryStale = false; + if (surface->SyncFromMemory(resource)) continue; + stored.emplace_back(surface.get(), surface->StoreDepthPlane(resource)); + ranges.emplace_back(plane, planeBytes); + } + auto& planes = stalePlanes(); + std::lock_guard staleLock(planes.mutex); + planes.ranges = std::move(remaining); + publishStaleHull(planes); + } + for (const auto& entry : stored) entry.second->MarkWholeWritten(); + for (const auto& [plane, planeBytes] : ranges) StorageTexture::FlushPending(plane, static_cast(planeBytes), nullptr, "depth plane store"); + const auto generation = GuestMemory::TrackerGeneration(); + std::lock_guard lock(surfacesMutex()); + for (const auto& entry : stored) { + if (std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& candidate) { return candidate.get() == entry.first; })) { + entry.first->syncedGeneration = generation; + entry.first->NoteMemory(); + } + } + return ranges; +} + std::uint64_t HtileDepthClearAddress(std::span code, std::span userData, const std::array& numThreads) { static constexpr std::array htileMaskKernel{0xd7460000u, 0x04010c06u, 0x34000084u, 0xdc388000u, 0x04020000u, 0xbf8c3f70u, 0xd7710008u, 0x00120805u, 0xd7710009u, 0x00120a05u, 0xd771000au, 0x00120c05u, 0xd771000bu, 0x00120e05u, 0xdc788000u, 0x00020800u, 0xbf810000u}; if (userData.size() < 6 || numThreads[0] != 64 || numThreads[1] != 1 || numThreads[2] != 1) return 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e0a69a53ac..63125623ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2211,6 +2211,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); + static_cast(FlushDepthPlanes(color.address, colorLayout.Bytes())); RetireDepthSurfaces(context.device, color.address, colorLayout.Bytes()); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; timer.phase(PhaseSetup); @@ -2287,7 +2288,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.target = std::make_unique(context, color, state.blends.at(color.exportIndex).blendEnable != 0); targetViews.push_back(binding.target->View()); } - if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth)); + if (state.depth) targetViews.push_back(DepthSurfaceView(context, *state.depth, state.depthWrite)); timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); @@ -2842,7 +2843,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con } if (recipe.hasDepth) { timer.phase(PhaseSetup); - const auto depthView = DepthSurfaceView(context, *state.depth); + const auto depthView = DepthSurfaceView(context, *state.depth, state.depthWrite); timer.phase(PhaseReadTarget); if (recipe.targetViews.back() != depthView) return miss(DrawRecipeMiss::TargetGone); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp index 973aff1e34..9881ba2473 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GpuColorTransfer.cpp @@ -28,10 +28,7 @@ GpuColorTransfer::GpuColorTransfer(const Context& context) : context(context) { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout color transfer"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(COLOR_TRANSFER_SPV); - moduleInfo.pCode = COLOR_TRANSFER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule color transfer"); + module = CreateShaderModule(context, COLOR_TRANSFER_SPV, sizeof(COLOR_TRANSFER_SPV), "vkCreateShaderModule color transfer"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..7944e8b199 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Graphics/include/BdaResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -527,7 +528,7 @@ VkResult createImportBuffer(const Context& context, VkExternalMemoryHandleTypeFl const char* bindImport(const Context& context, HostImport& entry, VkExternalMemoryHandleTypeFlagBits handleType, const void* import, std::uint32_t importTypes, bool& allocated, VkResult& failure) { const auto failed = [&](const char* step, VkResult result) -> const char* { if (entry.buffer) context.Function("vkDestroyBuffer")(context.device, entry.buffer, nullptr); - if (entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); + if (allocated && entry.memory) context.Function("vkFreeMemory")(context.device, entry.memory, nullptr); entry.buffer = VK_NULL_HANDLE; entry.memory = VK_NULL_HANDLE; failure = result; @@ -544,7 +545,9 @@ const char* bindImport(const Context& context, HostImport& entry, VkExternalMemo ImportFamilies families; if (handleType == VK_EXTERNAL_MEMORY_HANDLE_TYPE_DMA_BUF_BIT_EXT) shareDmaBufImport(context, info, families, false); #endif - if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &entry.buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + VkBuffer buffer = VK_NULL_HANDLE; + if (const auto result = context.Function("vkCreateBuffer")(context.device, &info, nullptr, &buffer); result != VK_SUCCESS) return failed("vkCreateBuffer", result); + entry.buffer = buffer; VkMemoryRequirements requirements{}; context.Function("vkGetBufferMemoryRequirements")(context.device, entry.buffer, &requirements); const auto types = requirements.memoryTypeBits & importTypes; @@ -4552,6 +4555,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) { for (const auto& region : regions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); if (space != nullptr) completeSpaceWrites(); uploaded = true; + for (const auto& region : regions) static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast(end - begin), nullptr, "written buffer range"); if (context.hostImportAlignment != 0) { auto& state = Imports(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp index 8f731eb881..fe92fa1b97 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleReadback.cpp @@ -46,10 +46,8 @@ VkPipeline TextureDetiler::multisampleReadbackPipeline(std::uint32_t elementByte } auto& module = readbackModules[depth ? 1u : 0u]; if (module == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - info.codeSize = depth ? sizeof(MULTISAMPLE_READBACK_DEPTH_SPV) : sizeof(MULTISAMPLE_READBACK_UINT_SPV); - info.pCode = depth ? MULTISAMPLE_READBACK_DEPTH_SPV : MULTISAMPLE_READBACK_UINT_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &info, nullptr, &module), "vkCreateShaderModule multisample readback"); + module = depth ? CreateShaderModule(context, MULTISAMPLE_READBACK_DEPTH_SPV, sizeof(MULTISAMPLE_READBACK_DEPTH_SPV), "vkCreateShaderModule multisample readback") + : CreateShaderModule(context, MULTISAMPLE_READBACK_UINT_SPV, sizeof(MULTISAMPLE_READBACK_UINT_SPV), "vkCreateShaderModule multisample readback"); } const VkSpecializationMapEntry entry{0u, 0u, sizeof(elementBytes)}; const VkSpecializationInfo specialization{1u, &entry, sizeof(elementBytes), &elementBytes}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..1eaef4b7ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -359,10 +359,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput const VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT meshSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT, nullptr, 32u}; for (std::uint32_t i = 0; i < shaders.size(); ++i) { const auto& shader = *shaders[i].program; - VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - module.codeSize = shader.spirv.size() * sizeof(std::uint32_t); - module.pCode = shader.spirv.data(); - Check(context.Function("vkCreateShaderModule")(context.device, &module, nullptr, &_modules[i]), "vkCreateShaderModule graphics"); + _modules[i] = CreateShaderModule(context, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule graphics"); const auto stage = VulkanStage(shaders[i].stage); stages[i].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; stages[i].stage = stage; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c7a0f679a5..d97bd373b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 4u; +constexpr std::uint32_t FileFormat = 5u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index a4dfce1f09..9f03343a67 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -1083,6 +1084,60 @@ void FlushForAccess(std::uint64_t address, std::size_t bytes) { } +struct Recorder::ProgressWatcher { + std::mutex mutex; + std::condition_variable wake; + std::uint64_t submitted = 0; + bool stop = false; + std::thread thread; + + ProgressWatcher(VkDevice device, VkSemaphore timeline, PFN_vkWaitSemaphoresKHR waitSemaphores, PFN_vkGetSemaphoreCounterValueKHR counterValue) { + thread = std::thread([this, device, timeline, waitSemaphores, counterValue] { + CpuTopology::PinHelperThread("GPU progress watcher"); + std::uint64_t seen = 0; + for (;;) { + { + std::unique_lock lock(mutex); + wake.wait(lock, [&] { return stop || submitted > seen; }); + if (stop) return; + } + const std::uint64_t target = seen + 1; + VkSemaphoreWaitInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO_KHR}; + info.semaphoreCount = 1; + info.pSemaphores = &timeline; + info.pValues = ⌖ + const auto result = waitSemaphores(device, &info, 50'000'000ull); + if (result == VK_TIMEOUT) continue; + if (result != VK_SUCCESS) { + std::fprintf(stderr, "[gpu] progress watcher: vkWaitSemaphoresKHR failed (%d), watcher stops\n", static_cast(result)); + return; + } + std::uint64_t value = target; + if (counterValue(device, timeline, &value) != VK_SUCCESS) value = target; + seen = std::max(target, value); + KernelWakeSleepers_nid_no_patch(); + } + }); + } + + void Submitted(std::uint64_t serial) { + { + std::lock_guard lock(mutex); + submitted = serial; + } + wake.notify_one(); + } + + ~ProgressWatcher() { + { + std::lock_guard lock(mutex); + stop = true; + } + wake.notify_one(); + thread.join(); + } +}; + Recorder::Recorder(const Context& context, bool timelineSemaphores) : context(context), id(nextRecorderId.fetch_add(1)) { { std::lock_guard lock(liveRecordersMutex); @@ -1129,6 +1184,7 @@ Recorder::~Recorder() { } catch (const std::exception& error) { std::fprintf(stderr, "[gpu] recorder teardown: %s\n", error.what()); } + progressWatcher.reset(); holdKept.clear(); if (activeRecorder == this) { activeRecorder = nullptr; @@ -1676,6 +1732,18 @@ bool Recorder::closeStoreRun(bool atSubmit) { std::vector writes; writes.reserve(stores.size()); for (const auto& store : stores) writes.push_back({store.address, store.End(), store.dumpTarget != 0}); + if (const auto order = QueuedWriteOrder(writes); !std::is_sorted(order.begin(), order.end())) { + std::vector ordered; + std::vector orderedWrites; + ordered.reserve(stores.size()); + orderedWrites.reserve(writes.size()); + for (const auto index : order) { + ordered.push_back(std::move(stores[index])); + orderedWrites.push_back(writes[index]); + } + stores = std::move(ordered); + writes = std::move(orderedWrites); + } const bool dumps = std::any_of(writes.begin(), writes.end(), [](const QueuedWrite& write) { return write.computed; }); if (dumps) recordQueuedDumps(commands, stores); std::size_t first = 0; @@ -1981,8 +2049,8 @@ void Recorder::EndGpuTiming(std::uint32_t index, std::uint64_t bytes) { context.Function("vkCmdWriteTimestamp")(open->commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, open->queries, index * 2 + 1); } -bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments) { - constexpr std::uint32_t PushBytes = 32; +bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array* threadGroup) { + constexpr std::uint32_t PushBytes = 48; if (dispatchArgumentState == 0) { dispatchArgumentState = -1; VkShaderModule module = VK_NULL_HANDLE; @@ -1992,10 +2060,7 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dispatchArgumentLayout), "vkCreatePipelineLayout dispatch arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(DISPATCH_ARGUMENTS_SPV); - moduleInfo.pCode = DISPATCH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule dispatch arguments"); + module = CreateShaderModule(context, DISPATCH_ARGUMENTS_SPV, sizeof(DISPATCH_ARGUMENTS_SPV), "vkCreateShaderModule dispatch arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2018,11 +2083,15 @@ bool Recorder::RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress VkDeviceAddress source; VkDeviceAddress arguments; std::array limits; - } parameters{source, arguments, {limit[0], limit[1], limit[2], 0u}}; + std::array group; + } parameters{source, arguments, {limit[0], limit[1], limit[2], threadGroup != nullptr ? 1u : 0u}, {1u, 1u, 1u, 0u}}; + if (threadGroup != nullptr) std::copy(threadGroup->begin(), threadGroup->end(), parameters.group.begin()); static_assert(sizeof(parameters) == PushBytes); function(cmdPushConstants, "vkCmdPushConstants")(commands, dispatchArgumentLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, PushBytes, ¶meters); function(cmdDispatch, "vkCmdDispatch")(commands, 1, 1, 1); - recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_INDIRECT_COMMAND_READ_BIT); + const VkPipelineStageFlags readers = VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | (threadGroup != nullptr ? VK_PIPELINE_STAGE_TRANSFER_BIT : 0u); + const VkAccessFlags reads = VK_ACCESS_INDIRECT_COMMAND_READ_BIT | (threadGroup != nullptr ? VK_ACCESS_TRANSFER_READ_BIT : 0u); + recordBarrier(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, readers, VK_ACCESS_SHADER_WRITE_BIT, reads); CountBarriers(CommandClass::DispatchLeading, 2); return true; } @@ -2075,10 +2144,7 @@ bool Recorder::gpuSampleCounter() { layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &sampleLayout), "vkCreatePipelineLayout sample counter"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(SAMPLE_COUNTER_SPV); - moduleInfo.pCode = SAMPLE_COUNTER_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample counter"); + module = CreateShaderModule(context, SAMPLE_COUNTER_SPV, sizeof(SAMPLE_COUNTER_SPV), "vkCreateShaderModule sample counter"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -2094,9 +2160,7 @@ bool Recorder::gpuSampleCounter() { const VkPushConstantRange dumpPush{VK_SHADER_STAGE_COMPUTE_BIT, 0, SampleDumpPushBytes}; layoutInfo.pPushConstantRanges = &dumpPush; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &dumpLayout), "vkCreatePipelineLayout sample dumps"); - moduleInfo.codeSize = sizeof(SAMPLE_DUMPS_SPV); - moduleInfo.pCode = SAMPLE_DUMPS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule sample dumps"); + module = CreateShaderModule(context, SAMPLE_DUMPS_SPV, sizeof(SAMPLE_DUMPS_SPV), "vkCreateShaderModule sample dumps"); pipelineInfo.stage.module = module; pipelineInfo.layout = dumpLayout; { @@ -2125,6 +2189,56 @@ std::uint64_t Recorder::Batch::StoreRun::Queued::End() const { return address + (dumpTarget != 0 ? SampleDumpBytes : bytes.size()); } +std::vector Recorder::QueuedWriteOrder(std::span writes) { + std::vector order(writes.size()); + for (std::size_t i = 0; i < order.size(); ++i) order[i] = i; + std::vector coveredLater(writes.size()), overLater(writes.size()); + std::map later; + for (auto i = writes.size(); i-- > 0;) { + const auto& write = writes[i]; + if (!write.computed) { + const auto after = later.upper_bound(write.begin); + coveredLater[i] = after != later.begin() && std::prev(after)->second >= write.end; + const auto last = later.lower_bound(write.end); + overLater[i] = last != later.begin() && std::prev(last)->second > write.begin; + continue; + } + auto begin = write.begin, end = write.end; + auto at = later.lower_bound(begin); + if (at != later.begin() && std::prev(at)->second >= begin) --at; + while (at != later.end() && at->first <= end) { + begin = std::min(begin, at->first); + end = std::max(end, at->second); + at = later.erase(at); + } + later.emplace(begin, end); + } + std::vector before, dumps, after; + std::vector> earlier, delayed; + for (std::size_t i = 0; i < writes.size(); ++i) { + const auto& write = writes[i]; + const auto over = [&](const std::vector>& ranges) { + return std::any_of(ranges.begin(), ranges.end(), [&](const auto& range) { return write.begin < range.second && range.first < write.end; }); + }; + if (write.computed) { + dumps.push_back(i); + earlier.emplace_back(write.begin, write.end); + } else if (dumps.empty()) { + before.push_back(i); + } else if (overLater[i]) { + if (over(delayed) || (!coveredLater[i] && over(earlier))) return order; + before.push_back(i); + } else { + after.push_back(i); + delayed.emplace_back(write.begin, write.end); + } + } + order = std::move(before); + order.insert(order.end(), dumps.begin(), dumps.end()); + order.insert(order.end(), after.begin(), after.end()); + return order; +} + std::vector Recorder::QueuedWriteGroups(std::span writes) { std::vector ends; std::vector> computed; @@ -2391,10 +2505,7 @@ bool Recorder::RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress rec layoutInfo.pushConstantRangeCount = 1; layoutInfo.pPushConstantRanges = &push; Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &meshArgumentLayout), "vkCreatePipelineLayout mesh arguments"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(MESH_ARGUMENTS_SPV); - moduleInfo.pCode = MESH_ARGUMENTS_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule mesh arguments"); + module = CreateShaderModule(context, MESH_ARGUMENTS_SPV, sizeof(MESH_ARGUMENTS_SPV), "vkCreateShaderModule mesh arguments"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; @@ -3494,6 +3605,13 @@ void Recorder::Submit() { newestSubmitted = batch->serial; newestSubmittedAt = batch->submittedAt; } + if (timeline != VK_NULL_HANDLE) { + if (!progressWatcher) { + progressWatcher = std::make_unique(context.device, timeline, context.Function("vkWaitSemaphoresKHR"), + context.Function("vkGetSemaphoreCounterValueKHR")); + } + progressWatcher->Submitted(batch->serial); + } inFlight.push_back(std::move(batch)); inFlightKeptBytes += inFlight.back()->keptBytes; if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); @@ -3989,6 +4107,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool } } } + if (!batch->completions.empty()) KernelWakeSleepers_nid_no_patch(); if (profile) holdCounters.completions += batch->completions.size(); const auto keptStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // Deferred only from inside a hold: the unlock that releases the list is this thread's own, and diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6dae4405cb..d0f6029601 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -120,6 +120,7 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; + std::uint64_t surfaceBytes = 0; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -723,6 +724,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(guestBytes); + static_cast(FlushDepthPlanes(address, bytes)); // A storage image whose results for this surface (or for a mip chain containing it) are still on // the GPU supplies the texture by a view of it; anything else needs those results in guest // memory first. @@ -864,18 +866,19 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } - } else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) { + } else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) { // Unwritten pages need no comparison; partially resident textures compare only committed // pages. The compare goes through the flush hook: it waits for recorded work over the // surface (counted, and named for the [hooksync] line). const auto equalsCommitted = [&] { + if (it->bytes.empty()) return false; if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed); const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare); return GuestMemory::EqualsCommitted(address, it->bytes); }; const auto unchanged = [&] { if (it->committed != committed) return false; - return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation); + return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast(it->surfaceBytes), it->generation); }; const auto kept = [&] { if (untracked[0] == 0) return false; @@ -899,6 +902,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); + entry.surfaceBytes = bytes; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); @@ -933,6 +937,8 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); + constexpr std::size_t SnapshotCompareBytes = 256u << 10u; + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); @@ -1117,6 +1123,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std reportGuestAllocation(viewed, viewedBytes); } } + if (DepthPlanesStale() || written) static_cast(FlushDepthPlanes(viewed.baseAddress, static_cast(viewedBytes))); if (written && DepthSurfaceOverlaps(context.device, viewed.baseAddress, viewedBytes)) { const auto storage = WrittenStorageBytes(viewed, mip); RetireCoveredDepthSurfaces(context.device, storage.bytes, storage.extents); @@ -1885,6 +1892,11 @@ void ShaderResources::buildPrepare(std::span shaders, cons for (const auto& shader : shaders) { Require(shader.program != nullptr, "missing compiled shader"); ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion); + if (shader.program->poisonedSrtReads != 0) { + const auto& programBindings = shader.program->bindings; + const auto flat = std::find_if(programBindings.begin(), programBindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt; }); + if (flat != programBindings.end()) srtGuardFaults.push_back({IrStage(shader.stage), flat->guestDescriptor}); + } const VkShaderStageFlags flags = VulkanStage(shader.stage); std::uint64_t stageStorageBuffers = 0; std::uint64_t stageResources = shader.stage == ShaderRecompiler::ShaderStage::Fragment ? colorAttachments : 0; @@ -2284,6 +2296,7 @@ void ShaderResources::buildComplete() { timing.uploadMs += phase(BuildPhase::Upload); if (usesBda) bda = std::make_unique(context, guestMemory); else if (usesFaultBuffer) bda = std::make_unique(context); + if (bda) bda->SetSrtGuards(std::move(srtGuardFaults)); finishImageTables(); phase(BuildPhase::Bda); if (_set != VK_NULL_HANDLE || fragment.set != VK_NULL_HANDLE) { @@ -2720,7 +2733,7 @@ namespace { // of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated. // Why the fast path left an object to the full walk (the "fast-fail by reason" counts). using FastFail = ShaderResources::FastFail; -constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"}; +constexpr std::array(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"}; using OwnRefreshFallback = ShaderResources::OwnRefreshFallback; constexpr std::array(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"}; @@ -2858,6 +2871,19 @@ bool EpochRevalidate() { return enabled; } +std::chrono::steady_clock::duration TemplateAuditInterval() { + static const auto interval = [] { + const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS"); + return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull); + }(); + return interval; +} + +bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { + const auto interval = TemplateAuditInterval(); + return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval; +} + } // Turns the lookups' records into this object's per-texture validation records (see @@ -2936,8 +2962,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::spanReleased()) return fail(FastFail::Replaced); const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return fail(FastFail::Changed); surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return fail(FastFail::Collect); const auto* source = surface.source; @@ -3062,8 +3090,10 @@ bool ShaderResources::fastRevalidateEach() { if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false; continue; } + if (textures[i]->Released()) return false; const auto address = surface.resource.baseAddress; const auto bytes = static_cast(surface.bytes); + if (!FlushDepthPlanes(address, bytes).empty()) return false; surface.collected = GuestMemory::CollectWrites(address, bytes); if (surface.collected == 0) return false; if (PendingStorageOverlaps(address, bytes, surface.source)) return false; @@ -3237,7 +3267,10 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR if (!reusable || shaders.empty()) return false; static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + for (const auto& region : directRegions) { + static_cast(FlushDepthPlanes(region.begin, static_cast(region.end - region.begin))); + CompletePendingCpuWrites(context, region.begin, static_cast(region.end - region.begin)); + } for (const auto& range : describedRanges) { if (!range.imageTable) RequireNoMultisampleColorTarget(range.address, static_cast(range.bytes), "a cached image access"); CompletePendingCpuWrites(context, range.address, static_cast(range.bytes)); @@ -3262,6 +3295,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR // they must hand back the very objects the set's views belong to. The build appended them stage // by stage, binding by binding, so the walk repeats that order. const auto fullWalk = [&] { + lastFullWalk = std::chrono::steady_clock::now(); lookupLog().clear(); BuildLookups lookups; std::size_t textureIndex = 0; @@ -3367,6 +3401,11 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR } if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback); } + if (fast && TemplateAuditDue(lastFullWalk)) { + fast = false; + ownRefreshed = false; + reason = FastFail::Audit; + } if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast; if (!fast) { countFullWalk(reason); @@ -3374,7 +3413,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR const auto failure = [&] { switch (reason) { case FastFail::Pending: return ProofFailure::Pending; - case FastFail::Evicted: return ProofFailure::Evicted; + case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted; case FastFail::Changed: return ProofFailure::Changed; case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys; default: return ProofFailure::Other; @@ -4224,7 +4263,7 @@ std::size_t ShaderResources::addDataBuffer(std::span words) const auto size = words.size() * sizeof(std::uint32_t); Require(size <= context.limits.maxStorageBufferRange, "shader data buffer exceeds descriptor range limit"); const bool refreshable = TemplateDataRefresh() && size <= MaxRefreshBytes; - auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | (refreshable ? VK_BUFFER_USAGE_TRANSFER_DST_BIT : 0u)); + auto buffer = std::make_unique(context, size, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT); std::memcpy(buffer->Bytes().data(), words.data(), size); Allocation allocation{0, size, false, std::move(buffer)}; if (refreshable) allocation.dataWords.assign(words.begin(), words.end()); @@ -4300,6 +4339,31 @@ bool ShaderResources::refreshData(VkCommandBuffer commands, const CompiledShader return recorded; } +std::optional ShaderResources::shaderDataAllocation(const CompiledShader& shader) const { + Require(shader.program != nullptr, "missing compiled shader"); + const auto& program = *shader.program; + if (program.bindings.size() != bindings.size()) return std::nullopt; + for (std::size_t index = 0; index < program.bindings.size(); ++index) { + if (program.bindings[index].role != ShaderRecompiler::DescriptorRole::ShaderData || bindings[index].allocations.size() != 1) continue; + const auto allocation = bindings[index].allocations.front(); + if (allocations[allocation].guest || allocations[allocation].buffer == nullptr) return std::nullopt; + return allocation; + } + return std::nullopt; +} + +VkBuffer ShaderResources::shaderDataBuffer(const CompiledShader& shader) const { + const auto allocation = shaderDataAllocation(shader); + return allocation.has_value() ? allocations[*allocation].buffer->Handle() : VK_NULL_HANDLE; +} + +void ShaderResources::forgetShaderDataWords(const CompiledShader& shader) { + if (const auto allocation = shaderDataAllocation(shader)) { + allocations[*allocation].dataWords.clear(); + rehashDataWords(); + } +} + void ShaderResources::writeDataWords(VkCommandBuffer commands, std::size_t allocation, std::span words) const { const auto& buffer = *allocations[allocation].buffer; const auto size = words.size() * sizeof(std::uint32_t); @@ -4499,7 +4563,7 @@ std::shared_ptr ShaderResources::fastTexture(const ImageRecord& record) const auto address = record.resource.baseAddress; const auto bytes = static_cast(record.guestBytes); RequireNoMultisampleColorTarget(address, bytes != 0 ? bytes : 1u, "a texture read"); - if (DepthSurfaceAt(address)) return nullptr; + if (DepthSurfaceAt(address) || !FlushDepthPlanes(address, bytes).empty()) return nullptr; if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr; if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr; auto keys = record.keys; @@ -4571,14 +4635,15 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words); const bool forcedSrgb = ForcesDegammaOn(resource.format, binding.imageSamplers[element], shaderSamplers); RequireNonSeamlessCube(resource.dimension == TextureDimension::kCube, binding.imageSamplers[element], shaderSamplers); - const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); + const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; - const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; if (texture == nullptr && record != nullptr && record->texture != nullptr) { @@ -4916,8 +4981,10 @@ void ShaderResources::resolveImageTables() { try { resource = DecodeTextureResource(words); shape = binding.imageShapes.at(element); - firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray; - if (!firstLayer && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool depthCompare = element < binding.imageDepthCompare.size() && binding.imageDepthCompare[element]; + firstLayer = shape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); + const bool depth1D = depthCompare && ((shape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (shape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); + if (!firstLayer && !depth1D && !MatchesGuestDimension(shape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the table entry's image shape (shape " + std::to_string(static_cast(shape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); guestBytes = DescribeSurface(resource).guestBytes; if (!GuestMemory::Accessible(reinterpret_cast(resource.baseAddress), static_cast(guestBytes)) && GuestMemory::CommittedRanges(resource.baseAddress, static_cast(guestBytes), false).empty()) throw std::runtime_error("AGC graphics: the table entry's surface at 0x" + HexText(resource.baseAddress) + " (0x" + HexText(guestBytes) + " bytes) is not mapped"); static_cast(ResolveTextureFormat(resource.format)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..e50f5fec8e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1978,7 +1978,9 @@ std::string NullPixelProgramRejection(const QueueState& queue) { const auto targetMask = find(queue.context, 0x8e); const auto shaderMask = find(queue.context, 0x8f); if (targetMask == queue.context.end() || shaderMask == queue.context.end()) return "AGC graphics: a draw without a pixel program needs CB_TARGET_MASK and CB_SHADER_MASK"; - if (ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; + const auto colorControl = find(queue.context, 0x202); + const bool colorDisabled = colorControl != queue.context.end() && ((colorControl->second >> 4u) & 7u) == 0u; + if (!colorDisabled && ColorWriteMask(queue.context) != 0) return "AGC graphics: a draw without a pixel program writes color"; return {}; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a2d90b6926..eb824c85d1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -40,6 +41,17 @@ namespace AgcDriver::Graphics { +OwnedImage::~OwnedImage() { + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); +} + +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); + memoryBytes = allocation.allocationSize; + context.bufferPool = device.bufferPool; +} + namespace { // APS5_PROFILE_DRAW: accumulate texture setup phases and report every 200 textures. @@ -90,7 +102,7 @@ thread_local const char* uploadReason = "first"; // (DiscardPendingInside) and units an alias's own results superseded (markLayersPending); relaxed, // reported only. std::atomic singlePassMoves{0}, slabRetiles{0}; -std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}; +std::atomic partialUploads{0}, partialUploadBytes{0}, partialWriteBacks{0}, partialWriteBackBytes{0}, emptyWriteBacks{0}, coalescedWriteBacks{0}, unitsDropped{0}, unitsSuperseded{0}, clearHandOvers{0}, handedOverSuperseded{0}; // FlushPending listings skipped because no pending unit of the image lay inside the range, images // whose selected pending units were dropped because their memory is no longer registered (see // writeBackLayers), and pending units a DCC clear -> uncompressed key flip kept as the texels @@ -143,7 +155,7 @@ void reportStorageTraffic(StorageTraffic& traffic) { const auto take = [](std::atomic& counter) { return static_cast(counter.exchange(0, std::memory_order_relaxed)); }; const auto partialUploadCount = take(partialUploads), partialWriteBackCount = take(partialWriteBacks); const auto uploadMiB = take(partialUploadBytes) / 1048576.0, writeBackMiB = take(partialWriteBackBytes) / 1048576.0; - AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); + AgcDriver::ProfilePrint_nid_no_patch( "[storage] uploads by path (count/MiB, 10 s):%s; %llu write-backs GPU-direct, %llu stored nothing; block units: partial uploads %llu/%.1f, partial write-backs %llu/%.1f, %llu widened to every pending unit, units dropped %llu, superseded %llu, handed over at %llu clear-key refreshes (%llu units superseded), dropped unregistered %llu, kept over a key flip %llu%s, pretest skipped %llu, held for a queued draw read %llu, evicted stale %llu\n", line.c_str(), static_cast(traffic.directWriteBacks), take(emptyWriteBacks), partialUploadCount, uploadMiB, partialWriteBackCount, writeBackMiB, take(coalescedWriteBacks), take(unitsDropped), take(unitsSuperseded), take(clearHandOvers), take(handedOverSuperseded), take(unregisteredDropped), take(keyFlipKept), ShadowReport().c_str(), take(pretestSkipped), take(queuedReadHeld), take(staleEvicted)); traffic.writeBacks.clear(); traffic.uploadReasons.clear(); traffic.directWriteBacks = 0; @@ -268,7 +280,7 @@ ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - result.owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); result.bytes = requirements.size; return result; @@ -329,11 +341,12 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex Require(arrayLayers == 0 || sliceLinearBytes <= UINT64_MAX / arrayLayers, "detiled texture buffer size overflows"); const auto linearBytes = sliceLinearBytes * arrayLayers; + const bool depth1D = depthCompare && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u) | (SrgbTwinFormat(context, vkFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u); - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = vkFormat; - imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, geometry.imageDepth}; imageInfo.mipLevels = descriptor.mipCount; Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented"); imageInfo.arrayLayers = geometry.imageLayers; @@ -351,7 +364,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); const VkImage uploadImage = image; @@ -531,7 +544,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; @@ -627,7 +640,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -685,7 +698,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -722,11 +735,12 @@ Texture::Texture(const Context& context, const std::shared_ptr& copyElementBytes = BytesPerElement(descriptor.format); AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast(colorFormat), static_cast(depthFormat)); + const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray; VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u; - imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.imageType = depth1D ? VK_IMAGE_TYPE_2D : ImageTypeFor(descriptor.dimension); imageInfo.format = depthFormat; - imageInfo.extent = {descriptor.width, descriptor.height, 1u}; + imageInfo.extent = {descriptor.width, depth1D ? 1u : descriptor.height, 1u}; imageInfo.mipLevels = copyLevels; imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; @@ -742,7 +756,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -752,14 +766,16 @@ Texture::Texture(const Context& context, const std::shared_ptr& } VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; - viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.viewType = depth1D ? (descriptor.dimension == TextureDimension::k1DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D) : ViewTypeFor(descriptor.dimension, viewLayerCount); viewInfo.format = depthFormat; viewInfo.components = VkComponentMapping{}; viewInfo.subresourceRange = {VK_IMAGE_ASPECT_DEPTH_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; ChainMinLod(context, descriptor, viewInfo, minLod); Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); static_cast(components); depthCopyTextures.fetch_add(1, std::memory_order_relaxed); } catch (...) { @@ -1067,7 +1083,7 @@ Texture::~Texture() { } void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { - if (descriptor.dimension != TextureDimension::k2DArray) return; + if (descriptor.dimension != TextureDimension::k2DArray && (viewInfo.viewType != VK_IMAGE_VIEW_TYPE_2D_ARRAY || descriptor.dimension != TextureDimension::k1DArray)) return; viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.layerCount = 1; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer"); @@ -1076,9 +1092,10 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma void Texture::prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo) { if (viewInfo.subresourceRange.levelCount == 1u) return; - if (descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; + const bool depth1D = (viewInfo.subresourceRange.aspectMask & VK_IMAGE_ASPECT_DEPTH_BIT) && (descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray); + if (!depth1D && descriptor.dimension != TextureDimension::k1D && descriptor.dimension != TextureDimension::k2D && descriptor.dimension != TextureDimension::k2DArray) return; viewInfo.pNext = nullptr; - viewInfo.viewType = descriptor.dimension == TextureDimension::k1D ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; + viewInfo.viewType = (descriptor.dimension == TextureDimension::k1D && !depth1D) ? VK_IMAGE_VIEW_TYPE_1D : VK_IMAGE_VIEW_TYPE_2D; viewInfo.subresourceRange.levelCount = 1; viewInfo.subresourceRange.layerCount = 1; baseLevelInfo = viewInfo; @@ -1411,7 +1428,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture"); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); @@ -2056,7 +2073,10 @@ bool StorageTexture::Refresh() { } } // No publish: the upload below reads the stored units from the unit shadow itself. - const bool flushed = FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); + handedOver.clear(); + handedUnits.clear(); + const auto handOverKeys = alias == nullptr ? clearHandOver() : std::nullopt; + const bool flushed = !handOverKeys.has_value() && FlushPending(descriptor.baseAddress, static_cast(guestBytes), alias.get(), "storage refresh", PublishScope::None); if (profile && flushed) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); // `original` holds the guest bytes the image was last uploaded from or written back as; while the // guest memory and the DCC keys still match, the image content is current. Pages nobody wrote @@ -2101,7 +2121,7 @@ bool StorageTexture::Refresh() { } }; const auto keysStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - keys = ProvedKeys(); + keys = handOverKeys.has_value() ? *handOverKeys : ProvedKeys(); if (profile && descriptor.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, keysStart); if (keys != uploadedKeys) { changed.assign(trackedLayers, true); @@ -3543,6 +3563,7 @@ void StorageTexture::refreshGeneration() { void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) { CaptureTrace::Log("image-write image=%llx first=%u count=%u generation=%llu", static_cast(descriptor.baseAddress), first, count, static_cast(generation)); + ++writes; static const bool eager = std::getenv("APS5_EAGER_WRITEBACK") != nullptr || std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr; std::vector unsaved(trackedLayers, false); bool anyUnsaved = false; @@ -3567,6 +3588,7 @@ void StorageTexture::markLayersPending(std::uint32_t first, std::uint32_t count) } if (superseded) source->reconcilePending(); } + if (!handedOver.empty() || !handedUnits.empty()) supersedeHandedOver(first, count); if (eager) { WriteBack(); return; @@ -3646,6 +3668,7 @@ bool StorageTexture::pendingUnitInside(std::uint64_t address, std::size_t bytes) } bool StorageTexture::FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except, const char* reason, PublishScope scope, bool* published) { + static_cast(FlushDepthPlanes(address, bytes)); if (published != nullptr) *published = false; if (AgcDriver::DriverDetail::DrawPipeline::HeldReads(address, bytes)) { queuedReadHeld.fetch_add(1, std::memory_order_relaxed); @@ -4763,6 +4786,11 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { copied.dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &copied); recorder->EndGpuTiming(timing, guestBytes); + MarkWholeWritten(); + return true; +} + +void StorageTexture::MarkWholeWritten() { // As after a clear: the image holds the surface, guest memory follows at the write-back (which // keeps the 64 KiB blocks the CPU writes from now on; the collect is uncached for the reason // given in FillClear). @@ -4777,7 +4805,6 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { refreshGeneration(); forgetBorrowed(0, trackedLayers); MarkDirty(); - return true; } // See AdjacentGenerationEnabled above. @@ -4849,6 +4876,104 @@ void StorageTexture::blockGenerations(std::vector& generations) c } } +namespace { + +std::atomic& ClearHandOverOn() { + static std::atomic on{std::getenv("APS5_NO_CLEAR_HAND_OVER") == nullptr}; + return on; +} + +} + +void StorageTexture::SetClearHandOver(bool enabled) { + ClearHandOverOn().store(enabled, std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::ClearHandOvers() { + return clearHandOvers.load(std::memory_order_relaxed); +} + +std::uint64_t StorageTexture::HandedOverSuperseded() { + return handedOverSuperseded.load(std::memory_order_relaxed); +} + +std::optional StorageTexture::clearHandOver() { + if (!ClearHandOverOn().load(std::memory_order_relaxed) || !blockUnits || descriptor.dccAddress == 0 || anyLayerPending()) return std::nullopt; + if (!GuestMemory::Watched(descriptor.baseAddress, static_cast(guestBytes))) return std::nullopt; + const auto others = overlappingPending(descriptor.baseAddress, static_cast(guestBytes)); + if (others.empty()) return std::nullopt; + const auto begin = descriptor.baseAddress; + const auto end = descriptor.baseAddress + guestBytes; + const auto keyBytes = DccKeyBytes(guestBytes); + for (const auto& other : others) { + if (!other->blockUnits || (other->descriptor.dccAddress != 0 && other->uploadedKeys != DccKeys::Uncompressed)) return std::nullopt; + if (other->descriptor.dccAddress != 0 && other->descriptor.dccAddress < descriptor.dccAddress + keyBytes && descriptor.dccAddress < other->descriptor.dccAddress + DccKeyBytes(other->guestBytes)) return std::nullopt; + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + if (!other->layerPending[unit]) continue; + const auto from = other->layerBegin(unit); + const auto to = from + other->layerBytes(unit); + if (to <= begin || from >= end) continue; + if (from < begin || to > end || other->layerBytes(unit) != trackedLayerBytes) return std::nullopt; + } + } + const auto keys = ProvedKeys(); + VkClearColorValue clear{}; + if (!IsDccClear(keys) || !ClearColorFor(storageFormat, keys, clear)) return std::nullopt; + if (handedUnits.size() != trackedLayers) handedUnits.assign(trackedLayers, false); + for (const auto& other : others) { + for (std::uint32_t unit = 0; unit < other->trackedLayers; ++unit) { + const auto from = other->layerBegin(unit); + if (other->layerPending[unit] && from >= begin && from < end) handedUnits[static_cast((from - begin) / trackedLayerBytes)] = true; + } + const auto same = std::find_if(handedOver.begin(), handedOver.end(), [&](const auto& entry) { return entry.first.lock() == other; }); + if (same != handedOver.end()) same->second = other->writes; + else handedOver.emplace_back(other, other->writes); + } + clearHandOvers.fetch_add(1, std::memory_order_relaxed); + return keys; +} + +void StorageTexture::supersedeHandedOver(std::uint32_t first, std::uint32_t count) { + const auto begin = layerBegin(first); + const auto end = first + count >= trackedLayers ? descriptor.baseAddress + guestBytes : layerBegin(first + count); + for (auto entry = handedOver.begin(); entry != handedOver.end();) { + const auto source = entry->first.lock(); + if (source == nullptr || source->writes != entry->second) { + entry = handedOver.erase(entry); + continue; + } + bool superseded = false; + for (std::uint32_t unit = 0; unit < source->trackedLayers; ++unit) { + const auto from = source->layerBegin(unit); + if (!source->layerPending[unit] || from < begin || from + source->layerBytes(unit) > end) continue; + source->layerPending[unit] = false; + handedOverSuperseded.fetch_add(1, std::memory_order_relaxed); + superseded = true; + } + if (superseded) source->reconcilePending(); + ++entry; + } + if (first == 0 && count >= trackedLayers) handedOver.clear(); + if (std::any_of(handedUnits.begin() + first, handedUnits.begin() + std::min(first + count, handedUnits.size()), [](bool handed) { return handed; })) { + const auto current = GuestMemory::CollectWrites(descriptor.baseAddress, static_cast(guestBytes)); + std::vector generations; + blockGenerations(generations); + std::vector stamped(generations.size(), 0); + std::vector cpu(generations.size(), 0); + const bool tracked = GuestMemory::ChangedBlocks(descriptor.baseAddress, static_cast(guestBytes), generations, stamped, cpu); + bool moved = false; + for (std::uint32_t unit = first; unit < first + count && unit < handedUnits.size(); ++unit) { + if (!handedUnits[unit]) continue; + handedUnits[unit] = false; + if (!tracked || unit >= cpu.size() || cpu[unit] != 0) continue; + layerGeneration[unit] = current; + moved = true; + } + if (moved) refreshGeneration(); + } + if (std::none_of(handedUnits.begin(), handedUnits.end(), [](bool handed) { return handed; })) handedUnits.clear(); +} + std::shared_ptr StorageTexture::pendingAlias() const { static const bool disabled = std::getenv("APS5_NO_ALIAS_BORROW") != nullptr; if (disabled || !blockUnits) return nullptr; @@ -5481,7 +5606,7 @@ void StorageTexture::release() noexcept { if (proxyMemory) context.Function("vkFreeMemory")(context.device, proxyMemory, nullptr); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); - if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + ReleaseImageMemory(context, memory, memoryBytes, memoryType); } std::uint64_t StorageTexture::GuestBytes() const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp index 0bd34f6890..74621bdba1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureDetiler.cpp @@ -89,10 +89,7 @@ TextureDetiler::TextureDetiler(const Context& context) : context(context) { pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &module), "vkCreateShaderModule"); + module = CreateShaderModule(context, TEXTURE_DETILE_SPV, sizeof(TEXTURE_DETILE_SPV), "vkCreateShaderModule"); bindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &layoutInfo, nullptr, &imageDescriptorLayout), "vkCreateDescriptorSetLayout"); pipelineLayoutInfo.pSetLayouts = &imageDescriptorLayout; @@ -168,10 +165,7 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem std::copy(equation->bits.begin(), equation->bits.end(), values.begin() + 4); } if (image && imageModule == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_DETILE_IMAGE_SPV); - moduleInfo.pCode = TEXTURE_DETILE_IMAGE_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &imageModule), "vkCreateShaderModule"); + imageModule = CreateShaderModule(context, TEXTURE_DETILE_IMAGE_SPV, sizeof(TEXTURE_DETILE_IMAGE_SPV), "vkCreateShaderModule"); } std::array entries{}; for (std::uint32_t index = 0; index < entries.size(); ++index) entries[index] = {index, index * 4u, 4}; @@ -273,10 +267,7 @@ void TextureDetiler::DispatchRepack(VkCommandBuffer commands, VkBuffer source, s Require(commands != VK_NULL_HANDLE && source != VK_NULL_HANDLE && destination != VK_NULL_HANDLE, "a texel repack needs a command buffer and its buffers"); Require(bytes != 0 && bytes % 4u == 0 && sourceOffset % 4u == 0 && destinationOffset % 4u == 0, "a texel repack needs whole aligned words"); if (repackPipeline == VK_NULL_HANDLE) { - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(TEXTURE_REPACK_SPV); - moduleInfo.pCode = TEXTURE_REPACK_SPV; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &repackModule), "vkCreateShaderModule repack"); + repackModule = CreateShaderModule(context, TEXTURE_REPACK_SPV, sizeof(TEXTURE_REPACK_SPV), "vkCreateShaderModule repack"); VkPipelineShaderStageCreateInfo stage{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; stage.module = repackModule; @@ -347,11 +338,7 @@ void TextureDetiler::DispatchCmaskClear(VkCommandBuffer commands, VkBuffer cmask pipelineLayoutInfo.pushConstantRangeCount = 1; pipelineLayoutInfo.pPushConstantRanges = &pushRange; Check(context.Function("vkCreatePipelineLayout")(context.device, &pipelineLayoutInfo, nullptr, &cmaskPipelineLayout), "vkCreatePipelineLayout CMASK clear"); - VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; - moduleInfo.codeSize = sizeof(CMASK_CLEAR_SPV); - moduleInfo.pCode = CMASK_CLEAR_SPV; - VkShaderModule cmaskModule = VK_NULL_HANDLE; - Check(context.Function("vkCreateShaderModule")(context.device, &moduleInfo, nullptr, &cmaskModule), "vkCreateShaderModule CMASK clear"); + VkShaderModule cmaskModule = CreateShaderModule(context, CMASK_CLEAR_SPV, sizeof(CMASK_CLEAR_SPV), "vkCreateShaderModule CMASK clear"); VkComputePipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO}; pipelineInfo.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}; pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp index 61a1612f87..93f5ab6b6b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaDevice.cpp @@ -1,12 +1,15 @@ #include "BdaShader.hpp" #include "ColorTransferTests.hpp" +#include "HostImportFailure.hpp" #include #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include +#include #include #include +#include #include #include #include @@ -23,7 +26,7 @@ using namespace AgcDriver::Graphics; class Device { public: - Device() { + explicit Device(bool importHostMemory = false) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -86,11 +89,13 @@ public: VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; address.pNext = &bytes; - const std::array extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; + const bool externalHost = importHostMemory && std::any_of(available.begin(), available.end(), [](const auto& item) { return std::strcmp(item.extensionName, VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) == 0; }); + if (externalHost) extensionsEnabled.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME); VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, &address}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; - device.enabledExtensionCount = 2; + device.enabledExtensionCount = static_cast(extensionsEnabled.size()); device.ppEnabledExtensionNames = extensionsEnabled.data(); device.pEnabledFeatures = &enabled; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); @@ -99,6 +104,13 @@ public: VkPhysicalDeviceProperties properties{}; function("vkGetPhysicalDeviceProperties")(context.physical, &properties); context.limits = properties.limits; + if (externalHost) { + VkPhysicalDeviceExternalMemoryHostPropertiesEXT host{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 extended{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &host}; + function("vkGetPhysicalDeviceProperties2")(context.physical, &extended); + context.hostImportAlignment = host.minImportedHostPointerAlignment; + } + if (importHostMemory) std::printf("host import device %s vendor=0x%04x device=0x%04x driver=0x%08x alignment=%llu\n", properties.deviceName, properties.vendorID, properties.deviceID, properties.driverVersion, static_cast(context.hostImportAlignment)); context.bufferDeviceAddress = true; context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; @@ -143,7 +155,8 @@ private: int main(int argc, char** argv) { try { RunBdaContractTests(); - if (argc == 2) { + const bool importFailures = argc == 2 && std::strcmp(argv[1], "--host-import-failures") == 0; + if (argc == 2 && !importFailures) { const auto shader = MakeBdaTestShader(0x7fff12340000ULL, 32); std::ofstream file(argv[1], std::ios::binary); file.write(reinterpret_cast(shader.data()), static_cast(shader.size() * sizeof(std::uint32_t))); @@ -152,12 +165,20 @@ int main(int argc, char** argv) { } std::unique_ptr device; try { - device = std::make_unique(); + device = std::make_unique(importFailures); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; return 77; } + if (importFailures) { + if (device->GetContext().hostImportAlignment == 0) { + std::cout << "skipped, no external host memory import support\n"; + return 77; + } + AgcDriver::Tests::RunHostImportFailureTests(device->GetContext(), true); + return 0; + } Buffer buffer(device->GetContext(), 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); Require(buffer.DeviceAddress() != 0 && buffer.Bytes().size() == 256, "invalid real BDA buffer"); buffer.Bytes()[255] = std::byte{0x5a}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index e089fa2a6a..abdf8f0166 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -3,6 +3,7 @@ #include "AllocationFailure.hpp" #include "SlabReturn.hpp" #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include using namespace AgcDriver::Graphics; @@ -895,6 +897,110 @@ void DeviceBindingFailures() { } +VkDeviceMemory reclaimImage = VK_NULL_HANDLE; +const Context* reclaimImageContext = nullptr; + +VkDeviceSize mockImageReclaim(VkDeviceSize) { + if (reclaimImage == VK_NULL_HANDLE) return 0; + ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); + reclaimImage = VK_NULL_HANDLE; + return 10 * MiB; +} + +void ImageMemoryReclaim() { + mock = MockDevice{}; + auto context = mockContext(); + context.cacheReclaim = mockImageReclaim; + reclaimImageContext = &context; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 10 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + reclaimImage = AllocateImageMemory(context, allocation, 1u, "test image", &type); + mock.limit = 12 * MiB; + auto request = allocation; + request.allocationSize = 4 * MiB; + const auto before = mock.attempts; + try { + const auto recovered = AllocateImageMemory(context, request, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.attempts == before + 2 && mock.liveBytes == 4 * MiB && context.bufferPool->RetainedImages() == 0, "the image memory a cache release returns to the pool is freed before the retry"); + ReleaseImageMemory(context, recovered, 4 * MiB, 0); + } catch (const std::exception& error) { + Expect(false, std::string("the cache release of an image did not permit recovery: ") + error.what()); + } + reclaimImageContext = nullptr; +} + +void ImageMemoryOverBudget() { + mock = MockDevice{}; + auto context = budgetContext(); + reportedBudget = 1000 * MiB; + reportedUsage = 500 * MiB; + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 0; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "image memory is retained while the heap is under its reported budget"); + reportedUsage = 1100 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto second = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(second == first && mock.allocations == 1, "a retained allocation is still handed out over the budget"); + ReleaseImageMemory(context, second, 4 * MiB, 0); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 0, "image memory released while the heap is over its reported budget is freed"); + const auto third = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, third, 4 * MiB, 0); + reportedUsage = 500 * MiB; + std::this_thread::sleep_for(std::chrono::milliseconds(150)); + const auto fourth = AllocateImageMemory(context, allocation, 1u, "test image", &type); + ReleaseImageMemory(context, fourth, 4 * MiB, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again once the heap is under its budget"); +} + +void ImageMemoryReuse() { + mock = MockDevice{}; + auto context = mockContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto first = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(type == 0 && mock.allocations == 1, "the first image allocates device memory of its type"); + ReleaseImageMemory(context, first, 4 * MiB, 0); + Expect(mock.frees == 0 && context.bufferPool->RetainedImages() == 1, "a released device-local image allocation is retained"); + const auto again = AllocateImageMemory(context, allocation, 1u, "test image", &type); + Expect(again == first && type == 0 && mock.allocations == 1 && context.bufferPool->RetainedImages() == 0, "the next image of the same type and size takes the retained allocation"); + auto smaller = allocation; + smaller.allocationSize = 2 * MiB; + const auto other = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(other != again && mock.allocations == 2, "an image of another size allocates"); + ReleaseImageMemory(context, other, 2 * MiB, 0); + ReleaseImageMemory(context, again, 4 * MiB, 0); + auto host = allocation; + host.memoryTypeIndex = 1; + const auto placed = AllocateImageMemory(context, host, 2u, "test image", &type); + Expect(type == 1 && mock.allocations == 3, "a host-visible image allocation is made"); + ReleaseImageMemory(context, placed, 4 * MiB, 1); + Expect(mock.frees == 1 && context.bufferPool->RetainedImages() == 2, "a host-visible image allocation is freed when released"); + const auto unsized = AllocateImageMemory(context, smaller, 1u, "test image", &type); + Expect(unsized == other && mock.allocations == 3 && context.bufferPool->RetainedImages() == 1, "an image of a retained size takes that allocation"); + ReleaseImageMemory(context, unsized, 0, 0); + Expect(mock.frees == 2 && context.bufferPool->RetainedImages() == 1, "an allocation of unknown size is freed"); + std::vector many; + for (int i = 0; i < 33; ++i) many.push_back(AllocateImageMemory(context, allocation, 1u, "test image", &type)); + Expect(many.front() == again && mock.allocations == 35, "a burst takes the retained allocation first and allocates the rest"); + for (const auto memory : many) ReleaseImageMemory(context, memory, 4 * MiB, 0); + Expect(context.bufferPool->RetainedImages() == 32 && mock.frees == 3 && !mock.memoryBytes.contains(many.front()) && mock.memoryBytes.contains(many.back()), "retained image memory is evicted least recently used under its budget"); + mock.results.push_back(VK_ERROR_OUT_OF_DEVICE_MEMORY); + auto third = allocation; + third.allocationSize = 3 * MiB; + const auto recovered = AllocateImageMemory(context, third, 1u, "test image", &type); + Expect(recovered != VK_NULL_HANDLE && mock.allocations == 36 && mock.frees == 35 && context.bufferPool->RetainedImages() == 0, "an allocation failure frees the retained image memory"); + ReleaseImageMemory(context, recovered, 3 * MiB, 0); + Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); +} + int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { @@ -937,6 +1043,9 @@ int main(int argc, char** argv) { PartialSlabDoesNotRetry(); CacheReclaimRecovery(); ImageCacheReclaim(); + ImageMemoryReuse(); + ImageMemoryReclaim(); + ImageMemoryOverBudget(); HostPlacement(); RefusedDevicePlacement(); AttachmentReclaim(); diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp new file mode 100644 index 0000000000..0f3fe85da7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = std::span(capabilities).first(flushCapability ? capabilities.size() : capabilities.size() - 1); + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float32DenormPreserve = preserve; + request.target.float32FlushToZero = flush; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +struct Modes { + bool preserve32 = false; + bool flush32 = false; + bool preserveCapability = false; +}; + +Modes scan(std::span words) { + Modes modes; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = static_cast(words[cursor] & 0xffffu); + require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityDenormPreserve) modes.preserveCapability = true; + if (op == spv::OpExecutionMode && count == 4 && words[cursor + 3] == 32u) { + if (words[cursor + 2] == spv::ExecutionModeDenormPreserve) modes.preserve32 = true; + if (words[cursor + 2] == spv::ExecutionModeDenormFlushToZero) modes.flush32 = true; + } + cursor += count; + } + return modes; +} + +void verify() { + const auto preserved = scan(recompile(true, false)); + require(preserved.preserve32 && preserved.preserveCapability && !preserved.flush32, "float32DenormPreserve did not emit the 32-bit DenormPreserve execution mode and capability"); + const auto plain = scan(recompile(false, false)); + require(!plain.preserve32 && !plain.flush32, "a target without float32DenormPreserve or float32FlushToZero set a 32-bit denormal execution mode"); + const auto flushed = scan(recompile(true, true)); + require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); + const auto nativeFlush = scan(recompile(true, true, false)); + require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); +} + +} + +int main() { + try { + verify(); + } catch (const std::exception& error) { + std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); + return 1; + } + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp new file mode 100644 index 0000000000..f51d28a2bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/Float32FlushControl.cpp @@ -0,0 +1,37 @@ +#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp" +#include + +namespace { + +struct Case { + VkDriverId driver; + VkBool32 flushToZero; + VkShaderFloatControlsIndependence independence; + bool probe; + bool declared; +}; + +constexpr Case Cases[]{ + {VK_DRIVER_ID_MOLTENVK, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, true, false}, + {VK_DRIVER_ID_MOLTENVK, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, + {VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false}, + {VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true}, + {static_cast(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false}, +}; + +} + +int main() { + int failures = 0; + for (const auto& item : Cases) { + const auto control = AgcDriver::Float32FlushControlFor(item.driver, item.flushToZero, item.independence); + if (control.probe != item.probe || control.declared != item.declared) { + std::fprintf(stderr, "driver %d flush %u independence %d: expected probe %d declared %d, got %d %d\n", static_cast(item.driver), static_cast(item.flushToZero), static_cast(item.independence), item.probe ? 1 : 0, item.declared ? 1 : 0, control.probe ? 1 : 0, control.declared ? 1 : 0); + ++failures; + } + } + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..4a79bf8bdb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -521,6 +521,12 @@ void stateTests() { queue.shader[0x008] = 0; Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "a zero pixel program address was not read as unset"); Require(AgcDriver::Graphics::DrawRejection(queue, true).find("writes color") != std::string::npos, "a draw without a pixel program that writes color was accepted"); + auto colorDisabled = queue; + colorDisabled.context[0x202] = colorDisabled.context.at(0x202) & ~0x70u; + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).empty(), "a draw without a pixel program was rejected while CB_COLOR_CONTROL disables color"); + Require(!AgcDriver::Graphics::DecodeState(colorDisabled).hasColorTarget, "a draw with CB_COLOR_CONTROL disabling color decoded a color target"); + colorDisabled.context.erase(0x202); + Require(AgcDriver::Graphics::DrawRejection(colorDisabled, true).find("writes color") != std::string::npos, "a draw without a pixel program and an unwritten CB_COLOR_CONTROL was accepted"); queue.shader.erase(0x008); queue.shader.erase(0x009); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "an unwritten pixel program address was not read as unset"); @@ -8393,6 +8399,25 @@ void LineStripTests() { Require(rejection.empty(), "the draw precheck rejected an indexed line strip with primitive restart: " + rejection); } +std::uint32_t keyedGenerator = 0; +std::size_t keyedWords = 0; + +VKAPI_ATTR VkResult VKAPI_CALL keyedCreateShaderModule(VkDevice, const VkShaderModuleCreateInfo* info, const VkAllocationCallbacks*, VkShaderModule* module) { + keyedWords = info->codeSize / sizeof(std::uint32_t); + keyedGenerator = info->pCode[2]; + *module = makeHandle(); + return VK_SUCCESS; +} + +void ShaderModuleKeyTests() { + const std::array code{0x07230203u, 0x00010300u, 0x00080001u, 8u, 0u}; + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, true, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == 0x00080001u, "a shader module with graphics pipeline libraries changed its generator word"); + static_cast(AgcDriver::Graphics::CreateShaderModule(keyedCreateShaderModule, VK_NULL_HANDLE, false, code.data(), sizeof(code), "keyed module")); + Require(keyedWords == code.size() && keyedGenerator == (0x00080001u ^ 0x8000u), "a shader module without graphics pipeline libraries kept the generator word of the other mode"); + Require(code[2] == 0x00080001u, "keying a shader module changed its source words"); +} + void ProvokingVertexTests() { using namespace AgcDriver::Graphics; constexpr std::uint32_t lastVertex = 1u << 19u; @@ -9506,6 +9531,24 @@ void highestDrawIndexTests() { Require(HighestDrawIndex(bytesOf(restartOnly), 2, false) == 0xffff, "a 16-bit all-ones index without restart was skipped"); Require(HighestDrawIndex(bytesOf(narrow).first(4), 2, true) == 7 && HighestDrawIndex(bytesOf(narrow).first(2), 2, true) == 0, "the scan read past its index range"); expectFailure([&] { HighestDrawIndex(bytesOf(narrow), 1, false); }, "unsupported index size"); + Require(!HighestDrawIndex({}, 2, false).has_value() && !HighestDrawIndex({}, 4, true).has_value(), "an empty index range reached a vertex"); + std::vector long16(1027, 0xffff); + long16[1026] = 0; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 0, "index 0 after many restart indices was not reached"); + long16[513] = 4000; + Require(HighestDrawIndex(std::as_bytes(std::span(long16)), 2, true) == 4000, "a 16-bit index inside a long range was missed"); + std::vector long32(1029); + for (std::size_t i = 0; i < long32.size(); ++i) long32[i] = static_cast(i * 7u % 1000u); + long32[1028] = 0x80000001u; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 0x80000001u, "a 32-bit index above 2^31 in the tail was missed"); + long32[1028] = 0xffffffffu; + Require(HighestDrawIndex(std::as_bytes(std::span(long32)), 4, true) == 999u && HighestDrawIndex(std::as_bytes(std::span(long32)), 4, false) == 0xffffffffu, "a 32-bit restart index at the end of a long range"); + std::vector shifted(2 * 1000 + 3); + for (std::size_t i = 0; i < 1000; ++i) { + const auto value = static_cast(i == 777 ? 0x8001u : i % 300u); + std::memcpy(shifted.data() + 1 + 2 * i, &value, sizeof(value)); + } + Require(HighestDrawIndex(std::span(shifted).subspan(1, 2 * 1000 + 1), 2, false) == 0x8001u, "an unaligned 16-bit range with a trailing byte"); } void storeAtFlipTests() { @@ -10391,6 +10434,7 @@ int main(int argc, char** argv) { stencilReferenceUnusedTests(); aliasedBufferViewTests(); LineStripTests(); + ShaderModuleKeyTests(); ProvokingVertexTests(); ProvokingVertexRenderPassTests(); ConservativeRasterizationTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp index 64b394f21b..6ac8547f1c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostHdr.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -125,6 +126,29 @@ void hostState() { Expect(!AgcDriver::CurrentHostHdr().available, "a display switched to SDR leaves HDR output available"); } +void forceSdr() { + resetHost(); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", "1"); +#else + setenv("APS5_FORCE_SDR", "1", 1); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + AgcDriverSetHostDisplayHdr_nid_postfix(AgcDriver::DisplayHdrFromOutput(true, 800.0f, 0.01f, 400.0f)); + const auto forced = AgcDriver::CurrentHostHdr(); + Expect(!forced.available && forced.display.hdr && forced.surfaceHdr10, "forcing SDR does not suppress HDR availability while preserving host capabilities"); + Expect(AgcDriver::ToneMapPeakNits(forced) == 100.0f, "forcing SDR does not report SDR reference white"); +#ifdef _WIN32 + _putenv_s("APS5_FORCE_SDR", ""); +#else + unsetenv("APS5_FORCE_SDR"); +#endif + AgcDriverSetSurfaceHdr10_nid_postfix(true); + const auto restored = AgcDriver::CurrentHostHdr(); + Expect(restored.available && restored.generation != forced.generation, "removing the SDR override does not restore HDR availability"); + resetHost(); +} + void reportedWhiteLevel() { resetHost(); constexpr std::uint64_t rgbPq = 0x8100070422000000ull; @@ -158,6 +182,7 @@ int main() { whiteLevels(); hostState(); reportedWhiteLevel(); + forceSdr(); if (failures != 0) return 1; std::puts("host HDR tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp new file mode 100644 index 0000000000..939c31b830 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.cpp @@ -0,0 +1,176 @@ +#include "HostImportFailure.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +enum class Step { None, Create, Allocate, Bind }; + +struct Probe { + PFN_vkGetDeviceProcAddr backend = nullptr; + Step fail = Step::None; + VkResult result = VK_SUCCESS; + unsigned injected = 0; + unsigned invalid = 0; + std::set buffers; + std::set memories; +}; + +Probe probe; + +VKAPI_ATTR VkResult VKAPI_CALL CreateBuffer(VkDevice device, const VkBufferCreateInfo* info, const VkAllocationCallbacks* allocator, VkBuffer* buffer) { + if (probe.fail == Step::Create) { + probe.fail = Step::None; + ++probe.injected; + *buffer = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkCreateBuffer"))(device, info, allocator, buffer); + if (result == VK_SUCCESS) probe.buffers.insert(*buffer); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateMemory(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (probe.fail == Step::Allocate) { + probe.fail = Step::None; + ++probe.injected; + *memory = reinterpret_cast(std::uintptr_t{0xdeadbeef}); + return probe.result; + } + const auto result = reinterpret_cast(probe.backend(device, "vkAllocateMemory"))(device, info, allocator, memory); + if (result == VK_SUCCESS) probe.memories.insert(*memory); + return result; +} + +VKAPI_ATTR VkResult VKAPI_CALL BindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) { + if (probe.fail == Step::Bind) { + probe.fail = Step::None; + ++probe.injected; + return probe.result; + } + return reinterpret_cast(probe.backend(device, "vkBindBufferMemory"))(device, buffer, memory, offset); +} + +VKAPI_ATTR void VKAPI_CALL DestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + if (probe.buffers.erase(buffer) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL FreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + if (probe.memories.erase(memory) != 1) { + ++probe.invalid; + return; + } + reinterpret_cast(probe.backend(device, "vkFreeMemory"))(device, memory, allocator); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreateBuffer") == 0) return reinterpret_cast(CreateBuffer); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(AllocateMemory); + if (std::strcmp(name, "vkBindBufferMemory") == 0) return reinterpret_cast(BindBufferMemory); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(DestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(FreeMemory); + return probe.backend(device, name); +} + +constexpr std::size_t Alignment = 65536; +constexpr std::size_t Bytes = 4096; + +class Heap { +public: + Heap() { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, Alignment, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(Alignment, Alignment)); +#endif + Require(data != nullptr, "cannot allocate the host import failure heap"); + std::memset(data, 0x37, Alignment); + GuestAllocations::Mutation().Add(data, Alignment, true, true, true); + } + + ~Heap() { + GuestAllocations::Mutation().Remove(data); +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + + std::byte* Data() const { return data; } + std::uint64_t Address() const { return reinterpret_cast(data); } + +private: + std::byte* data = nullptr; +}; + +} + +void AgcDriver::Tests::RunHostImportFailureTests(const Graphics::Context& original, bool native) { + unsigned failures = 0; + for (const auto step : {Step::Create, Step::Allocate, Step::Bind}) { + for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) { + Heap heap; + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + ClearHostImports(original.device); + probe = {}; + probe.backend = original.deviceProc; + probe.fail = step; + probe.result = result; + auto context = original; + context.deviceProc = DeviceProc; + struct Cleanup { + VkDevice device; + ~Cleanup() { ClearHostImports(device); } + } cleanup{context.device}; + SetImportWatch(context, ImportWatch::Unwatch); + const HostImport* failed = nullptr; + bool threw = false; + try { + failed = HostImportFor(context, heap.Address(), Bytes); + } catch (const std::runtime_error&) { + threw = true; + } + const bool rejected = failed == nullptr && probe.injected == 1 && ExistingHostImportFor(context, heap.Address(), Bytes) == nullptr && HostImportLiveBytes() == 0; + const bool released = probe.buffers.empty() && probe.memories.empty(); + const auto invalid = probe.invalid; + ClearHostImports(context.device); + probe.fail = Step::None; + const auto* recovered = HostImportFor(context, heap.Address(), Bytes); + Require(recovered != nullptr && recovered->buffer != VK_NULL_HANDLE && recovered->memory != VK_NULL_HANDLE && recovered->address != 0, "host import retry failed"); + if (native) { + Buffer readback(original, Bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + std::memset(readback.Bytes().data(), 0, Bytes); + CommandBatch batch(original); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + CopyBuffer(original, batch.Handle(), recovered->buffer, heap.Address() - recovered->base, readback.Handle(), 0, Bytes); + RecordMemoryBarrier(original, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + batch.SubmitAndWait(); + readback.Invalidate(); + Require(std::memcmp(heap.Data(), readback.Bytes().data(), Bytes) == 0, "host import retry returned incorrect GPU readback data"); + } + ClearHostImports(context.device); + const bool clean = probe.buffers.empty() && probe.memories.empty() && HostImportLiveBytes() == 0; + std::printf("host import step=%u result=%d injected=%u rejected=%u threw=%u invalid releases=%u failure cleaned=%u retry cleaned=%u readbacks=%u\n", static_cast(step), static_cast(result), probe.injected, rejected, threw, invalid, released, clean, native ? 1u : 0u); + if (!rejected || !released || !clean || invalid != 0 || probe.invalid != invalid) ++failures; + } + } + Require(failures == 0, "failed host import creation released an undefined handle or leaked a resource"); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp new file mode 100644 index 0000000000..70364786ad --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportFailure.hpp @@ -0,0 +1,12 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_HOSTIMPORTFAILURE_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Tests { + +void RunHostImportFailureTests(const Graphics::Context& context, bool native); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp index 92c6fe639a..8d0d9cddc7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/HostImportRetireHold.cpp @@ -1,3 +1,4 @@ +#include "HostImportFailure.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -372,6 +373,7 @@ int main() { WindowPromotionOutlivesTheHold(); IdleRetireOutlivesTheHold(); BusyRetireOutlivesALaterBatch(); + AgcDriver::Tests::RunHostImportFailureTests(mockContext(), false); } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3c749730b7..9c1ae9ae14 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -117,6 +117,54 @@ void ReleaseWatched(void* block, std::size_t bytes) { #endif } +bool partialWatchRestorationTests() { +#ifdef _WIN32 + constexpr std::size_t bytes = 131072; + constexpr std::size_t page = 4096; + constexpr std::size_t restoredOffset = 4 * page; + constexpr std::size_t restoredBytes = 4 * page; + if (!AgcDriver::GuestMemory::WriteWatched()) return false; + auto* memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory == nullptr) return false; + GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, restoredBytes); + struct Cleanup { + std::uint8_t* memory; + ~Cleanup() { ReleaseWatched(memory, bytes); } + } cleanup{memory}; + const auto address = reinterpret_cast(memory); + AgcDriver::GuestMemory::Unwatch(address, 65536); + Require(!AgcDriver::GuestMemory::Watched(address, page), "the partial remap fixture was not excluded"); + memory[page] = 0x22; + AgcDriver::GuestMemory::MarkWritten(address + page, 1); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) != 0, "the excluded remap fixture has no driver hint"); + GuestArena::GuestArenaReset_nid_postfix(memory + restoredOffset, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory + restoredOffset, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address + restoredOffset, restoredBytes), "a partial private remap did not restore its write watch"); + Require(!AgcDriver::GuestMemory::Watched(address, restoredOffset), "a partial remap restored an excluded prefix"); + Require(!AgcDriver::GuestMemory::Watched(address + restoredOffset + restoredBytes, page), "a partial remap restored an excluded suffix"); + Require(AgcDriver::GuestMemory::UntrackedHint(address + page, 1) == 0, "a partial remap retained a stale driver hint"); + std::memset(memory + restoredOffset, 0x33, restoredBytes); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto generation = AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes); + Require(generation != 0, "the private remap has no collected CPU write history"); + memory[restoredOffset + page] = 0x44; + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(AgcDriver::GuestMemory::CollectWrites(address + restoredOffset, restoredBytes) != 0, "the private remap stopped collecting CPU writes"); + Require(!AgcDriver::GuestMemory::UnchangedSince(address + restoredOffset, restoredBytes, generation), "the private remap hid a genuine CPU write"); + GuestArena::GuestArenaReset_nid_postfix(memory, restoredBytes); + GuestArena::GuestArenaCommit_nid_postfix(memory, restoredBytes, PAGE_READWRITE, restoredBytes); + Require(AgcDriver::GuestMemory::Watched(address, restoredBytes), "the first private pages did not regain coverage"); + Require(!AgcDriver::GuestMemory::Watched(address + 12 * page, page), "restoring the first pages restored an excluded tail"); + memory[12 * page] = 0x55; + AgcDriver::GuestMemory::MarkWritten(address + 12 * page, 1); + AgcDriver::GuestMemory::Unwatch(address + 12 * page, page); + Require(AgcDriver::GuestMemory::UntrackedHint(address + 12 * page, 1) == 0, "excluding a partially watched block invented a write-watch hint"); + return true; +#else + return false; +#endif +} + std::optional selectDevice(std::span devices, std::string_view request) { const auto rankDeviceType = [](VkPhysicalDeviceType type) { switch (type) { @@ -6630,7 +6678,32 @@ void queuedWriteGroupTests() { } return memory; }; + const auto recorded = [](const std::vector& writes) { + const auto order = Recorder::QueuedWriteOrder(writes); + std::vector ordered; + for (const auto index : order) ordered.push_back(writes[index]); + const auto groups = Recorder::QueuedWriteGroups(ordered); + std::vector sequence; + std::size_t first = 0; + for (const auto end : groups) { + for (const bool computed : {false, true}) { + for (auto i = first; i < end; ++i) { + if (ordered[i].computed == computed) sequence.push_back(order[i]); + } + } + first = end; + } + return std::pair{sequence, groups.size()}; + }; Require(Recorder::QueuedWriteGroups({}).empty(), "no queued writes made a group"); + const std::vector flagged{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}, {0x500, 0x508, false}, {0x800, 0x804, false}}; + Require(recorded(flagged) == std::pair{std::vector{0, 4, 1, 2, 5, 6, 3, 7, 8}, std::size_t{2}}, "the dumps of a run with a flag after each query slot were not folded once, with the clears before them and the flags after them"); + const std::vector open{{0x50, 0x58, false}, {0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x208, false}, {0x400, 0x500, false}, {0x400, 0x4f8, true}}; + Require(recorded(open) == std::pair{std::vector{0, 1, 5, 2, 3, 6, 4}, std::size_t{2}}, "the clear of a slot whose end dump the run does not hold was not moved ahead of the dumps"); + const std::vector overDump{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x1f0, 0x410, false}, {0x400, 0x4f8, true}}; + Require(Recorder::QueuedWriteOrder(overDump) == std::vector{0, 1, 2, 3, 4}, "a store over an earlier dump that the later dumps cover only in part was moved ahead of that dump"); + const std::vector overFlag{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x500, 0x508, false}, {0x400, 0x508, false}, {0x400, 0x4f8, true}, {0x408, 0x500, true}}; + Require(Recorder::QueuedWriteOrder(overFlag) == std::vector{0, 1, 2, 3, 4, 5, 6}, "a clear over an earlier store after a dump was moved ahead of that store"); const std::vector query{{0x100, 0x200, false}, {0x100, 0x1f8, true}, {0x108, 0x200, true}, {0x200, 0x300, false}, {0x200, 0x2f8, true}, {0x208, 0x300, true}}; Require(Recorder::QueuedWriteGroups(query) == Groups{3, 6}, "the clear of the next query slot did not wait for the dumps before it"); const std::vector reused{{0x100, 0x1f8, true}, {0x100, 0x200, false}, {0x100, 0x1f8, true}}; @@ -6660,6 +6733,26 @@ void queuedWriteGroupTests() { const auto groups = Recorder::QueuedWriteGroups(writes); Require(!groups.empty() && groups.back() == writes.size() && std::is_sorted(groups.begin(), groups.end()), "queued write groups do not end at the last write in order"); Require(paint(writes, groups, bytes) == programOrder(writes, bytes), "recording each group's stores before its computed writes changed the final bytes of the program order"); + const auto sequence = recorded(writes).first; + std::vector position(writes.size(), writes.size()); + for (std::size_t i = 0; i < sequence.size(); ++i) position[sequence[i]] = i; + Require(sequence.size() == writes.size() && std::find(position.begin(), position.end(), writes.size()) == position.end(), "the recorded order is not a permutation of the queued writes"); + std::vector memory(bytes, 0); + for (const auto index : sequence) { + for (auto at = writes[index].begin; at < writes[index].end; ++at) memory[at] = static_cast(index + 1); + } + Require(memory == programOrder(writes, bytes), "the recorded order changed the final bytes of the program order"); + for (std::size_t j = 0; j < writes.size(); ++j) { + if (writes[j].computed) continue; + std::vector covered(bytes); + for (auto k = j + 1; k < writes.size(); ++k) { + if (writes[k].computed) std::fill(covered.begin() + writes[k].begin, covered.begin() + writes[k].end, true); + } + if (std::any_of(covered.begin() + writes[j].begin, covered.begin() + writes[j].end, [](bool value) { return value; })) continue; + for (std::size_t i = 0; i < j; ++i) { + Require(position[i] < position[j], writes[i].computed ? "a store no later dump overlaps was recorded before a dump ahead of it" : "a store no later dump overlaps was recorded before a store ahead of it"); + } + } } } @@ -9279,6 +9372,216 @@ void integerClearTests(const Device& device) { std::cout << "integer storage clears: " << tested << " formats ok\n"; } +bool clearHandOverTests(const Device& device, Recorder& recorder, bool watched) { + const auto& base = device.GetContext(); + const std::string what = watched ? "clear-key hand-over (watched)" : "clear-key hand-over"; + if (base.hostImportAlignment == 0) { + std::cout << "host imports unavailable: " << what << " not tested\n"; + return false; + } + constexpr std::size_t surfaceA = 256 * 256 * 4; + constexpr std::size_t surfaceB = 128 * 128 * 8; + constexpr std::size_t keyCount = surfaceB / 256; + constexpr std::size_t bytes = surfaceA + 65536; + static_assert(surfaceB <= surfaceA && keyCount <= bytes - surfaceA, "B lies over the first units of A, and B's DCC keys follow A inside the block"); + const auto decided = PrepareImportWatch(base); +#ifndef _WIN32 + if (watched && decided == ImportWatch::Unwatch) { + std::cout << "host imports are compared on this device: " << what << " not tested\n"; + return false; + } +#endif + void* block = nullptr; + if (watched) { + block = AllocateWatched(bytes, 65536); + if (block == nullptr) { + std::cout << "no write watching: " << what << " not tested\n"; + return false; + } + } else { +#ifdef _WIN32 + block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + block = std::aligned_alloc(65536, bytes); +#endif + } + Require(block != nullptr && reinterpret_cast(block) % 65536 == 0, "cannot allocate the clear-key hand-over block"); + if (watched) SetImportWatch(base, ImportWatch::Watch); + struct RestoreWatch { + const Context& context; + ImportWatch decided; + ~RestoreWatch() { SetImportWatch(context, decided); } + } restoreWatch{base, decided}; + auto* memory = static_cast(block); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + std::uint64_t address; + bool watched; + ~Unregister() { + StorageTexture::SetClearHandOver(true); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, bytes); + if (watched) ReleaseWatched(block, bytes); + } + } unregister{base, block, address, watched}; + if (HostImportFor(base, address, bytes) == nullptr) { + std::cout << "host import of the clear-key hand-over block refused: " << what << " not tested\n"; + return false; + } + if (watched && !AgcDriver::GuestMemory::Watched(address, bytes)) { + std::cout << "the imported block left the write watch: " << what << " not tested\n"; + return false; + } + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + const auto surface = [&](std::uint32_t width, std::uint32_t height, std::uint32_t format, std::uint64_t dcc) { + GuestTextureResource resource{}; + resource.baseAddress = address; + resource.width = width; + resource.height = height; + resource.mipCount = 1; + resource.tileMode = TextureTileMode::kR64KBX; + resource.dimension = TextureDimension::k2D; + resource.format = format; + resource.dstSelX = 4; + resource.dstSelY = 5; + resource.dstSelZ = 6; + resource.dstSelW = 7; + resource.dccAddress = dcc; + return resource; + }; + const auto resourceA = surface(256, 256, 56, 0); + const auto resourceB = surface(128, 128, 71, address + surfaceA); + Require(DescribeSurface(resourceA).guestBytes == surfaceA && DescribeSurface(resourceB).guestBytes == surfaceB, "the clear-key hand-over surfaces have unexpected sizes"); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + const auto draw = [&](const std::shared_ptr& image, 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 readImage = [&](const std::shared_ptr& image, std::uint32_t width, std::uint32_t height, std::size_t texelBytes) { + Buffer readback(context, static_cast(width) * height * texelBytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const auto commands = recorder.Commands(); + 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_READ_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {width, height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image->Image(), VK_IMAGE_LAYOUT_GENERAL, readback.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + const auto pixels = readback.Bytes(); + return std::vector(pixels.begin(), pixels.end()); + }; + const auto readMemory = [&](std::uint64_t from, std::size_t length) { + std::vector read(length); + AgcDriver::GuestMemory::Read(from, read); + recorder.Sync(); + if (!watched) std::memcpy(read.data(), memory + (from - address), length); + return read; + }; + const auto filled = [](std::span read, std::span texel) { + for (std::size_t i = 0; i < read.size(); ++i) { + if (std::to_integer(read[i]) != texel[i % texel.size()]) return false; + } + return true; + }; + constexpr std::array red{255, 0, 0, 255}; + constexpr std::array blue{0, 0, 255, 255}; + constexpr std::array green{0, 0, 0x00, 0x3c, 0, 0, 0x00, 0x3c}; + constexpr std::array cleared{}; + enum class Order { Superseded, ReadFirst, Rewritten, CpuStore }; + struct Outcome { + std::vector bytes; + std::vector imageB; + std::uint64_t handOvers = 0; + std::uint64_t superseded = 0; + }; + const auto run = [&](bool on, Order order) { + const std::string where = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote") + (on ? ", on" : ", off"); + StorageTexture::SetClearHandOver(on); + recorder.Sync(); + std::memset(memory, 0x55, surfaceA); + std::memset(memory + surfaceA, 0x00, keyCount); + Outcome outcome{}; + const auto handOvers = StorageTexture::ClearHandOvers(); + const auto superseded = StorageTexture::HandedOverSuperseded(); + { + auto imageA = StorageTexture::Create(context, detiler, resourceA, 0); + auto imageB = StorageTexture::Create(context, detiler, resourceB, 0); + draw(imageA, {{1.0f, 0.0f, 0.0f, 1.0f}}); + imageB->Refresh(); + if (on && watched) { + Require(StorageTexture::ClearHandOvers() == handOvers + 1, where + ": B's refresh under clear keys did not hand A's results over"); + Require(StorageTexture::FindPending(address, surfaceA) == imageA, where + ": A's results did not stay pending"); + Require(std::all_of(memory, memory + 2 * 65536, [](std::uint8_t value) { return value == 0x55; }), where + ": B's refresh stored A's results over B"); + } else { + Require(StorageTexture::ClearHandOvers() == handOvers, where + ": the hand-over ran while off"); + } + outcome.imageB = readImage(imageB, 128, 128, 8); + Require(filled(outcome.imageB, cleared), where + ": B does not hold its keys' clear"); + if (order == Order::ReadFirst) { + Require(filled(readMemory(address, 65536), red), where + ": a read of A's memory before B wrote it did not see A's results"); + imageA->Refresh(); + Require(filled(readImage(imageA, 256, 256, 4), red), where + ": A read again as A lost its results"); + } + if (order == Order::Rewritten) { + draw(imageA, {{0.0f, 0.0f, 1.0f, 1.0f}}); + Require(filled(readMemory(address, 65536), blue), where + ": a read of A's memory did not see A's newer results"); + imageB->Refresh(); + Require(filled(readImage(imageB, 128, 128, 8), cleared), where + ": B refreshed again does not hold its keys' clear"); + } + if (order == Order::CpuStore) { + std::memset(memory + 65536, 0x77, 65536); + } + draw(imageB, {{0.0f, 1.0f, 0.0f, 1.0f}}); + Require(StorageTexture::FlushPending(address, bytes, nullptr, "clear-key hand-over test", PublishScope::Whole), where + ": nothing was pending at the end"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + outcome.bytes = readMemory(address, surfaceA); + Require(filled(readImage(imageB, 128, 128, 8), green), where + ": B lost its results"); + } + outcome.handOvers = StorageTexture::ClearHandOvers() - handOvers; + outcome.superseded = StorageTexture::HandedOverSuperseded() - superseded; + recorder.Sync(); + return outcome; + }; + for (const auto order : {Order::Superseded, Order::ReadFirst, Order::Rewritten, Order::CpuStore}) { + const std::string name = what + (order == Order::Superseded ? ", A never read again" : order == Order::ReadFirst ? ", A read before B wrote" : order == Order::Rewritten ? ", A written again after the hand-over" : ", a CPU store before B wrote"); + const auto off = run(false, order); + const auto on = run(true, order); + const auto& lastA = order == Order::Rewritten ? blue : red; + if (order != Order::CpuStore) Require(filled(std::span(off.bytes).first(2 * 65536), green), name + ": B's results did not reach guest memory with the hand-over off"); + Require(filled(std::span(off.bytes).subspan(2 * 65536), lastA), name + ": A's results did not reach guest memory with the hand-over off"); + Require(on.imageB == off.imageB, name + ": B held other texels with the hand-over on than off"); + Require(on.bytes == off.bytes, name + ": the hand-over left other guest bytes than the old order"); + Require(off.handOvers == 0 && off.superseded == 0, name + ": the hand-over ran while off"); + if (!watched) { + Require(on.handOvers == 0, name + ": results were handed over in memory the write tracker does not watch"); + continue; + } + if (order == Order::Superseded) Require(on.superseded == 2, name + ": B's results did not drop A's two units under B (dropped " + std::to_string(on.superseded) + ")"); + if (order == Order::ReadFirst) Require(on.superseded <= 1, name + ": a unit stored by the read was dropped again"); + } + std::cout << what << ": ok\n"; + return true; +} + void denormalClearTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (base.hostImportAlignment == 0) { @@ -9641,6 +9944,11 @@ void Run(Context context) { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--partial-watch-restoration-only") { + if (!partialWatchRestorationTests()) return 77; + std::cout << "partial private write-watch restoration tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--device-selection-only") { deviceSelectionTests(); std::cout << "recorder device selection tests passed\n"; @@ -9791,6 +10099,14 @@ int main(int argc, char** argv) { keptUnitSettleTests(device, recorder); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--clear-hand-over-only") { + clearHandOverTests(device, recorder, false); + if (!clearHandOverTests(device, recorder, true)) { + std::cout << "skipped, the device imports no watched host memory for the clear-key hand-over\n"; + return 77; + } + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--integer-clear-only") { integerClearTests(device); return 0; @@ -10026,6 +10342,8 @@ int main(int argc, char** argv) { movedMetadataTests(device, recorder); viewPastLastMipTests(device, recorder); keysFillTests(device, recorder); + clearHandOverTests(device, recorder, false); + clearHandOverTests(device, recorder, true); integerClearTests(device); denormalClearTests(device, recorder); unitShadowTests(device, recorder); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 51c043e6cb..7506189a53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -169,6 +169,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right require(left.shaderDataUserWords == right.shaderDataUserWords, prefix + "shader-data user words differ"); require(left.imageTableRanges == right.imageTableRanges && left.imageTableShader == right.imageTableShader && left.imageTableFaults == right.imageTableFaults && left.imageTablePoison.size() == right.imageTablePoison.size(), prefix + "image table invocation data differs"); require(left.imageTableReadRanges == right.imageTableReadRanges, prefix + "image table read ranges differ"); + require(left.userDataRegisterReads == right.userDataRegisterReads, prefix + "user data register read mask differs"); for (std::size_t i = 0; i < left.imageTablePoison.size(); ++i) { const auto& a = left.imageTablePoison[i]; const auto& b = right.imageTablePoison[i]; @@ -269,6 +270,7 @@ RecompileResult sampleResult() { result.imageTableReadRanges = {{{0x1000u, 32u}, {0x1040u, 32u}}, {}, {{0x5000000000ull, 16u}}}; result.imageTableShader = 0x80003aa700ull; result.imageTableFaults = 3u; + result.userDataRegisterReads = (std::uint64_t{1} << 63u) | (std::uint64_t{1} << 32u) | 0x25u; return result; } @@ -321,6 +323,7 @@ CompiledVariant sampleVariant() { image.flatLineCompatible = false; image.flatVolumeLineCompatible = false; image.foldTexelOffsetsOnly = true; + image.remappedDepth1D = true; image.byElements = 4; image.byComponents = 1; image.emulatedCompare = 0x1a07u; @@ -633,6 +636,43 @@ void verifyPrebuiltAcrossProcesses(const char* self, const std::filesystem::path require(std::system(command.c_str()) == 0, "the prebuilt loading process failed"); } +void verifyPrebuiltDiscovery(std::string_view layout, bool seed) { + using namespace ShaderRecompiler::ShaderDiskCache; + require(layout == "pack" || layout == "loose" || layout == "versioned" || layout == "file" || layout == "empty" || layout == "missing", "unknown prebuilt discovery layout"); + Shutdown(); + SampleRequest sample = prebuiltSample(); + const auto key = sample.Key(); + const auto variant = sampleVariant(); + if (seed) { + const auto directory = ShaderRecompiler::ShaderCacheDirectory() / "prebuilt"; + const auto entry = EncodeEntry(key, variant); + if (layout == "pack") { + auto otherKey = key; + otherKey.back() ^= std::byte{1}; + require(WriteFileAtomically(directory / "a.pack", packBytes({{key, entry}})), "cannot seed the matching prebuilt pack"); + require(WriteFileAtomically(directory / "z.pack", packBytes({{otherKey, EncodeEntry(otherKey, variant)}})), "cannot seed the other prebuilt pack"); + } else if (layout == "loose") { + require(WriteFileAtomically(directory / EntryName(key), entry), "cannot seed the loose prebuilt entry"); + } else if (layout == "versioned") { + require(WriteFileAtomically(directory / EntryDirectory().filename() / EntryName(key), entry), "cannot seed the versioned prebuilt entry"); + } else if (layout == "file") { + require(WriteFileAtomically(directory, packBytes({{key, entry}})), "cannot seed the direct prebuilt pack"); + } else if (layout == "empty") { + require(WriteFileAtomically(directory / "ignored.txt", entry), "cannot seed the ignored prebuilt file"); + } + std::cout << "prebuilt discovery seeded " << layout << '\n'; + return; + } + CompiledVariant loaded; + const bool expected = layout != "empty" && layout != "missing"; + require(Load(key, loaded) == expected, "prebuilt discovery returned the wrong result for " + std::string(layout)); + if (expected) requireSameVariant(variant, loaded, "prebuilt discovery"); + const auto totals = Totals(); + require(totals.prebuiltHits == (expected ? 1u : 0u) && totals.hits == 0 && totals.writes == 0 && Writers() == 0, + "prebuilt discovery did not remain a read-only prebuilt lookup"); + std::cout << "prebuilt discovery verified " << layout << '\n'; +} + void verifyArtifactStorageIsolation() { SampleRequest sample; const auto key = sample.Key(); @@ -704,6 +744,7 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("f32 denormal preservation", [](SampleRequest& sample) { sample.request.target.float32DenormPreserve = true; }); changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); @@ -2434,6 +2475,10 @@ int main(int argc, char** argv) { return 0; } #endif + if (argc == 3 && (std::string_view(argv[1]) == "--seed-prebuilt-discovery" || std::string_view(argv[1]) == "--verify-prebuilt-discovery")) { + verifyPrebuiltDiscovery(argv[2], std::string_view(argv[1]) == "--seed-prebuilt-discovery"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite(); if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess(); if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 09ada8fddd..f19683805c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4848,7 +4848,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); + require(requestPrefix(encoded, 8u) == "NVNQQSkAAAA=", "new requests did not use serialization version 41"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4966,7 +4966,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5059,6 +5059,7 @@ void verifyVertexInfoVersion19() { auto version20Vertex = flagged; version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -5069,6 +5070,7 @@ void verifyVertexInfoVersion19() { auto legacy = plain; legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -5120,6 +5122,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5127,6 +5130,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5138,8 +5142,10 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); auto version36 = sampledBytes; + version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5151,6 +5157,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5159,6 +5166,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5167,6 +5175,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5216,9 +5225,10 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 6, remappedBytes.begin()), "the depth remap flag and mode are not the last 6 bytes before the f32 flush flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5226,6 +5236,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.pop_back(); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -5272,6 +5283,7 @@ void verifyMergedHullConfiguration() { require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); auto legacy = decodeBase64(merged); + legacy.pop_back(); require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { @@ -6883,6 +6895,10 @@ void verifySrtReadPatching() { const auto* replacement = repeatedUse.Argument(0); require(replacement->Opcode() == IrOpcode::ReadConst, "SRT read patching: the uses do not reference a flat read"); require(repeatedUse.Argument(1) == replacement && secondUser.Argument(0) == replacement, "SRT read patching: not every operand uses the same replacement"); + const std::vector order(block.Instructions().begin(), block.Instructions().end()); + const auto readAt = std::find(order.begin(), order.end(), &read); + require(readAt != order.end() && readAt - order.begin() >= 2 && *(readAt - 1) == replacement && *(readAt - 2) == replacement->Argument(0) && (*(readAt - 2))->Opcode() == IrOpcode::GetSrtResource, "SRT read patching: the flat read is not right before the original read"); + require(order.back()->Opcode() == IrOpcode::ReferenceU32 && order.back()->Argument(0) == &read, "SRT read patching: the planning reference is not at the end of the block"); const auto& remainingUses = read.OperandUses(); require(remainingUses.size() == 1u && remainingUses.front().user->Opcode() == IrOpcode::ReferenceU32, "SRT read patching: the original read retains non-planning uses"); } @@ -6981,6 +6997,28 @@ void verifyGuardedNullPointers() { const auto samePipeline = [](const RecompileResult& left, const RecompileResult& right) { return left.variantId == right.variantId && left.specializationId == right.specializationId && left.PipelineVariantId() == right.PipelineVariantId() && left.spirv == right.spirv; }; + const auto guardSites = [](const RecompileResult& result) { + const auto words = result.spirv.Words(); + std::map names; + std::map constants; + std::vector guardWords; + std::string function; + std::size_t calls = 0; + std::size_t stores = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto opcode = words[cursor] & 0xffffu; + require(count != 0 && count <= words.size() - cursor, "guarded pointer: truncated SPIR-V instruction"); + if (opcode == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (opcode == spv::OpConstant && count == 4u) constants[words[cursor + 2]] = words[cursor + 3]; + if (opcode == spv::OpFunction) function = names[words[cursor + 2]]; + if (opcode == spv::OpAccessChain && count == 6u && names[words[cursor + 3]] == "fault_buffer" && constants.contains(words[cursor + 5]) && constants[words[cursor + 5]] == BdaAbi::SrtGuardWord) guardWords.push_back(words[cursor + 2]); + if (function == "main" && opcode == spv::OpFunctionCall && names[words[cursor + 3]] == "record_bda_fault") ++calls; + if (function == "main" && opcode == spv::OpAtomicStore && std::find(guardWords.begin(), guardWords.end(), words[cursor + 1]) != guardWords.end()) ++stores; + cursor += count; + } + return std::pair{calls, stores}; + }; NestedRequest branch(branchCode, root.data()); point(payloadAddress); @@ -6992,6 +7030,8 @@ void verifyGuardedNullPointers() { branch.request.context.memory = validRegions; const auto validResult = Recompile(branch.request, *valid); require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer"); + const auto [branchCalls, branchStores] = guardSites(*validResult); + require(branchCalls == 0u && branchStores == 1u, ("guarded pointer: a guarded read calls record_bda_fault " + std::to_string(branchCalls) + " times in main and stores its record to the fault buffer's guard word " + std::to_string(branchStores) + " times, expected one store and no call").c_str()); for (const auto pointer : {std::uint64_t{0}, unmapped}) { for (const auto condition : {1u, 0u}) { @@ -7050,6 +7090,8 @@ void verifyGuardedNullPointers() { chain.request.context.memory = mappedChainRegions; const auto mappedChainResult = Recompile(chain.request, *mappedChain); require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads"); + const auto [chainCalls, chainStores] = guardSites(*mappedChainResult); + require(chainCalls == 0u && chainStores == mappedChain->plan->guardedSrtSlots.size(), ("guarded pointer: a chain's guarded reads call record_bda_fault " + std::to_string(chainCalls) + " times in main and store to the guard word " + std::to_string(chainStores) + " times, expected one store per guard and no call").c_str()); require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module"); const auto poisonedWords = [](const ResourceCapture& capture, std::uint32_t pc, std::uint64_t base) { @@ -7620,6 +7662,13 @@ int main(int argc, char** argv) { std::vector float32FlushKey; RecompileCacheKey::Build(float32Flush, float32FlushKey); require(plainKey != float32FlushKey, "f32 hardware flush did not change the cache key"); + auto float32Preserve = uncached; + float32Preserve.target.float32DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Preserve)).request.target.float32DenormPreserve, "f32 denormal preservation was lost in serialization"); + require(!RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(uncached)).request.target.float32DenormPreserve, "f32 denormal preservation appeared from nowhere in serialization"); + std::vector float32PreserveKey; + RecompileCacheKey::Build(float32Preserve, float32PreserveKey); + require(plainKey != float32PreserveKey && float32FlushKey != float32PreserveKey, "f32 denormal preservation did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp index 287ca9bfd8..1b1ec2d0da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TessellationLowering.cpp @@ -102,11 +102,12 @@ struct Outcome { }; const ShaderTessellationInputInfo Tessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u}; +const ShaderTessellationInputInfo GroupedTessellation{.inputControlPoints = InputControlPoints, .outputControlPoints = InputControlPoints, .patchesPerGroup = 1u, .primitiveGroupSize = 1u}; -Outcome Lower(IrProgram& program) { +Outcome Lower(IrProgram& program, const ShaderTessellationInputInfo& tessellation = Tessellation) { Outcome outcome; try { - TessellationLowering{}.Lower(program, Tessellation); + TessellationLowering{}.Lower(program, tessellation); } catch (const std::exception& error) { outcome.reason = error.what(); return outcome; @@ -193,5 +194,17 @@ int main() { return RefusedBecause(hull.program, "not a whole number of control points"); }); + passed &= run("a hull read at a computed control-point index stays in its patch when the relative patch is also data", [] { + Hull hull; + auto& address = hull.inPatch(hull.computed(2u, LocalStride)); + hull.read(address); + static_cast(hull.builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&hull.relativePatch()})); + const Outcome outcome = Lower(hull.program, GroupedTessellation); + if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str()); + if (!outcome.lowered || outcome.emitted->Opcode() != IrOpcode::ISub32 || outcome.emitted->Argument(0)->Resolve() != address.Resolve()) return false; + const IrValue* patch = outcome.emitted->Argument(1)->Resolve(); + return patch->Opcode() == IrOpcode::IMul32 && patch->Argument(1)->Resolve()->ImmediateU32() == PatchStride; + }); + return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index 2028dcb2b1..a69e325898 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -3,6 +3,7 @@ #include "RdnaDecoder/RdnaInstructionDecoder.hpp" #include "Translation/InstructionTranslator.hpp" #include +#include #include #include #include @@ -21,6 +22,7 @@ using namespace ShaderRecompiler; constexpr std::uint32_t MoveM0Two = 0xbefc0382u; constexpr std::uint32_t MoveM0GroupX = 0xbefc0304u; constexpr std::uint32_t MoveV4One = 0x7e0802f2u; +constexpr std::uint32_t MoveV4Two = 0x7e0802f4u; constexpr std::uint32_t MoveV5Two = 0x7e0a02f4u; constexpr std::uint32_t MoveV6Four = 0x7e0c02f6u; constexpr std::uint32_t MovrelsV1V4 = 0x7e028704u; @@ -117,6 +119,24 @@ Shape Scan(std::span words) { return shape; } +std::size_t WindowStores(std::span words) { + std::vector functionPointers; + std::vector pointers; + std::size_t stores = 0; + const auto contains = [](const std::vector& ids, std::uint32_t id) { return std::find(ids.begin(), ids.end(), id) != ids.end(); }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> spv::WordCountShift; + const auto op = static_cast(words[cursor] & spv::OpCodeMask); + Require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpTypePointer && words[cursor + 2] == spv::StorageClassFunction) functionPointers.push_back(words[cursor + 1]); + if (op == spv::OpVariable && words[cursor + 3] == spv::StorageClassFunction && contains(functionPointers, words[cursor + 1])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpAccessChain && contains(pointers, words[cursor + 3])) pointers.push_back(words[cursor + 2]); + if (op == spv::OpStore && contains(pointers, words[cursor + 1])) ++stores; + cursor += count; + } + return stores; +} + IrProgram TranslateStage(ShaderStageKind stage, std::uint32_t waveSize, std::uint32_t base = 0u, std::uint32_t fetchStart = 0u, std::uint32_t fetchCount = 0u) { ShaderComputeInputInfo compute{}; compute.threadIdsNum = 3; @@ -293,6 +313,15 @@ int main(int argc, char** argv) { return shape.functionArrays == 1; }); + passed &= run("a window register written twice before a relative read keeps only its last window store", [] { + const std::array overwritten{MoveM0GroupX, MoveV4One, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const std::array once{MoveM0GroupX, MoveV4Two, MoveV5Two, MoveV6Four, MovrelsV1V4, 0xe0700000u, 0x80000100u, EndProgram}; + const auto twice = WindowStores(RecompileProgram(overwritten)); + const auto single = WindowStores(RecompileProgram(once)); + if (twice != single || single < 3u) std::fprintf(stderr, " window stores: written twice %zu, written once %zu\n", twice, single); + return twice == single && single >= 3u; + }); + if (passed) std::puts("Vector window shape and SSA control tests passed"); return passed ? 0 : 1; } diff --git a/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp new file mode 100644 index 0000000000..bd6a291b22 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/VertexInputValidation.cpp @@ -0,0 +1,150 @@ +#include "Translation/ShaderInputInfoBuilder.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +struct BufferInput { + std::uint64_t address; + std::uint32_t stride; + std::uint32_t records; + std::uint32_t fetchIndex; +}; + +ShaderVertexStageInfo vertexInputs(const std::vector& buffers, bool embedded = false) { + ShaderVertexStageInfo vertex{}; + for (const auto& buffer : buffers) { + const auto index = vertex.resourcesNum++; + vertex.resources[index].fields = {static_cast(buffer.address), static_cast(buffer.address >> 32u) | buffer.stride << 16u, buffer.records, 0u}; + vertex.resourcesDst[index] = {static_cast(index * 4u), 4, static_cast(index), buffer.fetchIndex}; + } + vertex.fetchEmbedded = embedded; + return vertex; +} + +template +std::string errorOf(TRun&& run) { + try { + run(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +} + +int main() { + std::vector> vertexCases{std::nullopt}; + auto overfull = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + overfull.resourcesNum = ShaderVertexStageInfo::MaxResources + 1u; + vertexCases.push_back(overfull); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}})); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 99u, 0u}}, true)); + vertexCases.push_back(vertexInputs({{0x10000u, 16u, 100u, 0u}, {0x10004u, 16u, 100u, 0u}, {0x20000u, 8u, 50u, 0u}, {0x10008u, 16u, 100u, 1u}})); + std::vector attributes; + std::vector separate; + for (std::uint32_t index = 0; index < ShaderVertexStageInfo::MaxResources; ++index) { + attributes.push_back({0x30000u + index * 4u, 128u, 10u, 0u}); + separate.push_back({0x40000u + index * 0x1000u, 4u + index * 4u, 10u, 0u}); + } + vertexCases.push_back(vertexInputs(attributes)); + vertexCases.push_back(vertexInputs(separate)); + + const MeshConfiguration mesh{3u, 64u, 64u, 64u, 64u, 64u, 0u, 0u}; + auto noThreads = mesh; + noThreads.threadsPerGroup = 0u; + auto badProvoking = mesh; + badProvoking.provokingVertex = 3u; + std::vector> meshCases{std::nullopt, mesh, noThreads, badProvoking}; + struct MeshExpectation { + std::uint32_t outputPrimitiveSize; + std::uint32_t provokingVertex; + bool accepted; + }; + constexpr std::array meshExpectations{MeshExpectation{0u, 0u, false}, MeshExpectation{1u, 0u, false}, MeshExpectation{2u, 0u, true}, MeshExpectation{2u, 1u, true}, MeshExpectation{2u, 2u, false}, MeshExpectation{3u, 2u, true}, MeshExpectation{3u, 3u, false}, MeshExpectation{4u, 0u, false}, MeshExpectation{4u, 3u, false}}; + const auto meshWith = [&](const MeshExpectation& expectation) { + auto varied = mesh; + varied.outputPrimitiveSize = expectation.outputPrimitiveSize; + varied.provokingVertex = expectation.provokingVertex; + return varied; + }; + for (const auto& expectation : meshExpectations) meshCases.push_back(meshWith(expectation)); + + const TessellationConfiguration tessellation{3u, 3u, 1u, 2u, 2u, 16u}; + std::vector> tessellationCases{std::nullopt, tessellation}; + const auto vary = [&](auto change) { + auto varied = tessellation; + change(varied); + tessellationCases.push_back(varied); + }; + vary([](TessellationConfiguration& config) { config.inputControlPoints = 0u; }); + vary([](TessellationConfiguration& config) { config.outputControlPoints = 33u; }); + vary([](TessellationConfiguration& config) { config.domain = 3u; }); + vary([](TessellationConfiguration& config) { config.partitioning = 1u; }); + vary([](TessellationConfiguration& config) { config.outputTopology = 1u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 0u; }); + vary([](TessellationConfiguration& config) { config.patchesPerGroup = 257u; }); + + constexpr std::array stages{ShaderStageKind::Vertex, ShaderStageKind::Local, ShaderStageKind::TessellationControl, ShaderStageKind::TessellationEvaluation, ShaderStageKind::Mesh}; + std::size_t mismatches = 0; + std::size_t accepted = 0; + std::set errors; + for (const auto stage : stages) { + for (std::size_t vertexCase = 0; vertexCase < vertexCases.size(); ++vertexCase) { + for (std::size_t meshCase = 0; meshCase < meshCases.size(); ++meshCase) { + for (std::size_t tessellationCase = 0; tessellationCase < tessellationCases.size(); ++tessellationCase) { + GuestContext context{}; + context.waveSize = 64u; + context.vertex = vertexCases[vertexCase]; + const auto* meshConfig = meshCases[meshCase] ? &*meshCases[meshCase] : nullptr; + const auto* tessellationConfig = tessellationCases[tessellationCase] ? &*tessellationCases[tessellationCase] : nullptr; + const auto built = errorOf([&] { static_cast(BuildShaderStageInputInfo(stage, context, 64u, meshConfig, tessellationConfig)); }); + const auto validated = errorOf([&] { ValidateVertexStageInputs(stage, context, meshConfig, tessellationConfig); }); + if (built != validated) { + ++mismatches; + std::fprintf(stderr, "stage %d, vertex case %zu, mesh case %zu, tessellation case %zu: the build says \"%s\", the validation \"%s\"\n", static_cast(stage), vertexCase, meshCase, tessellationCase, built.c_str(), validated.c_str()); + } + if (built.empty()) ++accepted; + else errors.insert(built); + } + } + } + } + const std::array expected{"GuestContext.vertex is not set", "invalid vertex resource count", "disagree on record count", "tessellation configuration is missing", "invalid tessellation control-point counts", "unsupported tessellation configuration", "invalid tessellation patches per group", "has no mesh configuration", "invalid mesh configuration"}; + for (const auto* message : expected) { + const bool seen = std::any_of(errors.begin(), errors.end(), [&](const std::string& error) { return error.find(message) != std::string::npos; }); + if (!seen) { + ++mismatches; + std::fprintf(stderr, "no case raised \"%s\"\n", message); + } + } + GuestContext meshContext{}; + meshContext.waveSize = 64u; + meshContext.vertex = vertexInputs({{0x10000u, 16u, 100u, 0u}}); + for (const auto& expectation : meshExpectations) { + const auto varied = meshWith(expectation); + const bool built = errorOf([&] { static_cast(BuildShaderStageInputInfo(ShaderStageKind::Mesh, meshContext, 64u, &varied, nullptr)); }).empty(); + const bool validated = errorOf([&] { ValidateVertexStageInputs(ShaderStageKind::Mesh, meshContext, &varied, nullptr); }).empty(); + if (built != expectation.accepted || validated != expectation.accepted) { + ++mismatches; + std::fprintf(stderr, "mesh output primitive size %u, provoking vertex %u: expected %s, the build %s, the validation %s\n", expectation.outputPrimitiveSize, expectation.provokingVertex, expectation.accepted ? "accepted" : "rejected", built ? "accepts" : "rejects", validated ? "accepts" : "rejects"); + } + } + if (accepted == 0) { + ++mismatches; + std::fprintf(stderr, "no case was accepted\n"); + } + if (mismatches != 0) return 1; + std::printf("vertex input validation matches the input info build in %zu cases (%zu accepted)\n", stages.size() * vertexCases.size() * meshCases.size() * tessellationCases.size(), accepted); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index 806c3334e7..62e56cfe52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -41,7 +41,8 @@ int main() { require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena"); coverage.Initialize(base, 4 * block + 1); coverage.Exclude(base, 4 * block + 1); - require(!coverage.Restore(base + 1, block - 1) && !coverage.Covers(base, block), "a partial replacement restored an excluded block"); + require(coverage.Restore(base + 1, block - 1) && coverage.Covers(base + 4096, block - 4096), "a partial replacement did not restore its complete pages"); + require(!coverage.Covers(base, 4096) && !coverage.Covers(base, block), "a partial replacement restored an incompletely remapped page"); require(coverage.Restore(base + block, block) && coverage.Covers(base + block, block), "fresh private backing stayed excluded"); require(!coverage.Restore(base + block, block), "restoring an already watched block reports a change"); require(!coverage.Covers(base, block) && !coverage.Covers(base + 2 * block, block), "restoring one block restored its neighbours"); @@ -60,6 +61,44 @@ int main() { coverage.Exclude(base, block, 7); require(!coverage.Restore(base, block, 6) && !coverage.Covers(base, block), "a delayed mapping notification erased a repeated import"); require(coverage.Restore(base, block, 8), "a fresh replacement did not restore the latest import exclusion"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 10); + require(coverage.Restore(base + 16384, 16384, 11), "a fresh partial-block private mapping stayed excluded"); + require(coverage.Covers(base + 16384, 16384), "the restored private pages are not watched"); + require(!coverage.Covers(base, 16384) && !coverage.Covers(base + 32768, 32768), "restoring private pages watched an imported neighbour"); + require(!coverage.Covers(base + 16383, 2) && !coverage.Covers(base + 32767, 2), "a boundary query included excluded pages"); + require(!coverage.Restore(base + 32768, 4096, 10), "a stale partial mapping restored an exclusion"); + coverage.Exclude(base + 4096, 1, 12); + require(!coverage.Covers(base + 16384, 16384), "a repeated import kept partial coverage from an older mapping"); + require(!coverage.Restore(base + 16384, 16384, 11), "a delayed partial mapping restored a newer import"); + require(coverage.Restore(base + 16384, 16384, 13), "fresh partial coverage was not restored after re-import"); + constexpr std::size_t page = 4096; + for (std::size_t first = 0; first < block / page; ++first) { + for (std::size_t count = 1; count <= block / page - first; ++count) { + coverage.Initialize(base, 3 * block); + coverage.Exclude(base + block, block, 20); + require(coverage.Restore(base + block + first * page, count * page, 21), "a complete-page replacement was not restored"); + for (std::size_t index = 0; index < 3 * block / page; ++index) { + const bool expected = index < block / page || index >= 2 * block / page || (index >= block / page + first && index < block / page + first + count); + require(coverage.Covers(base + index * page, page) == expected, "partial restoration changed an unrelated page"); + if (index != 0) { + const bool previous = index - 1 < block / page || index - 1 >= 2 * block / page || (index - 1 >= block / page + first && index - 1 < block / page + first + count); + require(coverage.Covers(base + index * page - 1, 2) == (previous && expected), "a page boundary proof ignored an excluded page"); + } + } + require(coverage.Covers(base + block, block) == (count == block / page), "partial page coverage proved a whole block"); + require(!coverage.Restore(base + block + first * page, count * page, 21), "restoring watched pages again reports a change"); + } + } + coverage.Initialize(base, 2 * block + 17); + coverage.Exclude(base + 2 * block, 17, 30); + require(!coverage.Restore(base + 2 * block, 16, 31), "an incomplete terminal page was restored"); + require(coverage.Restore(base + 2 * block, 17, 31) && coverage.Covers(base + 2 * block, 17), "a complete terminal arena page stayed excluded"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 40); + coverage.Exclude(base + block, block, 43); + require(coverage.Restore(base + block - page, 2 * page, 42), "a newer boundary page was not restored"); + require(coverage.Covers(base + block - page, page) && !coverage.Covers(base + block, page), "a stale boundary mapping erased a newer neighbouring exclusion"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp index a5c5f41b01..e1d74d89e9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthFastClearHarness.hpp @@ -41,6 +41,7 @@ struct Surface { VkFormat format; std::uint32_t slice = 0; std::uint8_t clearStencil = 0; + std::uint32_t swizzle = 0; }; struct DrawOptions { @@ -71,11 +72,11 @@ struct Shaders { std::uint32_t vertexPush = 0; }; -inline Shaders Compile(AgcDriver::VulkanDevice& device) { +inline Shaders Compile(AgcDriver::VulkanDevice& device, const void* vertices = Triangles().data()) { const auto target = device.Target(); Shaders shaders; std::vector vertexUserData(4, 0u); - const auto vertexBuffer = BufferDescriptor(Triangles().data(), 16u, static_cast(Triangles().size())); + const auto vertexBuffer = BufferDescriptor(vertices, 16u, static_cast(Triangles().size())); std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -121,6 +122,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil state.hasColorTarget = true; state.depth = AgcDriver::Graphics::DepthTarget{surface.depth, surface.stencil, extent, surface.format, 1.0f, surface.clearStencil, surface.htile, surface.htileStencil}; state.depth->firstSlice = surface.slice; + state.depth->depthSwizzle = surface.swizzle; state.depthTest = options.depthTest; state.depthWrite = options.depthWrite; state.depthCompare = options.depthCompare; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp new file mode 100644 index 0000000000..fcaadb3722 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthPlaneBufferMemory.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.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" +#include "DepthFastClearHarness.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using DepthFastClearHarness::Compile; +using DepthFastClearHarness::Covered; +using DepthFastClearHarness::Draw; +using DepthFastClearHarness::Height; +using DepthFastClearHarness::HtileBytes; +using DepthFastClearHarness::Require; +using DepthFastClearHarness::Surface; +using DepthFastClearHarness::Width; + +constexpr std::size_t Block = 65536; +alignas(256) std::array Htile{}; +alignas(256) std::array ViewedHtile{}; +alignas(256) constexpr std::array LoadCode{ + 0x7e02020du, 0x7e04020cu, 0xf0001108u, 0x00010300u, 0xbf8c3f70u, 0x34080082u, 0xe0701000u, 0x80000304u, 0xbf810000u, 0xbf810000u, 0xbf810000u, +}; +constexpr std::uint32_t LoadThreads = 32; +constexpr std::size_t ColorBytes = static_cast(Width) * Height * 4u; + +void* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the depth plane block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +} + +std::array LoadRow(AgcDriver::VulkanDevice& device, std::uint64_t address, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) { + alignas(256) static std::array output{}; + std::vector userData(16, 0u); + const auto out = reinterpret_cast(output.data()); + const std::array buffer{static_cast(out), static_cast((out >> 32u) & 0xffffu), static_cast(sizeof(output)), 0x31016facu}; + const std::array texture{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (swizzle << 20u) | (9u << 28u), 0u, 0u, 0u, 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{{LoadThreads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::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; + output.fill(0xdeadbeefu); + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + return output; +} + +bool RowHolds(const std::array& row, float depth) { + return std::all_of(row.begin(), row.end(), [&](std::uint32_t bits) { return bits == std::bit_cast(depth); }); +} + +std::size_t Holding(std::span plane, float depth) { + return static_cast(std::count(plane.begin(), plane.end(), depth)); +} + +void DrawBlendKeepingMemory(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& compiled, std::uint64_t address) { + const std::array shaders{{ + {ShaderRecompiler::ShaderStage::Vertex, &compiled.vertex, 0}, + {ShaderRecompiler::ShaderStage::Fragment, &compiled.pixel, PixelPushOffset(compiled.vertexPush, device.Target())} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {address, {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, ColorBytes, 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0.0f, 1.0f}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend = {VK_TRUE, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, VK_BLEND_FACTOR_ZERO, VK_BLEND_FACTOR_ONE, VK_BLEND_OP_ADD, 15}; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(DepthFastClearHarness::Triangles().size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto planeBytes = static_cast(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u)); + Require(planeBytes <= Block, "the depth plane does not fit one block"); + auto* block = static_cast(AllocateWatched(Block)); + const std::span plane(block, planeBytes / sizeof(float)); + const auto address = reinterpret_cast(block); + const auto shaders = Compile(*device); + const auto reader = Compile(*device, block); + const Surface depth{address, 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, AgcDriver::Graphics::XorSwizzleMode(AgcDriver::Graphics::TextureTileMode::kZ64KBX)}; + + Require(Draw(*device, shaders, depth, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 did not cover the target"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"); + + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(address, Block)) { + std::puts("guest memory is not write-watched here: a buffer write into the depth plane is not exercised"); + std::puts("depth plane buffer memory tests passed"); + return 0; + } + std::fill(plane.begin(), plane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, depth, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written into the depth plane's memory"); + Require(Draw(*device, shaders, depth, {.depthWrite = true}) == Covered, "a draw at depth 0.5 writing depth failed LESS against 0.75"); + Draw(*device, reader, depth, {.depthTest = false}); + Require(Holding(plane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from the depth plane did not see the depth 0.5 written after the plane was reloaded"); + + auto* fresh = static_cast(AllocateWatched(Block)); + const std::span freshPlane(fresh, planeBytes / sizeof(float)); + const auto freshReader = Compile(*device, fresh); + const Surface unstored{reinterpret_cast(fresh), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a new plane did not cover the target"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.75f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.75f) == freshPlane.size(), "the depth 0.5 a draw wrote before a guest write into the plane was stored over that write"); + Require(Draw(*device, shaders, unstored, {}) == Covered, "a draw at depth 0.5 failed LESS against the 0.75 written over depth the surface had not stored"); + std::fill(freshPlane.begin(), freshPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + Require(Draw(*device, shaders, unstored, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.25 written into the plane before it"); + Require(Draw(*device, shaders, unstored, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the reloaded plane did not cover the target"); + Draw(*device, freshReader, depth, {.depthTest = false}); + Require(Holding(freshPlane, 0.5f) == static_cast(Width) * Height, "the depth 0.5 a draw wrote after a guest write into the plane did not reach memory"); + + Htile.fill(0xffffffffu); + auto* cleared = static_cast(AllocateWatched(Block)); + const std::span clearedPlane(cleared, planeBytes / sizeof(float)); + const auto clearedReader = Compile(*device, cleared); + const Surface compressed{reinterpret_cast(cleared), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, compressed, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane with HTILE did not cover the target"); + Draw(*device, clearedReader, depth, {.depthTest = false}); + Require(Holding(clearedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch from a plane with HTILE did not see the depth 0.5 a draw wrote into it"); + std::fill(clearedPlane.begin(), clearedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(compressed.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, compressed, {}) == Covered, "a guest write into the depth plane won over a later HTILE fast clear of it"); + + auto* viewed = static_cast(AllocateWatched(Block)); + const std::span viewedBlock(viewed, Block / sizeof(float)); + std::fill(viewedBlock.begin(), viewedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + ViewedHtile.fill(0xffffffffu); + const Surface sampled{reinterpret_cast(viewed), 0, reinterpret_cast(ViewedHtile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + const auto viewedReader = Compile(*device, viewed); + constexpr std::size_t ViewOffset = 0x100; + const auto view = sampled.depth + ViewOffset; + const auto matchesMemory = [&](const std::array& row) { + Draw(*device, viewedReader, depth, {.depthTest = false}); + return std::memcmp(row.data(), reinterpret_cast(viewed) + ViewOffset, sizeof(row)) == 0; + }; + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane did not read the depth drawn into it"); + AgcDriver::Graphics::NoteDepthMetadataFill(sampled.htile, HtileBytes, 0u); + Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_NEVER}); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a cached linear texture at an offset into a depth plane did not read the depth cleared after it was cached"); + Require(Draw(*device, shaders, sampled, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 over the cleared viewed plane did not cover the target"); + Require(matchesMemory(LoadRow(*device, view, Width, Height / 2u, 0u)), "a linear texture at an offset into a depth plane, viewed through its cached storage image, did not read the depth drawn after the image was made"); + + auto* blended = static_cast(AllocateWatched(Block)); + const std::span blendedBlock(blended, Block / sizeof(float)); + std::fill(blendedBlock.begin(), blendedBlock.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const Surface underBlend{reinterpret_cast(blended), 0, 0, false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, underBlend, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 under a later blended color target did not cover the target"); + DrawBlendKeepingMemory(*device, shaders, underBlend.depth); + Draw(*device, Compile(*device, blended), depth, {.depthTest = false}); + Require(Holding(blendedBlock, 0.5f) == static_cast(Width) * Height, "a color target blending over a depth plane did not load the depth drawn into the plane"); + Htile.fill(0xffffffffu); + auto* reused = static_cast(AllocateWatched(Block)); + const std::span reusedPlane(reused, planeBytes / sizeof(float)); + const auto reusedReader = Compile(*device, reused); + const Surface recleared{reinterpret_cast(reused), 0, reinterpret_cast(Htile.data()), false, VK_FORMAT_D32_SFLOAT, 0, 0, depth.swizzle}; + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS}) == Covered, "the draw writing depth 0.5 into a plane that is cleared each frame did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, 0.5f) == static_cast(Width) * Height, "a vertex fetch did not see the depth 0.5 drawn into a plane that is cleared each frame"); + std::fill(reusedPlane.begin(), reusedPlane.end(), 0.25f); + AgcDriver::GuestMemory::BumpCollectEpoch(); + for (int frame = 0; frame < 2; ++frame) { + const float value = 0.75f - 0.125f * static_cast(frame); + AgcDriver::Graphics::NoteDepthMetadataFill(recleared.htile, HtileBytes, 0u); + Require(Draw(*device, shaders, recleared, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = value, .maxDepth = value}) == Covered, "the draw after the HTILE clear did not cover the target"); + Draw(*device, reusedReader, depth, {.depthTest = false}); + Require(Holding(reusedPlane, value) == static_cast(Width) * Height, "after a guest write into a stored depth plane, the depth drawn behind an HTILE clear did not reach memory (frame " + std::to_string(frame) + ")"); + } + std::puts("depth plane buffer memory 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/DrawDataHitUserWords.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp new file mode 100644 index 0000000000..13d67731a9 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawDataHitUserWords.cpp @@ -0,0 +1,233 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.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 +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array PixelCode{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0300000u, 0x80010000u, 0xbf8c3f70u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) constexpr std::array TriangleVertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u, +}; +alignas(256) constexpr std::array ArithmeticPixelCode{0xf4000100u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0x06000002u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u}; +alignas(256) std::array PixelConstant{}; +alignas(256) std::array ColorMemory{}; + +constexpr std::size_t BlockBytes = 65536; + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; +alignas(8) Header ArithmeticPixelHeader{}; + +void Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +std::uint32_t* GuestBlock() { +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "data-only hit: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + return block; +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +void PixelUserData(std::vector& commands, const std::uint32_t* pointer, std::uint32_t unread) { + const auto address = reinterpret_cast(pointer); + Append(commands, 0x76, {0x00c, static_cast(address)}); + Append(commands, 0x76, {0x00d, static_cast(address >> 32u)}); + Append(commands, 0x76, {0x00e, unread}); +} + +std::vector Setup(bool triangle = false) { + const auto color = reinterpret_cast(ColorMemory.data()); + const auto vertex = reinterpret_cast(triangle ? TriangleVertexCode.data() : VertexCode.data()); + const auto pixel = reinterpret_cast(triangle ? ArithmeticPixelCode.data() : PixelCode.data()); + std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(32.0f)}, {0x110, std::bit_cast(32.0f)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, triangle ? 0u : 1u << 1u}, {0x8c, 0}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 3u << 1u}, + }; + if (triangle) context.insert(context.end(), {{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}}); + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + return commands; +} + +AgcDriver::DriverDetail::DrawEntryCounters Submit(std::vector commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); +} + +std::string Counts(const AgcDriver::DriverDetail::DrawEntryCounters& counters) { + return std::to_string(counters.lookups) + " lookups, " + std::to_string(counters.dataHits) + " data-only hits, stages " + std::to_string(counters.dataStagesReused) + " reused / " + std::to_string(counters.dataStagesCompiled) + " compiled / " + std::to_string(counters.dataVerified) + " verified"; +} + +void UnreadUserWordsKeepTheStage(bool verify) { + Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + auto* first = GuestBlock(); + auto* second = GuestBlock(); + const auto describe = [](std::uint32_t* table, std::uint32_t* data, float value) { + const auto address = reinterpret_cast(data); + data[0] = std::bit_cast(value); + table[0] = static_cast(address); + table[1] = static_cast(address >> 32u) & 0xffffu; + table[2] = 16; + table[3] = 0x4bfacu; + }; + describe(first, first + 1024, 1.0f); + describe(second, second + 1024, 0.5f); + + auto commands = Setup(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + auto counters = Submit(std::move(commands)); + Require(counters.lookups == 1 && counters.dataHits == 0, "the first draw: " + Counts(counters)); + commands.clear(); + PixelUserData(commands, first, 0x11); + Append(commands, 0x2d, {3, 2}); + counters = Submit(std::move(commands)); + Require(counters.inserts != 0, "the repeated register key was not admitted: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, first, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto before = counters; + counters = Submit(std::move(commands)); + const auto reused = counters.dataStagesReused - before.dataStagesReused; + const auto compiled = counters.dataStagesCompiled - before.dataStagesCompiled; + Require(counters.dataHits == before.dataHits + 1, "a draw that changed only a user word no stage reads was not a data-only hit: " + Counts(counters)); + if (verify) Require(compiled == 2 && counters.dataVerified - before.dataVerified == 2, "verify mode: a draw that changed only an unread user word did not build and compare both stages: " + Counts(counters)); + else Require(reused == 2 && compiled == 0, "a draw that changed only a user word no stage reads did not reuse both stages: " + Counts(counters)); + + commands.clear(); + PixelUserData(commands, second, 0x22); + Append(commands, 0x2d, {3, 2}); + const auto moved = counters; + counters = Submit(std::move(commands)); + Require(counters.dataHits == moved.dataHits + 1, "a draw that pointed the pixel stage at another buffer descriptor was not a data-only hit: " + Counts(counters)); + Require(counters.dataStagesCompiled - moved.dataStagesCompiled >= 1, "a draw that pointed the pixel stage at another buffer descriptor reused the pixel stage: " + Counts(counters)); + if (!verify) Require(counters.dataStagesReused - moved.dataStagesReused == 1, "a draw that changed only the pixel stage's descriptor pointer did not reuse the vertex stage: " + Counts(counters)); +} + +unsigned BrightestColorByte() { + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(ColorMemory.data()), ColorMemory.size()); + unsigned brightest = 0; + for (const auto value : ColorMemory) brightest = std::max(brightest, static_cast(std::to_integer(value))); + return brightest; +} + +void ReadUserWordsChangeTheOutput() { + Registered(VertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ArithmeticPixelHeader, ArithmeticPixelCode.data(), sizeof(ArithmeticPixelCode), 1); + PixelConstant[0] = 0.25f; + auto* table = reinterpret_cast(PixelConstant.data()); + const auto draw = [&](float word, bool setup) { + auto commands = setup ? Setup(true) : std::vector{}; + PixelUserData(commands, table, std::bit_cast(word)); + Append(commands, 0x2d, {3, 2}); + Submit(std::move(commands)); + return BrightestColorByte(); + }; + draw(0.25f, true); + const auto first = draw(0.25f, false); + const auto before = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto second = draw(0.5f, false); + const auto after = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters(); + const auto closeTo = [](unsigned value, unsigned expected) { return value + 2 >= expected && value <= expected + 2; }; + Require(closeTo(first, 128), "a pixel stage adding user word 2 (0.25) to a loaded 0.25 wrote " + std::to_string(first) + ", expected 128"); + Require(closeTo(second, 191), "a pixel stage adding user word 2 changed to 0.5 wrote " + std::to_string(second) + ", expected 191 (" + Counts(after) + ", before: " + Counts(before) + ")"); +} + +} + +int main() { + try { + bool libraries = false; + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + libraries = device->GraphicsPipelineLibraries(); + } + if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; + const bool verify = std::getenv("APS5_VERIFY_DRAW_DATA_HITS") != nullptr; + UnreadUserWordsKeepTheStage(verify); + ReadUserWordsChangeTheOutput(); + AgcDriverShutdown_nid_postfix(); + std::cout << "Data-only hits " << (verify ? "verified" : "reused") << " the stages whose read user words did not change\n"; + 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/F32DenormalFlush.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp index 6aa88b0a7d..19beb4db4c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp @@ -505,13 +505,15 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto capabilities = device->Target().supportedCapabilities; - const bool hardware = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool declared = std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != capabilities.end(); + const bool native = !declared && device->Target().float32FlushToZero; + const bool hardware = declared || native; if (hardware) { bool softwareDeclares = true; bool hardwareDeclares = false; const auto software = CompiledWords(*device, false, softwareDeclares); const auto hardwareWords = CompiledWords(*device, true, hardwareDeclares); - Require(!softwareDeclares && hardwareDeclares, "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32"); + Require(!softwareDeclares && hardwareDeclares == declared, declared ? "f32 denormal flush: only the hardware-flush module declares DenormFlushToZero 32" : "f32 denormal flush: a module flushed natively declares DenormFlushToZero 32 without the device capability"); Require(hardwareWords < software, "f32 denormal flush: the hardware-flush module is " + std::to_string(hardwareWords) + " words, the emulated one " + std::to_string(software)); } for (const bool flush : {false, true}) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp index d9ad01a30a..eddf6735b9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GuardedNullPointer.cpp @@ -64,6 +64,12 @@ alignas(256) constexpr std::array ChainCode{ 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, }; +alignas(256) constexpr std::array TwoGuardCode{ + 0xf4080100u, 0xfa000000u, 0xf4040280u, 0xfa000018u, 0xf4040380u, 0xfa000020u, 0xbe8c03ffu, 0x11111111u, + 0xbf8cc07fu, 0xbf068002u, 0xbf840003u, 0xf4000305u, 0xfa000000u, 0xbf820002u, 0xf4000307u, 0xfa000000u, + 0xbf8cc07fu, 0x7e02020cu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, +}; + alignas(256) constexpr std::array DescriptorCode{ 0xf4080100u, 0xfa000000u, 0xf4000200u, 0xfa000010u, 0xf4040280u, 0xfa000018u, 0x7e0202ffu, 0x11111111u, 0xbf8cc07fu, 0xbf068008u, 0xbf850006u, 0xf4080405u, 0xfa000010u, 0xbf8cc07fu, 0xe0300000u, 0x80040100u, @@ -117,7 +123,7 @@ struct Outcome { }; template -Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard) { +Outcome Run(AgcDriver::VulkanDevice& device, const std::array& code, std::uint64_t pointer, std::uint32_t guard, std::uint32_t groups = 1u, std::uint64_t second = 0u) { const auto output = static_cast(reinterpret_cast(Output.data())); Root = {}; Root[0] = static_cast(output); @@ -127,11 +133,13 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(pointer); Root[7] = static_cast(pointer >> 32u); + Root[8] = static_cast(second); + Root[9] = static_cast(second >> 32u); std::fill(Output.begin(), Output.end(), 0xdeadbeefu); const auto root = static_cast(reinterpret_cast(Root.data())); const std::array userData{static_cast(root), static_cast(root >> 32u)}; const std::span span(code); - const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0u, {groups > 1u, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(span.data()), span, 0, {}}, {64, 0, userData, compute, std::nullopt, std::nullopt, {}}, @@ -150,7 +158,7 @@ Outcome Run(AgcDriver::VulkanDevice& device, const std::array(span.data())); + device.Dispatch(*result, groups, 1, 1, {}, reinterpret_cast(span.data())); device.WaitIdle(); } catch (...) { static_cast(log.Finish()); @@ -204,6 +212,11 @@ void RunTests(AgcDriver::VulkanDevice& device) { Require(Faulted(takenUnmapped) && takenUnmapped.log.find(Fault(unmapped, 0x2cu)) != std::string::npos, "guarded pointer: a read through an unmapped pointer that runs did not fault at its address:\n" + takenUnmapped.log); Require(takenUnmapped.pipelineVariantId == taken.pipelineVariantId, "guarded pointer: an unmapped pointer changed the pipeline variant"); + const auto twoGuards = Run(device, TwoGuardCode, 0u, 0u, 2u, unmapped); + const bool nullRecord = twoGuards.log.find(Fault(0u, 0x2cu)) != std::string::npos; + const bool unmappedRecord = twoGuards.log.find(Fault(unmapped, 0x38u)) != std::string::npos; + Require(twoGuards.poisoned == 2u && Faulted(twoGuards) && nullRecord != unmappedRecord, "guarded pointer: two workgroups faulting through different guards did not report exactly one of their two records:\n" + twoGuards.log); + const auto someLanes = Run(device, ExecCode, valid, 5u); Require(!Faulted(someLanes), "guarded pointer: a mapped pointer under EXEC faulted:\n" + someLanes.log); RequireWords(someLanes, 5u, Payload, "guarded pointer: a mapped pointer read under EXEC"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp new file mode 100644 index 0000000000..b119ee834f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageOneDimensionalCompare.cpp @@ -0,0 +1,365 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Layers = 3; +constexpr std::uint32_t Format16UNorm = 7; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Type1DArray = 12; +constexpr std::uint32_t ReferenceWord = 6; +constexpr std::uint32_t Result = 8; +constexpr std::uint32_t Step1D = 4000; +constexpr std::uint32_t StepArray = 1200; +constexpr std::uint32_t Step2D = 200; +constexpr std::uint32_t Row2D = 2; +constexpr std::uint32_t CompareLess = 1; + +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(4096) std::array ArrayTexels{}; +alignas(4096) std::array PlaneTexels{}; + +alignas(256) constexpr std::array Code1D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0bc0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf0a00100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_ExplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0280, 0xf0b00100, 0x00610c14, 0xbf8c3f70, + 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0xf0fc0100, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz_Offset{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e2802ff, 0x00000f00, 0x7e2a0308, 0x7e2c0302, 0x7e2e0303, 0xf0fc0120, 0x00610c14, + 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0bc0108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code2D_ImplicitLod{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf0a00108, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +alignas(256) constexpr std::array Code1D_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0xf13c0100, 0x00610c14, 0xbf8c3f70, 0xe0701020, + 0x80000c01, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array Code1DArray_Gather_C_Lz{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301018, 0x80000801, 0xbf8c3f70, + 0x7e280308, 0x7e2a0302, 0x7e2c0303, 0xf13c0120, 0x00610c14, 0xbf8c3f70, 0xe0701020, 0x80000c01, + 0xbf810000, +}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +bool ReferenceBelow(std::uint32_t tid) { + return (tid & 1u) == 0u; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput1D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((x + 1u) * Step1D) / 65535.0f; + const float half = static_cast(Step1D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput1DArray() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto layer = (tid / 7u) % Layers; + const float texel = static_cast((layer * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits(static_cast(layer)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInput2D() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((Row2D * Size + x + 1u) * Step2D) / 65535.0f; + const float half = static_cast(Step2D / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + 1u] = Bits((static_cast(Row2D) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +void FillInputArrayFirstLayer() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const float texel = static_cast((0u * Size + x + 1u) * StepArray) / 65535.0f; + const float half = static_cast(StepArray / 2u) / 65535.0f; + Buffer[tid * Words] = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + Buffer[tid * Words + ReferenceWord] = Bits(ReferenceBelow(tid) ? texel - half : texel + half); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array ArrayDescriptor() { + const auto address = static_cast(reinterpret_cast(ArrayTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1DArray << 28u), + Layers - 1u, 0u, 0u, 0u, + }; +} + +std::array PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(PlaneTexels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format16UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +void FillLine() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(LineDescriptor())); + Require(geometry.guestBytes <= Line.size(), "1D texture does not fit texel storage"); + std::fill(Line.begin(), Line.end(), 0xeeu); + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((x + 1u) * Step1D); + auto* texel = &Line[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } +} + +void FillArray() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(ArrayDescriptor())); + Require(geometry.guestBytes <= ArrayTexels.size(), "1D array texture does not fit texel storage"); + std::fill(ArrayTexels.begin(), ArrayTexels.end(), 0xeeu); + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((layer * Size + x + 1u) * StepArray); + auto* texel = &ArrayTexels[geometry.GuestLayerOffset(layer) + geometry.mips.at(0).tiledOffset + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +void FillPlane() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(PlaneDescriptor())); + Require(geometry.guestBytes <= PlaneTexels.size(), "2D texture does not fit texel storage"); + std::fill(PlaneTexels.begin(), PlaneTexels.end(), 0xeeu); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + const auto value = static_cast((y * Size + x + 1u) * Step2D); + auto* texel = &PlaneTexels[geometry.GuestLayerOffset(0) + geometry.mips.at(0).tiledOffset + static_cast(y) * geometry.mips.at(0).pitchBytes + x * 2u]; + texel[0] = static_cast(value); + texel[1] = static_cast(value >> 8u); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {CompareLess << 12u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(const std::string& op) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const float value = std::bit_cast(Buffer[tid * Words + Result]); + const float expected = ReferenceBelow(tid) ? 1.0f : 0.0f; + Require(value == expected, op + ": thread " + std::to_string(tid) + " is " + std::to_string(value) + ", expected " + std::to_string(expected)); + } +} + +void ExpectGatherReject(AgcDriver::VulkanDevice& device, std::span code, const std::array& textureDesc) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(sizeof(Buffer))); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(textureDesc.begin(), textureDesc.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + 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; + try { + (void)ShaderRecompiler::Recompile(request); + Require(false, "remapped 1D depth gather was accepted unexpectedly"); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("has an unsupported 1D gather variant") != std::string::npos, "unexpected error for 1D depth gather: " + std::string(error.what())); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillLine(); + FillArray(); + FillPlane(); + + FillInput1D(); + Run(*device, Code1D_Lz, LineDescriptor()); + Check("1D image_sample_c_lz"); + + FillInput1D(); + Run(*device, Code1D_ImplicitLod, LineDescriptor()); + Check("1D image_sample_c"); + + FillInput1D(); + Run(*device, Code1D_ExplicitLod, LineDescriptor()); + Check("1D image_sample_c_l"); + + FillInput1D(); + Run(*device, Code1D_Lz_Offset, LineDescriptor()); + Check("1D image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz, ArrayDescriptor()); + Check("1D array image_sample_c_lz"); + + FillInput1DArray(); + Run(*device, Code1DArray_Lz_Offset, ArrayDescriptor()); + Check("1D array image_sample_c_lz_o"); + + FillInput1DArray(); + Run(*device, Code1DArray_ImplicitLod, ArrayDescriptor()); + Check("1D array image_sample_c"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_Lz, ArrayDescriptor()); + Check("1D image_sample_c_lz on 1D array first layer"); + + FillInputArrayFirstLayer(); + Run(*device, Code1D_ImplicitLod, ArrayDescriptor()); + Check("1D image_sample_c on 1D array first layer"); + + FillInput2D(); + Run(*device, Code2D_Lz, PlaneDescriptor()); + Check("2D image_sample_c_lz"); + + FillInput2D(); + Run(*device, Code2D_ImplicitLod, PlaneDescriptor()); + Check("2D image_sample_c"); + + ExpectGatherReject(*device, Code1D_Gather_C_Lz, LineDescriptor()); + ExpectGatherReject(*device, Code1DArray_Gather_C_Lz, ArrayDescriptor()); + + std::puts("1D, 1D-array, and 2D comparison 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/ImageStoreUnormRounding.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp new file mode 100644 index 0000000000..bb6c6f25cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreUnormRounding.cpp @@ -0,0 +1,264 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.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 +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 32; +constexpr std::uint32_t Height = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t TwoChannelSwizzle = 0x22cu; +constexpr std::size_t TexelBytes = 8192; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Input{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 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, "image store unorm rounding: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + + +struct Format { + const char* name; + std::uint32_t format; + std::uint32_t components; + std::uint32_t bits; + bool snorm; +}; + +constexpr std::array Formats{{ + {"8_UNORM", 1u, 1u, 8u, false}, + {"16_16_UNORM", 23u, 2u, 16u, false}, + {"8_8_8_8_UNORM", 56u, 4u, 8u, false}, + {"8_8_8_8_SNORM", 57u, 4u, 8u, true}, + {"16_16_16_16_UNORM", 65u, 4u, 16u, false}, +}}; + +struct Write { + std::uint32_t row; + std::uint32_t dmask; +}; + +constexpr std::array StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +float Scale(const Format& format) { + return static_cast((1u << (format.snorm ? format.bits - 1u : format.bits)) - 1u); +} + +float Value(const Format& format, std::uint32_t tid, std::uint32_t index) { + constexpr std::array Edges{0.0f, -0.0f, 1.0f, 0.5f, -0.25f, 1.5f, -1.5f, 1e-30f, -1e-30f, 0.999f, 0.0019607844f, 0.9980392f, 0.33333334f, -0.6666667f, 2e-5f, 0.75f}; + const auto slot = tid * 4u + index; + if (slot < Edges.size()) return Edges[slot]; + if (slot == Edges.size()) return std::numeric_limits::quiet_NaN(); + if (slot == Edges.size() + 1u) return std::numeric_limits::infinity(); + if (slot == Edges.size() + 2u) return -std::numeric_limits::infinity(); + const auto scale = Scale(format); + const auto level = static_cast((slot * 37u) % static_cast(scale)); + auto value = (level + 0.5f) / scale; + for (std::uint32_t step = 0; step < slot % 5u; ++step) value = std::nextafter(value, slot % 2u == 0u ? 2.0f : -2.0f); + return format.snorm && slot % 3u == 0u ? -value : value; +} + +std::uint32_t Level(const Format& format, float value) { + const auto lower = format.snorm ? -1.0f : 0.0f; + const auto clamped = std::isnan(value) ? 0.0f : std::clamp(value, lower, 1.0f); + const auto product = static_cast(clamped) * static_cast(Scale(format)); + const auto level = static_cast(std::nearbyint(product)); + return static_cast(level) & ((1u << format.bits) - 1u); +} + +std::uint32_t TexelBytesOf(const Format& format) { + return format.components * format.bits / 8u; +} + +std::uint32_t SwizzleOf(const Format& format) { + return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle; +} + +AgcDriver::Graphics::TileMipLayout Mip(const Format& format) { + return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0); +} + +std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) { + const auto mip = Mip(format); + return mip.tiledOffset + static_cast(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(0x80u + 37u * index + 11u * (index >> 4u)); + } + return bytes; +} + +std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) { + if (((dmask >> component) & 1u) == 0u) return 0u; + std::uint32_t index = 0; + for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u; + return Level(format, Value(format, tid, index)); +} + +std::vector Expected(const Format& format) { + auto bytes = Initial(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (const auto& write : StoreWrites) { + auto* texel = bytes.data() + TexelOffset(format, tid, write.row); + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = StoredComponent(format, tid, write.dmask, component); + std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u); + } + } + } + return bytes; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(texels, format, swizzle); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = std::bit_cast(Value(format, tid, index)); + } + const auto initial = Initial(); + std::copy(initial.begin(), initial.end(), texels); + Dispatch(device, texels, StoreCode, format.format, SwizzleOf(format), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format) { + const auto expected = Expected(format); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto offset = TexelOffset(format, x, y); + for (std::uint32_t component = 0; component < format.components; ++component) { + std::uint32_t actual = 0; + std::uint32_t wanted = 0; + std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u); + std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u); + Require(actual == wanted, std::string("image_store of ") + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted) + " (level nearest to the exact product)"); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock block; + auto* texels = block.Data(); + for (const auto& format : Formats) { + Store(*device, texels, format); + CheckTexels(texels, format); + } + std::puts("image store unorm rounding 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/IndirectThreadDimensions.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp new file mode 100644 index 0000000000..37573087cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IndirectThreadDimensions.cpp @@ -0,0 +1,214 @@ +#include "VulkanTestDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include +#include "prx/libSceAgcDriver/Submit/include/Acb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +alignas(256) std::array AddressWriterCode{0x7e000202u, 0x7e020203u, 0x7e040204u, 0x7e060280u, 0xdc7c8000u, 0x00000003u, 0xbf810000u}; +alignas(256) std::array CounterCode{0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e000281u, 0xe0c80000u, 0x80020000u, 0xbf810000u}; + +constexpr std::uint32_t ThreadDimensions = 0x8061u; +constexpr std::uint32_t GroupCounts = 0x8041u; +constexpr std::uint32_t Compute = 0x20u; +constexpr std::size_t GuestBytes = 1u << 20u; + +struct Slot { + alignas(256) std::array arguments{}; + alignas(256) std::array counter{}; + alignas(256) std::array counterDescriptor{}; + alignas(256) std::uint32_t done = 0; +}; + +struct Shape { + const char* name; + std::array threads; + std::array group; +}; + +constexpr std::array Shapes{{ + {"100x1x1 threads in groups of 64x1x1", {100u, 1u, 1u}, {64u, 1u, 1u}}, + {"5x3x2 threads in groups of 4x2x2", {5u, 3u, 2u}, {4u, 2u, 2u}}, + {"64x2x1 threads in groups of 32x1x1", {64u, 2u, 1u}, {32u, 1u, 1u}}, + {"0x7x1 threads in groups of 8x1x1", {0u, 7u, 1u}, {8u, 1u, 1u}}, +}}; + +std::uint32_t Low(const volatile void* pointer) { + return static_cast(reinterpret_cast(pointer)); +} + +std::uint32_t High(const volatile void* pointer) { + return static_cast(static_cast(reinterpret_cast(pointer)) >> 32u); +} + +std::array Descriptor(const void* pointer, std::uint32_t bytes) { + return {Low(pointer), High(pointer) & 0xffffu, bytes, 0x31016facu}; +} + +std::uint32_t Load(const std::uint32_t& value) { + return reinterpret_cast(value); +} + +std::uint32_t Expected(const Shape& shape) { + return shape.threads[0] * shape.threads[1] * shape.threads[2]; +} + +void SetShaderRegister(std::vector& words, std::uint32_t offset, std::uint32_t value) { + words.insert(words.end(), {0xc0017600u, offset, value}); +} + +void Program(std::vector& words, const void* code, std::array group, std::initializer_list userData) { + const auto address = static_cast(reinterpret_cast(code)); + SetShaderRegister(words, 0x20c, static_cast(address >> 8u)); + SetShaderRegister(words, 0x20d, static_cast(address >> 40u) & 0xffu); + for (std::uint32_t axis = 0; axis < 3; ++axis) SetShaderRegister(words, 0x207u + axis, group[axis]); + SetShaderRegister(words, 0x212, 0); + SetShaderRegister(words, 0x213, static_cast(userData.size()) << 1u); + std::uint32_t offset = 0x240; + for (const auto value : userData) SetShaderRegister(words, offset++, value); +} + +void Reset(Slot& slot) { + slot.arguments = {0xffffffffu, 0xffffffffu, 0xffffffffu, 0}; + slot.counter = {}; + slot.counterDescriptor = Descriptor(slot.counter.data(), sizeof(slot.counter)); + slot.done = 0; +} + +void Check(const Slot& slot, const Shape& shape, const std::string& name) { + const auto start = std::chrono::steady_clock::now(); + while (Load(slot.done) == 0) { + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(10), name + ": the completion label did not land"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + Require(Load(slot.counter[0]) == Expected(shape), name + ": " + std::to_string(Load(slot.counter[0])) + " threads ran, expected " + std::to_string(Expected(shape))); +} + +void RunSubmitted(Slot& slot, const Shape& shape, bool producedOnGpu) { + Reset(slot); + if (!producedOnGpu) { + for (std::size_t axis = 0; axis < 3; ++axis) slot.arguments[axis] = shape.threads[axis]; + } + std::vector words; + if (producedOnGpu) { + Program(words, AddressWriterCode.data(), {1u, 1u, 1u}, {Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, GroupCounts}); + } + Program(words, CounterCode.data(), shape.group, {Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}); + words.insert(words.end(), {0xc0021102u, 1u, Low(slot.arguments.data()), High(slot.arguments.data())}); + words.insert(words.end(), {0xc0011600u, 0u, ThreadDimensions}); + words.insert(words.end(), {0xc0033700u, 0x00100200u, Low(&slot.done), High(&slot.done), 1u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitAcb(Compute, &packet) == 0, "compute submit failed"); + AgcDriverWaitIdle_nid_postfix(); + Check(slot, shape, std::string(shape.name) + (producedOnGpu ? ", arguments stored by the GPU" : ", arguments stored by the CPU")); +} + +ShaderRecompiler::RecompileResult Recompile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::span memory, std::array group, std::array partialThreads, std::uint32_t pushBytes) { + const ShaderRecompiler::ShaderComputeStageInfo compute{group, 0, {false, false, false}, false, 1, partialThreads}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.ComputeTarget(32), + {0, 0, 0, pushBytes} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void RunCpuFallback(Slot& slot, const Shape& shape) { + AgcDriverShutdown_nid_postfix(); + Reset(slot); + const auto device = OpenVulkanTestDevice(); + Require(device != nullptr, "cannot open the CPU fallback test device"); + const std::array producerData{Low(slot.arguments.data()), High(slot.arguments.data()), shape.threads[0], shape.threads[1], shape.threads[2]}; + const std::array producerMemory{{{reinterpret_cast(AddressWriterCode.data()), std::as_bytes(std::span(AddressWriterCode))}}}; + const auto producer = Recompile(*device, AddressWriterCode, producerData, producerMemory, {1u, 1u, 1u}, {}, 128u); + const std::array consumerData{Low(slot.counterDescriptor.data()), High(slot.counterDescriptor.data())}; + const std::array consumerMemory{{ + {reinterpret_cast(CounterCode.data()), std::as_bytes(std::span(CounterCode))}, + {reinterpret_cast(slot.counterDescriptor.data()), std::as_bytes(std::span(slot.counterDescriptor))}, + }}; + const auto consumer = Recompile(*device, CounterCode, consumerData, consumerMemory, shape.group, {~0u, ~0u, ~0u}, 0u); + const auto preparedProducer = device->PrepareDispatch(producer, {}, 1, reinterpret_cast(AddressWriterCode.data())); + const auto preparedConsumer = device->PrepareDispatch(consumer, {}, 0, reinterpret_cast(CounterCode.data())); + std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the CPU fallback test has no active recorder"); + device->Dispatch(producer, 1, 1, 1, {}, reinterpret_cast(AddressWriterCode.data()), preparedProducer); + const auto labelAddress = reinterpret_cast(&slot.arguments[2]); + Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&shape.threads[2], 1)), 1, Compute, false) == 0, "the argument label was not recorded on the GPU"); + const auto outcome = device->DispatchIndirect(consumer, reinterpret_cast(slot.arguments.data()), {}, reinterpret_cast(CounterCode.data()), preparedConsumer, nullptr, shape.group); + Require(outcome.cpuReason == 2, std::string(shape.name) + ": the indirect dispatch did not take the pending-label CPU fallback (reason " + std::to_string(outcome.cpuReason) + ")"); + recorder->Sync(); + slot.done = 1; + Check(slot, shape, std::string(shape.name) + ", read on the CPU behind a pending argument label"); +} + +} + +int main() { + try { + auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + device.reset(); +#ifdef _WIN32 + auto* block = static_cast(VirtualAlloc(nullptr, GuestBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* block = static_cast(std::aligned_alloc(GuestBytes, GuestBytes)); +#endif + Require(block != nullptr, "cannot allocate guest memory"); + GuestAllocations::Mutation().Add(block, GuestBytes, true, true, true); + auto* slot = new (block) Slot{}; + const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr; + for (const auto& shape : Shapes) { + if (gpuThreadDimensions) RunSubmitted(*slot, shape, true); + RunSubmitted(*slot, shape, false); + std::cout << shape.name << ": passed\n"; + } + if (gpuThreadDimensions) { + for (const auto& shape : Shapes) { + RunCpuFallback(*slot, shape); + std::cout << shape.name << ", CPU fallback: passed\n"; + } + } + std::cout << "indirect thread dimension tests passed\n"; + AgcDriverShutdown_nid_postfix(); + 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/LookupLogFitsBuild.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp index 39de3611e3..5ca84cfaf6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookupLogFitsBuild.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -195,6 +196,11 @@ void RunTests(const AgcDriver::VulkanDevice& device) { } int main() { +#ifdef _WIN32 + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "0"); +#else + setenv("APS5_TEMPLATE_AUDIT_MS", "0", 0); +#endif try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp index 5d747bf85a..0afb33266f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PartiallyResidentSnapshot.cpp @@ -45,6 +45,8 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t BlockBytes = 65536; constexpr std::size_t SurfaceBytes = std::size_t{Side} * Side * 4u; +constexpr std::uint32_t LargeSide = 512; +constexpr std::size_t LargeSurfaceBytes = std::size_t{LargeSide} * LargeSide * 4u; constexpr int Skipped = 77; class Device { @@ -149,16 +151,16 @@ private: class ReservedSurface { public: - ReservedSurface() { - block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, BlockBytes)); + explicit ReservedSurface(std::size_t bytes) : size(bytes) { + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(size, BlockBytes)); Require(block != nullptr, "partially resident snapshot: cannot reserve the guest surface"); - GuestAllocations::Mutation().Add(block, SurfaceBytes, true, true, true); + GuestAllocations::Mutation().Add(block, size, true, true, true); } ~ReservedSurface() { GuestAllocations::Mutation().Remove(block); - Decommit(0, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(block, SurfaceBytes); + Decommit(0, size); + GuestArena::GuestArenaRelease_nid_postfix(block, size); } ReservedSurface(const ReservedSurface&) = delete; @@ -189,6 +191,7 @@ public: std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } private: + std::size_t size = 0; std::uint8_t* block = nullptr; }; @@ -200,11 +203,11 @@ void CountReads(std::uint64_t address, std::size_t bytes) { if (address < surfaceEnd.load() && address + bytes > surfaceBegin.load()) surfaceReads.fetch_add(1); } -std::array Descriptor(std::uint64_t address) { +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), - ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, 0u, @@ -263,6 +266,48 @@ void Run(const Context& context, ReservedSurface& surface) { AgcDriver::GuestMemory::SetFlushHook(nullptr); } +void LargeTracked(const Context& context, ReservedSurface& surface, std::size_t committedBytes) { + const auto address = surface.Address(); + const auto words = Descriptor(address, LargeSide); + const auto layout = committedBytes == LargeSurfaceBytes ? std::string("committed") : std::string("partially resident"); + Require(DescribeSurface(DecodeTextureResource(words)).guestBytes == LargeSurfaceBytes, "the large test descriptor does not describe the reserved surface"); + surface.Commit(0, committedBytes); + std::memset(surface.Data(), 0x5a, committedBytes); + surfaceBegin = address; + surfaceEnd = address + LargeSurfaceBytes; + std::shared_ptr previous; + const auto sample = [&](const std::string& when, bool uploaded) { + AgcDriver::GuestMemory::BumpCollectEpoch(); + surfaceReads = 0; + auto texture = CachedSampledTexture(context, words); + Require(texture != nullptr && !texture->ViewsStorageImage(), "the large " + layout + " surface was not sampled through a snapshot " + when); + const auto reads = surfaceReads.load(); + if (uploaded && (reads == 0 || texture == previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " kept the texture uploaded before it"); + if (!uploaded && (reads != 0 || texture != previous)) throw std::runtime_error("sampling the large " + layout + " surface " + when + " uploaded it again although the write tracker saw no write (" + std::to_string(reads) + " reads)"); + previous = texture; + }; + AgcDriver::GuestMemory::SetFlushHook(&CountReads); + try { + sample("at its first use", true); + sample("again", false); + surface.Data()[0x100] = 0x11; + sample("after a CPU write", true); + sample("after the upload of the written surface", false); + surface.Data()[committedBytes - 0x40] = 0x22; + sample("after a CPU write to its last committed block", true); + sample("after the upload of the written block", false); + AgcDriver::GuestMemory::MarkWritten(address + 0x200, 4); + sample("after a driver store of the bytes already there, which a snapshot it did not keep cannot tell from a change", true); + sample("after the upload that followed the driver store", false); + } catch (...) { + AgcDriver::GuestMemory::SetFlushHook(nullptr); + throw; + } + AgcDriver::GuestMemory::SetFlushHook(nullptr); + ClearCachedTextures(context.device); + surface.Decommit(0, committedBytes); +} + #ifdef _WIN32 void UntrackedHint(const Context& context, ReservedSurface& surface) { const auto address = surface.Address(); @@ -369,9 +414,13 @@ int main(int argc, char** argv) { auto context = device->GetContext(); TextureDetiler detiler(context); context.detiler = &detiler; - ReservedSurface surface; + const bool large = argc == 2 && std::string_view(argv[1]) == "--large-tracked"; + ReservedSurface surface(large ? LargeSurfaceBytes : SurfaceBytes); std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); + if (large) { + LargeTracked(context, surface, LargeSurfaceBytes); + LargeTracked(context, surface, BlockBytes); + } else if (argc == 2 && std::string_view(argv[1]) == "--pending-stores") PendingStores(context, surface); else if (argc == 2 && std::string_view(argv[1]) == "--untracked-hint") { #ifdef _WIN32 UntrackedHint(context, surface); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index be30cdaff8..46e3184ea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -9,6 +9,8 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "VulkanTestDevice.hpp" #include #ifdef _WIN32 @@ -16,6 +18,8 @@ #define NOMINMAX #endif #include +#else +#include #endif #include #include @@ -275,6 +279,53 @@ private: void* memory = nullptr; }; +class WatchedSurface { +public: + explicit WatchedSurface(const Context& context) : context(context) { +#ifdef _WIN32 + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + constexpr std::uintptr_t alignment = 65536; + 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) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + memory = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + } +#endif + Require(memory != nullptr, "cannot allocate the watched surface"); + Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, SurfaceBytes); + GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + } + + ~WatchedSurface() { + ClearCachedTextures(context.device); + GuestAllocations::Mutation().Remove(memory); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); +#else + munmap(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); +#endif + } + + WatchedSurface(const WatchedSurface&) = delete; + WatchedSurface& operator=(const WatchedSurface&) = delete; + + std::uint64_t Address() const { return reinterpret_cast(memory); } + +private: + const Context& context; + std::uint8_t* memory = nullptr; +}; + std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { return { static_cast(address >> 8u), @@ -480,6 +531,103 @@ void viewTests(const Context& context) { Require(heldView.expired() && heldStorage.expired(), "the evicted image outlived its last caller without a sampled lookup"); } +ShaderRecompiler::RecompileResult SampledProgram(std::uint64_t address) { + ShaderRecompiler::DescriptorBinding binding{}; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding + PlaceholderDimensionsPerClass + 2u; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageWritten.assign(1, false); + binding.imageSamplers.assign(1, 0u); + const auto words = Descriptor(address); + binding.guestDescriptor.assign(words.begin(), words.end()); + ShaderRecompiler::RecompileResult program; + program.bindings.push_back(std::move(binding)); + return program; +} + +void replacedEntryTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable(), "the sampled snapshot's descriptor set is not eligible for resource caching"); + Require(first->SampledTextures().size() == 1, "the sampled descriptor set binds no texture"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the replaced-entry fixture is not a snapshot"); + Require(Sampled(context, surface.Address()) == bound, "the descriptor set does not bind the cache's texture"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "a template over an unchanged, still cached texture did not prove current"); + Require(first->Revalidate(shader, &report), "a template over an unchanged, still cached texture did not prove current twice"); + Require(report.path == ShaderResources::ProofPath::Fast, "the fixture's template did not take the fast proof"); + Require(first->SampledTextures().front() == bound, "the proof changed the bound texture"); + ClearCachedTextures(context.device); + Require(TextureCacheUsage().sampledEntries == 0, "clearing the sampled cache left entries"); + Require(!first->Revalidate(shader, &report), "a template binding a texture whose cache entry left still proved current"); + const auto replacement = Sampled(context, surface.Address()); + Require(replacement != bound, "the lookup after the clear handed back the old texture"); + Require(!first->Revalidate(shader), "a template binding the replaced texture proved current against the new entry"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == replacement, "the rebuilt descriptor set does not bind the cache's new texture"); + Require(rebuilt->Revalidate(shader), "the rebuilt template did not prove current over its cached texture"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a replaced sampled cache entry passed"); +} + +bool lookupChangedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + if (context.hostImportAlignment == 0) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the lookup-changed test requires host imports"); + std::puts("skipped, host imports are unavailable"); + return false; + } + WatchedSurface surface(context); + ClearCachedTextures(context.device); + const auto program = SampledProgram(surface.Address()); + const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + auto first = std::make_shared(context, shader); + Require(first->Reusable() && first->SampledTextures().size() == 1, "the lookup-changed template is not reusable"); + const auto bound = first->SampledTextures().front(); + Require(!bound->ViewsStorageImage(), "the lookup-changed fixture is not a snapshot before the import"); + ShaderResources::ProofReport report{}; + Require(first->Revalidate(shader), "the lookup-changed template did not prove current"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the lookup-changed template did not take the fast proof"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(first->Revalidate(shader, &report), "the audit refused an unchanged template"); + Require(report.path == ShaderResources::ProofPath::Full, "a proof past the audit interval did not take the full walk"); + Require(first->SampledTextures().front() == bound, "the audit changed the bound texture"); + Require(first->Revalidate(shader, &report) && report.path == ShaderResources::ProofPath::Fast, "the proof right after an audit did not take the fast proof"); + if (HostImportFor(context, surface.Address(), SurfaceBytes) == nullptr) { + Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "the host import of the watched surface was refused"); + std::puts("skipped, host imports are unavailable"); + return false; + } + Require(!bound->Released(), "the snapshot left the cache without a lookup"); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + Require(!first->Revalidate(shader, &report), "a template binding a snapshot of a surface its lookup now views through a storage image still proved current past the audit interval"); + const auto view = Sampled(context, surface.Address()); + Require(view != bound && view->ViewsStorageImage(), "the lookup of the imported surface does not view its storage image"); + auto rebuilt = std::make_shared(context, shader); + Require(rebuilt->SampledTextures().front() == view, "the rebuilt template does not bind the storage view"); + first.reset(); + rebuilt.reset(); + ClearCachedTextures(context.device); + std::puts("template over a changed sampled lookup passed"); + return true; +} + void reportedBudgetTests(const Context& context) { if (context.memoryProperties2 == nullptr) { std::puts("VK_EXT_memory_budget unavailable: the sampled texture cache keeps a quarter of the device-local heap"); @@ -872,8 +1020,10 @@ int main(int argc, char** argv) { const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; try { @@ -912,6 +1062,18 @@ int main(int argc, char** argv) { std::puts("resource cache tests passed"); return 0; } + if (mode == "resource-replaced") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + replacedEntryTests(context); + std::puts("resource cache replaced entry tests passed"); + return 0; + } + if (mode == "resource-lookup-changed") { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + if (!lookupChangedTests(context)) return VulkanTestSkipped; + std::puts("resource cache lookup-changed tests passed"); + return 0; + } if (memory) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); memoryTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp index e35849eb7d..c6dd1e4ca1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SgprPatternFillKeys.cpp @@ -7,6 +7,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -95,6 +96,10 @@ void MatchTests() { Require(!MatchSgprPatternFill(packet, Code, data, noGroup), "a dispatch without the group id matched"); } +alignas(8) std::uint32_t Finished = 0; +std::uint32_t Submitted = 0; +constexpr std::uint32_t MaxResubmits = 16; + void Submit(const std::vector& data, std::uint32_t groups) { std::vector words; const auto registers = [&](std::uint32_t first, std::span values) { @@ -111,6 +116,8 @@ void Submit(const std::vector& data, std::uint32_t groups) { registers(0x213, resource); registers(0x240, data); words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + const auto finished = reinterpret_cast(&Finished); + words.insert(words.end(), {0xc0033700u, 0x00100200u, static_cast(finished), static_cast(finished >> 32u), ++Submitted}); Packet packet{words.data(), static_cast(words.size()), 0, {}}; AgcDriver::Submit(&packet, 0); } @@ -118,32 +125,43 @@ void Submit(const std::vector& data, std::uint32_t groups) { struct KeyRead { DccKeys keys; bool waited; + std::uint32_t resubmits; }; std::uint64_t lastNote = 0; -KeyRead ReadAfterWrite(std::uint64_t surfaceBytes = SurfaceBytes) { +void Settle() { + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); +} + +KeyRead SubmitAndRead(const std::vector& data, std::uint32_t groups, std::uint64_t surfaceBytes = SurfaceBytes, bool retireFirst = false) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + Submit(data, groups); + if (retireFirst) Settle(); + std::uint32_t resubmits = 0; for (;;) { + bool retired = false; { std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex()); auto* recorder = AgcDriver::Graphics::Recorder::Active(); if (const auto noted = recorder != nullptr ? recorder->NewestWriteNote(KeysAddress(), KeyBytes) : 0; noted > lastNote) { lastNote = noted; const auto keys = AgcDriver::Graphics::CurrentDccKeys(KeysAddress(), surfaceBytes); - return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes)}; + return {keys, !recorder->PendingWriteOverlaps(KeysAddress(), KeyBytes), resubmits}; } + retired = std::atomic_ref(Finished).load() == Submitted; + } + if (retired) { + Require(resubmits < MaxResubmits, "the dispatch completed " + std::to_string(resubmits + 1) + " times without a pending write over the keys"); + ++resubmits; + Submit(data, groups); } Require(std::chrono::steady_clock::now() < deadline, "the submitted dispatch never became a pending write over the keys"); std::this_thread::yield(); } } -void Settle() { - AgcDriverWaitIdle_nid_postfix(); - AgcDriver::GuestMemory::FlushGpuWrites(KeysAddress(), KeyBytes); -} - void RequireStored(std::uint32_t count, const Pattern& pattern, std::uint32_t before, const std::string& what) { Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) { @@ -159,38 +177,37 @@ void ExecuteTests() { const std::array, 6> codes{{{0x00, DccKeys::Clear0000}, {0x40, DccKeys::Clear0001}, {0x80, DccKeys::Clear1110}, {0xc0, DccKeys::Clear1111}, {0x20, DccKeys::ClearRegister}, {0xff, DccKeys::Uncompressed}}}; for (const auto& [key, keys] : codes) { Settle(); - Submit(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); - const auto read = ReadAfterWrite(); + const auto read = SubmitAndRead(UserData(Dwords, Dwords, Uniform(key)), Dwords / 64); Require(read.keys == keys && !read.waited, Describe("a pending key code fill by the kernel", read)); RequireStored(Dwords, Uniform(key), 0, "the kernel's key code fill"); } - Submit(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); - const auto other = ReadAfterWrite(); + Settle(); + const auto retired = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64, SurfaceBytes, true); + Require(retired.resubmits > 0, "a fill that retired before the read was not submitted again"); + Require(retired.keys == DccKeys::Clear0001 && !retired.waited, Describe("a pending 0001 fill submitted again after it retired before the read", retired)); + RequireStored(Dwords, Uniform(0x40), 0, "the 0001 fill submitted again"); + + const auto other = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x5a)), Dwords / 64); Require(other.keys == DccKeys::Mixed && other.waited, Describe("a pending fill of a byte that is not a key code", other)); RequireStored(Dwords, Uniform(0x5a), 0, "the kernel's fill of another byte"); const Pattern several{0x40404040u, 0x40404040u, 0x40404040u, 0x80808080u}; - Submit(UserData(Dwords, Dwords, several), Dwords / 64); - const auto mixed = ReadAfterWrite(); + const auto mixed = SubmitAndRead(UserData(Dwords, Dwords, several), Dwords / 64); Require(mixed.keys == DccKeys::Mixed && mixed.waited, Describe("a pending fill of several key codes", mixed)); RequireStored(Dwords, several, 0, "the kernel's fill of several key codes"); - Submit(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); - const auto first = ReadAfterWrite(); + const auto first = SubmitAndRead(UserData(Dwords, Dwords, Uniform(0x40)), Dwords / 64); Require(first.keys == DccKeys::Clear0001 && !first.waited, Describe("a pending 0001 fill by the kernel", first)); - Submit(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); - const auto later = ReadAfterWrite(); + const auto later = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0x5a)), Dwords / 64); Require(later.keys == DccKeys::Mixed && later.waited, Describe("a later writer over half of a pending 0001 fill", later)); Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0x5a5a5a5au : 0x40404040u), "the later writer over the 0001 fill: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); - Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); - const auto half = ReadAfterWrite(); + const auto half = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); Require(half.keys == DccKeys::Mixed, Describe("a pending 1111 fill of half the keys over 0001 keys", half)); Settle(); - Submit(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64); - const auto inside = ReadAfterWrite(SurfaceBytes / 2); + const auto inside = SubmitAndRead(UserData(Dwords, Dwords / 2, Uniform(0xc0)), Dwords / 64, SurfaceBytes / 2); Require(inside.keys == DccKeys::Clear1111, Describe("the half of the keys a pending 1111 fill covers", inside)); Settle(); for (std::uint32_t i = 0; i < Dwords; ++i) Require(Keys[i] == (i < Dwords / 2 ? 0xc0c0c0c0u : 0x40404040u), "the 1111 fill of half the keys: dword " + std::to_string(i) + " holds " + std::to_string(Keys[i])); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp index e3725caa8e..87c935f0d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Tessellation.cpp @@ -118,6 +118,14 @@ alignas(256) constexpr std::array DomainCode{ 0x7e2802f2, 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, }; +alignas(256) constexpr std::array DomainComputedPoint{ + 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xbf8c3f70, 0x7c0214ff, + 0x7f7fffff, 0xd5010002, 0x01a90280, 0xd7460002, 0x04050902, 0xe0341010, 0x04030c02, 0xe0341020, + 0x04030e02, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, 0x10221705, + 0x3e221b06, 0x3e221f02, 0xd5510012, 0x040a0d05, 0x102424ff, 0x40400000, 0x7e260280, 0x7e2802f2, + 0xf80008cf, 0x14131110, 0xf800020f, 0x14131214, 0xbf810000, +}; + alignas(256) constexpr std::array DomainLoweredInterior{ 0xf8000941, 0x00000000, 0x34020e84, 0x16020283, 0xe0341000, 0x04030a01, 0xe0341010, 0x04030c01, 0xe0341020, 0x04030e01, 0xbf8c3f70, 0x08040af2, 0x08040d02, 0x10201505, 0x3e201906, 0x3e201d02, @@ -590,12 +598,12 @@ std::uint32_t CoveredPixels(std::uint32_t patch, bool interior) { return count; } -void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode) { +void RequirePatches(AgcDriver::VulkanDevice& device, const ShaderRecompiler::SpirvTarget& target, std::span hull, const std::string& program, const PatchDraw& patches = Triangles, std::span local = LocalCode, std::span domain = DomainCode) { std::uint32_t cursor = 0; const Stages stages{ Compile(target, ShaderStage::Local, local, cursor, patches), Compile(target, ShaderStage::TessellationControl, hull, cursor, patches), - Compile(target, ShaderStage::TessellationEvaluation, DomainCode, cursor, patches), + Compile(target, ShaderStage::TessellationEvaluation, domain, cursor, patches), Compile(target, ShaderStage::Fragment, PixelCode, cursor, patches), }; const auto center = Row(-0.5f + 1.0f / 3.0f); @@ -985,6 +993,7 @@ int main() { RequirePatches(*device, target, HullRestoredExecSaved, "hull program saving its restored exec"); RequirePatches(*device, target, HullNextInput, "hull program reading the next of 4 input control points of 24 bytes", NextInputTriangles, LocalCodeStride24); RequirePatches(*device, target, HullDynamicIndex, "hull program reading a control point at a computed index"); + RequirePatches(*device, target, HullCode, "domain program reading control points at a computed index", Triangles, LocalCode, DomainComputedPoint); RequirePatches(*device, target, HullAndSaveexec, "hull program narrowing exec with s_and_saveexec_b64"); RequirePatches(*device, target, HullSaveexecLoop, "hull program restoring exec through loop phis"); RequirePatches(*device, target, HullDescendingRecords, "hull program with descending patch records in 128-patch groups", {{3u, 3u, 1u, 2u, 2u, 128u}, Indices}); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp new file mode 100644 index 0000000000..54193f052e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VectorWindowLiveness.cpp @@ -0,0 +1,93 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t MaxThreads = 64; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Output{}; +alignas(256) std::array Unused{}; + +alignas(256) constexpr std::array Code{ + 0x34060084, 0x4a080081, 0xbf068008, 0xbf850001, 0x4a080890, 0xbefc0380, 0x7e028704, 0xe0701000, + 0x80010103, 0x4a080081, 0xbe890383, 0x7e0a0280, 0xbefc0380, 0x7e0c8704, 0x4a0a0b06, 0x4a080890, + 0x80898109, 0xbf078009, 0xbf85fff9, 0xe0701004, 0x80010503, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + Output.fill(0xdeadbeefu); + std::vector userData(9, 0u); + const auto input = BufferDescriptor(Unused.data(), static_cast(Unused.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[8] = overwrite; + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {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(); +} + +void Check(std::uint32_t waveSize, std::uint32_t threads, std::uint32_t overwrite) { + for (std::uint32_t tid = 0; tid < threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + const std::uint32_t written = tid + 1u; + const std::uint32_t branch = overwrite != 0u ? written + 16u : written; + const std::uint32_t loop = 3u * written + 48u; + const auto where = std::string("vector window liveness: wave") + std::to_string(waveSize) + " lane " + std::to_string(tid) + (overwrite != 0u ? " (overwritten before the read)" : " (overwrite branched over)"); + Require(out[0] == branch, where + ": the window register written before the branch read " + Hex(out[0]) + ", expected " + Hex(branch)); + Require(out[1] == loop, where + ": the window register carried around the loop read " + Hex(out[1]) + ", expected " + Hex(loop)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const std::uint32_t waveSize : {32u, 64u}) { + for (const std::uint32_t overwrite : {0u, 1u}) { + Run(*device, waveSize, waveSize, overwrite); + Check(waveSize, waveSize, overwrite); + } + } + std::puts("vector window liveness 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/Vop3CubeClamp.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp index 10857e6d31..a26753bd63 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3CubeClamp.cpp @@ -155,9 +155,135 @@ constexpr const char* Names[16] = { "v_cubema_f32 v25, -v4, |v5|, v6 clamp", }; -void Fill(std::uint32_t tid, std::uint32_t* words) { - std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); -} +alignas(256) constexpr std::array OmodCode{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd544000a, 0x0c1a0b04, 0xd544000b, + 0x1c1a0b04, 0xd545000c, 0x0c1a0b04, 0xd545000d, 0x141a0b04, 0xd545000e, 0x1c1a0b04, 0xd546000f, + 0x0c1a0b04, 0xd5460010, 0x141a0b04, 0xd5460011, 0x1c1a0b04, 0xd5470012, 0x0c1a0b04, 0xd5470013, + 0x141a0b04, 0xd5470014, 0x1c1a0b04, 0xd5440015, 0xdc1a0b04, 0xd5458216, 0x341a0b04, 0xd5468117, + 0x5c1a0b04, 0xd5478018, 0x8c1a0b04, 0xd5440019, 0x141a0b04, 0xe0781000, 0x80010a03, 0xe0781010, + 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, +}; + +constexpr std::uint32_t OmodRows[32][4] = { + {0x3f800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0xbf800000u, 0x3f000000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0x3f800000u, 0x3e800000u, 0x00000000u}, + {0x3f000000u, 0xbf800000u, 0xbe800000u, 0x00000000u}, + {0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3e800000u, 0xbf000000u, 0xbf800000u, 0x00000000u}, + {0x40400000u, 0xc0e00000u, 0x40000000u, 0x00000000u}, + {0xc0200000u, 0x3fc00000u, 0x40c00000u, 0x00000000u}, + {0x7f7fffffu, 0x7f000000u, 0xff000000u, 0x00000000u}, + {0xff7fffffu, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f800000u, 0x00800000u, 0x80ffffffu, 0x00000000u}, + {0x3f800000u, 0x01000000u, 0x81000000u, 0x00000000u}, + {0x3f800000u, 0x00000007u, 0x80000007u, 0x00000000u}, + {0x3f800000u, 0x80000000u, 0x00000000u, 0x00000000u}, + {0x00800000u, 0x00000000u, 0x80000000u, 0x00000000u}, + {0x00000007u, 0x00000003u, 0x00000001u, 0x00000000u}, + {0x7f800000u, 0x3f800000u, 0x40000000u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x3f800000u, 0x00000000u}, + {0x7fc00000u, 0x3f800000u, 0x3f000000u, 0x00000000u}, + {0x3f800000u, 0xffc00000u, 0x3f000000u, 0x00000000u}, + {0x40000000u, 0x3f800000u, 0x7fc00000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u}, + {0x7149f2cau, 0xf149f2cau, 0x71c9f2cau, 0x00000000u}, + {0x0da24260u, 0x0e224260u, 0x8e736390u, 0x00000000u}, + {0x7e800000u, 0x7e7fffffu, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x7e800000u, 0x7effffffu, 0x00000000u}, + {0x3f000000u, 0xbf000000u, 0x3f000000u, 0x00000000u}, + {0xc0c5edadu, 0xc08852a3u, 0xc0c18eacu, 0x00000000u}, + {0xbf3d7ad8u, 0xc0737ab3u, 0xc0e5e3c1u, 0x00000000u}, + {0x3f000000u, 0x3e800000u, 0xff812345u, 0x00000000u}, + {0x7f812345u, 0x3f000000u, 0x3e800000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpected[32][16] = { + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x3f800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x3fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x40c00000u, 0x3fc00000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0x3f000000u, 0x3f800000u, 0x3e000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x40200000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0x40000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x40c00000u, 0x3fc00000u, 0x40c00000u, 0x41400000u, 0x3fc00000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc1e00000u, 0xc2600000u, 0xc0e00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41400000u}, + {0x41000000u, 0x40000000u, 0xc0a00000u, 0xc1200000u, 0xbfa00000u, 0xc0400000u, 0xc0c00000u, 0xbf400000u, 0x41c00000u, 0x42400000u, 0x40c00000u, 0x40200000u, 0x3f800000u, 0x3f400000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0xff800000u, 0xff800000u, 0xfe800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x3f000000u, 0x7f7fffffu, 0x7f800000u, 0x7e7fffffu, 0xff000000u, 0xff800000u, 0xfe000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x017fffffu, 0x01ffffffu, 0x00000000u, 0x81000000u, 0x81800000u, 0x80000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x81800000u, 0x82000000u, 0x80800000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x02000000u, 0x00800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x00000000u, 0x00000000u, 0xc0800000u, 0xc1000000u, 0xbf800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x3f800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x3fc00000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x41000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0x41000000u, 0x41800000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x71c9f2cau, 0x7249f2cau, 0x70c9f2cau, 0x72c9f2cau, 0x7349f2cau, 0x71c9f2cau, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x41200000u, 0x40200000u, 0x8e224260u, 0x8ea24260u, 0x8d224260u, 0x8ea24260u, 0x8f224260u, 0x8da24260u, 0x8f736390u, 0x8ff36390u, 0x8e736390u, 0x40000000u, 0x0ea24260u, 0x0da24260u, 0x0f736390u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0800000u, 0xbf000000u, 0xfeffffffu, 0xff7fffffu, 0xfdffffffu, 0x7f800000u, 0x7f800000u, 0x7e800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x41000000u, 0x40000000u, 0x40800000u, 0x41000000u, 0x3f800000u, 0xff000000u, 0xff800000u, 0xfe000000u, 0x7f800000u, 0x7f800000u, 0x7effffffu, 0x40200000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x41800000u}, + {0x41000000u, 0x40000000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x3f800000u, 0x40000000u, 0x3e800000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x40200000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x41800000u}, + {0x40000000u, 0x3f000000u, 0xc1418eacu, 0xc1c18eacu, 0xc0418eacu, 0x410852a3u, 0x418852a3u, 0x400852a3u, 0xc1c5edadu, 0xc245edadu, 0xc0c5edadu, 0x3f000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x41200000u, 0x40200000u, 0x3fbd7ad8u, 0x403d7ad8u, 0x3ebd7ad8u, 0x40f37ab3u, 0x41737ab3u, 0x3ff37ab3u, 0xc1e5e3c1u, 0xc265e3c1u, 0xc0e5e3c1u, 0x40000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x41a00000u}, + {0x00000000u, 0x00000000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0x40000000u, 0x40800000u, 0x3f000000u, 0x00000000u, 0x00000000u, 0x3e000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf800000u, 0xbe000000u, 0xbf800000u, 0xc0000000u, 0xbe800000u, 0x7f812345u, 0x7f812345u, 0x7f812345u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x00000000u, 0x00000000u} +}; +constexpr std::uint32_t OmodExpectedIeee[32][16] = { + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3e800000u, 0x3f000000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x3f800000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u}, + {0x40000000u, 0x40000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40400000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x40400000u, 0x40400000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0x3e800000u, 0x3e800000u, 0x3e800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x40a00000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0x40800000u, 0x3e800000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0x40400000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xc1600000u, 0xc1600000u, 0xc1600000u, 0x40000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40400000u}, + {0x40800000u, 0x40800000u, 0xc0200000u, 0xc0200000u, 0xc0200000u, 0xbfc00000u, 0xbfc00000u, 0xbfc00000u, 0x41400000u, 0x41400000u, 0x41400000u, 0x40a00000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0xff000000u, 0xff000000u, 0xff000000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x3f800000u, 0x3f800000u, 0x7effffffu, 0x7effffffu, 0x7effffffu, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x3f800000u}, + {0x00000000u, 0x00000000u, 0x00ffffffu, 0x00ffffffu, 0x00ffffffu, 0x80800000u, 0x80800000u, 0x80800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x81000000u, 0x81000000u, 0x81000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000007u, 0x80000007u, 0x80000007u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000007u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x01000000u, 0x01000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000007u, 0x00000007u, 0x00000007u, 0x80000003u, 0x80000003u, 0x80000003u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000003u, 0x00000000u, 0x40800000u}, + {0x00000000u, 0x00000000u, 0xc0000000u, 0xc0000000u, 0xc0000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40000000u, 0x40000000u, 0xff800000u, 0xff800000u, 0xff800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x40400000u, 0x3f800000u, 0x00000000u, 0x3f800000u, 0x40000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x40000000u, 0x40000000u, 0x40000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0x40800000u, 0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x80000000u, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7149f2cau, 0x7249f2cau, 0x7249f2cau, 0x7249f2cau, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x40a00000u, 0x40a00000u, 0x8da24260u, 0x8da24260u, 0x8da24260u, 0x8e224260u, 0x8e224260u, 0x8e224260u, 0x8ef36390u, 0x8ef36390u, 0x8ef36390u, 0x40800000u, 0x0da24260u, 0x0e224260u, 0x0ef36390u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0xbf800000u, 0xbf800000u, 0xbf800000u, 0xfe7fffffu, 0xfe7fffffu, 0xfe7fffffu, 0x7f000000u, 0x7f000000u, 0x7f000000u, 0x00000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u}, + {0x40800000u, 0x40800000u, 0x40000000u, 0x40000000u, 0x40000000u, 0xfe800000u, 0xfe800000u, 0xfe800000u, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x40a00000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x40800000u}, + {0x40800000u, 0x40800000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f000000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x40a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40800000u}, + {0x3f800000u, 0x3f800000u, 0xc0c18eacu, 0xc0c18eacu, 0xc0c18eacu, 0x408852a3u, 0x408852a3u, 0x408852a3u, 0xc145edadu, 0xc145edadu, 0xc145edadu, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3f800000u}, + {0x40a00000u, 0x40a00000u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x3f3d7ad8u, 0x40737ab3u, 0x40737ab3u, 0x40737ab3u, 0xc165e3c1u, 0xc165e3c1u, 0xc165e3c1u, 0x40800000u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x40a00000u}, + {0x00000000u, 0x00000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0x3f800000u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x3e800000u, 0x3f800000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0xbe800000u, 0xbe800000u, 0xbe800000u, 0xbf000000u, 0xbf000000u, 0xbf000000u, 0x7fc12345u, 0x7fc12345u, 0x7fc12345u, 0x00000000u, 0x00000000u, 0x3f000000u, 0x00000000u, 0x00000000u} +}; +constexpr const char* OmodNames[16] = { + "v_cubeid_f32 v10, v4, v5, v6 mul:2", + "v_cubeid_f32 v11, v4, v5, v6 div:2", + "v_cubesc_f32 v12, v4, v5, v6 mul:2", + "v_cubesc_f32 v13, v4, v5, v6 mul:4", + "v_cubesc_f32 v14, v4, v5, v6 div:2", + "v_cubetc_f32 v15, v4, v5, v6 mul:2", + "v_cubetc_f32 v16, v4, v5, v6 mul:4", + "v_cubetc_f32 v17, v4, v5, v6 div:2", + "v_cubema_f32 v18, v4, v5, v6 mul:2", + "v_cubema_f32 v19, v4, v5, v6 mul:4", + "v_cubema_f32 v20, v4, v5, v6 div:2", + "v_cubeid_f32 v21, v4, -v5, -v6 div:2", + "v_cubesc_f32 v22, -v4, |v5|, v6 clamp mul:4", + "v_cubetc_f32 v23, |v4|, -v5, v6 clamp div:2", + "v_cubema_f32 v24, v4, v5, -v6 clamp mul:2", + "v_cubeid_f32 v25, v4, v5, v6 mul:4", +}; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { const auto address = reinterpret_cast(data); @@ -193,33 +319,19 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std:: return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const std::optional& floatMode) { - for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::uint32_t (&rows)[32][4], const std::optional& floatMode) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(rows[tid]), std::end(rows[tid]), &Input[tid * Inputs]); Output.fill(0xdeadbeefu); - const std::span code(Code); const auto result = Compile(device, code, floatMode); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); } -void CheckOmodRefused(AgcDriver::VulkanDevice& device) { - for (const std::uint32_t opcode : {0x144u, 0x145u, 0x146u, 0x147u}) { - alignas(256) const std::array code{0xd400000au | (opcode << 16u), 0x0c1a0b04u, 0xbf810000u}; - std::string refusal; - try { - static_cast(Compile(device, code)); - } catch (const std::exception& error) { - refusal = error.what(); - } - Require(refusal.find("VOP3 source modifiers are not implemented") != std::string::npos, "VOP3 opcode " + Hex(opcode) + " with omod was not refused"); - } -} - -void Check(const std::uint32_t (&expected)[32][16], const char* mode) { +void Check(const std::uint32_t (&expected)[32][16], const char* const (&names)[16], const char* mode) { for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; for (std::uint32_t i = 0; i < 16; ++i) { - Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + Names[i]).c_str()); + Expect(tid, out[i], expected[tid][i], (std::string(mode) + " " + names[i]).c_str()); } } } @@ -230,13 +342,22 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device, std::nullopt); - Check(Expected, "no float mode"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); - Check(Expected, "IEEE=0"); - Run(*device, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); - Check(ExpectedIeee, "IEEE=1"); - CheckOmodRefused(*device); + Run(*device, Code, Rows, std::nullopt); + Check(Expected, Names, "no float mode"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(Expected, Names, "IEEE=0"); + Run(*device, Code, Rows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(ExpectedIeee, Names, "IEEE=1"); + Run(*device, OmodCode, OmodRows, std::nullopt); + Check(OmodExpected, OmodNames, "no float mode"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}); + Check(OmodExpected, OmodNames, "IEEE=0 f32 denormals kept"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xc0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals flushed"); + Run(*device, OmodCode, OmodRows, ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}); + Check(OmodExpectedIeee, OmodNames, "IEEE=1 f32 denormals kept"); std::puts("vop3 cube clamp tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py new file mode 100644 index 0000000000..2b7523f8ab --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/test_shader_cache_prebuilt_discovery.py @@ -0,0 +1,74 @@ +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + + +def native(path): + return Path('\\\\?\\' + str(path)) if os.name == 'nt' else path + + +def snapshot(directory): + return {str(path.relative_to(directory)): None if path.is_dir() else path.read_bytes() + for path in directory.rglob('*')} + + +def main(): + executable = str(Path(sys.argv[1]).resolve()) + mode = sys.argv[2] + assert mode in ('plain', 'relative', 'extended', 'extended-forward', 'device', 'plain-long', 'extended-long') + assert os.name == 'nt' or mode in ('plain', 'relative', 'plain-long') + temporary = Path(tempfile.gettempdir()).resolve() + sandbox = Path(tempfile.mkdtemp(prefix='aps5-prebuilt-discovery-')).resolve() + try: + outside = sandbox / 'outside' + outside.mkdir() + (outside / 'z.pack').write_bytes(b'outside directory canary') + (outside / 'sentinel').write_bytes(b'unchanged outside the cache root') + original = snapshot(outside) + for layout in ('pack', 'loose', 'versioned', 'file', 'empty', 'missing'): + root = sandbox / (layout + '-\u00e4\u6f22') + if mode.endswith('long'): + while len(str(root)) < 270: + root /= 'd' * 40 + assert len(str(root / 'prebuilt')) >= 278 + root = native(root) + root.mkdir(parents=True) + plain = str(root)[4:] if os.name == 'nt' else str(root) + alias = plain + if mode.startswith('extended'): + alias = str(root) + if mode == 'extended-forward': + alias = '\\\\?\\' + plain.replace('\\', '/') + elif mode == 'device': + alias = '\\\\.\\' + plain + elif mode == 'relative': + alias = os.path.join('.', os.path.relpath(plain, outside)) + environment = dict(os.environ, ANYPS5_NO_SHADER_CACHE='0', ANYPS5_SHADER_CACHE_DIR=alias, + APS5_NO_PIPELINE_JOURNAL='1') + + def run(action): + try: + result = subprocess.run([executable, '--' + action + '-prebuilt-discovery', layout], cwd=outside, + env=environment, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, errors='replace', timeout=5) + except subprocess.TimeoutExpired as error: + raise AssertionError('prebuilt discovery did not return within five seconds; child terminated') from error + assert snapshot(outside) == original, 'prebuilt discovery changed files outside its root' + assert result.returncode == 0, mode + ' ' + layout + ':\n' + result.stdout + assert 'prebuilt discovery ' + ('seeded' if action == 'seed' else 'verified') + ' ' + layout in result.stdout + + run('seed') + before = snapshot(root) + run('verify') + assert snapshot(root) == before, 'prebuilt discovery modified its read-only input' + print(mode + ': six prebuilt layouts verified; cache and outside trees unchanged') + finally: + assert sandbox.parent == temporary and sandbox.name.startswith('aps5-prebuilt-discovery-') + shutil.rmtree(native(sandbox)) + + +if __name__ == '__main__': + main() diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index fd0d7cf527..6e55ecbd7b 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -7,7 +7,6 @@ #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include -#include #include #include #include @@ -226,18 +225,6 @@ static int RecordWriteAddressFromCounterPair(Apr::CommandBufferObject* buffer, v return AppendCommand(buffer, Apr::Opcode::WriteAddressFromCounterPair, Apr::WriteAddressFromCounterCommand{{}, reinterpret_cast(address), counter, counter + 1u}); } -static constexpr int ThrdSuccess = 0; -static constexpr int ThrdNomem = 1; -static constexpr int ThrdTimedout = 2; -static constexpr int ThrdBusy = 3; -static constexpr int ThrdError = 4; - -static constexpr int CppDeviceOrResourceBusy = 0; -static constexpr int CppInvalidArgument = 1; -static constexpr int CppResourceUnavailableTryAgain = 6; - -static constexpr int MtxPlain = 0x1; - static std::atomic_flag sharedPtrSpinLock; struct AmmUsageStatsData { @@ -252,55 +239,10 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18); static constexpr std::uint16_t AmmPageTablePoolEntries = 512; static constexpr std::uint32_t AmmRingIdleFlags = 0x7; -struct GuestPad { - unsigned (APS5_VABI* const* vtable)(GuestPad*); - void* condition; - void* mutex; - bool started; -}; - -static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); - extern "C" { -int APS5_VABI pthread_create_nid_postfix(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg); -int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value); -int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); -void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); -int APS5_VABI _Mtx_lock_nid_postfix(void** handle); -int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); int APS5_VABI _Cnd_init_nid_postfix(void** handle); -void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); -int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); -int APS5_VABI _Cnd_signal_nid_postfix(void** handle); -int APS5_VABI wcsrtombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state); +int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); [[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); -int APS5_VABI _Cnd_init_nid_postfix(void** handle); -int SignalCondition_nid_no_patch(void** handle); -[[noreturn]] void ThrowCppError_nid_no_patch(int code); -} - -static void RequireThrd(int result, const char* what) { - switch (result) { - case ThrdSuccess: - return; - case ThrdNomem: - case ThrdTimedout: - ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - case ThrdBusy: - ThrowCppError_nid_no_patch(CppDeviceOrResourceBusy); - case ThrdError: - ThrowCppError_nid_no_patch(CppInvalidArgument); - } - throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); -} - -static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { - auto* pad = static_cast(argument); - const auto go = pad->vtable[0]; - return reinterpret_cast(static_cast(go(pad))); -} - -extern "C" { int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t value) { if (!ValidWriteAddress(address)) return SCE_KERNEL_ERROR_EINVAL; @@ -347,6 +289,7 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter() { return sizeof std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion() { return sizeof(Apr::WriteKernelEventQueueCommand); } int APS5_VABI sceAmprAprCommandBufferConstructor(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState) { + if (!buffer || !gatherState || !scatterState) return SCE_KERNEL_ERROR_EINVAL; buffer->type = Apr::BufferType::Apr; *gatherState = 0; *scatterState = 0; @@ -452,6 +395,7 @@ int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; *buffer = {Apr::BufferType::Generic, 0, 0, 0, 0, nullptr}; return 0; } @@ -506,12 +450,14 @@ int APS5_VABI sceAmprCommandBufferPushMarkerWithColor(Apr::CommandBufferObject* } int APS5_VABI sceAmprCommandBufferReset(Apr::CommandBufferObject* buffer) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->offset = 0; buffer->numCommands = 0; return 0; } int APS5_VABI sceAmprCommandBufferSetBuffer(Apr::CommandBufferObject* buffer, void* memory, uint32_t size) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; buffer->base = static_cast(memory); buffer->size = size; buffer->offset = 0; @@ -901,59 +847,6 @@ int APS5_VABI sceAmprAmmWaitCommandBufferCompletion(std::uint32_t id) { return AmmSubmitted_nid_no_patch(id) ? 0 : SCE_KERNEL_ERROR_ESRCH; } -[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { - ThrowCppError_nid_no_patch(code); -} -int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code) { - void* result = nullptr; - if (pthread_join_nid_postfix(thread, &result) != 0) return ThrdError; - if (code) *code = static_cast(reinterpret_cast(result)); - return ThrdSuccess; -} - -void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); - pad->started = true; - RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); -} - -void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); - const int created = _Mtx_init_nid_postfix(&pad->mutex, MtxPlain); - if (created != ThrdSuccess) { - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(created, "_Pad::_Pad: _Mtx_init"); - } - pad->started = false; - const int locked = _Mtx_lock_nid_postfix(&pad->mutex); - if (locked != ThrdSuccess) { - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); - RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); - } -} - -void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { - if (!pad) APS5_INVALID_ARG_EX; - RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); - _Mtx_destroy_nid_postfix(&pad->mutex); - _Cnd_destroy_nid_postfix(&pad->condition); -} - -int APS5_VABI _Thrd_id_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, Pthread* thread) { - if (!pad || !thread) APS5_INVALID_ARG_EX; - const int created = pthread_create_nid_postfix(thread, nullptr, PadEntry_nid_no_patch, pad); - if (created != 0) ThrowCppError_nid_no_patch(CppResourceUnavailableTryAgain); - while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); -} int APS5_VABI _ZNSt7num_getIcSt19istreambuf_iteratorIcSt11char_traitsIcEEE2idE_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -992,11 +885,6 @@ void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void) { } -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { - std::array state{}; - return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); -} - [[noreturn]] void APS5_VABI __cxa_call_unexpected_nid_postfix(void* unwindException) { __cxa_call_terminate_nid_postfix(unwindException); } @@ -1046,6 +934,11 @@ void APS5_VABI sceAmprIgnoreHandlerS(const char* message, void* pointer, int err SonyIgnoreHandlerS(message, pointer, error); } +APS5_EXPORT("sZLrjx-yEx4", sceAmprWcstombsS); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit) { + return wcstombs_s_nid_postfix(result, destination, capacity, source, limit); +} + APS5_EXPORT("VUzjXknPPBs", sceAmprMbstowcs); std::size_t APS5_VABI sceAmprMbstowcs(std::uint16_t* dest, const char* src, std::size_t size) { return SonyMbstowcs(dest, src, size); diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index 1ed8b159bb..791d2fac7e 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -1,5 +1,6 @@ add_library(libSceAudio3d SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceAudio3d PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceAudio3d PRIVATE libSceAudioOut) configure_windows_unwind(libSceAudio3d) if(BUILD_TESTING) @@ -21,4 +22,14 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_mixer_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dMixer.cpp) + target_include_directories(guest_audio3d_mixer_tests PRIVATE + ${LIBS_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_audio3d_mixer_tests PRIVATE libc) + configure_windows_unwind(guest_audio3d_mixer_tests) + add_test(NAME guest_audio3d_mixer COMMAND guest_audio3d_mixer_tests) + if(WIN32) + set_tests_properties(guest_audio3d_mixer PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 47489f9d5e..28854734cc 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,23 +1,31 @@ #include -#include #include #include #include #include #include +#include #include #include #include #include +#include +#include "Mixer.hpp" #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -#include "prx/libc/include/PreciseWait.hpp" + +extern "C" { +int APS5_VABI sceAudioOutOpen(int user_id, int type, int index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudioOutOutput(int handle, const void* ptr); +int APS5_VABI sceAudioOutClose(int handle); +} namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); +constexpr int AUDIO3D_ERROR_NOT_SUPPORTED = static_cast(0x80EA0008); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); @@ -27,24 +35,44 @@ constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr int AUDIO3D_AUDIO_OUT_PORT_TYPE = 126; +constexpr std::uint32_t AUDIO3D_AUDIO_OUT_FORMAT_STEREO = 1; constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_RESET_STATE = 0x20000; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; +constexpr std::uint32_t AUDIO3D_FORMAT_S16 = 0; +constexpr std::uint32_t AUDIO3D_FORMAT_FLOAT = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_BOTH = 0; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_HMU_ONLY = 1; +constexpr std::uint32_t AUDIO3D_OUTPUT_ROUTE_TV_ONLY = 2; -using Clock = std::chrono::steady_clock; +struct Audio3dAudioOut { + std::size_t buffer_bytes = 0; + std::deque> pending; +}; + +struct Audio3dBed { + std::vector pcm; + std::uint32_t channels = 0; + std::uint32_t format = 0; + std::uint32_t samples = 0; +}; struct Audio3dPort { bool open = false; std::uint32_t queue_depth = 0; - Clock::duration frame_duration{}; - std::uint32_t advanced = 0; - std::deque playing; + std::uint32_t granularity = 0; + std::uint32_t buffer_mode = 0; std::uint32_t max_objects = 0; std::uint32_t next_object_id = 0; std::set objects; + std::deque beds; + std::deque> mixed; + int audio_out_handle = 0; + std::map audio_out_ports; }; std::mutex g_mutex; @@ -59,9 +87,39 @@ bool PortIsOpen(std::uint32_t port_id) { return port_id == AUDIO3D_PORT_ID && g_port.open; } -std::uint32_t QueueLevel(Clock::time_point now) { - while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); - return g_port.advanced + static_cast(g_port.playing.size()); +std::uint32_t QueueLevel() { + return static_cast(g_port.mixed.size()); +} + +std::size_t AudioOutBufferBytes(std::uint32_t len, std::uint32_t param) { + static constexpr std::uint32_t channels[] = {1, 2, 8, 1, 2, 8, 8, 8}; + static constexpr std::uint32_t sample_size[] = {2, 2, 2, 4, 4, 4, 2, 4}; + const std::uint32_t format = param & 0xFF; + if (format >= std::size(channels)) return 0; + return static_cast(len) * channels[format] * sample_size[format]; +} + +int DrainAssociatedAudioOuts() { + while (true) { + int handle = 0; + std::vector frame; + { + std::lock_guard lock(g_mutex); + const auto entry = std::find_if(g_port.audio_out_ports.begin(), g_port.audio_out_ports.end(), + [](const auto& candidate) { return !candidate.second.pending.empty(); }); + if (entry == g_port.audio_out_ports.end()) return 0; + handle = entry->first; + frame = std::move(entry->second.pending.front()); + entry->second.pending.pop_front(); + } + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) { + std::lock_guard lock(g_mutex); + const auto entry = g_port.audio_out_ports.find(handle); + if (entry != g_port.audio_out_ports.end()) entry->second.pending.push_front(std::move(frame)); + return result; + } + } } } @@ -72,6 +130,7 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { if (p == nullptr) APS5_INVALID_ARG_EX; constexpr Audio3dOpenParameters defaults{0x20, 256, 0, 512, 2, 2, 0, 0}; static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); + static_assert(sizeof(Audio3dOpenParameters) == 0x28); std::memcpy(p, &defaults, defaults.size_this); } @@ -93,15 +152,40 @@ int APS5_VABI sceAudio3dTerminate() { int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); - ++g_port.advanced; + if (g_port.buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_NOT_SUPPORTED; + if (QueueLevel() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + std::vector frame; + if (g_port.beds.empty()) { + Audio3dInternal::MixFrame(nullptr, 0, g_port.granularity, frame); + } else { + const Audio3dBed& bed = g_port.beds.front(); + const Audio3dInternal::Source source{ + bed.pcm.data(), + bed.samples, + bed.channels, + bed.format == AUDIO3D_FORMAT_FLOAT, + 1.0f, + 0.0f, + }; + Audio3dInternal::MixFrame(&source, 1, g_port.granularity, frame); + g_port.beds.pop_front(); + } + g_port.mixed.push_back(std::move(frame)); return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - std::lock_guard lock(g_mutex); - if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port = Audio3dPort{}; + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + for (const auto& entry : g_port.audio_out_ports) associated.push_back(entry.first); + g_port = Audio3dPort{}; + } + for (const int port : associated) sceAudioOutClose(port); + if (handle != 0) sceAudioOutClose(handle); return 0; } @@ -109,7 +193,7 @@ int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_leve std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; - const std::uint32_t level = QueueLevel(Clock::now()); + const std::uint32_t level = QueueLevel(); if (queue_level != nullptr) *queue_level = level; if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; return 0; @@ -164,33 +248,53 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.granularity = parameters->granularity; + g_port.buffer_mode = parameters->buffer_mode; g_port.max_objects = parameters->max_objects; - g_port.frame_duration = std::chrono::duration_cast( - std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - Clock::time_point wait_until; + int handle = 0; + std::uint32_t granularity = 0; { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { Unsupported(__func__, "blocking mode " + std::to_string(blocking)); } - const Clock::time_point now = Clock::now(); - const std::uint32_t level = QueueLevel(now); - Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); - for (; g_port.advanced != 0; --g_port.advanced) { - end += g_port.frame_duration; - g_port.playing.push_back(end); - } - if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; - wait_until = g_port.playing[level - g_port.queue_depth]; + handle = g_port.audio_out_handle; + granularity = g_port.granularity; } - PreciseSleepUntil(wait_until); - return 0; + if (handle == 0) { + const int opened = sceAudioOutOpen(AUDIO3D_USER_ID_SYSTEM, AUDIO3D_AUDIO_OUT_PORT_TYPE, 0, + granularity, AUDIO3D_SAMPLE_RATE, AUDIO3D_AUDIO_OUT_FORMAT_STEREO); + if (opened < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) { + sceAudioOutClose(opened); + return AUDIO3D_ERROR_INVALID_PORT; + } + if (g_port.audio_out_handle == 0) g_port.audio_out_handle = opened; + else sceAudioOutClose(opened); + } + std::deque> frames; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + handle = g_port.audio_out_handle; + const std::size_t target = blocking == AUDIO3D_BLOCKING_SYNC ? 0 : g_port.queue_depth - 1; + while (g_port.mixed.size() > target) { + frames.push_back(std::move(g_port.mixed.front())); + g_port.mixed.pop_front(); + } + } + for (const std::vector& frame : frames) { + const int result = sceAudioOutOutput(handle, frame.data()); + if (result < 0) return result; + } + return DrainAssociatedAudioOuts(); } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { @@ -242,10 +346,159 @@ int APS5_VABI sceAudio3dObjectSetAttributes(uint32_t port_id, uint32_t object_id } int APS5_VABI sceAudio3dPortFlush(uint32_t port_id) { + int handle = 0; + std::vector associated; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port.mixed.clear(); + handle = g_port.audio_out_handle; + for (auto& entry : g_port.audio_out_ports) { + entry.second.pending.clear(); + associated.push_back(entry.first); + } + } + for (const int port : associated) sceAudioOutOutput(port, nullptr); + if (handle != 0) sceAudioOutOutput(handle, nullptr); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite2(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples, uint32_t output_route, bool) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; - g_port.advanced = 0; - g_port.playing.clear(); + if (num_channels != 2 && num_channels != 6 && num_channels != 8) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (format > AUDIO3D_FORMAT_FLOAT) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (output_route != AUDIO3D_OUTPUT_ROUTE_BOTH && output_route != AUDIO3D_OUTPUT_ROUTE_HMU_ONLY && output_route != AUDIO3D_OUTPUT_ROUTE_TV_ONLY) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (buffer == nullptr || num_samples == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::size_t sample_size = format == AUDIO3D_FORMAT_S16 ? 2u : 4u; + if (reinterpret_cast(buffer) % sample_size != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t samples = std::min(num_samples, g_port.granularity); + const std::size_t bytes = static_cast(samples) * num_channels * sample_size; + if (g_port.beds.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(buffer); + Audio3dBed bed; + bed.pcm.assign(first, first + bytes); + bed.channels = num_channels; + bed.format = format; + bed.samples = samples; + g_port.beds.push_back(std::move(bed)); + return 0; +} + +int APS5_VABI sceAudio3dBedWrite(uint32_t port_id, uint32_t num_channels, uint32_t format, const void* buffer, uint32_t num_samples) { + return sceAudio3dBedWrite2(port_id, num_channels, format, buffer, num_samples, AUDIO3D_OUTPUT_ROUTE_BOTH, false); +} + +int APS5_VABI sceAudio3dAudioOutOpen(uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, uint32_t len, uint32_t freq, uint32_t param) { + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (len != g_port.granularity) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (index != 0 || (param & 0xFF) > 7) return AUDIO3D_ERROR_INVALID_PARAMETER; + } + const int handle = sceAudioOutOpen(user_id, type, index, len, freq, param); + if (handle < 0) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + bool registered = false; + { + std::lock_guard lock(g_mutex); + if (PortIsOpen(port_id)) { + g_port.audio_out_ports.emplace(handle, Audio3dAudioOut{AudioOutBufferBytes(len, param)}); + registered = true; + } + } + if (!registered) { + sceAudioOutClose(handle); + return AUDIO3D_ERROR_INVALID_PORT; + } + return handle; +} + +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle) { + { + std::lock_guard lock(g_mutex); + if (g_port.audio_out_ports.erase(handle) == 0) return AUDIO3D_ERROR_INVALID_PORT; + } + sceAudioOutClose(handle); + return 0; +} + +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr) { + std::lock_guard lock(g_mutex); + const auto port = g_port.audio_out_ports.find(handle); + if (port == g_port.audio_out_ports.end()) return AUDIO3D_ERROR_INVALID_PORT; + if (ptr == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (port->second.pending.size() >= g_port.queue_depth) return AUDIO3D_ERROR_NOT_READY; + const auto* first = static_cast(ptr); + port->second.pending.emplace_back(first, first + port->second.buffer_bytes); + return static_cast(g_port.granularity); +} + +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, uint32_t num) { + if (param == nullptr || num == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + for (std::uint32_t i = 0; i < num; ++i) { + const int result = sceAudio3dAudioOutOutput(param[i].handle, param[i].ptr); + if (result < 0) return result; + } + return 0; +} + +int APS5_VABI sceAudio3dStrError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dPortGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportRegisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dReportUnregisterHandler(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dCreateSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dDeleteSpeakerArray(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudio3dGetSpeakerArrayMixCoefficients2(void) { + NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceAudio3d/Mixer.hpp b/core/libs/prx/libSceAudio3d/Mixer.hpp new file mode 100644 index 0000000000..569d653e19 --- /dev/null +++ b/core/libs/prx/libSceAudio3d/Mixer.hpp @@ -0,0 +1,70 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP +#define CORE_LIBS_PRX_LIBSCEAUDIO3D_MIXER_HPP + +#include +#include +#include +#include +#include + +namespace Audio3dInternal { + +struct Source { + const void* samples; + std::uint32_t num_samples; + std::uint32_t channels; + bool is_float; + float gain; + float pan; +}; + +constexpr float kSurroundFold = 0.7071f; +constexpr std::uint32_t kChannelL = 0; +constexpr std::uint32_t kChannelR = 1; +constexpr std::uint32_t kChannelCenter = 2; +[[maybe_unused]] constexpr std::uint32_t kChannelLfe = 3; +constexpr std::uint32_t kChannelLs = 4; +constexpr std::uint32_t kChannelRs = 5; +constexpr std::uint32_t kChannelLrs = 6; +constexpr std::uint32_t kChannelRrs = 7; + +inline float ReadSample(const Source& source, std::uint32_t frame, std::uint32_t channel) { + const std::size_t index = static_cast(frame) * source.channels + channel; + if (source.is_float) return static_cast(source.samples)[index]; + return static_cast(static_cast(source.samples)[index]) / 32768.0f; +} + +inline void MixFrame(const Source* sources, std::size_t count, std::uint32_t granularity, std::vector& out) { + std::vector mix(static_cast(granularity) * 2, 0.0f); + for (std::size_t source_index = 0; source_index < count; ++source_index) { + const Source& src = sources[source_index]; + if (src.samples == nullptr || src.num_samples == 0) continue; + const float pan = std::clamp(src.pan, -1.0f, 1.0f); + const float gl = (pan <= 0.0f ? 1.0f : 1.0f - pan) * src.gain; + const float gr = (pan >= 0.0f ? 1.0f : 1.0f + pan) * src.gain; + for (std::uint32_t frame = 0; frame < granularity && frame < src.num_samples; ++frame) { + const std::size_t left_index = static_cast(frame) * 2; + float left = ReadSample(src, frame, kChannelL); + float right = src.channels == 1 ? left : ReadSample(src, frame, kChannelR); + if (src.channels > kChannelCenter) { + const float center = ReadSample(src, frame, kChannelCenter) * kSurroundFold; + left += center; + right += center; + } + if (src.channels > kChannelLs) left += ReadSample(src, frame, kChannelLs) * kSurroundFold; + if (src.channels > kChannelRs) right += ReadSample(src, frame, kChannelRs) * kSurroundFold; + if (src.channels > kChannelLrs) left += ReadSample(src, frame, kChannelLrs) * kSurroundFold; + if (src.channels > kChannelRrs) right += ReadSample(src, frame, kChannelRrs) * kSurroundFold; + mix[left_index] += left * gl; + mix[left_index + 1] += right * gr; + } + } + out.resize(static_cast(granularity) * 2); + for (std::size_t i = 0; i < out.size(); ++i) { + out[i] = static_cast(std::lround(std::clamp(mix[i], -1.0f, 1.0f) * 32767.0f)); + } +} + +} + +#endif diff --git a/core/libs/prx/libSceAudioIn/Export.cpp b/core/libs/prx/libSceAudioIn/Export.cpp index a5a107c931..68fa257250 100644 --- a/core/libs/prx/libSceAudioIn/Export.cpp +++ b/core/libs/prx/libSceAudioIn/Export.cpp @@ -208,4 +208,164 @@ int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) { return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE; } +int APS5_VABI sceAudioInDeviceIdOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdAsyncOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUsbGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInIsSharedDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceIdHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceHqOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortConnections(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetPortStatuses(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInGetHandleStatusInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInHqOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInDeviceOpenEx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetSparkSideTone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetCompressorPreGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetNoiseGateThreshold(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMicLevel(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDeviceMuteStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetDevConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInCountPorts(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetUserMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicCreate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicDestroy(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicWrite(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicGetStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInVmicSetMute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInSetMode2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInExtCtrl(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAudioInInputs(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 1911a35344..8c68010f10 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -24,9 +24,16 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_SLANT = 0x02; constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; +struct FallbackFace { + std::shared_ptr> data; + FT_Face face = nullptr; + std::uint32_t subFontIndex = 0; +}; + struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; + std::vector fallbacks; float scaleW = 16.0f; float scaleH = 16.0f; std::map scriptLanguages; @@ -90,11 +97,15 @@ FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +void AddStateFallbackFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); +FT_Face FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); +FT_Face FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex); bool IsSystemFontSet(std::uint32_t fontSetType); std::vector SystemFontCandidates(std::uint32_t fontSetType); std::filesystem::path SystemFontDirectory(); std::optional FindSystemFontFile(std::uint32_t fontSetType); +std::vector FindSystemFontFallbackFiles(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 8a067968b8..a59a3b5f2f 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -404,10 +404,10 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe float scaleH = 0.0f; const int scaleRc = StyleStateGetScalePixel(style, &scaleW, &scaleH); if (scaleRc != SCE_FONT_OK) return finish(scaleRc); - const FT_Face face = state->face; - if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); - const FT_UInt glyphIndex = ResolveGlyphIndexWithFallback(face, code); - if (glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + if (!state->face) return finish(SCE_FONT_ERROR_NO_SUPPORT_FUNCTION); + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, code, &glyphIndex); + if (!face) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_F26Dot6 usedW = charW; @@ -475,19 +475,23 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe obj->shift_units_y = 0; obj->layout_seed_pair = 0; StyleStateGetScalePixel(style, &obj->scale_x_0x50, &obj->scale_y_0x54); - float outScaleX = 0.0f; - float outScaleY = 0.0f; - int rc; - if (style->scale_unit == 0) { - rc = driver->set_char_default_dpi(obj, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } else { - rc = driver->set_char_with_dpi(obj, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); - } - if (rc != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + const auto setCharSize = [&](FontObj* target) { + float outScaleX = 0.0f; + float outScaleY = 0.0f; + if (style->scale_unit == 0) return driver->set_char_default_dpi(target, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + return driver->set_char_with_dpi(target, style->dpi_x ? style->dpi_x : 0x48, style->dpi_y ? style->dpi_y : 0x48, style->scale_w, style->scale_h, &outScaleX, &outScaleY); + }; + if (setCharSize(obj) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); std::uint32_t glyphIndex = 0; - rc = driver->glyph_index(obj, code, &glyphIndex); - if (rc != SCE_FONT_OK || glyphIndex == 0) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); - return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + if (driver->glyph_index(obj, code, &glyphIndex) == SCE_FONT_OK && glyphIndex != 0) return finish(RenderGlyphIndexToSurface(*obj, glyphIndex, surface, x, y, metrics, result)); + const FontState* state = TryGetState(handle); + FT_UInt fallbackIndex = 0; + const FT_Face fallbackFace = state ? FallbackFaceForCode(*state, code, &fallbackIndex) : nullptr; + if (!fallbackFace) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontObj fallback = *obj; + fallback.ft_face = fallbackFace; + if (setCharSize(&fallback) != SCE_FONT_OK) return finish(SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + return finish(RenderGlyphIndexToSurface(fallback, fallbackIndex, surface, x, y, metrics, result)); } bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { @@ -495,9 +499,9 @@ bool Font::BuildTrueOutline(GeneratedGlyph& glyph) { if (!state || !state->face) return false; const auto* font = GetNativeFont(glyph.owner); if (font->magic != HANDLE_MAGIC) return false; - const FT_Face face = state->face; - const FT_UInt glyphIndex = FT_Get_Char_Index(face, glyph.codepoint); - if (glyphIndex == 0) return false; + FT_UInt glyphIndex = 0; + const FT_Face face = FaceForCode(*state, glyph.codepoint, &glyphIndex); + if (!face) return false; const auto charW = static_cast(static_cast(glyph.glyph.scale_x * 64.0f)); const auto charH = static_cast(static_cast(glyph.glyph.base_scale * 64.0f)); if (FT_Set_Char_Size(face, charW, charH, 72, 72) != 0) return false; diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 8e21cdb937..21e0272990 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -400,6 +400,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -441,8 +442,13 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; if (openRc != SCE_FONT_OK) return openRc; GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + const std::vector fallbackFiles = FindSystemFontFallbackFiles(fontSetType); + for (const SystemFontFile& fallback : fallbackFiles) APS5_LOG_OUT("fontSetType=0x%08X fallback=%s", fontSetType, fallback.path.string().c_str()); FontState& state = ResetState(*pFontHandle); if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + for (const SystemFontFile& fallback : fallbackFiles) { + if (auto bytes = SystemFontBytes(fallback.path)) AddStateFallbackFace(state, std::move(bytes), fallback.subFontIndex); + } return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 4b667c8710..3f78fa635a 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -81,10 +81,11 @@ int CachedColumnOffset(FontHandle handle, FontHandleNative* font, float& offset) return rc; } -void LoadKerning(FT_Face face, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { - const FT_UInt previousGlyph = face ? FT_Get_Char_Index(face, preCode) : 0; - const FT_UInt glyph = face ? FT_Get_Char_Index(face, code) : 0; - if (!face || previousGlyph == 0 || glyph == 0) return; +void LoadKerning(const FontState& state, float scaleW, float scaleH, std::uint32_t preCode, std::uint32_t code, FontKerning* kerning) { + FT_UInt previousGlyph = 0; + FT_UInt glyph = 0; + const FT_Face face = FaceForCode(state, preCode, &previousGlyph); + if (!face || FaceForCode(state, code, &glyph) != face) return; const auto charW = static_cast(static_cast(scaleW * 64.0f)); const auto charH = static_cast(static_cast(scaleH * 64.0f)); FT_Set_Char_Size(face, charW, charH, 72, 72); @@ -187,7 +188,7 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(state->face, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -209,7 +210,7 @@ int APS5_VABI sceFontGetRenderScaledKerning(FontHandle fontHandle, std::uint32_t float scaleH = 0.0f; rc = StyleStateGetScalePixel(&font->cached_style.state, &scaleW, &scaleH); const FontState* state = TryGetState(fontHandle); - if (rc == SCE_FONT_OK && state) LoadKerning(state->face, scaleW, scaleH, preCode, code, kerning); + if (rc == SCE_FONT_OK && state) LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); } ReleaseCachedStyleLock(font, cachedLock); ReleaseFontLock(font, fontLock); diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index ee86c2dac0..4ebd92348b 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -13,6 +13,7 @@ #include #include "prx/libc/include/PreciseWait.hpp" +#include "prx/libSceFont/include/FontFreeType.hpp" #include "prx/libSceFont/include/FontInternal.hpp" namespace { @@ -54,10 +55,9 @@ void ReleaseWord(std::uint32_t& word, std::uint32_t previous) { } Font::FontState::~FontState() { - if (face) { - std::lock_guard lock(faceMutex); - FT_Done_Face(face); - } + std::lock_guard lock(faceMutex); + if (face) FT_Done_Face(face); + for (const FallbackFace& fallback : fallbacks) FT_Done_Face(fallback.face); } void Font::Backoff() { @@ -267,6 +267,36 @@ void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { + std::lock_guard lock(faceMutex); + const FT_Library library = HostFreeTypeLibrary(); + if (!library || !bytes || bytes->empty()) return; + FT_Face face = nullptr; + if (FT_New_Memory_Face(library, bytes->data(), static_cast(bytes->size()), static_cast(subFontIndex), &face) != 0) return; + FT_Select_Charmap(face, FT_ENCODING_UNICODE); + state.fallbacks.push_back({std::move(bytes), face, subFontIndex}); +} + +FT_Face Font::FallbackFaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + for (const FallbackFace& fallback : state.fallbacks) { + const FT_UInt index = ResolveGlyphIndexWithFallback(fallback.face, code); + if (index == 0) continue; + *glyphIndex = index; + return fallback.face; + } + *glyphIndex = 0; + return nullptr; +} + +FT_Face Font::FaceForCode(const FontState& state, std::uint32_t code, FT_UInt* glyphIndex) { + const FT_UInt index = ResolveGlyphIndexWithFallback(state.face, code); + if (index != 0) { + *glyphIndex = index; + return state.face; + } + return FallbackFaceForCode(state, code, glyphIndex); +} + void Font::TrackGeneratedGlyph(FontGlyph glyph) { std::lock_guard lock(glyphMutex); generatedGlyphs.insert(glyph); diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp index f164d86462..5bcfb0789e 100644 --- a/core/libs/prx/libSceFont/src/SystemFont.cpp +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -37,7 +37,7 @@ constexpr std::array SYSTEM_FONT_SET_TYPES = { 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, }; -enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb, Hangul, Arabic }; struct FamilyFiles { std::array system; @@ -45,7 +45,7 @@ struct FamilyFiles { std::uint32_t substituteSubFont; }; -constexpr std::array FAMILY_FILES = {{ +constexpr std::array FAMILY_FILES = {{ {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, @@ -60,6 +60,10 @@ constexpr std::array FAMILY_FILES = {{ {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, + {{"SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Medium.otf", "SCEPS4Yoongd-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 1}, + {{"SSTArabic-Light.otf", "SSTArabic-Roman.otf", "SSTArabic-Medium.otf", "SSTArabic-Bold.otf"}, + {"NotoSansArabic-Light.ttf", "NotoSansArabic-Regular.ttf", "NotoSansArabic-Medium.ttf", "NotoSansArabic-Bold.ttf"}, 0}, }}; Family FamilyOf(std::uint32_t fontSetType) { @@ -87,6 +91,37 @@ std::size_t WeightOf(std::uint32_t fontSetType) { } } +std::vector FallbackFamilies(std::uint32_t fontSetType) { + const std::uint32_t scripts = (fontSetType >> 8) & 0xFFu; + const Family primary = FamilyOf(fontSetType); + std::vector families; + const auto add = [&](bool wanted, Family family) { + if (wanted && family != primary && std::find(families.begin(), families.end(), family) == families.end()) families.push_back(family); + }; + add((fontSetType & 0x10u) != 0, Family::Vietnamese); + add((scripts & 0x10u) != 0, Family::Thai); + add((scripts & 0x88u) == 0x88u, Family::ChineseGb); + add((scripts & 0x04u) != 0, Family::Japanese); + add((scripts & 0x80u) != 0, Family::ChineseGb); + add((scripts & 0x20u) != 0, Family::Hangul); + add((fontSetType & 0x80u) != 0, Family::Arabic); + return families; +} + +std::vector FamilyCandidates(Family family, std::size_t weight) { + const FamilyFiles& files = FAMILY_FILES[static_cast(family)]; + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::optional FirstExisting(const std::filesystem::path& directory, const std::vector& candidates) { + for (const Font::SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return Font::SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} + std::filesystem::path ExecutableDirectory() { #ifdef _WIN32 wchar_t module[MAX_PATH]; @@ -115,9 +150,7 @@ bool Font::IsSystemFontSet(std::uint32_t fontSetType) { std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { if (!IsSystemFontSet(fontSetType)) return {}; - const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; - const std::size_t weight = WeightOf(fontSetType); - return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; + return FamilyCandidates(FamilyOf(fontSetType), WeightOf(fontSetType)); } std::filesystem::path Font::SystemFontDirectory() { @@ -132,11 +165,22 @@ std::filesystem::path Font::SystemFontDirectory() { std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { const std::vector candidates = SystemFontCandidates(fontSetType); if (candidates.empty()) return std::nullopt; + return FirstExisting(SystemFontDirectory(), candidates); +} + +std::vector Font::FindSystemFontFallbackFiles(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; const std::filesystem::path directory = SystemFontDirectory(); - for (const SystemFontFile& candidate : candidates) { - std::error_code error; - const std::filesystem::path path = directory / candidate.path; - if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + const std::size_t weight = WeightOf(fontSetType); + std::vector files; + const std::optional primary = FirstExisting(directory, SystemFontCandidates(fontSetType)); + const auto opened = [&](const SystemFontFile& file) { + const auto same = [&](const SystemFontFile& other) { return other.path == file.path && other.subFontIndex == file.subFontIndex; }; + return (primary && same(*primary)) || std::any_of(files.begin(), files.end(), same); + }; + for (const Family family : FallbackFamilies(fontSetType)) { + auto file = FirstExisting(directory, FamilyCandidates(family, weight)); + if (file && !opened(*file)) files.push_back(std::move(*file)); } - return std::nullopt; + return files; } diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index b28b535c8e..aa280dcad4 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -40,4 +40,9 @@ int APS5_VABI sceGameLiveStreamingGetProgramInfo() { return 0; } +int APS5_VABI sceGameLiveStreamingEnableLiveStreaming(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHmd2/Export.cpp b/core/libs/prx/libSceHmd2/Export.cpp index ab5733971f..f6f53d804c 100644 --- a/core/libs/prx/libSceHmd2/Export.cpp +++ b/core/libs/prx/libSceHmd2/Export.cpp @@ -1,4 +1,5 @@ #include "Hmd2.hpp" +#include "prx/libc/include/general/ExportMacros.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceHmd/Hmd.hpp" @@ -18,6 +19,12 @@ std::mutex stateMutex; bool initialized = false; std::int32_t openHandle = 0; +std::int32_t BeforeInitialize(std::int32_t answer, const char* name) { + std::lock_guard lock(stateMutex); + if (initialized) NotImplemented_nid_no_patch(name); + return answer; +} + std::uint32_t CompiledSdkVersion() { static const auto* processParameters = static_cast(ApplicationProcessParameters_nid_no_patch()); std::uint32_t version; @@ -217,86 +224,6 @@ std::int32_t APS5_VABI sceHmd2SetVibration(std::int32_t handle, std::uint32_t le return 0; } -int APS5_VABI sceHmd2GazeGetCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetCalibrationDataMaxSize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionClearUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventEnd() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2ReprojectionSetUserEventStart() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeActivateGazeTracking() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetDeviceStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeGetUserCalibrationStatus() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeInsertRawDataWithHandle() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetEnabledEye() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeSetUserCalibrationPoint() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStartUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeStopUserCalibration() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionSeeThroughDraw() { NotImplemented_nid_no_patch(__func__); return 0; @@ -322,26 +249,6 @@ int APS5_VABI sceHmd2SeeThroughCallbackGetPoseDataFunc() { return 0; } -int APS5_VABI sceHmd2GazeObjectSelectionGetSelectedObject() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionQueryMemory() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHmd2GazeObjectSelectionTerminate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceHmd2ReprojectionClear() { NotImplemented_nid_no_patch(__func__); return 0; @@ -367,15 +274,179 @@ int APS5_VABI sceHmd2RpeorjectionSetParam() { return 0; } -int APS5_VABI sceHmd2SetFieldOfView() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -int APS5_VABI sceHmd2SetIpdOffset() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::int32_t APS5_VABI Hmd2BeforeInitializeZero() { + return BeforeInitialize(0, __func__); } +APS5_EXPORT("vDgpdS2N9Rw", Hmd2BeforeInitializeZero); +APS5_EXPORT("Ri35gKTZpvE", Hmd2BeforeInitializeZero); +APS5_EXPORT("9oyW8puiKeI", Hmd2BeforeInitializeZero); +APS5_EXPORT("1HZNE6jhBnQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("cdwAKBZLXjs", Hmd2BeforeInitializeZero); +APS5_EXPORT("Yub1MF58jfY", Hmd2BeforeInitializeZero); +APS5_EXPORT("JYXAuQFnPyw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VuPKqCu3wzw", Hmd2BeforeInitializeZero); +APS5_EXPORT("GSThagVyW5U", Hmd2BeforeInitializeZero); +APS5_EXPORT("J4qE5N937MY", Hmd2BeforeInitializeZero); +APS5_EXPORT("9KnNj-dyASg", Hmd2BeforeInitializeZero); +APS5_EXPORT("Qta2bkstbFk", Hmd2BeforeInitializeZero); +APS5_EXPORT("5ZcObnDRBXE", Hmd2BeforeInitializeZero); +APS5_EXPORT("AvPNn0qx4WQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("jp0apqNGgi8", Hmd2BeforeInitializeZero); +APS5_EXPORT("-kZT7X7OMg0", Hmd2BeforeInitializeZero); +APS5_EXPORT("1ewLtng-SVk", Hmd2BeforeInitializeZero); +APS5_EXPORT("B0b8DeIUGes", Hmd2BeforeInitializeZero); +APS5_EXPORT("HclLg4Oj5yw", Hmd2BeforeInitializeZero); +APS5_EXPORT("VbEvrQXsZyY", Hmd2BeforeInitializeZero); +APS5_EXPORT("bB6MKZi5UCQ", Hmd2BeforeInitializeZero); +APS5_EXPORT("OXQ1EDf02XU", Hmd2BeforeInitializeZero); +APS5_EXPORT("IAxfMTvFm0M", Hmd2BeforeInitializeZero); +APS5_EXPORT("qTJtAtaLeno", Hmd2BeforeInitializeZero); +APS5_EXPORT("j7Q16UX6l34", Hmd2BeforeInitializeZero); +APS5_EXPORT("pmmNM06rLqU", Hmd2BeforeInitializeZero); +APS5_EXPORT("zdQfyDK9Ods", Hmd2BeforeInitializeZero); +APS5_EXPORT("aPRwp9GnljM", Hmd2BeforeInitializeZero); +APS5_EXPORT("MjY1gBSaed0", Hmd2BeforeInitializeZero); +APS5_EXPORT("evAMjJYHqW4", Hmd2BeforeInitializeZero); +APS5_EXPORT("r93FLEZ3BBk", Hmd2BeforeInitializeZero); +APS5_EXPORT("IdYURdEdNFo", Hmd2BeforeInitializeZero); +APS5_EXPORT("CVcL2Cu2AEI", Hmd2BeforeInitializeZero); +APS5_EXPORT("NmKIfVxr2s0", Hmd2BeforeInitializeZero); +APS5_EXPORT("KeDLkkp62Gg", Hmd2BeforeInitializeZero); +APS5_EXPORT("+aadu6-ra+A", Hmd2BeforeInitializeZero); +APS5_EXPORT("FgnTiwwGSzk", Hmd2BeforeInitializeZero); +APS5_EXPORT("PWAUwOTHbbo", Hmd2BeforeInitializeZero); +APS5_EXPORT("WzaEApHnsP8", Hmd2BeforeInitializeZero); +APS5_EXPORT("dXzQI8QVY7Y", Hmd2BeforeInitializeZero); +APS5_EXPORT("cCvFKGqRh1k", Hmd2BeforeInitializeZero); +APS5_EXPORT("uj4-2VEAFPU", Hmd2BeforeInitializeZero); +APS5_EXPORT("sIaGStI-iLM", Hmd2BeforeInitializeZero); +APS5_EXPORT("JzwpZOemDO8", Hmd2BeforeInitializeZero); +APS5_EXPORT("IsC4R4bZFVU", Hmd2BeforeInitializeZero); + +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized() { + return BeforeInitialize(Hmd2::NotInitialized, __func__); +} +APS5_EXPORT("sXaPEPY-fLA", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("3F5inHhKzXg", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("sJAiB-6p55U", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("E8hDydLjsSs", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("xEQkSb2WJ7Y", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("aaP4dnR4q-0", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("lEU0TeUoGwI", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("cFVyjHBpUUU", Hmd2BeforeInitializeNotInitialized); +APS5_EXPORT("zROtb46jTj4", Hmd2BeforeInitializeNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004() { + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("kAMsjYHBbNQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("5m73zmLpsQ0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("EPeH4xuQ2XQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("TJ8lyRsjFY4", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-XQVcLnyX68", Hmd2BeforeInitializeError0004); +APS5_EXPORT("giQwQoHhYYA", Hmd2BeforeInitializeError0004); +APS5_EXPORT("zNc9IkgawMI", Hmd2BeforeInitializeError0004); +APS5_EXPORT("-tnG5UJPvAc", Hmd2BeforeInitializeError0004); +APS5_EXPORT("FPhFmG+jXuQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("CKlkvNngofQ", Hmd2BeforeInitializeError0004); +APS5_EXPORT("yL1CZaciFWE", Hmd2BeforeInitializeError0004); +APS5_EXPORT("qlzgyG3s0RY", Hmd2BeforeInitializeError0004); +APS5_EXPORT("kTIDrDLzMv0", Hmd2BeforeInitializeError0004); +APS5_EXPORT("9Gee0ef8a+U", Hmd2BeforeInitializeError0004); +APS5_EXPORT("ecZVjQdwmBI", Hmd2BeforeInitializeError0004); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull() { + return BeforeInitialize(Hmd2::ParameterNull, __func__); +} +APS5_EXPORT("MNtiJTDOPAU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("RRy9WmqZfgU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("uobR4m9lgwc", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("-XPk5KWBYdo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("J44idh9izZM", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Ps0wAvYaa1E", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kLnMR8oQ1MY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("kMz4rbBxvjo", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("DIU1sFRRaiE", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("ZQvU4vDzZGQ", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("TJpyGGP9YEY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("Dih+Odk+RC0", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("oaqdSJw+HVY", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("m35gtHuanXU", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("CTM7QPLJF9Q", Hmd2BeforeInitializeParameterNull); +APS5_EXPORT("+rExHeC9lP0", Hmd2BeforeInitializeParameterNull); + +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid() { + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("bBh5bCp9coo", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("G3PNDE2VjfY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("nScCT0OPU0o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("6Q0Ng7OTN9o", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("-fJKh4SnFR0", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("jUA2s-l0npY", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("+9XhyAIJVj4", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("eBGK-o6E4RQ", Hmd2BeforeInitializeParameterInvalid); +APS5_EXPORT("MTuFeL9184E", Hmd2BeforeInitializeParameterInvalid); + +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized() { + return BeforeInitialize(Hmd2::ReprojectionNotInitialized, __func__); +} +APS5_EXPORT("d3SEIYTP6wc", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("EVECbTvBezo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("ciusPdJKjSU", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("DIwqTkcZiJo", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("64ERZWVFBTE", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("z0+0gXUJWs8", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("rjAd4nCY4DM", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("Acyg4vTdGm0", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("aGOWaEeg04I", Hmd2BeforeInitializeReprojectionNotInitialized); +APS5_EXPORT("9up+EPhw1No", Hmd2BeforeInitializeReprojectionNotInitialized); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C() { + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("V1kLQmP-17U", Hmd2BeforeInitializeError002C); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002() { + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("2LVD1EC5SYE", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Bf87AL6zWxU", Hmd2BeforeInitializeError1002); +APS5_EXPORT("XKt7w1MODFY", Hmd2BeforeInitializeError1002); +APS5_EXPORT("Y5s22M-qUDA", Hmd2BeforeInitializeError1002); +APS5_EXPORT("JJKY-BtQJLg", Hmd2BeforeInitializeError1002); + +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003() { + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("9kj9vw8UShQ", Hmd2BeforeInitializeError1003); + +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne() { + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("EAkv8Mqt33E", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("Rv7bkmKvmz0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("1oPZizn0SNA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("TwCpE-nNrlo", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("jkqAeQfdtxw", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("+l0RQph81Z8", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("qsmAtGw1Ncc", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("uKhwYoMsIsA", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("WNdaigr94dI", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("P6S5ZVWf-H0", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("sun6NWE1sdg", Hmd2BeforeInitializeMinusOne); +APS5_EXPORT("--TFsJYXo04", Hmd2BeforeInitializeMinusOne); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400() { + return BeforeInitialize(0x400, __func__); +} +APS5_EXPORT("BWqcwjCozzI", Hmd2BeforeInitializeValue0400); + +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000() { + return BeforeInitialize(0x20000, __func__); +} +APS5_EXPORT("PRz7TdBGUI0", Hmd2BeforeInitializeValue20000); int APS5_VABI sceHmd2InternalSetPortStatus(void) { NotImplemented_nid_no_patch(__func__); @@ -472,4 +543,277 @@ int APS5_VABI sceHmd2InternalImageCloseBySlot(void) { return 0; } +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error0004, __func__); +} +APS5_EXPORT("9WeZr4kYfzk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("N+5h1hCOFAg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("w2vEs2y5ZmE", Hmd2CheckFirstThenError0004); +APS5_EXPORT("L5gat1pe0y0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("4GnLHR9NJ1w", Hmd2CheckFirstThenError0004); +APS5_EXPORT("AHk7nGSzef0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ncjX9d3mc0M", Hmd2CheckFirstThenError0004); +APS5_EXPORT("l63QYAUuogc", Hmd2CheckFirstThenError0004); +APS5_EXPORT("mrcBySCdXSo", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ieBra+td8sU", Hmd2CheckFirstThenError0004); +APS5_EXPORT("s+rzBnewgGs", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Nfcm0GBdzZI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("Tc9rasWfrm4", Hmd2CheckFirstThenError0004); +APS5_EXPORT("ckTbKTX4KZg", Hmd2CheckFirstThenError0004); +APS5_EXPORT("D7576PUbUjk", Hmd2CheckFirstThenError0004); +APS5_EXPORT("tTJ04SrBT8U", Hmd2CheckFirstThenError0004); +APS5_EXPORT("sDP7hZ-P6OI", Hmd2CheckFirstThenError0004); +APS5_EXPORT("J+eVNbN9ndY", Hmd2CheckFirstThenError0004); +APS5_EXPORT("kozvHBNyxf0", Hmd2CheckFirstThenError0004); +APS5_EXPORT("638Tmg5y6U4", Hmd2CheckFirstThenError0004); + +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("8ETtFtp5zqY", Hmd2CheckFirstThenZero); +APS5_EXPORT("CWPkBm3EXQA", Hmd2CheckFirstThenZero); +APS5_EXPORT("R62liQQIAaA", Hmd2CheckFirstThenZero); +APS5_EXPORT("-bcWLHsdzo0", Hmd2CheckFirstThenZero); +APS5_EXPORT("JngJzgNbu8U", Hmd2CheckFirstThenZero); +APS5_EXPORT("xNBSz8xrGdk", Hmd2CheckFirstThenZero); +APS5_EXPORT("yUVPfN-jRIU", Hmd2CheckFirstThenZero); +APS5_EXPORT("BKPR3RZ5aO4", Hmd2CheckFirstThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("IG4y+drWAWw", Hmd2CheckFirstThenError1002); +APS5_EXPORT("rhYUml9vppE", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6574xHS+jZk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("mXzaY6xVoxg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6af-HIZQQn4", Hmd2CheckFirstThenError1002); +APS5_EXPORT("6qWtdTHQsTk", Hmd2CheckFirstThenError1002); +APS5_EXPORT("pUm8t8xWhFg", Hmd2CheckFirstThenError1002); +APS5_EXPORT("A0XKI-WjxK8", Hmd2CheckFirstThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("A9s2j8OioPQ", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("YIgtmrL+HLY", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("Ng8zEkGxDNs", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("EtslTnqO1xU", Hmd2CheckFirstThenMinusOne); +APS5_EXPORT("hfrnRTryKQQ", Hmd2CheckFirstThenMinusOne); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("PPX68fNToFM", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("LxPIfjHaY4c", Hmd2CheckFirstTwoThenParameterInvalid); +APS5_EXPORT("w8gBTTdg+po", Hmd2CheckFirstTwoThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("tWJPX2ycRMM", Hmd2CheckSecondThenError002C); +APS5_EXPORT("5UMB9PJA70o", Hmd2CheckSecondThenError002C); +APS5_EXPORT("xQs62qCk1EI", Hmd2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("MMc+bSlqkzk", Hmd2CheckFirstThenParameterInvalid); +APS5_EXPORT("NpZHgvSYgEA", Hmd2CheckFirstThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void* a0, const void* a1, const void* a2, const void* a3, const void* a4, const void* a5) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a2 == nullptr) return Hmd2::ParameterNull; + if (a3 == nullptr) return Hmd2::ParameterNull; + if (a4 == nullptr) return Hmd2::ParameterNull; + if (a5 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("z-serf9vsb0", Hmd2CheckAllSixThenZero); +APS5_EXPORT("IRMUFu+uG5s", Hmd2CheckAllSixThenZero); + +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::ParameterInvalid, __func__); +} +APS5_EXPORT("72U64E+0z6U", Hmd2CheckSecondThenParameterInvalid); +APS5_EXPORT("LCQzi5ILGrY", Hmd2CheckSecondThenParameterInvalid); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error800d0203, __func__); +} +APS5_EXPORT("Im0qL0j9Ubg", Hmd2CheckFirstThenError800d0203); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("ng6eI8uA0h4", Hmd2CheckFirstThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1010, __func__); +} +APS5_EXPORT("mPPGgjyJrCI", Hmd2CheckFirstThenError1010); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("ZupCJmV5BDI", Hmd2CheckFirstTwoThenError1002); + +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void* a0, const void* a1) { + if (a0 == nullptr) return Hmd2::ParameterNull; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("mAQZwOX1IHU", Hmd2CheckFirstTwoThenError1003); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("oacDkQkinLo", Hmd2CheckFirstAndThirdThenZero); + +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void* a0, const void* a1, const void* a2) { + if (a0 == nullptr) return Hmd2::ParameterNull; + (void)a1; + if (a2 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("zn7PHGzzEyI", Hmd2CheckFirstAndThirdThenError1003); + +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("wEHILc7TP04", Hmd2CheckSecondThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void* a0, const void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("W-ZIlUXcrNA", Hmd2CheckSecondThenError1002); + + +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x02, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("omimgG5itSs", Hmd2WriteStateTwo); + +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1) { + if (a0 == nullptr || a1 == nullptr) return Hmd2::ParameterNull; + *a0 = 0x2000; + *a1 = 0x100; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("6PeHcAaKYg8", Hmd2WriteSize0x2000Align0x100); + +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0) { + if (a0 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0xe1, 0xf0, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c, 0x1f, 0xf1, 0x74, 0x3c, 0xd8, 0xf0, 0x74, 0x3c}; + std::memcpy(a0, bytes, sizeof bytes); + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("yur+xPdWPAg", Hmd2WriteFourConstants); + +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("RnieDDp5ge4", Hmd2WriteSixHalvesThenInvalidHandle); +APS5_EXPORT("jM3NEJA11yw", Hmd2WriteSixHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1) { + (void)a0; + if (a1 == nullptr) return Hmd2::ParameterNull; + static constexpr std::uint8_t bytes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(a1, bytes, sizeof bytes); + return BeforeInitialize(Hmd2::InvalidHandle, __func__); +} +APS5_EXPORT("b0H-n-tUk0U", Hmd2WriteTwelveHalvesThenInvalidHandle); + +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2) { + if (a1 == nullptr || a2 == nullptr) return Hmd2::ParameterNull; + if (a0 > 3) return Hmd2::ParameterInvalid; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("MXhloJ6Iw3w", Hmd2SlotUpTo3CheckSecondThirdThenZero); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("19wrIIiqFgM", Hmd2SlotUpTo1ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("Z389vAm47Ys", Hmd2SlotUpTo1CheckSecondThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0) { + if (a0 > 1) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("cDcrvEG8j8k", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("tuqJO6O6cb4", Hmd2SlotUpTo1ThenMinusOne); +APS5_EXPORT("fd4fu1cziaI", Hmd2SlotUpTo1ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(-1, __func__); +} +APS5_EXPORT("UxMg7iL4nRs", Hmd2SlotUpTo2ThenMinusOne); +APS5_EXPORT("K1GELwlaaGc", Hmd2SlotUpTo2ThenMinusOne); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1002, __func__); +} +APS5_EXPORT("RFn39VCHjGY", Hmd2SlotUpTo2ThenError1002); + +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0) { + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error1003, __func__); +} +APS5_EXPORT("FRoSjceDv10", Hmd2SlotUpTo2ThenError1003); + +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 > 2) return Hmd2::ParameterInvalid; + return BeforeInitialize(Hmd2::Error002C, __func__); +} +APS5_EXPORT("wQ44gBwBw44", Hmd2SlotUpTo2CheckSecondThenError002C); + +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1) { + if (a1 == nullptr) return Hmd2::ParameterNull; + if (a0 != 0) return Hmd2::Error100F; + return BeforeInitialize(0, __func__); +} +APS5_EXPORT("wiv8+7ogrq0", Hmd2IndexZeroCheckSecondThenZero); + } diff --git a/core/libs/prx/libSceHmd2/Hmd2.hpp b/core/libs/prx/libSceHmd2/Hmd2.hpp index 5e978e686f..c5ae98b060 100644 --- a/core/libs/prx/libSceHmd2/Hmd2.hpp +++ b/core/libs/prx/libSceHmd2/Hmd2.hpp @@ -14,6 +14,14 @@ inline constexpr std::int32_t InvalidHandle = static_cast(0x8a7200 inline constexpr std::int32_t AlreadyOpened = static_cast(0x8a720006); inline constexpr std::int32_t ParameterNull = static_cast(0x8a720008); inline constexpr std::int32_t ParameterInvalid = static_cast(0x8a720009); +inline constexpr std::int32_t Error0004 = static_cast(0x8a720004); +inline constexpr std::int32_t ReprojectionNotInitialized = static_cast(0x8a72000b); +inline constexpr std::int32_t Error002C = static_cast(0x8a72002c); +inline constexpr std::int32_t Error1002 = static_cast(0x8a721002); +inline constexpr std::int32_t Error1003 = static_cast(0x8a721003); +inline constexpr std::int32_t Error1010 = static_cast(0x8a721010); +inline constexpr std::int32_t Error800d0203 = static_cast(0x800d0203); +inline constexpr std::int32_t Error100F = static_cast(0x8a72100f); inline constexpr std::uint32_t UserIdSdkVersion = 0x07000000; diff --git a/core/libs/prx/libSceJpegDec/Export.cpp b/core/libs/prx/libSceJpegDec/Export.cpp index 1c67856235..19ec61c18b 100644 --- a/core/libs/prx/libSceJpegDec/Export.cpp +++ b/core/libs/prx/libSceJpegDec/Export.cpp @@ -111,9 +111,10 @@ int32_t APS5_VABI sceJpegDecDecode(void* handle, const JpegDecDecodeParam* param if (!context) return SCE_JPEG_DEC_ERROR_INVALID_HANDLE; (void)context; if (!param) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; - if (!param->jpeg_mem_addr || !param->image_mem_addr) return SCE_JPEG_DEC_ERROR_INVALID_ADDR; + if (!param->jpeg_mem_addr || !param->image_mem_addr || (reinterpret_cast(param->image_mem_addr) & 3u) != 0) 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; + if (param->alpha_value > 0xFF) return SCE_JPEG_DEC_ERROR_INVALID_PARAM; const std::uint32_t downScale = requireDownScale(__func__, param->decode_mode, param->down_scale); const std::span jpeg = toBytes(param->jpeg_mem_addr, param->jpeg_mem_size); diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index cdab277419..95faf4584b 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -1,8 +1,36 @@ add_library(libSceNet SHARED EXCLUDE_FROM_ALL Export.cpp) target_include_directories(libSceNet PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNet PRIVATE libSceNetCtl) configure_windows_unwind(libSceNet) if(BUILD_TESTING) + add_test_executable(net_dontwait_tests ${LIBS_INCLUDE_DIR}/tests/NetDontWait.cpp) + target_include_directories(net_dontwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_dontwait_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(net_dontwait_tests) + foreach(api IN ITEMS send sendto sendmsg recv recvfrom recvmsg duplex blocking) + add_test(NAME net_dontwait_${api} COMMAND net_dontwait_tests ${api}) + set_tests_properties(net_dontwait_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(net_blocking_send_tests ${LIBS_INCLUDE_DIR}/tests/NetBlockingSend.cpp) + target_include_directories(net_blocking_send_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(net_blocking_send_tests PRIVATE libkernel libc libSceNetCtl) + configure_windows_unwind(net_blocking_send_tests) + if(WIN32) + target_link_libraries(net_blocking_send_tests PRIVATE ws2_32) + endif() + foreach(api IN ITEMS send sendto sendmsg) + add_test(NAME net_dontwait_blocking_${api} COMMAND net_blocking_send_tests ${api}) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(net_dontwait_blocking_${api} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test_executable(guest_datagram_receive_tests ${LIBS_INCLUDE_DIR}/tests/GuestDatagramReceive.cpp) target_include_directories(guest_datagram_receive_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_datagram_receive_tests PRIVATE libSceNet libkernel libc) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index bd4752bdb6..a9a2230457 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -39,6 +39,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestHeap.hpp" +#include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -120,32 +121,7 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int net_error(int code) { - switch (code) { - case WSAEWOULDBLOCK: return NET_EAGAIN; - case WSAEINPROGRESS: return NET_EINPROGRESS; - case WSAEALREADY: return NET_EALREADY; - case WSAEADDRINUSE: return NET_EADDRINUSE; - case WSAEADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case WSAEAFNOSUPPORT: return NET_EAFNOSUPPORT; - case WSAENETUNREACH: return NET_ENETUNREACH; - case WSAEHOSTUNREACH: return NET_EHOSTUNREACH; - case WSAECONNABORTED: return NET_ECONNABORTED; - case WSAENOBUFS: return NET_ENOBUFS; - case WSAEISCONN: return NET_EISCONN; - case WSAENOTCONN: return NET_ENOTCONN; - case WSAEACCES: return NET_EACCES; - case WSAECONNRESET: return 54; - case WSAETIMEDOUT: return NET_ETIMEDOUT; - case WSAECONNREFUSED: return NET_ECONNREFUSED; - case WSAEMSGSIZE: return NET_EMSGSIZE; - case WSAEINTR: return 4; - case WSAEINVAL: return NET_EINVAL; - case WSAENOTSOCK: return NET_ENOTSOCK; - case WSAESHUTDOWN: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; @@ -154,41 +130,25 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int net_error(int code) { - switch (code) { - case EAGAIN: return NET_EAGAIN; -#if EWOULDBLOCK != EAGAIN - case EWOULDBLOCK: return NET_EAGAIN; -#endif - case EINPROGRESS: return NET_EINPROGRESS; - case EALREADY: return NET_EALREADY; - case EADDRINUSE: return NET_EADDRINUSE; - case EADDRNOTAVAIL: return NET_EADDRNOTAVAIL; - case EAFNOSUPPORT: return NET_EAFNOSUPPORT; - case ENETUNREACH: return NET_ENETUNREACH; - case EHOSTUNREACH: return NET_EHOSTUNREACH; - case ECONNABORTED: return NET_ECONNABORTED; - case ENOBUFS: return NET_ENOBUFS; - case EISCONN: return NET_EISCONN; - case ENOTCONN: return NET_ENOTCONN; - case EACCES: return NET_EACCES; - case ECONNRESET: return 54; - case ETIMEDOUT: return NET_ETIMEDOUT; - case ECONNREFUSED: return NET_ECONNREFUSED; - case EMSGSIZE: return NET_EMSGSIZE; - case EINTR: return 4; - case EINVAL: return NET_EINVAL; - case ENOTSOCK: return NET_ENOTSOCK; - case EPIPE: return 32; - default: return 5; - } -} +int net_error(int code) { return GuestSockets::GuestErrorFromHost(code); } int native_error() { return net_error(errno); } #endif -struct NativeSocketHandle { +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length); +#endif + +struct NativeSocketHandle : KernelSocketPoll::NativeOwner { NativeSocket value; - explicit NativeSocketHandle(NativeSocket socket) : value(socket) {} + const bool stream; + std::atomic nonblock{false}; + std::atomic receiveTimeout{0}; + std::atomic sendTimeout{0}; + NativeSocketHandle(NativeSocket socket, bool stream) : value(socket), stream(stream) { +#ifdef _WIN32 + transfer = &TransferSocketForKernel; +#endif + } ~NativeSocketHandle() { if (value != INVALID_NATIVE_SOCKET) close_socket(value); } @@ -253,13 +213,10 @@ struct Sock { int family = 0; int type = 0; std::shared_ptr native; - bool nonblock = false; bool bound = false; bool listening = false; bool aborted = false; bool read_shut = false; - int rcv_timeout_us = 0; - int snd_timeout_us = 0; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; @@ -349,6 +306,7 @@ int* errno_slot() { } extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity); void set_errno(int err) { *errno_slot() = err; @@ -393,8 +351,8 @@ int set_kernel_timeout(int descriptor, int optname, const void* value, std::uint const auto it = g_socks.find(descriptor); if (it != g_socks.end()) { const int stored = static_cast(std::min(microseconds, INT_MAX)); - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = stored; - else it->second.snd_timeout_us = stored; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = stored; + else it->second.native->sendTimeout = stored; } return 0; } @@ -482,7 +440,7 @@ std::int64_t message_length(const NetMsghdr* message) { int receive_error(const Sock& socket) { #ifdef _WIN32 - if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.rcv_timeout_us > 0) return NET_EAGAIN; + if (WSAGetLastError() == WSAETIMEDOUT && socket.type != NET_SOCK_STREAM && socket.native->receiveTimeout > 0) return NET_EAGAIN; #else (void)socket; #endif @@ -493,21 +451,60 @@ bool message_flags_supported(int flags, int accepted) { return (flags & ~accepted) == 0; } -bool would_block(const Sock& socket, int flags, bool send) { +template +auto native_transfer(const NativeSocketHandle& socket, int flags, bool send, TOperation operation, bool timed = true, int length = 0) { #ifdef _WIN32 - if ((flags & NET_MSG_DONTWAIT) == 0) return false; - WSAPOLLFD entry{}; - entry.fd = socket.native->value; - entry.events = send ? POLLWRNORM : POLLRDNORM; - return WSAPoll(&entry, 1, 0) == 0; + using Result = decltype(operation(0)); + const bool nonblocking = (flags & NET_MSG_DONTWAIT) != 0 || socket.nonblock.load(); + const int completeLength = send && socket.stream && !nonblocking ? length : 0; + const int timeout = timed ? (send ? socket.sendTimeout.load() : socket.receiveTimeout.load()) : 0; + const auto deadline = timeout > 0 ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) + : std::chrono::steady_clock::time_point::max(); + int transferred = 0; + for (;;) { + const auto result = operation(transferred); + if (result != static_cast(-1)) { + if (completeLength == 0) return result; + transferred += static_cast(result); + if (transferred == completeLength || result == 0) return static_cast(transferred); + } else if (WSAGetLastError() != WSAEWOULDBLOCK || nonblocking) { + return transferred > 0 ? static_cast(transferred) : result; + } + int milliseconds = 10; + if (timeout > 0) { + const auto remaining = deadline - std::chrono::steady_clock::now(); + if (remaining <= std::chrono::steady_clock::duration::zero()) { + WSASetLastError(completeLength > 0 ? WSAEWOULDBLOCK : WSAETIMEDOUT); + return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } + milliseconds = static_cast(std::min(std::chrono::ceil(remaining).count(), milliseconds)); + } + WSAPOLLFD descriptor{}; + descriptor.fd = socket.value; + descriptor.events = send ? POLLWRNORM : POLLRDNORM; + const int ready = WSAPoll(&descriptor, 1, milliseconds); + if (ready < 0) return transferred > 0 ? static_cast(transferred) : static_cast(-1); + } #else static_cast(socket); static_cast(flags); static_cast(send); - return false; + static_cast(timed); + static_cast(length); + return operation(0); #endif } +#ifdef _WIN32 +int TransferSocketForKernel(KernelSocketPoll::NativeOwner* owner, bool send, void* buffer, int length) { + const auto* socket = static_cast(owner); + return native_transfer(*socket, 0, send, [&](int offset) { + return send ? ::send(socket->value, offset ? static_cast(buffer) + offset : static_cast(buffer), length - offset, 0) + : ::recv(socket->value, static_cast(buffer), length, 0); + }, true, length); +} +#endif + int native_message_flags(int flags) { int native = (flags & NET_MSG_PEEK) ? MSG_PEEK : 0; #ifndef _WIN32 @@ -548,9 +545,10 @@ int wait_for_local_address(int s, const Sock& socket, bool empty, int flags) { if (socket.type == NET_SOCK_STREAM || socket.bound) return 0; const bool named = has_local_address(socket.native->value); if (!named && (socket.read_shut || empty)) return RECEIVE_NOTHING; - if (!named && (socket.nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; - const auto deadline = socket.rcv_timeout_us > 0 - ? std::chrono::steady_clock::now() + std::chrono::microseconds(socket.rcv_timeout_us) + if (!named && (socket.native->nonblock || (flags & NET_MSG_DONTWAIT) != 0)) return NET_EAGAIN; + const int timeout = socket.native->receiveTimeout; + const auto deadline = timeout > 0 + ? std::chrono::steady_clock::now() + std::chrono::microseconds(timeout) : std::chrono::steady_clock::time_point::max(); std::unique_lock lk(g_mutex); for (bool waited = false;; waited = true) { @@ -726,7 +724,7 @@ bool ControlSocketStatusForKernel(int descriptor, int command, int flags, int* r std::lock_guard lk(g_mutex); const auto it = g_socks.find(descriptor); if (it == g_socks.end()) return false; - nonblock = it->second.nonblock; + nonblock = it->second.native->nonblock; } *result = 0; *error = 0; @@ -754,7 +752,7 @@ bool FindSocketNativeForKernel(int descriptor, std::shared_ptr* owner, std const auto it = g_socks.find(descriptor); if (it == g_socks.end() || !it->second.native) return false; *native = static_cast(it->second.native->value); - *owner = it->second.native; + *owner = std::static_pointer_cast(it->second.native); return true; } @@ -800,6 +798,14 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) if (!initialize_sockets()) return fail(5); const NativeSocket native = ::socket(family == NET_AF_INET ? AF_INET : AF_INET6, type, protocol); if (native == INVALID_NATIVE_SOCKET) return fail(native_error()); +#ifdef _WIN32 + u_long nonblocking = 1; + if (ioctlsocket(native, FIONBIO, &nonblocking) != 0) { + const int error = native_error(); + close_socket(native); + return fail(error); + } +#endif const int ipv6_only = 1; if (family == NET_AF_INET6 && ::setsockopt(native, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6_only), sizeof(ipv6_only)) != 0) { @@ -809,7 +815,7 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) } std::shared_ptr handle; try { - handle = std::make_shared(native); + handle = std::make_shared(native, type == NET_SOCK_STREAM); } catch (const std::bad_alloc&) { close_socket(native); return fail(12); @@ -927,12 +933,15 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { if (addr && !addrlen) return fail(NET_EINVAL); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - const NativeSocket accepted = ::accept(listener.native->value, - addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + const NativeSocket accepted = native_transfer(*listener.native, 0, false, [&](int) { + peer_length = sizeof(peer); + return ::accept(listener.native->value, + addr ? reinterpret_cast(&peer) : nullptr, addr ? &peer_length : nullptr); + }, false); if (accepted == INVALID_NATIVE_SOCKET) return fail(native_error()); std::shared_ptr handle; try { - handle = std::make_shared(accepted); + handle = std::make_shared(accepted, true); } catch (const std::bad_alloc&) { close_socket(accepted); return fail(12); @@ -941,13 +950,17 @@ int APS5_VABI sceNetAccept(int s, void* addr, uint32_t* addrlen) { connection.family = listener.family; connection.type = NET_SOCK_STREAM; connection.native = std::move(handle); +#ifdef _WIN32 + connection.native->receiveTimeout = listener.native->receiveTimeout.load(); + connection.native->sendTimeout = listener.native->sendTimeout.load(); +#endif const int descriptor = KernelReserveSocketNumber_nid_no_patch(static_cast(connection.native->value)); if (descriptor < 0) return fail(-descriptor); { std::lock_guard lk(g_mutex); g_socks.emplace(descriptor, connection); } - if (listener.nonblock) { + if (listener.native->nonblock) { const int enabled = 1; if (sceNetSetsockopt(descriptor, NET_SOL_SOCKET, NET_SO_NBIO, &enabled, sizeof(enabled)) != 0) { const int error = *errno_slot(); @@ -1025,7 +1038,21 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { return 0; } #ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); + if (WSAGetLastError() == WSAEWOULDBLOCK) { + if (socket.native->nonblock) return fail(NET_EINPROGRESS); + fd_set writable; + fd_set failed; + FD_ZERO(&writable); + FD_ZERO(&failed); + FD_SET(socket.native->value, &writable); + FD_SET(socket.native->value, &failed); + if (::select(0, nullptr, &writable, &failed, nullptr) < 0) return fail(native_error()); + int error = 0; + int size = sizeof(error); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &size) != 0) + return fail(native_error()); + return error == 0 ? 0 : fail(net_error(error)); + } #endif return fail(native_error()); } @@ -1046,9 +1073,10 @@ int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { #endif const GuestArena::HostWrite destination(len == 0 ? nullptr : buf, len); if (!destination.Open()) return fail(NET_EFAULT); - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); - const int result = ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + const int result = native_transfer(*socket.native, flags, false, [&](int) { + return ::recv(socket.native->value, static_cast(buf), static_cast(len), native_flags); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) return static_cast(len); #endif @@ -1084,10 +1112,12 @@ int64_t APS5_VABI sceNetRecvfrom(int s, void* buf, size_t len, int flags, void* if (!destination.Open()) return fail(NET_EFAULT); sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); const int native_flags = native_message_flags(flags); - int result = ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), - native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + int result = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, static_cast(buf), static_cast(len), + native_flags, from ? reinterpret_cast(&peer) : nullptr, from ? &peer_length : nullptr); + }); #ifdef _WIN32 if (result < 0 && socket.type == NET_SOCK_DGRAM && WSAGetLastError() == WSAEMSGSIZE) result = static_cast(len); #endif @@ -1107,8 +1137,9 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (would_block(socket, flags, true)) return fail(NET_EAGAIN); - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + const int result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); return result >= 0 ? result : fail(native_error()); } @@ -1123,18 +1154,21 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - if (would_block(socket, flags, true)) return fail(NET_EAGAIN); int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags)); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::send(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags)); + }, true, static_cast(len)); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS | native_message_flags(flags), - reinterpret_cast(&destination), destination_length); + result = native_transfer(*socket.native, flags, true, [&](int offset) { + return ::sendto(socket.native->value, offset ? static_cast(buf) + offset : static_cast(buf), static_cast(len) - offset, NATIVE_SEND_FLAGS | native_message_flags(flags), + reinterpret_cast(&destination), destination_length); + }, true, static_cast(len)); } if (result < 0) return fail(native_error()); #ifdef _WIN32 @@ -1190,7 +1224,6 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { return 0; } #endif - if (would_block(socket, flags, false)) return fail(NET_EAGAIN); std::vector buffer; try { buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); @@ -1199,8 +1232,11 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { } sockaddr_storage peer{}; NativeLength peer_length = sizeof(peer); - std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), - native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + std::int64_t received = native_transfer(*socket.native, flags, false, [&](int) { + peer_length = sizeof(peer); + return ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + native_message_flags(flags), reinterpret_cast(&peer), &peer_length); + }); if (received < 0) { #ifdef _WIN32 if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(receive_error(socket)); @@ -1282,10 +1318,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval int value = 0; std::memcpy(&value, optval, sizeof(value)); if (optname == NET_SO_NBIO) { -#ifdef _WIN32 - u_long enabled = value != 0; - if (ioctlsocket(socket.native->value, FIONBIO, &enabled) != 0) return fail(native_error()); -#else +#ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, value ? old_flags | O_NONBLOCK : old_flags & ~O_NONBLOCK) != 0) return fail(native_error()); @@ -1304,9 +1337,9 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { - if (optname == NET_SO_NBIO) it->second.nonblock = value != 0; - if (optname == NET_SO_RCVTIMEO) it->second.rcv_timeout_us = value; - if (optname == NET_SO_SNDTIMEO) it->second.snd_timeout_us = value; + if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; + if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; + if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; if (optname == NET_SO_SNDBUF) it->second.snd_buffer = value; if (optname == NET_SO_RCVBUF) it->second.rcv_buffer = value; if (optname == NET_SO_ONESBCAST) it->second.onesbcast = value; @@ -1324,7 +1357,7 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint } if (optval == nullptr || optlen == nullptr) return fail(NET_EINVAL); if (level == NET_SOL_SOCKET && (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO)) { - const int microseconds = optname == NET_SO_RCVTIMEO ? socket.rcv_timeout_us : socket.snd_timeout_us; + const int microseconds = optname == NET_SO_RCVTIMEO ? socket.native->receiveTimeout.load() : socket.native->sendTimeout.load(); const KernelTimeval timeout{microseconds / 1000000, microseconds % 1000000}; *optlen = std::min(*optlen, static_cast(sizeof(timeout))); std::memcpy(optval, &timeout, *optlen); @@ -1346,9 +1379,9 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (timeout_option(optname)) { - case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; - case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; - case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; + case NET_SO_NBIO: value = socket.native->nonblock ? 1 : 0; break; + case NET_SO_RCVTIMEO: value = socket.native->receiveTimeout; break; + case NET_SO_SNDTIMEO: value = socket.native->sendTimeout; break; case NET_SO_ONESBCAST: value = socket.onesbcast; break; case NET_SO_ERROR: { int pending = 0; @@ -1500,7 +1533,7 @@ static int fill_sock_info(int s, NetSockInfo& record) { record.remote_adr = remote.sin_addr.s_addr; record.local_port = local.sin_port; record.remote_port = remote.sin_port; - record.flags = NET_SOCKINFO_F_SELF | (socket.nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); + record.flags = NET_SOCKINFO_F_SELF | (socket.native->nonblock ? NET_SOCKINFO_F_NONBLOCK : 0); return 0; } @@ -1554,7 +1587,7 @@ int APS5_VABI sceNetShowNetstatWithMemory(int memid) { poolSize = pool->second; sockets.reserve(g_socks.size()); for (const auto& [id, socket] : g_socks) - sockets.push_back({id, socket.family, socket.type, socket.nonblock, socket.bound, socket.listening}); + sockets.push_back({id, socket.family, socket.type, socket.native->nonblock, socket.bound, socket.listening}); } std::fprintf(stdout, "sceNet netstat: pool=%d bytes=%zu sockets=%zu\n", memid, poolSize, sockets.size()); for (const auto& socket : sockets) @@ -2118,9 +2151,13 @@ int APS5_VABI sceNetGetAppMemInfo(void) { return 0; } -int APS5_VABI sceNetGetDnsInfo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNetGetDnsInfo(std::uint32_t* info, int flags) { + if (flags != 0) throw std::runtime_error("sceNetGetDnsInfo: flags " + std::to_string(flags) + " are not supported"); + if (!info) throw std::runtime_error("sceNetGetDnsInfo: a null info is not supported"); + std::array servers{}; + const std::size_t count = std::min(NetCtlHostDnsServers_nid_no_patch(servers.data(), servers.size()), servers.size()); + std::memcpy(info, servers.data(), sizeof(servers)); + return static_cast(count); } int APS5_VABI sceNetGetIfList(void) { diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 1e11aadff5..12f11a7a1a 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -64,6 +64,8 @@ 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_SERVER = 20; static constexpr int NET_CTL_INFO_HTTP_PROXY_PORT = 21; +static constexpr int NET_CTL_INFO_V6_IP_ADDRESS = 1; +static constexpr int NET_CTL_INFO_V6_SECONDARY_DNS = 5; 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; @@ -350,6 +352,13 @@ static bool FindHostIpv4Interface(HostIpv4Interface& found) { #endif } +extern "C" std::size_t NetCtlHostDnsServers_nid_no_patch(std::uint32_t* servers, std::size_t capacity) { + HostIpv4Interface host; + if (!FindHostIpv4Interface(host)) return 0; + for (std::size_t index = 0; index < host.dns.size() && index < capacity; ++index) servers[index] = host.dns[index].s_addr; + return host.dns.size(); +} + static void WriteInfoWord(NetCtlInfo* info, std::uint32_t value) { std::memcpy(info->opaque, &value, sizeof(value)); } @@ -456,11 +465,6 @@ int APS5_VABI sceNetCtlGetInfo(int code, NetCtlInfo* info) { return 0; } -int APS5_VABI sceNetCtlGetInfoV6() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo* nat_info) { if (!nat_info) return SCE_NET_CTL_ERROR_INVALID_ADDR; return SCE_NET_CTL_ERROR_NOT_CONNECTED; @@ -497,6 +501,13 @@ int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* c return RegisterCallback(g_callbacksV6, func, arg, cid); } +int APS5_VABI sceNetCtlGetInfoV6(int code, void* info) { + if (!info) return SCE_NET_CTL_ERROR_INVALID_ADDR; + if (code < NET_CTL_INFO_V6_IP_ADDRESS || code > NET_CTL_INFO_V6_SECONDARY_DNS) return SCE_NET_CTL_ERROR_INVALID_CODE; + if (!HostHasAddress(AF_INET6)) return SCE_NET_CTL_ERROR_NOT_CONNECTED; + throw std::runtime_error("sceNetCtlGetInfoV6: info code " + std::to_string(code) + " is not implemented"); +} + void APS5_VABI sceNetCtlTerm(void) { std::lock_guard lock(g_callbackLock); for (auto& slot : g_callbacks) slot = {}; diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index 11b8a99218..bb57fa5ba4 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -20,6 +20,7 @@ static constexpr int MAX_SLOTS = 64; namespace { struct Request { + int titleCtxId = 0; bool started = false; bool aborted = false; int result = 0; @@ -68,10 +69,17 @@ int APS5_VABI sceNpTusCreateRequest(int titleCtxId) { if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; if (requests.size() >= MAX_REQUESTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; const int reqId = nextRequest++; - requests.emplace(reqId, Request{}); + requests.emplace(reqId, Request{titleCtxId}); return reqId; } +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(tusMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); + return 0; +} + int APS5_VABI sceNpTusDeleteRequest(int reqId) { std::lock_guard lock(tusMutex); return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; @@ -126,4 +134,614 @@ int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAcc return StartOffline(reqId); } +int APS5_VABI sceNpTusSetThreadParam(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssCreateNpTitleCtxA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateTitleCtx(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetTimeout(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusChangeModeForOtherSaveDataOwners(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusWaitAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariable(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetSmallStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorage(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetStorageAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTssGetDataAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusAddAndGetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetFriendsDataStatusForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusDeleteMultiSlotDataAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableA(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableAVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSave(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUser(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 68b159393a..efbb4743fc 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -78,8 +78,8 @@ int APS5_VABI scePadGetControllerInformation(int handle, PadControllerInformatio } std::memset(info, 0, sizeof(*info)); info->touchPadInfo.pixelDensity = 44.86f; - info->touchPadInfo.resolution.x = 1920; - info->touchPadInfo.resolution.y = 943; + info->touchPadInfo.resolution.x = kTouchPadWidth; + info->touchPadInfo.resolution.y = kTouchPadHeight; info->stickInfo.deadZoneLeft = 2; info->stickInfo.deadZoneRight = 2; info->connectionType = PAD_CONNECTION_TYPE_LOCAL; @@ -122,8 +122,8 @@ int APS5_VABI scePadGetExtControllerInformation(int handle, void* info) { std::memset(info, 0, 0x2c); PadControllerInformation* base = static_cast(info); base->touchPadInfo.pixelDensity = 44.86f; - base->touchPadInfo.resolution.x = 1920; - base->touchPadInfo.resolution.y = 943; + base->touchPadInfo.resolution.x = kTouchPadWidth; + base->touchPadInfo.resolution.y = kTouchPadHeight; base->stickInfo.deadZoneLeft = 2; base->stickInfo.deadZoneRight = 2; base->connectionType = PAD_CONNECTION_TYPE_LOCAL; diff --git a/core/libs/prx/libScePad/include/PadState.hpp b/core/libs/prx/libScePad/include/PadState.hpp index 8186ee492a..fb621171ee 100644 --- a/core/libs/prx/libScePad/include/PadState.hpp +++ b/core/libs/prx/libScePad/include/PadState.hpp @@ -6,6 +6,9 @@ #include #include "SceTypes.hpp" +constexpr std::uint16_t kTouchPadWidth = 1920; +constexpr std::uint16_t kTouchPadHeight = 1080; + struct PadTouchPoint { bool active = false; std::uint16_t x = 0; diff --git a/core/libs/prx/libScePad/src/PadState.cpp b/core/libs/prx/libScePad/src/PadState.cpp index a5a82256eb..5a6795f32e 100644 --- a/core/libs/prx/libScePad/src/PadState.cpp +++ b/core/libs/prx/libScePad/src/PadState.cpp @@ -202,8 +202,8 @@ PadData BuildData(const PadInputState& state, std::uint64_t changeTime, std::uin if (touchNum == 0 && (state.touchLeft || state.touchRight || injected.touchLeft || injected.touchRight)) { data.buttons |= 0x100000; touchNum = 1; - data.touch_data_touch0_x = state.touchRight || injected.touchRight ? 1440 : 480; - data.touch_data_touch0_y = 471; + data.touch_data_touch0_x = state.touchRight || injected.touchRight ? kTouchPadWidth * 3 / 4 : kTouchPadWidth / 4; + data.touch_data_touch0_y = kTouchPadHeight / 2; data.touch_data_touch0_id = 0; } data.touch_data_touch_num = touchNum; diff --git a/core/libs/prx/libSceRazorCpu/CMakeLists.txt b/core/libs/prx/libSceRazorCpu/CMakeLists.txt index 38dc075ae6..e4d7808ed9 100644 --- a/core/libs/prx/libSceRazorCpu/CMakeLists.txt +++ b/core/libs/prx/libSceRazorCpu/CMakeLists.txt @@ -6,6 +6,7 @@ if(BUILD_TESTING) add_test_executable(guest_razor_cpu_tests ${LIBS_INCLUDE_DIR}/tests/GuestRazorCpu.cpp) target_include_directories(guest_razor_cpu_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_razor_cpu_tests PRIVATE libSceRazorCpu) + configure_windows_unwind(guest_razor_cpu_tests) add_test(NAME guest_razor_cpu COMMAND guest_razor_cpu_tests) if(WIN32) set_tests_properties(guest_razor_cpu PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 0714155d1b..af49b0fa3c 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -69,17 +69,16 @@ int APS5_VABI sceRazorCpuSync(void) { } void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void) { - NotImplemented_nid_no_patch(__func__); } int APS5_VABI sceRazorCpuInitDataTags(void* buffer, size_t size) { (void)buffer; (void)size; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuShutdownDataTags(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(uint32_t tagCount) { @@ -93,7 +92,7 @@ int APS5_VABI sceRazorCpuTagArray(const char* label, uint32_t category, void* ar (void)array; (void)bufferSize; (void)elementSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* buffer, size_t bufferSize) { @@ -101,20 +100,20 @@ int APS5_VABI sceRazorCpuTagBuffer(const char* label, uint32_t category, void* b (void)category; (void)buffer; (void)bufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuResizeTaggedBuffer(uint32_t category, void* buffer, size_t newBufferSize) { (void)category; (void)buffer; (void)newBufferSize; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuUnTagBuffer(uint32_t category, void* buffer) { (void)category; (void)buffer; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t tag, uint64_t size, uint8_t operation) { @@ -122,11 +121,11 @@ int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, uint64_t t (void)tag; (void)size; (void)operation; - NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI sceRazorCpuEndLogicalFileAccess(void) { - NotImplemented_nid_no_patch(__func__); + return 0; } } diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 636719b717..698fcbb800 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -356,7 +356,10 @@ struct Session { const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { #ifdef _WIN32 - const auto sent = ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + auto* owner = static_cast(session.socketOwner.get()); + const auto sent = owner && owner->transfer + ? owner->transfer(owner, true, const_cast(buffer), chunk) + : ::send(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto sent = ::send(static_cast(session.socket), buffer, static_cast(chunk), MSG_NOSIGNAL); #endif @@ -375,7 +378,10 @@ struct Session { const auto chunk = static_cast(std::min(length, INT_MAX)); for (;;) { #ifdef _WIN32 - const auto received = ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); + auto* owner = static_cast(session.socketOwner.get()); + const auto received = owner && owner->transfer + ? owner->transfer(owner, false, buffer, chunk) + : ::recv(static_cast(session.socket), reinterpret_cast(buffer), chunk, 0); #else const auto received = ::recv(static_cast(session.socket), buffer, static_cast(chunk), 0); #endif diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 645da47bd0..d7a2e4817a 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -38,6 +38,8 @@ if(BUILD_TESTING) add_test(NAME guest_requested_exit COMMAND guest_system_exit_tests --requested-exit-driver) add_test(NAME guest_quick_exit COMMAND guest_system_exit_tests --quick-exit) add_test(NAME guest_immediate_exit COMMAND guest_system_exit_tests --immediate) + add_test(NAME guest_immediate_exit_drain COMMAND guest_system_exit_tests --exit-drain-driver) + set_tests_properties(guest_immediate_exit_drain PROPERTIES TIMEOUT 20) add_test(NAME guest_return_from_main COMMAND guest_system_exit_tests --return-from-main) add_test(NAME guest_system_relaunch COMMAND guest_system_exit_tests --relaunch-driver) add_test(NAME guest_nested_exit COMMAND guest_system_exit_tests --nested-exit-driver) @@ -62,7 +64,7 @@ if(BUILD_TESTING) set_tests_properties(guest_system_load_exec_standard_error PROPERTIES TIMEOUT 20) endif() if(WIN32) - set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_system_exit guest_requested_exit guest_system_exit_parameters guest_immediate_exit guest_immediate_exit_drain guest_return_from_main guest_quick_exit guest_system_relaunch guest_nested_exit guest_exit_finalizers_before_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_system_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemService.cpp) diff --git a/core/libs/prx/libSceUlt/CMakeLists.txt b/core/libs/prx/libSceUlt/CMakeLists.txt index 25fa2dad49..ea7e2f4a31 100644 --- a/core/libs/prx/libSceUlt/CMakeLists.txt +++ b/core/libs/prx/libSceUlt/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp) +add_library(libSceUlt SHARED EXCLUDE_FROM_ALL Export.cpp src/ReaderWriterLock.cpp) target_include_directories(libSceUlt PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceUlt) @@ -41,4 +41,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + add_test_executable(guest_ult_sync_tests ${LIBS_INCLUDE_DIR}/tests/GuestUltSync.cpp) + target_include_directories(guest_ult_sync_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ult_sync_tests PRIVATE libSceUlt libc) + configure_windows_unwind(guest_ult_sync_tests) + foreach(testCase IN ITEMS mutex-trylock opt-params rwlock-validation rwlock-readers rwlock-writer rwlock-pool rwlock-finalize ulthreads) + add_test(NAME guest_ult_sync_${testCase} COMMAND guest_ult_sync_tests ${testCase}) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_ult_sync_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() endif() diff --git a/core/libs/prx/libSceUlt/Export.cpp b/core/libs/prx/libSceUlt/Export.cpp index 7d2af42297..2e4eb263f3 100644 --- a/core/libs/prx/libSceUlt/Export.cpp +++ b/core/libs/prx/libSceUlt/Export.cpp @@ -14,9 +14,12 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "SceUltTypes.hpp" +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" extern "C" int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" void APS5_VABI scePthreadExit(void* retval); +extern "C" void APS5_VABI scePthreadYield(); namespace { @@ -108,8 +111,11 @@ void ReleaseQueue(UltQueueState& state) { --pool._queues; } +thread_local UltUlthreadState* gCurrentUlthread = nullptr; + void* APS5_VABI ulthreadRunner(void* arg) { auto* state = static_cast(arg); + gCurrentUlthread = state; const auto result = state->_entry(state->_arg); state->_exited.store(true, std::memory_order_release); return reinterpret_cast(static_cast(result)); @@ -117,6 +123,11 @@ void* APS5_VABI ulthreadRunner(void* arg) { } +bool UltWaitingPoolExists(void* pool) { + std::lock_guard lock(gMutex); + return gResourcePools.contains(pool); +} + extern "C" { int APS5_VABI sceUltInitialize() { @@ -154,6 +165,7 @@ int APS5_VABI sceUltFinalize() { } gConditions.clear(); gMutexes.clear(); + UltFinalizeReaderWriterLocks(); gResourcePools.clear(); for (auto& entry : gQueues) { auto& state = *entry.second; @@ -184,6 +196,31 @@ int APS5_VABI sceUltMutexOptParamInitialize(UltMutexOptParam* optParam, std::uin return ULT_OK; } +int APS5_VABI sceUltSemaphoreOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltUlthreadOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + int APS5_VABI sceUltMutexCreate(void* mutex, const char* name, void* waitingQueueResourcePool, const UltMutexOptParam* optParam, std::uint32_t buildVersion) { (void)name; (void)buildVersion; @@ -261,6 +298,33 @@ int APS5_VABI sceUltMutexLock(void* mutex) { return ULT_OK; } +int APS5_VABI sceUltMutexTryLock(void* mutex) { + std::shared_ptr state; + { + std::lock_guard lock(gMutex); + auto it = gMutexes.find(mutex); + if (it == gMutexes.end()) { + return mutex == nullptr ? ULT_ERROR_NULL : ULT_ERROR_STATE; + } + state = it->second; + } + const auto self = std::this_thread::get_id(); + std::lock_guard lock(state->_mutex); + if (!state->_alive) { + return ULT_ERROR_STATE; + } + if (state->_owner == self) { + ++state->_lockCount; + return ULT_OK; + } + if (state->_lockCount != 0) { + return ULT_ERROR_BUSY; + } + state->_owner = self; + state->_lockCount = 1; + return ULT_OK; +} + int APS5_VABI sceUltMutexUnlock(void* mutex) { std::shared_ptr state; { @@ -429,6 +493,9 @@ int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void* pool) { } } } + if (UltReaderWriterLocksWaitOn(pool)) { + return ULT_ERROR_BUSY; + } gResourcePools.erase(it); return ULT_OK; } @@ -699,6 +766,7 @@ int APS5_VABI sceUltUlthreadCreate(void* ulthread, const char* name, UltUlthread auto state = std::make_shared(); state->_entry = entry; state->_arg = arg; + state->_object = ulthread; state->_runtime = runtime; { std::lock_guard lock(gMutex); @@ -762,6 +830,34 @@ int APS5_VABI sceUltUlthreadTryJoin(void* ulthread, std::int32_t* status) { return sceUltUlthreadJoin(ulthread, status); } +int APS5_VABI sceUltUlthreadGetSelf(void** ulthread) { + if (ulthread == nullptr) { + return ULT_ERROR_NULL; + } + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + *ulthread = gCurrentUlthread->_object; + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadYield() { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + scePthreadYield(); + return ULT_OK; +} + +int APS5_VABI sceUltUlthreadExit(std::int32_t status) { + if (gCurrentUlthread == nullptr) { + return ULT_ERROR_STATE; + } + gCurrentUlthread->_exited.store(true, std::memory_order_release); + scePthreadExit(reinterpret_cast(static_cast(status))); + return ULT_OK; +} + int APS5_VABI sceUltUlthreadRuntimeDestroy(void* runtime) { if (runtime == nullptr) { return ULT_ERROR_NULL; @@ -866,61 +962,6 @@ int APS5_VABI sceUltGetWaitingQueueResourcePoolInfo() { return 0; } -int APS5_VABI sceUltMutexTryLock() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockDestroy() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockLockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockLockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockTryLockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockTryLockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockUnlockRead() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockUnlockWrite() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadExit() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadGetSelf() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadYield() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceUltAbstractThreadFactoryCreateThread() { NotImplemented_nid_no_patch(__func__); return 0; @@ -991,39 +1032,4 @@ int APS5_VABI sceUltConditionVariableOptParamInitialize() { return 0; } -int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltQueueOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockCreate() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltReaderWriterLockOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltSemaphoreOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltUlthreadOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceUlt/SceUltTypes.hpp b/core/libs/prx/libSceUlt/SceUltTypes.hpp index 0da430382c..efe53ec5a8 100644 --- a/core/libs/prx/libSceUlt/SceUltTypes.hpp +++ b/core/libs/prx/libSceUlt/SceUltTypes.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,18 @@ constexpr int ULT_ERROR_STATE = -2139029498; constexpr int ULT_ERROR_BUSY = -2139029497; constexpr int ULT_ERROR_AGAIN = -2139029496; +struct UltOptParam { + std::uint8_t bytes[128]; +}; + +inline int InitializeOptParam(UltOptParam* optParam) { + if (optParam == nullptr) { + return ULT_ERROR_NULL; + } + std::memset(optParam, 0, sizeof(*optParam)); + return ULT_OK; +} + struct UltMutexState { std::mutex _mutex; std::condition_variable _available; @@ -93,6 +106,7 @@ struct UltRuntimeState { struct UltUlthreadState { UltUlthreadEntry _entry = nullptr; std::uint64_t _arg = 0; + void* _object = nullptr; Pthread _thread = nullptr; void* _runtime = nullptr; std::atomic _exited{false}; diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp new file mode 100644 index 0000000000..7e1273394a --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp @@ -0,0 +1,200 @@ +#include "prx/libSceUlt/src/ReaderWriterLock.hpp" +#include "prx/libSceUlt/SceUltTypes.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct UltReaderWriterLockState { + std::mutex _mutex; + std::condition_variable _released; + std::uint32_t _readers = 0; + bool _writing = false; + std::thread::id _writer{}; + std::uint32_t _waiters = 0; + bool _alive = true; + void* _waitingPool = nullptr; +}; + +std::mutex gLocksMutex; +std::unordered_map> gLocks; + +int FindLock(void* rwlock, std::shared_ptr& state) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + state = it->second; + return ULT_OK; +} + +bool HeldByCaller(const UltReaderWriterLockState& state) { + return state._writing && state._writer == std::this_thread::get_id(); +} + +int Wait(UltReaderWriterLockState& state, std::unique_lock& lock, bool writer) { + ++state._waiters; + state._released.wait(lock, [&] { return !state._alive || (!state._writing && (!writer || state._readers == 0)); }); + --state._waiters; + return state._alive ? ULT_OK : ULT_ERROR_STATE; +} + +} + +bool UltReaderWriterLocksWaitOn(void* pool) { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + if (entry.second->_waitingPool != pool) continue; + std::lock_guard stateLock(entry.second->_mutex); + if (entry.second->_waiters != 0) return true; + } + return false; +} + +void UltFinalizeReaderWriterLocks() { + std::lock_guard lock(gLocksMutex); + for (const auto& entry : gLocks) { + std::lock_guard stateLock(entry.second->_mutex); + entry.second->_alive = false; + entry.second->_released.notify_all(); + } + gLocks.clear(); +} + +extern "C" { + +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(UltOptParam* optParam, std::uint32_t buildVersion) { + (void)buildVersion; + return InitializeOptParam(optParam); +} + +int APS5_VABI sceUltReaderWriterLockCreate(void* rwlock, const char* name, void* waitingQueueResourcePool, const UltOptParam* optParam, std::uint32_t buildVersion) { + (void)name; + (void)optParam; + (void)buildVersion; + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + if (waitingQueueResourcePool != nullptr && !UltWaitingPoolExists(waitingQueueResourcePool)) { + return ULT_ERROR_INVALID; + } + auto state = std::make_shared(); + state->_waitingPool = waitingQueueResourcePool; + std::lock_guard lock(gLocksMutex); + if (gLocks.contains(rwlock)) { + return ULT_ERROR_STATE; + } + std::memset(rwlock, 0, 256); + gLocks[rwlock] = std::move(state); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockDestroy(void* rwlock) { + if (rwlock == nullptr) { + return ULT_ERROR_NULL; + } + std::lock_guard lock(gLocksMutex); + const auto it = gLocks.find(rwlock); + if (it == gLocks.end()) { + return ULT_ERROR_STATE; + } + auto& state = *it->second; + std::lock_guard stateLock(state._mutex); + if (state._readers != 0 || state._writing || state._waiters != 0) { + return ULT_ERROR_BUSY; + } + state._alive = false; + gLocks.erase(it); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, false); waited != ULT_OK) return waited; + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing) { + return ULT_ERROR_BUSY; + } + ++state->_readers; + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockRead(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || state->_readers == 0) { + return ULT_ERROR_STATE; + } + if (--state->_readers == 0) { + state->_released.notify_all(); + } + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::unique_lock lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (const int waited = Wait(*state, lock, true); waited != ULT_OK) return waited; + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + if (state->_writing || state->_readers != 0) { + return ULT_ERROR_BUSY; + } + state->_writing = true; + state->_writer = std::this_thread::get_id(); + return ULT_OK; +} + +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void* rwlock) { + std::shared_ptr state; + if (const int found = FindLock(rwlock, state); found != ULT_OK) return found; + std::lock_guard lock(state->_mutex); + if (!state->_alive || !HeldByCaller(*state)) { + return ULT_ERROR_STATE; + } + state->_writing = false; + state->_writer = {}; + state->_released.notify_all(); + return ULT_OK; +} + +} diff --git a/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp new file mode 100644 index 0000000000..df98dcd077 --- /dev/null +++ b/core/libs/prx/libSceUlt/src/ReaderWriterLock.hpp @@ -0,0 +1,8 @@ +#ifndef CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP +#define CORE_LIBS_PRX_LIBSCEULT_SRC_READERWRITERLOCK_HPP + +bool UltWaitingPoolExists(void* pool); +bool UltReaderWriterLocksWaitOn(void* pool); +void UltFinalizeReaderWriterLocks(); + +#endif diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 7aa248c61c..d9abeeb048 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -192,12 +192,12 @@ PadInputState PadInput::sampleController() const { float pressure = 0.0f; if (SDL_GameControllerGetTouchpadFinger(controller, 0, finger, &down, &x, &y, &pressure) != 0 || down == 0) continue; result.touch[finger].active = true; - result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * 1919.0f); - result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); + result.touch[finger].x = static_cast(std::clamp(x, 0.0f, 1.0f) * (kTouchPadWidth - 1)); + result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * (kTouchPadHeight - 1)); } } if (viewPressed && !result.touch[0].active && !result.touch[1].active) { - result.touch[0] = {true, 960, 471, 0}; + result.touch[0] = {true, kTouchPadWidth / 2, kTouchPadHeight / 2, 0}; } return result; } diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index 764d9d9c88..636ad25e45 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -19,6 +19,7 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libSceVideoOut/include/VideoOutDriver.hpp" #if APS5_ENABLE_TIMING_LOG #include "prx/libSceVideoOut/include/FrameTimingLog.hpp" @@ -619,6 +620,7 @@ void VideoOutDriver::processFlip(FlipRequest& req) { } req.terminal = true; req.cfg->vblankCond.notify_all(); + KernelWakeSleepers_nid_no_patch(); timing.Mark("notify_game"); } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 3ff904d406..6cb273c095 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -280,4 +280,64 @@ int APS5_VABI sceVoiceDisableChat(void) { return 0; } +int APS5_VABI sceVoiceInitHQ(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceGetMemorySize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceDeviceInCustomerData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceQAEnableRecording(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADPVTAdjustment(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceVADSetVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetMuteFlagAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceSetBitRate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceUpdatePort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoicePausePortAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceResumePortAll(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index e69c93f6cd..96053a9e69 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -68,6 +69,22 @@ int SetAttribute(std::map& endpoints, std::int32_t id, s return 0; } +int GetAttribute(std::map& endpoints, std::int32_t id, std::int32_t attribute, void* value, std::int32_t size) { + const auto endpoint = endpoints.find(id); + if (endpoint == endpoints.end() || !value || size <= 0) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + const auto attr = endpoint->second.find(attribute); + if (attr != endpoint->second.end() && !attr->second.empty()) { + const auto toCopy = std::min(static_cast(size), attr->second.size()); + std::memcpy(value, attr->second.data(), toCopy); + if (static_cast(size) > toCopy) { + std::memset(static_cast(value) + toCopy, 0, static_cast(size) - toCopy); + } + } else { + std::memset(value, 0, static_cast(size)); + } + return 0; +} + template int Locked(Body&& body) { auto& qos = State(); @@ -144,19 +161,23 @@ int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(int32_t remoteId, int32_t at return Locked([&](QoS& qos) { return SetAttribute(qos.remotes, remoteId, attributeId, value, size); }); } -int APS5_VABI sceVoiceQoSReadPacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSReadPacket(int32_t connectionId, void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + *size = 0; + return 0; + }); } -int APS5_VABI sceVoiceQoSWritePacket() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSWritePacket(int32_t connectionId, const void* buffer, uint32_t* size) { + return Locked([&](QoS& qos) { + if (!buffer || !size || !qos.connections.contains(connectionId)) return SCE_VOICE_ERROR_LIBVOICEQOS_ARGUMENT_INVALID; + return 0; + }); } -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(int32_t localId, int32_t attributeId, void* value, int32_t size) { + return Locked([&](QoS& qos) { return GetAttribute(qos.locals, localId, attributeId, value, size); }); } int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(int32_t remoteId, int32_t attributeId, const void* value, int32_t size) { diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index f6ea8a59b5..3306a6bb46 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -1,4 +1,17 @@ if(BUILD_TESTING) + if(WIN32) + add_test_executable(windows_mappings_release_tests ${LIBS_INCLUDE_DIR}/tests/WindowsMappingsRelease.cpp) + target_include_directories(windows_mappings_release_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(windows_mappings_release_tests PRIVATE libc) + configure_windows_unwind(windows_mappings_release_tests) + add_test(NAME windows_mappings_release COMMAND windows_mappings_release_tests) + set_tests_properties(windows_mappings_release PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + foreach(mode 0 1 2) + add_test(NAME windows_mappings_release_mismatch_${mode} COMMAND windows_mappings_release_tests ${mode}) + set_tests_properties(windows_mappings_release_mismatch_${mode} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endforeach() + endif() + add_test_executable(guest_binary_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestBinaryWord.cpp) target_include_directories(guest_binary_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_binary_word_tests PRIVATE libc) @@ -30,7 +43,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FailureReport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/gcc/StackGuard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CrashReport.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/windows/CallTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/x86_64/CallTrace.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/linux/DlIteratePhdr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/specifics/macos/GuestImages.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Strings.cpp @@ -445,7 +458,7 @@ if(BUILD_TESTING) endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_tests PRIVATE libc) + target_link_libraries(guest_directory_tests PRIVATE libc libkernel) configure_windows_unwind(guest_directory_tests) add_test(NAME guest_directory COMMAND guest_directory_tests) if(WIN32) @@ -605,17 +618,24 @@ if(BUILD_TESTING) set_tests_properties(guest_errors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(WIN32) + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") add_test_executable(guest_call_trace_tests ${LIBS_INCLUDE_DIR}/tests/GuestCallTrace.cpp) target_include_directories(guest_call_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_call_trace_tests PRIVATE libc) configure_windows_unwind(guest_call_trace_tests) add_test(NAME guest_call_trace COMMAND guest_call_trace_tests) add_test(NAME guest_call_trace_abort COMMAND guest_call_trace_tests abort) - set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + if(WIN32) + set_tests_properties(guest_call_trace guest_call_trace_abort PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set_tests_properties(guest_call_trace_abort PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION - "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests.exe[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + "last traced system library calls[^~]*#1 thread [0-9]+ libTraced.prx Integers from [^~]*guest_call_trace_tests${CMAKE_EXECUTABLE_SUFFIX}[^~]*#2 thread [0-9]+ libOther.prx Blocks from[^~]*most called traced imports \\(2 calls in total\\):[^~]* 1 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + if(NOT WIN32) + add_test(NAME guest_call_trace_abort_race COMMAND guest_call_trace_tests abort-race) + set_tests_properties(guest_call_trace_abort_race PROPERTIES TIMEOUT 20 PASS_REGULAR_EXPRESSION + "last traced system library calls[^~]*#102 thread [0-9]+ libTraced.prx Integers from[^~]*most called traced imports \\(102 calls in total\\):[^~]* 101 libTraced.prx Integers[^~]* 1 libOther.prx Blocks") + endif() endif() add_test_executable(guest_syslog_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyslog.cpp) target_include_directories(guest_syslog_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -808,6 +828,7 @@ if(BUILD_TESTING) add_dependencies(application_heap_tests guest_sdk_heap_fixture guest_sdk_heap_partial_fixture) add_test(NAME application_heap_guest_sdk COMMAND application_heap_tests sdk $) add_test(NAME application_heap_guest_sdk_partial COMMAND application_heap_tests sdk-partial $) + add_test(NAME application_heap_guest_sdk_default COMMAND application_heap_tests sdk-default $) endif() if(WIN32) set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order application_heap_scripting application_heap_lazy @@ -838,11 +859,18 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_backtrace_tests PRIVATE libc) + configure_windows_unwind(guest_backtrace_tests) set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(WIN32) + set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -855,7 +883,11 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") add_test_executable(thread_name_tests ${LIBS_INCLUDE_DIR}/tests/ThreadName.cpp) target_include_directories(thread_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(thread_name_tests PRIVATE libc) + configure_windows_unwind(thread_name_tests) add_test(NAME thread_name COMMAND thread_name_tests) + set_tests_properties(thread_name PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") endif() add_test_executable(guest_heap_report_for_game_tests ${LIBS_INCLUDE_DIR}/tests/GuestHeapReportForGame.cpp) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index f5737cc3ce..5fe2c41701 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -119,7 +119,8 @@ public: return randomDevice; } - bool Reopen(const std::filesystem::path& filename, const char* mode) { + bool Reopen(const std::filesystem::path& filename, const char* mode, + [[maybe_unused]] std::FILE* (*open)(const std::filesystem::path&, const std::string&) = nullptr) { auto* previous = GetHandle(); _guest = {}; guestError = false; @@ -130,10 +131,16 @@ public: const int replaced = previous ? _fileno(previous) : -1; auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); - const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; - if (kept >= 0) ::_lock_file(previous); - _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); - if (kept >= 0) RestoreHostStandard(previous, kept); + if (open && !IsHostStandard() && !filename.empty()) { + _handle = nullptr; + std::fclose(previous); + _handle = open(filename, mode); + } else { + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); + _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); + } KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 333900b870..6dade90518 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -51,6 +51,13 @@ extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); #endif +using PthreadCreateFn_nid_no_patch = int (APS5_VABI*)(void**, const void*, void* (APS5_VABI*)(void*), void*); +using PthreadJoinFn_nid_no_patch = int (APS5_VABI*)(void*, void**); +using PthreadDetachFn_nid_no_patch = int (APS5_VABI*)(void*); +extern "C" void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn); +extern "C" void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn); +extern "C" void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn); + #define APS5_INVALID_ARG_EX throw std::invalid_argument(std::string(__func__) + ": invalid argument") #define APS5_DUMMY_FUN \ diff --git a/core/libs/prx/libc/include/Shutdown.hpp b/core/libs/prx/libc/include/Shutdown.hpp index 8e0f9ec209..a59aa05ced 100644 --- a/core/libs/prx/libc/include/Shutdown.hpp +++ b/core/libs/prx/libc/include/Shutdown.hpp @@ -17,5 +17,6 @@ extern "C" [[noreturn]] void LibcExitWithoutHandlers_nid_no_patch(int code); extern "C" [[noreturn]] void LibcTerminate_nid_no_patch(int code); extern "C" void LibcSetEndFlush_nid_no_patch(void (*flush)()); extern "C" void LibcRunEndFlush_nid_no_patch(); +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()); #endif diff --git a/core/libs/prx/libc/include/WindowsDescriptor.hpp b/core/libs/prx/libc/include/WindowsDescriptor.hpp index d8ac9209ca..66ebc1d9ac 100644 --- a/core/libs/prx/libc/include/WindowsDescriptor.hpp +++ b/core/libs/prx/libc/include/WindowsDescriptor.hpp @@ -28,6 +28,15 @@ static inline ACCESS_MASK WindowsHandleAccess_nid_no_patch(HANDLE handle, int de return information.GrantedAccess; } +static inline std::uint32_t WindowsFileNumber_nid_no_patch(std::uint64_t fileId) { + const std::uint32_t folded = static_cast(fileId) ^ (static_cast(fileId >> 32) * 0x9E3779B1u); + return folded == 0 ? 1 : folded; +} + +static inline std::uint32_t WindowsFileNumber_nid_no_patch(const BY_HANDLE_FILE_INFORMATION& identity) { + return WindowsFileNumber_nid_no_patch((static_cast(identity.nFileIndexHigh) << 32) | identity.nFileIndexLow); +} + static inline bool WindowsHandleOnlyAppends_nid_no_patch(ACCESS_MASK access) { return (access & (FILE_WRITE_DATA | FILE_APPEND_DATA)) == FILE_APPEND_DATA; } diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 8b4ec7beb8..0fc0f22ae8 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -519,11 +519,24 @@ private: ~Section() { CloseHandle(handle); } }; struct Saved { - unsigned char* bytes; - explicit Saved(unsigned char* bytes) : bytes(bytes) {} + unsigned char* bytes = nullptr; + std::size_t length; + std::array committed{}; + explicit Saved(std::size_t length) : length(length) { + bytes = static_cast(VirtualAlloc(nullptr, length, MEM_RESERVE, PAGE_READWRITE)); + if (bytes == nullptr) fail("reserve unmapped direct memory"); + } Saved(const Saved&) = delete; Saved& operator=(const Saved&) = delete; ~Saved() { VirtualFree(bytes, 0, MEM_RELEASE); } + + void Commit(std::size_t offset) { + const auto page = offset / pageBytes; + const auto bit = std::uint64_t{1} << (page & 63); + if ((committed[page >> 6] & bit) != 0) return; + if (!VirtualAlloc(bytes + offset, pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + committed[page >> 6] |= bit; + } }; struct Physical { std::weak_ptr page; @@ -687,12 +700,8 @@ private: const auto found = views.find(page); auto& view = found->second; if (!blank(page)) { - if (!saved) { - auto* bytes = static_cast(VirtualAlloc(nullptr, stop - start, MEM_RESERVE, PAGE_READWRITE)); - if (bytes == nullptr) fail("reserve unmapped direct memory"); - saved = std::make_shared(bytes); - } - if (!VirtualAlloc(saved->bytes + (page - start), pageBytes, MEM_COMMIT, PAGE_READWRITE)) fail("keep unmapped direct memory"); + if (!saved) saved = savedMemory(stop - start); + saved->Commit(page - start); std::memcpy(saved->bytes + (page - start), reinterpret_cast(page), pageBytes); view.slot->saved = saved; view.slot->savedOffset = page - start; @@ -704,6 +713,28 @@ private: residentRuns.erase(run); } + std::shared_ptr savedMemory(std::size_t bytes) { + for (const auto& saved : savedPool) { + if (saved.use_count() == 1 && saved->length == bytes) return saved; + } + constexpr std::size_t limit = 32u * 1024u * 1024u; + const auto reserved = (bytes + granularity - 1) / granularity * granularity; + for (auto it = savedPool.begin(); reserved <= limit && savedPoolBytes > limit - reserved && it != savedPool.end();) { + if (it->use_count() != 1) { + ++it; + continue; + } + savedPoolBytes -= ((*it)->length + granularity - 1) / granularity * granularity; + it = savedPool.erase(it); + } + auto saved = std::make_shared(bytes); + if (reserved <= limit && savedPoolBytes <= limit - reserved) { + savedPool.push_back(saved); + savedPoolBytes += reserved; + } + return saved; + } + static void forget(Ranges& ranges, std::uintptr_t start, std::uintptr_t end) { auto it = ranges.lower_bound(start); if (it != ranges.begin() && std::prev(it)->second > start) --it; @@ -820,6 +851,21 @@ private: forgetClean(address, end); breakRun(address); breakRun(end); + if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { + const auto memory = query(address); + if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); + if (memory.Type != MEM_PRIVATE) throw std::runtime_error("resident guest mapping is not private memory"); + auto allocationEnd = address; + do { + const auto part = query(allocationEnd); + if (part.AllocationBase != memory.AllocationBase) break; + allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + } while (allocationEnd < end); + if (allocationEnd != end || query(end).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); + evict(run); + if (!VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); + return; + } for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { @@ -910,6 +956,8 @@ private: } std::map, std::shared_ptr> physical; std::map residentRuns; + std::vector> savedPool; + std::size_t savedPoolBytes = 0; std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 538e038d40..84a8f46eb7 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -156,11 +156,12 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { const auto* replacement = read(libcParameters, 0x30); if (read(replacement, 0) != 0x78 || read(replacement, 8) != 2) throw std::runtime_error("application heap: unsupported allocator replacement table"); std::array api; - if (ApplicationGuestHeapApi_nid_no_patch(api.data())) { + std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); + const bool replaced = std::any_of(api.begin(), api.end(), [](const void* entry) { return entry != nullptr; }); + if (replaced && ApplicationGuestHeapApi_nid_no_patch(api.data())) { ApplicationHeapRegister_nid_no_patch(api.data()); return; } - std::memcpy(api.data(), static_cast(replacement) + 0x20, sizeof(api)); ApplicationHeapRegister_nid_no_patch(api.data()); const auto initialize = read(replacement, 0x10); if (initialize != nullptr) initialize(); diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 29c7dae111..a07a476b4a 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -7,9 +7,35 @@ #include "prx/libc/include/General.hpp" -#ifdef __linux__ +#if defined(__linux__) #include #include +#elif defined(_WIN32) +#include +#include "prx/libc/include/exceptions/Unwind.hpp" +extern "C" { +_Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); +_Unwind_Ptr APS5_VABI _Unwind_GetIP_nid_postfix(_Unwind_Context*); +std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initialCapacity) { + std::string module; + std::size_t capacity = initialCapacity == 0 ? 1 : initialCapacity; + while (true) { + module.resize(capacity); + SetLastError(ERROR_SUCCESS); + const DWORD length = GetModuleFileNameA(handle, module.data(), static_cast(module.size())); + if (length == 0) return {}; + if (length < capacity && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + module.resize(length); + return module; + } + if (capacity >= 32768) return {}; + capacity *= 2; + } +} +} +#endif + +#if defined(__linux__) || defined(_WIN32) namespace { @@ -31,15 +57,45 @@ static_assert(sizeof(FrameRecord) == NameOffset); [[gnu::noinline]] std::vector CaptureFrames(int limit) { void* addresses[MaxFrames]; +#if defined(__linux__) const int captured = backtrace(addresses, MaxFrames); +#elif defined(_WIN32) + struct TraceState { + void** addrs; + int max; + int count; + }; + static const auto traceCallback = [](_Unwind_Context* ctx, void* arg) APS5_VABI -> _Unwind_Reason_Code { + auto* s = static_cast(arg); + if (s->count < s->max) { + s->addrs[s->count++] = reinterpret_cast(_Unwind_GetIP_nid_postfix(ctx)); + } + return (s->count >= s->max) ? _URC_NORMAL_STOP : _URC_NO_REASON; + }; + TraceState state{addresses, MaxFrames, 0}; + _Unwind_Backtrace_nid_postfix(traceCallback, &state); + const int captured = state.count; +#endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { +#if defined(__linux__) Dl_info info{}; if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); if (offset > UINT32_MAX) continue; std::string module = info.dli_fname; - if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); +#elif defined(_WIN32) + HMODULE handle = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addresses[index]), &handle) || handle == nullptr) { + continue; + } + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(handle); + if (offset > UINT32_MAX) continue; + std::string module = BacktraceModulePath_nid_no_patch(handle, MAX_PATH); + if (module.empty()) continue; +#endif + if (const auto slash = module.find_last_of("/\\"); slash != std::string::npos) module.erase(0, slash + 1); if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) module.resize(module.size() - (sizeof(GuestSuffix) - 1)); frames.push_back({static_cast(offset), std::move(module)}); diff --git a/core/libs/prx/libc/src/Directory.cpp b/core/libs/prx/libc/src/Directory.cpp index 2ea8cc0946..425669cdcf 100644 --- a/core/libs/prx/libc/src/Directory.cpp +++ b/core/libs/prx/libc/src/Directory.cpp @@ -9,6 +9,12 @@ #include #include #include +#include +#ifdef _WIN32 +#include "prx/libc/include/WindowsDescriptor.hpp" +#include +#include +#endif namespace { int DirectoryError(int error) { @@ -28,19 +34,102 @@ int DirectoryError(int error) { } } #ifdef _WIN32 -using NativeDirectory = _WDIR; -NativeDirectory* OpenNative(const std::filesystem::path& path) { return ::_wopendir(path.c_str()); } -int CloseNative(NativeDirectory* native) { return ::_wclosedir(native); } -void RewindNative(NativeDirectory* native) { ::_wrewinddir(native); } -long TellNative(NativeDirectory* native) { return ::_wtelldir(native); } -void SeekNative(NativeDirectory* native, long location) { ::_wseekdir(native, location); } +struct NativeDirectory { + HANDLE handle = INVALID_HANDLE_VALUE; + std::uint32_t seed = 0; + bool identifiers = true; + bool restart = true; + bool buffered = false; + std::size_t next = 0; + long position = 0; + alignas(8) std::byte buffer[65536]; + ~NativeDirectory() { if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); } +}; +int NativeOpenError(DWORD error) { + if (error == ERROR_CANT_RESOLVE_FILENAME) return ELOOP; + if (error == ERROR_DIRECTORY) return ENOTDIR; + return WindowsOpenError_nid_no_patch(error); +} +NativeDirectory* OpenNative(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { errno = NativeOpenError(GetLastError()); return nullptr; } + if ((attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + auto native = std::make_unique_for_overwrite(); + native->handle = CreateFileW(path.c_str(), FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (native->handle == INVALID_HANDLE_VALUE) { errno = NativeOpenError(GetLastError()); return nullptr; } + BY_HANDLE_FILE_INFORMATION identity{}; + if (GetFileInformationByHandle(native->handle, &identity)) { + if ((identity.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { errno = ENOTDIR; return nullptr; } + native->seed = WindowsFileNumber_nid_no_patch(identity); + } + return native.release(); +} +int CloseNative(NativeDirectory* native) { delete native; return 0; } +void RewindNative(NativeDirectory* native) { + native->restart = true; + native->buffered = false; + native->position = 0; +} +bool QueryNative(NativeDirectory* native) { + const auto query = [native](FILE_INFO_BY_HANDLE_CLASS restartClass, FILE_INFO_BY_HANDLE_CLASS nextClass) { + return GetFileInformationByHandleEx(native->handle, native->restart ? restartClass : nextClass, native->buffer, sizeof(native->buffer)) != 0; + }; + bool filled = native->identifiers ? query(FileIdBothDirectoryRestartInfo, FileIdBothDirectoryInfo) : query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + DWORD error = filled ? ERROR_SUCCESS : GetLastError(); + if (!filled && native->restart && native->identifiers && (error == ERROR_INVALID_PARAMETER || error == ERROR_INVALID_FUNCTION || + error == ERROR_INVALID_LEVEL || error == ERROR_NOT_SUPPORTED)) { + native->identifiers = false; + filled = query(FileFullDirectoryRestartInfo, FileFullDirectoryInfo); + error = filled ? ERROR_SUCCESS : GetLastError(); + } + if (!filled) { + if (error != ERROR_NO_MORE_FILES && error != ERROR_FILE_NOT_FOUND) errno = EIO; + return false; + } + native->restart = false; + native->buffered = true; + native->next = 0; + return true; +} +const std::byte* NextRecord(NativeDirectory* native) { + if (!native->buffered && !QueryNative(native)) return nullptr; + const std::byte* record = native->buffer + native->next; + const DWORD offset = native->identifiers ? reinterpret_cast(record)->NextEntryOffset + : reinterpret_cast(record)->NextEntryOffset; + if (offset == 0) native->buffered = false; + native->next += offset; + ++native->position; + return record; +} +std::uint32_t StableFileNumber(std::uint32_t seed, std::wstring_view name) { + std::uint32_t hash = 2166136261u ^ seed; + for (const wchar_t character : name) hash = (hash ^ static_cast(character)) * 16777619u; + return hash == 0 ? 1 : hash; +} +long TellNative(NativeDirectory* native) { return native->position; } +void SeekNative(NativeDirectory* native, long location) { + if (location < 0) return; + RewindNative(native); + while (native->position < location && NextRecord(native)) {} +} bool ReadNative(NativeDirectory* native, std::filesystem::path& component, std::string& name, std::uint64_t& inode) { - auto* entry = ::_wreaddir(native); - if (!entry) return false; - component = entry->d_name; + const std::byte* record = NextRecord(native); + if (!record) return false; + std::wstring_view fileName; + std::uint64_t fileId = 0; + if (native->identifiers) { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + fileId = static_cast(information->FileId.QuadPart); + } else { + const auto* information = reinterpret_cast(record); + fileName = std::wstring_view(information->FileName, information->FileNameLength / sizeof(WCHAR)); + } + component = fileName; const auto utf8 = component.u8string(); name.assign(utf8.begin(), utf8.end()); - inode = static_cast(entry->d_ino); + inode = fileId != 0 ? WindowsFileNumber_nid_no_patch(fileId) : StableFileNumber(native->seed, fileName); return true; } #else @@ -87,16 +176,17 @@ GuestDirectoryEntry* ReadEntry(Directory& directory) { std::filesystem::path component; std::string name; std::uint64_t inode = 0; - if (!ReadNative(directory.native, component, name, inode)) { - errno = errno ? DirectoryError(errno) : savedError; - return nullptr; - } + try { + if (!ReadNative(directory.native, component, name, inode)) { + errno = errno ? DirectoryError(errno) : savedError; + return nullptr; + } + } catch (const std::bad_alloc&) { errno = 12; return nullptr; } const auto length = name.size(); if (length > 255) { errno = 63; return nullptr; } try { directory.entry = {}; - // MinGW reports no inode identity (zero); do not fabricate one. - directory.entry.fileNumber = inode <= UINT32_MAX ? static_cast(inode) : 0; + directory.entry.fileNumber = static_cast(inode); directory.entry.recordLength = static_cast(8 + ((length + 1 + 3) & ~3)); directory.entry.nameLength = static_cast(length); directory.entry.type = DirectoryType(directory.path / component); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4b8f5328ba..d28c6c7b6c 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -323,7 +323,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode return nullptr; } } - if (stream->Reopen(path, parsed->native.c_str())) { + if (stream->Reopen(path, parsed->native.c_str(), OpenNative)) { #ifdef _WIN32 if (parsed->append && !AppendOnlyDescriptor(_fileno(stream->GetHandle()))) { const int error = errno; diff --git a/core/libs/prx/libc/src/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp index 304814503c..e3717ef61b 100644 --- a/core/libs/prx/libc/src/FloatingPoint.cpp +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -17,6 +17,17 @@ void ReplaceFlags(unsigned mask, unsigned flags) { _mm_setcsr((_mm_getcsr() & ~mask) | (flags & mask)); } +int ChangeTraps(int exceptions, bool enable) { + const auto mask = static_cast(exceptions) & Exceptions; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + const unsigned csr = _mm_getcsr(); + const auto changed = static_cast(enable ? (control & ~mask) : (control | mask)); + asm volatile("fldcw %0" : : "m"(changed)); + _mm_setcsr(enable ? (csr & ~(mask << 7)) : (csr | (mask << 7))); + return static_cast(~((csr >> 7) | control) | ~Exceptions); +} + } extern "C" { @@ -50,6 +61,24 @@ int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions) return 0; } +int* APS5_VABI __error_nid_postfix(); + +void APS5_VABI _Feraise_nid_postfix(int exceptions) { + constexpr unsigned Invalid = 0x01, Ranges = 0x1c, Overflows = 0x18, Inexact = 0x20; + constexpr int GuestEdom = 33, GuestErange = 34; + auto raised = static_cast(exceptions); + if ((raised & Overflows) != 0) raised |= Inexact; + *__error_nid_postfix() = GuestEdom; + std::array environment; + asm volatile("fnstenv %0" : "=m"(environment)); + environment[1] = (environment[1] & ~raised) | (raised & 0xffff); + asm volatile("fldenv %0" : : "m"(environment)); + _mm_setcsr((_mm_getcsr() & ~raised) | (raised & 0xffff)); + asm volatile("fwait"); + if ((raised & Invalid) != 0) *__error_nid_postfix() = GuestEdom; + else if ((raised & Ranges) != 0) *__error_nid_postfix() = GuestErange; +} + int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions) { if (flags == nullptr) throw std::invalid_argument("fesetexceptflag: null flags"); ReplaceFlags(static_cast(exceptions) & Exceptions, *flags); @@ -67,4 +96,38 @@ int APS5_VABI feclearexcept_nid_postfix(int exceptions) { return 0; } + +int APS5_VABI feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI __feenableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, true); +} + +int APS5_VABI __fedisableexcept_nid_postfix(int exceptions) { + return ChangeTraps(exceptions, false); +} + +int APS5_VABI fegettrapenable_nid_postfix() { + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + return ~control & Exceptions; +} + +int APS5_VABI fesettrapenable_nid_postfix(int exceptions) { + const auto mask = static_cast(exceptions); + if (mask > Exceptions) return 1; + std::uint16_t control; + asm volatile("fnstcw %0" : "=m"(control)); + control = static_cast(control & ~mask); + asm volatile("fldcw %0" : : "m"(control)); + _mm_setcsr(_mm_getcsr() & ~(static_cast(mask) << 7)); + return 0; +} + } diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 2ae7dd343c..a2b3d0fe7d 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -167,6 +167,8 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = if (length == "h") value = static_cast(value); if (length == "hh") value = static_cast(value); } else value = args.Next(); + if (conversion == u'o' && value == 0 && precision == 0 && spec.find('#') != std::string::npos) + spec = spec.substr(0, spec.find('.')) + ".1"; AppendNumber(out, spec + "ll" + static_cast(conversion), value); } else if (In(conversion, "aAeEfFgG")) { if (length == "L") AppendNumber(out, spec + "L" + static_cast(conversion), args.Next()); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d3c8d0753c..b7347468cf 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -86,7 +86,46 @@ struct NameMatch { bool ambiguous = false; }; +#ifdef _WIN32 +bool CaseInsensitiveDirectoryWindows(const std::filesystem::path& directory) { + const auto handle = CreateFileW(directory.c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return false; + const std::unique_ptr owner(handle, &CloseHandle); + struct { ULONG flags; } information{}; + constexpr auto caseSensitiveInfo = static_cast(23); + return GetFileInformationByHandleEx(handle, caseSensitiveInfo, &information, sizeof(information)) && + (information.flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) == 0; +} + +std::error_code MatchNamesWindows(const std::filesystem::path& directory, std::span names) { + for (auto& name : names) { + const auto pattern = directory / name.name; + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileExW(pattern.c_str(), FindExInfoBasic, &data, FindExSearchNameMatch, nullptr, 0); + if (handle == INVALID_HANDLE_VALUE) { + const auto error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_NO_MORE_FILES) continue; + return {static_cast(error), std::system_category()}; + } + const std::unique_ptr owner(handle, &FindClose); + do { + const auto filename = std::filesystem::path(data.cFileName); + if (!SameName(filename.string(), name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = directory / filename; + } while (FindNextFileW(handle, &data)); + const auto error = GetLastError(); + if (error != ERROR_NO_MORE_FILES) return {static_cast(error), std::system_category()}; + } + return {}; +} +#endif + std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { +#ifdef _WIN32 + if (CaseInsensitiveDirectoryWindows(directory)) return MatchNamesWindows(directory, names); +#endif std::error_code error; std::filesystem::directory_iterator entry(directory, error), end; for (; !error && entry != end; entry.increment(error)) { @@ -142,8 +181,26 @@ std::filesystem::path Child(std::filesystem::path parent, const std::string& nam std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector& relative, bool module = false) { const auto direct = Child(root, JoinNames(relative)); std::error_code error; + auto first = relative.begin(); +#ifdef _WIN32 + const auto attributes = GetFileAttributesW(direct.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return direct; + const auto failure = GetLastError(); + const bool missing = attributes == INVALID_FILE_ATTRIBUTES && (failure == ERROR_FILE_NOT_FOUND || failure == ERROR_PATH_NOT_FOUND); + if (!missing && std::filesystem::exists(direct, error)) return direct; + for (auto last = relative.end(); last != relative.begin();) { + const auto parent = Child(root, JoinNames(std::vector(relative.begin(), std::prev(last)))); + const auto parentAttributes = GetFileAttributesW(parent.c_str()); + const bool present = parentAttributes != INVALID_FILE_ATTRIBUTES && (parentAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0; + if (!present && !std::filesystem::exists(parent, error)) { --last; continue; } + root = parent; + first = std::prev(last); + break; + } +#else if (std::filesystem::exists(direct, error)) return direct; - for (auto part = relative.begin(); part != relative.end(); ++part) { +#endif + for (auto part = first; part != relative.end(); ++part) { const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { @@ -707,6 +764,8 @@ extern "C" int WindowsOpenError_nid_no_patch(unsigned long error) { return ENOMEM; case ERROR_DISK_FULL: return ENOSPC; + case ERROR_CANT_RESOLVE_FILENAME: + return ELOOP; default: return EINVAL; } diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 063eca4f01..bfed7a8a4d 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -183,11 +183,31 @@ long double AsinDenominator(long double t) { return 1.0L + t * (q[0] + t * (q[1] + t * (q[2] + t * (q[3] + t * q[4])))); } +template +bool NanWithoutException(T value) { + if constexpr (sizeof(T) == sizeof(std::uint32_t)) { + std::uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT32_C(0x7fffffff)) > UINT32_C(0x7f800000); + } else if constexpr (sizeof(T) == sizeof(std::uint64_t)) { + std::uint64_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + return (bits & UINT64_C(0x7fffffffffffffff)) > UINT64_C(0x7ff0000000000000); + } else { + static_assert(std::numeric_limits::digits == 64); + std::uint64_t significand; + std::uint16_t signAndExponent; + std::memcpy(&significand, &value, sizeof(significand)); + std::memcpy(&signAndExponent, reinterpret_cast(&value) + sizeof(significand), sizeof(signAndExponent)); + return (signAndExponent & 0x7fff) == 0x7fff && (significand & UINT64_C(0x7fffffffffffffff)) != 0; + } +} + template T GuestFmin(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? x : y; @@ -196,9 +216,9 @@ T GuestFmin(T x, T y) { template T GuestFmax(T x, T y) { - if (std::isnan(x)) + if (NanWithoutException(x)) return y; - if (std::isnan(y)) + if (NanWithoutException(y)) return x; if (std::signbit(x) != std::signbit(y)) return std::signbit(x) ? y : x; @@ -210,6 +230,38 @@ float GuestBesselSecondKindZero() { return -std::numeric_limits::infinity(); } +float ScaledHyperbolic(float x, float y, bool sine) { + const auto xBits = std::bit_cast(x); + const auto xMagnitude = xBits & 0x7fffffffu; + const auto yMagnitude = std::bit_cast(y) & 0x7fffffffu; + constexpr std::uint32_t infinity = 0x7f800000u; + if (xMagnitude > infinity) return x; + if (yMagnitude > infinity) return y; + if ((xMagnitude == infinity && yMagnitude == 0) || (sine && xMagnitude == 0 && yMagnitude == infinity)) { + errno = EDOM; + std::feraiseexcept(FE_INVALID); + return std::numeric_limits::quiet_NaN(); + } + if (xMagnitude == infinity) return sine ? x * y : std::copysign(std::numeric_limits::infinity(), y); + if (xMagnitude == 0 || yMagnitude == 0 || yMagnitude == infinity) return sine ? x * y : y; + constexpr double limit = 2.0 * std::numeric_limits::max_exponent; + const float magnitude = std::bit_cast(xMagnitude); + const double argument = magnitude < limit ? static_cast(magnitude) : limit; + const double value = static_cast(y) * (sine ? std::sinh(argument) : std::cosh(argument)); + const bool exponential = magnitude >= (sine ? 7.7f : 8.675f); + float result; + if (exponential && std::fabs(value) >= 0x1p128) { + result = std::copysign(std::numeric_limits::infinity(), y); + } else { + result = static_cast(value); + } + if (exponential && std::isinf(result)) { + errno = ERANGE; + std::feraiseexcept(FE_OVERFLOW | FE_INEXACT); + } + return sine && (xBits & 0x80000000u) != 0 ? -result : result; +} + } extern "C" { @@ -283,6 +335,7 @@ double APS5_VABI scalbn_nid_postfix(double x, int exponent) { return std::scalbn float APS5_VABI scalbnf_nid_postfix(float x, int exponent) { return std::scalbn(x, exponent); } long double APS5_VABI scalbnl_nid_postfix(long double x, int exponent) { return std::scalbn(x, exponent); } float APS5_VABI significandf_nid_postfix(float x) { return x == 0.0f || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } +double APS5_VABI significand_nid_postfix(double x) { return x == 0.0 || !std::isfinite(x) ? x : std::scalbn(x, -std::ilogb(x)); } double APS5_VABI frexp_nid_postfix(double x, int* exponent) { return std::frexp(x, exponent); } float APS5_VABI frexpf_nid_postfix(float x, int* exponent) { return std::frexp(x, exponent); } // Guest long is 64-bit, including on Windows where native long is 32-bit. @@ -582,8 +635,8 @@ int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } double APS5_VABI remquo_nid_postfix(double x, double y, int* quo) { return std::remquo(x, y, quo); } float APS5_VABI remquof_nid_postfix(float x, float y, int* quo) { return std::remquo(x, y, quo); } -float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } -float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } +float APS5_VABI _FSinh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, true); } +float APS5_VABI _FCosh_nid_postfix(float x, float y) { return ScaledHyperbolic(x, y, false); } struct alignas(16) LibcFloatConstant { std::uint32_t bits[4]; }; LibcFloatConstant _FInf_nid_postfix {{0x7f800000u, 0, 0, 0}}; @@ -640,6 +693,26 @@ int APS5_VABI __isfinitel_nid_postfix(long double x) { return (exponent & 0x7fffu) != 0x7fffu; } +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) std::uint16_t epsilon[8]; + alignas(16) std::uint16_t max[8]; + alignas(16) std::uint16_t min[8]; +}; +static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + +DinkumwareFloatCharacteristics _Flt_nid_postfix {6, 24, 38, 128, -37, -125, + {0, 0x3400}, {0xffff, 0x7f7f}, {0, 0x0080}}; +DinkumwareFloatCharacteristics _Dbl_nid_postfix {15, 53, 308, 1024, -307, -1021, + {0, 0, 0, 0x3cb0}, {0xffff, 0xffff, 0xffff, 0x7fef}, {0, 0, 0, 0x0010}}; +DinkumwareFloatCharacteristics _Ldbl_nid_postfix {18, 64, 4932, 16384, -4931, -16381, + {0, 0, 0, 0x8000, 0x3fc0}, {0xffff, 0xffff, 0xffff, 0xffff, 0x7ffe}, {0, 0, 0, 0x8000, 0x0001}}; + static std::mutex g_randLock; static std::uint32_t g_randState = 1; @@ -706,4 +779,14 @@ union SonyDconst { static_assert(std::endian::native == std::endian::little); SonyDconst _Inf_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Denorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _Hugeval_nid_postfix {{0, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +SonyDconst _Nan_nid_postfix {{0, 0, 0, 0x7ff8, 0, 0, 0, 0}}; +SonyDconst _Snan_nid_postfix {{1, 0, 0, 0x7ff0, 0, 0, 0, 0}}; +LibcFloatConstant _FDenorm_nid_postfix {{0x00000001u, 0, 0, 0}}; +LibcFloatConstant _FSnan_nid_postfix {{0x7f800001u, 0, 0, 0}}; +SonyDconst _LDenorm_nid_postfix {{1, 0, 0, 0, 0, 0, 0, 0}}; +SonyDconst _LInf_nid_postfix {{0, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; +SonyDconst _LNan_nid_postfix {{0, 0, 0, 0xc000, 0x7fff, 0, 0, 0}}; +SonyDconst _LSnan_nid_postfix {{1, 0, 0, 0x8000, 0x7fff, 0, 0, 0}}; } diff --git a/core/libs/prx/libc/src/Mspace.cpp b/core/libs/prx/libc/src/Mspace.cpp index 01e8de2a22..8830e5c75c 100644 --- a/core/libs/prx/libc/src/Mspace.cpp +++ b/core/libs/prx/libc/src/Mspace.cpp @@ -339,6 +339,11 @@ void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void* handle, void* poi return sceLibcMspaceReallocalign_nid_postfix(handle, pointer, alignment, size); } +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void* handle, void** result, std::size_t alignment, std::size_t size, const void* owner) { + (void)owner; + return sceLibcMspacePosixMemalign_nid_postfix(handle, result, alignment, size); +} + int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats) { return sceLibcMspaceMallocStats_nid_postfix(handle, stats); } diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 5865fe5933..e7dc400ac3 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/SonyCrt.hpp" +#include #include #include #include @@ -303,6 +304,13 @@ int APS5_VABI wcsrtombs_s_nid_postfix( return 0; } +int APS5_VABI wcstombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit +) { + std::array state{}; + return wcsrtombs_s_nid_postfix(result, destination, capacity, &source, limit, state.data()); +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/prx/libc/src/Process.cpp b/core/libs/prx/libc/src/Process.cpp index e6d58a919f..9d2f6909b0 100644 --- a/core/libs/prx/libc/src/Process.cpp +++ b/core/libs/prx/libc/src/Process.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,7 @@ extern "C" void LibcRunShutdown_nid_postfix() { namespace { std::atomic endFlush{nullptr}; +std::atomic exitDrain{nullptr}; void (*previousAbortHandler)(int) = SIG_DFL; void FlushOnAbort(int number) { @@ -129,6 +131,12 @@ extern "C" void LibcRunEndFlush_nid_no_patch() { if (const auto flush = endFlush.load(std::memory_order_acquire)) flush(); } +extern "C" void LibcSetExitDrain_nid_no_patch(void (*drain)()) { + if (drain == nullptr) throw std::runtime_error("libc: invalid exit drain registration"); + void (*registered)() = nullptr; + if (!exitDrain.compare_exchange_strong(registered, drain) && registered != drain) throw std::runtime_error("libc: exit drain already registered"); +} + namespace { void StartExit() { @@ -154,6 +162,26 @@ extern "C" { } [[noreturn]] void LibcTerminate_nid_no_patch(int code) { + if (const auto drain = exitDrain.exchange(nullptr)) { + try { + auto completion = std::make_shared>(); + auto finished = completion->get_future(); + std::thread([completion, drain] { + try { + drain(); + completion->set_value(); + } catch (...) { + completion->set_exception(std::current_exception()); + } + }).detach(); + if (finished.wait_for(std::chrono::seconds(8)) == std::future_status::ready) finished.get(); + else std::fputs("[libc] host exit drain exceeded 8 seconds\n", stderr); + } catch (const std::exception& error) { + std::fprintf(stderr, "[libc] host exit drain failed: %s\n", error.what()); + } catch (...) { + std::fputs("[libc] host exit drain failed\n", stderr); + } + } LibcRunEndFlush_nid_no_patch(); #ifdef _WIN32 TerminateProcess(GetCurrentProcess(), static_cast(code)); diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index a6cb27bb7c..18b9cb6f18 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -28,20 +29,25 @@ long long APS5_VABI strtonum_nid_postfix(const char* text, long long minimum, lo return value; } -char* APS5_VABI basename_nid_postfix(const char* path) { - thread_local char buffer[1024]; +char* APS5_VABI basename_r_nid_postfix(const char* path, char* buffer) { + if (buffer == nullptr) throw std::invalid_argument("basename_r: null buffer"); std::string_view name = path && *path ? path : "."; while (name.size() > 1 && name.back() == '/') name.remove_suffix(1); if (name != "/") { const auto separator = name.find_last_of('/'); if (separator != std::string_view::npos) name.remove_prefix(separator + 1); } - if (name.size() >= sizeof(buffer)) { errno = 63; return nullptr; } + if (name.size() >= 1024) { errno = 63; return nullptr; } std::memcpy(buffer, name.data(), name.size()); buffer[name.size()] = '\0'; return buffer; } +char* APS5_VABI basename_nid_postfix(const char* path) { + thread_local char buffer[1024]; + return basename_r_nid_postfix(path, buffer); +} + char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int address) { thread_local char buffer[sizeof("255.255.255.255")]; unsigned char bytes[sizeof(address)]; diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 8774ef52ff..65d6dfb9c6 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -183,7 +183,9 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha return value; } char* parsedEnd = nullptr; + const int saved = errno; const TFloat value = parse(text, &parsedEnd); + if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; return parsedEnd == text ? TFloat{0} : value; } @@ -231,6 +233,32 @@ void* APS5_VABI memrchr_nid_postfix(const void* s, int c, size_t n) { return nullptr; } +void* APS5_VABI memccpy_nid_postfix(void* dest, const void* src, int c, size_t n) { + auto* d = static_cast(dest); + const auto* s = static_cast(src); + const auto target = static_cast(c); + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + if (s[i] == target) return d + i + 1; + } + return nullptr; +} + +void* APS5_VABI memmem_nid_postfix(const void* haystack, size_t haystacklen, const void* needle, size_t needlelen) { + if (needlelen == 0) return const_cast(haystack); + if (haystacklen < needlelen) return nullptr; + const auto* h = static_cast(haystack); + const auto* n = static_cast(needle); + const unsigned char first = n[0]; + const size_t limit = haystacklen - needlelen; + for (size_t i = 0; i <= limit; ++i) { + if (h[i] == first && std::memcmp(h + i, n, needlelen) == 0) { + return const_cast(h + i); + } + } + return nullptr; +} + int APS5_VABI strcmp_nid_postfix(const char* s1, const char* s2) { return std::strcmp(s1, s2); } diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index c12cf7c03c..3373def131 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -384,5 +384,118 @@ int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); } +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +static_assert(offsetof(GuestPad, condition) == 0x8 && offsetof(GuestPad, mutex) == 0x10 && offsetof(GuestPad, started) == 0x18 && sizeof(GuestPad) == 0x20); + +[[noreturn]] void ThrowCppError_nid_no_patch(int code); + +static void RequireThrd(int result, const char* what) { + switch (result) { + case threadSuccess: + return; + case threadNomem: + case threadTimedout: + ThrowCppError_nid_no_patch(6); + case threadBusy: + ThrowCppError_nid_no_patch(0); + case threadError: + ThrowCppError_nid_no_patch(1); + } + throw std::runtime_error(std::string(what) + " failed with " + std::to_string(result)); +} + +static std::atomic registeredPthreadCreate{nullptr}; +static std::atomic registeredPthreadJoin{nullptr}; +static std::atomic registeredPthreadDetach{nullptr}; + +void RegisterPthreadCreate_nid_no_patch(PthreadCreateFn_nid_no_patch fn) { + registeredPthreadCreate.store(fn, std::memory_order_release); +} + +void RegisterPthreadJoin_nid_no_patch(PthreadJoinFn_nid_no_patch fn) { + registeredPthreadJoin.store(fn, std::memory_order_release); +} + +void RegisterPthreadDetach_nid_no_patch(PthreadDetachFn_nid_no_patch fn) { + registeredPthreadDetach.store(fn, std::memory_order_release); +} + +static void* APS5_VABI PadEntry_nid_no_patch(void* argument) { + auto* pad = static_cast(argument); + const auto go = pad->vtable[0]; + return reinterpret_cast(static_cast(go(pad))); +} + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Cnd_init_nid_postfix(&pad->condition), "_Pad::_Pad: _Cnd_init"); + const int created = _Mtx_init_nid_postfix(&pad->mutex, 1); + if (created != threadSuccess) { + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(created, "_Pad::_Pad: _Mtx_init"); + } + pad->started = false; + const int locked = _Mtx_lock_nid_postfix(&pad->mutex); + if (locked != threadSuccess) { + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); + RequireThrd(locked, "_Pad::_Pad: _Mtx_lock"); + } +} + +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::~_Pad: _Mtx_unlock"); + _Mtx_destroy_nid_postfix(&pad->mutex); + _Cnd_destroy_nid_postfix(&pad->condition); +} + +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad) { + if (!pad) APS5_INVALID_ARG_EX; + RequireThrd(_Mtx_lock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_lock"); + pad->started = true; + RequireThrd(_Cnd_signal_nid_postfix(&pad->condition), "_Pad::_Release: _Cnd_signal"); + RequireThrd(_Mtx_unlock_nid_postfix(&pad->mutex), "_Pad::_Release: _Mtx_unlock"); +} + +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread) { + if (!pad || !thread) APS5_INVALID_ARG_EX; + const auto create = registeredPthreadCreate.load(std::memory_order_acquire); + if (!create) throw std::runtime_error("_Pad::_Launch: pthread_create is not registered"); + const int created = create(thread, nullptr, PadEntry_nid_no_patch, pad); + if (created != 0) ThrowCppError_nid_no_patch(6); + while (!pad->started) RequireThrd(_Cnd_wait_nid_postfix(&pad->condition, &pad->mutex), "_Pad::_Launch: _Cnd_wait"); +} + +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code) { + const auto join = registeredPthreadJoin.load(std::memory_order_acquire); + if (!join) throw std::runtime_error("_Thrd_join: pthread_join is not registered"); + void* result = nullptr; + if (join(thread, &result) != 0) return threadError; + if (code) *code = static_cast(reinterpret_cast(result)); + return threadSuccess; +} + +int APS5_VABI _Thrd_detach_nid_postfix(void* thread) { + const auto detach = registeredPthreadDetach.load(std::memory_order_acquire); + if (!detach) throw std::runtime_error("_Thrd_detach: pthread_detach is not registered"); + return detach(thread) == 0 ? threadSuccess : threadError; +} + +int APS5_VABI _Thrd_id_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +[[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code) { + ThrowCppError_nid_no_patch(code); +} + } diff --git a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp b/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp deleted file mode 100644 index 7e30971457..0000000000 --- a/core/libs/prx/libc/src/specifics/windows/CallTrace.cpp +++ /dev/null @@ -1,202 +0,0 @@ -#ifdef _WIN32 -#include -#include "prx/libc/include/CallTrace.hpp" -#include "prx/libc/include/general/AsmMacros.hpp" -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -extern "C" void CallTraceEnter_nid_no_patch(); - -namespace { - -struct ImportRecord { - void* target; - std::string name; - std::string library; - mutable std::atomic calls{0}; -}; - -struct RingSlot { - std::atomic sequence{0}; - std::atomic thread{0}; - std::atomic returnAddress{0}; - std::atomic record{nullptr}; -}; - -constexpr std::size_t RingCapacity = 65536; -constexpr std::size_t ThunkBytes = 24; -constexpr std::size_t ThunkAreaBytes = 1024 * 1024; - -std::mutex g_mutex; -std::deque g_records; -std::unordered_map g_thunkByTarget; -std::unique_ptr g_ring; -std::atomic g_sequence{0}; -std::uint8_t* g_writable = nullptr; -std::uint8_t* g_executable = nullptr; -std::size_t g_thunks = 0; - -bool Selected(std::string_view library) { - const char* value = std::getenv("APS5_TRACE_CALLS"); - if (value == nullptr || *value == '\0') return false; - std::string_view rest(value); - if (library.ends_with(".prx")) library.remove_suffix(4); - while (!rest.empty()) { - const auto comma = rest.find(','); - auto token = rest.substr(0, comma); - rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); - if (token.ends_with(".prx")) token.remove_suffix(4); - if (token == "all" || token == library) return true; - } - return false; -} - -void MapThunkArea() { - const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); - if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); - g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); - g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); - const DWORD code = GetLastError(); - CloseHandle(section); - if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); -} - -void* WriteThunk(const ImportRecord& record) { - if ((g_thunks + 1) * ThunkBytes > ThunkAreaBytes) throw std::runtime_error("call trace: thunk area exhausted"); - const std::size_t offset = g_thunks * ThunkBytes; - const std::uint8_t prefix[] = {0x49, 0xbb}; - const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; - const auto recordAddress = reinterpret_cast(&record); - const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); - std::uint8_t* thunk = g_writable + offset; - std::memcpy(thunk, prefix, sizeof(prefix)); - std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); - std::memcpy(thunk + 10, jump, sizeof(jump)); - std::memcpy(thunk + 16, &handler, sizeof(handler)); - FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); - ++g_thunks; - return g_executable + offset; -} - -} - -static_assert(offsetof(ImportRecord, target) == 0); - -extern "C" { - -void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { - static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); - const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; - RingSlot& slot = g_ring[sequence % RingCapacity]; - slot.sequence.store(0, std::memory_order_release); - slot.thread.store(GetCurrentThreadId(), std::memory_order_relaxed); - slot.returnAddress.store(returnAddress, std::memory_order_relaxed); - slot.record.store(static_cast(record), std::memory_order_relaxed); - slot.sequence.store(sequence, std::memory_order_release); -} - -void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { - if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; - std::lock_guard lock(g_mutex); - if (g_ring == nullptr) { - MapThunkArea(); - g_ring = std::make_unique(RingCapacity); - } - if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; - g_records.emplace_back(target, name, library); - void* thunk = WriteThunk(g_records.back()); - g_thunkByTarget.emplace(target, thunk); - return thunk; -} - -std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { - if (entries == nullptr || capacity == 0 || g_ring == nullptr) return 0; - const std::uint64_t last = g_sequence.load(std::memory_order_acquire); - const std::uint64_t span = std::min({last, capacity, RingCapacity}); - std::size_t count = 0; - for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { - const RingSlot& slot = g_ring[sequence % RingCapacity]; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - const auto* record = slot.record.load(std::memory_order_relaxed); - const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; - if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; - entries[count++] = entry; - } - return count; -} - -std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { - if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); - if (entries == nullptr || capacity == 0) return 0; - std::unique_lock lock(g_mutex, std::try_to_lock); - if (!lock.owns_lock()) return 0; - std::size_t count = 0; - for (const auto& record : g_records) { - const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); - if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; - std::size_t position = count < capacity ? count++ : count - 1; - while (position > 0 && entries[position - 1].calls < calls) { - entries[position] = entries[position - 1]; - --position; - } - entries[position] = {calls, record.name.c_str(), record.library.c_str()}; - } - return count; -} - -} - -asm(".text\n" - APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") - " push %rdi\n" - " push %rsi\n" - " push %rdx\n" - " push %rcx\n" - " push %r8\n" - " push %r9\n" - " push %rax\n" - " push %r10\n" - " push %r11\n" - " sub $128, %rsp\n" - " movdqu %xmm0, 0(%rsp)\n" - " movdqu %xmm1, 16(%rsp)\n" - " movdqu %xmm2, 32(%rsp)\n" - " movdqu %xmm3, 48(%rsp)\n" - " movdqu %xmm4, 64(%rsp)\n" - " movdqu %xmm5, 80(%rsp)\n" - " movdqu %xmm6, 96(%rsp)\n" - " movdqu %xmm7, 112(%rsp)\n" - " mov %r11, %rdi\n" - " mov 200(%rsp), %rsi\n" - " call CallTraceRecord_nid_no_patch\n" - " movdqu 0(%rsp), %xmm0\n" - " movdqu 16(%rsp), %xmm1\n" - " movdqu 32(%rsp), %xmm2\n" - " movdqu 48(%rsp), %xmm3\n" - " movdqu 64(%rsp), %xmm4\n" - " movdqu 80(%rsp), %xmm5\n" - " movdqu 96(%rsp), %xmm6\n" - " movdqu 112(%rsp), %xmm7\n" - " add $128, %rsp\n" - " pop %r11\n" - " pop %r10\n" - " pop %rax\n" - " pop %r9\n" - " pop %r8\n" - " pop %rcx\n" - " pop %rdx\n" - " pop %rsi\n" - " pop %rdi\n" - " jmp *(%r11)\n"); -#endif diff --git a/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp new file mode 100644 index 0000000000..f5e0920719 --- /dev/null +++ b/core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp @@ -0,0 +1,419 @@ +#if defined(_WIN32) || defined(__linux__) +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#include +#endif +#include "prx/libc/include/CallTrace.hpp" +#include "prx/libc/include/general/AsmMacros.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" void CallTraceEnter_nid_no_patch(); + +namespace { + +struct ImportRecord { + void* target = nullptr; + std::string name; + std::string library; + mutable std::atomic calls{0}; +}; + +struct RingSlot { + std::atomic sequence{0}; + std::atomic thread{0}; + std::atomic returnAddress{0}; + std::atomic record{nullptr}; +}; + +constexpr std::size_t RingCapacity = 65536; +constexpr std::size_t ThunkBytes = 24; +constexpr std::size_t ThunkAreaBytes = 1024 * 1024; +constexpr std::size_t RecordCapacity = ThunkAreaBytes / ThunkBytes; + +std::mutex g_mutex; +std::unordered_map g_thunkByTarget; +std::atomic g_records{nullptr}; +std::atomic g_recordCount{0}; +std::atomic g_ring{nullptr}; +std::atomic g_sequence{0}; +std::uint8_t* g_writable = nullptr; +std::uint8_t* g_executable = nullptr; + +bool Selected(std::string_view library) { + const char* value = std::getenv("APS5_TRACE_CALLS"); + if (value == nullptr || *value == '\0') return false; + std::string_view rest(value); + if (library.ends_with(".prx")) library.remove_suffix(4); + while (!rest.empty()) { + const auto comma = rest.find(','); + auto token = rest.substr(0, comma); + rest = comma == std::string_view::npos ? std::string_view{} : rest.substr(comma + 1); + if (token.ends_with(".prx")) token.remove_suffix(4); + if (token == "all" || token == library) return true; + } + return false; +} + +std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +#ifdef _WIN32 +void MapThunkArea() { + const HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, static_cast(ThunkAreaBytes), nullptr); + if (section == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "call trace: CreateFileMappingW"); + g_writable = static_cast(MapViewOfFile(section, FILE_MAP_WRITE, 0, 0, ThunkAreaBytes)); + g_executable = static_cast(MapViewOfFile(section, FILE_MAP_READ | FILE_MAP_EXECUTE, 0, 0, ThunkAreaBytes)); + const DWORD code = GetLastError(); + CloseHandle(section); + if (g_writable == nullptr || g_executable == nullptr) throw std::system_error(static_cast(code), std::system_category(), "call trace: MapViewOfFile"); +} +#else +void MapThunkArea() { + const int file = memfd_create("aps5-call-trace", MFD_CLOEXEC); + if (file < 0) throw std::system_error(errno, std::generic_category(), "call trace: memfd_create"); + void* writable = MAP_FAILED; + void* executable = MAP_FAILED; + if (ftruncate(file, ThunkAreaBytes) == 0) { + writable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + if (writable != MAP_FAILED) executable = mmap(nullptr, ThunkAreaBytes, PROT_READ | PROT_EXEC, MAP_SHARED, file, 0); + } + const int code = errno; + close(file); + if (writable == MAP_FAILED || executable == MAP_FAILED) { + if (writable != MAP_FAILED) munmap(writable, ThunkAreaBytes); + throw std::system_error(code, std::generic_category(), "call trace: mmap"); + } + g_writable = static_cast(writable); + g_executable = static_cast(executable); +} + +struct ModuleSegment { + std::uint64_t start; + std::uint64_t end; + std::uint64_t base; + char name[64]; +}; + +struct ModuleMap { + std::size_t count; + ModuleSegment segments[256]; +}; + +struct sigaction g_previousAbort{}; +std::atomic g_modules{nullptr}; +std::atomic g_reporting{false}; +std::atomic g_reported{false}; +unsigned long long g_moduleChanges = ~0ull; + +void SnapshotModules() { + unsigned long long changes = 0; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + *static_cast(data) = info->dlpi_adds + info->dlpi_subs; + return 1; + }, &changes); + if (changes == g_moduleChanges) return; + g_moduleChanges = changes; + auto* map = new ModuleMap{}; + dl_iterate_phdr([](dl_phdr_info* info, std::size_t, void* data) { + auto& modules = *static_cast(data); + const char* path = info->dlpi_name != nullptr && *info->dlpi_name != '\0' ? info->dlpi_name : program_invocation_short_name; + const char* slash = std::strrchr(path, '/'); + const char* name = slash == nullptr ? path : slash + 1; + for (int index = 0; index < info->dlpi_phnum && modules.count < std::size(modules.segments); ++index) { + const auto& header = info->dlpi_phdr[index]; + if (header.p_type != PT_LOAD || header.p_memsz == 0) continue; + auto& segment = modules.segments[modules.count++]; + segment.base = info->dlpi_addr; + segment.start = info->dlpi_addr + header.p_vaddr; + segment.end = segment.start + header.p_memsz; + std::strncpy(segment.name, name, sizeof(segment.name) - 1); + } + return 0; + }, map); + g_modules.store(map, std::memory_order_release); +} + +class ReportLine { +public: + ReportLine& Text(const char* text) { + while (*text != '\0' && _length < sizeof(_buffer)) _buffer[_length++] = *text++; + return *this; + } + + ReportLine& Decimal(std::uint64_t value, std::size_t width = 0) { + char digits[20]; + std::size_t count = 0; + do { + digits[count++] = static_cast('0' + value % 10); + value /= 10; + } while (value != 0); + for (std::size_t pad = count; pad < width; ++pad) Text(" "); + while (count > 0 && _length < sizeof(_buffer)) _buffer[_length++] = digits[--count]; + return *this; + } + + ReportLine& Hex(std::uint64_t value, std::size_t digits) { + while (digits < 16 && (value >> (digits * 4)) != 0) ++digits; + Text("0x"); + for (std::size_t index = digits; index > 0 && _length < sizeof(_buffer); --index) { + _buffer[_length++] = "0123456789abcdef"[(value >> ((index - 1) * 4)) & 0xf]; + } + return *this; + } + + void Write() { + Text("\n"); + std::size_t written = 0; + while (written < _length) { + const ssize_t result = ::write(STDERR_FILENO, _buffer + written, _length - written); + if (result < 0 && errno == EINTR) continue; + if (result <= 0) break; + written += static_cast(result); + } + _length = 0; + } + +private: + char _buffer[512]; + std::size_t _length = 0; +}; + +void DescribeAddress(ReportLine& line, std::uint64_t address) { + line.Hex(address, 16); + const ModuleMap* modules = g_modules.load(std::memory_order_acquire); + if (modules == nullptr) return; + for (std::size_t index = 0; index < modules->count; ++index) { + const auto& segment = modules->segments[index]; + if (address < segment.start || address >= segment.end) continue; + line.Text(" ").Text(segment.name).Text("+"); + line.Hex(address - segment.base, 1); + return; + } +} + +void ReportCallTrace() { + static CallTraceEntry entries[64]; + static CallTraceCount counts[32]; + ReportLine line; + const std::size_t count = CopyCallTrace_nid_no_patch(entries, std::size(entries)); + if (count == 0) return; + line.Text(" last traced system library calls (APS5_TRACE_CALLS):").Write(); + for (std::size_t index = 0; index < count; ++index) { + line.Text(" #").Decimal(entries[index].sequence).Text(" thread ").Decimal(entries[index].thread); + line.Text(" ").Text(entries[index].library).Text(" ").Text(entries[index].name).Text(" from "); + DescribeAddress(line, entries[index].returnAddress); + line.Write(); + } + std::uint64_t total = 0; + const std::size_t counted = CopyCallCounts_nid_no_patch(counts, std::size(counts), &total); + if (counted == 0) return; + line.Text(" most called traced imports (").Decimal(total).Text(" calls in total):").Write(); + for (std::size_t index = 0; index < counted; ++index) { + line.Text(" ").Decimal(counts[index].calls, 12).Text(" ").Text(counts[index].library).Text(" ").Text(counts[index].name).Write(); + } +} + +void ReportOnAbort(int number, siginfo_t* info, void* context) { + const int savedErrno = errno; + if (!g_reporting.exchange(true, std::memory_order_acq_rel)) { + ReportLine line; + line.Text("[libc] SIGABRT").Write(); + ReportCallTrace(); + g_reported.store(true, std::memory_order_release); + } else { + const timespec pause{0, 1000000}; + for (int waited = 0; waited < 2000 && !g_reported.load(std::memory_order_acquire); ++waited) nanosleep(&pause, nullptr); + } + errno = savedErrno; + if ((g_previousAbort.sa_flags & SA_SIGINFO) != 0) { + if (g_previousAbort.sa_sigaction != nullptr) g_previousAbort.sa_sigaction(number, info, context); + return; + } + if (g_previousAbort.sa_handler == SIG_IGN) return; + if (g_previousAbort.sa_handler != SIG_DFL) { + g_previousAbort.sa_handler(number); + return; + } + struct sigaction fallback{}; + fallback.sa_handler = SIG_DFL; + sigemptyset(&fallback.sa_mask); + sigaction(number, &fallback, nullptr); + raise(number); +} + +void InstallAbortReport() { + struct sigaction action{}; + action.sa_sigaction = ReportOnAbort; + action.sa_flags = SA_SIGINFO; + sigemptyset(&action.sa_mask); + if (sigaction(SIGABRT, &action, &g_previousAbort) != 0) throw std::system_error(errno, std::generic_category(), "call trace: sigaction"); +} +#endif + +void* WriteThunk(const ImportRecord& record, const std::size_t index) { + const std::size_t offset = index * ThunkBytes; + const std::uint8_t prefix[] = {0x49, 0xbb}; + const std::uint8_t jump[] = {0xff, 0x25, 0, 0, 0, 0}; + const auto recordAddress = reinterpret_cast(&record); + const auto handler = reinterpret_cast(&CallTraceEnter_nid_no_patch); + std::uint8_t* thunk = g_writable + offset; + std::memcpy(thunk, prefix, sizeof(prefix)); + std::memcpy(thunk + 2, &recordAddress, sizeof(recordAddress)); + std::memcpy(thunk + 10, jump, sizeof(jump)); + std::memcpy(thunk + 16, &handler, sizeof(handler)); +#ifdef _WIN32 + FlushInstructionCache(GetCurrentProcess(), g_executable + offset, ThunkBytes); +#else + __builtin___clear_cache(reinterpret_cast(g_executable + offset), reinterpret_cast(g_executable + offset + ThunkBytes)); +#endif + return g_executable + offset; +} + +} + +static_assert(offsetof(ImportRecord, target) == 0); + +extern "C" { + +void APS5_VABI CallTraceRecord_nid_no_patch(const void* record, std::uint64_t returnAddress) { + static_cast(record)->calls.fetch_add(1, std::memory_order_relaxed); + const std::uint64_t sequence = g_sequence.fetch_add(1, std::memory_order_relaxed) + 1; + RingSlot& slot = g_ring.load(std::memory_order_relaxed)[sequence % RingCapacity]; + slot.sequence.store(0, std::memory_order_release); + slot.thread.store(CurrentThread(), std::memory_order_relaxed); + slot.returnAddress.store(returnAddress, std::memory_order_relaxed); + slot.record.store(static_cast(record), std::memory_order_relaxed); + slot.sequence.store(sequence, std::memory_order_release); +} + +void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) { + if (target == nullptr || name == nullptr || library == nullptr || !Selected(library)) return target; + std::lock_guard lock(g_mutex); + if (g_ring.load(std::memory_order_relaxed) == nullptr) { + MapThunkArea(); + g_records.store(new ImportRecord[RecordCapacity], std::memory_order_release); + g_ring.store(new RingSlot[RingCapacity], std::memory_order_release); +#ifdef __linux__ + InstallAbortReport(); +#endif + } +#ifdef __linux__ + SnapshotModules(); +#endif + if (const auto existing = g_thunkByTarget.find(target); existing != g_thunkByTarget.end()) return existing->second; + const std::size_t index = g_recordCount.load(std::memory_order_relaxed); + if (index == RecordCapacity) throw std::runtime_error("call trace: thunk area exhausted"); + ImportRecord& record = g_records.load(std::memory_order_relaxed)[index]; + record.target = target; + record.name = name; + record.library = library; + void* thunk = WriteThunk(record, index); + g_thunkByTarget.emplace(target, thunk); + g_recordCount.store(index + 1, std::memory_order_release); + return thunk; +} + +std::size_t CopyCallTrace_nid_no_patch(CallTraceEntry* entries, std::size_t capacity) { + const RingSlot* ring = g_ring.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0 || ring == nullptr) return 0; + const std::uint64_t last = g_sequence.load(std::memory_order_acquire); + const std::uint64_t span = std::min({last, capacity, RingCapacity}); + std::size_t count = 0; + for (std::uint64_t sequence = last - span + 1; sequence <= last; ++sequence) { + const RingSlot& slot = ring[sequence % RingCapacity]; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + const auto* record = slot.record.load(std::memory_order_relaxed); + const CallTraceEntry entry{sequence, slot.thread.load(std::memory_order_relaxed), slot.returnAddress.load(std::memory_order_relaxed), record->name.c_str(), record->library.c_str()}; + if (slot.sequence.load(std::memory_order_acquire) != sequence) continue; + entries[count++] = entry; + } + return count; +} + +std::size_t CopyCallCounts_nid_no_patch(CallTraceCount* entries, std::size_t capacity, std::uint64_t* total) { + if (total != nullptr) *total = g_sequence.load(std::memory_order_acquire); + if (entries == nullptr || capacity == 0) return 0; + const std::size_t recorded = g_recordCount.load(std::memory_order_acquire); + const ImportRecord* records = g_records.load(std::memory_order_acquire); + std::size_t count = 0; + for (std::size_t index = 0; index < recorded; ++index) { + const ImportRecord& record = records[index]; + const std::uint64_t calls = record.calls.load(std::memory_order_relaxed); + if (calls == 0 || (count == capacity && calls <= entries[count - 1].calls)) continue; + std::size_t position = count < capacity ? count++ : count - 1; + while (position > 0 && entries[position - 1].calls < calls) { + entries[position] = entries[position - 1]; + --position; + } + entries[position] = {calls, record.name.c_str(), record.library.c_str()}; + } + return count; +} + +} + +asm(".text\n" + APS5_ASM_FUNCTION("CallTraceEnter_nid_no_patch") + " push %rdi\n" + " push %rsi\n" + " push %rdx\n" + " push %rcx\n" + " push %r8\n" + " push %r9\n" + " push %rax\n" + " push %r10\n" + " push %r11\n" + " sub $128, %rsp\n" + " movdqu %xmm0, 0(%rsp)\n" + " movdqu %xmm1, 16(%rsp)\n" + " movdqu %xmm2, 32(%rsp)\n" + " movdqu %xmm3, 48(%rsp)\n" + " movdqu %xmm4, 64(%rsp)\n" + " movdqu %xmm5, 80(%rsp)\n" + " movdqu %xmm6, 96(%rsp)\n" + " movdqu %xmm7, 112(%rsp)\n" + " mov %r11, %rdi\n" + " mov 200(%rsp), %rsi\n" + " call CallTraceRecord_nid_no_patch\n" + " movdqu 0(%rsp), %xmm0\n" + " movdqu 16(%rsp), %xmm1\n" + " movdqu 32(%rsp), %xmm2\n" + " movdqu 48(%rsp), %xmm3\n" + " movdqu 64(%rsp), %xmm4\n" + " movdqu 80(%rsp), %xmm5\n" + " movdqu 96(%rsp), %xmm6\n" + " movdqu 112(%rsp), %xmm7\n" + " add $128, %rsp\n" + " pop %r11\n" + " pop %r10\n" + " pop %rax\n" + " pop %r9\n" + " pop %r8\n" + " pop %rcx\n" + " pop %rdx\n" + " pop %rsi\n" + " pop %rdi\n" + " jmp *(%r11)\n" + APS5_ASM_FUNCTION_END("CallTraceEnter_nid_no_patch")); +#endif diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 67847b576f..42e4274d9e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -166,7 +166,7 @@ void _readFile(const Apr::ReadFileCommand& command) { const GuestArena::HostWrite destination(reinterpret_cast(command.destination), command.size); if (!destination.Open()) throw std::runtime_error("APR: the read destination of " + file.path.string() + " is not writable guest memory"); #ifdef _WIN32 - std::array staging; + std::vector staging(static_cast(std::min(command.size, 64u * 1024u))); std::uint64_t read = 0; while (read < command.size) { const auto wanted = static_cast(std::min(command.size - read, staging.size())); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a2330a807a..cc7a774e23 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -74,6 +74,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/SleepWake.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp @@ -109,7 +110,7 @@ if(BUILD_TESTING) configure_windows_unwind(guest_apr_read_tests) set(aprReadCases host direct eof errors streams) if(WIN32) - list(APPEND aprReadCases fault) + list(APPEND aprReadCases fault stack) endif() foreach(aprReadCase IN LISTS aprReadCases) set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) @@ -736,6 +737,18 @@ if(BUILD_TESTING) set_tests_properties(guest_nanosleep_timer_self_test PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_TRACE_TIMERS=1" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sleep_wake_tests ${LIBS_INCLUDE_DIR}/tests/GuestSleepWake.cpp) + target_include_directories(guest_sleep_wake_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sleep_wake_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sleep_wake_tests) + add_test(NAME guest_sleep_wake COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake PROPERTIES TIMEOUT 20) + if(WIN32) + add_test(NAME guest_sleep_wake_coarse COMMAND guest_sleep_wake_tests) + set_tests_properties(guest_sleep_wake_coarse PROPERTIES TIMEOUT 20 ENVIRONMENT "APS5_COARSE_TIMED_WAITS=1") + set_tests_properties(guest_sleep_wake guest_sleep_wake_coarse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadSem.cpp) target_include_directories(guest_pthread_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_sem_tests PRIVATE libkernel libc) @@ -1649,12 +1662,17 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - if(NOT WIN32) - add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) - target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) - add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) - else() + add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) + target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) + add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + if(WIN32) + configure_windows_unwind(guest_directory_entry_tests) + set_tests_properties(guest_directory_entries PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + if(MINGW) + target_compile_options(guest_directory_entry_tests PRIVATE -fasynchronous-unwind-tables) + endif() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_close_tests PRIVATE libkernel libc) @@ -1786,6 +1804,13 @@ if(BUILD_TESTING) add_test(NAME guest_mutex_raise_exception COMMAND guest_mutex_raise_exception_tests) set_tests_properties(guest_mutex_raise_exception PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + add_test_executable(guest_raise_in_system_call_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInSystemCall.cpp) + target_include_directories(guest_raise_in_system_call_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_in_system_call_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_raise_in_system_call_tests) + add_test(NAME guest_raise_in_system_call COMMAND guest_raise_in_system_call_tests) + set_tests_properties(guest_raise_in_system_call PROPERTIES TIMEOUT 30 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") add_test_executable(guest_raise_in_host_code_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseInHostCode.cpp) target_include_directories(guest_raise_in_host_code_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_raise_in_host_code_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d5b7996ad..2addb21a56 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -662,7 +662,8 @@ void RequireAliasesUnpinned(GuestAllocations::Mutation& mutation, std::uint64_t std::vector> aliases; { std::lock_guard lock(g_directLock); - for (const auto& [base, mapping] : g_directMappings) { + for (const auto base : MappingsSharingPhys(phys, phys + len)) { + const auto& mapping = g_directMappings.at(base); const auto first = std::max(phys, mapping.phys); const auto last = std::min(phys + len, mapping.phys + (mapping.end - base)); if (first < last) aliases.emplace_back(base + (first - mapping.phys), last - first); diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index b6bbf66d7e..04fe98539f 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -368,7 +368,9 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { throw std::runtime_error("EVFILT_VNODE: reading the watched file failed, error=" + std::to_string(error)); } try { - host.parent = WatchParent(host.last.path); + if (!IsDeleted(host.last)) { + host.parent = WatchParent(host.last.path); + } if (host.directory) { host.entries = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_FILE_NAME, "entries"); host.subdirectories = WatchContents(host.last.path, FILE_NOTIFY_CHANGE_DIR_NAME, "subdirectories"); diff --git a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp index 8fed47f8ff..0e7f1e05e0 100644 --- a/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp +++ b/core/libs/prx/libkernel/Equeue/src/Kqueue.cpp @@ -134,9 +134,6 @@ std::int64_t DescriptorLowWater(const KernelEvent& change) { if (change.data < 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT below 1 on a socket"); } - if (change.filter == EVFILT_WRITE && change.data > 1) { - NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on EVFILT_WRITE"); - } if (change.data > 1 && GuestSockets::Type(static_cast(change.ident)) != 1) { NotImplemented_nid_no_patch("kevent: NOTE_LOWAT above 1 on a socket that is not a stream"); } diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index c2d2dab315..a6403ce990 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -18,6 +18,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; +static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; @@ -146,6 +147,18 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui return SCE_OK; } +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { + if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + std::shared_lock lock(eventFlagRegistryMutex); + for (const auto& [handle, flag] : eventFlagRegistry) { + std::lock_guard flagLock(flag->_mutex); + if (flag->_deleted || flag->_name != name) continue; + *ef = handle; + return SCE_OK; + } + return SCE_KERNEL_ERROR_ENOENT; +} + int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { if (!ef) return SCE_KERNEL_ERROR_ESRCH; const auto keepAlive = ResolveEventFlag(ef); diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 0cf519fb69..6978a7998a 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -20,7 +20,7 @@ std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); void DuplicateDirectoryDescriptor(int from, int to); std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base = nullptr); } diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index cca49c2431..7727bb9000 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -153,12 +153,13 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, return target; } -int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { +int ReadDirectoryDescriptor(int fd, char* buf, int nbytes, std::int64_t* base) { const auto found = Find(fd); if (!found) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = *found; std::lock_guard lock(state.mutex); Load(state, fd); + if (base != nullptr) *base = static_cast(state.cursor); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 3edfb3774b..75e7d82619 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -30,10 +30,6 @@ static int ReadHandleInfo(HANDLE handle, NativeStat* st) { st->hasIdentity = GetFileInformationByHandle(handle, &st->identity) != 0; return 0; } -static std::uint32_t FoldFileIndex(const BY_HANDLE_FILE_INFORMATION& identity) { - const std::uint32_t folded = identity.nFileIndexLow ^ (identity.nFileIndexHigh * 0x9E3779B1u); - return folded == 0 ? 1 : folded; -} static int DoStat(const std::filesystem::path& p, NativeStat* st) { if (_wstat64(p.c_str(), st) != 0) { if (_doserrno == ERROR_CANT_RESOLVE_FILENAME) File::WindowsFileTime::Failure(_doserrno); @@ -115,7 +111,7 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { } if (st.hasIdentity) { sb->st_dev = st.identity.dwVolumeSerialNumber; - sb->st_ino = FoldFileIndex(st.identity); + sb->st_ino = WindowsFileNumber_nid_no_patch(st.identity); sb->st_nlink = static_cast(std::min(st.identity.nNumberOfLinks, 0xffff)); } #else diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index c1fc6744d2..186ce63ff3 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -174,7 +174,6 @@ static int MapFlags(int sceFlags) { extern "C" int* APS5_VABI __error_nid_postfix(); extern "C" int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...); -static constexpr int GuestSetFlags = 4; static int SceErrorFromErrno(int error) { const int guest = error > 0 && error <= 34 ? error : FilesystemError(std::error_code(error, std::generic_category())); @@ -446,7 +445,8 @@ int APS5_VABI sceKernelFcntl(int d, int command, ...) { } int result; if (GuestSockets::IsOpen(d)) { - if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + if (type == File::FcntlArgument::Int) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else if (type == File::FcntlArgument::Pointer) result = fcntl_nid_postfix(d, command, reinterpret_cast(argument)); else result = fcntl_nid_postfix(d, command); } else { result = File::Fcntl(d, command, argument); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5e7f29b807..2400df7664 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -163,6 +163,8 @@ static int SetTimes(HANDLE handle, const KernelTimeval* times) { GetSystemTimePreciseAsFileTime(&access); modified = access; } else if (!File::WindowsFileTime::Encode(times[0], access) || !File::WindowsFileTime::Encode(times[1], modified)) { + const FILETIME unchanged{0xFFFFFFFF, 0xFFFFFFFF}; + SetFileTime(handle, nullptr, &unchanged, &unchanged); errno = EINVAL; return -1; } @@ -907,6 +909,7 @@ int APS5_VABI sceKernelFtruncate(int d, int64_t length) { } int64_t APS5_VABI lseek_nid_postfix(int d, int64_t offset, int whence) { + if (!GuestSockets::IsOpen(d) && !DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); if (whence < 0 || whence > 2) return PosixFailure(GUEST_EINVAL); return PosixResult(sceKernelLseek(d, offset, whence)); } @@ -1099,8 +1102,7 @@ int APS5_VABI sceKernelGetdirentries(int fd, char* buf, int nbytes, int64_t* bas const GuestArena::HostWrite destination(buf, static_cast(nbytes)); const GuestArena::HostWrite baseDestination(basep, basep == nullptr ? 0 : sizeof(*basep)); if (!destination.Open() || !baseDestination.Open()) return SceErrorFromErrno(GUEST_EFAULT); - if (basep != nullptr) *basep = 0; - return File::ReadDirectoryDescriptor(fd, buf, nbytes); + return File::ReadDirectoryDescriptor(fd, buf, nbytes, basep); } int APS5_VABI sceKernelGetdents(int fd, char* buf, int nbytes) { diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 6e06be8deb..3076dd6358 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -306,6 +306,17 @@ extern "C" { int APS5_VABI __elf_phdr_match_addr_nid_postfix(dl_phdr_info* phdrInfo, void* addr) { if (phdrInfo == nullptr) throw std::invalid_argument("__elf_phdr_match_addr: phdr_info is null"); const auto address = reinterpret_cast(addr); + + const auto* module = reinterpret_cast(phdrInfo); + if (module->st_size == sizeof(ModuleInfoEx)) { + for (std::uint32_t i = 0; i < module->segment_count && i < std::size(module->segments); ++i) { + const auto& segment = module->segments[i]; + if ((segment.prot & 4) == 0 || segment.size < sizeof(addr)) continue; + if (segment.address <= address && address - segment.address <= segment.size - sizeof(addr)) return 1; + } + return 0; + } + for (std::uint16_t i = 0; i < phdrInfo->dlpi_phnum; ++i) { const Elf64_Phdr& header = phdrInfo->dlpi_phdr[i]; if (header.p_type != PT_LOAD || (header.p_flags & PF_X) == 0) continue; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index 1ea13b5de0..86fd01c134 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -38,6 +38,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) p->stackAddress = thread->stackAddress; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); + p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); p->_affinity = thread->affinity.load(std::memory_order_relaxed); p->_affinityExplicit = true; p->_scope = thread->scope; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index 4cd5a94a3f..0a5444c390 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -225,6 +225,17 @@ int APS5_VABI pthread_join_nid_postfix(Pthread thread, void** value) { return PosixThread::ToErrno(scePthreadJoin(thread, value)); } +namespace { +struct RegisterPthreadOnLoad { + RegisterPthreadOnLoad() { + RegisterPthreadCreate_nid_no_patch(reinterpret_cast(pthread_create_nid_postfix)); + RegisterPthreadJoin_nid_no_patch(reinterpret_cast(pthread_join_nid_postfix)); + RegisterPthreadDetach_nid_no_patch(reinterpret_cast(pthread_detach_nid_postfix)); + } +}; +const RegisterPthreadOnLoad gRegisterPthread; +} + int APS5_VABI pthread_rename_np_nid_postfix(Pthread thread, const char* name) { if (thread && name && std::strlen(name) >= 32) return PosixThread::ToErrno(SCE_KERNEL_ERROR_ENAMETOOLONG); return PosixThread::ToErrno(scePthreadRename(thread, name)); diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index ea0eccba68..ce6ccac93c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -83,7 +83,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { (*attr)->stackAddress = thread->stackAddress; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); - (*attr)->_schedpolicy = SCHED_FIFO_PS5; + (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); (*attr)->_inheritsched = 4; (*attr)->_affinity = thread->affinity.load(std::memory_order_relaxed); (*attr)->_affinityExplicit = true; diff --git a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp index 9bf3b90265..97e68db173 100644 --- a/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp +++ b/core/libs/prx/libkernel/Socket/include/SocketPoll.hpp @@ -20,6 +20,10 @@ 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); +struct NativeOwner { + int (*transfer)(NativeOwner* owner, bool send, void* buffer, int length) = nullptr; +}; + using NativeLookup = bool (*)(int descriptor, std::shared_ptr* owner, std::uintptr_t* native); } diff --git a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp index 54a7114657..bbcb17e637 100644 --- a/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp +++ b/core/libs/prx/libkernel/Socket/src/AddressConversion.cpp @@ -219,4 +219,20 @@ const char* APS5_VABI __inet_ntop_nid_postfix(int family, const void* source, if (family == kGuestInet6) std::memset(destination + length + 1, 0, capacity - length - 1); return destination; } + +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t value) { + return __builtin_bswap32(value); +} + +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} + +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t value) { + return __builtin_bswap16(value); +} } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 9860dd60f7..f9a28df4fd 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -58,6 +58,7 @@ #include extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); namespace { #ifdef _WIN32 using NativeSocket = SOCKET; @@ -67,6 +68,7 @@ using NativeSocket = int; constexpr auto Invalid = -1; #endif int Fail(int error) { *__error_nid_postfix() = error; return -1; } +constexpr int GuestSocketDescriptorLimit = 1024; constexpr int GuestPeek = 0x2; constexpr int GuestDontWait = 0x80; constexpr int GuestNoSignal = 0x20000; @@ -108,7 +110,7 @@ int SendError(int type) { #endif return NativeError(); } -struct Socket { +struct Socket : KernelSocketPoll::NativeOwner { NativeSocket value; int family; int type; @@ -180,13 +182,35 @@ int ReserveNumber(NativeSocket) { ::CloseHandle(placeholder); return Fail(24); } + if (number >= GuestSocketDescriptorLimit) { + ::_close(number); + return Fail(24); + } return number; } -int CopyNumber(int number) { - const int copy = ::_dup(number); - return copy < 0 ? Fail(errno == EMFILE ? 24 : 9) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + std::vector below; + int copy = -1; + int error = 0; + try { + while ((copy = ::_dup(number)) >= 0 && copy < minimum) below.push_back(copy); + if (copy < 0) error = errno == EMFILE ? 24 : 9; + } catch (const std::bad_alloc&) { + if (copy >= 0) ::_close(copy); + copy = -1; + error = 12; + } + for (const int temporary : below) ::_close(temporary); + if (copy < 0) return Fail(error); + if (copy >= GuestSocketDescriptorLimit) { + ::_close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::_dup2(number, target) != 0 ? Fail(errno == EMFILE ? 24 : 9) : 0; } void ReleaseNumber(int number) { @@ -195,13 +219,25 @@ void ReleaseNumber(int number) { #else int ReserveNumber(NativeSocket native) { const int number = ::fcntl(native, F_DUPFD_CLOEXEC, 0); - return number < 0 ? Fail(NativeError()) : number; + if (number < 0) return Fail(NativeError()); + if (number >= GuestSocketDescriptorLimit) { + ::close(number); + return Fail(24); + } + return number; } -int CopyNumber(int number) { - const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, 0); - return copy < 0 ? Fail(NativeError()) : copy; +int CopyNumber(int number, int minimum = 0) { + if (minimum >= GuestSocketDescriptorLimit) return Fail(24); + const int copy = ::fcntl(number, F_DUPFD_CLOEXEC, minimum); + if (copy < 0) return Fail(NativeError()); + if (copy >= GuestSocketDescriptorLimit) { + ::close(copy); + return Fail(24); + } + return copy; } int CopyNumberTo(int number, int target) { + if (target >= GuestSocketDescriptorLimit) return Fail(24); return ::dup2(number, target) < 0 ? Fail(NativeError()) : 0; } void ReleaseNumber(int number) { @@ -562,21 +598,30 @@ bool GuestSockets::PipeReady(int descriptor, short events, short& revents) { } #endif -int GuestSockets::Duplicate(int descriptor) { +namespace { +int DuplicateSocket_nid_no_patch(int descriptor, int minimum, bool closeOnExec) { + if (minimum < 0) return Fail(22); std::lock_guard lock(socketsMutex); const auto found = sockets.find(descriptor); if (found == sockets.end()) return Fail(9); - const int duplicate = CopyNumber(descriptor); + const int duplicate = CopyNumber(descriptor, minimum); if (duplicate < 0) return -1; try { sockets.insert_or_assign(duplicate, found->second); - closeOnExecDescriptors.erase(duplicate); + if (closeOnExec) closeOnExecDescriptors.insert(duplicate); + else closeOnExecDescriptors.erase(duplicate); } catch (const std::bad_alloc&) { + sockets.erase(duplicate); ReleaseNumber(duplicate); return Fail(12); } return duplicate; } +} + +int GuestSockets::Duplicate(int descriptor) { + return DuplicateSocket_nid_no_patch(descriptor, 0, false); +} int GuestSockets::DuplicateTo(int descriptor, int target) { std::shared_ptr replaced; @@ -918,7 +963,7 @@ int GuestSockets::Poll(KernelSocketPoll::Entry* entries, int count, int timeoutM extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::shared_ptr* owner, std::uintptr_t* native) { if (auto socket = Find(descriptor)) { *native = static_cast(socket->value); - *owner = std::move(socket); + *owner = std::static_pointer_cast(std::move(socket)); return true; } return KernelFindSocketNative_nid_no_patch(descriptor, owner, native); @@ -926,24 +971,24 @@ extern "C" bool KernelLookupSocketNative_nid_no_patch(int descriptor, std::share extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { - if (!GuestSockets::IsOpen(descriptor)) { - const auto type = File::FcntlArgumentOf(command); - std::intptr_t argument = 0; - if (type != File::FcntlArgument::None) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) - : reinterpret_cast(__builtin_va_arg(arguments, void*)); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) - : reinterpret_cast(va_arg(arguments, void*)); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif - } + } + if (!GuestSockets::IsOpen(descriptor)) { int result = 0; int netError = 0; if (KernelControlSocketStatus_nid_no_patch(descriptor, command, static_cast(argument), &result, &netError)) { @@ -951,6 +996,12 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } return File::Fcntl(descriptor, command, argument); } + if (command == 0 || command == 17) return DuplicateSocket_nid_no_patch(descriptor, static_cast(argument), command == 17); + if (command == 10 || command == 18) { + const int duplicate = dup2_nid_postfix(descriptor, static_cast(argument)); + if (duplicate >= 0 && command == 18) fcntl_nid_postfix(duplicate, 2, 1); + return duplicate; + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); @@ -959,17 +1010,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { return closeOnExecDescriptors.count(descriptor) != 0 ? 1 : 0; } if (command == 2) { -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); std::lock_guard registry(socketsMutex); if ((flags & 1) != 0) closeOnExecDescriptors.insert(descriptor); else closeOnExecDescriptors.erase(descriptor); @@ -977,17 +1018,7 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { } if (command == 3) return 2 | (socket->options.nonblocking ? 4 : 0); // F_GETFL, O_RDWR if (command != 4) return Fail(22); -#ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); -#else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); -#endif + const int flags = static_cast(argument); if ((flags & ~7) != 0) return Fail(45); #ifdef _WIN32 unsigned long enabled = (flags & 4) != 0; diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 710d0bc069..3955668c7f 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -27,6 +27,8 @@ constexpr int UMTX_OP_WAKE = 3; constexpr int UMTX_OP_WAIT_UINT = 11; constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_SEM_WAIT = 19; +constexpr int UMTX_OP_SEM_WAKE = 20; constexpr int UMTX_OP_NWAKE_PRIVATE = 21; constexpr int UMTX_OP_MAX = 27; constexpr std::uint32_t UMTX_ABSTIME = 1; @@ -46,6 +48,12 @@ struct UmtxTime { std::uint32_t clockId; }; +struct UserSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + struct AddressWaiter { TimedWait::Condition condition; bool woken = false; @@ -72,11 +80,10 @@ void DeferringDelivery(TFunction function) { #endif } -template -std::optional WaitOnAddressLocked(TValue* address, TValue expected, std::optional deadlineNanos, bool& woken) { - const auto key = reinterpret_cast(address); +template +std::optional WaitOnKeyLocked(std::uintptr_t key, TReady ready, std::optional deadlineNanos, bool& woken) { std::unique_lock lock(g_waitersLock); - if (std::atomic_ref(*address).load() != expected) { + if (ready()) { woken = true; return std::nullopt; } @@ -103,17 +110,22 @@ std::optional WaitOnAddressLocked(TValue* address, TVa return waited; } -template -bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { +template +bool WaitOnKeyUntil(std::uintptr_t key, TReady ready, std::optional deadlineNanos, const void* caller) { bool woken = false; std::optional waited; - DeferringDelivery([&] { waited = WaitOnAddressLocked(address, expected, deadlineNanos, woken); }); + DeferringDelivery([&] { waited = WaitOnKeyLocked(key, ready, deadlineNanos, woken); }); if (waited) { KernelTraceWait_nid_postfix("addr", caller, static_cast(waited->count()), !woken); } return woken; } +template +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { + return WaitOnKeyUntil(reinterpret_cast(address), [&] { return std::atomic_ref(*address).load() != expected; }, deadlineNanos, caller); +} + template int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); @@ -228,6 +240,56 @@ int UmtxNwakePrivate(void* object, std::uint64_t value) { return 0; } +void WakeSemaphoreLocked(std::uintptr_t key, UserSemaphore* semaphore) { + std::lock_guard lock(g_waitersLock); + const auto entry = g_waiters.find(key); + if (entry == g_waiters.end()) { + return; + } + auto& queue = entry->second; + if (queue.size() == 1) { + std::atomic_ref(semaphore->hasWaiters).store(0); + } + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + if (queue.empty()) { + g_waiters.erase(entry); + } +} + +int SemWait(UserSemaphore* semaphore, const void* sizeArgument, const void* timeArgument, const void* caller) { + std::optional deadline; + if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) { + return error; + } + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const bool woken = WaitOnKeyUntil(reinterpret_cast(semaphore), [&] { + std::uint32_t idle = 0; + std::atomic_ref(semaphore->hasWaiters).compare_exchange_strong(idle, 1); + return std::atomic_ref(semaphore->count).load() != 0; + }, deadline, caller); + return woken ? 0 : GUEST_ETIMEDOUT; +} + +int SemWake(UserSemaphore* semaphore) { + if (semaphore == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(semaphore))) { + APS5_INVALID_ARG_EX; + } + const auto key = reinterpret_cast(semaphore); + DeferringDelivery([&] { WakeSemaphoreLocked(key, semaphore); }); + return 0; +} + } // namespace extern "C" { @@ -300,6 +362,12 @@ int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t va case UMTX_OP_NWAKE_PRIVATE: error = UmtxNwakePrivate(object, value); break; + case UMTX_OP_SEM_WAIT: + error = SemWait(static_cast(object), sizeArgument, timeArgument, caller); + break; + case UMTX_OP_SEM_WAKE: + error = SemWake(static_cast(object)); + break; default: if (operation < 0 || operation >= UMTX_OP_MAX) { error = GUEST_EINVAL; diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 0f8f66a520..b7eb4f65f7 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -192,6 +192,7 @@ void CallHandler(GuestExceptionHandler handler, int signum, GuestUcontext& ucont constexpr std::size_t RedZone = 128; constexpr std::size_t HomeArea = 32; constexpr auto RetryLimit = std::chrono::seconds(1); +constexpr auto HostCodeLimit = std::chrono::milliseconds(50); constexpr std::size_t VectorBytes = 16 * 32; struct Delivery { @@ -376,6 +377,17 @@ bool BlockedInWait(DWORD64 rip) { return std::any_of(waits.begin(), waits.end(), [rip](DWORD64 stub) { return rip - stub < 0x20; }); } +DWORD64 NtdllBase() { + static const auto ntdll = reinterpret_cast(GetModuleHandleW(L"ntdll.dll")); + return ntdll; +} + +bool InSystemCall(DWORD64 rip, DWORD64 base) { + if (base == 0 || base != NtdllBase() || rip < base + 2) return false; + const auto* code = reinterpret_cast(rip - 2); + return code[0] == 0x0f && code[1] == 0x05; +} + CodeKind RunningCode(DWORD64 rip, DWORD64& base) { base = ImageBase(rip); auto kind = CachedKind(base); @@ -393,12 +405,15 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const auto native = static_cast(thread->nativeHandle); auto queued = std::make_unique(Delivery{handler, signum, {}, {}, 0}); alignas(16) Delivery delivery{handler, signum, {}, {}, SaveVectors ? 1u : 0u}; - const auto retryDeadline = std::chrono::steady_clock::now() + RetryLimit; + const auto start = std::chrono::steady_clock::now(); + const auto retryDeadline = start + RetryLimit; + auto hostDeadline = start + HostCodeLimit; static_cast(RestoringContext(0)); static_cast(InWinpthread(0)); static_cast(BlockedInWait(0)); static_cast(ExecutableBase()); static_cast(Images()); + static_cast(NtdllBase()); for (;;) { if (SuspendThread(native) == static_cast(-1)) { if (Exited(native)) return false; @@ -423,7 +438,9 @@ bool RaiseOn(Pthread thread, GuestExceptionHandler handler, int signum) { const bool restoring = RestoringContext(delivery.context.Rip) || InWinpthread(delivery.context.Rip); DWORD64 image = 0; const auto running = restoring ? CodeKind::Guest : RunningCode(delivery.context.Rip, image); - if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && std::chrono::steady_clock::now() >= retryDeadline))) break; + const auto now = std::chrono::steady_clock::now(); + if (running == CodeKind::Host && InSystemCall(delivery.context.Rip, image) && now < retryDeadline) hostDeadline = std::max(hostDeadline, std::min(retryDeadline, now + HostCodeLimit)); + if (!restoring && (running == CodeKind::Guest || (running == CodeKind::Host && now >= hostDeadline))) break; ResumeThread(native); if (running == CodeKind::Unknown) { static_cast(ClassifyImage(image)); diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index d9edcad564..348464a177 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -453,6 +453,16 @@ int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid) { return 0; } +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid) { + if (!uuid) return sceInvalidArgument; + std::array value; + HostEntropy_nid_no_patch(value.data(), value.size()); + value[6] = static_cast((value[6] & 0x0fu) | 0x40u); + value[8] = static_cast((value[8] & 0x3fu) | 0x80u); + std::memcpy(uuid, value.data(), value.size()); + return 0; +} + void APS5_VABI sceKernelSync(void) { SyncWrittenPaths_nid_no_patch(); } diff --git a/core/libs/prx/libkernel/System/src/Signals.cpp b/core/libs/prx/libkernel/System/src/Signals.cpp index c9211181d4..17a84ec147 100644 --- a/core/libs/prx/libkernel/System/src/Signals.cpp +++ b/core/libs/prx/libkernel/System/src/Signals.cpp @@ -38,6 +38,7 @@ constexpr int MaxSignal = 128; constexpr int GuestSigkill = 9; constexpr int GuestSigstop = 17; constexpr int GuestSigalrm = 14; +constexpr int GuestSigcont = 19; constexpr int SaRestart = 0x2; constexpr int SaSiginfo = 0x40; std::atomic handlers[MaxSignal + 1]{}; @@ -75,6 +76,9 @@ struct GuestTimespec { std::int64_t nanoseconds; }; static_assert(sizeof(GuestTimespec) == 16 && offsetof(GuestTimespec, nanoseconds) == 8); +bool IgnoredByDefault(int guest) { + return guest == 16 || guest == GuestSigcont || guest == 20 || guest == 23 || guest == 28 || guest == 29; +} std::uint32_t SignalBit(int guest) { return 1u << ((guest - 1) % 32); } std::uint32_t SignalWord(int guest) { return static_cast((guest - 1) / 32); } bool SignalInSet(const GuestSignalSet& set, int guest) { return (set.bits[SignalWord(guest)] & SignalBit(guest)) != 0; } @@ -496,13 +500,16 @@ int APS5_VABI sigaction_nid_postfix(int guest, const GuestSigaction* action, Gue return 0; } int APS5_VABI raise_nid_postfix(int guest) { + if (guest == 0) return 0; if (guest < 1 || guest > MaxSignal) { *__error_nid_postfix() = 22; return -1; } const GuestSigaction action = Disposition(guest); if (action.handler > 1 && !Delivered(action)) throw std::runtime_error("raise: SA_SIGINFO handlers are not delivered"); + if (action.handler == 1 || (action.handler == 0 && guest != GuestSigcont && IgnoredByDefault(guest))) return 0; if (SignalBlocked(guest)) { - if (action.handler != 1) MarkPending(guest); + MarkPending(guest); return 0; } + if (action.handler == 0 && IgnoredByDefault(guest)) return 0; const int native = NativeSignal(guest); if (native != 0) { const int result = std::raise(native); diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index 8c73a85624..97371195e7 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -136,6 +136,30 @@ std::vector GuestVmEntries() { return entries; } +bool IsUnansweredMib(const int* name, std::uint32_t nameLength) { + if (nameLength != 2) return false; + return (name[0] == 1 && (name[1] == 6 || name[1] == 7 || name[1] == 12 || name[1] == 27 || name[1] == 32)) || (name[0] == 6 && (name[1] == 4 || name[1] == 11)); +} + +bool IsUnansweredName(const char* name) { + static constexpr const char* unanswered[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + }; + return std::any_of(std::begin(unanswered), std::end(unanswered), [name](const char* entry) { return std::strcmp(name, entry) == 0; }); +} + } extern "C" { @@ -207,6 +231,11 @@ int APS5_VABI sysctl_nid_postfix(const int* name, std::uint32_t nameLength, void *__error_nid_postfix() = 2; return -1; } + if (IsUnansweredMib(name, nameLength)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing MIB " + std::to_string(name[0]) + "." + std::to_string(name[1])); + *__error_nid_postfix() = 2; + return -1; + } if (nameLength == 2 && name[0] == 6 && name[1] == 7) { if (newValue != nullptr) { *__error_nid_postfix() = 1; @@ -331,6 +360,11 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si for (const auto& entry : SystemStrings) { if (std::strcmp(name, entry.name) == 0) return sysctl_nid_postfix(entry.mib, 2, oldValue, oldLength, newValue, newLength); } + if (IsUnansweredName(name)) { + if (newValue != nullptr) throw std::runtime_error(std::string(__func__) + ": writing " + name); + *__error_nid_postfix() = 2; + return -1; + } if (std::strcmp(name, "kern.proc.pathname") == 0) { static constexpr int mib[] = {1, 14, 12}; return sysctl_nid_postfix(mib, 3, oldValue, oldLength, newValue, newLength); @@ -411,14 +445,21 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ std::size_t length; }; static constexpr KnownName known[] = { + {"hw.byteorder", {6, 4}, 2}, + {"hw.machine_arch", {6, 11}, 2}, {"hw.model", {6, 2}, 2}, {"hw.ncpu", {6, 3}, 2}, {"hw.pagesize", {6, 7}, 2}, {"hw.physmem", {6, 5}, 2}, {"hw.realmem", {6, 12}, 2}, {"kern.arandom", {1, 37}, 2}, - {"kern.rng_pseudo", {1, 50}, 2}, + {"kern.clockrate", {1, 12}, 2}, + {"kern.maxfiles", {1, 7}, 2}, + {"kern.maxfilesperproc", {1, 27}, 2}, + {"kern.maxproc", {1, 6}, 2}, {"kern.proc.pathname", {1, 14, 12}, 3}, + {"kern.ps_strings", {1, 32}, 2}, + {"kern.rng_pseudo", {1, 50}, 2}, }; constexpr std::size_t MaximumPath = 1024; if (name == nullptr || size == nullptr) throw std::invalid_argument(std::string(__func__) + ": null name or size"); @@ -427,7 +468,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ *__error_nid_postfix() = 63; return -1; } - if (nameLength == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { + if (nameLength == 0 || std::strcmp(name, "hw.cacheline_size") == 0 || std::strcmp(name, "hw.usermem") == 0 || std::strcmp(name, "machdep.cacheline_size") == 0) { *__error_nid_postfix() = 2; return -1; } diff --git a/core/libs/prx/libkernel/Time/SleepWake.cpp b/core/libs/prx/libkernel/Time/SleepWake.cpp new file mode 100644 index 0000000000..a6a774e8cb --- /dev/null +++ b/core/libs/prx/libkernel/Time/SleepWake.cpp @@ -0,0 +1,57 @@ +#include "prx/libkernel/Time/include/SleepWake.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include + +namespace SleepWake { + +namespace { + +struct ThreadState { + unsigned earlyWakes = 0; + std::uint64_t lastSleepEndNanos = 0; + bool sleeping = false; +}; + +thread_local ThreadState state; + +std::atomic completions{0}; + +bool Enabled() { + static const bool enabled = std::getenv("APS5_NO_SLEEP_WAKE") == nullptr; + return enabled; +} + +} + +void Sleep(std::uint64_t nanos) { + bool woken = false; + if (nanos != 0 && nanos <= MAX_WAKEABLE_NANOS && !state.sleeping && Enabled()) { + if (TimedWait::NowNanos() - state.lastSleepEndNanos >= nanos) state.earlyWakes = 0; + if (state.earlyWakes < EARLY_WAKES_PER_RUN) { + state.sleeping = true; + const bool early = TimedWait::SleepNanosUnlessChanged(nanos, completions, completions.load(std::memory_order_seq_cst)); + state.sleeping = false; + if (early) ++state.earlyWakes; + woken = true; + } + } + if (!woken) TimedWait::SleepNanos(nanos); + state.lastSleepEndNanos = TimedWait::NowNanos(); +} + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch() { + SleepWake::completions.fetch_add(1, std::memory_order_seq_cst); + TimedWait::WakeChangeSleepers(); +} + +void KernelNoteGpuSubmit_nid_no_patch() { + SleepWake::state.earlyWakes = 0; +} + +} diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 4c447a1a45..afd6d5d40e 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include "prx/libkernel/Time/include/SleepWake.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" #include "prx/libc/include/General.hpp" @@ -274,7 +275,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint static void CancellableSleep(std::uint64_t nanos) { ThreadCancel::Check(); - TimedWait::SleepNanos(nanos); + SleepWake::Sleep(nanos); ThreadCancel::Check(); } diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp index c4ae764f64..7c171db399 100644 --- a/core/libs/prx/libkernel/Time/TimedWait.cpp +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -1,9 +1,11 @@ #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Pthread/include/WindowsThreadLocal.hpp" +#include #include #include #include +#include #ifdef _WIN32 #include @@ -19,6 +21,28 @@ namespace { constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; constexpr std::uint64_t LEAD_NANOS = 500000ULL; +std::mutex& ChangeSleepersMutex() { + static std::mutex mutex; + return mutex; +} + +std::atomic& ChangeSleeperCount() { + static std::atomic count{0}; + return count; +} + +#ifdef _WIN32 +std::vector& ChangeSleepers() { + static std::vector sleepers; + return sleepers; +} +#else +std::condition_variable& ChangeCondition() { + static std::condition_variable condition; + return condition; +} +#endif + struct ThreadWaitState { std::atomic own{0}; std::atomic* bound = &own; @@ -114,12 +138,14 @@ struct Waiter { Waiter* next = nullptr; Waiter* nested = nullptr; const void* key = nullptr; + HANDLE wake = nullptr; bool queued = false; bool busy = false; ~Waiter() { delete nested; if (event) CloseHandle(event); + if (wake) CloseHandle(wake); if (timer) CloseHandle(timer); for (const auto sleepTimer : sleepTimers) if (sleepTimer) CloseHandle(sleepTimer); } @@ -426,6 +452,72 @@ void SleepUntil(std::uint64_t deadlineNanos) { #endif } +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed) { + const std::uint64_t deadlineNanos = NowNanos() + nanos; +#ifdef _WIN32 + if (Coarse() || nanos < YIELD_BELOW_NANOS) { + SleepNanos(nanos); + return false; + } + Waiter* waiter = ThisThreadWaiter(); + if (!waiter->wake) waiter->wake = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!waiter->wake) throw std::runtime_error("TimedWait: CreateEventW failed for a wakeable sleep"); + { + std::lock_guard lock(ChangeSleepersMutex()); + ChangeSleepers().push_back(waiter->wake); + } + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + struct Unregister { + HANDLE wake; + ~Unregister() { + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + std::lock_guard lock(ChangeSleepersMutex()); + auto& sleepers = ChangeSleepers(); + sleepers.erase(std::find(sleepers.begin(), sleepers.end(), wake)); + } + } unregister{waiter->wake}; + ResetEvent(waiter->wake); + std::uint64_t now = NowNanos(); + if (counter.load(std::memory_order_seq_cst) != observed) return true; + if (deadlineNanos > now + LEAD_NANOS) { + const HANDLE handles[2] = {ThreadTimer(1), waiter->wake}; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast((deadlineNanos - now - LEAD_NANOS) / 100ULL); + if (due.QuadPart == 0) due.QuadPart = -1; + if (handles[0] && SetWaitableTimer(handles[0], &due, 0, nullptr, nullptr, FALSE)) { + if (AlertableWait(2, handles, INFINITE) == WAIT_OBJECT_0 + 1) CancelWaitableTimer(handles[0]); + } else { + AlertableWait(1, &handles[1], WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + } + if (counter.load(std::memory_order_seq_cst) != observed) return true; + now = NowNanos(); + } + while (now < deadlineNanos) { + if (counter.load(std::memory_order_seq_cst) != observed) return true; + YieldProcessor(); + now = NowNanos(); + } + return false; +#else + std::unique_lock lock(ChangeSleepersMutex()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::nanoseconds(nanos); + ChangeSleeperCount().fetch_add(1, std::memory_order_seq_cst); + const bool changed = ChangeCondition().wait_until(lock, deadline, [&] { return counter.load(std::memory_order_seq_cst) != observed; }); + ChangeSleeperCount().fetch_sub(1, std::memory_order_seq_cst); + return changed; +#endif +} + +void WakeChangeSleepers() { + if (ChangeSleeperCount().load(std::memory_order_seq_cst) == 0) return; + std::lock_guard lock(ChangeSleepersMutex()); +#ifdef _WIN32 + for (const HANDLE sleeper : ChangeSleepers()) SetEvent(sleeper); +#else + ChangeCondition().notify_all(); +#endif +} + void SleepNanos(std::uint64_t nanos) { if (nanos == 0) return; #ifdef _WIN32 diff --git a/core/libs/prx/libkernel/Time/include/SleepWake.hpp b/core/libs/prx/libkernel/Time/include/SleepWake.hpp new file mode 100644 index 0000000000..47185c0cb1 --- /dev/null +++ b/core/libs/prx/libkernel/Time/include/SleepWake.hpp @@ -0,0 +1,22 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP +#define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_SLEEPWAKE_HPP + +#include + +namespace SleepWake { + +constexpr std::uint64_t MAX_WAKEABLE_NANOS = 1000000ULL; +constexpr unsigned EARLY_WAKES_PER_RUN = 8; + +void Sleep(std::uint64_t nanos); + +} + +extern "C" { + +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); + +} + +#endif diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp index 446b0c73f3..2f1725819d 100644 --- a/core/libs/prx/libkernel/Time/include/TimedWait.hpp +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -16,6 +16,8 @@ std::uint64_t DeadlineNanos(std::uint64_t microseconds); std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); void SleepNanos(std::uint64_t nanos); void SleepUntil(std::uint64_t deadlineNanos); +bool SleepNanosUnlessChanged(std::uint64_t nanos, const std::atomic& counter, std::uint64_t observed); +void WakeChangeSleepers(); struct Waiter; diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 090d314539..b0cef12689 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -121,4 +121,10 @@ int APS5_VABI sceKernelUnknown19(void) { NotImplemented_nid_no_patch("uDGXwUKqi8s"); return 0; } + +int APS5_VABI sceKernelPrintBacktraceWithModuleInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index a0adb45364..9c7d6fc2b6 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -139,6 +139,15 @@ std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t return static_cast(count); } +bool HasNonDirectoryPrefix(const std::filesystem::path& native) { + std::error_code error; + for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { + const auto status = std::filesystem::status(prefix, error); + if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); + } + return false; +} + 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()); @@ -153,8 +162,10 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s #endif std::error_code error; const auto status = std::filesystem::symlink_status(native, error); - if (error) return Fail(GuestError(error)); - if (status.type() == std::filesystem::file_type::not_found) return Fail(GuestEnoent); + if (error) + return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); + if (status.type() == std::filesystem::file_type::not_found) + return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); @@ -176,15 +187,6 @@ bool HasLongComponent(std::string_view path) { return false; } -bool HasNonDirectoryPrefix(const std::filesystem::path& native) { - std::error_code error; - for (auto prefix = native.parent_path(); prefix.has_relative_path(); prefix = prefix.parent_path()) { - const auto status = std::filesystem::status(prefix, error); - if (std::filesystem::exists(status)) return !std::filesystem::is_directory(status); - } - return false; -} - bool IsDotOrDotDot(std::string_view path) { while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.remove_suffix(1); const auto separator = path.find_last_of("/\\"); @@ -273,18 +275,18 @@ int APS5_VABI fstatfs_nid_postfix(int d, void* buf) { } std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return Fail(error); return ReadNativeLink(native, buffer, size); } std::int64_t APS5_VABI readlinkat_nid_postfix(int fd, const char* path, char* buffer, std::size_t size) { + if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (path == nullptr) return Fail(GuestEfault); if (*path == '\0') return Fail(GuestEnoent); - if (size > static_cast(std::numeric_limits::max())) return Fail(GuestEinval); if (fd == GuestAtFdcwd || *path == '/') return readlink_nid_postfix(path, buffer, size); int error = 0; const char* base = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 16f537b48f..be99345d50 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -238,6 +238,18 @@ int main(int argc, char** argv) { require(frees == 3 && initializes == 1); return 0; } + if (argc > 1 && std::strcmp(argv[1], "sdk-default") == 0) { + require(argc == 3); + void* sdk = dlopen(argv[2], RTLD_NOW | RTLD_GLOBAL); + require(sdk != nullptr && initializes == 1); + std::memset(replacement.data() + 0x10, 0, replacement.size() - 0x10); + ApplicationHeapInitialize_nid_no_patch(process.data()); + void* pointer = ApplicationHeapAllocate_nid_no_patch(24); + require(pointer != nullptr && pointer != storage.data() && lastSize == 0); + ApplicationHeapFree_nid_no_patch(pointer); + require(frees == 0 && dlclose(sdk) == 0 && finalizes == 1); + return 0; + } #endif if (argc > 1 && std::strcmp(argv[1], "lazy") == 0) { installProcessParameters(); diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 0a4742b900..c6916aea8e 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -206,6 +206,17 @@ int main(int argc, char** argv) { std::array appended{}; Require(stream != nullptr && fread_nid_postfix(appended.data(), 1, 4, stream) == 4 && std::strcmp(appended.data(), "abcd") == 0); Require(fclose_nid_postfix(stream) == 0 && remove_nid_postfix(heldName.c_str()) == 0); + stream = fopen_nid_postfix(heldName.c_str(), "w+b"); + Require(stream != nullptr); + stream = freopen_nid_postfix(movedName.c_str(), "w+b", stream); + Require(stream != nullptr && fwrite_nid_postfix(payload, 1, sizeof(payload), stream) == sizeof(payload)); + Require(fflush_nid_postfix(stream) == 0); + Require(rename_nid_postfix(movedName.c_str(), heldName.c_str()) == 0 && !std::filesystem::exists(movedName)); + Require(remove_nid_postfix(heldName.c_str()) == 0 && !std::filesystem::exists(heldName)); + Require(fseek_nid_postfix(stream, 0, SEEK_SET) == 0); + std::array reopenedBuffer{}; + Require(fread_nid_postfix(reopenedBuffer.data(), 1, reopenedBuffer.size(), stream) == reopenedBuffer.size()); + Require(std::strcmp(reopenedBuffer.data(), payload) == 0 && fclose_nid_postfix(stream) == 0); constexpr char marker[] = "marker"; stream = fopen_nid_postfix(argv[1], "wb"); Require(stream != nullptr); diff --git a/core/libs/tests/GuestAddressConversion.cpp b/core/libs/tests/GuestAddressConversion.cpp index 2deab22264..4d44b0a41b 100644 --- a/core/libs/tests/GuestAddressConversion.cpp +++ b/core/libs/tests/GuestAddressConversion.cpp @@ -11,6 +11,10 @@ const char* APS5_VABI __inet_ntop_nid_postfix(int, const void*, char*, std::uint int APS5_VABI inet_pton_nid_postfix(int, const char*, void*); const char* APS5_VABI inet_ntop_nid_postfix(int, const void*, char*, std::uint32_t); int* APS5_VABI __error_nid_postfix(); +std::uint32_t APS5_VABI htonl_nid_postfix(std::uint32_t); +std::uint32_t APS5_VABI ntohl_nid_postfix(std::uint32_t); +std::uint16_t APS5_VABI htons_nid_postfix(std::uint16_t); +std::uint16_t APS5_VABI ntohs_nid_postfix(std::uint16_t); } static void Require(bool value) { if (!value) std::abort(); } @@ -566,6 +570,12 @@ static void CheckErrnoKept() { } int main() { + Require(htonl_nid_postfix(0x11223344u) == 0x44332211u && ntohl_nid_postfix(0x44332211u) == 0x11223344u); + Require(htons_nid_postfix(0x1234) == 0x3412 && ntohs_nid_postfix(0x3412) == 0x1234); + const std::uint32_t loopback = htonl_nid_postfix(0x7f000001u); + unsigned char loopbackBytes[4]; + std::memcpy(loopbackBytes, &loopback, sizeof(loopback)); + Require(loopbackBytes[0] == 127 && loopbackBytes[3] == 1); for (int family : {2, 28}) { const char* input = family == 2 ? "192.0.2.17" : "2001:db8::17"; std::array address{}; diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 2340688d38..ca17fc2c23 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -243,6 +243,16 @@ void TestRejectedArguments() { Require(sceAmprMeasureCommandSizePushMarkerWithColor(nullptr, color) == rejected); Require(sceAmprMeasureCommandSizeSetMarker(nullptr) == rejected); Require(sceAmprMeasureCommandSizeSetMarkerWithColor(nullptr, color) == rejected); + + std::uint64_t gatherState = 0; + std::uint64_t scatterState = 0; + std::array memory{}; + Require(sceAmprCommandBufferConstructor(nullptr) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(nullptr, &gatherState, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, nullptr, &scatterState) == invalidArgument); + Require(sceAmprAprCommandBufferConstructor(&recorder.buffer, &gatherState, nullptr) == invalidArgument); + Require(sceAmprCommandBufferReset(nullptr) == invalidArgument); + Require(sceAmprCommandBufferSetBuffer(nullptr, memory.data(), memory.size()) == invalidArgument); } void TestFullBuffer() { diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 0c669decd2..403c445304 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -61,7 +61,7 @@ int APS5_VABI _Thrd_join_nid_postfix(Pthread thread, int* code); int APS5_VABI _Thrd_sleep_nid_postfix(const GuestXtime* target, GuestXtime* remaining); void APS5_VABI _Lock_shared_ptr_spin_lock_nid_postfix(void); void APS5_VABI _Unlock_shared_ptr_spin_lock_nid_postfix(void); -int APS5_VABI wcstombs_s_nid_postfix(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); +int APS5_VABI sceAmprWcstombsS(std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI pthread_detach_nid_postfix(Pthread thread); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); @@ -233,42 +233,42 @@ static void TestWcstombs() { char buffer[8]; std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, sizeof(buffer)) == 0); Require(result == 3 && std::strcmp(buffer, "abc") == 0); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), text, 2) == 0); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), text, 2) == 0); Require(result == 2 && std::strcmp(buffer, "ab") == 0); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0); + Require(sceAmprWcstombsS(&result, nullptr, 0, text, 0) == 0); Require(result == 3); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 3, text, 3) == GuestErange); + Require(sceAmprWcstombsS(&result, buffer, 3, text, 3) == GuestErange); Require(result == Failed && buffer[0] == '\0'); result = 0; - Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, nullptr, 4, text, 4) == GuestEinval); Require(result == Failed); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), nullptr, sizeof(buffer)) == GuestEinval); Require(result == Failed && buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); + Require(sceAmprWcstombsS(nullptr, buffer, sizeof(buffer), text, sizeof(buffer)) == GuestEinval); Require(buffer[0] == '\0'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, 0, text, 4) == GuestEinval); + Require(sceAmprWcstombsS(&result, buffer, 0, text, 4) == GuestEinval); Require(result == Failed && buffer[0] == 'x'); std::memset(buffer, 'x', sizeof(buffer)); - Require(wcstombs_s_nid_postfix(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); + Require(sceAmprWcstombsS(&result, buffer, sizeof(buffer), wide, sizeof(buffer)) == GuestEilseq); Require(result == Failed && buffer[0] == 'a' && buffer[1] == '\0'); - Require(wcstombs_s_nid_postfix(&result, nullptr, 0, wide, 0) == GuestEilseq); + Require(sceAmprWcstombsS(&result, nullptr, 0, wide, 0) == GuestEilseq); Require(result == Failed); } diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index 5421462aea..860ff13718 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -12,6 +12,7 @@ extern "C" { int APS5_VABI sceKernelGetAppInfo(int pid, void* info); int APS5_VABI getpid_nid_postfix(void); int APS5_VABI sceKernelUuidCreate(std::uint32_t* uuid); +int APS5_VABI sceKernelCreateUuidV4(std::uint8_t* uuid); } static constexpr int SCE_KERNEL_ERROR_EPERM = static_cast(0x80020001); @@ -109,4 +110,11 @@ int main(int argc, char** argv) { Require(sceKernelUuidCreate(first) == 0 && sceKernelUuidCreate(second) == 0); for (const std::uint32_t* uuid : {first, second}) Require((uuid[1] >> 28) == 4u && (uuid[2] & 0xc0u) == 0x80u); Require(std::memcmp(first, second, sizeof(first)) != 0); + + Require(sceKernelCreateUuidV4(nullptr) == SCE_KERNEL_ERROR_EINVAL); + std::uint8_t third[16]{}; + std::uint8_t fourth[16]{}; + Require(sceKernelCreateUuidV4(third) == 0 && sceKernelCreateUuidV4(fourth) == 0); + for (const std::uint8_t* uuid : {third, fourth}) Require((uuid[6] >> 4) == 4u && (uuid[8] & 0xc0u) == 0x80u); + Require(std::memcmp(third, fourth, sizeof(third)) != 0); } diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp index d2f12108a7..a8ba636dc3 100644 --- a/core/libs/tests/GuestAprRead.cpp +++ b/core/libs/tests/GuestAprRead.cpp @@ -228,13 +228,39 @@ private: void* pointer = nullptr; void* handler = nullptr; }; + +struct SmallStackRead { + const Fixture* fixture = nullptr; + std::vector* storage = nullptr; + std::string error; +}; + +DWORD WINAPI ReadOnSmallStack(void* parameter) { + auto& read = *static_cast(parameter); + try { + CheckRead(*read.fixture, read.storage->data() + 17, 65537, 0); + } catch (const std::exception& failure) { + read.error = failure.what(); + } + return 0; +} + +void TestSmallStack(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + SmallStackRead read{&fixture, &storage, {}}; + const HANDLE thread = CreateThread(nullptr, 64u * 1024u, ReadOnSmallStack, &read, STACK_SIZE_PARAM_IS_A_RESERVATION, nullptr); + Require(thread != nullptr, "cannot start the small-stack APR reader"); + WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + Require(read.error.empty(), read.error.c_str()); +} #endif } int main(int argc, char** argv) { try { - Require(argc == 2, "expected host, direct, eof, errors or fault"); + Require(argc == 2, "expected host, direct, eof, errors, streams, fault or stack"); const Fixture fixture; const std::string mode = argv[1]; if (mode == "host") { @@ -254,6 +280,8 @@ int main(int argc, char** argv) { const FaultBuffer storage; TestRead(fixture, storage.Data()); Require(committedPages > 2, "APR did not exercise demand-committed destination pages"); + } else if (mode == "stack") { + TestSmallStack(fixture); #endif } else { throw std::runtime_error("unknown APR read case"); diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp index 6a9a907466..e7863563a6 100644 --- a/core/libs/tests/GuestArenaHost.cpp +++ b/core/libs/tests/GuestArenaHost.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -20,7 +21,11 @@ using namespace GuestArena; namespace { constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; +constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uintptr_t ReservationChunk = 1ull << 30; +constexpr std::size_t GapBytes = 0x10000; constexpr std::uint32_t ProtectionNoAccess = 0x01; constexpr std::uint32_t ProtectionReadOnly = 0x02; constexpr std::uint32_t ProtectionReadWrite = 0x04; @@ -51,6 +56,15 @@ std::vector RegionsInArenaRange() { return regions; } +const std::vector RegionsBeforeArena = RegionsInArenaRange(); + +bool Overlaps(const std::vector& regions, std::uintptr_t start, std::uintptr_t end) { + for (const auto& region : regions) { + if (region.start < end && start < region.end) return true; + } + return false; +} + void CheckAvailable() { Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); std::uintptr_t base = 0; @@ -88,6 +102,24 @@ void CheckHostRegionsKept() { std::printf("%zu host regions inside the arena range are protected\n", hostRegions); } +void CheckGapBesideHostRegion() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + Require(RegionsBeforeArena.size() < RegionsInArenaRange().size(), "the host map was read after the guest arena reserved its range"); + for (const auto& host : RegionsBeforeArena) { + const auto chunk = host.start & ~(ReservationChunk - 1); + for (auto gap = std::max(chunk, ArenaStart); gap + GapBytes <= chunk + ReservationChunk && gap + GapBytes <= ApplicationAreaEnd; gap += GapBytes) { + if (Overlaps(RegionsBeforeArena, gap, gap + GapBytes) || (gap < SystemReservedEnd && SystemReservedStart < gap + GapBytes)) continue; + auto* pointer = reinterpret_cast(gap); + GuestArenaMarkUsed_nid_postfix(pointer, GapBytes); + GuestArenaRelease_nid_postfix(pointer, GapBytes); + std::printf("free range 0x%llx beside host region 0x%llx-0x%llx accepted\n", static_cast(gap), + static_cast(host.start), static_cast(host.end)); + return; + } + } + std::puts("no free range shares a 1 GiB reservation chunk with a host region on this host; nothing to check"); +} + void CheckCommitAndProtect() { constexpr std::size_t bytes = 0x10000; auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); @@ -106,11 +138,12 @@ void CheckCommitAndProtect() { int main(int argc, char** argv) { try { - Require(argc == 2, "expected available, unmappable, host or commit"); + Require(argc == 2, "expected available, unmappable, host, gap or commit"); const std::string_view mode(argv[1]); if (mode == "available") CheckAvailable(); else if (mode == "unmappable") CheckSkipsUnmappable(); else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "gap") CheckGapBesideHostRegion(); else if (mode == "commit") CheckCommitAndProtect(); else throw std::runtime_error("unknown guest arena test"); std::printf("guest arena %s passed\n", argv[1]); diff --git a/core/libs/tests/GuestAudio3dMixer.cpp b/core/libs/tests/GuestAudio3dMixer.cpp new file mode 100644 index 0000000000..0175519890 --- /dev/null +++ b/core/libs/tests/GuestAudio3dMixer.cpp @@ -0,0 +1,114 @@ +#include "Mixer.hpp" + +#include +#include +#include +#include + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +int main() { + std::int16_t pcm[4] = {16384, -16384, 8192, -8192}; + Audio3dInternal::Source s{pcm, 2, 2, false, 1.0f, 0.0f}; + std::vector out; + Audio3dInternal::MixFrame(&s, 1, 2, out); + Require(out.size() == 4, "frame is granularity*2 samples"); + Require(out[0] == 16384 && out[1] == -16384, "centered stereo passes through"); + Require(out[2] == 8192 && out[3] == -8192, "centered stereo passes through the second frame"); + + std::int16_t panned[2] = {10000, 20000}; + Audio3dInternal::Source hard_right{panned, 1, 2, false, 1.0f, 1.0f}; + std::vector panned_out; + Audio3dInternal::MixFrame(&hard_right, 1, 1, panned_out); + Require(panned_out.size() == 2 && panned_out[0] == 0 && panned_out[1] == 19999, "hard right keeps only the right channel"); + + float floats[2] = {0.5f, -0.5f}; + Audio3dInternal::Source float_source{floats, 1, 2, true, 1.0f, 0.0f}; + std::vector float_out; + Audio3dInternal::MixFrame(&float_source, 1, 1, float_out); + Require(float_out.size() == 2 && float_out[0] == 16384 && float_out[1] == -16384, "float samples are converted"); + + std::int16_t mono[1] = {10000}; + Audio3dInternal::Source mono_source{mono, 1, 1, false, 1.0f, 0.0f}; + std::vector mono_out; + Audio3dInternal::MixFrame(&mono_source, 1, 1, mono_out); + Require(mono_out.size() == 2 && mono_out[0] == 10000 && mono_out[1] == 10000, "centered mono is duplicated"); + + std::vector short_out; + Audio3dInternal::MixFrame(&mono_source, 1, 4, short_out); + Require(short_out.size() == 8, "short frame keeps the full frame size"); + Require(short_out[0] == 10000 && short_out[1] == 10000, "short frame mixes the first sample"); + Require(short_out[2] == 0 && short_out[3] == 0 && short_out[4] == 0 && short_out[5] == 0 && short_out[6] == 0 && short_out[7] == 0, "short frame zero pads the tail"); + + std::int16_t loud[1] = {32767}; + Audio3dInternal::Source loud_source{loud, 1, 1, false, 1.0f, 0.0f}; + Audio3dInternal::Source loud_sources[2] = {loud_source, loud_source}; + std::vector loud_out; + Audio3dInternal::MixFrame(loud_sources, 2, 1, loud_out); + Require(loud_out.size() == 2 && loud_out[0] == 32767 && loud_out[1] == 32767, "sums above full scale clamp"); + + Audio3dInternal::Source out_of_range{panned, 1, 2, false, 1.0f, 2.0f}; + std::vector out_of_range_out; + Audio3dInternal::MixFrame(&out_of_range, 1, 1, out_of_range_out); + Require(out_of_range_out.size() == 2 && out_of_range_out[0] == 0 && out_of_range_out[1] == 19999, "out of range pan clamps"); + + std::vector empty_out; + Audio3dInternal::MixFrame(nullptr, 0, 2, empty_out); + Require(empty_out.size() == 4 && empty_out[0] == 0 && empty_out[1] == 0 && empty_out[2] == 0 && empty_out[3] == 0, "no sources produce silence"); + + std::int16_t skipped[2] = {10000, 10000}; + Audio3dInternal::Source null_samples{nullptr, 1, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source empty_samples{skipped, 0, 2, false, 1.0f, 0.0f}; + Audio3dInternal::Source skipped_sources[2] = {null_samples, empty_samples}; + std::vector skipped_out; + Audio3dInternal::MixFrame(skipped_sources, 2, 1, skipped_out); + Require(skipped_out.size() == 2 && skipped_out[0] == 0 && skipped_out[1] == 0, "null and empty sources are skipped"); + + std::int16_t surround6[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_source{surround6, 1, 6, false, 1.0f, 0.0f}; + std::vector surround6_out; + Audio3dInternal::MixFrame(&surround6_source, 1, 1, surround6_out); + Require(surround6_out.size() == 2 && surround6_out[0] == 5950 && surround6_out[1] == 7657, + "6 channel downmix folds center and surrounds, dropping LFE"); + + std::int16_t surround8[8] = {1000, 2000, 3000, 0, 4000, 5000, 6000, 7000}; + Audio3dInternal::Source surround8_source{surround8, 1, 8, false, 1.0f, 0.0f}; + std::vector surround8_out; + Audio3dInternal::MixFrame(&surround8_source, 1, 1, surround8_out); + Require(surround8_out.size() == 2 && surround8_out[0] == 10192 && surround8_out[1] == 12606, + "8 channel downmix folds rear surrounds into stereo"); + + std::int16_t surround5[5] = {1000, 2000, 3000, 0, 4000}; + Audio3dInternal::Source surround5_source{surround5, 1, 5, false, 1.0f, 0.0f}; + std::vector surround5_out; + Audio3dInternal::MixFrame(&surround5_source, 1, 1, surround5_out); + Require(surround5_out.size() == 2 && surround5_out[0] == 5950 && surround5_out[1] == 4121, + "5 channel downmix reads only the channels present"); + + std::int16_t surround3[3] = {1000, 2000, 3000}; + Audio3dInternal::Source surround3_source{surround3, 1, 3, false, 1.0f, 0.0f}; + std::vector surround3_out; + Audio3dInternal::MixFrame(&surround3_source, 1, 1, surround3_out); + Require(surround3_out.size() == 2 && surround3_out[0] == 3121 && surround3_out[1] == 4121, + "3 channel downmix folds center into both sides"); + + std::int16_t surround4[4] = {1000, 2000, 3000, 0}; + Audio3dInternal::Source surround4_source{surround4, 1, 4, false, 1.0f, 0.0f}; + std::vector surround4_out; + Audio3dInternal::MixFrame(&surround4_source, 1, 1, surround4_out); + Require(surround4_out.size() == 2 && surround4_out[0] == 3121 && surround4_out[1] == 4121, + "4 channel downmix drops LFE"); + + std::int16_t surround6_panned[6] = {1000, 2000, 3000, 0, 4000, 5000}; + Audio3dInternal::Source surround6_panned_source{surround6_panned, 1, 6, false, 1.0f, 1.0f}; + std::vector surround6_panned_out; + Audio3dInternal::MixFrame(&surround6_panned_source, 1, 1, surround6_panned_out); + Require(surround6_panned_out.size() == 2 && surround6_panned_out[0] == 0 && surround6_panned_out[1] == 7657, + "6 channel downmix is panned after folding"); + + return 0; +} diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index 6c754ac77a..5b5467437d 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -7,6 +7,7 @@ #include #include #include +#include extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); @@ -23,6 +24,12 @@ int APS5_VABI sceAudio3dPortFlush(std::uint32_t port_id); int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); int APS5_VABI sceAudio3dObjectSetAttributes(std::uint32_t port_id, std::uint32_t object_id, std::uint64_t num_attributes, const Audio3dAttribute* attribute_array); +int APS5_VABI sceAudio3dBedWrite(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples); +int APS5_VABI sceAudio3dBedWrite2(std::uint32_t port_id, std::uint32_t num_channels, std::uint32_t format, const void* buffer, std::uint32_t num_samples, std::uint32_t output_route, bool restricted); +int APS5_VABI sceAudio3dAudioOutOpen(std::uint32_t port_id, int user_id, std::int32_t type, std::int32_t index, std::uint32_t len, std::uint32_t freq, std::uint32_t param); +int APS5_VABI sceAudio3dAudioOutClose(std::int32_t handle); +int APS5_VABI sceAudio3dAudioOutOutput(std::int32_t handle, const void* ptr); +int APS5_VABI sceAudio3dAudioOutOutputs(AudioOutOutputParam* param, std::uint32_t num); } namespace { @@ -195,11 +202,8 @@ void CheckObjects() { } void CheckQueue() { - using namespace std::chrono; - constexpr std::uint32_t granularity = 0x1800; - constexpr auto frame = microseconds(1000000ull * granularity / 48000); Audio3dOpenParameters parameters = Defaults(); - parameters.granularity = granularity; + parameters.granularity = 0x1800; std::uint32_t id = 7; Require(Open(parameters, &id) == 0 && id == 0, "open"); CheckAttributes(); @@ -213,23 +217,53 @@ void CheckQueue() { RequireLevel(1, 1, "one frame queued"); Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); RequireLevel(2, 0, "queue full"); - RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "advance into a full queue must be NOT_READY"); RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); - - auto start = steady_clock::now(); - Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); - auto waited = steady_clock::now() - start; - Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); - RequireLevel(1, 1, "blocking push returns with one free slot"); - + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); - start = steady_clock::now(); + Require(sceAudio3dPortAdvance(0) == 0, "advance 4"); Require(sceAudio3dPortPush(0, 0) == 0, "async push"); - Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); - std::uint32_t level = 0; - Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); - std::this_thread::sleep_for(frame * 3); - RequireLevel(0, 2, "frames drain at the port's rate"); + RequireLevel(1, 1, "async push keeps the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckMixAndPush() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes an empty bed into silence"); + RequireLevel(1, 1, "the silent frame is queued"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + Require(sceAudio3dPortAdvance(0) == 0, "advance mixes the bed"); + RequireLevel(2, 0, "the mixed frame is queued"); + Require(sceAudio3dPortAdvance(0) == NOT_READY, "a full queue rejects the mix"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + RequireLevel(0, 2, "sync push drains every queued frame"); + Require(sceAudio3dPortAdvance(0) == 0, "advance after the sync drain"); + Require(sceAudio3dPortAdvance(0) == 0, "advance fills the queue again"); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + RequireLevel(1, 1, "async push leaves the queue one below the depth"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckPushKeepsRealTime() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "bed write"); + constexpr std::uint32_t frames = 200; + const std::uint64_t blockUs = 1000000ull * parameters.granularity / 48000ull; + const auto start = std::chrono::steady_clock::now(); + for (std::uint32_t i = 0; i < frames; i++) { + Require(sceAudio3dPortAdvance(0) == 0, "advance"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push"); + } + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + const std::uint64_t expected = frames * blockUs; + Require(elapsed + 150000 >= static_cast(expected), "a blocking push must run at real time"); Require(sceAudio3dPortClose(0) == 0, "close"); } @@ -246,6 +280,38 @@ void CheckFlush() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckAudioOutPorts() { + const std::uint32_t granularity = Defaults().granularity; + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + Require(sceAudio3dAudioOutOpen(1, SYSTEM_USER, 126, 0, granularity, 48000, 1) == INVALID_PORT, "audio out open on port 1"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity + 0x100, 48000, 1) == INVALID_PARAMETER, "len must equal the granularity"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 1, granularity, 48000, 1) == INVALID_PARAMETER, "index must be 0"); + Require(sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 8) == INVALID_PARAMETER, "unknown format"); + const int handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(handle > 0, "audio out open returns a handle"); + const std::vector block(static_cast(granularity) * 2, 256); + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "audio out output returns the sample count"); + } + Require(sceAudio3dAudioOutOutput(handle, block.data()) == NOT_READY, "a full pending queue must be NOT_READY"); + Require(sceAudio3dPortPush(0, 1) == 0, "sync push drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "push drains the pending queue"); + Require(sceAudio3dPortFlush(0) == 0, "flush drains the associated ports"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == static_cast(granularity), "flush resets the pending queue"); + Require(sceAudio3dAudioOutOutputs(nullptr, 0) == INVALID_PARAMETER, "outputs rejects an empty batch"); + Require(sceAudio3dAudioOutClose(handle) == 0, "audio out close"); + Require(sceAudio3dAudioOutClose(handle) == INVALID_PORT, "audio out double close"); + Require(sceAudio3dAudioOutOutput(handle, block.data()) == INVALID_PORT, "output on a closed handle"); + const int open_handle = sceAudio3dAudioOutOpen(0, SYSTEM_USER, 126, 0, granularity, 48000, 1); + Require(open_handle > 0, "audio out reopen"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dAudioOutOutput(open_handle, block.data()) == INVALID_PORT, "port close clears the associated ports"); + Require(sceAudio3dAudioOutClose(open_handle) == INVALID_PORT, "port close closes the associated ports"); +} + void CheckObjectAttributes() { float value = 0.5f; Audio3dAttribute attribute{0x10001, 0, &value, sizeof(value)}; @@ -268,6 +334,62 @@ void CheckObjectAttributes() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckBed() { + const std::int16_t samples[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == INVALID_PORT, "bed write needs an open port"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == INVALID_PORT, "bed write2 needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const auto consume = [] { + Require(sceAudio3dPortAdvance(0) == 0, "advance after a bed write"); + Require(sceAudio3dPortFlush(0) == 0, "flush the mixed frame after a bed write"); + }; + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "2 channel s16 bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 1, 0, samples, 4) == INVALID_PARAMETER, "1 channel bed write"); + Require(sceAudio3dBedWrite(0, 2, 0, nullptr, 4) == INVALID_PARAMETER, "null bed buffer"); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 0, false) == 0, "bed write2 route both"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 1, false) == 0, "bed write2 route hmu only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 2, false) == 0, "bed write2 route tv only"); + consume(); + Require(sceAudio3dBedWrite2(0, 2, 0, samples, 4, 3, false) == INVALID_PARAMETER, "bed write2 unknown route"); + Require(sceAudio3dBedWrite(0, 2, 2, samples, 4) == INVALID_PARAMETER, "unknown bed format"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 0) == INVALID_PARAMETER, "zero bed samples"); + Require(sceAudio3dBedWrite(0, 2, 0, reinterpret_cast(samples) + 1, 4) == INVALID_PARAMETER, "misaligned s16 bed buffer"); + const float floats[8] = {}; + Require(sceAudio3dBedWrite(0, 2, 1, floats, 4) == 0, "float bed write"); + consume(); + const std::uint32_t granularity = parameters.granularity; + std::vector wide(static_cast(granularity) * 8, 0); + Require(sceAudio3dBedWrite(0, 6, 0, wide.data(), granularity) == 0, "6 channel bed write"); + consume(); + Require(sceAudio3dBedWrite(0, 8, 0, wide.data(), granularity) == 0, "8 channel bed write"); + consume(); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +void CheckBedQueue() { + Audio3dOpenParameters parameters = Defaults(); + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + const std::int16_t samples[8] = {1000, -1000, 2000, -2000, 3000, -3000, 4000, -4000}; + for (std::uint32_t i = 0; i < parameters.queue_depth; ++i) { + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "each bed write fills the queue"); + } + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == NOT_READY, "a full bed queue must be NOT_READY"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes one bed"); + Require(sceAudio3dBedWrite(0, 2, 0, samples, 4) == 0, "the consumed bed makes room"); + Require(sceAudio3dPortAdvance(0) == 0, "advance consumes the next bed"); + RequireLevel(2, 0, "two frames are queued from the queued beds"); + Require(sceAudio3dPortFlush(0) == 0, "flush"); + Require(sceAudio3dPortAdvance(0) == 0, "advance with no bed left mixes silence"); + RequireLevel(1, 1, "silence is queued behind the flushed frames"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckTerminate() { std::uint32_t id = 7; const Audio3dOpenParameters parameters = Defaults(); @@ -292,7 +414,12 @@ int main() { CheckOpenClose(); CheckObjects(); CheckQueue(); + CheckMixAndPush(); + CheckPushKeepsRealTime(); CheckFlush(); + CheckAudioOutPorts(); CheckObjectAttributes(); + CheckBed(); + CheckBedQueue(); CheckTerminate(); } diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index 25e958cdb9..7ae55383e3 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -4,13 +4,20 @@ #include #include #include +#if defined(_WIN32) +#include +#else #include +#endif #include #include extern "C" { int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +#if defined(_WIN32) +std::string BacktraceModulePath_nid_no_patch(HMODULE, std::size_t); +#endif } namespace { @@ -63,13 +70,29 @@ std::size_t Used(const std::vector& records) { return total; } +[[gnu::noinline]] std::uintptr_t CurrentIp() { + asm volatile("" ::: "memory"); + return reinterpret_cast(__builtin_return_address(0)); +} + } [[gnu::noinline]] void CheckBacktrace() { +#if defined(_WIN32) + HMODULE handle = nullptr; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&CheckBacktrace), &handle) != 0 && handle != nullptr, + "GetModuleHandleExA of the test failed"); + std::string executable = BacktraceModulePath_nid_no_patch(handle, 1); + const auto slash = executable.find_last_of("/\\"); + Require(!executable.empty() && slash != std::string::npos, "BacktraceModulePath of the test failed"); + executable.erase(0, slash + 1); +#else Dl_info self{}; Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); std::string executable = self.dli_fname; executable.erase(0, executable.find_last_of('/') + 1); +#endif int count = -1; std::size_t size = 0; @@ -78,16 +101,26 @@ std::size_t Used(const std::vector& records) { std::vector buffer(size + 64, Fill); int written = -1; - Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); + const auto before = CurrentIp(); + const int selfStatus = sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written); + const auto after = CurrentIp(); + Require(selfStatus == 0, "Self failed"); Require(written == count, "Self wrote a different count than GetBufferSize reported"); const auto records = Parse(buffer, size, written); Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); +#if defined(_WIN32) + const auto address = reinterpret_cast(handle) + records.front().offset; + const auto callerReturn = reinterpret_cast(handle) + records[1].offset; + Require(address > before && address < after, "first frame is not the caller"); + Require(callerReturn == reinterpret_cast(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace"); +#else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); +#endif for (const int limit : {0, 1, 2}) { Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); diff --git a/core/libs/tests/GuestCallTrace.cpp b/core/libs/tests/GuestCallTrace.cpp index 4be35302dc..11d3999982 100644 --- a/core/libs/tests/GuestCallTrace.cpp +++ b/core/libs/tests/GuestCallTrace.cpp @@ -1,6 +1,13 @@ #include "prx/libc/include/CallTrace.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#ifdef _WIN32 #include +#else +#include +#include +#endif +#include +#include #include #include #include @@ -49,18 +56,65 @@ static Block APS5_VABI Blocks(std::uint64_t seed) { return {{seed, seed + 1, seed + 2, seed + 3}}; } +static std::uint64_t CurrentThread() { +#ifdef _WIN32 + return GetCurrentThreadId(); +#else + return static_cast(gettid()); +#endif +} + +static const void* ImageOf(const void* address) { +#ifdef _WIN32 + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(address, &memory, sizeof(memory)) != sizeof(memory)) return nullptr; + return memory.AllocationBase; +#else + Dl_info info{}; + if (dladdr(address, &info) == 0) return nullptr; + return info.dli_fbase; +#endif +} + template static TFunction* Traced(TFunction* function, const char* name, const char* library) { return reinterpret_cast(TraceImport_nid_no_patch(reinterpret_cast(function), name, library)); } int main(int argc, char** argv) { +#ifdef _WIN32 Require(_putenv_s("APS5_TRACE_CALLS", "libTraced,libOther.prx") == 0); - if (argc > 1 && std::strcmp(argv[1], "abort") == 0) { +#else + Require(setenv("APS5_TRACE_CALLS", "libTraced,libOther.prx", 1) == 0); +#endif + if (argc > 1 && (std::strcmp(argv[1], "abort") == 0 || std::strcmp(argv[1], "abort-race") == 0)) { +#ifndef _WIN32 + Require(std::signal(SIGABRT, [](int) { _exit(3); }) != SIG_ERR); +#endif const auto first = Traced(&Integers, "Integers", "libTraced.prx"); const auto second = Traced(&Blocks, "Blocks", "libOther.prx"); Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204 && second(7).words[3] == 10); +#ifdef _WIN32 _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT); +#else + std::atomic locked{false}; + std::thread([&locked] { + flockfile(stderr); + locked.store(true); + for (;;) pause(); + }).detach(); + while (!locked.load()) std::this_thread::yield(); + if (std::strcmp(argv[1], "abort-race") == 0) { + for (int call = 0; call < 100; ++call) Require(first(1, 2, 3, 4, 5, 6, 7, 8) == 204); + std::atomic go{false}; + std::thread([&go] { + while (!go.load()) { + } + std::abort(); + }).detach(); + go.store(true); + } +#endif std::abort(); } CallTraceEntry entries[16]; @@ -90,15 +144,13 @@ int main(int argc, char** argv) { const char* const names[] = {"Integers", "Doubles", "Mixed", "Variadic", "Blocks"}; const char* const libraries[] = {"libTraced.prx", "libTraced", "libOther.prx", "libOther", "libTraced.prx"}; - MEMORY_BASIC_INFORMATION image{}; - Require(VirtualQuery(reinterpret_cast(&main), &image, sizeof(image)) == sizeof(image)); + const void* image = ImageOf(reinterpret_cast(&main)); + Require(image != nullptr); Require(CopyCallTrace_nid_no_patch(entries, 16) == 5); for (std::uint64_t index = 0; index < 5; ++index) { - Require(entries[index].sequence == index + 1 && entries[index].thread == GetCurrentThreadId()); + Require(entries[index].sequence == index + 1 && entries[index].thread == CurrentThread()); Require(std::strcmp(entries[index].name, names[index]) == 0 && std::strcmp(entries[index].library, libraries[index]) == 0); - MEMORY_BASIC_INFORMATION caller{}; - Require(VirtualQuery(reinterpret_cast(entries[index].returnAddress), &caller, sizeof(caller)) == sizeof(caller)); - Require(caller.AllocationBase == image.AllocationBase); + Require(ImageOf(reinterpret_cast(entries[index].returnAddress)) == image); } Require(CopyCallTrace_nid_no_patch(entries, 2) == 2 && entries[0].sequence == 4 && entries[1].sequence == 5); Require(CopyCallCounts_nid_no_patch(counts, 8, &total) == 5 && total == 5); diff --git a/core/libs/tests/GuestDirectory.cpp b/core/libs/tests/GuestDirectory.cpp index 5c3e5dd0cc..4df4394622 100644 --- a/core/libs/tests/GuestDirectory.cpp +++ b/core/libs/tests/GuestDirectory.cpp @@ -1,7 +1,9 @@ #include "prx/libc/include/GuestDirectory.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" #include #include +#include #include #include #include @@ -9,6 +11,10 @@ #include #include #include +#include +#ifdef _WIN32 +#include +#endif extern "C" { void* APS5_VABI opendir_nid_postfix(const char*); GuestDirectoryEntry* APS5_VABI readdir_nid_postfix(void*); @@ -18,25 +24,83 @@ void APS5_VABI rewinddir_nid_postfix(void*); std::int64_t APS5_VABI telldir_nid_postfix(void*); void APS5_VABI seekdir_nid_postfix(void*, std::int64_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI lstat_nid_postfix(const char*, FileStat*); } static void Require(bool value) { if (!value) std::abort(); } +static std::map ReadFileNumbers(void* directory) { + std::map numbers; + rewinddir_nid_postfix(directory); + while (auto* entry = readdir_nid_postfix(directory)) { + Require(entry->fileNumber != 0); + numbers[entry->name] = entry->fileNumber; + } + return numbers; +} +static std::uint32_t FileNumber(const std::filesystem::path& path) { + FileStat status{}; + Require(lstat_nid_postfix(path.string().c_str(), &status) == 0); + return static_cast(status.st_ino); +} +static bool CreateLinks(const std::filesystem::path& root) { +#ifdef _WIN32 + if (CreateSymbolicLinkW((root / "file-link").c_str(), L"sample.txt", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) == 0) { + Require(GetLastError() == ERROR_PRIVILEGE_NOT_HELD); + std::fprintf(stderr, "Windows readdir link checks skipped: creating symbolic links needs Developer Mode or administrator rights\n"); + return false; + } + Require(CreateSymbolicLinkW((root / "directory-link").c_str(), L"subdirectory", + SYMBOLIC_LINK_FLAG_DIRECTORY | SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); +#else + std::filesystem::create_symlink("sample.txt", root / "file-link"); + std::filesystem::create_directory_symlink("subdirectory", root / "directory-link"); +#endif + return true; +} int main() { const auto root = std::filesystem::path("anyps5-directory-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); Require(std::filesystem::create_directory(root / "subdirectory")); { std::ofstream file(root / "sample.txt"); file << "test"; } + const bool links = CreateLinks(root); + std::vector numbered{"sample.txt", "subdirectory"}; + if (links) numbered.insert(numbered.end(), {"file-link", "directory-link"}); void* directory = opendir_nid_postfix(root.string().c_str()); Require(directory != nullptr); std::map entries; + std::map fileNumbers; *__error_nid_postfix() = 13; while (auto* entry = readdir_nid_postfix(directory)) { Require(entry->nameLength == std::strlen(entry->name)); Require(entry->recordLength == 8 + ((entry->nameLength + 4) & ~3)); Require(entry->recordLength <= sizeof(*entry)); entries[entry->name] = entry->type; + Require(entry->fileNumber != 0); + fileNumbers[entry->name] = entry->fileNumber; } Require(*__error_nid_postfix() == 13); // EOF preserves errno. + for (const char* name : numbered) Require(fileNumbers.at(name) == FileNumber(root / name)); + if (links) Require(fileNumbers.at("file-link") != fileNumbers.at("sample.txt")); + { std::ofstream file(root / "created.txt"); file << "new"; } + auto changed = ReadFileNumbers(directory); + Require(changed.at("created.txt") == FileNumber(root / "created.txt")); + Require(changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(std::filesystem::remove(root / "created.txt")); + changed = ReadFileNumbers(directory); + Require(changed.count("created.txt") == 0 && changed == fileNumbers); +#ifdef _WIN32 + const HANDLE pending = CreateFileW((root / "pending.txt").c_str(), GENERIC_WRITE | DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW, 0, nullptr); + Require(pending != INVALID_HANDLE_VALUE); + FILE_DISPOSITION_INFO disposition{TRUE}; + Require(SetFileInformationByHandle(pending, FileDispositionInfo, &disposition, sizeof(disposition)) != 0); + FileStat pendingStatus{}; + Require(lstat_nid_postfix((root / "pending.txt").string().c_str(), &pendingStatus) == -1); + changed = ReadFileNumbers(directory); + Require(changed.count("pending.txt") == 1 && changed.at("sample.txt") == fileNumbers.at("sample.txt")); + Require(CloseHandle(pending) != 0); + Require(ReadFileNumbers(directory) == fileNumbers); +#endif Require(entries.at("sample.txt") == 8); Require(entries.at("subdirectory") == 4); Require(entries.at(".") == 4 && entries.at("..") == 4); @@ -99,6 +163,7 @@ int main() { seekdir_nid_postfix(directory, second); auto* entryAfterSeek = readdir_nid_postfix(directory); Require(entryAfterSeek != nullptr && secondName == entryAfterSeek->name); + Require(entryAfterSeek->fileNumber == fileNumbers.at(secondName)); *__error_nid_postfix() = 0; Require(telldir_nid_postfix(nullptr) == -1 && *__error_nid_postfix() == 9); *__error_nid_postfix() = 0; @@ -133,6 +198,10 @@ int main() { Require(names.at(std::string(file.begin(), file.end())) == 8 && names.size() == 3); Require(closedir_nid_postfix(unicode) == 0); std::filesystem::remove_all(root / std::filesystem::path(folder)); + if (links) { + std::filesystem::remove(root / "file-link"); + std::filesystem::remove(root / "directory-link"); + } std::filesystem::remove(root / "sample.txt"); std::filesystem::remove(root / "subdirectory"); std::filesystem::remove(root); diff --git a/core/libs/tests/GuestElfPhdrMatch.cpp b/core/libs/tests/GuestElfPhdrMatch.cpp index 5e021769a4..3e8d585a70 100644 --- a/core/libs/tests/GuestElfPhdrMatch.cpp +++ b/core/libs/tests/GuestElfPhdrMatch.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "prx/libc/include/specifics/linux/ElfTypes.hpp" +#include "SceTypes.hpp" #include #include #include @@ -41,4 +42,17 @@ int main() { info.dlpi_phnum = 0; Require(__elf_phdr_match_addr_nid_postfix(&info, Address(0x403800)) == 0); + + ModuleInfoEx module{}; + module.st_size = sizeof(module); + module.segment_count = 3; + module.segments[0] = {0x401000, 0x1000, 1 | 2}; + module.segments[1] = {0x403000, 0x1000, 1 | 4}; + module.segments[2] = {0x408000, 0x100, 1 | 2 | 4}; + + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403000)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x403ff8)) == 1); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x404000)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x401800)) == 0); + Require(__elf_phdr_match_addr_nid_postfix(reinterpret_cast(&module), Address(0x408010)) == 1); } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 07b86670f3..eb78467b74 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -322,6 +322,42 @@ static void TestRelinkedModuleNameCase() { } #ifdef _WIN32 +static void TestCaseSensitiveHostDirectory() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-sensitive-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto handle = CreateFileW(root.c_str(), FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + Require(handle != INVALID_HANDLE_VALUE); + struct { ULONG flags; } information{FILE_CS_FLAG_CASE_SENSITIVE_DIR}; + constexpr auto caseSensitiveInfo = static_cast(23); + const bool enabled = SetFileInformationByHandle(handle, caseSensitiveInfo, &information, sizeof(information)); + const auto error = GetLastError(); + CloseHandle(handle); + if (!enabled && (error == ERROR_NOT_SUPPORTED || error == ERROR_INVALID_PARAMETER || error == ERROR_ACCESS_DENIED)) { + std::printf("case-sensitive host directory unavailable: %lu\n", error); + std::filesystem::remove(root); + return; + } + Require(enabled); + { std::ofstream file(root / "File.txt"); file << "payload"; } + Require(std::filesystem::create_directory(root / "Sub")); + Require(std::filesystem::create_directory(root / "Both")); + Require(std::filesystem::create_directory(root / "both")); + { std::ofstream file(root / "libCase.prx.guest.prx"); file << "module"; } + const auto host = root.string(); + AddPathAlias_nid_no_patch("/sensitive-host", host.c_str()); + Require(ResolvePath_nid_no_patch("/sensitive-host/file.txt") == root / "File.txt"); + Require(ResolvePath_nid_no_patch("/sensitive-host/sub") == root / "Sub"); + Require(ResolvePathOrRelinkedModule_nid_no_patch("/sensitive-host/libcase.prx") == root / "libCase.prx.guest.prx"); + bool ambiguous = false; + try { ResolvePath_nid_no_patch("/sensitive-host/BOTH/file.txt"); } + catch (const std::runtime_error& failure) { ambiguous = std::strstr(failure.what(), "Ambiguous case-insensitive guest path") != nullptr; } + Require(ambiguous); + RemovePathAlias_nid_no_patch("/sensitive-host"); + std::filesystem::remove_all(root); +} + static std::filesystem::path ExtendedPath(const std::filesystem::path& path) { const auto& native = path.native(); if (native.starts_with(L"\\\\?\\") || native.starts_with(L"\\\\.\\")) return path; @@ -412,6 +448,7 @@ int main() { TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); #ifdef _WIN32 + TestCaseSensitiveHostDirectory(); TestLongHostPathStat(); #endif #ifdef __linux__ @@ -954,6 +991,9 @@ int main() { const auto statLoop = root / "stat-loop"; Require(CreateSymbolicLinkW(statLoop.c_str(), L"stat-loop", SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); Require(sceKernelStat(statLoop.generic_string().c_str(), &status) == static_cast(0x8002003eu)); + Require(sceKernelOpen(statLoop.generic_string().c_str(), 0, 0) == static_cast(0x8002003eu)); + Require(open_nid_postfix(statLoop.string().c_str(), 0, 0) == -1 && *__error_nid_postfix() == 62); + Require(fopen_nid_postfix(statLoop.string().c_str(), "r") == nullptr && *__error_nid_postfix() == 62); 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 && @@ -1034,6 +1074,9 @@ int main() { Require(flock_nid_postfix(other, 2 | 4) == 0); Require(flock_nid_postfix(other, 8) == 0); Require(flock_nid_postfix(locked, 2 | 4) == 0); + Require(lseek_nid_postfix(-1, 0, 7) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(0x7fffffff, 0, -1) == -1 && *__error_nid_postfix() == 9); + Require(lseek_nid_postfix(locked, 0, 7) == -1 && *__error_nid_postfix() == 22); Require(flock_nid_postfix(-1, 2 | 4) == -1 && *__error_nid_postfix() == 9); Require(flock_nid_postfix(0x7fffffff, 1) == -1 && *__error_nid_postfix() == 9); int flockPipe[2] = {-1, -1}; diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 7b06f4dbe1..3ea9884bb0 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -9,12 +9,20 @@ #include extern "C" { +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _Feraise_nid_postfix(int); int APS5_VABI fegetround_nid_postfix(); int APS5_VABI fesetround_nid_postfix(int); int APS5_VABI fetestexcept_nid_postfix(int); int APS5_VABI feclearexcept_nid_postfix(int); int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t*, int); int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t*, int); +int APS5_VABI feenableexcept_nid_postfix(int); +int APS5_VABI fedisableexcept_nid_postfix(int); +int APS5_VABI __feenableexcept_nid_postfix(int); +int APS5_VABI __fedisableexcept_nid_postfix(int); +int APS5_VABI fegettrapenable_nid_postfix(); +int APS5_VABI fesettrapenable_nid_postfix(int); } namespace { @@ -90,6 +98,21 @@ void CheckRounding() { Require(fegetround_nid_postfix() == 0x400, "query uses the x87 rounding mode"); } +void CheckFeraise() { + for (unsigned requested = 0; requested <= 0x3f; ++requested) { + const auto seeded = SeedFlags(0, 0); + *__error_nid_postfix() = 0; + _Feraise_nid_postfix(static_cast(requested)); + const unsigned raised = (requested & 0x18) != 0 ? requested | 0x20 : requested; + const auto state = ReadEnvironment(); + Require((state.x87[1] & 0x3f) == raised && (state.sse & 0x3f) == raised, "_Feraise raises the flags on both units"); + Require(state.x87[0] == seeded.x87[0] && (state.sse & ~0x3fu) == (seeded.sse & ~0x3fu), "_Feraise preserves control bits"); + const int error = (raised & 0x01) != 0 || (raised & 0x1c) == 0 ? 33 : 34; + Require(*__error_nid_postfix() == error, "_Feraise sets EDOM or ERANGE"); + } + SeedFlags(0, 0); +} + void CheckFlags() { const auto initial = SeedFlags(0x03, 0x24); Require(fetestexcept_nid_postfix(0) == 0, "empty exception query"); @@ -133,6 +156,7 @@ void CheckFlags() { } catch (const std::invalid_argument&) { threw = true; } Require(threw, "reject null exception storage"); } + CheckFeraise(); } void CheckThreads() { @@ -155,10 +179,42 @@ void CheckThreads() { } +unsigned X87Masks() { + return ReadEnvironment().x87[0] & 0x3fu; +} + +unsigned SseMasks() { + return (ReadEnvironment().sse >> 7) & 0x3fu; +} + +void CheckTraps() { + const auto seeded = SeedFlags(0, 0); + Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); + Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); + Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == static_cast(0xffffffc9u), "disable returns the traps enabled on both units"); + Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); + Require(SseMasks() == 0x37, "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == static_cast(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == static_cast(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + auto mixed = ReadEnvironment(); + mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; + mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); + WriteEnvironment(mixed); + Require(feenableexcept_nid_postfix(0) == static_cast(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); + WriteEnvironment(seeded); +} + int main() { const auto original = ReadEnvironment(); CheckRounding(); CheckFlags(); + CheckTraps(); CheckThreads(); WriteEnvironment(original); std::puts("floating-point environment checks passed"); diff --git a/core/libs/tests/GuestFminf.cpp b/core/libs/tests/GuestFminf.cpp index 742fad398f..fcf48171a6 100644 --- a/core/libs/tests/GuestFminf.cpp +++ b/core/libs/tests/GuestFminf.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -40,6 +41,12 @@ static void CheckMinAndMax(T (APS5_VABI *minimum)(T, T), T (APS5_VABI *maximum)( Require(maximum(T(4.0), nan) == T(4.0)); Require(std::isnan(maximum(nan, nan))); + const auto signaling = std::numeric_limits::signaling_NaN(); + std::feclearexcept(FE_ALL_EXCEPT); + Require(minimum(signaling, T(4.0)) == T(4.0) && minimum(T(4.0), signaling) == T(4.0)); + Require(maximum(signaling, T(4.0)) == T(4.0) && maximum(T(4.0), signaling) == T(4.0)); + Require(std::fetestexcept(FE_INVALID) == 0); + Require(std::signbit(minimum(T(-0.0), T(+0.0)))); Require(std::signbit(minimum(T(+0.0), T(-0.0)))); Require(std::signbit(minimum(T(-0.0), T(-0.0)))); diff --git a/core/libs/tests/GuestHmd2.cpp b/core/libs/tests/GuestHmd2.cpp index f9781e407b..4f05d57f02 100644 --- a/core/libs/tests/GuestHmd2.cpp +++ b/core/libs/tests/GuestHmd2.cpp @@ -11,10 +11,52 @@ #include extern "C" { -int APS5_VABI sceHmd2GazeActivateGazeTracking(); -int APS5_VABI sceHmd2ReprojectionGetCurrentOutput(); -int APS5_VABI sceHmd2SetFieldOfView(); +std::int32_t APS5_VABI Hmd2WriteStateTwo(void* a0); +std::int32_t APS5_VABI Hmd2WriteSize0x2000Align0x100(std::uint64_t* a0, std::uint64_t* a1); +std::int32_t APS5_VABI Hmd2WriteFourConstants(void* a0); +std::int32_t APS5_VABI Hmd2WriteSixHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2WriteTwelveHalvesThenInvalidHandle(std::uint64_t a0, void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo3CheckSecondThirdThenZero(std::uint64_t a0, const void* a1, const void* a2); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo1CheckSecondThenError1002(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2SlotUpTo1ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenMinusOne(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1002(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2ThenError1003(std::uint64_t a0); +std::int32_t APS5_VABI Hmd2SlotUpTo2CheckSecondThenError002C(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2IndexZeroCheckSecondThenZero(std::uint64_t a0, const void* a1); +std::int32_t APS5_VABI Hmd2CheckFirstThenError0004(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenZero(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1002(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenMinusOne(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError002C(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenParameterInvalid(const void*); +std::int32_t APS5_VABI Hmd2CheckAllSixThenZero(const void*, const void*, const void*, const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenParameterInvalid(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError800d0203(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1003(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstThenError1010(const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstTwoThenError1003(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenZero(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckFirstAndThirdThenError1003(const void*, const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenInvalidHandle(const void*, const void*); +std::int32_t APS5_VABI Hmd2CheckSecondThenError1002(const void*, const void*); +std::int32_t APS5_VABI Hmd2BeforeInitializeZero(); +std::int32_t APS5_VABI Hmd2BeforeInitializeNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError0004(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterNull(); +std::int32_t APS5_VABI Hmd2BeforeInitializeParameterInvalid(); +std::int32_t APS5_VABI Hmd2BeforeInitializeReprojectionNotInitialized(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError002C(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1002(); +std::int32_t APS5_VABI Hmd2BeforeInitializeError1003(); +std::int32_t APS5_VABI Hmd2BeforeInitializeMinusOne(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue0400(); +std::int32_t APS5_VABI Hmd2BeforeInitializeValue20000(); } + namespace { alignas(16) std::array process{}; @@ -48,6 +90,95 @@ void TestBeforeInitialize() { Require(sceHmd2Open(USER_SERVICE_INITIAL_USER_ID, 0, 0, nullptr) == Hmd2::NotInitialized, "open before initialization"); Require(sceHmd2Close(0) == Hmd2::NotInitialized, "close before initialization"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate before initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes before initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null before initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values before initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes before initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null before initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null before initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots before initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 before initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 before initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne before initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 before initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 before initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C before initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero before initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present before initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present before initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null before initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present before initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null before initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present before initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null before initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present before initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present before initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null before initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero before initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 before initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull before initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid before initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized before initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C before initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 before initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 before initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne before initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 before initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 before initialization"); Require(sceHmd2SetVibration(0, 0) == 0, "vibration of the unopened handle 0 failed"); } @@ -59,6 +190,95 @@ void TestWithoutHeadset() { Hmd2::DeviceInformation info; Require(sceHmd2GetDeviceInformation(&info) == Hmd2::NotInitialized, "device information without a headset"); Require(sceHmd2Terminate() == Hmd2::NotInitialized, "terminate after the failed initialization"); + { + std::uint8_t out[64]; + std::uint64_t words[2]; + Require(Hmd2WriteStateTwo(nullptr) == Hmd2::ParameterNull, "Hmd2WriteStateTwo null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteStateTwo(out) == 0 && out[0] == 2 && out[1] == 0 && out[4] == 0 && out[5] == 0xaa, "Hmd2WriteStateTwo bytes after the failed initialization"); + Require(Hmd2WriteSize0x2000Align0x100(nullptr, words) == Hmd2::ParameterNull && Hmd2WriteSize0x2000Align0x100(words, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSize0x2000Align0x100 null after the failed initialization"); + words[0] = words[1] = ~0ull; + Require(Hmd2WriteSize0x2000Align0x100(words, words + 1) == 0 && words[0] == 0x2000 && words[1] == 0x100, "Hmd2WriteSize0x2000Align0x100 values after the failed initialization"); + Require(Hmd2WriteFourConstants(nullptr) == Hmd2::ParameterNull, "Hmd2WriteFourConstants null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteFourConstants(out) == 0 && out[0] == 0xe1 && out[15] == 0x3c && out[16] == 0xaa, "Hmd2WriteFourConstants bytes after the failed initialization"); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteSixHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteSixHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[20] == 0x00 && out[23] == 0x3f && out[24] == 0xaa, "Hmd2WriteSixHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, nullptr) == Hmd2::ParameterNull, "Hmd2WriteTwelveHalvesThenInvalidHandle null after the failed initialization"); + std::memset(out, 0xaa, sizeof out); + Require(Hmd2WriteTwelveHalvesThenInvalidHandle(0, out) == Hmd2::InvalidHandle && out[7] == 0x3f && out[31] == 0x3f && out[47] == 0x00 && out[48] == 0xaa, "Hmd2WriteTwelveHalvesThenInvalidHandle bytes after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(9, nullptr, out) == Hmd2::ParameterNull && Hmd2SlotUpTo3CheckSecondThirdThenZero(0, out, nullptr) == Hmd2::ParameterNull, "Hmd2SlotUpTo3CheckSecondThirdThenZero null after the failed initialization"); + Require(Hmd2SlotUpTo3CheckSecondThirdThenZero(3, out, out) == 0 && Hmd2SlotUpTo3CheckSecondThirdThenZero(4, out, out) == Hmd2::ParameterInvalid && Hmd2SlotUpTo3CheckSecondThirdThenZero(~0ull, out, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo3CheckSecondThirdThenZero slots after the failed initialization"); + Require(Hmd2SlotUpTo1ThenError1002(1) == Hmd2::Error1002 && Hmd2SlotUpTo1ThenError1002(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1CheckSecondThenError1002(5, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo1CheckSecondThenError1002(0, out) == Hmd2::Error1002 && Hmd2SlotUpTo1CheckSecondThenError1002(2, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1CheckSecondThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo1ThenMinusOne(1) == -1 && Hmd2SlotUpTo1ThenMinusOne(2) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo1ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenMinusOne(2) == -1 && Hmd2SlotUpTo2ThenMinusOne(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenMinusOne after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1002(2) == Hmd2::Error1002 && Hmd2SlotUpTo2ThenError1002(3) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1002 after the failed initialization"); + Require(Hmd2SlotUpTo2ThenError1003(0) == Hmd2::Error1003 && Hmd2SlotUpTo2ThenError1003(16) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2ThenError1003 after the failed initialization"); + Require(Hmd2SlotUpTo2CheckSecondThenError002C(8, nullptr) == Hmd2::ParameterNull && Hmd2SlotUpTo2CheckSecondThenError002C(2, out) == Hmd2::Error002C && Hmd2SlotUpTo2CheckSecondThenError002C(3, out) == Hmd2::ParameterInvalid, "Hmd2SlotUpTo2CheckSecondThenError002C after the failed initialization"); + Require(Hmd2IndexZeroCheckSecondThenZero(1, nullptr) == Hmd2::ParameterNull && Hmd2IndexZeroCheckSecondThenZero(0, out) == 0 && Hmd2IndexZeroCheckSecondThenZero(1, out) == Hmd2::Error100F, "Hmd2IndexZeroCheckSecondThenZero after the failed initialization"); + } + const std::uint8_t dummyBytes[16]{}; + const void* dummy = dummyBytes; + Require(Hmd2CheckFirstThenError0004(dummy) == Hmd2::Error0004, "Hmd2CheckFirstThenError0004 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError0004(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError0004 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenZero(dummy) == 0, "Hmd2CheckFirstThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenZero(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(dummy) == Hmd2::Error1002, "Hmd2CheckFirstThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1002(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(dummy) == -1, "Hmd2CheckFirstThenMinusOne with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenMinusOne(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenMinusOne with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstTwoThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, dummy) == Hmd2::Error002C, "Hmd2CheckSecondThenError002C with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError002C(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError002C with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(dummy) == Hmd2::ParameterInvalid, "Hmd2CheckFirstThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenParameterInvalid(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenParameterInvalid with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, dummy) == 0, "Hmd2CheckAllSixThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(nullptr, dummy, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, nullptr, dummy, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 1 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, nullptr, dummy, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 3 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 4 null after the failed initialization"); + Require(Hmd2CheckAllSixThenZero(dummy, dummy, dummy, dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckAllSixThenZero with pointer 5 null after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, dummy) == Hmd2::ParameterInvalid, "Hmd2CheckSecondThenParameterInvalid with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenParameterInvalid(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenParameterInvalid with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(dummy) == Hmd2::Error800d0203, "Hmd2CheckFirstThenError800d0203 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError800d0203(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError800d0203 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(dummy) == Hmd2::Error1003, "Hmd2CheckFirstThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1003(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(dummy) == Hmd2::Error1010, "Hmd2CheckFirstThenError1010 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstThenError1010(nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstThenError1010 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckFirstTwoThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstTwoThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(nullptr, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstTwoThenError1003(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstTwoThenError1003 with pointer 1 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, dummy) == 0, "Hmd2CheckFirstAndThirdThenZero with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenZero(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenZero with pointer 2 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, dummy) == Hmd2::Error1003, "Hmd2CheckFirstAndThirdThenError1003 with every pointer present after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(nullptr, dummy, dummy) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 0 null after the failed initialization"); + Require(Hmd2CheckFirstAndThirdThenError1003(dummy, dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckFirstAndThirdThenError1003 with pointer 2 null after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, dummy) == Hmd2::InvalidHandle, "Hmd2CheckSecondThenInvalidHandle with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenInvalidHandle(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenInvalidHandle with pointer 1 null after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, dummy) == Hmd2::Error1002, "Hmd2CheckSecondThenError1002 with every pointer present after the failed initialization"); + Require(Hmd2CheckSecondThenError1002(dummy, nullptr) == Hmd2::ParameterNull, "Hmd2CheckSecondThenError1002 with pointer 1 null after the failed initialization"); + Require(Hmd2BeforeInitializeZero() == 0, "Hmd2BeforeInitializeZero after the failed initialization"); + Require(Hmd2BeforeInitializeNotInitialized() == Hmd2::NotInitialized, "Hmd2BeforeInitializeNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError0004() == Hmd2::Error0004, "Hmd2BeforeInitializeError0004 after the failed initialization"); + Require(Hmd2BeforeInitializeParameterNull() == Hmd2::ParameterNull, "Hmd2BeforeInitializeParameterNull after the failed initialization"); + Require(Hmd2BeforeInitializeParameterInvalid() == Hmd2::ParameterInvalid, "Hmd2BeforeInitializeParameterInvalid after the failed initialization"); + Require(Hmd2BeforeInitializeReprojectionNotInitialized() == Hmd2::ReprojectionNotInitialized, "Hmd2BeforeInitializeReprojectionNotInitialized after the failed initialization"); + Require(Hmd2BeforeInitializeError002C() == Hmd2::Error002C, "Hmd2BeforeInitializeError002C after the failed initialization"); + Require(Hmd2BeforeInitializeError1002() == Hmd2::Error1002, "Hmd2BeforeInitializeError1002 after the failed initialization"); + Require(Hmd2BeforeInitializeError1003() == Hmd2::Error1003, "Hmd2BeforeInitializeError1003 after the failed initialization"); + Require(Hmd2BeforeInitializeMinusOne() == -1, "Hmd2BeforeInitializeMinusOne after the failed initialization"); + Require(Hmd2BeforeInitializeValue0400() == 0x400, "Hmd2BeforeInitializeValue0400 after the failed initialization"); + Require(Hmd2BeforeInitializeValue20000() == 0x20000, "Hmd2BeforeInitializeValue20000 after the failed initialization"); } } @@ -79,27 +299,6 @@ int main() { InstallProcessParameters(); TestBeforeInitialize(); TestWithoutHeadset(); - bool threw = false; - try { - sceHmd2GazeActivateGazeTracking(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "gaze activation did not throw"); - threw = false; - try { - sceHmd2ReprojectionGetCurrentOutput(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "reprojection get current output did not throw"); - threw = false; - try { - sceHmd2SetFieldOfView(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "set field of view did not throw"); std::puts("HMD2 tests passed"); return 0; } diff --git a/core/libs/tests/GuestJpegDec.cpp b/core/libs/tests/GuestJpegDec.cpp index 8dfa4cb137..018dba662d 100644 --- a/core/libs/tests/GuestJpegDec.cpp +++ b/core/libs/tests/GuestJpegDec.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include static_assert(sizeof(JpegDecCreateParam) == 0xC); @@ -140,6 +141,20 @@ int main() { JpegDecDecodeParam unknownScale = decodeParam; unknownScale.down_scale = 3; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &unknownScale, &info); })); + std::vector invalidAlphaImage(7 * 5 * 4, 0xCC); + JpegDecDecodeParam invalidAlpha = DecodeParam(jpeg, invalidAlphaImage, 7 * 4); + invalidAlpha.alpha_value = 0x100; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + invalidAlpha.alpha_value = 0xFFFF; + Require(sceJpegDecDecode(handle, &invalidAlpha, &info) == invalidParam); + Require(std::all_of(invalidAlphaImage.begin(), invalidAlphaImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); + std::vector misalignedImage(7 * 5 * 4 + 1, 0xCC); + JpegDecDecodeParam misaligned = DecodeParam(jpeg, misalignedImage, 7 * 4); + misaligned.image_mem_addr = misalignedImage.data() + 1; + misaligned.image_mem_size = static_cast(misalignedImage.size() - 1); + Require(sceJpegDecDecode(handle, &misaligned, &info) == invalidAddr); + Require(std::all_of(misalignedImage.begin(), misalignedImage.end(), [](std::uint8_t byte) { return byte == 0xCC; })); JpegDecDecodeParam otherMode = decodeParam; otherMode.decode_mode = 1; Require(ThrowsRuntimeError([&] { sceJpegDecDecode(handle, &otherMode, &info); })); diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index b1981c7ee0..1932625356 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -34,6 +34,7 @@ int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); @@ -204,6 +205,19 @@ int main() { Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + KernelEventFlag opened = nullptr; + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + std::uint64_t openedPattern = 0; + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + opened = nullptr; + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); std::uint64_t pattern = 0; Require(sceKernelPollEventFlag(ef, 0, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 339c3c4697..b75d3c22a9 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -28,6 +28,7 @@ int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } @@ -39,12 +40,14 @@ static constexpr int SetFlags = 4; static constexpr int OldGetLock = 7; static constexpr int OldSetLock = 8; static constexpr int OldSetLockWait = 9; +static constexpr int Duplicate2Command = 10; static constexpr int GetLock = 11; static constexpr int SetLock = 12; static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; +static constexpr int Duplicate2CloseOnExec = 18; static constexpr int ReadOnly = 0; static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; @@ -118,6 +121,66 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == (ReadWrite | NonBlocking)); Require(sceKernelFcntl(socket, SetFlags, 0) == 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + GuestLock lock{}; + Require(sceKernelFcntl(socket, GetLock, &lock) == SceEinval); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 1) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + const int duplicate = sceKernelFcntl(socket, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(sceKernelFcntl(duplicate, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetFlags, ReadWrite | NonBlocking) == 0); + Require(sceKernelFcntl(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + Require(sceKernelFcntl(duplicate, SetFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); + const int closing = sceKernelFcntl(socket, DuplicateCloseOnExec, duplicate + 1); + Require(closing >= duplicate + 1); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, closing) == closing); + Require(sceKernelFcntl(closing, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, Duplicate2Command, socket) == socket); + Require(sceKernelFcntl(socket, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(socket, SetDescriptorFlags, 0) == 0); + int pipe[2] = {-1, -1}; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, pipe[0]) == pipe[0]); + Require(sceKernelFcntl(pipe[0], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[0], GetDescriptorFlags) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, pipe[1]) == pipe[1]); + Require(sceKernelFcntl(pipe[1], GetFlags) == ReadWrite); + Require(sceKernelFcntl(pipe[1], GetDescriptorFlags) == 1); + Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + const int random = sceKernelOpen("/dev/urandom", ReadOnly, 0); + Require(random >= 0); + Require(fcntl_nid_postfix(socket, Duplicate2Command, random) == random); + Require(sceKernelFcntl(random, GetFlags) == ReadWrite); + Require(close_nid_postfix(random) == 0); + const int file = sceKernelOpen("kernel_fcntl_socket_dup2.bin", CreateReadWrite, 0644); + Require(file == random); + char byte = 0; + Require(sceKernelRead(file, &byte, 1) == 0); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, file) == file); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 1); + Require(close_nid_postfix(file) == 0); + std::remove("kernel_fcntl_socket_dup2.bin"); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCommand, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, DuplicateCloseOnExec, -1), GuestEinval); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2Command, -1), GuestEbadf); + RequireFailure(fcntl_nid_postfix(socket, Duplicate2CloseOnExec, -1), GuestEbadf); + Require(sceKernelFcntl(socket, DuplicateCommand, -1) == SceEinval); + Require(sceKernelFcntl(socket, DuplicateCloseOnExec, -1) == SceEinval); + Require(sceKernelFcntl(socket, Duplicate2Command, -1) == SceEbadf); + Require(sceKernelFcntl(socket, Duplicate2CloseOnExec, -1) == SceEbadf); Require(sceKernelFcntl(socket, SetFlags, 0x8000) == SceEnotsup); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); @@ -127,7 +190,7 @@ static void SocketsKeepTheirPath() { RequireMalformedRequestsFail(socket); RequireFailure(ioctl_nid_postfix(socket, IntegerArgumentRequest, nullptr), GuestEfault); RequireFailure(ioctl_nid_postfix(socket, EmptyInputRequest, nullptr), GuestEfault); - Require(close_nid_postfix(socket) == 0); + for (const int descriptor : {closing, duplicate, socket}) Require(close_nid_postfix(descriptor) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index ea71a5644b..a60abca020 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -298,8 +298,16 @@ int main() { Require(readlink_nid_postfix(file.c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix(root.string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlink_nid_postfix((root / "file.txt" / "child").string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlink_nid_postfix("", buffer, sizeof(buffer)) == -1 && Error() == 2); Require(readlink_nid_postfix(nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); + constexpr auto hugeSize = static_cast(-1); + Require(readlink_nid_postfix(nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix("", buffer, hugeSize) == -1 && Error() == 22); + Require(readlink_nid_postfix((root / "missing").string().c_str(), buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, "", buffer, hugeSize) == -1 && Error() == 22); + Require(readlinkat_nid_postfix(-100, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); #ifndef _WIN32 Require(readlink_nid_postfix((root / std::string(300, 'n')).string().c_str(), buffer, sizeof(buffer)) == -1 && Error() == 63); #endif @@ -497,6 +505,7 @@ int main() { Require(openat_nid_postfix(plain, "file.txt", SCE_KERNEL_O_RDONLY) == -1 && Error() == 20); Require(readlinkat_nid_postfix(base, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 22); Require(readlinkat_nid_postfix(base, "missing.bin", buffer, sizeof(buffer)) == -1 && Error() == 2); + Require(readlinkat_nid_postfix(base, "file.txt/child", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(plain, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 20); Require(readlinkat_nid_postfix(-1, "file.txt", buffer, sizeof(buffer)) == -1 && Error() == 9); Require(readlinkat_nid_postfix(base, nullptr, buffer, sizeof(buffer)) == -1 && Error() == 14); diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index aa199d6bd0..2ff23394f4 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -386,7 +386,14 @@ static void LowWaterMarks() { Require(Poll(kq, events, 4) == 0); Require(Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd | EvOneshot, NoteLowat, 1)) == 0); Require(Wait(kq, events, 4) == 1 && events[0].filter == EvfiltWrite); - Require(Throws([kq, &pair] { Apply(kq, Change(static_cast(pair[1]), EvfiltWrite, EvAdd, NoteLowat, 2)); })); + const auto writer = static_cast(pair[1]); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Wait(kq, events, 4) == 1 && events[0].ident == writer && events[0].filter == EvfiltWrite && events[0].data >= 2); + const auto freeSpace = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].ident == writer && events[0].data == freeSpace); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, freeSpace + 1)) == 0); + Require(Poll(kq, events, 4) == 0); Require(Throws([kq, reader] { Apply(kq, Change(reader, EvfiltRead, EvAdd, NoteLowat, 0)); })); int datagrams[2]; Require(socketpair_nid_postfix(Unix, 2, 0, datagrams) == 0); @@ -503,6 +510,49 @@ static void EdgeTriggeredSocketWrite() { Require(close_nid_postfix(kq) == 0); } +static void SocketWriteLowWaterWhileFilling() { + const int kq = kqueue_nid_postfix(); + int pair[2]; + Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0); + const auto writer = static_cast(pair[1]); + KernelEvent events[4]{}; + Require(fcntl_nid_postfix(pair[1], SetStatusFlags, NonBlocking) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 1); + const auto empty = events[0].data; + const std::string chunk(4096, 'x'); + std::int64_t queued = 0; + Require(send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0) == static_cast(chunk.size())); + queued += static_cast(chunk.size()); + Require(Poll(kq, events, 4) == 1 && events[0].data > 0 && events[0].data < empty); + const auto partial = events[0].data; + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial)) == 0); + Require(Poll(kq, events, 4) == 1 && events[0].data == partial); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, partial + 1)) == 0); + Require(Poll(kq, events, 4) == 0); + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, empty)) == 0); + Require(Poll(kq, events, 4) == 0); + for (;;) { + const std::int64_t written = send_nid_postfix(pair[1], chunk.data(), chunk.size(), 0); + if (written < 0) { + Require(*__error_nid_postfix() == GuestEagain); + break; + } + queued += written; + } + Require(Apply(kq, Change(writer, EvfiltWrite, EvAdd, NoteLowat, 2)) == 0); + Require(Poll(kq, events, 4) == 0); + std::string drained(chunk.size(), '\0'); + while (queued > 0) { + const std::int64_t received = recv_nid_postfix(pair[0], drained.data(), drained.size(), 0); + Require(received > 0); + queued -= received; + } + Require(Poll(kq, events, 4) == 1 && events[0].data == empty); + Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0); + Require(close_nid_postfix(kq) == 0); +} + static void EdgeTriggeredPipeRead() { const int kq = kqueue_nid_postfix(); int pipe[2]; @@ -966,6 +1016,7 @@ int main() { #ifdef __linux__ EdgeTriggeredSocketRead(); EdgeTriggeredSocketWrite(); + SocketWriteLowWaterWhileFilling(); EdgeTriggeredPipeRead(); EdgeTriggeredDispatch(); #endif diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 0aabfe21ce..ac1015b41f 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -17,6 +17,23 @@ void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); int APS5_VABI _Cnd_wait_nid_postfix(void** condition, void** mutex); int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); int APS5_VABI _Cnd_signal_nid_postfix(void** handle); + +struct GuestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; +}; + +void APS5_VABI _ZNSt4_PadC2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_PadD2Ev_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad8_ReleaseEv_nid_postfix(GuestPad* pad); +void APS5_VABI _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(GuestPad* pad, void** thread); +int APS5_VABI _Thrd_join_nid_postfix(void* thread, int* code); +int APS5_VABI _Thrd_detach_nid_postfix(void* thread); +void RegisterPthreadCreate_nid_no_patch(int (APS5_VABI* fn)(void**, const void*, void* (APS5_VABI*)(void*), void*)); +void RegisterPthreadJoin_nid_no_patch(int (APS5_VABI* fn)(void*, void**)); +void RegisterPthreadDetach_nid_no_patch(int (APS5_VABI* fn)(void*)); } static void Require(bool value) { if (!value) std::abort(); } @@ -183,10 +200,111 @@ static void ConditionSignal() { _Cnd_destroy_nid_postfix(&condition); } +struct TestPad { + unsigned (APS5_VABI* const* vtable)(GuestPad*); + void* condition; + void* mutex; + bool started; + std::atomic ran{false}; +}; + +struct HostPadThread { + std::thread worker; + void* result = nullptr; +}; + +static int APS5_VABI HostPthreadCreate(void** thread, const void*, void* (APS5_VABI* entry)(void*), void* arg) { + auto* handle = new HostPadThread{}; + handle->worker = std::thread([handle, entry, arg] { handle->result = entry(arg); }); + *thread = handle; + return 0; +} + +static int APS5_VABI HostPthreadJoin(void* thread, void** value) { + if (!thread) return 22; + auto* handle = static_cast(thread); + handle->worker.join(); + if (value) *value = handle->result; + delete handle; + return 0; +} + +static int APS5_VABI HostPthreadDetach(void* thread) { + if (!thread) return 22; + static_cast(thread)->worker.detach(); + return 0; +} + +static unsigned APS5_VABI TestPadGo(GuestPad* base) { + auto* pad = reinterpret_cast(base); + pad->ran.store(true); + _ZNSt4_Pad8_ReleaseEv_nid_postfix(base); + return 42; +} + +static void PadLaunchAndJoin() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + TestPad unregPad{}; + unregPad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&unregPad)); + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); + void* unregThread = nullptr; + RequireThrows([&] { + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&unregPad), &unregThread); + }); + RequireThrows([&] { + int code = 0; + _Thrd_join_nid_postfix(&unregPad, &code); + }); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&unregPad)); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadJoin_nid_no_patch(HostPthreadJoin); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.started && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + int code = 0; + Require(_Thrd_join_nid_postfix(thread, &code) == success && code == 42); + Require(_Thrd_join_nid_postfix(nullptr, &code) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadJoin_nid_no_patch(nullptr); +} + +static void PadDetach() { + unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; + RegisterPthreadDetach_nid_no_patch(nullptr); + RequireThrows([] { _Thrd_detach_nid_postfix(nullptr); }); + + RegisterPthreadCreate_nid_no_patch(HostPthreadCreate); + RegisterPthreadDetach_nid_no_patch(HostPthreadDetach); + + TestPad pad{}; + pad.vtable = vtable; + _ZNSt4_PadC2Ev_nid_postfix(reinterpret_cast(&pad)); + void* thread = nullptr; + _ZNSt4_Pad7_LaunchEPP7pthread_nid_postfix(reinterpret_cast(&pad), &thread); + Require(thread != nullptr && pad.ran.load()); + _ZNSt4_PadD2Ev_nid_postfix(reinterpret_cast(&pad)); + Require(_Thrd_detach_nid_postfix(thread) == success); + Require(_Thrd_detach_nid_postfix(nullptr) == 4); + + RegisterPthreadCreate_nid_no_patch(nullptr); + RegisterPthreadDetach_nid_no_patch(nullptr); +} + int main() { MutexBasics(); RecursiveMutex(); MutualExclusion(); ConditionBroadcast(); ConditionSignal(); + PadLaunchAndJoin(); + PadDetach(); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 3edad3eb4e..b027130f96 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -57,6 +59,18 @@ long double APS5_VABI frexpl_nid_postfix(long double, int*); struct LibcFloatConstant { std::uint32_t bits[4]; }; extern LibcFloatConstant _FInf_nid_postfix; extern LibcFloatConstant _FNan_nid_postfix; +extern LibcFloatConstant _FDenorm_nid_postfix; +extern LibcFloatConstant _FSnan_nid_postfix; +union SonyDconst { unsigned short _Word[8]; float _Float; double _Double; long double _Long_double; }; +extern SonyDconst _Inf_nid_postfix; +extern SonyDconst _Denorm_nid_postfix; +extern SonyDconst _Hugeval_nid_postfix; +extern SonyDconst _Nan_nid_postfix; +extern SonyDconst _Snan_nid_postfix; +extern SonyDconst _LDenorm_nid_postfix; +extern SonyDconst _LInf_nid_postfix; +extern SonyDconst _LNan_nid_postfix; +extern SonyDconst _LSnan_nid_postfix; short APS5_VABI _FDtest_nid_postfix(const float*); short APS5_VABI _Dtest_nid_postfix(const double*); int APS5_VABI __fpclassifyf_nid_postfix(float); @@ -116,6 +130,8 @@ double APS5_VABI sinh_nid_postfix(double); double APS5_VABI cosh_nid_postfix(double); float APS5_VABI coshf_nid_postfix(float); float APS5_VABI sinhf_nid_postfix(float); +float APS5_VABI _FSinh_nid_postfix(float, float); +float APS5_VABI _FCosh_nid_postfix(float, float); double APS5_VABI asinh_nid_postfix(double); float APS5_VABI asinhf_nid_postfix(float); double APS5_VABI acosh_nid_postfix(double); @@ -141,6 +157,7 @@ std::int64_t APS5_VABI llabs_nid_postfix(std::int64_t); std::int64_t APS5_VABI imaxabs_nid_postfix(std::int64_t); long double APS5_VABI scalbnl_nid_postfix(long double, int); float APS5_VABI significandf_nid_postfix(float); +double APS5_VABI significand_nid_postfix(double); long double APS5_VABI acoshl_nid_postfix(long double); long double APS5_VABI asinhl_nid_postfix(long double); long double APS5_VABI atanhl_nid_postfix(long double); @@ -184,6 +201,20 @@ long double APS5_VABI truncl_nid_postfix(long double); long double APS5_VABI nearbyintl_nid_postfix(long double); double APS5_VABI logb_nid_postfix(double); double APS5_VABI remainder_nid_postfix(double, double); +struct DinkumwareFloatCharacteristics { + int digits; + int mantissaDigits; + int maxExponent10; + int maxExponent; + int minExponent10; + int minExponent; + alignas(16) unsigned char epsilon[16]; + alignas(16) unsigned char max[16]; + alignas(16) unsigned char min[16]; +}; +extern DinkumwareFloatCharacteristics _Flt_nid_postfix; +extern DinkumwareFloatCharacteristics _Dbl_nid_postfix; +extern DinkumwareFloatCharacteristics _Ldbl_nid_postfix; } static void Require(bool value) { if (!value) std::abort(); } @@ -449,6 +480,22 @@ static void CheckNanConversions() { } } +static void CheckFloatUnderflowErrno() { + char* end = nullptr; + errno = 7; + Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); + Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); + Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); + Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtof_nid_postfix("1e-46", &end) == 0.0f && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -791,6 +838,98 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckScaledHyperbolic() { + const int rounding = std::fegetround(); + Require(std::fesetround(FE_TONEAREST) == 0); + unsigned checks = 0; + for (bool sine : {true, false}) { + const auto function = sine ? _FSinh_nid_postfix : _FCosh_nid_postfix; + const char* name = sine ? "_FSinh" : "_FCosh"; + const auto checkValue = [&](float x, float y) { + const long double value = sine ? std::sinh(static_cast(x)) : std::cosh(static_cast(x)); + const float expected = static_cast(value * static_cast(y)); + std::feclearexcept(FE_ALL_EXCEPT); + errno = 12345; + const float result = function(x, y); + const auto actualBits = std::bit_cast(result); + const auto expectedBits = std::bit_cast(expected); + const auto distance = actualBits > expectedBits ? actualBits - expectedBits : expectedBits - actualBits; + const bool exact = expected == 0.0f || std::isinf(expected); + if ((exact ? actualBits != expectedBits : !std::isfinite(result) || distance > 2) || + (std::isfinite(expected) && (std::fetestexcept(FE_OVERFLOW | FE_INVALID) != 0 || errno != 12345))) { + std::fprintf(stderr, "%s(%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), actualBits, expectedBits, errno, std::fetestexcept(FE_ALL_EXCEPT)); + std::abort(); + } + ++checks; + }; + checkValue(90.0f, 1e-20f); + checkValue(100.0f, 1e-40f); + const float magnitudes[] = {0.0f, 0x1p-126f, 0.001f, 0.5f, 1.0f, 4.0f, 7.0f, 8.0f, 10.0f, 20.0f, 50.0f, 88.0f, 90.0f, 100.0f, 150.0f, 180.0f, 190.0f, 193.0f}; + const float scales[] = {0.0f, 0x1p-149f, 0x1p-140f, 0x1p-126f, 1e-20f, 0.5f, 1.0f, 1e20f, std::numeric_limits::max()}; + for (float magnitude : magnitudes) { + for (float scale : scales) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + } + } + for (float scale : scales) { + if (scale == 0.0f) continue; + for (long double target : {static_cast(std::numeric_limits::max()), 0x1p-150L}) { + if (!sine && target < scale) continue; + const long double ratio = target / static_cast(scale); + const float boundary = static_cast(sine ? std::asinh(ratio) : std::acosh(ratio)); + float magnitude = std::nextafter(std::nextafter(boundary, -INFINITY), -INFINITY); + for (unsigned neighbor = 0; neighbor != 5; ++neighbor) { + for (float xSign : {1.0f, -1.0f}) { + for (float ySign : {1.0f, -1.0f}) checkValue(magnitude * xSign, scale * ySign); + } + magnitude = std::nextafter(magnitude, INFINITY); + } + } + } + const auto checkSpecial = [&](float x, float y, std::uint32_t expectedBits, int error, int flags) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(FE_DIVBYZERO); + errno = 12345; + const float result = function(x, y); + const int raised = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast(result) != expectedBits || errno != error || raised != (FE_DIVBYZERO | flags)) { + std::fprintf(stderr, "%s special (%a, %a): got %08x, expected %08x, errno %d, flags %x\n", + name, static_cast(x), static_cast(y), std::bit_cast(result), expectedBits, errno, raised); + std::abort(); + } + ++checks; + }; + const float infinity = std::numeric_limits::infinity(); + for (float sign : {1.0f, -1.0f}) { + checkSpecial(sign * infinity, 0.0f, 0x7fc00000u, EDOM, FE_INVALID); + checkSpecial(sign * 100.0f, 0.0f, sine && sign < 0 ? 0x80000000u : 0u, 12345, 0); + checkSpecial(sign * 100.0f, -0.0f, sine && sign < 0 ? 0u : 0x80000000u, 12345, 0); + checkSpecial(sign * 1.0f, infinity, sine && sign < 0 ? 0xff800000u : 0x7f800000u, 12345, 0); + } + for (std::uint32_t bits : {0x7fc12345u, 0xffc54321u, 0x7fa12345u, 0xffa54321u}) { + const float nan = std::bit_cast(bits); + checkSpecial(nan, 0.0f, bits, 12345, 0); + checkSpecial(infinity, nan, bits, 12345, 0); + checkSpecial(nan, std::bit_cast(0x7fc99999u), bits, 12345, 0); + } + checkSpecial(0.0f, infinity, sine ? 0x7fc00000u : 0x7f800000u, sine ? EDOM : 12345, sine ? FE_INVALID : 0); + checkSpecial(1.0f, std::numeric_limits::max(), 0x7f800000u, 12345, FE_OVERFLOW | FE_INEXACT); + checkSpecial(100.0f, 1.0f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + for (int mode : {FE_TONEAREST, FE_DOWNWARD, FE_UPWARD, FE_TOWARDZERO}) { + Require(std::fesetround(mode) == 0); + checkSpecial(std::numeric_limits::max(), 0x1p-149f, 0x7f800000u, ERANGE, FE_OVERFLOW | FE_INEXACT); + } + Require(std::fesetround(FE_TONEAREST) == 0); + if (sine) checkSpecial(0x1p-126f, 0x1p-149f, 0u, 12345, FE_UNDERFLOW | FE_INEXACT); + } + std::feclearexcept(FE_ALL_EXCEPT); + Require(std::fesetround(rounding) == 0); + std::printf("Scaled hyperbolic checks: %u\n", checks); +} + static void CheckSinhf() { const float infinity = std::numeric_limits::infinity(); const struct { std::uint32_t input; std::uint32_t output; } cases[] = { @@ -881,6 +1020,36 @@ static void CheckFloatClassification() { Require(_FDtest_nid_postfix(reinterpret_cast(&_FNan_nid_postfix)) == 2); } +static void RequireBytes(const void* constant, std::initializer_list expected) { + unsigned char bytes[16]; + std::memcpy(bytes, constant, sizeof(bytes)); + int index = 0; + for (const unsigned char byte : expected) Require(bytes[index++] == byte); + while (index < 16) Require(bytes[index++] == 0); +} + +static void CheckDinkumwareConstants() { + RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Denorm_nid_postfix, {1}); + RequireBytes(&_Hugeval_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_Nan_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf8, 0x7f}); + RequireBytes(&_Snan_nid_postfix, {1, 0, 0, 0, 0, 0, 0xf0, 0x7f}); + RequireBytes(&_FDenorm_nid_postfix, {1}); + RequireBytes(&_FSnan_nid_postfix, {1, 0, 0x80, 0x7f}); + RequireBytes(&_LDenorm_nid_postfix, {1}); + RequireBytes(&_LInf_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + RequireBytes(&_LNan_nid_postfix, {0, 0, 0, 0, 0, 0, 0, 0xc0, 0xff, 0x7f}); + RequireBytes(&_LSnan_nid_postfix, {1, 0, 0, 0, 0, 0, 0, 0x80, 0xff, 0x7f}); + Require(_Dtest_nid_postfix(&_Denorm_nid_postfix._Double) == -2 && _Denorm_nid_postfix._Double == std::numeric_limits::denorm_min()); + Require(_Dtest_nid_postfix(&_Hugeval_nid_postfix._Double) == 1 && _Hugeval_nid_postfix._Double == HUGE_VAL); + Require(_Dtest_nid_postfix(&_Nan_nid_postfix._Double) == 2 && _Dtest_nid_postfix(&_Snan_nid_postfix._Double) == 2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FDenorm_nid_postfix)) == -2); + Require(_FDtest_nid_postfix(reinterpret_cast(&_FSnan_nid_postfix)) == 2); + Require(_LDenorm_nid_postfix._Long_double == std::numeric_limits::denorm_min()); + Require(_LInf_nid_postfix._Long_double == std::numeric_limits::infinity()); + Require(std::isnan(_LNan_nid_postfix._Long_double) && std::isnan(_LSnan_nid_postfix._Long_double)); +} + static void CheckMissingClassificationExports() { const float infinity = std::numeric_limits::infinity(); const float nan = std::numeric_limits::quiet_NaN(); @@ -936,6 +1105,11 @@ static void CheckScaling() { Require(std::signbit(significandf_nid_postfix(-0.0f)) && significandf_nid_postfix(-0.0f) == 0.0f); Require(significandf_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(significandf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(significand_nid_postfix(12.0) == 1.5 && significand_nid_postfix(-0.375) == -1.5); + Require(significand_nid_postfix(0x1p-1074) == 1.0); + Require(std::signbit(significand_nid_postfix(-0.0)) && significand_nid_postfix(-0.0) == 0.0); + Require(significand_nid_postfix(std::numeric_limits::infinity()) == std::numeric_limits::infinity()); + Require(std::isnan(significand_nid_postfix(std::numeric_limits::quiet_NaN()))); } static bool Near(long double actual, long double expected) { @@ -1072,8 +1246,35 @@ static void CheckNoConversionIsPositiveZero() { Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); } +template +static void RequireCharacteristic(const unsigned char (&constant)[16], TValue expected) { + TValue value; + std::memcpy(&value, constant, sizeof(value)); + Require(value == expected); + for (std::size_t index = TBytes; index < 16; ++index) Require(constant[index] == 0); +} + +template +static void RequireCharacteristics(const DinkumwareFloatCharacteristics& table, const int (&integers)[6]) { + const int fields[] = {table.digits, table.mantissaDigits, table.maxExponent10, table.maxExponent, table.minExponent10, table.minExponent}; + for (int index = 0; index < 6; ++index) Require(fields[index] == integers[index]); + RequireCharacteristic(table.epsilon, std::numeric_limits::epsilon()); + RequireCharacteristic(table.max, std::numeric_limits::max()); + RequireCharacteristic(table.min, std::numeric_limits::min()); +} + +static void CheckDinkumwareFloatCharacteristics() { + static_assert(sizeof(DinkumwareFloatCharacteristics) == 80 && offsetof(DinkumwareFloatCharacteristics, epsilon) == 32); + RequireCharacteristics(_Flt_nid_postfix, {FLT_DIG, FLT_MANT_DIG, FLT_MAX_10_EXP, FLT_MAX_EXP, FLT_MIN_10_EXP, FLT_MIN_EXP}); + RequireCharacteristics(_Dbl_nid_postfix, {DBL_DIG, DBL_MANT_DIG, DBL_MAX_10_EXP, DBL_MAX_EXP, DBL_MIN_10_EXP, DBL_MIN_EXP}); + RequireCharacteristics(_Ldbl_nid_postfix, {18, 64, 4932, 16384, -4931, -16381}); + for (const auto* table : {&_Flt_nid_postfix, &_Dbl_nid_postfix, &_Ldbl_nid_postfix}) + for (std::size_t index = 24; index < 32; ++index) Require(reinterpret_cast(table)[index] == 0); +} + int main() { CheckFloatClassification(); + CheckDinkumwareConstants(); CheckNoConversionIsPositiveZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); @@ -1355,12 +1556,15 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); CheckIntegerAbsAndDivision(); CheckScaling(); CheckLongDoubleFunctions(); + CheckDinkumwareFloatCharacteristics(); } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 04fe239d47..effc215e0e 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -208,12 +208,83 @@ static void CheckAudioCoprocessorProtection() { static void CheckMprotectIgnoresUndefinedBits() { constexpr std::size_t length = 0x4000; + const auto check = [&](void* mapping, int protection) { + Require(sceKernelMprotect(mapping, length, protection) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + int expected = protection & 0x3f7; + if ((expected & 0x2) != 0) expected |= 0x1; + if ((expected & 0x80) != 0) expected |= 0x40; + if ((expected & 0x200) != 0) expected |= 0x100; + if (info.protection != expected) + std::fprintf(stderr, "Memory protection 0x%x reported 0x%x, expected 0x%x\n", static_cast(protection), static_cast(info.protection), static_cast(expected)); + Require(info.protection == expected); + auto* bytes = static_cast(mapping); + if ((protection & 0x222) != 0) { + bytes[0] = 5; + bytes[length - 1] = 7; + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } else if ((protection & 0x117) != 0) { + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + }; void* mapping = nullptr; Require(sceKernelMapFlexibleMemory(&mapping, length, 3, 0) == 0); - Require(sceKernelMprotect(mapping, length, static_cast(0xfffffff2u)) == 0); - static_cast(mapping)[length - 1] = 5; - Require(static_cast(mapping)[length - 1] == 5); + for (int byte = 0x80; byte <= 0xff; ++byte) check(mapping, byte - 0x100); + for (const int protection : {0, 1, 2, 3, 4, 0x10, 0x20, 0x40, 0x80, 0x100, 0x200, 0x3f7}) { + for (const auto undefined : {0u, 8u, 0x400u, 0x80000000u, 0xfffffc08u}) + check(mapping, static_cast(static_cast(protection) | undefined)); + } Require(sceKernelMunmap(mapping, length) == 0); + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 0xf2, 0, physical, 0) == 0); + check(mapping, -14); + check(mapping, 0x400); + check(mapping, 0x100); + check(mapping, 0x200); + check(mapping, 0x3f7); + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); +} + +static void CheckStrictMemoryProtectionBits() { + constexpr std::size_t length = 0x4000; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, length, 0, 0, &physical) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, length, 3, 0, physical, 0) == 0); + auto* bytes = static_cast(mapping); + bytes[0] = 5; + bytes[length - 1] = 7; + const auto rejected = [&](auto operation) { + bool threw = false; + try { operation(); } catch (const std::invalid_argument&) { threw = true; } + Require(threw); + }; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0); + for (const int protection : {-256, -129, -128, -14, -1, 8, 0x400, std::numeric_limits::min()}) { + void* invalid = nullptr; + rejected([&] { sceKernelMapFlexibleMemory(&invalid, length, protection, 0); }); + Require(invalid == nullptr); + std::size_t after = 0; + Require(sceKernelAvailableFlexibleMemorySize(&after) == 0 && after == available); + rejected([&] { sceKernelMapDirectMemory(&invalid, length, protection, 0, physical, 0); }); + Require(invalid == nullptr); + rejected([&] { sceKernelMtypeprotect(mapping, length, 2, protection); }); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapping, 0, &info, sizeof(info)) == 0); + Require(info.protection == 3 && info.memory_type == 0); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(physical, &type, &start, &end) == 0 && type == 0); + Require(bytes[0] == 5 && bytes[length - 1] == 7); + } + Require(sceKernelMunmap(mapping, length) == 0); + Require(sceKernelReleaseDirectMemory(physical, length) == 0); } static void CheckInternalNamedFlexibleMapping() { @@ -484,6 +555,50 @@ static void CheckSparseDirectMemoryBacking() { Require(sceKernelReleaseDirectMemory(physical, size) == 0); } +static void CheckSavedDirectMemorySurvivesCachePressure() { +#ifdef _WIN32 + constexpr std::size_t bytes = 0x200000; + std::array retained{}; + for (std::size_t index = 0; index < retained.size(); ++index) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &retained[index]) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + std::memset(mapped, static_cast(index + 1), bytes); + Require(sceKernelMunmap(mapped, bytes) == 0); + } + const auto temporary = [] { + constexpr std::size_t small = 0x10000; + for (unsigned char round = 1; round <= 24; ++round) { + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, small, 0, 0, &physical) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + auto* data = static_cast(mapped); + Require(std::all_of(data, data + small, [](unsigned char value) { return value == 0; })); + data[7] = round; + data[small - 1] = static_cast(round + 0x40); + Require(sceKernelMunmap(mapped, small) == 0); + mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, small, 3, 0, physical, 0) == 0); + data = static_cast(mapped); + for (std::size_t at = 0; at < small; ++at) Require(data[at] == (at == 7 ? round : at == small - 1 ? static_cast(round + 0x40) : 0)); + Require(sceKernelMunmap(mapped, small) == 0); + Require(sceKernelReleaseDirectMemory(physical, small) == 0); + } + }; + temporary(); + for (std::size_t index = 0; index < retained.size(); ++index) { + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, bytes, 3, 0, retained[index], bytes) == 0); + const auto* data = static_cast(mapped); + Require(std::all_of(data, data + bytes, [&](unsigned char value) { return value == index + 1; })); + Require(sceKernelMunmap(mapped, bytes) == 0); + Require(sceKernelReleaseDirectMemory(retained[index], bytes) == 0); + } + temporary(); +#endif +} + static void CheckReleaseDirectMemoryClearsMappings() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -2899,8 +3014,10 @@ int main(int argc, char** argv) { CheckCheckedReleaseDirectMemory(); CheckAudioCoprocessorProtection(); CheckMprotectIgnoresUndefinedBits(); + CheckStrictMemoryProtectionBits(); CheckDirectMemoryFollowsPhysicalPages(); CheckSparseDirectMemoryBacking(); + CheckSavedDirectMemorySurvivesCachePressure(); CheckReleaseDirectMemoryClearsMappings(); CheckReleaseDirectMemoryRejectsInvalidRanges(); CheckFixedMappingReplacesPartialOverlap(); diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5ee4c2a871..a74159ef72 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -37,6 +37,7 @@ int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); int APS5_VABI sceNetCtlGetStateV6(int*); +int APS5_VABI sceNetCtlGetInfoV6(int, void*); int APS5_VABI sceNetCtlGetResult(int, int*); #ifdef __APPLE__ std::size_t NetCtlDefaultRoutes_nid_no_patch(const char*, std::size_t, NetCtlDefaultRoute*, std::size_t); @@ -275,6 +276,12 @@ int main() { Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(14, nullptr) == kErrInvalidAddr); + std::uint8_t infoV6[256] = {}; + Require(sceNetCtlGetInfoV6(0, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(6, infoV6) == kErrInvalidCode); + Require(sceNetCtlGetInfoV6(1, nullptr) == kErrInvalidAddr); + if (stateV6 != kStateIpObtained) Require(sceNetCtlGetInfoV6(1, infoV6) == kErrNotConnected); + const int result = sceNetCtlGetInfo(14, &info); if (result == kErrNotConnected) { std::puts("skipped, the host has no IPv4 address"); diff --git a/core/libs/tests/GuestNpTus.cpp b/core/libs/tests/GuestNpTus.cpp index f2376a4113..4f8afcc04c 100644 --- a/core/libs/tests/GuestNpTus.cpp +++ b/core/libs/tests/GuestNpTus.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include extern "C" { int APS5_VABI sceNpTusCreateNpTitleCtxA(std::uint32_t serviceLabel, int userId); +int APS5_VABI sceNpTusDeleteNpTitleCtx(int titleCtxId); int APS5_VABI sceNpTusCreateRequest(int titleCtxId); int APS5_VABI sceNpTusDeleteRequest(int reqId); int APS5_VABI sceNpTusAbortRequest(int reqId); @@ -16,6 +19,7 @@ int APS5_VABI sceNpTusAddAndGetVariableAAsync(int reqId, std::uint64_t targetAcc static constexpr int SIGNED_OUT = static_cast(0x80550006); static constexpr int INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int TOO_MANY_OBJECTS = static_cast(0x80550706); static constexpr int ABORTED = static_cast(0x80550707); static constexpr int INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); static constexpr int INVALID_ID = static_cast(0x8055070E); @@ -70,4 +74,53 @@ int main() { Require(sceNpTusAbortRequest(request) == INVALID_ID); Require(sceNpTusPollAsync(request, &result) == INVALID_ID); Require(sceNpTusSetMultiSlotVariableAAsync(request, 0, slots, values, 2, nullptr) == INVALID_ID); + + Require(sceNpTusDeleteNpTitleCtx(0) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(-1) == INVALID_ID); + Require(sceNpTusDeleteNpTitleCtx(context + 1000) == INVALID_ID); + const int siblingContext = sceNpTusCreateNpTitleCtxA(0, 1); + const int siblingRequest = sceNpTusCreateRequest(siblingContext); + Require(siblingContext > 0 && siblingRequest > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(siblingRequest, 0, slots, values, 2, nullptr) == 0); + const int idle = sceNpTusCreateRequest(context); + const int finished = sceNpTusCreateRequest(context); + const int aborted = sceNpTusCreateRequest(context); + Require(idle > 0 && finished > 0 && aborted > 0); + Require(sceNpTusSetMultiSlotVariableAAsync(finished, 0, slots, values, 2, nullptr) == 0); + Require(sceNpTusAbortRequest(aborted) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == 0); + Require(sceNpTusDeleteNpTitleCtx(context) == INVALID_ID); + Require(sceNpTusCreateRequest(context) == INVALID_ID); + for (const int child : {idle, finished, aborted}) { + Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + Require(sceNpTusDeleteRequest(child) == INVALID_ID); + Require(sceNpTusAbortRequest(child) == INVALID_ID); + Require(sceNpTusSetMultiSlotVariableAAsync(child, 0, slots, values, 2, nullptr) == INVALID_ID); + } + RequireFinished(siblingRequest, SIGNED_OUT); + Require(sceNpTusDeleteNpTitleCtx(siblingContext) == 0); + + std::array contexts{}; + for (int& titleContext : contexts) { + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0); + } + Require(sceNpTusCreateNpTitleCtxA(0, 1) == TOO_MANY_OBJECTS); + for (int& titleContext : contexts) { + const int previous = titleContext; + Require(sceNpTusDeleteNpTitleCtx(previous) == 0); + titleContext = sceNpTusCreateNpTitleCtxA(0, 1); + Require(titleContext > 0 && titleContext != previous); + Require(sceNpTusCreateRequest(previous) == INVALID_ID); + } + std::array children{}; + for (int& child : children) { + child = sceNpTusCreateRequest(contexts[0]); + Require(child > 0); + } + Require(sceNpTusCreateRequest(contexts[1]) == TOO_MANY_OBJECTS); + Require(sceNpTusDeleteNpTitleCtx(contexts[0]) == 0); + Require(sceNpTusCreateRequest(contexts[1]) > 0); + for (const int child : children) Require(sceNpTusPollAsync(child, &result) == INVALID_ID); + for (std::size_t index = 1; index < contexts.size(); ++index) Require(sceNpTusDeleteNpTitleCtx(contexts[index]) == 0); } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 6e79af4b49..461ef23b7b 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -3,8 +3,10 @@ #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/InputCommandFile.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include #include #include #include @@ -28,6 +30,8 @@ int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); int APS5_VABI scePadSetAngularVelocityBiasCorrectionState(int, bool); int APS5_VABI scePadIsRemoteController(int, bool*); int APS5_VABI scePadGetAccessibilityControllerState(int, std::uint32_t*); +int APS5_VABI scePadGetControllerInformation(int, PadControllerInformation*); +int APS5_VABI scePadGetExtControllerInformation(int, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -156,6 +160,46 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckTouchPadInformation(int handle) { + PadControllerInformation info; + std::memset(&info, 0x5a, sizeof(info)); + Require(scePadGetControllerInformation(handle, &info) == PAD_OK); + Require(info.touchPadInfo.resolution.x == kTouchPadWidth); + Require(info.touchPadInfo.resolution.y == kTouchPadHeight); + Require(info.touchPadInfo.pixelDensity == 44.86f); + Require(scePadGetControllerInformation(handle + 1, &info) == PAD_ERROR_INVALID_HANDLE); + + std::array extended{}; + extended.fill(0x5a); + Require(scePadGetExtControllerInformation(handle, extended.data()) == PAD_OK); + PadControllerInformation base{}; + std::memcpy(&base, extended.data(), sizeof(base)); + Require(base.touchPadInfo.resolution.x == kTouchPadWidth); + Require(base.touchPadInfo.resolution.y == kTouchPadHeight); + Require(base.touchPadInfo.pixelDensity == 44.86f); +} + +static void CheckVirtualTouchContact() { + PadInputState left; + left.touchLeft = true; + PadPublishInput_nid_postfix(left); + const auto leftData = Pad::ReadState(); + Require(leftData.touch_data_touch_num == 1); + Require(leftData.touch_data_touch0_x == kTouchPadWidth / 4); + Require(leftData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadInputState right; + right.touchRight = true; + PadPublishInput_nid_postfix(right); + const auto rightData = Pad::ReadState(); + Require(rightData.touch_data_touch_num == 1); + Require(rightData.touch_data_touch0_x == kTouchPadWidth * 3 / 4); + Require(rightData.touch_data_touch0_y == kTouchPadHeight / 2); + + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + static void CheckPartialTrigger() { PadInputState partial; partial.buttons = static_cast(Pad::PadButton::L2) | static_cast(Pad::PadButton::R2); @@ -307,7 +351,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { const auto centred = Pad::ReadState(); Require(centred.left_stick_x == 42 && centred.right_stick_y == 17); Require(centred.timestamp > moved.timestamp); - for (const auto& [command, x] : {std::pair{"3 2097152 300\n", 480}, std::pair{"4 4194304 300\n", 1440}}) { + for (const auto& [command, x] : {std::pair{"3 2097152 300\n", kTouchPadWidth / 4}, std::pair{"4 4194304 300\n", kTouchPadWidth * 3 / 4}}) { std::ofstream(path) << command; PadData touched{}; const auto touchDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); @@ -316,7 +360,7 @@ static void CheckInjectedState(const std::filesystem::path& path) { if (touched.touch_data_touch_num != 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); } while (std::chrono::steady_clock::now() < touchDeadline); - Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == 471); + Require(touched.touch_data_touch_num == 1 && touched.touch_data_touch0_x == x && touched.touch_data_touch0_y == kTouchPadHeight / 2); Require(touched.buttons == (0x2000 | 0x100000)); std::this_thread::sleep_for(std::chrono::milliseconds(310)); const auto lifted = Pad::ReadState(); @@ -393,6 +437,8 @@ int main() { CheckTiltCorrection(handle); CheckAngularVelocityDeadband(handle); CheckTouchContact(); + CheckVirtualTouchContact(); + CheckTouchPadInformation(handle); CheckPartialTrigger(); CheckReadQueue(handle); CheckTapBetweenReads(); diff --git a/core/libs/tests/GuestPthreadSchedparam.cpp b/core/libs/tests/GuestPthreadSchedparam.cpp index ddafd9090e..986a1fd1d0 100644 --- a/core/libs/tests/GuestPthreadSchedparam.cpp +++ b/core/libs/tests/GuestPthreadSchedparam.cpp @@ -1,5 +1,6 @@ #include "SceTypes.hpp" #include +#include #include extern "C" { @@ -12,6 +13,10 @@ int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, Ker int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, const KernelSchedParam* param); int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetschedpolicy(const PthreadAttr* attr, int* policy); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); @@ -19,15 +24,33 @@ int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedPa static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int GUEST_EINVAL = 22; -static constexpr int SCHED_FIFO = 1; -static constexpr int SCHED_OTHER = 2; -static constexpr int SCHED_RR = 3; +static constexpr int GUEST_SCHED_FIFO = 1; +static constexpr int GUEST_SCHED_OTHER = 2; +static constexpr int GUEST_SCHED_RR = 3; static constexpr int EXPLICIT_SCHED = 0; static constexpr int INHERIT_SCHED = 4; static void Require(bool value) { if (!value) std::abort(); } +static void RequireAttributes(Pthread thread, int policy, int priority) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + for (auto query : {scePthreadAttrGet, pthread_attr_get_np_nid_postfix}) { + for (int previous : {GUEST_SCHED_FIFO, GUEST_SCHED_RR}) { + Require(scePthreadAttrSetschedpolicy(&attr, previous) == SCE_OK); + Require(query(thread, &attr) == SCE_OK); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK); + Require(scePthreadAttrGetschedparam(&attr, ¶m) == SCE_OK); + Require(current == policy && param.sched_priority == priority); + } + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK); +} + static void RequireSched(int policy, int priority) { int current = -1; KernelSchedParam param{}; @@ -37,6 +60,7 @@ static void RequireSched(int policy, int priority) { param.sched_priority = -1; Require(pthread_getschedparam_nid_postfix(scePthreadSelf(), ¤t, ¶m) == 0); Require(current == policy && param.sched_priority == priority); + RequireAttributes(scePthreadSelf(), policy, priority); } static int SceSet(int policy, int priority) { @@ -53,36 +77,44 @@ static void* APS5_VABI Run(void*) { int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - Require(policy == SCHED_FIFO); + Require(policy == GUEST_SCHED_FIFO); for (int priority : {256, 767, 896, 959}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); - RequireSched(SCHED_FIFO, priority); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); + RequireSched(GUEST_SCHED_FIFO, priority); } for (int priority : {-1, 0, 255, 768, 895, 960, 1023, 4096}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); - Require(PosixSet(SCHED_FIFO, priority) == GUEST_EINVAL); - RequireSched(SCHED_FIFO, 959); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, priority) == GUEST_EINVAL); + RequireSched(GUEST_SCHED_FIFO, 959); } - Require(SceSet(SCHED_RR, 300) == SCE_OK); - RequireSched(SCHED_RR, 300); - for (int other : {-1, 0, SCHED_OTHER, 4}) { + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + RequireSched(GUEST_SCHED_RR, 300); + for (int other : {-1, 0, GUEST_SCHED_OTHER, 4}) { Require(SceSet(other, 700) == SCE_KERNEL_ERROR_EINVAL); Require(PosixSet(other, 700) == GUEST_EINVAL); - RequireSched(SCHED_RR, 300); + RequireSched(GUEST_SCHED_RR, 300); } - Require(PosixSet(SCHED_FIFO, 700) == 0); - RequireSched(SCHED_FIFO, 700); - Require(scePthreadSetschedparam(scePthreadSelf(), SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(PosixSet(GUEST_SCHED_FIFO, 700) == 0); + RequireSched(GUEST_SCHED_FIFO, 700); + Require(scePthreadSetschedparam(scePthreadSelf(), GUEST_SCHED_FIFO, nullptr) == SCE_KERNEL_ERROR_EINVAL); return nullptr; } +struct PolicyRequest { + std::promise ready; + std::future released; +}; + static void* APS5_VABI ReportPolicy(void* arg) { + auto& request = *static_cast(arg); int policy = -1; KernelSchedParam param{}; Require(scePthreadGetschedparam(scePthreadSelf(), &policy, ¶m) == SCE_OK); - *static_cast(arg) = policy; + RequireAttributes(scePthreadSelf(), policy, param.sched_priority); + request.ready.set_value(policy); + request.released.wait(); return nullptr; } @@ -91,10 +123,28 @@ static int CreatedPolicy(int inheritSched, int policy) { Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); - int reported = -1; + std::promise release; + PolicyRequest request{{}, release.get_future()}; + auto ready = request.ready.get_future(); Pthread thread = nullptr; - Require(scePthreadCreate(&thread, &attr, ReportPolicy, &reported, "policy") == SCE_OK); + Require(scePthreadCreate(&thread, &attr, ReportPolicy, &request, "policy") == SCE_OK); + const int reported = ready.get(); + int current = -1; + KernelSchedParam param{}; + Require(scePthreadGetschedparam(thread, ¤t, ¶m) == SCE_OK); + Require(current == reported); + RequireAttributes(thread, reported, param.sched_priority); + for (int updated : {GUEST_SCHED_RR, GUEST_SCHED_FIFO}) { + const KernelSchedParam requested{450}; + Require(scePthreadSetschedparam(thread, updated, &requested) == SCE_OK); + RequireAttributes(thread, updated, requested.sched_priority); + } + release.set_value(); Require(scePthreadJoin(thread, nullptr) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); + Require(scePthreadAttrGet(thread, &attr) == SCE_KERNEL_ERROR_ESRCH); + current = -1; + Require(scePthreadAttrGetschedpolicy(&attr, ¤t) == SCE_OK && current == GUEST_SCHED_RR); Require(scePthreadAttrDestroy(&attr) == SCE_OK); return reported; } @@ -119,7 +169,7 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { PthreadAttr attr = nullptr; Require(scePthreadAttrInit(&attr) == SCE_OK); Require(scePthreadAttrSetinheritsched(&attr, inheritSched) == SCE_OK); - Require(scePthreadAttrSetschedpolicy(&attr, SCHED_RR) == SCE_OK); + Require(scePthreadAttrSetschedpolicy(&attr, GUEST_SCHED_RR) == SCE_OK); if (param) Require(scePthreadAttrSetschedparam(&attr, param) == SCE_OK); const int reported = CreatedPriority(&attr); Require(scePthreadAttrDestroy(&attr) == SCE_OK); @@ -129,13 +179,15 @@ static int CreatedPriority(int inheritSched, const KernelSchedParam* param) { static void* APS5_VABI Spawn(void*) { const KernelSchedParam param{300}; for (int priority : {450, 600}) { - Require(SceSet(SCHED_FIFO, priority) == SCE_OK); + Require(SceSet(GUEST_SCHED_FIFO, priority) == SCE_OK); Require(CreatedPriority(INHERIT_SCHED, ¶m) == priority); Require(CreatedPriority(INHERIT_SCHED, nullptr) == priority); Require(CreatedPriority(nullptr) == priority); Require(CreatedPriority(EXPLICIT_SCHED, ¶m) == 300); Require(CreatedPriority(EXPLICIT_SCHED, nullptr) == 700); } + Require(SceSet(GUEST_SCHED_RR, 300) == SCE_OK); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_RR); return nullptr; } @@ -143,9 +195,9 @@ int main() { Pthread thread = nullptr; Require(scePthreadCreate(&thread, nullptr, Run, nullptr, "schedparam") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_RR) == SCHED_RR); - Require(CreatedPolicy(EXPLICIT_SCHED, SCHED_FIFO) == SCHED_FIFO); - Require(CreatedPolicy(INHERIT_SCHED, SCHED_RR) == SCHED_FIFO); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_RR); + Require(CreatedPolicy(EXPLICIT_SCHED, GUEST_SCHED_FIFO) == GUEST_SCHED_FIFO); + Require(CreatedPolicy(INHERIT_SCHED, GUEST_SCHED_RR) == GUEST_SCHED_FIFO); Require(scePthreadCreate(&thread, nullptr, Spawn, nullptr, "spawn") == SCE_OK); Require(scePthreadJoin(thread, nullptr) == SCE_OK); } diff --git a/core/libs/tests/GuestRaiseInSystemCall.cpp b/core/libs/tests/GuestRaiseInSystemCall.cpp new file mode 100644 index 0000000000..834594d260 --- /dev/null +++ b/core/libs/tests/GuestRaiseInSystemCall.cpp @@ -0,0 +1,86 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +int APS5_VABI sceKernelInstallExceptionHandler(int, void*); +int APS5_VABI sceKernelRemoveExceptionHandler(int); +int APS5_VABI sceKernelRaiseException(Pthread, int); +} + +static constexpr int SigUsr1 = 30; +static constexpr int Raises = 10; +static constexpr DWORD PipeBytes = 4096; +static void Require(bool value) { if (!value) std::abort(); } + +static std::FILE* shared = nullptr; +static std::atomic deliveries{0}; +static std::atomic released{0}; +static std::atomic stop{false}; +static std::atomic blocks{0}; + +static void APS5_VABI Handler(int, void*) { + const auto delivery = deliveries.fetch_add(1) + 1; + while (released.load() < delivery) std::this_thread::yield(); +} + +static void* APS5_VABI Writer(void*) { + const std::string block(16u * PipeBytes, 'x'); + while (!stop.load()) { + Require(std::fwrite(block.data(), 1, block.size(), shared) == block.size()); + blocks.fetch_add(1); + volatile std::uint64_t spin = 0; + for (std::uint64_t i = 0; i < 200000; ++i) spin = spin + i; + } + return nullptr; +} + +int main() { + std::atomic done{false}; + std::thread watchdog([&done] { + for (int i = 0; i < 25000 && !done.load(); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (!done.load()) std::abort(); + }); + HANDLE read = nullptr; + HANDLE write = nullptr; + Require(CreatePipe(&read, &write, nullptr, PipeBytes) != 0); + shared = _fdopen(_open_osfhandle(reinterpret_cast(write), _O_WRONLY | _O_BINARY), "wb"); + Require(shared != nullptr); + std::atomic draining{true}; + std::thread reader([&] { + char chunk[PipeBytes]; + DWORD got = 0; + while (ReadFile(read, chunk, sizeof(chunk), &got, nullptr) != 0 && got != 0) { + if (draining.load()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + }); + Require(sceKernelInstallExceptionHandler(SigUsr1, reinterpret_cast(&Handler)) == 0); + Pthread writer = nullptr; + Require(scePthreadCreate(&writer, nullptr, Writer, nullptr, "pipe writer") == 0); + while (blocks.load() == 0) std::this_thread::yield(); + for (int raise = 1; raise <= Raises; ++raise) { + Require(sceKernelRaiseException(writer, SigUsr1) == 0); + while (deliveries.load() < raise) std::this_thread::yield(); + Require(std::fputc('y', shared) == 'y'); + released.store(raise); + } + stop.store(true); + draining.store(false); + Require(scePthreadJoin(writer, nullptr) == 0); + Require(sceKernelRemoveExceptionHandler(SigUsr1) == 0); + std::fclose(shared); + reader.join(); + CloseHandle(read); + done.store(true); + watchdog.join(); +} diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 19ac107f19..13572d5feb 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -1,6 +1,9 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include +#include +#include extern "C" { std::uint32_t APS5_VABI sceRazorCpuIsCapturing(void); @@ -15,6 +18,16 @@ int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); int APS5_VABI sceRazorCpuPlotValue(const char* series, float value); int APS5_VABI sceRazorCpuWriteBookmark(const char* label, const char* description); int APS5_VABI sceRazorCpuSync(void); +void APS5_VABI sceRazorCpuDisableFiberUserMarkers(void); +int APS5_VABI sceRazorCpuInitDataTags(void* buffer, std::size_t size); +int APS5_VABI sceRazorCpuShutdownDataTags(void); +std::size_t APS5_VABI sceRazorCpuGetDataTagStorageSize(std::uint32_t tagCount); +int APS5_VABI sceRazorCpuTagArray(const char* label, std::uint32_t category, void* array, std::size_t bufferSize, std::size_t elementSize); +int APS5_VABI sceRazorCpuTagBuffer(const char* label, std::uint32_t category, void* buffer, std::size_t bufferSize); +int APS5_VABI sceRazorCpuResizeTaggedBuffer(std::uint32_t category, void* buffer, std::size_t newBufferSize); +int APS5_VABI sceRazorCpuUnTagBuffer(std::uint32_t category, void* buffer); +int APS5_VABI sceRazorCpuBeginLogicalFileAccess(const char* filename, std::uint64_t tag, std::uint64_t size, std::uint8_t operation); +int APS5_VABI sceRazorCpuEndLogicalFileAccess(void); } namespace { @@ -47,4 +60,39 @@ int main() { Require(sceRazorCpuSync() == 0); Require(sceRazorCpuSync() == 0); Require(sceRazorCpuIsCapturing() == 0); + + sceRazorCpuDisableFiberUserMarkers(); + sceRazorCpuDisableFiberUserMarkers(); + Require(sceRazorCpuPushMarker("thread", 0x80ffffffu, 0) == 0); + Require(sceRazorCpuPopMarker() == 0); + + Require(sceRazorCpuBeginLogicalFileAccess("data/level.pak", 0x0123456789abcdefull, 4096, 0) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuBeginLogicalFileAccess("save/slot0.bin", 0, 512, 1) == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + Require(sceRazorCpuEndLogicalFileAccess() == 0); + + unsigned char storage[256]; + std::memset(storage, 0x5a, sizeof(storage)); + int elements[16]{}; + unsigned char buffer[64]{}; + Require(sceRazorCpuInitDataTags(storage, sizeof(storage)) == 0); + Require(sceRazorCpuTagArray("elements", 7, elements, sizeof(elements), sizeof(elements[0])) == 0); + Require(sceRazorCpuTagBuffer("buffer", 7, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuResizeTaggedBuffer(7, buffer, sizeof(buffer) / 2) == 0); + Require(sceRazorCpuUnTagBuffer(7, buffer) == 0); + Require(sceRazorCpuUnTagBuffer(7, elements) == 0); + Require(sceRazorCpuShutdownDataTags() == 0); + for (const unsigned char byte : storage) Require(byte == 0x5a); + Require(sceRazorCpuTagBuffer("untracked", 3, buffer, sizeof(buffer)) == 0); + Require(sceRazorCpuUnTagBuffer(3, buffer) == 0); + Require(sceRazorCpuIsCapturing() == 0); + + bool threw = false; + try { + sceRazorCpuGetDataTagStorageSize(16); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); } diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 6e2b621d33..ddebf49ac2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -72,7 +72,9 @@ int APS5_VABI sceNetResolverAbort(int, int); int APS5_VABI sceNetResolverDestroy(int); int APS5_VABI sceNetResolverGetError(int, int*); int APS5_VABI sceNetClearDnsCache(int); +int APS5_VABI sceNetGetDnsInfo(std::uint32_t*, int); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetIfStat(NetCtlIfStat*); int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); int APS5_VABI sceNetCtlUnregisterCallback(int); @@ -145,6 +147,10 @@ static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } +static void CheckSocketHostErrors() { + Require(Failed(sceNetSocket(nullptr, 2, 1, 17), 43)); +} + static void CheckStreamSender(int family) { const auto addressSize = family == 2 ? 16u : 28u; const int listener = sceNetSocket(nullptr, family, 1, 6); @@ -1431,6 +1437,7 @@ int main() { } Require(sceNetInit_nid_postfix() == 0); CheckEpollWritable(); + CheckSocketHostErrors(); CheckPoolStats(); CheckIfName(); CheckRandom(); @@ -1856,6 +1863,34 @@ int main() { } Require(missing); } + { + std::array dns{}; + dns.fill(0xA5A5A5A5u); + const int count = sceNetGetDnsInfo(dns.data(), 0); + Require(count >= 0 && count <= 2 && dns[2] == 0xA5A5A5A5u); + Require(count == 0 || state == 3); + for (int index = count; index < 2; ++index) Require(dns[index] == 0); + for (int index = 0; index < 2; ++index) { + NetCtlInfo info{}; + const int result = sceNetCtlGetInfo(17 + index, &info); + if (result != 0) { + Require(result == static_cast(0x80412108) && count == 0); + continue; + } + const auto* bytes = reinterpret_cast(&dns[index]); + char text[16] = {}; + 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])); + Require(std::strcmp(text, reinterpret_cast(info.opaque)) == 0); + } + for (const int flags : {0x1, -1}) { + bool threw = false; + try { sceNetGetDnsInfo(dns.data(), flags); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } + bool threw = false; + try { sceNetGetDnsInfo(nullptr, 0); } catch (const std::runtime_error&) { threw = true; } + Require(threw); + } NetCtlIfStat first{}; first.device = 7; first.reserved[7] = 7; diff --git a/core/libs/tests/GuestSignals.cpp b/core/libs/tests/GuestSignals.cpp index 016c4c5a0f..b8011da900 100644 --- a/core/libs/tests/GuestSignals.cpp +++ b/core/libs/tests/GuestSignals.cpp @@ -14,6 +14,8 @@ struct GuestSignalSet { extern "C" { Handler APS5_VABI signal_nid_postfix(int, Handler); int APS5_VABI raise_nid_postfix(int); +int APS5_VABI kill_nid_postfix(int, int); +int APS5_VABI getpid_nid_postfix(); int APS5_VABI pthread_kill_nid_postfix(void*, int); void* APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadCreate(void** thread, const void* attr, void* (APS5_VABI *entry)(void*), void* arg, const char* name); @@ -93,6 +95,11 @@ int main() { Require(raise_nid_postfix(15) == 0 && received == 0); Require(signal_nid_postfix(15, nullptr) == ignore); Require(raise_nid_postfix(100) == -1 && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(0) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(raise_nid_postfix(129) == -1 && *__error_nid_postfix() == 22); + Require(raise_nid_postfix(-1) == -1 && *__error_nid_postfix() == 22); GuestSignalSet blocked{{0x20, 0, 0, 0}}; GuestSignalSet previous{{}}; Require(sigprocmask_nid_postfix(3, &blocked, &previous) == 0); @@ -310,4 +317,28 @@ int main() { pendingSignals.bits[0] == 0 && pendingSignals.bits[1] == 0 && pendingSignals.bits[2] == 0 && pendingSignals.bits[3] == 0); for (int guest : {40, 73, 128}) Require(signal_nid_postfix(guest, nullptr) == Callback); + for (const int ignoredByDefault : {16, 19, 20, 23, 28, 29}) { + *__error_nid_postfix() = 0; + received = 0; + Require(raise_nid_postfix(ignoredByDefault) == 0 && received == 0 && *__error_nid_postfix() == 0); + Require(kill_nid_postfix(getpid_nid_postfix(), ignoredByDefault) == 0 && *__error_nid_postfix() == 0); + } + const GuestSignalSet ignoredByDefaultSet{{(1u << 15) | (1u << 19) | (1u << 22) | (1u << 27) | (1u << 28), 0, 0, 0}}; + GuestSignalSet pendingIgnored{}; + Require(sigprocmask_nid_postfix(3, &ignoredByDefaultSet, nullptr) == 0); + for (const int ignoredByDefault : {16, 20, 23, 28, 29}) Require(raise_nid_postfix(ignoredByDefault) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + const GuestSignalSet noneBlocked{{0, 0, 0, 0}}; + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + const GuestSigaction ignored{1, 0, {}}; + const GuestSigaction restored{0, 0, {}}; + const GuestSignalSet userSignal{{1u << 29, 0, 0, 0}}; + Require(sigaction_nid_postfix(30, &ignored, nullptr) == 0); + *__error_nid_postfix() = 0; + Require(raise_nid_postfix(30) == 0 && kill_nid_postfix(getpid_nid_postfix(), 30) == 0 && *__error_nid_postfix() == 0); + Require(sigprocmask_nid_postfix(3, &userSignal, nullptr) == 0); + Require(raise_nid_postfix(30) == 0); + Require(sigpending_nid_postfix(&pendingIgnored) == 0 && pendingIgnored.bits[0] == 0); + Require(sigprocmask_nid_postfix(3, &noneBlocked, nullptr) == 0); + Require(sigaction_nid_postfix(30, &restored, nullptr) == 0); } diff --git a/core/libs/tests/GuestSleepWake.cpp b/core/libs/tests/GuestSleepWake.cpp new file mode 100644 index 0000000000..177ef4b990 --- /dev/null +++ b/core/libs/tests/GuestSleepWake.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +void KernelWakeSleepers_nid_no_patch(); +void KernelNoteGpuSubmit_nid_no_patch(); +} + +static constexpr KernelUseconds WAKEABLE_MICROS = 1000; +static constexpr KernelUseconds LONG_MICROS = 2000; +static constexpr unsigned EARLY_WAKES_PER_RUN = 8; +static constexpr unsigned TRIES = 20; +static constexpr auto EARLY = std::chrono::microseconds(900); + +static void Require(bool value) { if (!value) std::abort(); } + +static std::chrono::microseconds Usleep(KernelUseconds microseconds) { + const auto start = std::chrono::steady_clock::now(); + Require(sceKernelUsleep_nid_postfix(microseconds) == 0); + return std::chrono::duration_cast(std::chrono::steady_clock::now() - start); +} + +static void Idle(std::chrono::microseconds length) { + const auto until = std::chrono::steady_clock::now() + length; + while (std::chrono::steady_clock::now() < until) std::this_thread::yield(); +} + +class Waker { +public: + Waker() : thread([this] { + while (!stop.load()) { + KernelWakeSleepers_nid_no_patch(); + std::this_thread::sleep_for(std::chrono::microseconds(50)); + } + }) {} + ~Waker() { + stop.store(true); + thread.join(); + } + +private: + std::atomic stop{false}; + std::thread thread; +}; + +#ifdef _WIN32 +static void CoarseWaitsKeepEverySleepFull() { + Waker waker; + for (unsigned i = 0; i < 12; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); + } +} +#endif + +static void SleepWithoutWakeIsFull() { + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void WakeEndsShortSleep() { + Waker waker; + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void WakeLeavesLongSleep() { + Waker waker; + for (unsigned i = 0; i < 4; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + Require(Usleep(LONG_MICROS) >= std::chrono::microseconds(LONG_MICROS)); + } +} + +static void RunOfSleepsEndsEarlyAtMostEightTimes() { + Waker waker; + KernelNoteGpuSubmit_nid_no_patch(); + unsigned early = 0; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 8; ++i) { + if (Usleep(WAKEABLE_MICROS) < std::chrono::microseconds(WAKEABLE_MICROS)) ++early; + } + Require(early <= EARLY_WAKES_PER_RUN); + for (unsigned i = 0; i < 4; ++i) Require(Usleep(WAKEABLE_MICROS) >= std::chrono::microseconds(WAKEABLE_MICROS)); +} + +static void SubmitStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + KernelNoteGpuSubmit_nid_no_patch(); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +static void GapStartsNewRun() { + Waker waker; + for (unsigned i = 0; i < EARLY_WAKES_PER_RUN + 4; ++i) Usleep(WAKEABLE_MICROS); + bool early = false; + for (unsigned i = 0; i < TRIES && !early; ++i) { + Idle(std::chrono::microseconds(WAKEABLE_MICROS + 200)); + early = Usleep(WAKEABLE_MICROS) < EARLY; + } + Require(early); +} + +int main() { +#ifdef _WIN32 + if (std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr) { + CoarseWaitsKeepEverySleepFull(); + return 0; + } +#endif + SleepWithoutWakeIsFull(); + WakeEndsShortSleep(); + WakeLeavesLongSleep(); + RunOfSleepsEndsEarlyAtMostEightTimes(); + SubmitStartsNewRun(); + GapStartsNewRun(); + return 0; +} diff --git a/core/libs/tests/GuestSocketDescriptorFlags.cpp b/core/libs/tests/GuestSocketDescriptorFlags.cpp index a8c1ba10f2..ef8772f738 100644 --- a/core/libs/tests/GuestSocketDescriptorFlags.cpp +++ b/core/libs/tests/GuestSocketDescriptorFlags.cpp @@ -8,17 +8,24 @@ int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI dup_nid_postfix(int); int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } namespace { +constexpr int DuplicateDescriptor = 0; constexpr int GetDescriptorFlags = 1; constexpr int SetDescriptorFlags = 2; +constexpr int Duplicate2Descriptor = 10; +constexpr int DuplicateDescriptorCloseOnExec = 17; +constexpr int Duplicate2DescriptorCloseOnExec = 18; constexpr int CloseOnExec = 1; constexpr int Stream = 1; constexpr int SocketCloseOnExec = 0x10000000; +constexpr int GuestEbadf = 9; +constexpr int GuestEinval = 22; void Require(bool condition, const char* what) { if (condition) return; @@ -51,6 +58,47 @@ int main() { Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); Require(close_nid_postfix(target) == 0, "close failed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + const int minimumDup = fcntl_nid_postfix(plain, DuplicateDescriptor, plain + 4); + Require(minimumDup >= plain + 4 && fcntl_nid_postfix(minimumDup, GetDescriptorFlags) == 0, "F_DUPFD must honor the minimum and clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUPFD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + const int cloexecDup = fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, minimumDup + 1); + Require(cloexecDup >= minimumDup + 1 && fcntl_nid_postfix(cloexecDup, GetDescriptorFlags) == CloseOnExec, "F_DUPFD_CLOEXEC must honor the minimum and set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUPFD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptor, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD with a negative minimum must fail with EINVAL"); + Require(fcntl_nid_postfix(plain, DuplicateDescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEinval, "F_DUPFD_CLOEXEC with a negative minimum must fail with EINVAL"); + Require(close_nid_postfix(cloexecDup) == 0 && close_nid_postfix(minimumDup) == 0, "close failed"); + + const int dup2Target = socket_nid_postfix(2, Stream, 0); + Require(dup2Target >= 0, "socket failed"); + Require(fcntl_nid_postfix(dup2Target, SetDescriptorFlags, CloseOnExec) == 0 && fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, dup2Target) == dup2Target, "F_DUP2FD onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == 0, "F_DUP2FD must clear FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD onto the same descriptor must preserve FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD onto the same descriptor must preserve a cleared flag"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, dup2Target) == dup2Target, "F_DUP2FD_CLOEXEC onto another socket must return the target"); + Require(fcntl_nid_postfix(dup2Target, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC must set FD_CLOEXEC on the target"); + Require(fcntl_nid_postfix(plain, GetDescriptorFlags) == 0, "F_DUP2FD_CLOEXEC must leave the flag of the original"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, plain) == plain && fcntl_nid_postfix(plain, GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto the same descriptor must set FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD with a negative target must fail with EBADF"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, -1) == -1 && *__error_nid_postfix() == GuestEbadf, "F_DUP2FD_CLOEXEC with a negative target must fail with EBADF"); + Require(close_nid_postfix(dup2Target) == 0, "close failed"); + + int pipeDescriptors[2] = {-1, -1}; + Require(pipe_nid_postfix(pipeDescriptors) == 0, "pipe failed"); + Require(fcntl_nid_postfix(pipeDescriptors[0], SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a pipe must succeed"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, CloseOnExec) == 0, "F_SETFD on a socket must succeed"); + Require(fcntl_nid_postfix(plain, Duplicate2Descriptor, pipeDescriptors[0]) == pipeDescriptors[0], "F_DUP2FD onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[0], GetDescriptorFlags) == 0, "F_DUP2FD onto a pipe must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, SetDescriptorFlags, 0) == 0, "F_SETFD 0 must clear FD_CLOEXEC"); + Require(fcntl_nid_postfix(plain, Duplicate2DescriptorCloseOnExec, pipeDescriptors[1]) == pipeDescriptors[1], "F_DUP2FD_CLOEXEC onto a pipe must return the target"); + Require(fcntl_nid_postfix(pipeDescriptors[1], GetDescriptorFlags) == CloseOnExec, "F_DUP2FD_CLOEXEC onto a pipe must set FD_CLOEXEC"); + Require(close_nid_postfix(pipeDescriptors[0]) == 0 && close_nid_postfix(pipeDescriptors[1]) == 0, "close failed"); + const int marked = socket_nid_postfix(2, Stream | SocketCloseOnExec, 0); Require(marked >= 0 && fcntl_nid_postfix(marked, GetDescriptorFlags) == CloseOnExec, "SOCK_CLOEXEC must set FD_CLOEXEC"); Require(close_nid_postfix(marked) == 0, "close failed"); diff --git a/core/libs/tests/GuestSocketDescriptors.cpp b/core/libs/tests/GuestSocketDescriptors.cpp index 632f528ccf..9a0ac4f5e6 100644 --- a/core/libs/tests/GuestSocketDescriptors.cpp +++ b/core/libs/tests/GuestSocketDescriptors.cpp @@ -42,6 +42,8 @@ int APS5_VABI close_nid_postfix(int); int APS5_VABI sceKernelClose(int); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); int APS5_VABI dup_nid_postfix(int); +int APS5_VABI dup2_nid_postfix(int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int APS5_VABI open_nid_postfix(const char*, int, int); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); int APS5_VABI select_nid_postfix(int, void*, void*, void*, const void*); @@ -58,6 +60,8 @@ static void Check(bool value, int line) { #define Require(value) Check((value), __LINE__) constexpr int DescriptorSetSize = 1024; +constexpr int DuplicateCommand = 0; +constexpr int DuplicateCloseOnExec = 17; constexpr int Inet = 2; constexpr int Datagram = 2; constexpr short In = 0x1; @@ -181,6 +185,29 @@ static void DuplicateKeepsSocket() { Require(close_nid_postfix(sender) == 0); } +static void SocketNumbersStayInDescriptorSet() { + const int socket = Socket(); + std::vector files; + for (;;) { + const int file = open_nid_postfix("/dev/urandom", 0, 0); + Require(file >= 0); + if (file >= DescriptorSetSize) { + Require(close_nid_postfix(file) == 0); + break; + } + files.push_back(file); + } + Require(dup_nid_postfix(socket) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCommand, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, 0) == -1 && *__error_nid_postfix() == 24); + Require(fcntl_nid_postfix(socket, DuplicateCloseOnExec, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + Require(socket_nid_postfix(Inet, Datagram, 0) == -1 && *__error_nid_postfix() == 24); + Require(dup2_nid_postfix(socket, DescriptorSetSize) == -1 && *__error_nid_postfix() == 24); + for (const int file : files) Require(close_nid_postfix(file) == 0); + Require(close_nid_postfix(socket) == 0); +} + static void SocketTypeErrors() { constexpr int PrototypeError = 41; constexpr int ProtocolError = 43; @@ -202,5 +229,6 @@ int main() { SelectSocketAndFile(); CloseReleasesNumber(); DuplicateKeepsSocket(); + SocketNumbersStayInDescriptorSet(); return 0; } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index b2d98ae513..ef2f2f0a69 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -11,6 +11,7 @@ extern "C" { void APS5_VABI bcopy_nid_postfix(const void*, void*, std::size_t); char* APS5_VABI basename_nid_postfix(const char*); +char* APS5_VABI basename_r_nid_postfix(const char*, char*); char* APS5_VABI __inet_ntoa_nid_postfix(unsigned int); char* APS5_VABI dirname_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); @@ -37,6 +38,8 @@ int APS5_VABI strncat_s_nid_postfix(char*, std::size_t, const char*, std::size_t int APS5_VABI memcpy_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memmove_s_nid_postfix(void*, std::size_t, const void*, std::size_t); int APS5_VABI memset_s_nid_postfix(void*, std::size_t, int, std::size_t); +void* APS5_VABI memccpy_nid_postfix(void*, const void*, int, std::size_t); +void* APS5_VABI memmem_nid_postfix(const void*, std::size_t, const void*, std::size_t); char* APS5_VABI strnstr_nid_postfix(const char*, const char*, std::size_t); std::size_t APS5_VABI strxfrm_nid_postfix(char*, const char*, std::size_t); int APS5_VABI snprintf_s_nid_postfix(char*, std::size_t, const char*, ...); @@ -360,6 +363,15 @@ int main() { Require(std::strcmp(basename_nid_postfix("one\\two"), "one\\two") == 0); const std::string longName(1024, 'x'); Require(basename_nid_postfix(longName.c_str()) == nullptr && *__error_nid_postfix() == 63); + char baseBuffer[1024]; + Require(basename_r_nid_postfix(path, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "two") == 0); + Require(basename_r_nid_postfix(nullptr, baseBuffer) == baseBuffer && std::strcmp(baseBuffer, ".") == 0); + Require(basename_r_nid_postfix("///", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "/") == 0); + Require(basename_r_nid_postfix("plain", baseBuffer) == baseBuffer && std::strcmp(baseBuffer, "plain") == 0); + const std::string longest(1023, 'y'); + Require(basename_r_nid_postfix(("/a/" + longest).c_str(), baseBuffer) == baseBuffer && std::strlen(baseBuffer) == 1023); + *__error_nid_postfix() = 0; + Require(basename_r_nid_postfix(longName.c_str(), baseBuffer) == nullptr && *__error_nid_postfix() == 63); const unsigned char loopbackBytes[4] = {127, 0, 0, 1}; unsigned int loopback; std::memcpy(&loopback, loopbackBytes, sizeof(loopback)); @@ -513,6 +525,16 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + const char hay[] = "hello world"; + Require(memmem_nid_postfix(hay, 11, "world", 5) == hay + 6); + Require(memmem_nid_postfix(hay, 11, "world", 0) == hay); + Require(memmem_nid_postfix(hay, 5, "world", 5) == nullptr); + Require(memmem_nid_postfix(hay, 11, "xyz", 3) == nullptr); + char dest[16] = {}; + Require(memccpy_nid_postfix(dest, "abc:def", ':', 8) == dest + 4); + Require(std::memcmp(dest, "abc:", 4) == 0); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 6) == nullptr); + Require(memccpy_nid_postfix(dest, "abcdef", ':', 0) == nullptr); CheckSecureCopySizeLimits(); CheckSecureStringSizeLimits(); return CheckMemcpyOverlap() ? 0 : 1; diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 5c08d7e2d4..5fbecd4dfd 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -152,13 +152,109 @@ static void CheckTscFrequencySysctl() { length = sizeof(value); Require(sysctlbyname_nid_postfix("machdep.tsc_freq", &value, &length, &value, sizeof(value)) == -1 && *__error_nid_postfix() == 1); } -static void CheckMachdepCachelineSizeSysctl() { +static bool Untouched(const unsigned char* bytes, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) + if (bytes[i] != 0xaa) return false; + return true; +} +template +static bool Throws(TCall call) { + try { + call(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} +static void CheckUnansweredSysctl() { + static constexpr const char* names[] = { + "hw.byteorder", + "hw.cacheline_size", + "hw.machine_arch", + "hw.usermem", + "kern.clockrate", + "kern.maxfiles", + "kern.maxfilesperproc", + "kern.maxproc", + "kern.ps_strings", + "kern.sched.name", + "kern.smp.maxcpus", + "kern.stackprot", + "kern.timecounter.hardware", + "machdep.cacheline_size", + }; + unsigned char buffer[64]; + for (const char* name : names) { + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + if (std::strcmp(name, "machdep.cacheline_size") == 0) continue; + length = sizeof(buffer); + Require(Throws([&] { sysctlbyname_nid_postfix(name, buffer, &length, buffer, sizeof(buffer)); })); + } + struct Numbered { + const char* name; + int mib[2]; + }; + static constexpr Numbered numbered[] = { + {"hw.byteorder", {6, 4}}, + {"hw.machine_arch", {6, 11}}, + {"kern.clockrate", {1, 12}}, + {"kern.maxfiles", {1, 7}}, + {"kern.maxfilesperproc", {1, 27}}, + {"kern.maxproc", {1, 6}}, + {"kern.ps_strings", {1, 32}}, + }; + for (const auto& entry : numbered) { + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(entry.name, mib, &size) == 0 && size == 2); + Require(mib[0] == entry.mib[0] && mib[1] == entry.mib[1] && mib[2] == 0x5a5a5a5a); + Require(*__error_nid_postfix() == 13); + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + Require(sysctl_nid_postfix(mib, 2, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + length = 77; + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, buffer, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && Untouched(buffer, sizeof(buffer))); + *__error_nid_postfix() = 13; + Require(sysctl_nid_postfix(mib, 2, nullptr, nullptr, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + length = sizeof(buffer); + Require(Throws([&] { sysctl_nid_postfix(mib, 2, buffer, &length, buffer, sizeof(buffer)); })); + } + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t size = 0; *__error_nid_postfix() = 13; - int value = 0x5a5a5a5a; - std::size_t length = sizeof(value); - Require(sysctlbyname_nid_postfix("machdep.cacheline_size", &value, &length, nullptr, 0) == -1); - Require(*__error_nid_postfix() == 2); - Require(value == 0x5a5a5a5a); + Require(sysctlnametomib_nid_postfix("kern.maxproc", nullptr, &size) == 0 && size == 2 && *__error_nid_postfix() == 13); + size = 1; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 1 && mib[0] == 1 && mib[1] == 0x5a5a5a5a); + mib[0] = 0x5a5a5a5a; + size = 0; + Require(sysctlnametomib_nid_postfix("kern.maxproc", mib, &size) == -1 && *__error_nid_postfix() == 12); + Require(size == 0 && mib[0] == 0x5a5a5a5a); + static constexpr const char* unresolved[] = {"hw.cacheline_size", "hw.usermem", "machdep.cacheline_size"}; + for (const char* name : unresolved) { + int untouched[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + size = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, untouched, &size) == -1 && *__error_nid_postfix() == 2); + Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); + } } static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; @@ -663,7 +759,7 @@ int main() { CheckModelSysctl(); #endif CheckTscFrequencySysctl(); - CheckMachdepCachelineSizeSysctl(); + CheckUnansweredSysctl(); CheckPathnameSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); diff --git a/core/libs/tests/GuestSystemExit.cpp b/core/libs/tests/GuestSystemExit.cpp index 93ff93b094..4cf3ad3931 100644 --- a/core/libs/tests/GuestSystemExit.cpp +++ b/core/libs/tests/GuestSystemExit.cpp @@ -108,6 +108,15 @@ void SecondaryShutdown() { AppendMarker("secondary shutdown"); } void APS5_VABI RemainingHandler(void*) { AppendMarker("remaining handler"); } void APS5_VABI ExitingHandler(void*) { exit_nid_postfix(7); } void GuestExitShutdown() { AppendMarker("shutdown"); } +void DelayedExitDrain() { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + AppendMarker("drain"); +} +void ExitFlushAfterDrain() { AppendMarker("flush"); } +void ThrowingExitDrain() { throw std::runtime_error("exit drain test failure"); } +void BlockedExitDrain() { + for (;;) std::this_thread::sleep_for(std::chrono::seconds(1)); +} void APS5_VABI CheckShutdownStillRunning(void*) { if (LibcShutdownToken_nid_postfix().stop_requested()) std::_Exit(8); AppendMarker("guest finalizer"); @@ -193,9 +202,40 @@ int DriveRelaunch() { std::puts("Guest relaunch completed"); return 0; } + +int DriveExitDrain() { + const auto self = OwnExecutable(); + const auto marker = std::filesystem::temp_directory_path() / ("anyps5-exit-drain-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".txt"); + SetVariable(RelaunchMarker, marker.string().c_str()); + Require(RunChild(self, "--exit-drain") == 7); + std::ifstream file(marker); + const std::string text{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + file.close(); + std::filesystem::remove(marker); + Require(text == "drain\nflush\n"); + Require(RunChild(self, "--exit-drain-throws") == 7); + const auto started = std::chrono::steady_clock::now(); + Require(RunChild(self, "--exit-drain-blocked") == 7); + const auto elapsed = std::chrono::steady_clock::now() - started; + Require(elapsed >= std::chrono::seconds(8) && elapsed < std::chrono::seconds(12)); + std::puts("Host exit drain completed before immediate termination and stayed bounded"); + return 0; +} } int main(int argc, char** argv) { const int firstArgument = argc > 0 && std::strncmp(argv[0], "--", 2) == 0 ? 0 : 1; + if (argc > firstArgument && std::strcmp(argv[firstArgument], "--exit-drain-driver") == 0) return DriveExitDrain(); + if (argc > firstArgument && std::strncmp(argv[firstArgument], "--exit-drain", 12) == 0) { + LibcRegisterShutdown_nid_postfix(UnexpectedCleanup); + Require(std::atexit(UnexpectedCleanup) == 0); + if (std::strcmp(argv[firstArgument], "--exit-drain-throws") == 0) LibcSetExitDrain_nid_no_patch(ThrowingExitDrain); + else if (std::strcmp(argv[firstArgument], "--exit-drain-blocked") == 0) LibcSetExitDrain_nid_no_patch(BlockedExitDrain); + else { + LibcSetExitDrain_nid_no_patch(DelayedExitDrain); + LibcSetEndFlush_nid_no_patch(ExitFlushAfterDrain); + } + _Exit_nid_postfix(7); + } if (argc > firstArgument && std::strcmp(argv[firstArgument], "--requested-exit-driver") == 0) { Require(RunChild(OwnExecutable(), "--requested-exit") == 0); return 0; diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp index 5f374153a7..f7f07156cf 100644 --- a/core/libs/tests/GuestSystemFont.cpp +++ b/core/libs/tests/GuestSystemFont.cpp @@ -30,7 +30,16 @@ int APS5_VABI sceFontCloseFont(FontHandle); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontCreateRenderer(const FontMemory*, const void*, FontRenderer*); +int APS5_VABI sceFontDestroyRenderer(FontRenderer*); +int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); +int APS5_VABI sceFontUnbindRenderer(FontHandle); +int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); +void APS5_VABI sceFontRenderSurfaceInit(FontRenderSurface*, void*, int, int, int, int); +int APS5_VABI sceFontRenderCharGlyphImageHorizontal(FontHandle, std::uint32_t, FontRenderSurface*, float, float, FontGlyphMetrics*, FontRenderOutput*); const void* APS5_VABI sceFontSelectLibraryFt(int); +const void* APS5_VABI sceFontSelectRendererFt(int); } static void Check(bool value, int line) { @@ -165,6 +174,29 @@ static float AdvanceOf(FontHandle font, std::uint32_t code) { return metrics.Horizontal.advance; } +static float SetAdvanceOf(FontLibrary library, std::uint32_t fontSetType, std::uint32_t code) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, fontSetType, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + const float advance = AdvanceOf(set, code); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + return advance; +} + +static int RenderedPixels(FontHandle font, std::uint32_t code, float& advance) { + std::vector pixels(128 * 128); + FontRenderSurface surface{}; + sceFontRenderSurfaceInit(&surface, pixels.data(), 128, 1, 128, 128); + FontGlyphMetrics metrics{}; + FontRenderOutput result{}; + const int rc = sceFontRenderCharGlyphImageHorizontal(font, code, &surface, 10.0f, 100.0f, &metrics, &result); + advance = metrics.Horizontal.advance; + if (rc != SCE_FONT_OK) return rc; + int lit = 0; + for (const std::uint8_t pixel : pixels) lit += pixel != 0 ? 1 : 0; + return lit; +} + int main() { constexpr std::uint32_t EuropeanLight = 0x180700C3u; constexpr std::uint32_t EuropeanBold = 0x180700C7u; @@ -264,7 +296,52 @@ int main() { Require(AdvanceOf(memoryFont, 'A') == 90.0f); Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); - for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + constexpr std::uint32_t EuropeanJpAr = 0x180704C4u; + constexpr std::uint32_t EuropeanJpCjk = 0x1807A444u; + constexpr std::uint32_t EuropeanGbCjk = 0x1807AC44u; + constexpr std::uint32_t European = 0x18070044u; + const auto fallbackFonts = root / "fallback-fonts"; + std::filesystem::create_directories(fallbackFonts); + WriteFile(fallbackFonts / "SST-Roman.otf", SquareGlyphFont('A', 600)); + WriteFile(fallbackFonts / "SSTJpPro-Regular.otf", SquareGlyphFont(0x4E00, 630)); + WriteFile(fallbackFonts / "DFHEI5-SONY.ttf", SquareGlyphFont(0x4E00, 640)); + WriteFile(fallbackFonts / "SCEPS4Yoongd-Medium.otf", SquareGlyphFont(0xAC00, 650)); + WriteFile(fallbackFonts / "NotoSansArabic-Regular.ttf", SquareGlyphFont(0x0627, 660)); + SetFontDirectory(fallbackFonts); + Require(SetAdvanceOf(library, EuropeanJpAr, 'A') == 60.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpAr, 0x0627) == 66.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0x4E00) == 63.0f); + Require(SetAdvanceOf(library, EuropeanJpCjk, 0xAC00) == 65.0f); + Require(SetAdvanceOf(library, EuropeanGbCjk, 0x4E00) == 64.0f); + WriteFile(fallbackFonts / "NotoSans-Regular.ttf", SquareGlyphFont(0x1EA0, 670)); + Require(SetAdvanceOf(library, 0x18070454u, 0x1EA0) == 67.0f); + FontHandle plain = nullptr; + Require(sceFontOpenFontSet(library, European, 1, nullptr, &plain) == SCE_FONT_OK); + Require(sceFontGetCharGlyphMetrics(plain, 0x4E00, &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontCloseFont(plain) == SCE_FONT_OK); + + FontHandle mixed = nullptr; + Require(sceFontOpenFontSet(library, EuropeanJpAr, 1, nullptr, &mixed) == SCE_FONT_OK); + FontHandle mixedInstance = nullptr; + Require(sceFontOpenFontInstance(mixed, nullptr, &mixedInstance) == SCE_FONT_OK); + Require(sceFontSetScalePixel(mixedInstance, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(mixedInstance, 0x0627) == 66.0f); + FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; + Require(sceFontGetKerning(mixed, 'A', 0x4E00, &kerning) == SCE_FONT_OK && kerning.offsetX == 0.0f); + FontRenderer renderer = nullptr; + Require(sceFontCreateRenderer(&memory, sceFontSelectRendererFt(0), &renderer) == SCE_FONT_OK); + Require(sceFontBindRenderer(mixed, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(mixed, 100.0f, 100.0f) == SCE_FONT_OK); + float advance = 0.0f; + Require(RenderedPixels(mixed, 'A', advance) > 0 && advance == 60.0f); + Require(RenderedPixels(mixed, 0x4E00, advance) > 0 && advance == 63.0f); + Require(RenderedPixels(mixed, 0x0627, advance) > 0 && advance == 66.0f); + Require(RenderedPixels(mixed, 0xAC00, advance) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + Require(sceFontUnbindRenderer(mixed) == SCE_FONT_OK); + Require(sceFontDestroyRenderer(&renderer) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont, mixed, mixedInstance}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); Require(allocations == 0); std::filesystem::remove_all(root); diff --git a/core/libs/tests/GuestUltSync.cpp b/core/libs/tests/GuestUltSync.cpp new file mode 100644 index 0000000000..c10706460d --- /dev/null +++ b/core/libs/tests/GuestUltSync.cpp @@ -0,0 +1,314 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceUltInitialize(); +int APS5_VABI sceUltFinalize(); +int APS5_VABI _sceUltMutexCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltMutexLock(void*); +int APS5_VABI sceUltMutexTryLock(void*); +int APS5_VABI sceUltMutexUnlock(void*); +int APS5_VABI sceUltMutexDestroy(void*); +int APS5_VABI sceUltSemaphoreOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltQueueDataResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltUlthreadOptParamInitialize(void*, std::uint32_t); +int APS5_VABI _sceUltWaitingQueueResourcePoolCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltWaitingQueueResourcePoolDestroy(void*); +int APS5_VABI sceUltReaderWriterLockOptParamInitialize(void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockCreate(void*, const char*, void*, const void*, std::uint32_t); +int APS5_VABI sceUltReaderWriterLockDestroy(void*); +int APS5_VABI sceUltReaderWriterLockLockRead(void*); +int APS5_VABI sceUltReaderWriterLockTryLockRead(void*); +int APS5_VABI sceUltReaderWriterLockUnlockRead(void*); +int APS5_VABI sceUltReaderWriterLockLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockTryLockWrite(void*); +int APS5_VABI sceUltReaderWriterLockUnlockWrite(void*); +std::uint64_t APS5_VABI sceUltUlthreadRuntimeGetWorkAreaSize(std::uint32_t, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeCreate(void*, const char*, std::uint32_t, std::uint32_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadRuntimeDestroy(void*); +int APS5_VABI sceUltUlthreadCreate(void*, const char*, UltUlthreadEntry, std::uint64_t, void*, std::uint64_t, void*, const void*, std::uint32_t); +int APS5_VABI sceUltUlthreadJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadTryJoin(void*, std::int32_t*); +int APS5_VABI sceUltUlthreadGetSelf(void**); +int APS5_VABI sceUltUlthreadYield(); +int APS5_VABI sceUltUlthreadExit(std::int32_t); +} + +namespace { + +constexpr int Ok = 0; +constexpr int Null = -2139029503; +constexpr int Invalid = -2139029500; +constexpr int State = -2139029498; +constexpr int Busy = -2139029497; + +void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "ULT sync check failed at line %d\n", line); + std::abort(); +} + +#define CHECK(value) Require((value), __LINE__) + +struct alignas(8) Object { + std::uint8_t bytes[256]; +}; + +void MutexTryLock() { + Object mutex{}; + CHECK(sceUltMutexTryLock(nullptr) == Null); + CHECK(sceUltMutexTryLock(&mutex) == State); + CHECK(_sceUltMutexCreate(&mutex, "trylock", nullptr, nullptr, 0) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + std::thread([&] { other = sceUltMutexTryLock(&mutex); }).join(); + CHECK(other == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexUnlock(&mutex) == State); + std::thread([&] { + other = sceUltMutexTryLock(&mutex); + if (other == Ok) other = sceUltMutexUnlock(&mutex); + }).join(); + CHECK(other == Ok); + CHECK(sceUltMutexLock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Busy); + CHECK(sceUltMutexUnlock(&mutex) == Ok); + CHECK(sceUltMutexDestroy(&mutex) == Ok); + CHECK(sceUltMutexTryLock(&mutex) == State); +} + +void OptParams() { + using Initializer = int (APS5_VABI *)(void*, std::uint32_t); + for (const Initializer initialize : {sceUltSemaphoreOptParamInitialize, sceUltQueueOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, + sceUltWaitingQueueResourcePoolOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltReaderWriterLockOptParamInitialize}) { + std::uint8_t bytes[136]; + std::memset(bytes, 0xa5, sizeof(bytes)); + CHECK(initialize(nullptr, 0) == Null); + CHECK(initialize(bytes, 0) == Ok); + for (std::size_t index = 0; index < 128; ++index) CHECK(bytes[index] == 0); + for (std::size_t index = 128; index < sizeof(bytes); ++index) CHECK(bytes[index] == 0xa5); + } +} + + +void WaitUntil(const std::atomic& flag) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!flag.load() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + CHECK(flag.load()); +} + +void Settle() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +void ReaderWriterValidation() { + Object rwlock{}; + Object pool{}; + std::memset(&rwlock, 0x5a, sizeof(rwlock)); + CHECK(sceUltReaderWriterLockCreate(nullptr, "rwlock", nullptr, nullptr, 0) == Null); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", &pool, nullptr, 0) == Invalid); + for (const auto call : {sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockUnlockRead, + sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockWrite}) { + CHECK(call(nullptr) == Null); + CHECK(call(&rwlock) == State); + } + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == Ok); + for (const auto byte : rwlock.bytes) CHECK(byte == 0); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "rwlock", nullptr, nullptr, 0) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == State && sceUltReaderWriterLockTryLockWrite(&rwlock) == State); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockTryLockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockUnlockWrite(&rwlock); }).join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == State && sceUltReaderWriterLockDestroy(&rwlock) == State); +} + +void ReaderWriterReaders() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "readers", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockRead(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { + other = sceUltReaderWriterLockTryLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockLockRead(&rwlock); + }).join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Busy); + std::atomic started{false}; + std::atomic written{false}; + std::thread writer([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + written = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockWrite(&rwlock); + }); + WaitUntil(started); + for (int held = 3; held > 0; --held) { + Settle(); + CHECK(!written.load()); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == Ok); + } + writer.join(); + CHECK(written.load() && other == Ok); + CHECK(sceUltReaderWriterLockUnlockRead(&rwlock) == State); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterWriter() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "writer", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok); + std::atomic other{0}; + std::thread([&] { other = sceUltReaderWriterLockTryLockRead(&rwlock); }).join(); + CHECK(other == Busy); + std::thread([&] { other = sceUltReaderWriterLockTryLockWrite(&rwlock); }).join(); + CHECK(other == Busy); + std::atomic started{false}; + std::atomic read{false}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + read = true; + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(!read.load()); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(read.load() && other == Ok); + CHECK(sceUltReaderWriterLockTryLockWrite(&rwlock) == Ok && sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); +} + +void ReaderWriterPool() { + Object pool{}; + Object rwlock{}; + std::uint8_t workArea[2048]; + CHECK(_sceUltWaitingQueueResourcePoolCreate(&pool, "pool", 2, 2, workArea, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockCreate(&rwlock, "pooled", &pool, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread reader([&] { + started = true; + other = sceUltReaderWriterLockLockRead(&rwlock); + if (other == Ok) other = sceUltReaderWriterLockUnlockRead(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Busy); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == Ok); + reader.join(); + CHECK(other == Ok); + CHECK(sceUltReaderWriterLockDestroy(&rwlock) == Ok); + CHECK(sceUltWaitingQueueResourcePoolDestroy(&pool) == Ok); +} + +void ReaderWriterFinalize() { + Object rwlock{}; + CHECK(sceUltReaderWriterLockCreate(&rwlock, "finalize", nullptr, nullptr, 0) == Ok); + CHECK(sceUltReaderWriterLockLockWrite(&rwlock) == Ok); + std::atomic started{false}; + std::atomic other{0}; + std::thread waiter([&] { + started = true; + other = sceUltReaderWriterLockLockWrite(&rwlock); + }); + WaitUntil(started); + Settle(); + CHECK(sceUltFinalize() == Ok); + waiter.join(); + CHECK(other == State); + CHECK(sceUltReaderWriterLockUnlockWrite(&rwlock) == State); + CHECK(sceUltInitialize() == Ok); +} + +struct alignas(8) UlthreadObject { + std::uint8_t bytes[512]; +}; + +std::atomic gSelf{nullptr}; +std::atomic gYield{-1}; +std::atomic gPastExit{false}; + +std::int32_t APS5_VABI ExitingEntry(std::uint64_t status) { + void* self = nullptr; + if (sceUltUlthreadGetSelf(&self) == Ok) gSelf = self; + gYield = sceUltUlthreadYield(); + sceUltUlthreadExit(static_cast(status)); + gPastExit = true; + return -1; +} + +void Ulthreads() { + void* self = &gSelf; + CHECK(sceUltUlthreadGetSelf(nullptr) == Null); + CHECK(sceUltUlthreadGetSelf(&self) == State && self == &gSelf); + CHECK(sceUltUlthreadYield() == State); + CHECK(sceUltUlthreadExit(1) == State); + std::vector workArea(sceUltUlthreadRuntimeGetWorkAreaSize(2, 1)); + Object runtime{}; + CHECK(sceUltUlthreadRuntimeCreate(&runtime, "runtime", 2, 1, workArea.data(), nullptr, 0) == Ok); + UlthreadObject first{}; + std::uint8_t context[4096]; + CHECK(sceUltUlthreadCreate(&first, "exits", ExitingEntry, 42, context, sizeof(context), &runtime, nullptr, 0) == Ok); + std::int32_t status = 0; + CHECK(sceUltUlthreadJoin(&first, &status) == Ok); + CHECK(status == 42 && gSelf.load() == &first && gYield.load() == Ok && !gPastExit.load()); + UlthreadObject second{}; + gSelf = nullptr; + CHECK(sceUltUlthreadCreate(&second, "tryjoin", ExitingEntry, 7, context, sizeof(context), &runtime, nullptr, 0) == Ok); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + int joined = Busy; + while (joined == Busy && std::chrono::steady_clock::now() < deadline) { + joined = sceUltUlthreadTryJoin(&second, &status); + if (joined == Busy) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + CHECK(joined == Ok && status == 7 && gSelf.load() == &second); + CHECK(sceUltUlthreadRuntimeDestroy(&runtime) == Ok); +} + +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + CHECK(sceUltInitialize() == Ok); + if (std::strcmp(argv[1], "mutex-trylock") == 0) MutexTryLock(); + else if (std::strcmp(argv[1], "opt-params") == 0) OptParams(); + else if (std::strcmp(argv[1], "rwlock-validation") == 0) ReaderWriterValidation(); + else if (std::strcmp(argv[1], "rwlock-readers") == 0) ReaderWriterReaders(); + else if (std::strcmp(argv[1], "rwlock-writer") == 0) ReaderWriterWriter(); + else if (std::strcmp(argv[1], "rwlock-pool") == 0) ReaderWriterPool(); + else if (std::strcmp(argv[1], "rwlock-finalize") == 0) ReaderWriterFinalize(); + else if (std::strcmp(argv[1], "ulthreads") == 0) Ulthreads(); + else return 2; + CHECK(sceUltFinalize() == Ok); + std::puts("ULT sync tests passed"); + return 0; +} diff --git a/core/libs/tests/GuestUltThread.cpp b/core/libs/tests/GuestUltThread.cpp index c89c38791d..0d074c1d20 100644 --- a/core/libs/tests/GuestUltThread.cpp +++ b/core/libs/tests/GuestUltThread.cpp @@ -38,7 +38,6 @@ int APS5_VABI sceUltSemaphoreDestroy(void*); int APS5_VABI sceUltQueueDestroy(void*); int APS5_VABI sceUltQueueDataResourcePoolDestroy(void*); int APS5_VABI sceUltConditionVariableDestroy(void*); -int APS5_VABI sceUltReaderWriterLockDestroy(); int APS5_VABI sceUltGetConditionVariableInfo(); } @@ -111,13 +110,6 @@ static void Validation() { CHECK(sceUltFinalize() == Ok); CHECK(sceUltUlthreadRuntimeGetWorkAreaSize(1, 1) == 256u + 16u * 1024u); CHECK(sceUltConditionVariableDestroy(nullptr) == -2139029503); - bool threwRwl = false; - try { - sceUltReaderWriterLockDestroy(); - } catch (const std::runtime_error&) { - threwRwl = true; - } - CHECK(threwRwl); bool threwInfo = false; try { sceUltGetConditionVariableInfo(); diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index ada58582be..97eef1d837 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -20,6 +20,8 @@ static constexpr int UMTX_OP_MUTEX_LOCK = 5; static constexpr int UMTX_OP_WAIT_UINT = 11; static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_SEM_WAIT = 19; +static constexpr int UMTX_OP_SEM_WAKE = 20; static constexpr int UMTX_OP_NWAKE_PRIVATE = 21; static constexpr int UMTX_OP_SEM2_WAIT = 23; static constexpr int UMTX_OP_MAX = 27; @@ -270,6 +272,61 @@ static void NwakeReadsOnlyTheCountedEntries() { Require(Umtx(addresses, UMTX_OP_NWAKE_PRIVATE, 1) == 0); } +struct GuestSemaphore { + std::uint32_t hasWaiters; + std::uint32_t count; + std::uint32_t flags; +}; + +static void SemWaitReturnsAtOnceWhileTheCountIsNonzero() { + GuestSemaphore sem{0, 2, 0}; + KernelTimespec none{0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(none)), &none) == 0); + Require(sem.hasWaiters == 1); + Require(sem.count == 2); +} + +static void SemWaitTimesOutWhileTheCountIsZero() { + GuestSemaphore sem{0, 0, 0}; + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(sem.hasWaiters == 1); +} + +static void SemWakeReleasesTheWaiterAndClearsTheFlag() { + GuestSemaphore sem{0, 0, 0}; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&sem, UMTX_OP_SEM_WAIT, 0, SizeOf(sizeof(failsafe)), &failsafe); + ++finished; + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (std::atomic_ref(sem.hasWaiters).load() == 0) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 0); + + std::atomic_ref(sem.count).store(1); + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + waiter.join(); + Require(result == 0); + Require(sem.hasWaiters == 0); +} + +static void SemWakeWithoutWaitersSucceeds() { + GuestSemaphore sem{0, 0, 0}; + Require(Umtx(&sem, UMTX_OP_SEM_WAKE, 0) == 0); + Require(sem.hasWaiters == 0); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAKE, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_SEM_WAIT, 0), GUEST_EFAULT)); +} + static void RejectsBadObjectsAndOperations() { Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); @@ -308,5 +365,9 @@ int main() { WakeWithoutWaitersSucceeds(); NwakeWakesEveryWaiterOnEachListedAddress(); NwakeReadsOnlyTheCountedEntries(); + SemWaitReturnsAtOnceWhileTheCountIsNonzero(); + SemWaitTimesOutWhileTheCountIsZero(); + SemWakeReleasesTheWaiterAndClearsTheFlag(); + SemWakeWithoutWaitersSucceeds(); RejectsBadObjectsAndOperations(); } diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp index f74b47675b..49d2671371 100644 --- a/core/libs/tests/GuestVoiceQoS.cpp +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -15,7 +15,7 @@ int APS5_VABI sceVoiceQoSConnect(std::int32_t*, std::int32_t, std::int32_t); int APS5_VABI sceVoiceQoSDisconnect(std::int32_t); int APS5_VABI sceVoiceQoSSetLocalEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSSetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); -int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(); +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute(std::int32_t, std::int32_t, void*, std::int32_t); int APS5_VABI sceVoiceQoSGetRemoteEndpointAttribute(std::int32_t, std::int32_t, const void*, std::int32_t); int APS5_VABI sceVoiceQoSGetConnectionAttribute(std::int32_t, std::int32_t, void*, std::int32_t); int APS5_VABI sceVoiceQoSGetLocalEndpoint(std::int32_t, std::int32_t*); @@ -27,6 +27,8 @@ int APS5_VABI sceVoiceQoSDebugGetStatus(); int APS5_VABI sceVoiceQoSInitHQ(); int APS5_VABI sceVoiceQoSSetMode(); int APS5_VABI sceVoiceQoSTerminate(); +int APS5_VABI sceVoiceQoSReadPacket(std::int32_t, void*, std::uint32_t*); +int APS5_VABI sceVoiceQoSWritePacket(std::int32_t, const void*, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } @@ -93,7 +95,14 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDisconnect(otherConnection) == 0); const std::uint32_t level = 0x1234; + std::uint32_t readLevel = 0; + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, sizeof(level)) == 0); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == 0 && readLevel == level); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, nullptr, sizeof(readLevel)) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, 0) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, -1) == argumentInvalid); + Require(sceVoiceQoSGetLocalEndpointAttribute(local + 100, 3, &readLevel, sizeof(readLevel)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, nullptr, sizeof(level)) == argumentInvalid); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, 0) == 0); Require(sceVoiceQoSSetLocalEndpointAttribute(local, 3, &level, -1) == 0); @@ -105,6 +114,19 @@ int main(int argc, char** argv) { Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, -1) == 0); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote + 100, 3, &level, sizeof(level)) == argumentInvalid); + std::vector packet(1024, 0); + std::uint32_t packetSize = static_cast(packet.size()); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == 0 && packetSize == 0); + Require(sceVoiceQoSReadPacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSReadPacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + + packetSize = 100; + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == 0); + Require(sceVoiceQoSWritePacket(connection, nullptr, &packetSize) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection, packet.data(), nullptr) == argumentInvalid); + Require(sceVoiceQoSWritePacket(connection + 100, packet.data(), &packetSize) == argumentInvalid); + Require(sceVoiceQoSDisconnect(connection) == 0); Require(sceVoiceQoSDisconnect(connection) == argumentInvalid); Require(sceVoiceQoSGetLocalEndpoint(connection, &queriedLocal) == argumentInvalid); @@ -121,13 +143,15 @@ int main(int argc, char** argv) { Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == notInitialized); Require(sceVoiceQoSCreateLocalEndpoint(&local, 1, 0, 0) == notInitialized); Require(sceVoiceQoSSetRemoteEndpointAttribute(remote, 3, &level, sizeof(level)) == notInitialized); + Require(sceVoiceQoSGetLocalEndpointAttribute(local, 3, &readLevel, sizeof(readLevel)) == notInitialized); + Require(sceVoiceQoSReadPacket(connection, packet.data(), &packetSize) == notInitialized); + Require(sceVoiceQoSWritePacket(connection, packet.data(), &packetSize) == notInitialized); Require(sceVoiceQoSInit(memory.data(), 0x40000, appType) == 0); Require(sceVoiceQoSDeleteRemoteEndpoint(remote) == argumentInvalid); Require(sceVoiceQoSEnd() == 0); Require(sceVoiceQoSEnd() == notInitialized); Require(sceVoiceQoSInit(nullptr, 0x40000, appType) == argumentInvalid); - Require(NotImplemented([] { sceVoiceQoSGetLocalEndpointAttribute(); })); Require(NotImplemented([] { sceVoiceQoSGetRemoteEndpointAttribute(1, 1, nullptr, 0); })); Require(NotImplemented([] { sceVoiceQoSGetConnectionAttribute(1, 1, nullptr, 0); })); Require(NotImplemented([] { sceVoiceQoSEnableChat(); })); diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 758ff5d33c..44a4206540 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -5,6 +5,7 @@ extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +extern "C" int APS5_VABI wcstombs_s_nid_postfix(std::size_t*, char*, std::size_t, const std::uint16_t*, std::size_t); static void Require(bool value) { if (!value) std::abort(); } constexpr auto Failed = static_cast(-1); @@ -75,4 +76,22 @@ int main() { result = 7; Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); + + char narrow[8]; + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, sizeof(narrow)) == 0 && result == 3); + Require(std::memcmp(narrow, "abc\0x", 5) == 0); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), text, 2) == 0 && result == 2); + Require(std::memcmp(narrow, "ab\0x", 4) == 0); + Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0 && result == 3); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, 3, text, 3) == 34 && result == Failed && narrow[0] == '\0'); + Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == 22 && result == Failed); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), nullptr, sizeof(narrow)) == 22); + Require(result == Failed && narrow[0] == '\0'); + std::memset(narrow, 'x', sizeof(narrow)); + Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), wide, sizeof(narrow)) == 86 && result == Failed); + Require(std::memcmp(narrow, "a\0xx", 4) == 0); } diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 818f401ffd..8ebfa2753e 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -276,7 +276,33 @@ static void CheckWcsftime() { Require(WcsftimeThrows(buffer, u"%Y", nullptr), "wcsftime null time"); } +static void CheckAlternateOctalZero() { + struct Case { const char16_t* format; const char16_t* expected; unsigned value; }; + for (const auto& test : { + Case{u"%#.0o", u"0", 0}, Case{u"%#.o", u"0", 0}, Case{u"%#.00o", u"0", 0}, + Case{u"[%#5.0o]", u"[ 0]", 0}, Case{u"[%-#5.0o]", u"[0 ]", 0}, + Case{u"[%#05.0o]", u"[ 0]", 0}, Case{u"%.0o", u"", 0}, Case{u"%#.0x", u"", 0}, + Case{u"%#.0o", u"01", 1}, Case{u"%#.1o", u"0", 0}, Case{u"%#.3o", u"000", 0}, + Case{u"%#.0hho", u"0", 256}, Case{u"%#.0ho", u"0", 65536} + }) { + char16_t buffer[32]; + const auto expected = std::u16string(test.expected); + Require(swprintf_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "swprintf alternate octal zero"); + Require(Format(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "vswprintf alternate octal zero"); + Require(snwprintf_s_nid_postfix(buffer, 32, test.format, test.value) == static_cast(expected.size()) && buffer == expected, "snwprintf_s alternate octal zero"); + } + char16_t buffer[16]; + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero dynamic precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.*o", -1, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero omitted precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%2$#.*1$o", 0, 0u) == 1 && buffer == std::u16string(u"0"), "octal zero positional precision"); + Require(swprintf_nid_postfix(buffer, 16, u"%#.0llo", 0ull) == 1 && buffer == std::u16string(u"0"), "octal zero long long"); + buffer[0] = u'x'; + buffer[1] = u'y'; + Require(snwprintf_s_nid_postfix(buffer, 1, u"%#.0o", 0u) == 1 && buffer[0] == 0 && buffer[1] == u'y', "octal zero bounded output"); +} + int main() { + CheckAlternateOctalZero(); CheckBounded(); CheckPositional(); CheckPointer(); diff --git a/core/libs/tests/ImageSampleGather.cpp b/core/libs/tests/ImageSampleGather.cpp index 6fa633e3a7..3d3b40d608 100644 --- a/core/libs/tests/ImageSampleGather.cpp +++ b/core/libs/tests/ImageSampleGather.cpp @@ -72,8 +72,25 @@ static void CheckUnsupportedOpcodeDiagnostic(std::uint32_t encoding, std::string } Require(false); } +static void CheckSampleCompareOffsetAdjust() { + const std::array code{0xf0e00109u, 0x00801e33u}; + const RdnaInstruction instruction = DecodeRdnaMimg(0u, code, 0u); + Require(instruction.op == RdnaOpcode::ImageSample); + Require(instruction.imageOpcodeId == 0xb8u); + Require(instruction.imageSampleFlags == (RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust)); + Require(instruction.imageAddressComponents == 4u); + Require(GetRdnaImageAddressComponentLayout(instruction.imageSampleFlags, 1u).bitOffset == 32u); + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(instruction); + Require(program.Resources().memoryInfo.size() == 1u); + Require(program.Resources().memoryInfo.back().imageSampleFlags == instruction.imageSampleFlags); +} int main() { CheckSamplerVccTuple(); + CheckSampleCompareOffsetAdjust(); CheckSamplerVccTupleOverflow(); Check(0x26u, RdnaOpcode::ImageSampleBCl, 0xfu); Check(0x36u, RdnaOpcode::ImageSampleBClO, 0xfu); diff --git a/core/libs/tests/Mspace.cpp b/core/libs/tests/Mspace.cpp index 2224e2b7a1..0741744299 100644 --- a/core/libs/tests/Mspace.cpp +++ b/core/libs/tests/Mspace.cpp @@ -30,6 +30,8 @@ void APS5_VABI sceLibcPafMspaceFree_nid_postfix(void*, void*, const void*); void* APS5_VABI sceLibcPafMspaceCalloc_nid_postfix(void*, std::size_t, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceRealloc_nid_postfix(void*, void*, std::size_t, const void*); void* APS5_VABI sceLibcPafMspaceReallocalign_nid_postfix(void*, void*, std::size_t, std::size_t, const void*); +void* APS5_VABI sceLibcPafMspaceMemalign_nid_postfix(void*, std::size_t, std::size_t, const void*); +int APS5_VABI sceLibcPafMspacePosixMemalign_nid_postfix(void*, void**, std::size_t, std::size_t, const void*); int APS5_VABI sceLibcPafMspaceMallocStats_nid_postfix(void*, void*); int APS5_VABI sceLibcPafMspaceMallocStatsFast_nid_postfix(void*, void*); } @@ -64,6 +66,15 @@ static void CheckPaf() { Require(block != nullptr && block[0] == 0x5a && block[39] == 0x5a); block = static_cast(sceLibcPafMspaceReallocalign_nid_postfix(arena, block, 256, 64, &owner)); Require(block != nullptr && reinterpret_cast(block) % 256 == 0 && block[39] == 0x5a); + auto* aligned = static_cast(sceLibcPafMspaceMemalign_nid_postfix(arena, 128, 50, &owner)); + Require(aligned != nullptr && reinterpret_cast(aligned) % 128 == 0); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 0, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + Require(sceLibcPafMspaceMemalign_nid_postfix(arena, 15, 50, &owner) == nullptr && *__error_nid_postfix() == 22); + void* posixAligned = nullptr; + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 256, 60, &owner) == 0); + Require(posixAligned != nullptr && reinterpret_cast(posixAligned) % 256 == 0); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, nullptr, 256, 60, &owner) == 22); + Require(sceLibcPafMspacePosixMemalign_nid_postfix(arena, &posixAligned, 15, 60, &owner) == 22); MallocManagedSize viaPaf{}, viaMspace{}, fastPaf{}, fastMspace{}; viaPaf.size = viaMspace.size = fastPaf.size = fastMspace.size = sizeof(MallocManagedSize); viaPaf.version = viaMspace.version = fastPaf.version = fastMspace.version = 1; @@ -73,6 +84,8 @@ static void CheckPaf() { Require(fastPaf.currentInuseSize == fastMspace.currentInuseSize); sceLibcPafMspaceFree_nid_postfix(arena, block, &owner); sceLibcPafMspaceFree_nid_postfix(arena, zeroed, arena); + sceLibcPafMspaceFree_nid_postfix(arena, aligned, &owner); + sceLibcPafMspaceFree_nid_postfix(arena, posixAligned, &owner); sceLibcPafMspaceFree_nid_postfix(arena, nullptr, &owner); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == 0); Require(sceLibcPafMspaceDestroy_nid_postfix(arena) == -1); diff --git a/core/libs/tests/NetBlockingSend.cpp b/core/libs/tests/NetBlockingSend.cpp new file mode 100644 index 0000000000..fb64d834ba --- /dev/null +++ b/core/libs/tests/NetBlockingSend.cpp @@ -0,0 +1,146 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags); +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength); +#define send(...) InterceptSend(__VA_ARGS__) +#define sendto(...) InterceptSendto(__VA_ARGS__) +#endif +#include "../prx/libSceNet/Export.cpp" +#ifdef _WIN32 +#undef send +#undef sendto +#endif +#include +#include +#include + +#ifdef _WIN32 +static int InterceptSend(SOCKET socket, const char* buffer, int length, int flags) { + return ::send(socket, buffer, std::min(length, 16384), flags); +} +static int InterceptSendto(SOCKET socket, const char* buffer, int length, int flags, const sockaddr* address, int addressLength) { + return ::sendto(socket, buffer, std::min(length, 16384), flags, address, addressLength); +} +#endif + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "blocking send: %s\n", message); + std::exit(1); +} + +static void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, NET_SOL_SOCKET, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("blocking-listener", 2, 1, 0); + int sender = sceNetSocket("blocking-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, NET_SO_RCVBUF, 4096); + SetOption(sender, NET_SO_SNDBUF, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "accept"); + SetOption(receiver, NET_SO_RCVBUF, 1024 * 1024); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } +}; + +static std::int64_t Send(std::string_view api, int descriptor, std::vector& bytes, int flags = 0) { + if (api == "send") return sceNetSend(descriptor, bytes.data(), bytes.size(), flags); + std::array address{}; + std::uint32_t size = address.size(); + Require(sceNetGetpeername(descriptor, address.data(), &size) == 0, "get destination address"); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), flags, address.data(), size); + Require(api == "sendmsg", "unknown API"); + std::array vectors{{{bytes.data(), bytes.size() / 2}, + {bytes.data() + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{address.data(), size, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, flags); +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + std::vector bytes(4 * 1024 * 1024); + for (std::size_t index = 0; index < bytes.size(); ++index) + bytes[index] = static_cast((index * 31 + index / 65536) % 256); + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const auto early = sending.wait_for(std::chrono::milliseconds(50)); +#ifdef _WIN32 + if (early == std::future_status::ready) { + std::fprintf(stderr, "early send returned %lld of %zu bytes\n", static_cast(sending.get()), bytes.size()); + Require(false, "send returned before the peer drained its buffer"); + } +#else + static_cast(early); +#endif + std::array buffer{}; + std::size_t received = 0; + while (received < bytes.size()) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), std::min(buffer.size(), bytes.size() - received), 0); + Require(count > 0, "send did not deliver the whole payload"); + Require(std::equal(buffer.begin(), buffer.begin() + count, bytes.begin() + received), "send changed the payload"); + received += static_cast(count); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "send did not complete"); + Require(sending.get() == static_cast(bytes.size()), "send returned a short count"); + std::printf("%.*s: sent=%zu, delivered=%zu\n", static_cast(api.size()), api.data(), bytes.size(), received); + } + { + Connection connection; + SetOption(connection.sender, 0x1105, 100000); + auto started = std::chrono::steady_clock::now(); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial < static_cast(bytes.size()), "timeout must return queued bytes"); + Require(std::chrono::steady_clock::now() - started >= std::chrono::milliseconds(80), "partial send returned before its timeout"); + bool emptyTimeout = false; + for (int attempt = 0; attempt < 16; ++attempt) { + const auto result = Send(api, connection.sender, bytes); + if (result < 0) { + Require(*sceNetErrnoLoc() == NET_EAGAIN, "timeout without progress must return EAGAIN"); + emptyTimeout = true; + break; + } + Require(result > 0 && result < static_cast(bytes.size()), "invalid timed byte count"); + } + Require(emptyTimeout, "peer never exhausted the send buffers"); + } + { + Connection connection; + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + Require(sending.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "send did not wait before abort"); + Require(sceNetSocketAbort(connection.sender, 0) == 0, "abort a blocked send"); + Require(sending.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "abort did not wake a blocked send"); + const auto partial = sending.get(); + Require(partial > 0 && partial < static_cast(bytes.size()), "error discarded queued bytes"); + } + { + Connection connection; + SetOption(connection.sender, NET_SO_NBIO, 1); + const auto partial = Send(api, connection.sender, bytes); + Require(partial > 0 && partial <= static_cast(bytes.size()), "invalid guest nonblocking send result"); +#ifdef _WIN32 + Require(partial <= 16384, "guest nonblocking send did not return its first host result"); +#endif + } + std::puts("timeout, abort and guest nonblocking checks passed"); +} diff --git a/core/libs/tests/NetDontWait.cpp b/core/libs/tests/NetDontWait.cpp new file mode 100644 index 0000000000..7507f2fde5 --- /dev/null +++ b/core/libs/tests/NetDontWait.cpp @@ -0,0 +1,239 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; + +extern "C" { +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSocketAbort(int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetSend(int, const void*, std::size_t, int); +std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int DontWait = 0x80; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "net dontwait: %s\n", message); + std::exit(1); +} + +void SetOption(int descriptor, int option, int value) { + Require(sceNetSetsockopt(descriptor, 0xffff, option, &value, sizeof(value)) == 0, "set socket option"); +} + +struct Connection { + int listener = sceNetSocket("dontwait-listener", 2, 1, 0); + int sender = sceNetSocket("dontwait-sender", 2, 1, 0); + int receiver = -1; + + Connection() { + Require(listener >= 0 && sender >= 0, "create TCP sockets"); + SetOption(listener, 0x1002, 4096); + SetOption(sender, 0x1001, 4096); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get listener address"); + Require(sceNetListen(listener, 1) == 0, "listen"); + Require(sceNetConnect(sender, address.data(), size) == 0, "blocking connect"); + receiver = sceNetAccept(listener, nullptr, nullptr); + Require(receiver >= 0, "blocking accept"); + SetOption(receiver, 0x1002, 4096); + SetOption(receiver, 0x1106, 2000000); + } + + ~Connection() { + for (const int descriptor : {receiver, sender, listener}) + Require(sceNetSocketClose(descriptor) == 0, "close socket"); + } + + void Abort() { + sceNetSocketAbort(sender, 0); + sceNetSocketAbort(receiver, 0); + } +}; + +std::int64_t Send(std::string_view api, int descriptor, const std::vector& bytes) { + if (api == "send" || api == "duplex") return sceNetSend(descriptor, bytes.data(), bytes.size(), DontWait); + if (api == "sendto") return sceNetSendto(descriptor, bytes.data(), bytes.size(), DontWait, nullptr, 0); + std::array vectors{{{const_cast(bytes.data()), bytes.size() / 2}, + {const_cast(bytes.data()) + bytes.size() / 2, bytes.size() - bytes.size() / 2}}}; + const NetMsghdr message{nullptr, 0, vectors.data(), static_cast(vectors.size()), nullptr, 0, 0}; + return sceNetSendmsg(descriptor, &message, DontWait); +} + +void CheckSend(std::string_view api) { + Connection connection; + std::vector bytes(16 * 1024 * 1024, 'x'); + std::future reading; + char reply = 0; + if (api == "duplex") { + reading = std::async(std::launch::async, [&] { return sceNetRecv(connection.sender, &reply, 1, 0); }); + Require(reading.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, + "ordinary receive must wait while a DONTWAIT send runs on the same socket"); + } + auto sending = std::async(std::launch::async, [&] { return Send(api, connection.sender, bytes); }); + const bool completed = sending.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto sent = sending.get(); + Require(completed, "MSG_DONTWAIT send blocked behind a slow peer or an ordinary receive"); + Require(sent > 0 && sent <= static_cast(bytes.size()), "large send returned an invalid byte count"); + int nonblocking = -1; + std::uint32_t size = sizeof(nonblocking); + Require(sceNetGetsockopt(connection.sender, 0xffff, 0x1200, &nonblocking, &size) == 0 && nonblocking == 0, + "DONTWAIT changed the socket option"); + Require(fcntl_nid_postfix(connection.sender, 3) == 2, "DONTWAIT changed descriptor flags"); + if (reading.valid()) { + Require(reading.wait_for(std::chrono::milliseconds(0)) == std::future_status::timeout, + "DONTWAIT interrupted an ordinary receive"); + const char value = 'r'; + Require(sceNetSend(connection.receiver, &value, 1, 0) == 1, "send reply"); + if (reading.wait_for(std::chrono::seconds(2)) != std::future_status::ready) connection.Abort(); + Require(reading.get() == 1 && reply == value, "ordinary receive did not complete with its reply"); + } + std::int64_t received = 0; + SetOption(connection.receiver, 0x1002, 1024 * 1024); + std::array buffer{}; + while (received < sent) { + const auto count = sceNetRecv(connection.receiver, buffer.data(), + std::min(buffer.size(), static_cast(sent - received)), 0); + Require(count > 0, "reported send bytes were not delivered"); + for (std::int64_t index = 0; index < count; ++index) Require(buffer[index] == 'x', "payload changed"); + received += count; + } + Require(sceNetRecv(connection.receiver, buffer.data(), buffer.size(), DontWait) < 0 && *sceNetErrnoLoc() == 35, + "send left unreported bytes or receive did not return EAGAIN"); + std::printf("%.*s: sent=%lld, delivered=%lld, blocking flags preserved\n", + static_cast(api.size()), api.data(), static_cast(sent), static_cast(received)); +} + +std::pair Receive(std::string_view api, int descriptor) { + char byte = 0; + std::int64_t result; + if (api == "recv") result = sceNetRecv(descriptor, &byte, 1, DontWait); + else if (api == "recvfrom") result = sceNetRecvfrom(descriptor, &byte, 1, DontWait, nullptr, nullptr); + else { + NetIovec vector{&byte, 1}; + NetMsghdr message{nullptr, 0, &vector, 1, nullptr, 0, 0}; + result = sceNetRecvmsg(descriptor, &message, DontWait); + } + if (result == 1) Require(byte == 'x', "received payload changed"); + return {result, *sceNetErrnoLoc()}; +} + +void CheckReceive(std::string_view api) { + Connection connection; + SetOption(connection.receiver, 0x1106, 0); + constexpr int readers = 8; + for (int iteration = 0; iteration < 32; ++iteration) { + const char byte = 'x'; + Require(sceNetSend(connection.sender, &byte, 1, 0) == 1, "send race payload"); + std::barrier start(readers + 1); + std::vector>> receiving; + for (int index = 0; index < readers; ++index) + receiving.push_back(std::async(std::launch::async, [&] { + start.arrive_and_wait(); + return Receive(api, connection.receiver); + })); + start.arrive_and_wait(); + int successes = 0; + for (auto& future : receiving) { + const bool completed = future.wait_for(std::chrono::milliseconds(500)) == std::future_status::ready; + if (!completed) connection.Abort(); + const auto [result, error] = future.get(); + Require(completed, "MSG_DONTWAIT receive blocked after another reader consumed the byte"); + if (result == 1) ++successes; + else Require(result < 0 && error == 35, "empty receive must return EAGAIN"); + } + Require(successes == 1, "one byte must be delivered to exactly one reader"); + } + std::printf("%.*s: 32 races, one successful reader and seven EAGAIN results each\n", + static_cast(api.size()), api.data()); +} + +void CheckBlocking() { + const int listener = sceNetSocket("blocking-listener", 2, 1, 0); + const int sender = sceNetSocket("blocking-sender", 2, 1, 0); + Require(listener >= 0 && sender >= 0, "create blocking sockets"); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0, "bind blocking listener"); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0, "get blocking listener address"); + Require(sceNetListen(listener, 1) == 0, "listen before accept"); + auto accepting = std::async(std::launch::async, [&] { return sceNetAccept(listener, nullptr, nullptr); }); + Require(accepting.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "accept did not wait for a connection"); + Require(sceNetConnect(sender, address.data(), size) == 0, "connect to waiting accept"); + if (accepting.wait_for(std::chrono::seconds(2)) != std::future_status::ready) sceNetSocketAbort(listener, 0); + const int receiver = accepting.get(); + Require(receiver >= 0, "waiting accept failed"); + SetOption(receiver, 0x1200, 1); + char byte = 0; + Require(sceNetRecv(receiver, &byte, 1, 0) < 0 && *sceNetErrnoLoc() == 35, "socket nonblocking option did not affect receive"); + SetOption(receiver, 0x1200, 0); + auto receiving = std::async(std::launch::async, [&] { return sceNetRecv(receiver, &byte, 1, 0); }); + Require(receiving.wait_for(std::chrono::milliseconds(50)) == std::future_status::timeout, "blocking receive did not wait"); + Require(sceNetSocketClose(receiver) == 0, "close a socket with a waiting receive"); + Require(receiving.wait_for(std::chrono::seconds(2)) == std::future_status::ready, "close did not wake the receive"); + const auto closedResult = receiving.get(); + std::fprintf(stderr, "closed receive result=%lld\n", static_cast(closedResult)); + Require(closedResult <= 0, "closed receive returned data that was never sent"); + Require(sceNetSocketClose(sender) == 0 && sceNetSocketClose(listener) == 0, "close blocking fixture"); + std::puts("blocking: accept waited, socket mode toggled, close woke the pending receive"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 2, "one API argument required"); + const std::string_view api(argv[1]); + if (api == "blocking") { + CheckBlocking(); + return 0; + } + if (api == "send" || api == "sendto" || api == "sendmsg" || api == "duplex") CheckSend(api); + else { + Require(api == "recv" || api == "recvfrom" || api == "recvmsg", "unknown API"); + CheckReceive(api); + } +} diff --git a/core/libs/tests/WindowsMappingsRelease.cpp b/core/libs/tests/WindowsMappingsRelease.cpp new file mode 100644 index 0000000000..6b2f016c97 --- /dev/null +++ b/core/libs/tests/WindowsMappingsRelease.cpp @@ -0,0 +1,81 @@ +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include + +static void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) throw std::runtime_error("Windows resident release regression at " + std::to_string(location.line())); +} + +static void CheckMetadataMismatch(unsigned mode) { + constexpr std::size_t page = 65536; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* base = static_cast(mappings.Reserve(nullptr, 3 * page)); + Require(base != nullptr); + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, 3 * page, nullptr); + Require(section != nullptr); + auto* address = base + page; + mappings.Map(address, page, section, 0, PAGE_READWRITE, true, true); + Require(VirtualFree(address, 0, MEM_RELEASE)); + if (mode == 0) { + Require(VirtualFree(base, 0, MEM_RELEASE)); + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(base, 3 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == base); + } else if (mode == 1) { + Require(VirtualFree(base + 2 * page, 0, MEM_RELEASE)); + Require(VirtualAlloc(address, 2 * page, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE) == address); + } else { + Require(MapViewOfFileEx(section, FILE_MAP_ALL_ACCESS, 0, 0, page, address) == address); + } + address[0] = 73; + bool rejected = false; + try { mappings.Reset(address, page); } + catch (const std::runtime_error&) { rejected = true; } + std::uint32_t protection = 0; + Require(rejected && address[0] == 73 && mappings.Protection(reinterpret_cast(address), &protection)); + if (mode == 2) Require(UnmapViewOfFile(address)); + else Require(VirtualFree(mode == 0 ? base : address, 0, MEM_RELEASE)); + CloseHandle(section); +} + +static void CheckValidRelease() { + constexpr std::size_t bytes = 2 * 1024 * 1024; + auto& mappings = GuestArena::WindowsMappings::Get(); + auto* reserved = static_cast(mappings.Reserve(nullptr, 2 * bytes)); + Require(reserved != nullptr); + const auto start = (reinterpret_cast(reserved) + bytes - 1) & ~static_cast(bytes - 1); + auto* base = reinterpret_cast(start); + const auto oneRun = [&] { + std::uintptr_t first = 0; + std::uintptr_t last = 0; + return mappings.ResidentSpan(start + bytes - 1, 1, &first, &last) && first == start && last == start + bytes; + }; + const auto section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0, bytes, nullptr); + Require(section != nullptr); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + base[0] = 31; + base[bytes - 1] = 89; + DWORD previous; + Require(VirtualProtect(base + 65536, 65536, PAGE_READONLY, &previous)); + mappings.Reset(base, bytes); + mappings.Map(base, bytes, section, 0, PAGE_READWRITE, true, true); + Require(oneRun()); + Require(base[0] == 31 && base[bytes - 1] == 89); + mappings.Reset(base, bytes); + Require(VirtualFree(reserved, 2 * bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)); + Require(VirtualFree(reserved, 0, MEM_RELEASE)); + mappings.ForgetSection(section); + CloseHandle(section); +} + +int main(int argc, char** argv) { + try { + if (argc == 2) CheckMetadataMismatch(static_cast(argv[1][0] - '0')); + else CheckValidRelease(); + } catch (const std::exception& failure) { + std::fprintf(stderr, "%s\n", failure.what()); + return 1; + } + return 0; +} diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt index f84f4e6842..e9a10f6a4c 100644 --- a/core/libs/tests/guest_arena/CMakeLists.txt +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -15,7 +15,7 @@ set_target_properties(guest_arena_tests PROPERTIES CXX_EXTENSIONS OFF RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") -foreach(mode available unmappable host commit) +foreach(mode available unmappable host gap commit) add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) endforeach() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index a3b0cc7e5c..c454613dbe 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -31,6 +31,7 @@ add_library(relinker_core STATIC ${CMAKE_CURRENT_SOURCE_DIR}/io/src/ByteReader.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/BufferUtils.cpp ${CMAKE_CURRENT_SOURCE_DIR}/io/src/NativePath.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/io/src/FileIdentity.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/Amd64OnlyConverter.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/InstructionScanner.cpp ${CMAKE_CURRENT_SOURCE_DIR}/codegen/src/x86/X64InstructionDecoder.cpp @@ -454,6 +455,9 @@ if(BUILD_TESTING) add_test(NAME guest_none_relocations COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_none_relocations.py $) set_tests_properties(guest_none_relocations PROPERTIES TIMEOUT 20) + add_test(NAME interpreter_in_code COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_interpreter_in_code.py $) + set_tests_properties(interpreter_in_code PROPERTIES TIMEOUT 20) add_test(NAME guest_relative_section_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_relative_section_symbols.py $) set_tests_properties(guest_relative_section_symbols PROPERTIES TIMEOUT 20) @@ -522,6 +526,8 @@ if(BUILD_TESTING) set_tests_properties(windows_runpath PROPERTIES TIMEOUT 30) add_test(NAME windows_trace_imports COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_trace_imports.py $) + add_test(NAME linux_trace_imports COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_linux_trace_imports.py $) add_test(NAME windows_address_space COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_address_space.py $) add_test(NAME windows_tls_thread_pointer COMMAND ${Python3_EXECUTABLE} @@ -604,6 +610,11 @@ if(BUILD_TESTING) add_asan_test_options(guest_module_fini verify_asan_link_order=0) add_test(NAME guest_needed_nested_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_nested_modules.py $) + add_test(NAME guest_zero_initializer COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_zero_initializer.py + $ $) + set_tests_properties(guest_zero_initializer PROPERTIES TIMEOUT 30) + add_asan_test_options(guest_zero_initializer verify_asan_link_order=0) set(windows_stdio_host_arguments) if(WIN32) if(MINGW) @@ -731,6 +742,10 @@ if(BUILD_TESTING) ) add_test(NAME windows_icon_resource COMMAND windows_icon_resource_tests) + add_test_executable(windows_trampoline_builder_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp) + target_link_libraries(windows_trampoline_builder_tests PRIVATE relinker_core) + add_test(NAME windows_trampoline_builder COMMAND windows_trampoline_builder_tests) + add_test_executable(entry_stub_tests ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/tests/EntryStubTests.cpp) target_link_libraries(entry_stub_tests PRIVATE relinker_core) add_test(NAME entry_stub COMMAND entry_stub_tests) diff --git a/core/relinker/cli/src/CliArgs.cpp b/core/relinker/cli/src/CliArgs.cpp index 7cfe452ea7..3fa915c6d2 100644 --- a/core/relinker/cli/src/CliArgs.cpp +++ b/core/relinker/cli/src/CliArgs.cpp @@ -197,8 +197,8 @@ Args ParseArgs(int argc, char* argv[]) { if (args.windowsFixedBase && !args.toWindows) throw std::runtime_error("--windows-fixed-base requires --windows"); - if (args.traceImports && !args.toWindows) - throw std::runtime_error("--trace-imports requires --windows"); + if (args.traceImports && args.toMacos) + throw std::runtime_error("--trace-imports cannot be used with --macos"); if (args.traceImports && args.lazyBinding) throw std::runtime_error("--trace-imports conflicts with --lazy-binding"); if (args.linuxFixedBase && (args.toWindows || args.toMacos)) diff --git a/core/relinker/cli/src/NativeModules.cpp b/core/relinker/cli/src/NativeModules.cpp index c90ef5027e..f01ac374d4 100644 --- a/core/relinker/cli/src/NativeModules.cpp +++ b/core/relinker/cli/src/NativeModules.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -64,7 +65,7 @@ NativeModules ReadModules(const std::filesystem::path& inputPath, const std::fil continue; } if (!entry.is_regular_file()) continue; - if (std::filesystem::equivalent(entry.path(), inputPath)) continue; + if (Io::SameFile(entry.path(), inputPath)) continue; bool self = false; if (isElf(entry.path(), self)) paths.push_back(entry.path()); else if (self) diff --git a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp index e5314b276a..a09771a326 100644 --- a/core/relinker/codegen/include/codegen/IInstructionScanner.hpp +++ b/core/relinker/codegen/include/codegen/IInstructionScanner.hpp @@ -17,6 +17,8 @@ public: virtual void VisitCodeSection(const std::vector& codeSection, Domain::FileByteOffset codeSectionOffset, Domain::FileByteOffset codeSectionSize, const std::function& visit) const = 0; }; +std::vector MergeCodeRanges(std::vector ranges); + std::unique_ptr MakeInstructionScanner(std::function progress = {}, std::vector unwindRanges = {}); } diff --git a/core/relinker/codegen/src/Amd64OnlyConverter.cpp b/core/relinker/codegen/src/Amd64OnlyConverter.cpp index c34ae87c93..8f312031fe 100644 --- a/core/relinker/codegen/src/Amd64OnlyConverter.cpp +++ b/core/relinker/codegen/src/Amd64OnlyConverter.cpp @@ -396,6 +396,8 @@ void Amd64OnlyConverter::_convertSegment( for (auto next = item.Index + 1; siteLength < Amd64OnlySubstitutionTable::kJmpRel32.Size; ++next) { if (next >= matches.size() || matches[next].Offset != match.Offset + siteLength) throw CodegenException("AMD-only instruction too short for a jump and not followed by an instruction", fileOffset); + if (claimed.contains(next)) + throw CodegenException("AMD-only instruction too short for a jump is followed by an instruction another stub already moved", ph.Offset + matches[next].Offset); const auto& following = matches[next]; const std::span bytes(seg.data() + following.Offset, following.Length); const auto info = decoder.DecodeInstruction(bytes.data(), bytes.size()); diff --git a/core/relinker/codegen/src/InstructionScanner.cpp b/core/relinker/codegen/src/InstructionScanner.cpp index 6a6c0018b1..28f8544994 100644 --- a/core/relinker/codegen/src/InstructionScanner.cpp +++ b/core/relinker/codegen/src/InstructionScanner.cpp @@ -8,8 +8,6 @@ namespace Codegen { -namespace { - std::vector MergeCodeRanges(std::vector ranges) { std::erase_if(ranges, [](const Domain::CodeRange& range) { return range.End <= range.Begin; }); std::sort(ranges.begin(), ranges.end(), [](const Domain::CodeRange& left, const Domain::CodeRange& right) { return left.Begin < right.Begin; }); @@ -23,8 +21,6 @@ std::vector MergeCodeRanges(std::vector ra return merged; } -} - class InstructionScanner : public IInstructionScanner { public: InstructionScanner(std::function progress, std::vector unwindRanges) : progress(std::move(progress)), unwindRanges(MergeCodeRanges(std::move(unwindRanges))) {} diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 0825fa3986..739a3aea7f 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,7 @@ private: [[nodiscard]] Amd64OnlyMatch _matchMcommit(const DecodedInstruction& instr) const; [[nodiscard]] std::optional _matchRosetta(const DecodedInstruction& instr, std::span trailing) const; [[nodiscard]] const char* _emitOutOfLine(StubBodyBuilder& body, const DecodedInstruction& instr) const; + [[nodiscard]] const char* _emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const; }; Amd64OnlyMatch Amd64OnlyInstructionMatcher::_matchMovnts(const DecodedInstruction& instr, const Entry& entry) const { @@ -326,6 +328,33 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c return nullptr; } +const char* Amd64OnlyInstructionMatcher::_emitInPlace(StubBodyBuilder& body, const DecodedInstruction& instr) const { + if (const auto inPlace = Match(instr.Data, instr.Length); inPlace && inPlace->Lowering == Amd64OnlyLowering::InPlace) { + for (const auto* entry : {&kMovntss, &kMovntsd, &kMonitorx, &kMwaitx, &kRdseed, &kClwb}) { + if (inPlace->InstructionName != entry->Name) + continue; + const X64InstructionDecoder decoder; + const std::span replacement(inPlace->ReplacementBytes); + for (std::size_t offset = 0; offset < replacement.size();) { + const auto info = decoder.DecodeInstruction(replacement.data() + offset, replacement.size() - offset); + if (info.Length == 0 || info.Length > replacement.size() - offset) + throw CodegenException("In-place replacement does not split into whole instructions"); + const auto part = replacement.subspan(offset, info.Length); + if (info.HasRipRelativeDisp) { + std::int32_t displacement = 0; + std::memcpy(&displacement, part.data() + info.RipRelativeDispOffset, sizeof(displacement)); + body.RipRelative(part, info.RipRelativeDispOffset, static_cast(offset + info.Length) + displacement); + } else { + body.Raw(part); + } + offset += info.Length; + } + return entry->Name; + } + } + return nullptr; +} + void Amd64OnlyInstructionMatcher::EmitOutOfLine(StubBodyBuilder& body, const std::span instruction) const { if (_emitOutOfLine(body, DecodedInstruction{instruction.data(), instruction.size()}) == nullptr) throw CodegenException("AMD-only instruction has no out-of-line lowering"); @@ -342,7 +371,9 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( const char* name = nullptr; for (const auto& instruction : instructions) { const DecodedInstruction instr{instruction.data(), instruction.size()}; - const auto emitted = _emitOutOfLine(body, instr); + auto emitted = _emitOutOfLine(body, instr); + if (emitted == nullptr && name != nullptr) + emitted = _emitInPlace(body, instr); if (emitted == nullptr) return std::nullopt; if (name == nullptr) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 3e77c91db6..08d527462d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -633,6 +633,29 @@ void converterMonitorWait() { require(result.Bytes == expected, "MONITORX/MWAITX/MCOMMIT were replaced with the wrong bytes"); } +void converterInPlaceFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(); + const auto convert = [&](const Bytes& following) { + Bytes file(0x300, 0xCC); + Bytes text = {0x66, 0x0F, 0x79, 0xCA}; + text.insert(text.end(), following.begin(), following.end()); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + return converter->Convert(file, {segmentHeader(text.size())}); + }; + const auto tail = [](const Codegen::TrampolineSite& site, const std::size_t length) { + return Bytes(site.Body.begin() + static_cast(site.ReturnBranchOffset - length), site.Body.begin() + static_cast(site.ReturnBranchOffset)); + }; + const auto store = convert({0xF3, 0x0F, 0x2B, 0x07}); + require(store.Trampolines.size() == 1 && store.Trampolines[0].Length == 8 && tail(store.Trampolines[0], 4) == Bytes{0xF3, 0x0F, 0x11, 0x07}, "MOVNTSS after a short EXTRQ was not moved into its stub as MOVSS"); + const auto ripStore = convert({0xF2, 0x0F, 0x2B, 0x05, 0x40, 0x00, 0x00, 0x00}); + require(ripStore.Trampolines.size() == 1 && ripStore.Trampolines[0].Length == 12, "RIP-relative MOVNTSD after a short EXTRQ was not moved into its stub"); + const auto& ripSite = ripStore.Trampolines[0]; + require(tail(ripSite, 8) == Bytes{0xF2, 0x0F, 0x11, 0x05, 0x00, 0x00, 0x00, 0x00} && ripSite.Relocations.size() == 1 && ripSite.Relocations[0].SiteTarget == 12 + 0x40 && ripSite.Relocations[0].InstructionEnd == ripSite.ReturnBranchOffset, "Moved MOVNTSD has the wrong bytes or relocation"); + const auto monitor = convert({0x0F, 0x01, 0xFA}); + require(monitor.Trampolines.size() == 1 && monitor.Trampolines[0].Length == 7 && tail(monitor.Trampolines[0], 3) == Bytes{0x0F, 0x1F, 0x00}, "MONITORX after a short EXTRQ was not moved into its stub as a NOP"); +} + void converterClzero() { const auto converter = Codegen::MakeAmd64OnlyConverter(); Bytes file(0x300, 0xCC); @@ -923,6 +946,32 @@ void converterWideStoreWindows() { requireFailure([&] { (void)convert(withStore(far, tail)); }, "A 256-bit store whose movable neighbours are all beyond a short jump was kept"); } +void converterClaimedFollower() { + const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); + for (const auto& [shortInstruction, name] : {std::pair{Bytes{0x0F, 0x53, 0xCA}, "RCPPS"}, std::pair{Bytes{0xF3, 0x0F, 0xC7, 0xF8}, "RDPID"}}) { + Bytes file(0x300, 0xCC); + Bytes text = {0x74, 0x06, 0xEB, 0x00, 0xC5, 0xFC, 0x11, 0x07}; + text.insert(text.end(), shortInstruction.begin(), shortInstruction.end()); + for (int index = 0; index < 4; ++index) + text.insert(text.end(), {0x48, 0x01, 0xC8}); + text.insert(text.end(), 8, 0x90); + text.push_back(0xC3); + std::copy(text.begin(), text.end(), file.begin() + 0x200); + const auto result = converter->Convert(file, {segmentHeader(text.size())}); + for (std::size_t left = 0; left < result.Trampolines.size(); ++left) { + for (std::size_t right = left + 1; right < result.Trampolines.size(); ++right) { + const auto& a = result.Trampolines[left]; + const auto& b = result.Trampolines[right]; + require(a.Offset + a.Length <= b.Offset || b.Offset + b.Length <= a.Offset, "A short instruction's site overlaps instructions another stub already moved"); + } + } + bool kept = false; + for (const auto& report : result.Reports) + kept = kept || (report.InstructionName == name && report.Lowering == Codegen::Amd64OnlyLowering::Kept); + require(result.KeptCount == 1 && kept, "An optional short instruction followed by moved instructions was not kept"); + } +} + void converterJumpTableTargets() { const auto converter = Codegen::MakeAmd64OnlyConverter(Codegen::Amd64OnlyTarget::Rosetta); const Bytes leaTable = {0x48, 0x8D, 0x0D, 0xF9, 0x1F, 0x00, 0x00}; @@ -1997,6 +2046,7 @@ int main() { converterSha256(); converterSha1(); converterMonitorWait(); + converterInPlaceFollower(); converterClzero(); rosettaSubstitutions(); converterRosetta(); @@ -2007,6 +2057,7 @@ int main() { rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); + converterClaimedFollower(); converterJumpTableTargets(); converterReciprocal(); converterStrayRex(); diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp index a59418beea..5ca491e9bf 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/GuestModuleWriter.hpp @@ -2,14 +2,16 @@ #define ELFPATCHER_GUESTMODULEWRITER_HPP #include +#include +#include namespace Elfpatcher { class GuestModuleWriter { public: std::vector WriteLinux(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; - std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime) const; - std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath) const; + std::vector WriteWindows(const Relinker::GuestImage& image, Domain::GuestRuntime& runtime, const std::function& message = {}) const; + std::vector WriteMacOs(const Relinker::GuestImage& image, const std::vector& dependencies, const std::string& runPath, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp index 17f12e8fc6..c24b3114f0 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/linux/LinuxElfPatcher.hpp @@ -21,7 +21,8 @@ public: std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase = std::nullopt + std::optional fixedBase = std::nullopt, + bool traceImports = false ); std::vector Patch( @@ -41,6 +42,7 @@ private: std::shared_ptr _sectionHeaderTableBuilder; std::shared_ptr _byteWriter; std::optional _fixedBase; + bool _traceImports; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; void _appendTrampoline(std::vector& buf, const Codegen::TrampolineSite& site, std::uint64_t extraBlockOffset, const std::function& vaddrOfExtraBlockOffset) const; diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp index b65c2f4851..99bc214e54 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsImage.hpp @@ -5,7 +5,9 @@ #include #include #include +#include #include +#include #include namespace Elfpatcher::MacOs { @@ -59,6 +61,7 @@ struct MacOsImageInput { std::vector TlsImports; std::vector TlsExports; const std::vector* Trampolines = nullptr; + std::function Message; }; std::uint64_t GuestTlsBlockSize(const Domain::ProgramHeader& tls); diff --git a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp index 397c96e826..2f2ffbc485 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/macos/MacOsMachOPatcher.hpp @@ -3,13 +3,15 @@ #include #include +#include #include +#include namespace Elfpatcher::MacOs { class MacOsMachOPatcher final : public IElfPatcher { public: - explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt); + explicit MacOsMachOPatcher(std::optional fixedBase = std::nullopt, std::function message = {}); std::vector Patch( std::vector sourceElf, @@ -24,6 +26,7 @@ public: private: std::optional fixedBase; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp index 6fe65d2fc7..07db6f888a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsElfPatcher.hpp @@ -6,12 +6,13 @@ #include #include #include +#include namespace Elfpatcher::Windows { class WindowsPePatcher : public IElfPatcher { public: - explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false); + explicit WindowsPePatcher(bool windowsGui = false, std::optional> icon = std::nullopt, std::filesystem::path iconName = "icon0.png", std::function progress = {}, std::optional fixedBase = std::nullopt, bool traceImports = false, std::function message = {}); const std::string& GetDiagnostics() const; std::vector Patch(std::vector sourceElf, const std::vector& originalHeaders, const Domain::SysVDynamicSection& dynamicSection, std::uint64_t originalPltGotVaddr, const std::string& runPath, bool lazyBinding, bool dependencyDiagnostics, const std::vector& trampolines) override; @@ -24,6 +25,7 @@ private: std::filesystem::path iconName; std::optional fixedBase; bool traceImports; + std::function message; }; } diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp index 207ef639bf..39b4a54f87 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsTlsBuilder.hpp @@ -3,12 +3,13 @@ #include #include +#include namespace Elfpatcher::Windows { class WindowsTlsBuilder { public: - PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}) const; + PeDirectory Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva = nullptr, const std::function& progress = {}, const std::function& message = {}) const; }; } diff --git a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp index 88e0279e25..df116a307c 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include namespace Elfpatcher::Linux { @@ -143,6 +145,75 @@ void SeparateSharedLoadPages(std::vector& buf, std::vector& strings, const std::uint32_t offset) { + if (offset >= strings.size()) + throw Domain::RelinkerException("Import symbol name lies outside the dynamic string table"); + std::string value; + for (std::size_t index = offset; index < strings.size() && strings[index] != 0; ++index) + value.push_back(static_cast(strings[index])); + return value; +} + +std::vector TracedImports(const Domain::SysVDynamicSection& dynSection) { + std::vector imports; + const auto collect = [&](const std::vector& table) { + RequireWholeEntries(table, kRelaEntrySize, "Relocation table"); + for (std::size_t entry = 0; entry < table.size(); entry += kRelaEntrySize) { + const auto info = Io::ReadU64(table, entry + 8); + const auto type = static_cast(info); + const auto symbol = info >> 32; + const auto target = Io::ReadU64(table, entry); + if ((type != R_X86_64_GLOB_DAT && type != R_X86_64_JUMP_SLOT) || symbol == 0 || Io::ReadU64(table, entry + 16) != 0) + continue; + if (type == R_X86_64_GLOB_DAT && !dynSection.FunctionImports.contains(target)) + continue; + if (symbol >= dynSection.DynSymData.size() / kSymEntrySize) + throw Domain::RelinkerException("Invalid import symbol index", target); + const auto symbolOffset = static_cast(symbol * kSymEntrySize); + if (Io::ReadU16(dynSection.DynSymData, symbolOffset + 6) != SHN_UNDEF || dynSection.DynSymData[symbolOffset + 4] >> 4 == STB_WEAK) + continue; + auto name = ReadDynamicString(dynSection.DynStrData, Io::ReadU32(dynSection.DynSymData, symbolOffset)); + if (name.empty()) + continue; + const auto library = dynSection.ImportModules.find(target); + imports.push_back({target, std::move(name), library == dynSection.ImportModules.end() ? std::string{} : library->second}); + } + }; + collect(dynSection.RelaData); + collect(dynSection.RelaPltData); + return imports; +} + +std::vector TracePrologue(const std::uint64_t vaddr, const std::uint64_t table, const std::uint64_t tableEnd, const std::uint64_t function) { + std::vector code = {0x52, 0x53, 0x41, 0x54, 0x48, 0x83, 0xec, 0x08}; + const auto rip = [&](std::initializer_list opcode, const std::uint64_t target) { + code.insert(code.end(), opcode); + const auto displacement = static_cast(target) - static_cast(vaddr + code.size() + 4); + if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) + throw Domain::RelinkerException("Import trace table exceeds rel32 range"); + Io::AppendU32(code, static_cast(displacement)); + }; + rip({0x48, 0x8d, 0x1d}, table); + rip({0x4c, 0x8d, 0x25}, tableEnd); + const auto loop = code.size(); + code.insert(code.end(), {0x4c, 0x39, 0xe3, 0x73, 0x00}); + const auto exit = code.size(); + code.insert(code.end(), {0x48, 0x8b, 0x03, 0x48, 0x8b, 0x3c, 0x03, 0x48, 0x8b, 0x73, 0x08, 0x48, 0x01, 0xde, 0x48, 0x8b, 0x53, 0x10, 0x48, 0x01, 0xda}); + rip({0xff, 0x15}, function); + code.insert(code.end(), {0x48, 0x8b, 0x3b, 0x48, 0x01, 0xdf, 0x48, 0x81, 0xe7, 0x00, 0xf0, 0xff, 0xff, 0xbe, 0x00, 0x10, 0x00, 0x00, 0xba, 0x07, 0x00, 0x00, 0x00, 0x50, 0xb8, 0x0a, 0x00, 0x00, 0x00, 0x0f, 0x05, 0x58}); + code.insert(code.end(), {0x48, 0x8b, 0x0b, 0x48, 0x89, 0x04, 0x0b, 0x48, 0x83, 0xc3, 0x18, 0xeb}); + code.push_back(static_cast(static_cast(static_cast(loop) - static_cast(code.size() + 1)))); + code[exit - 1] = static_cast(code.size() - exit); + code.insert(code.end(), {0x48, 0x83, 0xc4, 0x08, 0x41, 0x5c, 0x5b, 0x5a}); + return code; +} + void RebaseProgramHeaders(std::vector& buf, const std::uint64_t tableOffset, const std::uint16_t entrySize, const std::uint16_t count, const std::uint64_t base) { for (std::uint16_t index = 0; index < count; ++index) { const auto header = static_cast(tableOffset + static_cast(index) * entrySize); @@ -170,13 +241,15 @@ LinuxElfPatcher::LinuxElfPatcher( std::shared_ptr programHeaderLayoutBuilder, std::shared_ptr sectionHeaderTableBuilder, std::shared_ptr byteWriter, - std::optional fixedBase + std::optional fixedBase, + bool traceImports ) : _entryStubBuilder(std::move(entryStubBuilder)) , _programHeaderLayoutBuilder(std::move(programHeaderLayoutBuilder)) , _sectionHeaderTableBuilder(std::move(sectionHeaderTableBuilder)) , _byteWriter(std::move(byteWriter)) , _fixedBase(fixedBase) + , _traceImports(traceImports) { } @@ -235,8 +308,13 @@ std::vector LinuxElfPatcher::Patch( throw Domain::RelinkerException("Linux target does not support --windows-diagnostics"); if (_fixedBase && lazyBinding) throw Domain::RelinkerException("A fixed Linux base requires eager binding; --lazy-binding is incompatible"); + if (_traceImports && lazyBinding) + throw Domain::RelinkerException("--trace-imports conflicts with --lazy-binding"); + if (_traceImports && std::find(dynSection.NeededLibraries.begin(), dynSection.NeededLibraries.end(), "libc.prx") == dynSection.NeededLibraries.end()) + throw Domain::RelinkerException("--trace-imports requires libc.prx among the libraries"); if (_fixedBase) RequireOnlyDependencies(dynSection.DynamicSegmentData); + const auto traced = _traceImports ? TracedImports(dynSection) : std::vector{}; const std::uint64_t base = _fixedBase.value_or(0); const auto address = [this, base](const std::uint64_t vaddr) { return _fixedBase ? Rebased(base, vaddr, "Address") : vaddr; @@ -273,17 +351,32 @@ std::vector LinuxElfPatcher::Patch( for (char c : runPath) buf.push_back(static_cast(c)); buf.push_back(0); + const std::uint64_t traceSymbolStrOff = static_cast(buf.size()) - dynStrOff; + if (_traceImports) { + for (char c : std::string_view("TraceImport_nid_no_patch")) + buf.push_back(static_cast(c)); + buf.push_back(0); + } const std::uint64_t dynStrSize = static_cast(buf.size()) - dynStrOff; alignBuf(buf, kDynStrAlignment); const auto dynSymOff = static_cast(buf.size()); - const auto symbols = table(dynSection.DynSymData, false); + auto symbols = table(dynSection.DynSymData, false); + const auto traceSymbol = symbols.size() / kSymEntrySize; + if (_traceImports) { + Io::AppendU32(symbols, static_cast(traceSymbolStrOff)); + symbols.push_back(static_cast((STB_GLOBAL << 4) | STT_FUNC)); + symbols.push_back(0); + Io::AppendU16(symbols, SHN_UNDEF); + Io::AppendU64(symbols, 0); + Io::AppendU64(symbols, 0); + } for (std::uint8_t b : symbols) buf.push_back(b); alignBuf(buf, kDynSymAlignment); const auto hashOff = static_cast(buf.size()); - const auto symbolCount = static_cast(dynSection.DynSymData.size() / kSymEntrySize); + const auto symbolCount = static_cast(symbols.size() / kSymEntrySize); Io::AppendU32(buf, 1); Io::AppendU32(buf, symbolCount); Io::AppendU32(buf, symbolCount > 1 ? 1 : 0); @@ -292,7 +385,13 @@ std::vector LinuxElfPatcher::Patch( alignBuf(buf, kDynSymAlignment); const auto relaOff = static_cast(buf.size()); - const auto relocations = table(dynSection.RelaData, true); + auto relocations = table(dynSection.RelaData, true); + const auto traceRelocation = relocations.size(); + if (_traceImports) { + Io::AppendU64(relocations, 0); + Io::AppendU64(relocations, (static_cast(traceSymbol) << 32) | R_X86_64_GLOB_DAT); + Io::AppendU64(relocations, 0); + } for (std::uint8_t b : relocations) buf.push_back(b); alignBuf(buf, kRelaAlignment); @@ -321,9 +420,9 @@ std::vector LinuxElfPatcher::Patch( _appendDynEntry(dynSegBuf, DT_SYMENT, kSymEntrySize); _appendDynEntry(dynSegBuf, DT_HASH, address(vaddrOfExtraBlockOffset(hashOff))); _appendDynEntry(dynSegBuf, DT_DEBUG, 0); - if (!dynSection.RelaData.empty()) { + if (!relocations.empty()) { _appendDynEntry(dynSegBuf, DT_RELA, address(vaddrOfExtraBlockOffset(relaOff))); - _appendDynEntry(dynSegBuf, DT_RELASZ, dynSection.RelaData.size()); + _appendDynEntry(dynSegBuf, DT_RELASZ, relocations.size()); _appendDynEntry(dynSegBuf, DT_RELAENT, kRelaEntrySize); } if (!dynSection.RelaPltData.empty()) { @@ -341,12 +440,52 @@ std::vector LinuxElfPatcher::Patch( for (std::uint8_t b : dynSegBuf) buf.push_back(b); + std::uint64_t traceFunctionVaddr = 0; + std::uint64_t traceTableVaddr = 0; + std::uint64_t traceTableEndVaddr = 0; + std::vector traceEntries; + if (_traceImports) { + alignBuf(buf, 8); + const auto functionOff = static_cast(buf.size()); + traceFunctionVaddr = vaddrOfExtraBlockOffset(functionOff); + Io::AppendU64(buf, 0); + Io::WriteU64(buf, static_cast(relaOff + traceRelocation), address(traceFunctionVaddr)); + traceTableVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::size_t index = 0; index < traced.size(); ++index) { + traceEntries.push_back(buf.size()); + buf.resize(buf.size() + 24); + } + traceTableEndVaddr = vaddrOfExtraBlockOffset(buf.size()); + std::map strings; + const auto addString = [&](const std::string& value) { + if (const auto existing = strings.find(value); existing != strings.end()) return existing->second; + const auto vaddr = vaddrOfExtraBlockOffset(buf.size()); + for (char c : value) + buf.push_back(static_cast(c)); + buf.push_back(0); + strings.emplace(value, vaddr); + return vaddr; + }; + for (std::size_t index = 0; index < traced.size(); ++index) { + const auto entryVaddr = vaddrOfExtraBlockOffset(traceEntries[index]); + const auto relative = [entryVaddr](const std::uint64_t vaddr) { return vaddr - entryVaddr; }; + Io::WriteU64(buf, traceEntries[index], relative(traced[index].Slot)); + Io::WriteU64(buf, traceEntries[index] + 8, relative(addString(traced[index].Name))); + Io::WriteU64(buf, traceEntries[index] + 16, relative(addString(traced[index].Library))); + } + } + const std::uint64_t realEntryVaddr = *reinterpret_cast(buf.data() + kEhdrEntryOffset); const auto stubOff = static_cast(buf.size()); const auto stubVaddr = vaddrOfExtraBlockOffset(stubOff); - const auto stubBytes = _entryStubBuilder->BuildEntryStub(address(stubVaddr), address(realEntryVaddr)); - for (std::uint8_t b : stubBytes) + if (_traceImports) { + for (std::uint8_t b : TracePrologue(stubVaddr, traceTableVaddr, traceTableEndVaddr, traceFunctionVaddr)) + buf.push_back(b); + } + const auto entryStubVaddr = vaddrOfExtraBlockOffset(buf.size()); + for (std::uint8_t b : _entryStubBuilder->BuildEntryStub(address(entryStubVaddr), address(realEntryVaddr))) buf.push_back(b); + const auto stubSize = static_cast(buf.size()) - stubOff; _byteWriter->WriteU64(buf, kEhdrEntryOffset, address(stubVaddr)); for (const auto& site : trampolines) @@ -395,11 +534,11 @@ std::vector LinuxElfPatcher::Patch( sectionRequest.DynStrOffset = dynStrOff; sectionRequest.DynStrSize = dynStrSize; sectionRequest.DynSymOffset = dynSymOff; - sectionRequest.DynSymSize = dynSection.DynSymData.size(); + sectionRequest.DynSymSize = symbols.size(); sectionRequest.DynamicSegmentOffset = dynSegOff; sectionRequest.DynamicSegmentSize = dynSegBuf.size(); sectionRequest.StubOffset = stubOff; - sectionRequest.StubSize = stubBytes.size(); + sectionRequest.StubSize = stubSize; _sectionHeaderTableBuilder->WriteTable(buf, sectionRequest); diff --git a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp index a88271a519..76b91f97dc 100644 --- a/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/linux/LinuxGuestModuleWriter.cpp @@ -35,7 +35,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma const auto extraOffset = bytes.size(); const auto extraAddress = Io::AlignUp64(end, 0x4000); const auto address = [&] { return extraAddress + bytes.size() - extraOffset; }; - const bool passesModuleArgs = image.Init != 0; + const bool passesModuleArgs = image.HasInit; std::uint64_t argsSlot = 0; std::uint64_t resultSlot = 0; if (passesModuleArgs) { @@ -127,7 +127,7 @@ std::vector GuestModuleWriter::WriteLinux(const Relinker::GuestIma return static_cast(static_cast(distance)); }; const auto lifecycle = [&](std::uint64_t target) { - if (target == 0) return std::uint64_t{}; + if (!image.HasInit) return std::uint64_t{}; const auto start = address(); std::vector stub(kStubSize, 0xc3); stub[0x00] = 0x50; diff --git a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp index 74e31d19d3..7f8032b78a 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsGuestModuleWriter.cpp @@ -21,7 +21,7 @@ std::string DynamicName(const Relinker::GuestImage& guest, const std::size_t ind } -std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath) const { +std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestImage& guest, const std::vector& dependencies, const std::string& runPath, const std::function& message) const { using namespace MacOs; Windows::WindowsLoadImage image(guest.Bytes, guest.Headers, false); MacOsImageInput input; @@ -30,6 +30,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.Source = &guest.Bytes; input.Headers = &guest.Headers; input.Image = ℑ + input.Message = message; const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); std::map targets; @@ -109,7 +110,7 @@ std::vector GuestModuleWriter::WriteMacOs(const Relinker::GuestIma input.RunPaths = MachORunPaths(runPath, "@loader_path"); for (const auto slot : guest.InitArray) input.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) input.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - input.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + input.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; input.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); input.Trampolines = &guest.Trampolines; try { diff --git a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp index 3c1ee3d922..4c80a48eae 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsImage.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsImage.cpp @@ -250,10 +250,10 @@ struct TlsScan { std::vector GsTraps; }; -TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image) { +TlsScan ScanTls(const std::vector& source, const std::vector& headers, const Windows::WindowsLoadImage& image, const std::function& message) { TlsScan scan; const Codegen::X64InstructionDecoder decoder; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, {}, nullptr, message); for (const auto& header : headers) { if (header.Type == 7) { if (scan.Tls != nullptr) throw Domain::RelinkerException("Multiple ELF TLS segments"); @@ -411,7 +411,7 @@ std::vector WriteMacOsImage(MacOsImageInput& input) { auto& image = *input.Image; const auto& headers = *input.Headers; const auto imageBase = image.GetImageBase(); - const auto tls = ScanTls(*input.Source, headers, image); + const auto tls = ScanTls(*input.Source, headers, image, input.Message); const bool hasTls = tls.Tls != nullptr && (tls.Tls->MemorySize != 0 || !tls.Accesses.empty()); if (!hasTls && !tls.Accesses.empty()) throw Domain::RelinkerException("Guest TLS access without a non-empty PT_TLS"); if (!hasTls && !input.TlsModuleSlots.empty()) throw Domain::RelinkerException("Guest TLS module relocation without a non-empty PT_TLS"); diff --git a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp index f86bf76fcd..ff62ba6e2d 100644 --- a/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp +++ b/core/relinker/elfpatcher/src/macos/MacOsMachOPatcher.cpp @@ -51,7 +51,7 @@ std::string MachODependency(const std::string& name, const std::string& origin) return "@rpath/" + name; } -MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase) : fixedBase(fixedBase) { +MacOsMachOPatcher::MacOsMachOPatcher(const std::optional fixedBase, std::function message) : fixedBase(fixedBase), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed macOS guest image base"); } @@ -71,6 +71,7 @@ std::vector MacOsMachOPatcher::Patch(std::vector sou input.Source = &sourceElf; input.Headers = &originalHeaders; input.Image = ℑ + input.Message = message; for (std::size_t offset = 24; offset < dynamicSection.DynSymData.size(); offset += 24) { const auto& symbols = dynamicSection.DynSymData; const auto section = Io::ReadU16(symbols, offset + 6); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 87649a5534..0b284a4bcd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -13,7 +13,7 @@ namespace Elfpatcher { -std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime) const { +std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestImage& guest, Domain::GuestRuntime& runtime, const std::function& message) const { using namespace Windows; if (guest.Symbolic) throw Domain::RelinkerException("DF_SYMBOLIC guest modules are not supported by the Windows writer"); WindowsLoadImage image(guest.Bytes, guest.Headers, false); @@ -84,7 +84,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI std::array directories{}; std::uint32_t tlsIndex = 0; try { - directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex); + directories[9] = WindowsTlsBuilder().Build(guest.Bytes, guest.Headers, image, sections, relocations, nextRva, &tlsIndex, {}, message); } catch (Domain::RelinkerException& error) { if (error.InputPath.empty()) error.InputPath = Io::Utf8Path(guest.SourcePath); throw; @@ -150,7 +150,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } if (runtime.DeferInitialization) { PeSection lifecycle{".ginit", nextRva, SectionRead | 0x40u, {}}; - Io::AppendU32(lifecycle.Data, guest.Init == 0 ? 0 : image.GetRva(guest.Init)); + Io::AppendU32(lifecycle.Data, guest.HasInit ? image.GetRva(guest.Init) : 0); Io::AppendU32(lifecycle.Data, CheckedRva(guest.InitArray.size())); for (const auto slot : guest.InitArray) Io::AppendU32(lifecycle.Data, image.GetRva(slot, 8)); if (!exports.emplace(Relinker::GuestInitializeExport, nextRva).second) throw Domain::RelinkerException("Duplicate guest export: " + std::string(Relinker::GuestInitializeExport)); @@ -203,7 +203,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI sections.push_back({".dllmain", entry, SectionRead | SectionExecute | 0x20u, {0xb8, 1, 0, 0, 0, 0xc3}}); for (const auto slot : guest.InitArray) runtime.InitArrayRvas.push_back(image.GetRva(slot, 8)); for (const auto slot : guest.FiniArray) runtime.FiniArrayRvas.push_back(image.GetRva(slot, 8)); - runtime.InitRva = guest.Init == 0 ? 0 : image.GetRva(guest.Init); + runtime.InitRva = guest.HasInit ? image.GetRva(guest.Init) : 0; runtime.UnmatchedImportModules.assign(guest.Dynamic.UnmatchedImportModules.begin(), guest.Dynamic.UnmatchedImportModules.end()); runtime.FiniRva = guest.Fini == 0 ? 0 : image.GetRva(guest.Fini); auto result = WindowsPeWriter().Write(sections, entry, directories); diff --git a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp index 0ee6929940..4a547373bd 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsIconResourceBuilder.cpp @@ -16,7 +16,10 @@ std::uint32_t readBigEndian(const std::vector& bytes, const std::s } void validatePng(const std::vector& bytes, const std::filesystem::path& path) { - const auto fail = [&path](const std::string& reason) { throw Domain::RelinkerException("Invalid Windows icon PNG '" + path.string() + "': " + reason); }; + const auto fail = [&path](const std::string& reason) { + const auto utf8 = path.u8string(); + throw Domain::RelinkerException("Invalid Windows icon PNG '" + std::string(utf8.begin(), utf8.end()) + "': " + reason); + }; constexpr std::array signature{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; if (bytes.size() < 33 || !std::equal(signature.begin(), signature.end(), bytes.begin())) fail("missing PNG signature or IHDR"); if (readBigEndian(bytes, 8) != 13 || readBigEndian(bytes, 12) != 0x49484452) fail("invalid IHDR size or type"); diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 518f0d25d7..063af7924b 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -28,7 +28,7 @@ void writeGotStub(std::vector& sections, const std::uint32_t targetRv } -WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports) { +WindowsPePatcher::WindowsPePatcher(const bool windowsGui, std::optional> icon, std::filesystem::path iconName, std::function progress, const std::optional fixedBase, const bool traceImports, std::function message) : progress(std::move(progress)), windowsGui(windowsGui), icon(std::move(icon)), iconName(std::move(iconName)), fixedBase(fixedBase), traceImports(traceImports), message(std::move(message)) { if (fixedBase && (*fixedBase < 0x20000 || *fixedBase > 0x7fffffff0000 || (*fixedBase & 0xffff) != 0)) throw Domain::RelinkerException("Invalid fixed Windows guest image base"); } @@ -68,7 +68,7 @@ std::vector WindowsPePatcher::Patch(std::vector sour sections.push_back({".ehmeta", nextRva, SectionRead | 0x40u, std::move(metadata)}); nextRva = AlignRva(nextRva + sections.back().Data.size()); } - directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }); + directories[9] = WindowsTlsBuilder().Build(sourceElf, originalHeaders, image, sections, relocations.BaseRelocations, nextRva, nullptr, [&](auto completed, auto total) { report(1500 + 6500 * completed / total); }, message); report(8000); WindowsTrampolineBuilder().Build(trampolines, image, sections, nextRva); const WindowsImportBuilder importBuilder; diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 08576fe5f8..8bee6f3468 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -107,7 +107,7 @@ void trapInstruction(std::vector& sections, const std::uint32_t rva, } -PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { +PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress, const std::function& message) const { const auto report = [&](std::uint64_t completed) { if (progress) progress(completed, 10000); }; const Domain::ProgramHeader* tls = nullptr; const Codegen::X64InstructionDecoder decoder; @@ -116,7 +116,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co std::vector> gsTraps; std::vector branchTargets; std::set entryRoots; - const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); + const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots, message); std::uint64_t scanned = 0; for (const auto& header : headers) { diff --git a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp index f8d06aa53d..1fcf78fad7 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTrampolineBuilder.cpp @@ -5,18 +5,28 @@ #include #include #include +#include #include namespace Elfpatcher::Windows { namespace { +struct SitePatch { + PeSection* Section = nullptr; + std::ptrdiff_t Offset = 0; + std::size_t Length = 0; + std::vector JumpBytes; +}; + PeSection& findSection(std::vector& sections, const std::uint32_t rva, const std::size_t length) { for (auto& section : sections) { if (rva < section.Rva || rva - section.Rva >= section.Data.size()) continue; if (length > section.Data.size() - (rva - section.Rva)) throw Domain::RelinkerException("AMD-only instruction crosses a PE section boundary", rva); + if ((section.Characteristics & SectionExecute) == 0) + throw Domain::RelinkerException("AMD-only instruction is in a non-executable PE section", rva); return section; } throw Domain::RelinkerException("AMD-only instruction is outside the PE image", rva); @@ -28,12 +38,26 @@ void WindowsTrampolineBuilder::Build(const std::vector& using namespace Codegen::Amd64OnlySubstitutionTable; if (sites.empty()) return; + if (nextRva < LoadRva || nextRva % SectionAlignment != 0) + throw Domain::RelinkerException("Invalid Windows trampoline section RVA", nextRva); + if (std::any_of(sections.begin(), sections.end(), [](const auto& section) { return section.Name == ".amdstub"; })) + throw Domain::RelinkerException("Duplicate Windows trampoline section"); + if (!sections.empty() && nextRva < AlignRva(static_cast(sections.back().Rva) + sections.back().MemorySize())) + throw Domain::RelinkerException("Windows trampoline section overlaps preceding section", nextRva); const auto sectionRva = nextRva; std::vector bytes; + std::map ranges; + std::vector patches; + patches.reserve(sites.size()); for (const auto& site : sites) { const auto siteRva = image.GetRva(site.Address, site.Length); if (site.Length < kJmpRel32.Size || site.OriginalBytes.size() != site.Length || site.Body.size() < kJmpRel32.Size || site.ReturnBranchOffset > site.Body.size() - kJmpRel32.Size || site.Body[site.ReturnBranchOffset] != kJmpRel32.Bytes[0]) throw Domain::RelinkerException("Invalid AMD-only trampoline site", siteRva); + const auto siteEndRva = static_cast(siteRva) + site.Length; + const auto next = ranges.lower_bound(siteRva); + if ((next != ranges.end() && next->first < siteEndRva) || (next != ranges.begin() && std::prev(next)->second > siteRva)) + throw Domain::RelinkerException("Overlapping AMD-only trampoline sites", siteRva); + ranges.emplace(siteRva, siteEndRva); auto& section = findSection(sections, siteRva, site.Length); const auto offset = static_cast(siteRva - section.Rva); if (!std::equal(site.OriginalBytes.begin(), site.OriginalBytes.end(), section.Data.begin() + offset)) @@ -43,19 +67,22 @@ void WindowsTrampolineBuilder::Build(const std::vector& const auto bodyOffset = bytes.size(); bytes.insert(bytes.end(), site.Body.begin(), site.Body.end()); Codegen::ApplyStubRelocations(std::span(bytes.data() + bodyOffset, site.ReturnBranchOffset), site.Relocations, siteRva, stubRva, site.Offset); - const auto returnRva = CheckedRva(siteRva + site.Length); + const auto returnRva = CheckedRva(siteEndRva); const auto displacement = static_cast(returnRva) - (static_cast(stubRva) + static_cast(site.ReturnBranchOffset + kJmpRel32.Size)); if (displacement < std::numeric_limits::min() || displacement > std::numeric_limits::max()) throw Domain::RelinkerException("AMD-only stub return exceeds rel32 range", returnRva); Io::WriteU32(bytes, bodyOffset + site.ReturnBranchOffset + 1, static_cast(displacement)); WindowsStubEmitter jump(siteRva); jump.Rip({0xe9}, stubRva); - const auto jumpBytes = jump.TakeBytes(); - std::fill_n(section.Data.begin() + offset, site.Length, kNop1.Bytes[0]); - std::copy(jumpBytes.begin(), jumpBytes.end(), section.Data.begin() + offset); + patches.push_back({§ion, offset, site.Length, jump.TakeBytes()}); + } + const auto endRva = AlignRva(sectionRva + bytes.size()); + for (const auto& patch : patches) { + std::fill_n(patch.Section->Data.begin() + patch.Offset, patch.Length, kNop1.Bytes[0]); + std::copy(patch.JumpBytes.begin(), patch.JumpBytes.end(), patch.Section->Data.begin() + patch.Offset); } sections.push_back({".amdstub", sectionRva, SectionRead | SectionExecute | 0x20u, std::move(bytes)}); - nextRva = AlignRva(sectionRva + sections.back().Data.size()); + nextRva = endRva; } } diff --git a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp index dfedf0f6ab..a3813723aa 100644 --- a/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp +++ b/core/relinker/elfpatcher/tests/WindowsIconResourceTests.cpp @@ -54,13 +54,14 @@ void checkChunks() { } for (const auto& value : chunks) image.insert(image.end(), value.begin(), value.end()); image.insert(image.end(), Png.end() - 12, Png.end()); - const std::filesystem::path path = std::filesystem::path(name) / "icon.png"; + const std::filesystem::path path = std::filesystem::path(std::u8string(name.begin(), name.end())) / "icon.png"; + const auto utf8 = path.u8string(); std::vector sections; bool rejected = false; try { builder.Build(image, path, sections, 0x10000); } catch (const Domain::RelinkerException& error) { rejected = true; - require(std::string(error.what()).find(path.string()) != std::string::npos, "Icon failure omitted the path"); + require(std::string(error.what()).find(std::string(utf8.begin(), utf8.end())) != std::string::npos, "Icon failure omitted the path"); } require(rejected != valid, "Unexpected validation result for " + name); require(sections.size() == (valid ? 1u : 0u), "Unexpected section mutation for " + name); @@ -119,7 +120,8 @@ void run() { require(readU32(data, 174) == Png.size(), "Unexpected icon size in group entry"); require(std::equal(Png.begin(), Png.end(), data.begin() + 184), "Icon bytes changed"); - const auto badPath = std::filesystem::path("bad/icon0.png"); + const std::filesystem::path badPath(u8"\u65e5\u672c/bad/icon0.png"); + const auto badUtf8 = badPath.u8string(); auto truncated = Png; truncated.resize(40); std::vector badSections; @@ -127,7 +129,7 @@ void run() { try { builder.Build(truncated, badPath, badSections, nextRva); } catch (const Domain::RelinkerException& e) { - require(std::string(e.what()).find(badPath.string()) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); + require(std::string(e.what()).find(std::string(badUtf8.begin(), badUtf8.end())) != std::string::npos, std::string("Failure does not name the icon: ") + e.what()); require(badSections.empty(), "Section added despite the failure"); caughtTruncated = true; } diff --git a/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp new file mode 100644 index 0000000000..b710c96f05 --- /dev/null +++ b/core/relinker/elfpatcher/tests/WindowsTrampolineBuilderTests.cpp @@ -0,0 +1,118 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace Elfpatcher::Windows; +using Bytes = std::vector; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +template +void write(Bytes& bytes, const std::size_t offset, const TValue value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +const Bytes SiteBytes{0xF2, 0x0F, 0x78, 0xDB, 0x08, 0x08}; +const Bytes StubBody{0x90, 0xE9, 0x00, 0x00, 0x00, 0x00}; + +Bytes elfFixture() { + Bytes bytes(0x400); + bytes[0] = 0x7F; + bytes[1] = 'E'; + bytes[2] = 'L'; + bytes[3] = 'F'; + bytes[4] = 2; + bytes[5] = 1; + bytes[6] = 1; + write(bytes, 16, 3); + write(bytes, 18, 62); + write(bytes, 24, 0x1000); + std::fill(bytes.begin() + 0x200, bytes.begin() + 0x300, 0xCC); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x202); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x210); + std::copy(SiteBytes.begin(), SiteBytes.end(), bytes.begin() + 0x302); + return bytes; +} + +std::vector elfHeaders() { + return {{1, 5, 0x200, 0x1000, 0, 0x100, 0x100, 0x1000}, {1, 6, 0x300, 0x2000, 0, 0x100, 0x100, 0x1000}}; +} + +const Bytes Source = elfFixture(); + +Codegen::TrampolineSite site(const std::uint64_t address) { + const auto offset = static_cast(address & 0xFFF) + (address < 0x2000 ? 0x200 : 0x300); + return {static_cast(offset), address, SiteBytes.size(), {Source.begin() + offset, Source.begin() + offset + static_cast(SiteBytes.size())}, StubBody, 1}; +} + +void requireRejected(const WindowsLoadImage& image, const std::vector& sites, std::vector sections, std::uint32_t nextRva, const std::string& expected) { + const auto original = sections; + const auto originalRva = nextRva; + try { + WindowsTrampolineBuilder().Build(sites, image, sections, nextRva); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error '" + std::string(error.what()) + "', expected '" + expected + "'"); + require(sections.size() == original.size() && nextRva == originalRva, "Rejected build changed the section list for '" + expected + "'"); + for (std::size_t index = 0; index < sections.size(); ++index) + require(sections[index].Data == original[index].Data, "Rejected build patched section " + sections[index].Name + " for '" + expected + "'"); + return; + } + throw std::runtime_error("Build succeeded, expected '" + expected + "'"); +} + +void run() { + const WindowsLoadImage image(Source, elfHeaders()); + const auto endRva = image.GetEndRva(); + + auto sections = image.BuildSections(); + auto nextRva = endRva; + WindowsTrampolineBuilder().Build({site(0x1002), site(0x1010)}, image, sections, nextRva); + require(sections.size() == 3 && sections.back().Name == ".amdstub" && sections.back().Rva == endRva, "Stub section was not appended"); + require(nextRva == AlignRva(static_cast(endRva) + sections.back().Data.size()), "Next RVA does not follow the stub section"); + require(sections[0].Data[2] == 0xE9 && sections[0].Data[7] == 0x90 && sections[0].Data[0x10] == 0xE9 && sections[0].Data[0x15] == 0x90, "Sites were not replaced by jumps"); + + const auto fresh = image.BuildSections(); + requireRejected(image, {site(0x1002), site(0x1004)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1004), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x1002), site(0x1002)}, fresh, endRva, "Overlapping AMD-only trampoline sites"); + requireRejected(image, {site(0x2002)}, fresh, endRva, "AMD-only instruction is in a non-executable PE section"); + + auto changed = site(0x1010); + changed.OriginalBytes[3] = 0xDC; + requireRejected(image, {site(0x1002), changed}, fresh, endRva, "AMD-only site bytes changed before patching"); + + requireRejected(image, {site(0x1002)}, fresh, endRva + 0x10, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, LoadRva - SectionAlignment, "Invalid Windows trampoline section RVA"); + requireRejected(image, {site(0x1002)}, fresh, endRva - SectionAlignment, "Windows trampoline section overlaps preceding section"); + requireRejected(image, {site(0x1002)}, sections, nextRva, "Duplicate Windows trampoline section"); + + auto untouched = image.BuildSections(); + auto untouchedRva = endRva; + WindowsTrampolineBuilder().Build({}, image, untouched, untouchedRva); + require(untouched.size() == fresh.size() && untouchedRva == endRva, "Empty site list changed the image"); +} + +} + +int main() { + try { + run(); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } + return 0; +} diff --git a/core/relinker/io/include/io/FileIdentity.hpp b/core/relinker/io/include/io/FileIdentity.hpp new file mode 100644 index 0000000000..e33cfc5337 --- /dev/null +++ b/core/relinker/io/include/io/FileIdentity.hpp @@ -0,0 +1,12 @@ +#ifndef IO_FILEIDENTITY_HPP +#define IO_FILEIDENTITY_HPP + +#include + +namespace Io { + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second); + +} + +#endif diff --git a/core/relinker/io/src/FileIdentity.cpp b/core/relinker/io/src/FileIdentity.cpp new file mode 100644 index 0000000000..99d7ff2fe5 --- /dev/null +++ b/core/relinker/io/src/FileIdentity.cpp @@ -0,0 +1,37 @@ +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +namespace Io { + +#ifdef _WIN32 +namespace { + +std::tuple FileIdentity(const std::filesystem::path& path) { + const auto handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::filesystem::filesystem_error("Cannot open file identity", path, std::error_code(static_cast(GetLastError()), std::system_category())); + BY_HANDLE_FILE_INFORMATION information{}; + const bool read = GetFileInformationByHandle(handle, &information) != 0; + const auto error = GetLastError(); + CloseHandle(handle); + if (!read) throw std::filesystem::filesystem_error("Cannot read file identity", path, std::error_code(static_cast(error), std::system_category())); + return {information.dwVolumeSerialNumber, information.nFileIndexHigh, information.nFileIndexLow}; +} + +} +#endif + +bool SameFile(const std::filesystem::path& first, const std::filesystem::path& second) { +#ifdef _WIN32 + return FileIdentity(first) == FileIdentity(second); +#else + return std::filesystem::equivalent(first, second); +#endif +} + +} diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index dee892bb9b..33240459b0 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -104,11 +105,9 @@ public: } } - 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(); } @@ -118,25 +117,7 @@ private: }; 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; + return Io::SameFile(first, second); } bool SameOutputDestination(const std::filesystem::path& first, const std::filesystem::path& second) { @@ -305,10 +286,10 @@ int Run(const int argc, char* argv[]) { const auto relative = ExecutableRelative(executable); const auto input = ResolveExtraExecutable(moduleRoot, moduleDirectories, relative, executable); const auto output = Relinker::GamePatch::OutputExecutablePath(Io::NativePath(absPath), executable); - if (std::filesystem::equivalent(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(Io::NativePath(args.inputPath), input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (output.lexically_normal() == Io::NativePath(absPath).lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); for (const auto& extra : extraExecutables) { - if (std::filesystem::equivalent(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); + if (Io::SameFile(extra.Input, input)) throw Domain::RelinkerException("Executable is converted twice: " + executable); if (extra.Output.lexically_normal() == output.lexically_normal()) throw Domain::RelinkerException("Executable outputs collide: " + Io::Utf8Path(output)); } extraExecutables.push_back({executable, relative, input, output}); diff --git a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp index de93097012..cdd140af64 100644 --- a/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp +++ b/core/relinker/relinker/include/relinker/analysis/CodeInstructionCollector.hpp @@ -4,12 +4,13 @@ #include #include #include +#include namespace Relinker { class CodeInstructionCollector { public: - std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr) const; + std::set Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress = {}, std::set* entryRoots = nullptr, const std::function& message = {}) const; }; } diff --git a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp index 3e8bc10152..b80aae4651 100644 --- a/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp +++ b/core/relinker/relinker/include/relinker/analysis/RelativeJumpTables.hpp @@ -16,7 +16,7 @@ struct RelativeJumpTable { VirtualAddress End; }; -std::size_t FindTrailingRelativeJumpTable(std::span code); +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress = 0, std::size_t* tableEnd = nullptr, std::size_t unwindEnd = 0); std::vector FindRelativeJumpTables(std::span segment, VirtualAddress segmentVAddr, const std::vector>& functions); } diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index f49bd4c0ae..6be45157af 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -52,6 +52,7 @@ struct GuestImage { std::vector InitArray; std::vector FiniArray; std::uint64_t Init = 0; + bool HasInit = false; std::uint64_t Fini = 0; std::uint64_t Got = 0; bool UsePlatformTlsResolver = true; diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index e7a89396b1..645dd46f3a 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +40,7 @@ public: } -std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots) const { +std::set CodeInstructionCollector::Collect(const std::vector& bytes, const std::vector& headers, const std::function& progress, std::set* entryRoots, const std::function& message) const { std::uint64_t lastProgress = 0; const auto report = [&](std::uint64_t completed) { if (completed <= lastProgress) return; @@ -96,21 +98,34 @@ std::set CodeInstructionCollector::Collect(const std::ve range(offset, size); return offset; }; + std::vector sceFunctions; + const auto trimToCode = [&](Domain::ProgramHeader& header, std::uint64_t end, bool interpreterAnywhere) { + const std::span segment(bytes.data() + header.Offset, header.FileSize); + auto begin = header.MappedAddress; + for (const auto& interpreter : headers) + if (interpreter.Type == 3 && interpreter.MappedAddress < end && + (interpreterAnywhere ? interpreter.MappedAddress >= begin : interpreter.MappedAddress == begin)) + begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); + header.Offset += begin - header.MappedAddress; + header.MappedAddress = begin; + header.FileSize = end - begin; + }; if (sceBase) { const auto unwindFunctions = UnusedNidFilter::ReadExceptionFunctions(bytes, headers, {}, {}); + for (const auto& function : unwindFunctions) sceFunctions.push_back({function.Begin, function.End}); + sceFunctions = Codegen::MergeCodeRanges(std::move(sceFunctions)); for (auto& header : codeHeaders) { if (header.Type != 1 || (header.Flags & 1) == 0) continue; range(header.Offset, header.FileSize); const std::span segment(bytes.data() + header.Offset, header.FileSize); - const auto end = FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize); - auto begin = header.MappedAddress; - for (const auto& interpreter : headers) - if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); - const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); - if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); - header.Offset += begin - header.MappedAddress; - header.MappedAddress = begin; - header.FileSize = end - begin; + trimToCode(header, FindSceCodeEnd(segment, header.MappedAddress, unwindFunctions, value(3, 0x61000027).value_or(0), value(2, 0x6100002d).value_or(0)).value_or(header.MappedAddress + header.FileSize), true); + } + } else { + for (auto& header : codeHeaders) { + if (header.Type != 1 || (header.Flags & 1) == 0) continue; + trimToCode(header, header.MappedAddress + header.FileSize, false); } } std::set roots; @@ -118,10 +133,77 @@ std::set CodeInstructionCollector::Collect(const std::ve std::map tables; const auto inTable = [&](std::uint64_t address) { const auto after = tables.upper_bound(address); return after != tables.begin() && address < std::prev(after)->second; }; const auto addRoot = [&](std::uint64_t address) { if (isCode(address) && !inTable(address)) roots.insert(address); }; + const auto sceFunctionAfter = [&](std::uint64_t address) { + return std::upper_bound(sceFunctions.begin(), sceFunctions.end(), address, [](std::uint64_t value, const Domain::CodeRange& function) { return value < function.Begin; }); + }; + const auto insideSceFunction = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + return after != sceFunctions.begin() && address < std::prev(after)->End; + }; + const auto nearSceFunctionEnd = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + if (after == sceFunctions.begin()) return false; + const auto end = std::prev(after)->End; + return address >= end && address - end < 256; + }; + const auto codeAt = [&](std::uint64_t address) { + for (const auto& header : codeHeaders) + if (header.Type == 1 && (header.Flags & 1) != 0 && address >= header.MappedAddress && address - header.MappedAddress < header.FileSize) + return std::span(bytes.data() + header.Offset + (address - header.MappedAddress), header.FileSize - (address - header.MappedAddress)); + return std::span(); + }; + const auto absoluteFsAccess = [](const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { + return info.SegmentPrefix == 0x64 && info.OpcodeOffset + 2 < info.Length && (code[info.OpcodeOffset + 1] & 0xc7) == 0x04 && code[info.OpcodeOffset + 2] == 0x25; + }; + struct GapRun { + bool Transfer = false; + bool AbsoluteFsAccess = false; + }; + const auto decodeGapRun = [&](std::uint64_t address) { + const auto after = sceFunctionAfter(address); + const auto limit = after == sceFunctions.end() ? std::numeric_limits::max() : after->Begin; + const Codegen::X64InstructionDecoder probe; + GapRun run; + for (std::size_t count = 0; count < 256 && address < limit; ++count) { + const auto code = codeAt(address); + if (code.empty()) return run; + const auto available = std::min(code.size(), limit - address); + Codegen::DecodedInstructionInfo info; + try { + info = probe.DecodeInstruction(code.data(), available); + } catch (const Codegen::CodegenException&) { + return run; + } + if (info.Length == 0 || info.Length > available) return run; + if (absoluteFsAccess(code.data(), info)) run.AbsoluteFsAccess = true; + switch (info.FlowKind) { + case Codegen::ControlFlowKind::UnconditionalJump: + case Codegen::ControlFlowKind::Return: + case Codegen::ControlFlowKind::IndirectJump: + case Codegen::ControlFlowKind::Trap: + run.Transfer = true; + return run; + default: + break; + } + address += info.Length; + } + return run; + }; + std::set dataTargets; + const auto addPointerRoot = [&](std::uint64_t address) { + if (sceFunctions.empty() || !isCode(address) || insideSceFunction(address)) { + addRoot(address); + return; + } + const auto run = decodeGapRun(address); + if (run.AbsoluteFsAccess || (address % 16 == 0 && nearSceFunctionEnd(address) && run.Transfer)) addRoot(address); + else dataTargets.insert(address); + }; const auto addAddressTakenRoot = [&](std::uint64_t address, const std::uint8_t* code, const Codegen::DecodedInstructionInfo& info) { if (!info.HasRipRelativeDisp || code[info.OpcodeOffset] != 0x8d || !(info.RexPrefix & 8)) return; const auto displacement = std::bit_cast(Io::ReadU32(bytes, fileOffset(address, info.Length) + info.RipRelativeDispOffset)); - addRoot(address + info.Length + static_cast(displacement)); + addPointerRoot(address + info.Length + static_cast(displacement)); }; const auto addFunction = [&](std::uint64_t begin, std::uint64_t size, bool symbolAlias) { if (size == 0) return; @@ -145,7 +227,8 @@ std::set CodeInstructionCollector::Collect(const std::ve }; range(0, 64); if (const auto entry = Io::ReadU64(bytes, 24); entry != 0) addRoot(entry); - for (const auto tag : {12u, 13u}) if (const auto address = value(tag, 0x60000000 + tag); address && *address != 0) addRoot(*address); + if (const auto init = value(12, 0x6000000c)) addRoot(*init); + if (const auto fini = value(13, 0x6000000d); fini && *fini != 0) addRoot(*fini); const auto symbolBytes = value(0x6100003f, 0xffffffffffffffffull); std::uint64_t symbolCount = 0; if (symbolBytes) { @@ -226,7 +309,7 @@ std::set CodeInstructionCollector::Collect(const std::ve && addend <= static_cast(std::numeric_limits::max()) && Io::ReadU64(bytes, symbol + 8) == addend; if ((index != 0 && !zeroSectionSymbol && !matchingDefinedSymbol) || !pointers.Values.emplace(slot, addend).second) throw Domain::RelinkerException("Code analysis: invalid relative relocation", slot); - addRoot(addend); + addPointerRoot(addend); } else if (type == 1 || type == 6 || type == 7) { if (index != 0 && index >= symbolCount) throw Domain::RelinkerException("Code analysis: invalid relocation symbol", slot); const auto symbol = symbols + index * 24; @@ -234,7 +317,7 @@ std::set CodeInstructionCollector::Collect(const std::ve else { const auto target = Io::ReadU64(bytes, symbol + 8) + addend; if (!pointers.Values.emplace(slot, target).second) throw Domain::RelinkerException("Code analysis: duplicate pointer relocation", slot); - addRoot(target); + addPointerRoot(target); } } } @@ -366,8 +449,7 @@ std::set CodeInstructionCollector::Collect(const std::ve position = crossed; continue; } - const auto opcode = position + info.OpcodeOffset; - if (info.SegmentPrefix == 0x64 && opcode + 2 < next && (text[opcode + 1] & 0xc7) == 0x04 && text[opcode + 2] == 0x25) addRoot(header.MappedAddress + position); + if (absoluteFsAccess(text + position, info)) addRoot(header.MappedAddress + position); afterTransfer = info.FlowKind != Codegen::ControlFlowKind::Sequential && info.FlowKind != Codegen::ControlFlowKind::ConditionalBranch; position = next; } @@ -405,7 +487,7 @@ std::set CodeInstructionCollector::Collect(const std::ve for (const auto root : roots) if (root >= header.MappedAddress && root - header.MappedAddress < header.FileSize) entries.push_back(root); if (entries.empty()) continue; const std::span text(bytes.data() + header.Offset, header.FileSize); - const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, true, sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); + const auto graph = UnusedNidFilter::BuildControlFlowGraph(text, header.MappedAddress, entries.front(), entries, pointers, sceFunctions.empty(), sectionReachable[i], [&](std::uint64_t count) { report(6000 + 2500 * count / std::max(scanBytes, 1)); }, inTable); for (const auto address : graph->ReachableVaddrs()) { if (!decodedInstructions.insert(address).second) continue; instructions.insert(address); @@ -475,7 +557,16 @@ std::set CodeInstructionCollector::Collect(const std::ve } } report(10000); - if (entryRoots) *entryRoots = std::move(roots); + std::erase_if(dataTargets, [&](std::uint64_t address) { return roots.contains(address) || inTable(address); }); + if (!dataTargets.empty() && message) { + std::ostringstream text; + text << "Took " << std::dec << dataTargets.size() << " pointer target(s) outside the unwind functions as data, the first at 0x" << std::hex << *dataTargets.begin() << '\n'; + message(text.str()); + } + if (entryRoots) { + *entryRoots = std::move(roots); + entryRoots->insert(dataTargets.begin(), dataTargets.end()); + } return instructions; } diff --git a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp index 0ccbc28717..7c99111960 100644 --- a/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp +++ b/core/relinker/relinker/src/analysis/RelativeJumpTables.cpp @@ -24,15 +24,15 @@ bool IsNop(std::span code, std::size_t at, std::size_t lengt return code[position] == 0x0f && position + 1 < end && code[position + 1] == 0x1f; } -bool OnlyPadding(std::span code, std::size_t from, const Codegen::X64InstructionDecoder& decoder) { - for (auto position = from; position < code.size();) { +bool OnlyPadding(std::span code, std::size_t from, std::size_t to, const Codegen::X64InstructionDecoder& decoder) { + for (auto position = from; position < to;) { if (code[position] == 0x00 || code[position] == 0xcc || code[position] == 0x90) { ++position; continue; } try { - const auto length = decoder.DecodeInstruction(code.data() + position, code.size() - position).Length; - if (length == 0 || length > code.size() - position || !IsNop(code, position, length)) return false; + const auto length = decoder.DecodeInstruction(code.data() + position, to - position).Length; + if (length == 0 || length > to - position || !IsNop(code, position, length)) return false; position += length; } catch (const Codegen::CodegenException&) { return false; @@ -69,7 +69,7 @@ std::size_t TablesEnd(std::span code, std::size_t firstTable } -std::size_t FindTrailingRelativeJumpTable(std::span code) { +std::size_t FindTrailingRelativeJumpTable(std::span code, VirtualAddress codeAddress, std::size_t* tableEnd, std::size_t unwindEnd) { const Codegen::X64InstructionDecoder decoder; std::vector loads; std::vector instructionStarts(code.size() + 1, false); @@ -82,7 +82,13 @@ std::size_t FindTrailingRelativeJumpTable(std::span code) { for (const auto& load : loads) { if (load.Target != position || load.At >= lastIndirectJump) continue; const auto end = TablesEnd(code, position, loads, instructionStarts); - if (end != 0 && OnlyPadding(code, end, decoder)) return position; + if (end == 0) continue; + const auto aligned = std::min(code.size(), static_cast(((codeAddress + end + 15) & ~VirtualAddress{15}) - codeAddress)); + const auto checkedTo = std::max(aligned, std::min(code.size(), unwindEnd)); + const bool toRangeEnd = OnlyPadding(code, end, code.size(), decoder); + if (!toRangeEnd && !OnlyPadding(code, end, checkedTo, decoder)) continue; + if (tableEnd) *tableEnd = toRangeEnd ? code.size() : checkedTo; + return position; } } Codegen::DecodedInstructionInfo info; @@ -122,8 +128,9 @@ std::vector FindRelativeJumpTables(std::span inspectEnd && functions[index + 1].first - inspectEnd <= MaxTableGapAfterFunction) inspectEnd = std::min(functions[index + 1].first, segmentEnd); const auto code = segment.subspan(begin - segmentVAddr, inspectEnd - begin); - if (const auto tail = FindTrailingRelativeJumpTable(code); tail != 0 && tail < code.size()) - tables.push_back({begin, begin + tail, inspectEnd}); + std::size_t tableEnd = code.size(); + if (const auto tail = FindTrailingRelativeJumpTable(code, begin, &tableEnd, std::min(end, segmentEnd) - begin); tail != 0 && tail < code.size()) + tables.push_back({begin, begin + tail, begin + tableEnd}); } return tables; } diff --git a/core/relinker/relinker/src/analysis/SyscallScanner.cpp b/core/relinker/relinker/src/analysis/SyscallScanner.cpp index 08d5e87dcb..be3795ee18 100644 --- a/core/relinker/relinker/src/analysis/SyscallScanner.cpp +++ b/core/relinker/relinker/src/analysis/SyscallScanner.cpp @@ -5,11 +5,14 @@ #include #include #include +#include #include #include #include +#include #include +#include namespace Relinker { @@ -146,8 +149,12 @@ static std::optional ConstantRaxLoad(const std::uint8_t* bytes, c } static std::optional ConstantSyscallNumber(const std::vector& codeSection, const FileByteOffset codeSectionOffset, const RecentInstructions& recent) { + FileByteOffset next = recent.Before(0).Offset; for (std::size_t step = 1; step <= SYSCALL_NUMBER_LOOKBACK && step < recent.count; ++step) { const auto& match = recent.Before(step); + if (match.Offset + match.Length != next) + return std::nullopt; + next = match.Offset; const std::size_t start = static_cast(match.Offset - codeSectionOffset); if (start >= codeSection.size() || match.Length > codeSection.size() - start) @@ -170,6 +177,78 @@ static std::optional ConstantSyscallNumber(const std::vector ConstantRaxSetEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 10 && code[end - 10] == 0x48 && code[end - 9] == 0xB8) { + std::uint64_t value = 0; + std::memcpy(&value, code.data() + end - 8, sizeof(value)); + return value; + } + std::uint32_t immediate = 0; + if (end >= 7 && code[end - 7] == 0x48 && code[end - 6] == 0xC7 && code[end - 5] == 0xC0) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return static_cast(static_cast(static_cast(immediate))); + } + if (end >= 5 && code[end - 5] == 0xB8) { + std::memcpy(&immediate, code.data() + end - 4, sizeof(immediate)); + return immediate; + } + if (end >= 2 && (code[end - 2] == 0x31 || code[end - 2] == 0x33) && code[end - 1] == 0xC0) + return 0; + return std::nullopt; +} + +struct LowRaxWrite { + std::size_t Length; + std::uint64_t Mask; + std::uint64_t Value; +}; + +static std::optional ImmediateLowRaxWriteEndingAt(const std::vector& code, const std::size_t end) { + if (end >= 2 && code[end - 2] == 0xB0) + return LowRaxWrite{2, 0xFF, code[end - 1]}; + if (end >= 4 && code[end - 4] == 0x66 && code[end - 3] == 0xB8) + return LowRaxWrite{4, 0xFFFF, static_cast(code[end - 2]) | static_cast(code[end - 1]) << 8}; + return std::nullopt; +} + +static bool MovesOtherRegisterThanRax(const std::uint8_t* bytes, const std::size_t length) { + const std::uint8_t rex = length == 3 ? bytes[0] : 0; + if (length == 3 && (rex & 0xF0) != 0x40) + return false; + const std::uint8_t opcode = bytes[length - 2]; + const std::uint8_t modrm = bytes[length - 1]; + if ((modrm & 0xC0) != 0xC0) + return false; + const bool writesRm = opcode == 0x89 || opcode == 0x31; + const bool writesReg = opcode == 0x8B || opcode == 0x33; + if (!writesRm && !writesReg) + return false; + const unsigned written = writesRm ? (modrm & 7u) | ((rex & 1u) << 3) : ((modrm >> 3) & 7u) | ((rex & 4u) << 1); + return written != 0; +} + +static std::optional ConstantSyscallNumberInBytes(const std::vector& code, const std::size_t syscall) { + using Step = std::tuple; + std::vector pending{{syscall, 0, 0}}; + std::set seen(pending.begin(), pending.end()); + const auto visit = [&](const Step& step) { + if (seen.insert(step).second) + pending.push_back(step); + }; + while (!pending.empty()) { + const auto [end, mask, low] = pending.back(); + pending.pop_back(); + if (const auto value = ConstantRaxSetEndingAt(code, end)) + return (*value & ~mask) | low; + if (const auto write = ImmediateLowRaxWriteEndingAt(code, end)) + visit({end - write->Length, mask | write->Mask, low | (write->Value & ~mask)}); + for (const std::size_t length : {std::size_t{2}, std::size_t{3}}) + if (end >= length && MovesOtherRegisterThanRax(code.data() + end - length, length)) + visit({end - length, mask, low}); + } + return std::nullopt; +} + class SyscallScanner : public ISyscallScanner { public: SyscallScanner(Domain::IProgress& progress, const bool trapSdkStubs) : progress(progress), trapSdkStubs(trapSdkStubs) {} @@ -190,6 +269,13 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v const auto scanner = Codegen::MakeInstructionScanner([&](auto completed, auto total) { progress.Report({Domain::ProgressStage::Decode, source, completed, total}); }, unwindRanges); RecentInstructions recent; std::vector trapped; + const auto functions = Codegen::MergeCodeRanges(unwindRanges); + const auto insideFunction = [&](const std::size_t offset) { + const auto after = std::upper_bound(functions.begin(), functions.end(), offset, [](const std::size_t value, const CodeRange& range) { return value < range.Begin; }); + return after != functions.begin() && offset < std::prev(after)->End; + }; + std::size_t skipped = 0; + std::optional firstSkipped; scanner->VisitCodeSection(codeSection, codeSectionOffset, codeSectionSize, [&](const Codegen::InstructionMatch& match) { recent.Push(match); if (match.Offset < codeSectionOffset) throw RelinkerException("Instruction match precedes code section offset", match.Offset); @@ -213,15 +299,32 @@ std::vector SyscallScanner::ScanCodeSectionForSyscalls(const std::v return; } + std::optional number; + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent); + if (!functions.empty() && !number && !insideFunction(start)) { + if (kind == ForbiddenInstruction::Invocation) + number = ConstantSyscallNumberInBytes(codeSection, opcode); + if (!number) { + ++skipped; + if (!firstSkipped) firstSkipped = match.Offset; + return; + } + } + std::ostringstream msg; msg << "Forbidden syscall instruction at code offset 0x" << std::hex << match.Offset; - if (kind == ForbiddenInstruction::Invocation) - if (const auto number = ConstantSyscallNumber(codeSection, codeSectionOffset, recent)) - msg << ": syscall number " << std::dec << *number; + if (number) + msg << ": syscall number " << std::dec << *number; throw RelinkerException(msg.str(), match.Offset); }); + if (skipped != 0) { + std::ostringstream message; + message << "Skipped " << std::dec << skipped << " syscall byte pattern(s) without a syscall number outside the unwind functions, the first at code offset 0x" << std::hex << *firstSkipped << '\n'; + progress.Message(message.str()); + } progress.Report({Domain::ProgressStage::Syscalls, source, limit, limit}); return trapped; } diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 3e9a239afa..a630010692 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -271,9 +271,12 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector copyRelocations(relaOffset, relaSize, image.Dynamic.RelaData, false); copyRelocations(pltOffset, pltSize, image.Dynamic.RelaPltData, true); if (tags.contains(3) || tags.contains(0x61000027)) image.Got = get(3, 0x61000027); - if (tags.contains(12)) image.Init = tags.at(12); + if (tags.contains(12)) { + image.Init = tags.at(12); + image.HasInit = true; + } if (tags.contains(13)) image.Fini = tags.at(13); - if (image.Init != 0) mapped(image.Init, 1, 1); + if (image.HasInit) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; @@ -307,7 +310,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector }; readLifecycleArray(25, 27, image.InitArray); readLifecycleArray(26, 28, image.FiniArray); - if (tags.contains(33) && tags.at(33) != 0 && image.Init == 0) fail("Guest PREINIT_ARRAY has no module initializer"); + if (tags.contains(33) && tags.at(33) != 0 && !image.HasInit) fail("Guest PREINIT_ARRAY has no module initializer"); image.Bytes = std::move(bytes); return image; } catch (Domain::RelinkerException& error) { diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 0ced33fbb9..1f9eed1e66 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -99,20 +99,25 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::set outputNames; for (const auto& path : paths) { auto image = std::move(discovered.at(path)); - if (image.OutputName.find_first_of("$\r\n") != std::string::npos) throw Domain::RelinkerException("Unsupported guest filename: " + image.OutputName); + const auto fail = [&](const std::string& message) { + Domain::RelinkerException error(message); + error.InputPath = Io::Utf8Path(path); + throw error; + }; + if (image.OutputName.find_first_of("$\r\n") != std::string::npos) fail("Unsupported guest filename: " + image.OutputName); std::string folded = image.OutputName; if (windows) { for (auto& value : folded) { - if (static_cast(value) >= 128 || value == ':' || value == '$') throw Domain::RelinkerException("Unsupported Windows guest filename: " + image.OutputName); + if (static_cast(value) >= 128 || value == ':' || value == '$') fail("Unsupported Windows guest filename: " + image.OutputName); if (value >= 'A' && value <= 'Z') value = static_cast(value + ('a' - 'A')); } } - if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); + if (!outputNames.insert(folded).second) fail("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; auto& providers = exports[symbol.Name]; const bool repeated = !providers.empty() && providers.back().Provider == images.size(); - if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); + if (!windows && repeated) fail("Duplicate guest export after stripping #: " + symbol.Name + " in " + Io::Utf8Path(path) + " and " + Io::Utf8Path(images[providers.front().Provider].SourcePath)); if (!repeated) providers.push_back({images.size(), static_cast(symbol.Info & 15)}); if (!windows && providers.size() > 1) for (const auto& provider : providers) sharedExports[symbol.Name].insert(provider.Provider); @@ -317,7 +322,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path addHost(dependency, windows || !deferred(index)); if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); } - if (!windows && !macos && images[index].Init != 0) { + if (!windows && !macos && images[index].HasInit) { if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); } @@ -356,7 +361,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (sceImage) if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) code.resize(*end - header.MappedAddress); - const auto unwindRanges = SceUnwindRanges(code, header.MappedAddress, unwindFunctions); + const auto unwindRanges = sceImage ? SceUnwindRanges(code, header.MappedAddress, unwindFunctions) : std::vector{}; for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath), unwindRanges)) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); @@ -378,7 +383,8 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path runtime.Names = {Io::Utf8Path(image.SourcePath.filename()), image.Soname}; runtime.DeferInitialization = deferred(index); std::vector output; - if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); + const auto message = [&](std::string_view text) { progress.Message(text); }; + if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime, message); else { std::string guestRunPath = runPath; const auto relativeRoot = outputDirectory.lexically_relative(destination.lexically_normal()).generic_string(); @@ -394,7 +400,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); - output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); + output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath, message) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { for (const auto& [name, provider] : guestNames) { diff --git a/core/relinker/relinker/src/pipeline/Conversion.cpp b/core/relinker/relinker/src/pipeline/Conversion.cpp index 9b283de919..ad008fa002 100644 --- a/core/relinker/relinker/src/pipeline/Conversion.cpp +++ b/core/relinker/relinker/src/pipeline/Conversion.cpp @@ -127,9 +127,9 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti std::shared_ptr patcher; if (macos) { - patcher = std::make_shared(options.MacosFixedBase); + patcher = std::make_shared(options.MacosFixedBase, [&](std::string_view text) { inputProgress.Message(text); }); } else if (windows) { - patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports); + patcher = std::make_shared(options.WindowsGui, std::move(input.Icon), input.InputPath.parent_path() / "sce_sys" / "icon0.png", [&](auto completed, auto total) { inputProgress.Report({Domain::ProgressStage::Image, {}, completed, total}); }, options.WindowsFixedBase, options.TraceImports, [&](std::string_view text) { inputProgress.Message(text); }); } else { patcher = std::make_shared( std::make_shared(), @@ -139,7 +139,8 @@ ConversionOutput Conversion::Convert(ConversionInput input, const ConversionOpti ), std::make_shared(byteWriter), byteWriter, - options.LinuxFixedBase + options.LinuxFixedBase, + options.TraceImports ); } diff --git a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp index 718e5ddb07..f248468fc3 100644 --- a/core/relinker/relinker/tests/ControlFlowGraphTests.cpp +++ b/core/relinker/relinker/tests/ControlFlowGraphTests.cpp @@ -249,6 +249,57 @@ void CheckRelativeJumpTables() { nonPadding.back() = 0x55; Require(Relinker::FindTrailingRelativeJumpTable(nonPadding) == nonPadding.size(), "Table followed by non-padding bytes was accepted"); + Bytes codeAfter = text; + codeAfter.resize(48, 0xcc); + codeAfter[32] = 0x55; + codeAfter[33] = 0x5d; + codeAfter[34] = 0xc3; + std::size_t tableEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(codeAfter, TextAddress, &tableEnd) == tableOffset && tableEnd == 32, "Table padded to 16 bytes before more code was not found"); + const std::vector> codeAfterFunctions{{TextAddress, TextAddress + 9}, {TextAddress + codeAfter.size(), TextAddress + codeAfter.size() + 8}}; + const auto codeAfterTables = Relinker::FindRelativeJumpTables(codeAfter, TextAddress, codeAfterFunctions); + Require(codeAfterTables.size() == 1 && codeAfterTables[0].Begin == TextAddress + tableOffset && codeAfterTables[0].End == TextAddress + 32, "Table before more code did not end at its padding"); + + std::size_t shiftedEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 1, &shiftedEnd) == tableOffset && shiftedEnd == 31, "Table boundary ignored the code address"); + const auto shifted = Relinker::FindRelativeJumpTables(nonPadding, TextAddress + 1, {{TextAddress + 1, TextAddress + 10}, {TextAddress + 33, TextAddress + 41}}); + Require(shifted.size() == 1 && shifted[0].Begin == TextAddress + 17 && shifted[0].End == TextAddress + 32, "Function address was not passed to the table search"); + Require(Relinker::FindTrailingRelativeJumpTable(nonPadding, TextAddress + 8) == tableOffset, "Table ending on a 16-byte boundary was rejected"); + + Bytes padded = text; + padded.resize(48, 0xcc); + const auto paddedTables = Relinker::FindRelativeJumpTables(padded, TextAddress, {{TextAddress, TextAddress + 9}, {TextAddress + 48, TextAddress + 56}}); + Require(paddedTables.size() == 1 && paddedTables[0].End == TextAddress + 48, "Padding to the end of the range was cut at the boundary"); + + Bytes nopAtRangeEnd = text; + const std::uint8_t fourByteNop[]{0x0f, 0x1f, 0x40, 0x00}; + std::memcpy(nopAtRangeEnd.data() + 28, fourByteNop, sizeof(fourByteNop)); + const auto cut = std::span(nopAtRangeEnd).first(31); + Require(Relinker::FindTrailingRelativeJumpTable(cut) == cut.size(), "Padding check read past the inspected range"); + + Bytes longNops = text; + longNops.resize(48); + const std::uint8_t fifteenByteNop[]{0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + const std::uint8_t nineByteNop[]{0x66, 0x0f, 0x1f, 0x84, 0x00, 0x00, 0x00, 0x00, 0x00}; + std::memcpy(longNops.data() + 24, fifteenByteNop, sizeof(fifteenByteNop)); + std::memcpy(longNops.data() + 39, nineByteNop, sizeof(nineByteNop)); + std::size_t longNopsEnd = 0; + Require(Relinker::FindTrailingRelativeJumpTable(longNops, TextAddress, &longNopsEnd) == tableOffset && longNopsEnd == 48, "Table followed by a NOP across the boundary was not found"); + Bytes longNopsCodeAfter = longNops; + longNopsCodeAfter.resize(64, 0xcc); + longNopsCodeAfter[48] = 0x55; + longNopsCodeAfter[49] = 0x5d; + longNopsCodeAfter[50] = 0xc3; + Require(Relinker::FindTrailingRelativeJumpTable(longNopsCodeAfter, TextAddress) == longNopsCodeAfter.size(), "Table ending inside a NOP was accepted"); + + Bytes cutShort = text; + cutShort.resize(56, 0xcc); + const std::int32_t cutShortTargets[]{-16, -7, -6, -5, 0x20, 0x20}; + std::memcpy(cutShort.data() + tableOffset, cutShortTargets, sizeof(cutShortTargets)); + cutShort[48] = 0x55; + cutShort[49] = 0x5d; + cutShort[50] = 0xc3; + Require(Relinker::FindRelativeJumpTables(cutShort, TextAddress, {{TextAddress, TextAddress + cutShort.size()}}).empty(), "Table followed by more of its function was accepted"); } } diff --git a/core/relinker/relinker/tests/test_conversion_diagnostics.py b/core/relinker/relinker/tests/test_conversion_diagnostics.py index 776206cc5f..c86ca4ec0e 100644 --- a/core/relinker/relinker/tests/test_conversion_diagnostics.py +++ b/core/relinker/relinker/tests/test_conversion_diagnostics.py @@ -71,6 +71,8 @@ def main(): for name, image, message in cases: convert(f"{index}-{name}", image, message, flags) convert(f"{index}-extra", cases[0][1], cases[0][2], flags, extra=True) + if "--windows" in flags: + convert(f"{index}-module-name", None, "Unsupported Windows guest filename", flags, module=guest_fixture(PLAIN_SITE)) module = guest_fixture(PLAIN_SITE) struct.pack_into("\n" + "#include \n" + "static int replacement(void) { _exit(77); }\n" + "void* TraceImport_nid_no_patch(void* target, const char* name, const char* library) {\n" + " return strcmp(name, \"shared\") == 0 && strcmp(library, \"a.prx\") == 0 ? (void*)replacement : target;\n" + "}\n") +PLAIN = "int Unrelated(void) { return 0; }\n" + + +def relink(relinker, source, output, *options): + return subprocess.run([str(relinker), "--skip-sce-module", *options, str(source), str(output)], + capture_output=True, text=True, timeout=30) + + +def library(work, name, code): + source = work / (name + ".c") + source.write_text(code) + output = work / (name + ".so") + subprocess.run([os.environ.get("CC", "cc"), "-shared", "-fPIC", str(source), "-o", str(output)], check=True) + return output.read_bytes() + + +def main(): + relinker = Path(sys.argv[1]).resolve() + runnable = sys.platform.startswith("linux") and platform.machine() in ("x86_64", "AMD64") + with tempfile.TemporaryDirectory(prefix="anyps5-linux-trace-imports-") as directory: + work = Path(directory) + libraries = {} + if runnable: + libraries = {name: library(work, name, code) for name, code in (("a", PROVIDER), ("hook", HOOK), ("plain", PLAIN))} + + for name, options, libc, symbol_info, expected in ( + ("untraced", (), "hook", 0x12, 11), + ("traced", ("--trace-imports",), "hook", 0x12, 77), + ("fixed-base", ("--trace-imports", "--linux-fixed-base", "0x400000"), "hook", 0x12, 77), + ("data", ("--trace-imports",), "hook", 0x11, 11), + ("weak", ("--trace-imports",), "hook", 0x22, 11), + ("unsupported", ("--trace-imports",), "plain", 0x12, None)): + case = work / name + case.mkdir() + image = executable("a.prx", extra_dependencies=("libc.prx",)) + image[0x4898 + 4] = symbol_info + source = case / "input.elf" + source.write_bytes(image) + output = case / "output.elf" + result = relink(relinker, source, output, *options) + assert result.returncode == 0, (name, result.stdout, result.stderr) + if not runnable: + continue + (case / "libs").mkdir() + (case / "libs" / "a.prx").write_bytes(libraries["a"]) + (case / "libs" / "libc.prx").write_bytes(libraries[libc]) + output.chmod(0o755) + run = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert run.returncode != 0 and "TraceImport_nid_no_patch" in run.stderr, (name, run.returncode, run.stdout, run.stderr) + else: + assert run.returncode == expected, (name, run.returncode, run.stdout, run.stderr) + + case = work / "without-libc" + case.mkdir() + source = case / "input.elf" + source.write_bytes(executable("a.prx")) + result = relink(relinker, source, case / "output.elf", "--trace-imports") + assert result.returncode == 2 and "--trace-imports requires libc.prx" in result.stderr, (result.returncode, result.stderr) + assert not (case / "output.elf").exists() + result = relink(relinker, source, case / "output.elf", "--trace-imports", "--lazy-binding") + assert result.returncode != 0 and "--trace-imports conflicts with --lazy-binding" in result.stderr, (result.returncode, result.stderr) + result = relink(relinker, source, case / "output", "--trace-imports", "--macos") + assert result.returncode != 0 and "--trace-imports cannot be used with --macos" in result.stderr, (result.returncode, result.stderr) + + print("Linux import tracing tests passed") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 9af3a826a2..4213674c46 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -205,7 +205,7 @@ def main(): 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, ( + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( result.stdout, result.stderr) assert output.read_bytes() == b"previous output" assert paths["module"].read_bytes() == b"previous output" diff --git a/core/relinker/relinker/tests/test_sce_code_extent.py b/core/relinker/relinker/tests/test_sce_code_extent.py index 5bc8a2321c..1f58fb585e 100644 --- a/core/relinker/relinker/tests/test_sce_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_code_extent.py @@ -8,6 +8,7 @@ import tempfile PT_LOAD = 1 PT_DYNAMIC = 2 PT_INTERP = 3 +PT_TLS = 7 PT_GNU_EH_FRAME = 0x6474E550 PT_SCE_DYNLIBDATA = 0x61000000 PT_SCE_VERSION = 0x6FFFFF01 @@ -22,25 +23,30 @@ POINTER = 0xB00 INTERPRETER = b"/libexec/ld-elf.so.1\0" OVERLAPPING = bytes.fromhex("b8 64 c3 90 90 72 fa c3") UNSUPPORTED_TLS = bytes.fromhex("64 2d 65 6c 66 2e c3") +INVALID = bytes.fromhex("41 41 41 41 41 41 66 66 66 66 2e 0f 1f 84 00 00 00 00 00") +TLS_LOAD = bytes.fromhex("64 48 8b 04 25 00 00 00 00 c3") +FS_SUBTRACT = bytes.fromhex("64 2d 65 6c 66 2e") def fixture(sce_block=True, plt_push_offset=8, gap=0, interpreter_pointer=False, data_pointer=False, gap_entry=False, - plt_at=None, early_function=False, function_tls=False): + plt_at=None, early_function=False, function_tls=False, pointer_target=None, gap_bytes=None, functions=None, + tls=False): plt = FUNCTION + FUNCTION_SIZE + gap if plt_at is None else plt_at entry = FUNCTION + FUNCTION_SIZE if gap_entry else FUNCTION data = plt + 0x20 if plt > FUNCTION else FUNCTION + FUNCTION_SIZE - functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] - functions.append((FUNCTION, FUNCTION_SIZE)) + if functions is None: + functions = [(EARLY_FUNCTION, FUNCTION_SIZE)] if early_function else [] + functions.append((FUNCTION, FUNCTION_SIZE)) interpreter = FUNCTION - len(INTERPRETER) image = bytearray(0x1000) image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) - struct.pack_into("depthRemapMode); } writer.WriteBool(request.target.float32FlushToZero); + writer.WriteBool(request.target.float32DenormPreserve); return base64Encode(buffer); } @@ -866,7 +867,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 39u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 41u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -911,6 +912,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); + if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 2ff9d9a15a..307b83cdff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -98,6 +98,7 @@ struct ImageResource { bool flatVolumeLineCompatible = true; bool foldTexelOffsetsOnly = false; bool convertedSamples = false; + bool remappedDepth1D = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp index d7adec321f..ccdda16ed9 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrValue.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -35,7 +36,7 @@ public: [[nodiscard]] IrType Type() const; [[nodiscard]] std::uint32_t Id() const; [[nodiscard]] const std::vector& Arguments() const; - [[nodiscard]] const std::vector& Uses() const; + [[nodiscard]] auto Uses() const; [[nodiscard]] IrBlock* Parent() const; [[nodiscard]] bool HasImmediate() const; [[nodiscard]] std::uint32_t ImmediateU32() const; @@ -103,10 +104,13 @@ private: std::vector operandUses; bool hasImmediate; std::vector arguments; - std::vector uses; IrBlock* parent; }; +inline auto IrValue::Uses() const { + return std::views::transform(operandUses, [](const IrUse& use) { return use.user; }); +} + inline IrOpcode IrValue::Opcode() const { return opcode; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp index 3a2a7c9c41..a7cc1db50d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp @@ -17,10 +17,6 @@ const std::vector& IrValue::Arguments() const { return arguments; } -const std::vector& IrValue::Uses() const { - return uses; -} - IrBlock* IrValue::Parent() const { return parent; } @@ -34,7 +30,6 @@ void IrValue::AddArgument(IrValue* argument) { if (argument == nullptr) { return; } - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, arguments.size() - 1}); } @@ -44,10 +39,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } IrValue* previous = arguments[index]; if (previous != nullptr) { - const auto useIt = std::find(previous->uses.begin(), previous->uses.end(), this); - if (useIt != previous->uses.end()) { - previous->uses.erase(useIt); - } const auto operandIt = std::find_if(previous->operandUses.begin(), previous->operandUses.end(), [this, index](const IrUse& use) { return use.user == this && use.operand == index; }); @@ -57,7 +48,6 @@ void IrValue::ReplaceArgument(std::size_t index, IrValue* argument) { } arguments[index] = argument; if (argument != nullptr) { - argument->uses.push_back(this); argument->operandUses.push_back(IrUse{this, index}); } } @@ -78,17 +68,14 @@ void IrValue::ReplaceAllUsesWith(IrValue* replacement) { list.reserve(std::max(required, grown)); } }; - reserveAdditional(replacement->uses); reserveAdditional(replacement->operandUses); } for (auto use = operandUses.rbegin(); use != operandUses.rend(); ++use) { use->user->arguments[use->operand] = replacement; if (replacement != nullptr) { - replacement->uses.push_back(use->user); replacement->operandUses.push_back(*use); } } - uses.clear(); operandUses.clear(); } @@ -114,11 +101,11 @@ bool IrValue::MayHaveSideEffects() const { } bool IrValue::HasUses() const { - return !uses.empty(); + return !operandUses.empty(); } std::size_t IrValue::UseCount() const { - return uses.size(); + return operandUses.size(); } std::size_t IrValue::PhiBlockCount() const { @@ -225,7 +212,7 @@ void IrValue::ReplaceOpcode(IrOpcode opcode) { } void IrValue::Invalidate() { - if (!uses.empty()) { + if (!operandUses.empty()) { throw std::runtime_error("IrValue::Invalidate called on a value that still has uses"); } for (std::size_t index = 0; index < arguments.size(); index++) { diff --git a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp index 3308434316..0849790845 100644 --- a/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp @@ -1,4 +1,5 @@ #include "IntermediateRepresentation/IrBlock.hpp" +#include #include #include #include @@ -92,7 +93,7 @@ void CheckSimpleReplacement() { Require(second->Argument(0) == target); const std::vector expected{{existing, 0}, {first, 2}, {first, 1}, {second, 0}, {first, 0}}; Require(target->OperandUses() == expected); - Require(target->Uses() == std::vector({existing, first, first, second, first})); + Require(std::ranges::equal(target->Uses(), std::vector({existing, first, first, second, first}))); first->ReplaceArgument(1, source); Require(source->UseCount() == 1); source->ReplaceAllUsesWith(nullptr); @@ -130,7 +131,7 @@ void CheckPhiAndIdentity() { } void CheckAllocationFailure() { - for (std::ptrdiff_t failure = 0; failure < 2; ++failure) { + for (std::ptrdiff_t failure = 0; failure < 1; ++failure) { Graph graph(12); for (std::size_t index = 2; index < 12; ++index) { graph.Value(index)->AddArgument(graph.Value(0)); @@ -169,7 +170,7 @@ void CheckGeometricGrowth() { countAllocations = true; for (auto& source : sources) source->ReplaceAllUsesWith(graph.Value(1)); countAllocations = false; - Require(allocations <= 2 * (std::bit_width(count) + 1)); + Require(allocations <= std::bit_width(count) + 1); Require(graph.Value(1)->UseCount() == count); for (std::size_t index = 2; index < count + 2; ++index) Require(graph.Value(index)->Argument(0) == graph.Value(1)); } diff --git a/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp new file mode 100644 index 0000000000..fd8d5f6ac7 --- /dev/null +++ b/core/shader/recompiler/Optimization/include/Optimization/VectorWindowStoreEliminator.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP +#define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_VECTORWINDOWSTOREELIMINATOR_HPP + +#include "IntermediateRepresentation/IrProgram.hpp" +#include + +namespace ShaderRecompiler { + +struct VectorWindowStoreEliminationStats { + std::uint32_t removedStores = 0; +}; + +class VectorWindowStoreEliminator { +public: + [[nodiscard]] VectorWindowStoreEliminationStats Eliminate(IrProgram& program) const; +}; + +} + +#endif diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 208f11d4f0..170b97d6b8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -356,6 +358,22 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); + const auto& image = info.images[index]; + if (image.resourceClass != ImageResourceClass::Storage || !image.written || image.atomic) continue; + const auto code = (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu; + const auto format = code >= 1u && code <= 77u ? GetFormatInfo(static_cast(code)) : SpirvBufferFormatInfo{}; + const bool snorm = format.type == SpirvFormatComponentType::Snorm; + const auto bits = format.componentBits[0]; + const bool uniform = format.componentCount != 0u && !format.packedBitfield && bits >= 2u && bits <= 16u && std::all_of(format.componentBits, format.componentBits + format.componentCount, [&](std::uint32_t width) { return width == bits; }); + const bool quantized = uniform && (format.type == SpirvFormatComponentType::Unorm || snorm); + const auto scale = quantized ? static_cast((1u << (snorm ? bits - 1u : bits)) - 1u) : 1.0f; + auto reciprocal = 1.0f / scale; + while (static_cast(reciprocal) * static_cast(scale) < 1.0) reciprocal = std::nextafter(reciprocal, 2.0f); + const auto quantize = PipelineSpecialization::StoreQuantizeBase + index * PipelineSpecialization::StoreQuantizeWords; + plan.specialization.push_back({quantize, quantized ? 1u : 0u}); + plan.specialization.push_back({quantize + 1u, std::bit_cast(scale)}); + plan.specialization.push_back({quantize + 2u, std::bit_cast(reciprocal)}); + plan.specialization.push_back({quantize + 3u, quantized && snorm ? 1u : 0u}); } for (std::uint32_t target = 0; target < exportMappings.size() && !registrationModules; ++target) { for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({PipelineSpecialization::ExportBase + target * 4u + component, (exportMappings[target] >> (component * 2u)) & 3u}); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..2ccd9a4039 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -192,6 +192,13 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag return rejectImage(invalidImageReason(descriptor), "image descriptor is invalid"); } decoded.dimension = descriptorDimension(descriptor, base.dimension); + if (base.depthCompare) { + if (decoded.dimension == RdnaImageDimension::Dim1D) { + decoded.dimension = RdnaImageDimension::Dim2D; + } else if (decoded.dimension == RdnaImageDimension::Dim1DArray) { + decoded.dimension = RdnaImageDimension::Dim2DArray; + } + } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { @@ -1900,7 +1907,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; modes.push_back(plain); } - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { mode.cube = true; modes.push_back(mode); } @@ -1956,7 +1963,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.packedFormat = IrBufferFormat::Invalid; mode.shaderSwizzle = ShaderImageIdentitySwizzle; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2DArray) { + if (image.dimension == RdnaImageDimension::Dim2DArray && !image.remappedDepth1D) { auto plain = mode; plain.dimension = RdnaImageDimension::Dim2D; modes.push_back(plain); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index de80e05182..d2967e5231 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1750,9 +1750,12 @@ private: ImageResource image; image.source = source; image.resourceClass = ImageOpcodeInfoOf(op).resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.depthCompare = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + image.remappedDepth1D = image.depthCompare && (memory.imageDimension == RdnaImageDimension::Dim1D || memory.imageDimension == RdnaImageDimension::Dim1DArray); + image.dimension = image.remappedDepth1D + ? (memory.imageDimension == RdnaImageDimension::Dim1D ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DArray) + : memory.imageDimension; + image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; image.r128 = memory.imageR128; image.packed = memory.imagePacked; image.byElements = memory.imageByElements; @@ -1761,7 +1764,7 @@ private: } static bool SameImageKey(const ImageResource& image, const ImageResource& key) { - return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; + return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.remappedDepth1D == key.remappedDepth1D && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; } std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp index 6a1a86eae6..5cae7e7cf5 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtPlanBuilder.cpp @@ -117,12 +117,12 @@ void PlanBuilder::PatchReads() { throw std::runtime_error("SrtWalker::BuildPlan patched value has no parent block"); } builder.SetInsertionPoint(*block); - IrValue& resource = builder.Emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); - block->RemoveInstruction(&resource); + IrValue& resource = _program.CreateValue(IrOpcode::GetSrtResource, IrType::SrtResource); block->InsertInstructionBefore(patch.inst, &resource); IrValue& slotConstant = builder.Constant(patch.slot); - IrValue& flat = builder.Emit(IrOpcode::ReadConst, IrType::U32, {&resource, &slotConstant}); - block->RemoveInstruction(&flat); + IrValue& flat = _program.CreateValue(IrOpcode::ReadConst, IrType::U32); + flat.AddArgument(&resource); + flat.AddArgument(&slotConstant); block->InsertInstructionBefore(patch.inst, &flat); patch.inst->ReplaceAllUsesWith(&flat); for (auto& info : _program.Metadata().blockInfo) { diff --git a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp index f3ed83cbed..74181182b1 100644 --- a/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp +++ b/core/shader/recompiler/Optimization/src/SsaBuilder/SsaPass.cpp @@ -115,7 +115,8 @@ IrValue* Pass::TryRemoveTrivialPhi(IrValue& phi, Variable variable) { if (same == nullptr) { same = MakeInitialValue(variable); } - std::vector users = phi.Uses(); + std::vector users; + for (IrValue* user : phi.Uses()) users.push_back(user); phi.ReplaceUsesWith(same, true); for (IrValue* user : users) { if (user->IsPhi()) { diff --git a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp index 36981ccf21..79a616db94 100644 --- a/core/shader/recompiler/Optimization/src/TessellationLowering.cpp +++ b/core/shader/recompiler/Optimization/src/TessellationLowering.cpp @@ -62,6 +62,11 @@ bool IsBase(const IrValue* raw, TessellationBaseKind kind) { return value->Opcode() == IrOpcode::TessellationBase && IsImmediate(value->Argument(0), static_cast(kind)); } +bool OwnPatchRead(const IrValue* value) { + if (SharedAccessOf(value->Opcode()) == SharedAccess::Read) return true; + return BufferAccessOf(value->Opcode()) == BufferAccess::Read && value->ArgumentCount() > 3u && IsBase(value->Argument(3), TessellationBaseKind::OffChip); +} + std::int64_t FloorDiv(std::int64_t value, std::int64_t divisor) { const auto quotient = value / divisor; return quotient * divisor > value ? quotient - 1 : quotient; @@ -351,6 +356,7 @@ private: pending.push_back(value->Argument(1)->Resolve()); continue; } + if (value != start && OwnPatchRead(value)) continue; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) { if (IrValue* argument = value->Argument(index)) pending.push_back(argument->Resolve()); } @@ -591,6 +597,7 @@ public: } const bool data = usedAsData(relative); if (data) requireGroupedPatches(); + if (data) keepComputedReadsInPatch(*relative.front()); for (IrValue* base : relative) base->ReplaceAllUsesWith(data ? &groupedPatch(*base) : &constant(0u)); deadCodeEliminator.Eliminate(program); if (stage == IrShaderStage::Local && tessellation.drawParameters) writeInstance(); @@ -657,7 +664,7 @@ private: address = memory.offen ? inst->Argument(2) : nullptr; } if (address != nullptr) { - const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput); + const auto evaluated = evaluator.Evaluate(address, access.predicate, access.kind == TessellationAttribute::ControlInput || access.kind == TessellationAttribute::EvaluationInput); if (!evaluated) Fail("the tessellation ring or LDS address is not affine in the control point and relative patch", flags.pc); access.address = *evaluated; access.addressValue = address; @@ -696,6 +703,8 @@ private: const auto end = access.address.constant + 4 * static_cast(access.components); if (access.address.relative > 0 && access.address.relative % outputs == 0 && access.address.constant >= 0 && end <= access.address.relative) { agree(controlStride, access.address.relative / outputs, access.pc); + } else if (access.address.dynamic) { + Fail("the domain shader reads off-chip memory at a computed offset outside its patch's control points", access.pc); } else { access.kind = TessellationAttribute::PatchInput; } @@ -734,6 +743,7 @@ private: break; case TessellationAttribute::EvaluationInput: if (address.constant < 0 || end > outputs * controlStride) Fail("the domain shader reads off-chip memory outside its patch's control points", access.pc); + if (address.dynamic && (address.dynamicScale == 0 || address.dynamicScale % controlStride != 0)) Fail("the domain shader reads off-chip memory at a computed offset that is not a whole number of control points", access.pc); break; case TessellationAttribute::Factor: { const bool quads = tessellation.domain == 2u; @@ -766,9 +776,12 @@ private: bool dynamicInputAddress(IrValue* user, IrValue* value) { if (user->Opcode() != IrOpcode::GetTessellationAttribute || user->Argument(1)->Resolve() != value->Resolve()) return false; - if (!IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput))) return false; + const bool control = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::ControlInput)); + const bool evaluation = IsImmediate(user->Argument(0), static_cast(TessellationAttribute::EvaluationInput)); + if (!control && !evaluation) return false; const auto address = evaluator.Evaluate(value, user->Argument(2), true); - return address && address->dynamic && address->relative == static_cast(tessellation.inputControlPoints) * localStride; + const auto stride = control ? static_cast(tessellation.inputControlPoints) * localStride : static_cast(tessellation.outputControlPoints) * controlStride; + return address && address->dynamic && address->relative == stride; } bool usedAsData(const std::vector& bases) { @@ -983,6 +996,27 @@ private: return insert(access, IrOpcode::IAdd32, IrType::U32, {&scaled, &offset}); } + void keepComputedReadsInPatch(IrValue& base) { + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* inst : block->Instructions()) { + if (inst->Opcode() != IrOpcode::GetTessellationAttribute) continue; + const bool control = IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::ControlInput)); + if (!control && !IsImmediate(inst->Argument(0), static_cast(TessellationAttribute::EvaluationInput))) continue; + const auto address = evaluator.Evaluate(inst->Argument(1), inst->Argument(2), true); + if (!address || !address->dynamic || address->relative == 0) continue; + IrValue& patch = program.CreateValue(IrOpcode::IMul32, IrType::U32); + patch.AddArgument(&base); + patch.AddArgument(&constant(static_cast(address->relative))); + block->InsertInstructionBefore(inst, &patch); + IrValue& inPatch = program.CreateValue(IrOpcode::ISub32, IrType::U32); + inPatch.AddArgument(inst->Argument(1)); + inPatch.AddArgument(&patch); + block->InsertInstructionBefore(inst, &inPatch); + inst->ReplaceArgument(1, &inPatch); + } + } + } + void rewrite(const Access& access) { IrValue& kind = constant(static_cast(access.kind)); const bool shared = SharedAccessOf(access.inst->Opcode()) != SharedAccess::None; diff --git a/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp new file mode 100644 index 0000000000..32248e72f9 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/VectorWindowStoreEliminator.cpp @@ -0,0 +1,78 @@ +#include "Optimization/VectorWindowStoreEliminator.hpp" +#include +#include + +namespace ShaderRecompiler { + +namespace { + +using LiveSlots = std::vector; + +const IrValue* constantSlot(const IrValue* inst, std::size_t slots) { + const IrValue* index = inst->Argument(0)->Resolve(); + return index->HasImmediate() && index->ImmediateU32() < slots ? index : nullptr; +} + +void transfer(const IrValue* inst, LiveSlots& live) { + if (inst->Opcode() == IrOpcode::LoadVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = true; + else live.assign(live.size(), true); + } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { + if (const IrValue* slot = constantSlot(inst, live.size())) live[slot->ImmediateU32()] = false; + } +} + +} + +VectorWindowStoreEliminationStats VectorWindowStoreEliminator::Eliminate(IrProgram& program) const { + VectorWindowStoreEliminationStats stats; + const std::size_t slots = program.Metadata().vectorWindowSize; + if (slots == 0u) return stats; + const auto& order = program.BlockOrder(); + std::unordered_map liveIn; + for (const IrBlock* block : order) liveIn.emplace(block, LiveSlots(slots, false)); + const auto liveOut = [&](const IrBlock* block) { + LiveSlots live(slots, false); + for (const IrBlock* successor : block->Successors()) { + const auto found = liveIn.find(successor); + if (found == liveIn.end()) return LiveSlots(slots, true); + for (std::size_t slot = 0; slot < slots; ++slot) live[slot] = live[slot] || found->second[slot]; + } + return live; + }; + for (bool changed = true; changed;) { + changed = false; + for (auto block = order.rbegin(); block != order.rend(); ++block) { + auto live = liveOut(*block); + const auto& instructions = (*block)->Instructions(); + for (auto inst = instructions.rbegin(); inst != instructions.rend(); ++inst) transfer(*inst, live); + auto& entry = liveIn[*block]; + if (live != entry) { + entry = std::move(live); + changed = true; + } + } + } + for (IrBlock* block : order) { + auto live = liveOut(block); + auto& instructions = block->Instructions(); + for (auto inst = instructions.end(); inst != instructions.begin();) { + --inst; + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::StoreVectorWindow) { + const IrValue* slot = constantSlot(value, slots); + if (slot != nullptr && !live[slot->ImmediateU32()]) { + value->Invalidate(); + inst = instructions.erase(inst); + value->SetParent(nullptr); + ++stats.removedStores; + continue; + } + } + transfer(value, live); + } + } + return stats; +} + +} diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index b8088c6afd..ae9ec178cf 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -36,6 +36,8 @@ inline constexpr std::uint32_t WorkgroupOffsetBase = 8241u; inline constexpr std::uint32_t WorkgroupOffsetWords = 3u; inline constexpr std::uint32_t HeapCountBase = 8256u; inline constexpr std::uint32_t MipCountBase = 8320u; +inline constexpr std::uint32_t StoreQuantizeBase = 12288u; +inline constexpr std::uint32_t StoreQuantizeWords = 4u; inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index f64c3c6bd7..e3be618395 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -70,6 +70,7 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0xa5u, RdnaOpcode::ImageSample, "image_sample_b_a", RdnaImageSampleFlagBias | RdnaImageSampleFlagAdjust, true, false, false}, {0xa8u, RdnaOpcode::ImageSample, "image_sample_c_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagAdjust, true, false, false}, {0xb0u, RdnaOpcode::ImageSample, "image_sample_o_a", RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, + {0xb8u, RdnaOpcode::ImageSample, "image_sample_c_o_a", RdnaImageSampleFlagCompare | RdnaImageSampleFlagOffset | RdnaImageSampleFlagAdjust, true, false, false}, {0x3cu, RdnaOpcode::ImageSampleCLO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLod | RdnaImageSampleFlagOffset, true, false, false}, {0x3fu, RdnaOpcode::ImageSampleCLzO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagLevelZero | RdnaImageSampleFlagOffset, true, false, false}, {0x3au, RdnaOpcode::ImageSampleCDO, nullptr, RdnaImageSampleFlagCompare | RdnaImageSampleFlagDerivative | RdnaImageSampleFlagOffset, true, false, false}, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp index dd297de461..02ccb674b8 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp @@ -1935,7 +1935,7 @@ bool supportsNativeVop3SourceModifiers(RdnaOpcode opcode) { } bool supportsNativeVop3ResultModifiers(RdnaOpcode opcode) { - if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode)) { + if (isVop1FloatResultOpcode(opcode) || isNativeVop3F16TernaryOpcode(opcode) || isCubeOpcode(opcode)) { return true; } switch (opcode) { @@ -1990,7 +1990,7 @@ bool supportsNativeVop3Clamp(RdnaOpcode opcode) { return true; } return supportsNativeVop3ResultModifiers(opcode) || usesInexactClampControl(opcode) || usesIntegerSaturateClamp(opcode) || - isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode) || isCubeOpcode(opcode); + isVop3Integer16AddSubOpcode(opcode) || isF32DivisionStepOpcode(opcode); } void checkNativeVop3Modifiers(RdnaOpcode opcode, bool permlane, bool carryInOut, bool scalarDst, std::uint32_t abs, std::uint32_t opSel, std::uint32_t clamp, std::uint32_t omod, std::uint32_t neg) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 10352ab7fc..ad94823d64 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -40,6 +40,7 @@ #include "Optimization/include/Optimization/ResourceMaterializer.hpp" #include "Optimization/ResourceProgram.hpp" #include "Optimization/include/Optimization/ResourceTracker.hpp" +#include "Optimization/include/Optimization/VectorWindowStoreEliminator.hpp" #include "Optimization/include/Optimization/ShaderInfoCollector.hpp" #include "Optimization/include/Optimization/SrtWalker.hpp" #include "Optimization/include/Optimization/SsaBuilder.hpp" @@ -134,6 +135,18 @@ ShaderStageInputInfo RequestInputInfo(const RecompileRequest& request) { return BuildShaderStageInputInfo(toShaderStageKind(request.shader.stage), request.context, HostSubgroupSize(request), mesh, tessellation); } +void ValidateRequestInputs(const RecompileRequest& request) { + const auto stage = toShaderStageKind(request.shader.stage); + const bool vertexFamily = stage == ShaderStageKind::Vertex || stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation || stage == ShaderStageKind::Mesh; + if (!vertexFamily) { + static_cast(RequestInputInfo(request)); + return; + } + const auto* mesh = request.graphics && request.graphics->mesh ? &*request.graphics->mesh : nullptr; + const auto* tessellation = request.graphics && request.graphics->tessellation ? &*request.graphics->tessellation : nullptr; + ValidateVertexStageInputs(stage, request.context, mesh, tessellation); +} + std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { if (!request.context.compute.has_value()) return 0u; const auto bytes = static_cast(request.context.compute->ldsSizeDwords) * 4u; @@ -253,6 +266,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat constexpr DeadCodeEliminator deadCodeEliminator; constexpr ReadLaneEliminator readLaneEliminator; constexpr MaskedSelectEliminator maskedSelectEliminator; + constexpr VectorWindowStoreEliminator vectorWindowStoreEliminator; const auto simplify = [&] { dumpIr("translate"); ssaBuilder.Rewrite(program); @@ -272,6 +286,10 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat deadCodeEliminator.Eliminate(program); } + if (vectorWindowStoreEliminator.Eliminate(program).removedStores != 0u) { + deadCodeEliminator.Eliminate(program); + } + if (maskedSelectEliminator.Eliminate(program).removedSelects != 0u) { const bool liveAncillary = std::ranges::any_of(program.Blocks(), [](const auto& block) { return std::ranges::any_of(block->Instructions(), [](const IrValue* value) { @@ -585,6 +603,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; @@ -990,6 +1009,10 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; + for (const auto reg : variant.bindings.layout.userDataRegisters) { + const auto index = reg - request.context.userDataBaseRegister; + result.userDataRegisterReads |= reg >= request.context.userDataBaseRegister && index < 64u ? std::uint64_t{1} << index : ~std::uint64_t{0}; + } const auto plan = preparedBindingPlan(variant, snapshot, request.context.pixel ? std::span(request.context.pixel->targetExportMapping) : std::span{}, request.target.registrationModules); const DescriptorBindingPlan* bindingPlan = &plan->bindings; std::shared_ptr entry; @@ -1597,7 +1620,7 @@ std::optional PreparedShaderInvocation::TryCreate(cons std::optional PreparedShaderInvocation::TryCreate(const RecompileRequest& request, const std::shared_ptr& handle, std::span key) { if (handle == nullptr || !MatchesPreparedShader(request, *handle, key)) return std::nullopt; - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); return PreparedShaderInvocation(request, handle); } @@ -1651,7 +1674,7 @@ std::shared_ptr CaptureResources(const RecompileRequest& static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; const auto started = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; // The whole vertex family (Vertex, Local, TC, TE, Mesh) validates V# fields the memo key does not cover. - if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) static_cast(RequestInputInfo(request)); + if (request.shader.stage != ShaderStage::Compute && request.shader.stage != ShaderStage::Fragment) ValidateRequestInputs(request); auto capture = std::make_shared(); capture->source = handle.source; capture->plan = acquirePlan(request, handle.source, true, &handle); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 6642fa433c..48cbbd73af 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -291,6 +291,7 @@ struct SpirvTarget { bool depthRangeRemap = false; bool float32FlushToZero = false; bool waveLdsMemoryBarriers = false; + bool float32DenormPreserve = false; }; struct BindingLayout { @@ -678,6 +679,7 @@ struct ShaderInvocation { std::vector pushConstants; bool readsDepthRemapPush = false; std::vector vertexAttributes; + std::uint64_t userDataRegisterReads = 0; std::uint32_t poisonedSrtReads = 0; std::vector imageTablePoison; std::vector> imageTableRanges; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3a3d947b8c..78a7083e60 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -85,8 +85,8 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(CompiledShaderArtifact) == 280, "CompiledShaderArtifact changed: update the artifact encoder"); -static_assert(sizeof(ShaderInvocation) == 208, "ShaderInvocation changed: update the invocation encoder"); -static_assert(sizeof(RecompileResult) == 520, "RecompileResult changed: update EncodeResult and DecodeResult"); +static_assert(sizeof(ShaderInvocation) == 216, "ShaderInvocation changed: update the invocation encoder"); +static_assert(sizeof(RecompileResult) == 528, "RecompileResult changed: update EncodeResult and DecodeResult"); static_assert(sizeof(DescriptorBinding) == 656, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); @@ -105,7 +105,7 @@ static_assert(sizeof(BindingAllocationResult) == 184, "BindingAllocationResult c static_assert(sizeof(CompiledBindingLayout) == 88, "CompiledBindingLayout changed: update the allocation encoder"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); static_assert(sizeof(IrResourcePlan) == 728, "IrResourcePlan changed: update EncodePlan and DecodePlan"); -static_assert(sizeof(IrValue) == 152, "IrValue changed: update the plan value encoder"); +static_assert(sizeof(IrValue) == 128, "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) == 192, "DescriptorSource changed: update the plan encoder"); @@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 40, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -602,6 +602,7 @@ void encodeInvocation(Writer& writer, const ShaderInvocation& invocation) { out.Value(attribute.fetchIndex); out.Value(attribute.formatComponents); }); + writer.Value(invocation.userDataRegisterReads); writer.Value(invocation.poisonedSrtReads); writer.List(invocation.imageTablePoison, [](Writer& out, const ImageTableEntryPoison& poison) { for (const auto word : poison.words) out.Value(word); @@ -642,6 +643,7 @@ void decodeInvocation(Reader& reader, ShaderInvocation& invocation) { in.Value(attribute.fetchIndex); in.Value(attribute.formatComponents); }); + reader.Value(invocation.userDataRegisterReads); reader.Value(invocation.poisonedSrtReads); reader.List(invocation.imageTablePoison, 44, [](Reader& in, ImageTableEntryPoison& poison) { for (auto& word : poison.words) in.Value(word); @@ -745,6 +747,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.flatVolumeLineCompatible); out.Value(image.foldTexelOffsetsOnly); out.Value(image.convertedSamples); + out.Value(image.remappedDepth1D); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -849,6 +852,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.flatVolumeLineCompatible); in.Value(image.foldTexelOffsetsOnly); in.Value(image.convertedSamples); + in.Value(image.remappedDepth1D); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); @@ -1475,17 +1479,33 @@ private: entries += pack->Count(); packs.push_back(std::move(pack)); }; - for (const auto& path : PrebuiltCachePaths(root)) { + for (const auto& candidate : PrebuiltCachePaths(root)) { std::error_code error; +#ifdef _WIN32 + const auto path = NativeCachePath_nid_no_patch(candidate, error); + if (error) continue; +#else + const auto& path = candidate; +#endif if (std::filesystem::is_regular_file(path, error)) { addPack(path); continue; } if (!std::filesystem::is_directory(path, error)) continue; bool loose = false; std::vector packPaths; - for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) { - const auto extension = it->path().extension(); - if (extension == ".pack") packPaths.push_back(it->path()); - if (extension == ".bin" && it->path().stem().string().size() == 32) loose = true; + const auto collect = [&](const std::filesystem::path& entry) { + const auto extension = entry.extension(); + if (extension == ".pack") packPaths.push_back(entry); + if (extension == ".bin" && entry.stem().string().size() == 32) loose = true; + }; +#ifdef _WIN32 + WIN32_FIND_DATAW data{}; + const auto handle = FindFirstFileW((path / L"*").c_str(), &data); + if (handle != INVALID_HANDLE_VALUE) { + struct Search { HANDLE handle; ~Search() { FindClose(handle); } } search{handle}; + do { collect(path / data.cFileName); } while (FindNextFileW(handle, &data)); } +#else + for (std::filesystem::directory_iterator it(path, error), end; !error && it != end; it.increment(error)) collect(it->path()); +#endif std::sort(packPaths.begin(), packPaths.end()); for (const auto& item : packPaths) addPack(item); if (loose) looseDirectories.push_back(path); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5543565717..5d721bb300 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 41; +inline constexpr std::uint32_t FormatVersion = 42; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index b7a1c29f28..8c8c56b4ad 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -29,6 +29,7 @@ struct SpirvTargetOptions { bool spannedWaves = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index b8055b31e9..de69943bed 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -112,6 +112,7 @@ struct SpirvEmitterState { bool float16DenormPreserve = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index e612e413ee..17d2db36bf 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -278,6 +278,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.robustBufferAccess2 = target.robustBufferAccess2; state.float32FlushToZero = target.float32FlushToZero; state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; + state.float32DenormPreserve = target.float32DenormPreserve; state.hostSubgroupSize = target.subgroupSize; if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 57a3c77d68..772f4f8f4c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -193,9 +193,12 @@ std::uint32_t CoordF32(SpirvValueEmitContext& ctx, const ImageEmitAccess& access x = CubeAxis(ctx.state, x); y = CubeAxis(ctx.state, y); } + if (!gradient && access.image.depthCompare && (access.image.remappedDepth1D || access.mem.imageDimension == RdnaImageDimension::Dim1D || access.mem.imageDimension == RdnaImageDimension::Dim1DArray)) { + y = ConstantF32(ctx.state, 0x3f000000u); + } const auto result = ctx.state.module.AllocateId(); if (components == 3u) { - auto z = component(2u); + auto z = (access.image.depthCompare && (access.mem.imageDimension == RdnaImageDimension::Dim1DArray || (access.image.remappedDepth1D && access.image.dimension == RdnaImageDimension::Dim2DArray))) ? component(1u) : component(2u); if (cube) { z = CubeLayer(ctx.state, z); } @@ -467,6 +470,9 @@ std::uint32_t PackedOffset(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac values[index] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), values[index], packed, ConstantU32(state, index * 8u), ConstantU32(state, 6)); } + if (access.image.remappedDepth1D && components >= 2u) { + values[1] = ConstantI32(state, 0); + } if (components == 1u) { return values[0]; } @@ -1002,6 +1008,67 @@ std::uint32_t ImageSampleIndex(SpirvValueEmitContext& ctx, const ImageEmitAccess return AddressU32(ctx, access, addressInfo.coordinateComponents); } +std::uint32_t QuantizeStoreTexel(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t texel) { + const auto first = PipelineSpecialization::StoreQuantizeBase + resource * PipelineSpecialization::StoreQuantizeWords; + const auto f32 = TypeF32(state); + const auto vec4 = TypeF32Vector(state, 4u); + const auto bvec4 = state.module.Type(spv::OpTypeVector, TypeBool(state), 4u); + const auto splat = [&](std::uint32_t type, std::uint32_t scalar) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, scalar, scalar, scalar, scalar); + return result; + }; + const auto word = [&](std::uint32_t index) { return state.module.SpecializationConstant(TypeU32(state), first + index, 0u); }; + const auto enabled = Binary(state, spv::OpINotEqual, TypeBool(state), word(0u), ConstantU32(state, 0u)); + const auto scale = splat(vec4, Unary(state, spv::OpBitcast, f32, word(1u))); + const auto reciprocal = splat(vec4, Unary(state, spv::OpBitcast, f32, word(2u))); + const auto snorm = Binary(state, spv::OpINotEqual, TypeBool(state), word(3u), ConstantU32(state, 0u)); + const auto lower = splat(vec4, Select(state, f32, snorm, ConstantF32(state, std::bit_cast(-1.0f)), ConstantF32(state, 0u))); + const auto zero = splat(vec4, ConstantF32(state, 0u)); + const auto one = splat(vec4, ConstantF32(state, std::bit_cast(1.0f))); + const auto finite = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, finite, Unary(state, spv::OpIsNan, bvec4, texel), zero, texel); + const auto clamped = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, clamped, GlslStd450(state), GLSLstd450FClamp, finite, lower, one); + const auto product = Binary(state, spv::OpFMul, vec4, clamped, scale); + state.module.AddAnnotation(spv::OpDecorate, product, spv::DecorationNoContraction); + const auto ext = [&](GLSLstd450 op, const auto&... operands) { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, vec4, result, GlslStd450(state), static_cast(op), operands...); + return result; + }; + const auto exact = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { + const auto value = Binary(state, opcode, vec4, lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, value, spv::DecorationNoContraction); + return value; + }; + const auto spreadFactor = splat(vec4, ConstantF32(state, std::bit_cast(4097.0f))); + const auto high = [&](std::uint32_t value) { + const auto spread = exact(spv::OpFMul, value, spreadFactor); + return exact(spv::OpFSub, spread, exact(spv::OpFSub, spread, value)); + }; + const auto clampedHigh = high(clamped); + const auto scaleHigh = high(scale); + const auto clampedLow = exact(spv::OpFSub, clamped, clampedHigh); + const auto scaleLow = exact(spv::OpFSub, scale, scaleHigh); + auto error = exact(spv::OpFSub, exact(spv::OpFMul, clampedHigh, scaleHigh), product); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedHigh, scaleLow)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleHigh)); + error = exact(spv::OpFAdd, error, exact(spv::OpFMul, clampedLow, scaleLow)); + const auto below = ext(GLSLstd450Floor, product); + const auto fraction = Binary(state, spv::OpFSub, vec4, product, below); + state.module.AddAnnotation(spv::OpDecorate, fraction, spv::DecorationNoContraction); + const auto tie = Binary(state, spv::OpFOrdEqual, bvec4, fraction, splat(vec4, ConstantF32(state, std::bit_cast(0.5f)))); + const auto above = Binary(state, spv::OpFAdd, vec4, below, one); + const auto even = ext(GLSLstd450RoundEven, product); + const auto broken = Select(state, vec4, Binary(state, spv::OpFOrdGreaterThan, bvec4, error, zero), above, Select(state, vec4, Binary(state, spv::OpFOrdLessThan, bvec4, error, zero), below, even)); + const auto level = Select(state, vec4, tie, broken, even); + const auto quantized = Binary(state, spv::OpFMul, vec4, level, reciprocal); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, vec4, result, splat(bvec4, enabled), quantized, texel); + return result; +} + void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const bool uintImage = access.image.numericClass == IrTextureNumericClass::Uint; @@ -1015,7 +1082,8 @@ void EmitWriteOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { EmitIfCondition(state, condition, [&]() { const auto mipLod = access.image.mipMode == ImageMipMode::DynamicStorage ? LodU32(ctx, access) : 0u; const auto coord = CoordU32(ctx, access); - const auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + auto texel = access.mem.imagePacked ? PackedStoreTexel(ctx, access, ctx.Arg(access.inst, 2)) : StoreTexel(ctx, access, ctx.Arg(access.inst, 2), uintImage); + if (!uintImage && !access.mem.imagePacked && !access.image.atomic && access.image.table == NoTable && !state.registrationModules && access.image.conversionFormat == IrBufferFormat::Invalid) texel = QuantizeStoreTexel(state, access.mem.resource, texel); EmitStorageImageWrite(state, access.mem.resource, mipLod, coord, texel, ImageSampleIndex(ctx, access)); }); } @@ -1197,8 +1265,8 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con if (setup.dref && (access.image.emulatedCompare & EmulatedCompare::Enabled) != 0u) { ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } - if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (dimension == RdnaImageDimension::Dim1D || dimension == RdnaImageDimension::Dim1DArray || access.image.remappedDepth1D) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal) || access.image.remappedDepth1D) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); @@ -1483,6 +1551,9 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con const auto field = (bits >> (index * 8u)) & 0x3fu; values[index] = ConstantI32(state, static_cast(field ^ 0x20u) - 0x20); } + if (access.image.remappedDepth1D && setup.dimensionInfo.spatialComponents >= 2u) { + values[1] = ConstantI32(state, 0); + } const auto count = setup.dimensionInfo.spatialComponents; const auto offset = count == 1u ? values[0] : count == 2u ? state.module.Constant(spv::OpConstantComposite, TypeI32Vector(state, 2), values[0], values[1]) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 42eb7dd81b..d997a174eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1559,14 +1559,14 @@ std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto guard = static_cast(found - guarded.begin()); const auto flag = load(ConstantU32(state, resources.srtGuardOffset + guard)); const auto poisoned = Binary(state, spv::OpINotEqual, TypeBool(state), flag, ConstantU32(state, 0u)); + const auto records = resources.srtGuardOffset + static_cast(guarded.size()); + if (records + 3u * guarded.size() >= (1u << BdaAbi::SrtGuardStageShift)) { + ctx.Fail(inst, "has more guarded SRT records than the fault buffer's guard word can name"); + } EmitIfCondition(state, poisoned, [&] { - const auto records = resources.srtGuardOffset + static_cast(guarded.size()); const auto record = Binary(state, spv::OpIAdd, u32, ConstantU32(state, records), Binary(state, spv::OpIMul, u32, Binary(state, spv::OpISub, u32, flag, ConstantU32(state, 1u)), ConstantU32(state, 3u))); - const auto word = [&](std::uint32_t offset) { return load(Binary(state, spv::OpIAdd, u32, record, ConstantU32(state, offset))); }; - const auto pc = word(0u); - const auto low = word(1u); - const auto high = word(2u); - RecordBdaFaultWords(state, low, high, ConstantU32(state, 4u), pc, BdaAbi::FaultReason::Unmapped); + const auto stage = ConstantU32(state, static_cast(resources.stage) << BdaAbi::SrtGuardStageShift); + state.module.AddFunction(spv::OpAtomicStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, BdaAbi::SrtGuardWord)), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), Binary(state, spv::OpBitwiseOr, u32, record, stage)); }); StopBdaInvocationIf(state, poisoned); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9bbf4fb15b..5fad9ba460 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -341,9 +341,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); state.module.EmitExtension("SPV_KHR_float_controls"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); - if (state.float32FlushToZero) { + if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { state.module.EmitCapability(spv::CapabilityDenormFlushToZero); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } else if (state.float32DenormPreserve && !state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 32u); } const bool deviceFloat64 = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) != state.supportedCapabilities.end(); state.integerInterpolation = state.requirements.float64Interpolation && !deviceFloat64; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index e5fbb7f67c..8449663a77 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -796,9 +796,10 @@ std::uint32_t TessellationPointer(SpirvValueEmitContext& ctx, const IrValue& ins const auto stride = local ? state.program.Metadata().tessellationLocalStride : kind == TessellationAttribute::ControlShared ? state.program.Metadata().tessellationSharedStride : state.program.Metadata().tessellationControlStride; const auto type = TypePointer(state, storage, TypeU32(state)); const auto inPatch = [&](std::uint32_t vertex) { - if (kind != TessellationAttribute::ControlInput) return vertex; + if (kind != TessellationAttribute::ControlInput && kind != TessellationAttribute::EvaluationInput) return vertex; + const auto& tess = state.inputInfo.vertex->tess; const auto clamped = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, state.inputInfo.vertex->tess.inputControlPoints - 1u)); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, vertex, ConstantU32(state, (kind == TessellationAttribute::ControlInput ? tess.inputControlPoints : tess.outputControlPoints) - 1u)); return clamped; }; const bool sharedRead = kind == TessellationAttribute::ControlShared && inst.Opcode() == IrOpcode::GetTessellationAttribute; diff --git a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp index 8fe4a01871..4f7c73ee5c 100644 --- a/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp +++ b/core/shader/recompiler/Translation/include/Translation/ShaderInputInfoBuilder.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { // hostSubgroupSize is the device subgroup width; wave64 workgroups on 32-wide subgroups run each // invocation as two guest lanes so a guest wave stays one host subgroup. ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh = nullptr, const TessellationConfiguration* tessellation = nullptr); } diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 529ba7d4de..a7748044c4 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -58,6 +58,7 @@ private: IrU1 threadBit(const std::array& mask); void writeRawU32(const RdnaOperand& operand, IrU32 value); IrF32 applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value); + IrF32 scaleF32Result(std::uint32_t omod, IrF32 value); IrF32 applyF16ResultModifiers(const RdnaOperand& operand, IrF32 value); bool outputModifierApplies(std::uint32_t denormalShift) const; bool dx10Clamp() const { return !floatMode.has_value() || floatMode->dx10Clamp; } diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 5e3a9c4ea7..0632342acb 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -686,10 +686,7 @@ IrU32 TranslationContext::minMaxF16Bits(bool maxValue, IrU32 lhs, IrU32 rhs) { return ir.BitwiseXor(value, ir.BitwiseAnd(ir.ShiftRightArithmetic(value, ir.Constant(31u)), ir.Constant(0x7fffu))); }; const IrU1 ordered(ir.Emit(maxValue ? IrOpcode::SGreaterThanEqual32 : IrOpcode::SLessThan32, IrType::U1, {&order(lhs), &order(rhs)})); - const IrU32 either(ir.BitwiseOr(lhs.Value(), rhs.Value())); - const IrU1 bothZero(ir.IEqual(ir.BitwiseAnd(either.Value(), ir.Constant(0x7fffu)), ir.Constant(0u))); - const IrU32 zero = maxValue ? IrU32(ir.BitwiseAnd(lhs.Value(), rhs.Value())) : either; - return IrU32(ir.Select(bothZero.Value(), zero.Value(), ir.Select(ordered.Value(), lhs.Value(), rhs.Value()))); + return IrU32(ir.Select(ordered.Value(), lhs.Value(), rhs.Value())); } bool TranslationContext::minMaxF16(const RdnaInstruction& inst, IrOpcode opcode) { @@ -944,7 +941,13 @@ bool TranslationContext::floatCube(const RdnaInstruction& inst, std::uint32_t re } const IrU32 bits(ir.BitCastU32(result.Value())); const IrU1 nan(ir.UGreaterThan(ir.BitwiseAnd(bits.Value(), ir.Constant(0x7fffffffu)), ir.Constant(0x7f800000u))); - writeOperand(inst.destination, &ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + result = IrF32(ir.BitCastF32(ir.Select(nan.Value(), quietNan32(bits).Value(), bits.Value()))); + RdnaOperand destination = inst.destination; + if (destination.omod != 0u && !ieeeMode) { + result = scaleF32Result(destination.omod, result); + } + destination.omod = 0u; + writeOperand(destination, &result.Value()); return true; } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 6c894d3e8c..62fc9b9f65 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -61,6 +61,18 @@ void _detectVertexBuffers(ShaderVertexInputInfo& info) { } } +void _validateTessellation(const TessellationConfiguration* tessellation) { + if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); + if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); + if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); + if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); +} + +void _validateMesh(const MeshConfiguration* mesh) { + if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + if (mesh->threadsPerGroup == 0u || mesh->maxVertices == 0u || mesh->maxPrimitives == 0u || (mesh->outputPrimitiveSize != 2u && mesh->outputPrimitiveSize != 3u) || mesh->provokingVertex >= mesh->outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); +} + } ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const GuestContext& context, std::uint32_t hostSubgroupSize, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { @@ -191,10 +203,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues } _detectVertexBuffers(vertexStorage); if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { - if (tessellation == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: tessellation configuration is missing"); - if (tessellation->inputControlPoints == 0u || tessellation->inputControlPoints > 32u || tessellation->outputControlPoints == 0u || tessellation->outputControlPoints > 32u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation control-point counts"); - if ((tessellation->domain != 1u && tessellation->domain != 2u) || (tessellation->partitioning != 0u && tessellation->partitioning != 2u && tessellation->partitioning != 3u) || (tessellation->outputTopology != 2u && tessellation->outputTopology != 3u)) throw std::runtime_error("ShaderInputInfoBuilder: unsupported tessellation configuration"); - if (tessellation->patchesPerGroup == 0u || tessellation->patchesPerGroup > 256u) throw std::runtime_error("ShaderInputInfoBuilder: invalid tessellation patches per group"); + _validateTessellation(tessellation); auto& target = vertexStorage.tess; target.inputControlPoints = tessellation->inputControlPoints; target.outputControlPoints = tessellation->outputControlPoints; @@ -209,7 +218,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.mergedHull = tessellation->mergedHull; } if (stage == ShaderStageKind::Mesh) { - if (mesh == nullptr) throw std::runtime_error("ShaderInputInfoBuilder: a mesh-stage program has no mesh configuration"); + _validateMesh(mesh); auto& target = vertexStorage.mesh; target.threadsNum[0] = mesh->threadsPerGroup; target.threadsNum[1] = 1u; @@ -225,7 +234,6 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues target.provokingVertex = mesh->provokingVertex; target.esgsItemSize = mesh->esgsItemSize; target.outputPrimitiveSize = mesh->outputPrimitiveSize; - if (target.threadsNum[0] == 0u || target.maxVertices == 0u || target.maxPrimitives == 0u || (target.outputPrimitiveSize != 2u && target.outputPrimitiveSize != 3u) || target.provokingVertex >= target.outputPrimitiveSize) throw std::runtime_error("ShaderInputInfoBuilder: invalid mesh configuration"); } ShaderStageInputInfo result; result.vertex = &vertexStorage; @@ -238,4 +246,51 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues throw std::runtime_error("ShaderInputInfoBuilder: unexpected stage"); } +void ValidateVertexStageInputs(ShaderStageKind stage, const GuestContext& context, const MeshConfiguration* mesh, const TessellationConfiguration* tessellation) { + if (!context.vertex.has_value()) { + throw std::runtime_error("ShaderInputInfoBuilder: GuestContext.vertex is not set"); + } + const auto& vertex = *context.vertex; + if (vertex.resourcesNum > vertex.resources.size()) throw std::runtime_error("ShaderInputInfoBuilder: invalid vertex resource count"); + struct Buffer { + std::uint64_t addr; + std::uint32_t stride; + std::uint32_t numRecords; + std::uint32_t fetchIndex; + int attrNum; + }; + std::array buffers; + int buffersNum = 0; + const std::array embedded{}; + for (std::uint32_t ri = 0; ri < vertex.resourcesNum; ++ri) { + const auto& fields = vertex.fetchEmbedded ? embedded : vertex.resources[ri].fields; + const std::uint16_t stride = static_cast((fields[1] >> 16u) & 0x3fffu); + const std::uint64_t base = (static_cast(fields[0]) | (static_cast(fields[1]) << 32u)) & 0xffffffffffffull; + const std::uint32_t numRecords = fields[2]; + const std::uint32_t fetchIndex = vertex.fetchEmbedded ? 0u : vertex.resourcesDst[ri].fetchIndex; + bool merged = false; + for (int bi = 0; bi < buffersNum; ++bi) { + auto& b = buffers[bi]; + if (b.stride != stride || b.fetchIndex != fetchIndex) continue; + const auto low = base < b.addr ? base : b.addr; + if (base - low >= stride || b.addr - low >= stride) continue; + if (b.numRecords != numRecords) throw std::runtime_error("ShaderInputInfoBuilder: merged vertex buffers disagree on record count"); + b.addr = low; + if (b.attrNum >= ShaderVertexInputBuffer::MaxAttributes) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer attribute count exceeds the supported domain"); + ++b.attrNum; + merged = true; + break; + } + if (merged) continue; + if (buffersNum >= ShaderVertexInputInfo::MaxResources) throw std::runtime_error("ShaderInputInfoBuilder: vertex buffer count exceeds the supported domain"); + buffers[buffersNum++] = {base, stride, numRecords, fetchIndex, 1}; + } + if (stage == ShaderStageKind::Local || stage == ShaderStageKind::TessellationControl || stage == ShaderStageKind::TessellationEvaluation) { + _validateTessellation(tessellation); + } + if (stage == ShaderStageKind::Mesh) { + _validateMesh(mesh); + } +} + } diff --git a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp index d0ae0c4608..d54715a372 100644 --- a/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp +++ b/core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp @@ -379,21 +379,25 @@ std::array TranslationContext::quietNan64(const std::array& return ieeeMode ? std::array{bits[0], IrU32(ir.BitwiseOr(bits[1].Value(), ir.Constant(0x00080000u)))} : bits; } +IrF32 TranslationContext::scaleF32Result(std::uint32_t omod, IrF32 value) { + IrValue& bits = ir.BitCastU32(value.Value()); + IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); + IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); + IrValue* result = nullptr; + if (omod == 3u) { + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); + } else { + const std::uint32_t exponentStep = omod == 1u ? 0x00800000u : 0x01000000u; + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); + } + result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); + result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); + return IrF32(ir.BitCastF32(*result)); +} + IrF32 TranslationContext::applyF32ResultModifiers(const RdnaOperand& operand, IrF32 value) { if (operand.omod != 0u && outputModifierApplies(4u)) { - IrValue& bits = ir.BitCastU32(value.Value()); - IrValue& magnitude = ir.BitwiseAnd(bits, ir.Constant(0x7fffffffu)); - IrValue& sign = ir.BitwiseAnd(bits, ir.Constant(0x80000000u)); - IrValue* result = nullptr; - if (operand.omod == 3u) { - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x01000000u)), sign, ir.ISub(bits, ir.Constant(0x00800000u))); - } else { - const std::uint32_t exponentStep = operand.omod == 1u ? 0x00800000u : 0x01000000u; - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x7f800000u - exponentStep)), ir.IAdd(bits, ir.Constant(exponentStep)), ir.BitwiseOr(sign, ir.Constant(0x7f800000u))); - } - result = &ir.Select(ir.ULessThan(magnitude, ir.Constant(0x00800000u)), ir.Constant(0u), *result); - result = &ir.Select(ir.UGreaterThan(magnitude, ir.Constant(0x7f7fffffu)), bits, *result); - value = IrF32(ir.BitCastF32(*result)); + value = scaleF32Result(operand.omod, value); } if (operand.clamp) { const IrF32 saturated(ir.Emit(IrOpcode::FPSaturate32, IrType::F32, {&value.Value()})); diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 138602235a..8d5bae5a39 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -214,12 +214,12 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 39u; + upgraded[4] = 41u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -254,12 +254,12 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 39u; + layered[4] = 41u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -280,11 +280,11 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 39u; + bytes[4] = 41u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -410,10 +410,10 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - upgraded[4] = 39u; + upgraded[4] = 41u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -429,14 +429,14 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 39u; + main25[4] = 41u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -447,13 +447,19 @@ int main() { for (const bool preserve : {false, true}) { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; + request.target.float32DenormPreserve = preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 41u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto version40 = bytes; + version40.pop_back(); + version40[4] = 40u; + const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); + Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 40u; + future[4] = 42u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -496,6 +502,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); version35.erase(stencilOperationValue); diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index b1b725a14f..4cebf79a20 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -192,7 +192,9 @@ On a device that supports `VK_EXT_device_fault`, the first `VK_ERROR_DEVICE_LOST Set `APS5_SAMPLE_WORKER=` to sample queue worker 0 every millisecond (in practice every timer tick). Every 20 s and at exit it writes ``, one `module offset self inclusive` line per sampled address, and `.collapsed.txt`, the worker's whole stacks in the collapsed format that speedscope.app and flamegraph.pl read: one `frame;...;frame count` line per distinct stack, root first, each frame as `module+0xRVA`. A walk that ends before the thread's root starts with `[truncated]`. The `#` lines give the samples, the walk depth (64 frames), the truncated walks, the mean and longest time the worker was suspended, and the size and PE timestamp of each module, to check the binary a file is symbolized against. A frame with `.pdata` is stepped by `RtlVirtualUnwind`; any other frame by its DWARF record in `.ehfram`, the unwind data the libraries are built with. Those steps read only inside the worker's stack, and a module is indexed after the worker resumes, the first time a walk meets it. -`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. Both samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. +`APS5_SAMPLE_COMMITTER=` samples queue 0's draw committer the same way and writes the same two files. The committer records the draws the worker hands over (`APS5_PIPELINED_DRAWS`), so when the worker's stacks wait in `DrawPipeline::Enqueue`, its stacks show what bounds the frame. It starts with the first pipelined draw; with `APS5_PIPELINED_DRAWS=0` nothing is sampled. + +`APS5_SAMPLE_THREADS=` samples every thread of the process every 2 ms and writes the 40 hottest addresses of each busy thread to ``, 12 frames deep and through `.pdata` only. The samplers exist only on Windows; the addresses are symbolized offline, for example with `nm` on the `.prx`. ## Performance trace diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 600164daa1..58eb4f894d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -142,6 +142,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_FILL_SYNC` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | waits for every recorded store over the fill's range | | `APS5_FLIP_INFLIGHT` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1 | number of presentations that may trail on the GPU (0, 1 or 2) | | `APS5_FLIP_READ_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records the in-place reads of completed batches and checks them before the present blit's fence | +| `APS5_FORCE_SDR` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp` | unset | suppresses advertised HDR availability while retaining detected host capabilities; uses the existing SDR presentation path | | `APS5_FORCE_STRUCTURIZER_FALLBACK` | presence | `core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp` | | makes the control-flow test expect the reach-flag fallback instead of no cloning | | `APS5_FULL_BARRIERS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | records every barrier instead of merging redundant ones | | `APS5_GPU_CHECKPOINTS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | attaches device diagnostic checkpoints through VK_NV_device_diagnostic_checkpoints | @@ -156,6 +157,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_HOOK_LOCKED_WAIT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | keeps the flush hook's pending-write wait under the GpuMutex | | `APS5_HOST_IMPORT_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp` | 0 | budget in MiB for pinned host imports; `0` selects the larger of 6144 MiB and half the physical memory, at most the largest host heap minus the larger of 2 GiB and half that heap (4055 MiB on a 16 GB Windows PC with an RTX 4060) | | `APS5_IGNORE_DEPTH_TEST` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp` | | renders depth- and stencil-tested draws without a depth target as if the tests always passed | +| `APS5_IMAGE_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the released image memory the buffer pool keeps for reuse by memory type and size while the device-local heap is under its reported budget, a quarter of the device tier's budget by default; `0` frees image memory at once | | `APS5_INDIRECT_VERTEX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/IndirectDraw.cpp` | 64 | cap in MiB on a vertex attribute extent, above which a GPU-side indirect draw resolves on the CPU | | `APS5_INSERT_COMPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | | compares a fresh capture's regions against the recorder before inserting it into the cache | | `APS5_KEPT_DESCRIPTOR_SETS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 8 | number of freed dedicated image table descriptor sets kept for a later build with the same layout and sizes; 0 keeps none | @@ -192,6 +194,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_BLOCK_TRACKING` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | tracks array layers, or the whole surface, instead of 64 KiB blocks | | `APS5_NO_BUFFER_CLASSES` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | matches exact sizes in the buffer pool instead of rounding up to size classes | | `APS5_NO_CACHED_PRECOLLECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | leaves the write-watch walks of a cached object's Revalidate under the GPU mutex | +| `APS5_NO_CLEAR_HAND_OVER` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | writes other images' pending results back at a clear-key storage refresh instead of handing them over to the refreshed image | | `APS5_NO_CLEARED_VIEW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | keeps fast-cleared surfaces as CPU snapshots instead of viewing the cleared storage image | | `APS5_NO_COLLECT_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | disables the per-thread memo of write-watch collects | | `APS5_NO_CONTEXT_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | rebuilds the Graphics::Context on every call instead of returning the cached one | @@ -232,16 +235,18 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_GPU_INDIRECT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads indirect dispatch arguments on the CPU instead of letting the GPU count them | | `APS5_NO_GPU_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | reads indirect draw arguments through the flush hook on the CPU instead of from the GPU | | `APS5_NO_GPU_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | dumps occlusion-counter samples on the CPU instead of through the GPU copy path | +| `APS5_NO_GPU_THREAD_DIMENSIONS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/IndirectDispatch.cpp` | | reads the thread counts of an indirect dispatch with USE_THREAD_DIMENSIONS on the CPU instead of turning them into group counts and a thread limit on the GPU | | `APS5_NO_HOLD_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | turns off GpuMutex hold-time profiling, keeping only the wait timing | | `APS5_NO_HOOKSYNC_PROFILE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | leaves only the plain hook-sync counts under APS5_PROFILE_DRAW | | `APS5_NO_HOOK_COMPLETION_GUARD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets the flush hook record the accessing worker's queued labels even from inside a completion action | | `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | | `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | | `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | +| `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check | -| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch | +| `APS5_NO_KEEP_NEW_STORAGE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | does not keep a newly created storage image alive with the open batch; such an image evicted before the batch completes can hand its memory to another image through the buffer pool while the batch still writes it | | `APS5_NO_KEYS_FILL_MEMO` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp` | | records no memo of GPU-side DCC key fills | | `APS5_NO_KEYS_REFILL` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | keeps a covered image's uploaded keys as they were instead of marking them uncompressed after a matching key fill | | `APS5_NO_KEYS_RESCAN` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | | reuses the DCC keys scanned before the lookup's refresh instead of discarding them for a rescan | @@ -348,6 +353,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_RELEASE_UNDER_LOCK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | destroys deferred batches in finish() under the lock instead of on a release thread | | `APS5_RESOURCE_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1024 | maximum entries in the per-device shader-resource cache | | `APS5_RESOURCE_CACHE_TABLE_ELEMENTS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 65536 | number of image-table elements the shader-resource cache keeps (0 keeps the default) | +| `APS5_SAMPLE_COMMITTER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples queue 0's draw committer every millisecond and writes its stacks to a file (Windows only) | | `APS5_SAMPLE_THREADS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples every thread of the process every 2 ms and writes each busy thread's hottest frames to a file (Windows only) | | `APS5_SAMPLE_WORKER` | string | `core/libs/prx/libSceAgcDriver/Execution/src/WorkerSampler.cpp` | | samples the calling queue worker every millisecond and writes its stacks to a file (Windows only) | | `APS5_SHADOW_VERIFY` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp` | | verifies that the shadow-published bytes equal the guest bytes | @@ -377,6 +383,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SYNC_SAMPLE_DUMPS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | takes the CPU path for occlusion-counter sample dumps, as `APS5_NO_GPU_SAMPLE_DUMPS` does | | `APS5_SYNC_SHADER_PREPARE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp` | | prepares registered shaders on the registering thread instead of in the background | | `APS5_SYNC_TEXTURE_UPLOAD` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | restores the waiting batch for every texture upload | +| `APS5_TEMPLATE_AUDIT_MS` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 1000 | milliseconds after which a cached shader-resource object takes the full walk instead of the fast proof; `0` turns the audit off | | `APS5_TEXTURE_CACHE_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 0 | forced byte budget in MiB for the storage and sampled texture caches; `0` selects the budget automatically | | `APS5_THREAD_NICE` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | -10 | nice value the raised queue workers and presenter ask rtkit for (clamped to -20..19) | | `APS5_THREAD_PRIORITY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/ThreadPriority.cpp` | off | raises the queue workers and presenter above the title's threads: `off`, `high` or `rt` | @@ -533,7 +540,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_WRITE_WATCH` | presence | `core/libs/prx/libc/src/GuestArena.cpp` | | disables write watching so guest buffers are fully re-read on every use | | `APS5_PIN_WAIT_MS` | integer | `core/libs/prx/libc/src/GuestAllocations.cpp` | 60000 | milliseconds to wait for a pinned allocation lease before failing | | `APS5_TRACE_AFFINITY` | presence | `core/libs/prx/libc/include/CpuTopology.hpp` | | prints a line naming the role, tid and mask of every thread pinning | -| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/windows/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | +| `APS5_TRACE_CALLS` | string | `core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp` | | comma-separated libraries (or all) whose guest calls are traced | | `APS5_TRACE_EXIT` | presence | `core/libs/prx/libc/src/Process.cpp` | | prints a backtrace of the caller of exit, _Exit and the process-exit path | | `APS5_TRACE_SSE4A` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | prints each emulated SSE4a instruction once per rip | | `APS5_WATCH_ADDR` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | absolute hex address whose writes are reported after its page is committed | @@ -548,6 +555,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | pins the title's job worker threads to the efficiency cores of a hybrid CPU | | `APS5_JOB_AFFINITY_MASK` | integer | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | hex core mask that job worker threads are pinned to | | `APS5_NO_JOB_AFFINITY` | presence | `core/libs/prx/libkernel/Pthread/src/Thread.cpp` | | disables pinning of job worker threads | +| `APS5_NO_SLEEP_WAKE` | presence | `core/libs/prx/libkernel/Time/SleepWake.cpp` | | keeps every sleep of 1 ms or less in full instead of ending it at a GPU batch, batch completion action or flip completion | | `APS5_NO_TIMER_RESOLUTION` | presence | `core/libs/prx/libkernel/Time/Time.cpp` | | keeps the default 15.6 ms scheduler tick instead of raising it to 0.5 ms on Windows | | `APS5_TIME_SCALE` | integer | `core/libs/prx/libkernel/Time/Time.cpp` | 1 | factor applied to the guest monotonic clocks and TSC; sleeps still take real time | | `APS5_TRACE_APR` | presence | `core/libs/prx/libkernel/Apr/src/Apr.cpp` | | logs every APR file read with its path, offset, size and destination | diff --git a/docs/dev/TRACING.md b/docs/dev/TRACING.md index a4c7f10a6a..8cb84bb4a4 100644 --- a/docs/dev/TRACING.md +++ b/docs/dev/TRACING.md @@ -11,7 +11,7 @@ Set a switch before starting the emulator. Unless noted, any value enables it. M | `APS5_TRACE_APR` | APR file reads, including path, offset, size and destination. | | `APS5_TRACE_AUDIOOUT` | Audio output port parameters when a port is opened. | | `APS5_TRACE_AUDIOOUT2` | AudioOut2 context and port parameters, attributes, pushes, queue polls and periodic queue summaries. | -| `APS5_TRACE_CALLS` | On Windows, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | +| `APS5_TRACE_CALLS` | On Windows and Linux, records calls to the imports selected by this comma-separated library list (or `all`); crash and abort reports (abort only on Linux) include the last 64 calls and most-called imports. The executable must be converted with `--trace-imports`; see [Usage](../user/USAGE.md#tracing-system-library-calls). | | `APS5_TRACE_EXIT` | Guest `exit` and `_Exit` calls and process-exit backtraces. | | `APS5_TRACE_FIBER` | Fiber initialization, switches and returns. | | `APS5_TRACE_MEMORY` | Direct-memory allocation, mapping and protection operations. | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e1dbad279e..bbe289fd63 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -68,7 +68,8 @@ - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [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 dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. 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. -- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- A [depth surface](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is retired when a color target or a written storage image it does not serve overlaps it, or when it is sampled through a view it cannot serve and guest memory over it was written since it was last bound; lookups then skip it, so sampling reads the memory like any texture. Bound again, it keeps its own depth instead of the bytes written over it, except for the depth planes the next entry keeps in guest memory. Without write watching, a view it cannot serve still throws. A retired surface also gives up its depth and stencil planes, so another surface sharing them no longer copies from it. +- [Depth planes in guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): only single-sample `D32_SFLOAT` and `D32_SFLOAT_S8_UINT` planes with the 64 KiB Z swizzle (`SW_MODE` 24) reach guest memory; D16, other swizzles, stencil and multisampled planes keep depth in the host image only. A plane drawn since its last store is stored, uncompressed, when a buffer upload, a `GuestBuffers` V#, an image lookup or a pending-store flush overlaps it; CPU reads and reads through address-based heaps are not caught. Guest writes are ordered when collected (bind, flush, collect epoch), not when made, at 64 KiB granularity: a collected write reloads the whole plane unless an HTILE clear is pending. A store outside a recorder waits for the GPU. The console keeps HTILE-compressed tiles compressed in memory until a decompress (Mesa `ac_descriptors.h`). - [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). - [Repacked format-43 copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver): a sampled texture of format 43 (10-bit R, 11-bit G and B unsigned floats from bit 0) is uploaded into a `B10G11R11_UFLOAT` image with its R and B fields swapped (`RepackTextureWord`) and viewed with R and B swapped back. A texture whose results a storage image still holds is copied from it again, whole, whenever the image changed (`RefreshDepthCopy`); a 3D one is flushed to guest memory and uploaded instead. Storage reads and writes of format 43 keep the packed `R32_UINT` path. - [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. @@ -153,6 +154,7 @@ - [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value. A range a queued draw commit still has to read is left pending whatever the stamps say ([`DrawPipeline::HeldReads`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.hpp)), so a title that recycles one allocation between an index buffer and a storage image sees the draw's own indices; the trade is a title that reads a surface as index data and means the image gets the pre-image bytes instead, since the reservation does not compare the command-stream positions of the draw and of the image's write. `APS5_NO_QUEUED_DRAW_READ_ORDER=1` restores the old behaviour, and the `[storage]` line counts the listings the hold turned away (`held for a queued draw read`) and the `[draw] pipelined` line the stores it held. The hold covers what the commit reads of guest memory: the index range, the vertex input ranges as their descriptor extents ([`QueuedDrawReadRanges`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp), since the bytes a draw fetches depend on the highest index, which only the commit knows) and the source of a queued DMA copy. Every other commit-time read goes through the flush hook, so it sees the bytes the hold protects. Two commit-time reads are outside the hold: the texels a register or cmask clear materialization reads from the draw's own colour target (through the hook, so ordered, but the hold does not cover a render target), and a CPU read whose store the hold turned away, which sees the pre-image bytes. Nothing retries a refused store: the image reaches guest memory with the next flush of that range (the queue worker's idle store, a `WAIT_REG_MEM` poll over it, a later read or write of it). - [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws. - [BT.2100 PQ display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/include/PqToSdr.hpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0x8100070422000000` and `0x8100070400000000` are taken as 10:10:10:2 texels with BT.2020 primaries and the SMPTE ST 2084 transfer, red or blue in the low bits, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgb10A2Bt2100Pq`, `Bgr10A2Bt2100Pq`) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps the first; PPSA01317 registers display buffers of the first when its output reports HDR. With HDR output they are presented unchanged through an HDR10 swapchain (`VK_COLOR_SPACE_HDR10_ST2084_EXT`, 10-bit, with `VK_EXT_hdr_metadata`); otherwise they are converted to 8-bit sRGB: ST 2084 EOTF, BT.2020 to BT.709 primaries with negative components clipped, a luminance tone map and the sRGB encoding. The tone map is ours: min(203 nits, white level) is SDR white (the BT.2408 HDR reference white), luminance passes unchanged up to 0.75 of it and an extended Reinhard shoulder above reaches 1.0 at the white level, the peak sceSystemServiceGetHdrToneMapLuminance last reported to the title (or the current one when it never asked), since titles read it once. How a console shows such a buffer on an SDR display is unknown. Alpha is opaque. +- [HDR output override](../../../core/libs/prx/libSceAgcDriver/Execution/src/HostHdr.cpp) - setting `APS5_FORCE_SDR` suppresses HDR availability reported to titles while retaining the detected display and surface capabilities. It selects the existing SDR presentation path; it does not add support for floating-point BT.2100 PQ buffers (`0xC001070722000000` and `0xC001070700000000`), whose presentation remains refused. The override does not change the host display settings. - [DCC key stores](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) cover the console's DCC extent only for pipe-aligned DCC of single-level, single-layer 4-byte SW_64KB_R_X surfaces; other surfaces, and every texture and color target key read, cover one byte per 256 bytes. - [DCC key fills over other images](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) - a uniform fill at a live image's `dccAddress`, at least one key per 256 bytes long, is a key fill of that image even when other live images overlap it, but only while it is no longer than the image's key extent (`DccKeyCount`, the pipe-aligned count); a longer one is classified by its overlaps as before. The bound is inferred, not measured on a title: a longer fill also writes the bytes past the keys, which the overlapping image may own. Whether the title's bloom-target key fills stay within the extent was not checked (`APS5_TRACE_DCC_KEYS` prints the length of the fills classified as keys, not of the refused ones). - [Comp-to-single DCC keys](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp) (libSceAgcDriver) - key 0x10 (`GFX9_DCC_CLEAR_SINGLE` in Mesa `ac_descriptors.h`) uniform over a surface makes every texel the element at the start of its 256-byte block, the top-left texel of the block where radv's `build_clear_dcc_comp_to_single_shader` stores the clear color. Sampled textures and storage images read that expansion (a storage image's first write stores the keys uncompressed, as under a clear code), and a CB DCC decompress pass stores it over the texels. A fast-clear eliminate over such keys throws, as a metadata pass over mixed keys does; color targets that render over such keys use their stored texels, and textures read mixed keys, and comp-to-single keys of a format whose element does not divide 256 bytes, as stored. "Uniform" is judged over the key bytes the driver reads, one per 256 surface bytes (see DCC key stores above), so a pipe-aligned surface whose keys differ past that range is expanded as if all were comp-to-single, as for the other clear codes. Not verified on hardware diff --git a/docs/dev/debt/libSceAmpr.md b/docs/dev/debt/libSceAmpr.md index a9c31f321e..764ea7ec20 100644 --- a/docs/dev/debt/libSceAmpr.md +++ b/docs/dev/debt/libSceAmpr.md @@ -2,6 +2,7 @@ ### Unknown function info +- [AMPR command buffer null arguments](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the result for a null command buffer is unknown for `sceAmprCommandBufferConstructor`, `sceAmprAprCommandBufferConstructor`, `sceAmprCommandBufferReset` and `sceAmprCommandBufferSetBuffer`; these return `SCE_KERNEL_ERROR_EINVAL` here, but the behavior has not been confirmed on a PS5. - [AMPR WriteAddressFromCounterPairOnCompletion](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command buffer offsets](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - recording with a current offset greater than the bound memory size throws before calculating remaining capacity or writing a command. This validates the emulator's own command-buffer encoding; the console's response to a corrupted opaque buffer object is unknown. - [AMPR command sizes](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_KERNEL_ERROR_EBUSY` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes @@ -12,6 +13,5 @@ ### Functional - [sceAmprAmmGetUsageStatsData](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) fills the caller-sized 24-byte statistics structure with 512 page-table pool entries, no allocated entries, a zero high watermark and ring idle flags 0x7, the constants of [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libAmpr.cpp); they are not measured on a console, and the real call is a pass-through to `sceKernelMapperGetUsageStatsData` per [drakmor/ampr_emu](https://github.com/drakmor/ampr_emu/blob/main/src/ampr_emu_amm.cpp), which this port has no kernel-side counterpart for. -- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. -- [_Cnd_signal, _Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - take a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and act on the same conditions. `_Cnd_signal` wakes one waiter and returns `_Thrd_success` (0), as `_Cnd_broadcast` does; whether the console maps a failing internal signal to `_Thrd_error` is not known. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. Neither is confirmed on a PS5 `libc.prx`. `_Thrd_id` still throws: microsoft/STL returns an `unsigned int` thread id, but whether the PS5 returns the `pthread_t` or a kernel thread id, and at what width, is not known -- [std::_Pad](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follows Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` of Visual C++ 2013 ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. The pad is taken to be that header's class with the console's pointer handles: a vtable pointer whose first slot is the pure virtual `_Go()`, then `_Cnd_t` at 0x8, `_Mtx_t` at 0x10 and `bool _Started` at 0x18, 32 bytes, the derived `_LaunchPad` target following it; the console's `_Pad(const char*)` and `_Launch(const char*, pthread**)`/`_Launch(pthread_attr**, pthread**)` overloads suggest named primitives and could mean another layout or vtable. `_Launch` starts the thread with `pthread_create` and default attributes (the console's `_Thrd_start` name, stack size and priority are unknown), its entry calls `_Go()` and returns the result as the exit code, and it does not run `_Cnd_do_broadcast_at_thread_exit`, which is absent here. A failing primitive throws the guest `std::system_error` that `_Throw_C_error` picks for its code, and a failing thread start the `resource_unavailable_try_again` one of `_Thrd_startX`; any `pthread_create` error counts, since the console's `_Thrd_start` result is unknown. A null pad or thread slot throws +- [Dinkumware thread exports](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. The spin lock (byte exchange loop, no yield) matches that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning. +- [_Cnd_init_with_name](../../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - takes a pointer to the handle slot like the libc `_Cnd_*` exports from [#3079](https://github.com/boykopovar/AnyPS5/pull/3079) and acts on the same conditions. The signature `_Cnd_init_with_name(_Cnd_t*, const char* name)` is inferred from [fpPS4 `ps4_mtx_internal.pas`](https://github.com/red-prig/fpPS4/blob/trunk/src/libcinternal/ps4_mtx_internal.pas), where the PS4 `_Mtx_init` is `_Mtx_init_with_name` with the name `SceLibcI_Mtx` and the name only reaches the pthread object; here it is `_Cnd_init` and the name, which may be null, is ignored. This is not confirmed on a PS5 `libc.prx`. diff --git a/docs/dev/debt/libSceAudio3d.md b/docs/dev/debt/libSceAudio3d.md index 09f5bce9c1..c7d94441a4 100644 --- a/docs/dev/debt/libSceAudio3d.md +++ b/docs/dev/debt/libSceAudio3d.md @@ -2,10 +2,31 @@ ### Silent stubs -- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved and `sceAudio3dObjectSetAttributes` accepts their attributes, but the library has no bed exports or mixing to give them audio, the attributes have no effect and `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them. `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it and the error only for a released object; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others; the attribute identifiers and the value of each are not interpreted. `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output, as there is no output +- [libSceAudio3d](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - `sceAudio3dObjectSetAttributes` validates the port, the reserved object and the attribute array (an object id that was never reserved returns the invalid-object error, while shadPS4 returns success for it; neither is measured on a console), treating the reset-state attribute as valid with a null value and requiring a value for the others, but the identifiers and the value of each are not interpreted, so an object has no samples, no gain and no position: the ids of the `PCM`, `GAIN` and `POSITION` attributes and the `POSITION` layout are unknown (the aerolib NID list gives the names only, the Orbital IDA type database the signatures only and shadPS4's values are PS4). `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them, and `sceAudio3dPortFlush` drops the frames queued by `sceAudio3dPortAdvance` instead of mixing and submitting them to an output + +### Functional + +- [libSceAudio3d bed path](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - bed PCM is mixed and played: `sceAudio3dBedWrite` and `sceAudio3dBedWrite2` queue a bed, bounded by `queue_depth` and `NOT_READY` when full, `sceAudio3dPortAdvance` consumes one and mixes it into a stereo frame with a linear-balance pan and a 5.1/7.1 downmix, and an advance with no bed queued mixes silence ([Mixer.hpp](../../../core/libs/prx/libSceAudio3d/Mixer.hpp)) and queues the frame, and `sceAudio3dPortPush` opens a type-126 AudioOut port on first use and submits the queued frames to it. The explicit `sceAudio3dAudioOutOpen`, `sceAudio3dAudioOutOutput` and `sceAudio3dAudioOutOutputs` exports queue their blocks and the port drains them. The attribute enum values and the panning model are unverified ### Unknown function info - [sceAudio3dPortOpen](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too - [sceAudio3dPortGetAttributesSupported](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the supported list (late reverb level, downmix spread radius, downmix spread height aware) is inferred from the attributes `sceAudio3dPortSetAttribute` accepts; its order and the count written when the caller capacity is smaller are unverified. [shadPS4](https://github.com/shadps4-emu/shadPS4) returns object attributes (PCM, gain...) instead, prosper and KytyPS5 have no body - [sceAudio3dObjectReserve](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF +- [sceAudio3dStrError](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortOpenEx](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortCreate](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortDestroy](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dPortGetStatus](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportRegisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dReportUnregisterHandler](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMemorySize](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dCreateSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dDeleteSpeakerArray](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [sceAudio3dGetSpeakerArrayMixCoefficients2](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - unknown signature +- [Audio3d bed downmix coefficients](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the centre and the surrounds fold into stereo with a 0.7071 gain and the LFE channel is dropped, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4): its `DOWNMIX_CENTER` and `DOWNMIX_SURROUND` are 0.7071 and its `DOWNMIX_LFE` is 0, so the `if constexpr` that adds the LFE is compiled out. [prosper](https://github.com/mattias800/prosper) disagrees on the LFE only: it folds it into both sides with a 0.5 gain, in its AudioOut2 main bed path, which is not an Audio3d path. KytyPS5 has no Audio3d mixing at all. The absolute level is unmeasured: fpPS4 scales the front by 1 + 3/sqrt(2) instead of 1 +- [Audio3d bed channel order](../../../core/libs/prx/libSceAudio3d/Mixer.hpp) (libSceAudio3d) - the buffer is read as L, R, C, LFE, Ls, Rs, Lrs, Rrs, the standard order, which three references agree on: [shadPS4](https://github.com/shadps4-emu/shadPS4) reads the front, centre, LFE and surround samples at those indices, [prosper](https://github.com/mattias800/prosper) documents FL, FR, FC, LFE and then surround pairs, and fpPS4 names them `_FL=0 _FR=1 _FC=2 _LF=3 _SL=4 _SR=5 _BL=6 _BR=7`. fpPS4's `STD` layout swaps the side and back pairs, which this library does not model +- [sceAudio3dAudioOutOutput](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - the return is the number of frames per channel in the queued block, which follows [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [prosper](https://github.com/mattias800/prosper), whose `sceAudioOutOutput` returns the grain, and this project's own `sceAudioOutOutput`, which returns the port's `samplesNum` (the buffer length in frames). [shadPS4](https://github.com/shadps4-emu/shadPS4) returns the total samples instead (`len * channels`) and fpPS4 returns 0; no console measurement settles it +- [sceAudio3dPortAdvance buffer mode](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a mode other than 2 returns `NOT_SUPPORTED` (0x80EA0008), the value [shadPS4](https://github.com/shadps4-emu/shadPS4) uses for it and the one [prosper](https://github.com/mattias800/prosper) reads from the PS5 module's own error table, where 0x80EA0008 is what the per-port calls return for a buffer mode they do not support. shadPS4 and prosper refuse only mode 0 in `sceAudio3dPortAdvance` and modes 0 and 1 in `sceAudio3dPortPush`, while [KytyPS5](https://github.com/KytyPS5/KytyPS5) requires mode 2 at `sceAudio3dPortOpen` and halts otherwise, so this library refuses every other mode at the open and the check here is unreachable. 0x80EA0005 is `OUT_OF_MEMORY`, not `NOT_SUPPORTED` +- [sceAudio3dPortAdvance on a full queue](../../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - a full queue returns `NOT_READY` (0x80EA0007) instead of throwing, which follows [shadPS4](https://github.com/shadps4-emu/shadPS4), the only reference with a body: it returns `ORBIS_AUDIO3D_ERROR_NOT_READY` when the mixed queue is at the port's depth. [KytyPS5](https://github.com/KytyPS5/KytyPS5) halts instead and prosper has no queue at all diff --git a/docs/dev/debt/libSceAudioIn.md b/docs/dev/debt/libSceAudioIn.md index 63b2d8b02c..34211858e7 100644 --- a/docs/dev/debt/libSceAudioIn.md +++ b/docs/dev/debt/libSceAudioIn.md @@ -7,6 +7,38 @@ ### Unknown function info - [sceAudioInIsPadEmbeddedMic](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - name from its NID (`X+4jdIS75P0`); signature `int32_t(int32_t handle)` from the Ghost of Yotei (PPSA26344) call site in #2355; the result's meaning is unconfirmed, so it still throws rather than invent one +- [sceAudioInDeviceIdOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdAsyncOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUsbGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInIsSharedDevice](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceIdHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceHqOpen](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortConnections](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetPortStatuses](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInGetHandleStatusInfo](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInHqOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInDeviceOpenEx](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInChangeAppModuleState](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkParam](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetSparkSideTone](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetCompressorPreGain](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetNoiseGateThreshold](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMicLevel](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDeviceMuteStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetDevConnection](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInCountPorts](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetUserMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicCreate](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicDestroy](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicWrite](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicGetStatus](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInVmicSetMute](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInSetMode2](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInExtCtrl](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature +- [sceAudioInInputs](../../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown signature ### Functional diff --git a/docs/dev/debt/libSceFont.md b/docs/dev/debt/libSceFont.md index 6dfb5a445f..5c3d5ad062 100644 --- a/docs/dev/debt/libSceFont.md +++ b/docs/dev/debt/libSceFont.md @@ -18,6 +18,7 @@ ### Functional -- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the other script files of a set are chosen from its type, not measured on a console: Vietnamese for low-byte bit 0x10, Thai for script bit 0x10, Japanese for 0x04, simplified Chinese for 0x80 (before Japanese with 0x08), Hangul for 0x20 and Arabic for low-byte bit 0x80, as the SDK set names (`..._JP_AR`, `..._GBCJK_TH`) listed by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/font/font_internal.cpp) suggest; the file order and the Hangul file name (`SCEPS4Yoongd-Medium.otf`, `SCEPS4Yoongd-Bold.otf`) also come from shadPS4. A character the primary file lacks is taken from the first of them that has it; kerning between two files is 0, and `sceFontGetCharGlyphCode` and `sceFontGetFontGlyphsCount` answer for the primary file only +- [sceFontOpenFontSet](../../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) - the console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans Arabic, Noto Sans CJK with its Korean face for Hangul) is loaded instead; its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [sceFontGetCharGlyphMetrics, sceFontGetRenderCharGlyphMetrics](../../../core/libs/prx/libSceFont/src/Layout.cpp) (libSceFont) report the vertical metrics the way the [FreeType driver](../../../core/libs/prx/libSceFontFt/src/Driver.cpp) reports them for a rendered glyph, not confirmed on a title: FreeType's vertical advance, and for a face without vertical metrics a bearing of half the horizontal advance left of the horizontal bearing and of half the advance left over by the glyph height. - [Font writing lines](../../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics`, `sceFontWritingLineGetOrderingSpace` and `sceFontWritingLineClear` follow the PS4 11.00 `libSceFont.sprx` (firmware module, read from its disassembly and compared call for call on random sequences): runs are linked in visual order by embedding level, the space at each spaced join is summed for the line, steps walk the visual order from the left (form `0x12`) or from the right (form `0x11`) and the render metrics grow as steps are consumed. The PS5 library was not measured. Not from that module: form `0x10` is accepted and treated as left to right and a detail with id `0x0FD5` is accepted (the PS4 library rejects both); the memory argument of `sceFontCreateWritingLine` and the rest of the detail are not read (the PS4 library validates its library object there); an unknown writing form, another detail id, a null line pointer on creation, unknown and null handles (except in `sceFontWritingLineRefersRenderStep`, which returns null for a null line) and a null output of `sceFontWritingLineGetRenderMetrics` throw instead of returning the PS4 error codes, which are not confirmed for the PS5; `sceFontDestroyWritingLine` was not compared. Lines are allocated on the host, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. diff --git a/docs/dev/debt/libSceGameLiveStreaming.md b/docs/dev/debt/libSceGameLiveStreaming.md index b30576336c..24dfaa238f 100644 --- a/docs/dev/debt/libSceGameLiveStreaming.md +++ b/docs/dev/debt/libSceGameLiveStreaming.md @@ -2,4 +2,5 @@ ### Unknown function info +- [sceGameLiveStreamingEnableLiveStreaming](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws - [sceGameLiveStreamingGetCurrentStatus2](../../../core/libs/prx/libSceGameLiveStreaming/Export.cpp) (libSceGameLiveStreaming) - the status layout (user id, on-air flag, spectator, text and command message counts, broadcast resolution, 48 reserved bytes) is taken from shadPS4's `OrbisGameLiveStreamingStatus2`; the call reports that no stream is on air, with an invalid user id diff --git a/docs/dev/debt/libSceHmd2.md b/docs/dev/debt/libSceHmd2.md index 4ebd9bb64e..1dacc21ad2 100644 --- a/docs/dev/debt/libSceHmd2.md +++ b/docs/dev/debt/libSceHmd2.md @@ -11,7 +11,7 @@ - [sceHmd2SetVibration](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the second parameter is checked against 25 only while a headset is connected; its name and unit are unknown. The field of view member names are assumed: of the firmware's default tangents the first is the widest and the last two are equal - PPSA01317 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) exports that throw and the remaining [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp) exports. Names from the aerolib NID list - PPSA15878 imports declared without parameters, signatures unknown: the [libSceHmd2](../../../core/libs/prx/libSceHmd2/Export.cpp) reprojection and gaze exports, `sceVrTracker2Finalize`, `sceVrTracker2GetPlayAreaOrientedBoundingBox` and `sceVrTracker2LocateCoordinateSystem` in [libSceVrTracker2](../../../core/libs/prx/libSceVrTracker2/Export.cpp), and the `scePadVrController` trigger effect exports in [libScePad](../../../core/libs/prx/libScePad/Export.cpp). Names from the aerolib NID list -- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2GazeGetCalibrationData`, `sceHmd2GazeGetCalibrationDataMaxSize`, `sceHmd2ReprojectionClearUserEventEnd`, `sceHmd2ReprojectionClearUserEventStart`, `sceHmd2ReprojectionGetCurrentOutput`, `sceHmd2ReprojectionSetUserEventEnd`, `sceHmd2ReprojectionSetUserEventStart`, `sceHmd2GazeActivateGazeTracking`, `sceHmd2GazeGetDeviceStatus`, `sceHmd2GazeGetUserCalibrationStatus`, `sceHmd2GazeInsertRawDataWithHandle`, `sceHmd2GazeSetEnabledEye`, `sceHmd2GazeSetUserCalibrationData`, `sceHmd2GazeSetUserCalibrationPoint`, `sceHmd2GazeStartUserCalibration`, `sceHmd2GazeStopUserCalibration`, `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2GazeObjectSelectionGetSelectedObject`, `sceHmd2GazeObjectSelectionInitialize`, `sceHmd2GazeObjectSelectionQueryMemory`, `sceHmd2GazeObjectSelectionTerminate`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam`, `sceHmd2SetFieldOfView`, `sceHmd2SetIpdOffset` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws +- [libSceHmd2 gaze and reprojection exports](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `sceHmd2ReprojectionSeeThroughDraw`, `sceHmd2ReprojectionSeeThroughIC`, `sceHmd2ReprojectionVideoOutEq`, `sceHmd2ReprojectionWaitShader`, `sceHmd2SeeThroughCallbackGetPoseDataFunc`, `sceHmd2ReprojectionClear`, `sceHmd2ReprojectionSet`, `sceHmd2ReprojectionSetEventGpuEnd`, `sceHmd2ReprojectionSetEventGpuStart`, `sceHmd2RpeorjectionSetParam` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws - [sceHmd2InternalSetPortStatus](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalLedDetectorStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature - [sceHmd2InternalImageStartBySlot](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - unknown signature @@ -36,3 +36,50 @@ - [sceHmd2Initialize](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the firmware also fails with 0x8a72001f when `sceKernelGetAppInfo` fails and with 0x8a72000a for some applications, depending on app info fields at 0x2c, 0x4b and 0x4d whose meaning is unknown, and then calls `sceMbusInit`; none of this is modelled - [sceHmd2ReprojectionQueryDisplayBufferSizeAlign, sceHmd2ReprojectionQuerySeeThroughBufferSizeAlign, sceHmd2ReprojectionGetMirroringDisplayBufferSizeAlign and sceHmd2ReprojectionGetMirroringWorkMemorySizeAlign](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - each returns a `{uint64 size, uint64 alignment}` pair in RAX:RDX; the values were measured on a retail PS5 (firmware 13.60) with no PS VR2 headset from a homebrew payload that loaded `libSceHmd2.sprx` without initializing it, and stayed the same for every argument tried; `sceHmd2ReprojectionQueryBufferSizeAlign` measured there returns 0x100828 and 0x100 for every argument, including the 2, 0xAF and 1 that PPSA05666 passes. They are assumed not to depend on initialization or on a connected headset, and their parameters are unknown +- [Hmd2BeforeInitializeZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers 0 to these 45 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `vDgpdS2N9Rw` (sceHmd2InternalRegisterVibrationCallback), `Ri35gKTZpvE` (sceHmd2InternalUnregisterVibrationCallback), `9oyW8puiKeI`, `1HZNE6jhBnQ` (sceHmd2InternalPowerEventInitialize), `cdwAKBZLXjs`, `Yub1MF58jfY`, `JYXAuQFnPyw` (sceHmd2InternalDfuOpen), `VuPKqCu3wzw` (sceHmd2InternalDfuClose), `GSThagVyW5U` (sceHmd2InternalCrashReportOpen), `J4qE5N937MY` (sceHmd2InternalCrashReportClose), `9KnNj-dyASg`, `Qta2bkstbFk`, `5ZcObnDRBXE`, `AvPNn0qx4WQ`, `jp0apqNGgi8` (sceHmd2SetIpdOffset), `-kZT7X7OMg0` (sceHmd2InternalRelocalizerOpen), `1ewLtng-SVk` (sceHmd2InternalRelocalizerClose), `B0b8DeIUGes` (sceHmd2InternalTrackingOpen), `HclLg4Oj5yw` (sceHmd2InternalTrackingClose), `VbEvrQXsZyY` (sceHmd2InternalLedDetectorOpen), `bB6MKZi5UCQ` (sceHmd2InternalLedDetectorClose), `OXQ1EDf02XU` (sceHmd2InternalImageOpen), `IAxfMTvFm0M` (sceHmd2InternalImageClose), `qTJtAtaLeno` (sceHmd2InternalGenDataOpen), `j7Q16UX6l34` (sceHmd2InternalGenDataClose), `pmmNM06rLqU`, `zdQfyDK9Ods` (sceHmd2InternalSlamTraceOpen), `aPRwp9GnljM` (sceHmd2InternalSlamTraceClose), `MjY1gBSaed0` (sceHmd2InternalGazeOpen), `evAMjJYHqW4` (sceHmd2InternalGazeClose), `r93FLEZ3BBk` (sceHmd2InternalGazeStart), `IdYURdEdNFo` (sceHmd2InternalGazeStop), `CVcL2Cu2AEI` (sceHmd2GazeActivateGazeTracking), `NmKIfVxr2s0`, `KeDLkkp62Gg`, `+aadu6-ra+A`, `FgnTiwwGSzk`, `PWAUwOTHbbo` (sceHmd2InternalCreateSharedMemory), `WzaEApHnsP8` (sceHmd2InternalIsGameVr2d), `dXzQI8QVY7Y` (sceHmd2InternalIsMiniAppVr2d), `cCvFKGqRh1k` (sceHmd2InternalIsCommonDlgVr2d), `uj4-2VEAFPU` (sceHmd2InternalIsCommonDlgMiniAppVr2d), `sIaGStI-iLM` (sceHmd2InternalMapSharedMemory), `JzwpZOemDO8`, `IsC4R4bZFVU`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720002` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `sXaPEPY-fLA` (sceHmd2InternalOpen), `3F5inHhKzXg` (sceHmd2InternalGetDeviceInformation), `sJAiB-6p55U`, `E8hDydLjsSs`, `xEQkSb2WJ7Y`, `aaP4dnR4q-0`, `lEU0TeUoGwI`, `cFVyjHBpUUU`, `zROtb46jTj4`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720004` to these 15 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `kAMsjYHBbNQ` (sceHmd2InternalSetVolume), `5m73zmLpsQ0` (sceHmd2InternalResetViewPosition), `EPeH4xuQ2XQ` (sceHmd2InternalGetStatus), `TJ8lyRsjFY4` (sceHmd2InternalClearTrackerFts), `-XQVcLnyX68` (sceHmd2InternalSetSlamCommand), `giQwQoHhYYA` (sceHmd2InternalDfuStart), `zNc9IkgawMI`, `-tnG5UJPvAc` (sceHmd2GazeStartUserCalibration), `FPhFmG+jXuQ` (sceHmd2GazeStopUserCalibration), `CKlkvNngofQ`, `yL1CZaciFWE` (sceHmd2GazeSetUserCalibrationPoint), `qlzgyG3s0RY`, `kTIDrDLzMv0`, `9Gee0ef8a+U`, `ecZVjQdwmBI` (sceHmd2GazeSetEnabledEye). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterNull](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` to these 16 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `MNtiJTDOPAU`, `RRy9WmqZfgU` (sceHmd2InternalDecryptData), `uobR4m9lgwc` (sceHmd2InternalGetMirroringMode), `-XPk5KWBYdo` (sceHmd2InternalRelocalizerSetResultData), `J44idh9izZM`, `Ps0wAvYaa1E`, `kLnMR8oQ1MY`, `kMz4rbBxvjo`, `DIU1sFRRaiE` (sceHmd2GazeGetCalibrationData), `ZQvU4vDzZGQ` (sceHmd2GazeSetUserCalibrationData), `TJpyGGP9YEY`, `Dih+Odk+RC0`, `oaqdSJw+HVY`, `m35gtHuanXU`, `CTM7QPLJF9Q` (sceHmd2InternalGetFlipInfo), `+rExHeC9lP0` (sceHmd2GazeObjectSelectionInitialize). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720009` to these 9 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `bBh5bCp9coo` (sceHmd2InternalSetPositionResetIcon), `G3PNDE2VjfY` (sceHmd2InternalSetPowerOff), `nScCT0OPU0o`, `6Q0Ng7OTN9o`, `-fJKh4SnFR0` (sceHmd2InternalSetBudgetMode), `jUA2s-l0npY` (sceHmd2InternalSetForceIcon), `+9XhyAIJVj4`, `eBGK-o6E4RQ` (sceHmd2InternalBindDeviceWithUserId), `MTuFeL9184E`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeReprojectionNotInitialized](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72000b` to these 10 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `d3SEIYTP6wc`, `EVECbTvBezo`, `ciusPdJKjSU`, `DIwqTkcZiJo` (sceHmd2ReprojectionSetUserEventStart), `64ERZWVFBTE` (sceHmd2ReprojectionSetUserEventEnd), `z0+0gXUJWs8` (sceHmd2ReprojectionClearUserEventStart), `rjAd4nCY4DM` (sceHmd2ReprojectionClearUserEventEnd), `Acyg4vTdGm0` (sceHmd2ReprojectionGetCurrentOutput), `aGOWaEeg04I`, `9up+EPhw1No`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a72002c` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `V1kLQmP-17U` (sceHmd2GazeObjectSelectionTerminate). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721002` to these 5 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `2LVD1EC5SYE`, `Bf87AL6zWxU`, `XKt7w1MODFY`, `Y5s22M-qUDA`, `JJKY-BtQJLg`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a721003` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `9kj9vw8UShQ`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers -1 to these 12 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `EAkv8Mqt33E` (sceHmd2InternalRelocalizerStart), `Rv7bkmKvmz0` (sceHmd2InternalRelocalizerStop), `1oPZizn0SNA` (sceHmd2InternalTrackingStart), `TwCpE-nNrlo` (sceHmd2InternalTrackingStop), `jkqAeQfdtxw` (sceHmd2InternalLedDetectorStart), `+l0RQph81Z8` (sceHmd2InternalLedDetectorStop), `qsmAtGw1Ncc` (sceHmd2InternalImageStart), `uKhwYoMsIsA` (sceHmd2InternalImageStop), `WNdaigr94dI` (sceHmd2InternalGenDataStart), `P6S5ZVWf-H0` (sceHmd2InternalGenDataStop), `sun6NWE1sdg` (sceHmd2InternalSlamTraceStart), `--TFsJYXo04` (sceHmd2InternalSlamTraceStop). Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue0400](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x400` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `BWqcwjCozzI`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2BeforeInitializeValue20000](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x20000` to these 1 exports both with every argument null and with every argument pointing at a zeroed 16 KiB buffer, writing nothing: `PRz7TdBGUI0`. Measured from a payload process on firmware 13.60, so a game process before initialization is assumed to answer the same; the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 pre-initialization sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - the names in the entries above are the aerolib names of the NIDs where one exists; the error names `Error0004`, `Error002C`, `Error1002` and `Error1003` are placeholders for codes whose meaning is unknown, and `ReprojectionNotInitialized` is ours because every export answering `0x8a72000b` is a reprojection one. Exports that answered differently to null and to zeroed arguments, wrote output, returned a pointer-like value, answered `0x8a72000a` (which `sceHmd2Initialize` also returns for some applications) or crashed the probe are left as they were +- [Hmd2CheckFirstThenError0004](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720004` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 20 exports: `9WeZr4kYfzk` (sceHmd2InternalGetFwVersion), `N+5h1hCOFAg` (sceHmd2InternalGetRequiredPUPVersion), `w2vEs2y5ZmE` (sceHmd2InternalGetOpticalParam), `L5gat1pe0y0` (sceHmd2InternalSetCameraSeeThroughParam), `4GnLHR9NJ1w`, `AHk7nGSzef0` (sceHmd2InternalGetTrackerFtsData), `ncjX9d3mc0M`, `l63QYAUuogc` (sceHmd2InternalGetSlamCommand), `mrcBySCdXSo` (sceHmd2InternalDfuGetStatus), `ieBra+td8sU` (sceHmd2InternalDfuGetUpdateStatus), `s+rzBnewgGs` (sceHmd2InternalDfuCheckAfterPvt), `Nfcm0GBdzZI` (sceHmd2InternalRelocalizerGetVersion), `Tc9rasWfrm4` (sceHmd2InternalSetHomeCalibration), `ckTbKTX4KZg`, `D7576PUbUjk` (sceHmd2InternalGazeReadStatus), `tTJ04SrBT8U`, `sDP7hZ-P6OI` (sceHmd2GazeGetDeviceStatus), `J+eVNbN9ndY` (sceHmd2GazeGetUserCalibrationStatus), `kozvHBNyxf0`, `638Tmg5y6U4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `8ETtFtp5zqY` (sceHmd2InternalGetVrMode), `CWPkBm3EXQA`, `R62liQQIAaA` (sceHmd2InternalGetFakeLensSeparationDistance), `-bcWLHsdzo0`, `JngJzgNbu8U` (sceHmd2SetFieldOfView), `xNBSz8xrGdk`, `yUVPfN-jRIU`, `BKPR3RZ5aO4`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 8 exports: `IG4y+drWAWw` (sceHmd2InternalRelocalizerReadData), `rhYUml9vppE` (sceHmd2InternalRelocalizerReadDataAsync), `6574xHS+jZk` (sceHmd2InternalRelocalizerReadData2), `mXzaY6xVoxg` (sceHmd2InternalTrackingReadData), `6af-HIZQQn4` (sceHmd2InternalTrackingReadData2), `6qWtdTHQsTk`, `pUm8t8xWhFg`, `A0XKI-WjxK8` (sceHmd2InternalImageReadData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and -1 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 5 exports: `A9s2j8OioPQ`, `YIgtmrL+HLY`, `Ng8zEkGxDNs`, `EtslTnqO1xU` (sceHmd2InternalImageStart2), `hfrnRTryKQQ` (sceHmd2InternalImageSetConfig). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `PPX68fNToFM`, `LxPIfjHaY4c` (sceHmd2GazeObjectSelectionQueryMemory), `w8gBTTdg+po` (sceHmd2GazeObjectSelectionGetSelectedObject). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a72002c` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 3 exports: `tWJPX2ycRMM`, `5UMB9PJA70o`, `xQs62qCk1EI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `MMc+bSlqkzk` (sceHmd2InternalSetVirtualDisplaySize), `NpZHgvSYgEA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckAllSixThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 and argument 2 and argument 3 and argument 4 and argument 5 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `z-serf9vsb0` (sceHmd2InternalPushVr2dData), `IRMUFu+uG5s`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenParameterInvalid](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720009` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 2 exports: `72U64E+0z6U`, `LCQzi5ILGrY` (sceHmd2GazeGetCalibrationDataMaxSize). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError800d0203](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x800d0203` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `Im0qL0j9Ubg`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ng6eI8uA0h4` (sceHmd2InternalImageGetMemoryLayout). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstThenError1010](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 is null and `0x8a721010` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mPPGgjyJrCI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `ZupCJmV5BDI` (sceHmd2InternalImageGetFrameData). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstTwoThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 1 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `mAQZwOX1IHU` (sceHmd2InternalImageGetAverageLuminance). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and 0 when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `oacDkQkinLo`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckFirstAndThirdThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 0 and argument 2 is null and `0x8a721003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `zn7PHGzzEyI`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a720003` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `wEHILc7TP04` (sceHmd2GazeInsertRawDataWithHandle). Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console answers `0x8a720008` when argument 1 is null and `0x8a721002` when every argument points at a zeroed 16 KiB buffer, writing nothing, for these 1 exports: `W-ZIlUXcrNA`. Each argument was nulled on its own with the others present (probe `hmd2n`, payload process, firmware 13.60); what the present arguments mean, and the behaviour after initialization, are not measured, so the handler throws once `sceHmd2Initialize` has succeeded +- [libSceHmd2 null-argument sweep](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - `Error1010` and `Error800d0203` are placeholder names. Left as they were: the exports that write output (`omimgG5itSs`, `FBUEhH9tQ1A`, `6PeHcAaKYg8`, `yur+xPdWPAg`, `RnieDDp5ge4`, `jM3NEJA11yw`, `b0H-n-tUk0U`) and those whose answer with a single null argument is neither `0x8a720008` nor their all-present answer, which take a handle or slot number rather than a pointer first (`MXhloJ6Iw3w`, `19wrIIiqFgM`, `Z389vAm47Ys`, `cDcrvEG8j8k`, `tuqJO6O6cb4`, `fd4fu1cziaI`, `UxMg7iL4nRs`, `K1GELwlaaGc`, `RFn39VCHjGY`, `FRoSjceDv10`, `wiv8+7ogrq0`, `wQ44gBwBw44`) +- [Hmd2WriteStateTwo](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the five bytes `02 00 00 00 00` at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `omimgG5itSs`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSize0x2000Align0x100](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 64-bit values `0x2000` through its first argument and `0x100` through its second and returns 0; a null first or second argument returns `0x8a720008`: `6PeHcAaKYg8`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteFourConstants](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 16 bytes `e1 f0 74 3c d8 f0 74 3c 1f f1 74 3c d8 f0 74 3c` (four floats near 0.01495) at the start of its first argument and returns 0; a null first argument returns `0x8a720008`: `yur+xPdWPAg`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteSixHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 24 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 3f 00 00 00 3f` (floats 0, 0.5, 0.5, 0, 0.5, 0.5) at the start of its second argument and returns `0x8a720003` for every first argument tried (0, a pointer); a null second argument returns `0x8a720008`: `RnieDDp5ge4`, `jM3NEJA11yw`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2WriteTwelveHalvesThenInvalidHandle](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console writes the 48 bytes `00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3f 00 00 00 3f 00 00 00 00 00 00 00 00 00 00 00 00` (floats 0, 0.5, 0.5, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0) at the start of its second argument and returns `0x8a720003` for every first argument tried; a null second argument returns `0x8a720008`: `b0H-n-tUk0U`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo3CheckSecondThirdThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 to 3 and `0x8a720009` for 4, 5, 8, 16, 255 and -1; a null second or third argument returns `0x8a720008` even with an invalid first argument. Its fourth to sixth arguments answered `0x8a720009` when null with an invalid first argument, which does not separate them from the first-argument check, so they are not looked at: `MXhloJ6Iw3w`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; its other arguments were only nulled with an invalid first argument, which answered `0x8a720009` either way, so they are not looked at: `19wrIIiqFgM`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1CheckSecondThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `Z389vAm47Ys`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo1ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 or 1 and `0x8a720009` for 2 to 5, 8, 16, 255 and -1; the other arguments are not looked at, for the reason given above: `cDcrvEG8j8k`, `tuqJO6O6cb4` (sceHmd2InternalLedDetectorSetIrCameraConfigBySlot), `fd4fu1cziaI` (sceHmd2InternalLedDetectorSetPriorityConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenMinusOne](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns -1 for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `UxMg7iL4nRs`, `K1GELwlaaGc` (sceHmd2InternalImageSetConfigBySlot). Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1002](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721002` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `RFn39VCHjGY`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2ThenError1003](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a721003` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; the other arguments are not looked at: `FRoSjceDv10`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2SlotUpTo2CheckSecondThenError002C](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns `0x8a72002c` for a first argument of 0 to 2 and `0x8a720009` for 3 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with an invalid first argument: `wQ44gBwBw44`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [Hmd2IndexZeroCheckSecondThenZero](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - before `sceHmd2Initialize` the console returns 0 for a first argument of 0 and `0x8a72100f` for 1 to 5, 8, 16, 255 and -1; a null second argument returns `0x8a720008` even with a nonzero first argument. Its third to sixth arguments answered `0x8a72100f` when null with a nonzero first argument, which does not separate them from the first-argument check, so they are not looked at: `wiv8+7ogrq0`. Measured from a payload process on firmware 13.60 (probes `hmd2n` and `hmd2x`); the meaning of the arguments and the behaviour after initialization are not measured, and the handler throws once `sceHmd2Initialize` has succeeded +- [FBUEhH9tQ1A](../../../core/libs/prx/libSceHmd2/Export.cpp) (libSceHmd2) - left unimplemented: before initialization it writes the 32-bit value 9 through its second argument and returns 0 when its first argument points at zeroed memory, but returns `0x8a720001` without writing when that memory holds 0xAA bytes, and its fourth to sixth arguments answer `0x8a720001` when null, so the first argument is an in-out structure whose fields are not known diff --git a/docs/dev/debt/libSceJpegDec.md b/docs/dev/debt/libSceJpegDec.md index 833ad70e91..e5b7252fa0 100644 --- a/docs/dev/debt/libSceJpegDec.md +++ b/docs/dev/debt/libSceJpegDec.md @@ -7,4 +7,4 @@ ### Functional -- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified +- [libSceJpegDec](../../../core/libs/prx/libSceJpegDec/Export.cpp) (libSceJpegDec) - decode modes other than 0 and down scales other than 1, 2, 4 and 8 throw, as does a down scale of 2, 4 or 8 on a CMYK or YCCK image, which libjpeg-turbo cannot decode to RGB. The down scale factors are the ones [SharpProspero `JpegDec.cs`](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Image/JpegDec.cs#L57-L58) documents; the output size is the source size divided by the factor and rounded up, and the pixels come from libjpeg-turbo's scaled inverse DCT. The console's rounding, pixel values and the packed decode result for a down-scaled image (the output size here) are unverified. In `sceJpegDecDecode`, rejecting an output `image_mem_addr` not aligned to 4 bytes and rejecting an `alpha_value` above 0xFF follow libScePngDec validation and are not verified from a libSceJpegDec binary disassembly diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index a091bd2810..d54f1fc62c 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -36,7 +36,7 @@ - [sceNetFreeAllRouteInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetArpInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetAppMemInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature -- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature +- [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetIfList](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceNetGetIfListOnce](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -184,14 +184,14 @@ - [sceNetAccept](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) inherits the listener's non-blocking mode through the existing `SO_NBIO` setter, as the libkernel socket path does and FreeBSD's [`sonewconn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/uipc_socket.c#L571) copies the socket state. The mode is sampled when accept starts; changes between connection creation and accept are not modelled. Not measured on a console. - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had -- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. A stream receive that times out still reports `ETIMEDOUT` rather than FreeBSD's `EAGAIN`; a datagram receive reports `EAGAIN`. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) set `TCP_NODELAY` (`IPPROTO_TCP`, 1) on the host socket of a stream socket and read it back as `TF_NODELAY` (4) or 0, as FreeBSD 11.4's [`tcp_default_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1626-L1650) does. Every other `IPPROTO_TCP` option (`TCP_MAXSEG`, `TCP_NOPUSH` and `TCP_KEEPIDLE` among them, which FreeBSD implements) fails with `ENOPROTOOPT`, and the `IPPROTO_TCP` level on a socket that is not a stream socket fails with `EINVAL`, as [`ip_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/ip_output.c#L943-L944) answers it. A set on a connection that FreeBSD's [`tcp_ctloutput`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/netinet/tcp_usrreq.c#L1498-L1502) refuses with `ECONNRESET` (refused, reset or in `TIME_WAIT`) succeeds here because the host accepts it. A null value with a nonzero length fails with `EINVAL`, where FreeBSD's `kern_setsockopt` returns `EFAULT`, and a get into less than an `int` fails with `EINVAL`, where `sooptcopyout` truncates. None of this was measured on a console. - [sceNetGetSockInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reports 0 for the name, pid, policy, priority, queue lengths, virtual ports, state, bandwidth, drops and waits, sets no status flag except `SELF` (0x1) and `NONBLOCK` (0x200000), and reports the host's buffer sizes where none was set. An IPv6 socket fails with `EAFNOSUPPORT` and is not counted or listed; shadPS4 lists it with zero addresses. `flags` bits other than 0x40 fail with `EINVAL`, where shadPS4 ignores `flags`; 0x40 adds sockets in TIME_WAIT ([SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs)), but closed sockets are not kept. Otherwise the `s` below 0 count and list, the return value and `EINVAL` for a null `info` or an `n` below 1 follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/06e813ff8c8e2a6d81b02cabfcc9543c3debf78e/src/core/libraries/net/net.cpp#L1498-L1522). Not measured on a console. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported -- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) in `sceNetSend`, `sceNetSendto`, `sceNetRecv`, `sceNetRecvfrom`, `sceNetRecvmsg` and `sceNetSendmsg` 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`); the value `0x80` is FreeBSD's (`sys/sys/socket.h`) and [shadPS4's](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/net/net_types.h#L104). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `sceNetRecv` blocked, and a `sceNetSend` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count +- [MSG_DONTWAIT](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - console ordering between competing readers and writers is unmeasured. The per-call behavior follows FreeBSD 11.4's `soreceive_generic` and `sosend_generic` (`sys/kern/uipc_socket.c`); `0x80` is FreeBSD's flag value (`sys/sys/socket.h`). On Windows native sockets stay non-blocking. `MSG_DONTWAIT` and guest `SO_NBIO` return one host transfer's result. Blocking stream sends queue the whole buffer with one deadline and return progress after a timeout or host error. SSL uses this path through the native owner, retaining the socket, its mode and timeouts. Readiness waits poll at most every 10 ms for local close, so idle blocked operations wake periodically. - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index 99aa976623..87f5d96376 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -6,7 +6,7 @@ ### Unknown function info -- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587, which fails to load without it, and not called before its loading screen; declared without parameters as a throwing stub, signature unknown. Name from the aerolib NID list +- [sceNetCtlGetInfoV6](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl, NID `Jy1EO5GdlcM`) - imported by PPSA08587. `(int code, SceNetCtlInfoV6* info)` with codes 1-5 (IP address, link-local address, default route, primary and secondary DNS) comes only from [SharpProspero](https://github.com/SvenGDK/SharpProspero), whose source is unknown; shadPS4 and fpPS4 only stub it. A null `info` answers `INVALID_ADDR`, a code outside 1-5 `INVALID_CODE`, and a host without an IPv6 address `NOT_CONNECTED`, as `sceNetCtlGetStateV6` reports it. The layout of each answer is unverified, so every code throws on a host with an IPv6 address - [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 DHCP host name (12), the PPPoE user name (13), the HTTP proxy server (20) and the Wi-Fi BSSID and SSID (5, 6) are empty strings, as that configuration sets none of them; the remaining Wi-Fi codes (7-10) and the HTTP proxy port (21) report 0; this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/network.cpp) and is not measured on hardware - [sceNetCtlRegisterCallbackForNpToolkit](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature - [sceNetCtlRegisterCallbackIpcInt](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - unknown signature diff --git a/docs/dev/debt/libSceNpTus.md b/docs/dev/debt/libSceNpTus.md index 3ccc29383a..b48d262dcb 100644 --- a/docs/dev/debt/libSceNpTus.md +++ b/docs/dev/debt/libSceNpTus.md @@ -3,6 +3,129 @@ ### Unknown function info - [SceNpTusVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the layout of the output array of `sceNpTusGetMultiSlotVariableAAsync` and of `outVariable` in `sceNpTusAddAndGetVariableAAsync` is not verified, so the output is never written and `variableArraySize` / `outVariableSize` are not checked. The signatures and the community error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/tree/main/src/core/libraries/np/np_tus), which fpPS4 agrees with for the context and request calls; no console measurement. +- [sceNpTusSetThreadParam](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssCreateNpTitleCtxA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusCreateTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetTimeout](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusChangeModeForOtherSaveDataOwners](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusWaitAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariable](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotData](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatus](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetSmallStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorage](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetStorageAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTssGetDataAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusAddAndGetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiSlotDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetDataForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetMultiUserDataStatusForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusGetFriendsDataStatusForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusSetDataAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteMultiSlotDataAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableA](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableAVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSave](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUser](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusTryAndSetVariableForCrossSaveVUserAsync](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - unknown signature +- [sceNpTusDeleteNpTitleCtx](../../../core/libs/prx/libSceNpTus/Export.cpp) (libSceNpTus) - the PS5 signature, error codes and child-request lifetime have not been measured on a console. Deletion invalidates the context and all its requests, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e7626e3c7de8d0f0fda6a91f620b42f6c762/src/core/libraries/np/np_tus/np_tus.cpp#L158-L180). ### Functional diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index 7b1ac780bb..c52607d8ee 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -160,3 +160,7 @@ - [scePadControllerUpdateResetController](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateGetReconnectStatus](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature - [scePadControllerUpdateTerm](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown signature + +### Functional + +- [Touchpad resolution](../../../core/libs/prx/libScePad/include/PadState.hpp) (libScePad) - the library reports a DualSense to every title, so the controller information reports its touchpad, 1920 by 1080 at 44.86 pixels per cm, and the host controller's fingers and the keyboard touch contacts are placed in that range. [duaLib](https://github.com/WujekFoliarz/duaLib/blob/0b39f92261e2ff0e77eb0dac00943be6970e0dbf/src/source/duaLib.cpp#L847-L849) reports these values for a DualSense and 1920 by 943 at 44 for a DualShock 4, and the Linux kernel's [hid-playstation](https://github.com/torvalds/linux/blob/cd4f494ba535e848e2d299707d363de84e61a518/drivers/hid/hid-playstation.c#L177-L178) holds 1920 by 1080 against 1920 by 942 for the DualShock 4. A DualShock 4 on the host reports through the DualSense range too, so its last touchpad rows are never reached. The console's values are not measured diff --git a/docs/dev/debt/libSceRazorCpu.md b/docs/dev/debt/libSceRazorCpu.md index 30115eb282..3a4d1b15d4 100644 --- a/docs/dev/debt/libSceRazorCpu.md +++ b/docs/dev/debt/libSceRazorCpu.md @@ -4,6 +4,8 @@ - [sceRazorCpuPushMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature (label, color, flags) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), the same as sceRazorCpuPushMarkerStatic's except that the label is copied; it does nothing without a capture and returns 0 - [sceRazorCpuSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - the `int(void)` signature and its meaning, an unnamed frame-boundary event, come from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L119-L122). Without a capture there is no event to record, so it does nothing and returns 0, like the other `sceRazorCpu*` markers here and the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/libraries/razor_cpu/razor_cpu.cpp#L143-L146) stub. PRAGMATA (PPSA02530) continues on 0; the console's result with no capture attached is unverified +- [sceRazorCpuDisableFiberUserMarkers, sceRazorCpuBeginLogicalFileAccess and sceRazorCpuEndLogicalFileAccess](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures and meanings from SharpProspero ([fiber markers](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L73-L79), [logical file access](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L189-L209)): the first makes later markers carry the thread instead of the fiber, the other two attribute the thread's file reads and writes to a named logical file. Both only change what a capture records, so without one they do nothing, and the two file-access calls return 0 for any arguments. Whether the console checks the operation (0 read, 1 write) or an unmatched end without a capture is unverified +- [sceRazorCpuInitDataTags, sceRazorCpuShutdownDataTags, sceRazorCpuTagBuffer, sceRazorCpuTagArray, sceRazorCpuResizeTaggedBuffer and sceRazorCpuUnTagBuffer](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L130-L187), which documents them as the data sampler's buffer-tag table and the tags in it. Without a capture there is no data sampler, so they neither read the caller's storage nor keep the tags, and return 0 for any arguments; the console's errors for a tag before `sceRazorCpuInitDataTags`, an unknown buffer or a full table are unverified ### Unknown function info @@ -11,3 +13,4 @@ - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture - [sceRazorCpuPlotValue, sceRazorCpuWriteBookmark](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures (series, float value) and (label, optional description) from [SharpProspero](https://github.com/SvenGDK/SharpProspero), matching PPSA06320's calls; both do nothing without a capture and return 0 - [sceRazorCpuNamedSync](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - NID `XCuZoBSVFG8` is named by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/razor_cpu/razor_cpu.cpp); the NID and a stub returning 0 come from there, but its stub declares no arguments; the `const char* label` parameter (NUL-terminated, up to 16384 bytes) comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero), which documents it as the frame-boundary label. Without a capture it does nothing and returns 0; the label is not read, and the capture behavior and the real return values are unverified +- [sceRazorCpuGetDataTagStorageSize](../../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - throws: [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Debug/RazorCpu.cs#L142-L144) documents it as the byte size a tag table needs for a count of entries, but not the size of an entry diff --git a/docs/dev/debt/libSceUlt.md b/docs/dev/debt/libSceUlt.md index bbb48c53ce..4c2177a5ee 100644 --- a/docs/dev/debt/libSceUlt.md +++ b/docs/dev/debt/libSceUlt.md @@ -8,7 +8,8 @@ - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. - [sceUltQueueCreate, sceUltQueueDataResourcePoolCreate](../../../core/libs/prx/libSceUlt/Export.cpp) - a non-null option parameter is accepted and ignored, as the other Ult create calls already do. [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/00d087b2323bf377fd39b7163a256977383cb232/include/orbis/Ult.h) declares `sceUltQueueOptParamInitialize` and `sceUltQueueDataResourcePoolOptParamInitialize` without their structures; the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.h#L25-L78) defines both as a 16-byte reserved header followed by reserved fields, 128 bytes, with no settings, and [fpPS4](https://github.com/red-prig/fpPS4/blob/5ffbdbbd76b4da6e4b8721743ea491f494b28913/src/ps4_libsceult.pas#L142-L172) keeps them as 128 opaque bytes. That the PS5 structures carry no settings either is assumed from these, not verified - [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured -- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltMutexTryLock, sceUltReaderWriterLockDestroy, sceUltReaderWriterLockLockRead, sceUltReaderWriterLockLockWrite, sceUltReaderWriterLockTryLockRead, sceUltReaderWriterLockTryLockWrite, sceUltReaderWriterLockUnlockRead, sceUltReaderWriterLockUnlockWrite, sceUltUlthreadExit, sceUltUlthreadGetSelf, sceUltUlthreadYield, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize, sceUltQueueDataResourcePoolOptParamInitialize, sceUltQueueOptParamInitialize, sceUltReaderWriterLockCreate, sceUltReaderWriterLockOptParamInitialize, sceUltSemaphoreOptParamInitialize, sceUltUlthreadOptParamInitialize, sceUltWaitingQueueResourcePoolOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceUltConditionVariableSignalAll, sceUltGetConditionVariableInfo, sceUltGetMutexInfo, sceUltGetQueueDataResourcePoolInfo, sceUltGetQueueInfo, sceUltGetReaderWriterLockInfo, sceUltGetSemaphoreInfo, sceUltGetUlthreadInfo, sceUltGetUlthreadRuntimeInfo, sceUltGetWaitingQueueResourcePoolInfo, sceUltAbstractThreadFactoryCreateThread, sceUltAbstractThreadFactoryGetNumMaxThreads, sceUltAbstractThreadFactoryJoinThread, sceUltKernelThreadFactoryFinalize, sceUltKernelThreadFactoryGetFactoryPointer, sceUltKernelThreadFactoryInitialize, sceUltModuleFinalize, sceUltModuleInitialize, sceUltUlthreadFactoryFinalize, sceUltUlthreadFactoryGetFactoryPointer, sceUltUlthreadFactoryGetWorkAreaSize, sceUltUlthreadFactoryInitialize, sceUltConditionVariableCreate, sceUltConditionVariableOptParamInitialize](../../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) - throwing stubs declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware +- [sceUltMutexTryLock, the reader-writer lock and sceUltUlthreadGetSelf, sceUltUlthreadYield and sceUltUlthreadExit](../../../core/libs/prx/libSceUlt/src/ReaderWriterLock.cpp) (libSceUlt) - the signatures follow the PS Vita libult in [RPCS3](https://github.com/RPCS3/rpcs3/blob/13aa88c3c4d66ea8799e987f7d762d90943ab17b/rpcs3/Emu/PSP2/Modules/sceUlt.cpp) (a 256-byte lock and 128-byte option structures), not a PS5 binary. The rules are assumed: readers may take the lock while a writer waits, a thread that holds the write lock gets `ULT_ERROR_STATE` for any further lock instead of waiting on itself, a reader asking for the write lock waits on its own read lock, and the try forms return `ULT_ERROR_BUSY`. Outside an ulthread, `sceUltUlthreadGetSelf`, `sceUltUlthreadYield` and `sceUltUlthreadExit` return `ULT_ERROR_STATE`; inside one they act on the guest thread it runs on, since ulthreads are not scheduled cooperatively here ### Functional - [libSceUlt queues](../../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. diff --git a/docs/dev/debt/libSceVoice.md b/docs/dev/debt/libSceVoice.md index c7ee43b953..5c4f80575c 100644 --- a/docs/dev/debt/libSceVoice.md +++ b/docs/dev/debt/libSceVoice.md @@ -11,3 +11,15 @@ - [sceVoiceEnableChat, sceVoiceDisableChat](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) take no arguments and return 0, following [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Audio/AudioOut2.cs#L460-L464) and the `sceAudioOut2EnableChat` model. The host mixes no chat audio, so the calls only record whether chat audio is enabled and fail when the library is not initialized. Console error codes and chat session requirements are unverified. PPSA23566 imports them - [sceVoiceInit, sceVoiceStart, sceVoiceCreatePort, sceVoiceGetPortInfo](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - a second `sceVoiceInit` returns 0x804E0802, and Start, CreatePort and GetPortInfo return 0x804E0801 before checking any argument while the library is not initialized, as [prosper](https://github.com/mattias800/prosper/blob/ed42e499888d5a86792c3e1b2df3aebd8ca1a681/prosper/src/hle/audio/hle_audio.cpp#L4697-L4713) reads them from the shipped module. The other port and lifecycle calls still throw when the library is not initialized, since prosper gives no module address for them. Whether `sceVoiceInit` checks a null param before its state is unknown, so a null param still throws first, and that `sceVoiceEnd` returns the library to the not-initialized state is assumed. Closes when: the other entry points are read from the module or measured on a PS5 - [sceVoicePausePort, sceVoiceResumePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - not measured on a PS5: the `(uint32_t portId)` signature follows [VitaSDK](https://github.com/vitasdk/vita-headers/blob/ae5808f3105c42fa3373ba4ab64ab54e2e3ca848/include/psp2/voice.h#L516-L538) and the PS3 calls in [RPCS3](https://github.com/RPCS3/rpcs3/blob/be69c054eeac57e2e671697a463473d37fe5a7b2/rpcs3/Emu/Cell/Modules/cellVoice.cpp#L443-L529), and the console's error codes and its result for pausing a paused port or resuming a running one are unknown (0 here, as in RPCS3). Both only check that the library is initialized and the port exists: VitaSDK documents that a paused port keeps its data and reported state, and nothing here processes port data. PPSA24191 imports them +- [sceVoiceInitHQ](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceGetMemorySize](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceDeviceInCustomerData](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceQAEnableRecording](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADPVTAdjustment](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceVADSetVersion](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetMuteFlagAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceSetBitRate](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceUpdatePort](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoicePausePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceVoiceResumePortAll](../../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature diff --git a/docs/dev/debt/libSceVoiceQoS.md b/docs/dev/debt/libSceVoiceQoS.md index 6a2b748ea6..fa6732187f 100644 --- a/docs/dev/debt/libSceVoiceQoS.md +++ b/docs/dev/debt/libSceVoiceQoS.md @@ -1,9 +1,14 @@ # libSceVoiceQoS technical debt +### Silent stubs + +- [sceVoiceQoSReadPacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 and writes size 0 (indicating no packet is available). Aliens: Fireteam Elite (PPSA03393) calls it on its startup path (`0x76D14C`-`0x76D16E`) and stops with `not implemented` if it throws +- [sceVoiceQoSWritePacket](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - network voice packet streaming is not implemented: it returns 0 leaving `*size` untouched (treating all input bytes as accepted). Aliens: Fireteam Elite (PPSA03393) sets `*size` to the packet size before `WritePacket` (`0x76CEB9`-`0x76CEC4`), compares `*size` against that input size upon return (`0x76CF07`-`0x76CF10`) to verify all bytes were accepted, and stops with `not implemented` if it throws + ### Unknown function info - [sceVoiceQoSSetConnectionAttribute](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`) -- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); no public reference exists for the other seven signatures (`DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`), which are assumed from function naming and call patterns and not confirmed on a PS5 title. Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown; other endpoint and packet calls remain throwing stubs. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all seven implemented calls on its first load path, starting with `sceVoiceQoSCreateLocalEndpoint` when it registers the local talker; it stops with `not implemented` when any of them throws +- [sceVoiceQoSInit](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `0x804E0905` for a second initialization comes from [prosper](https://github.com/mattias800/prosper)'s reading of the module and was not measured: on a PS5 a valid call blocks inside a homebrew payload, so it never returns there. PPSA26344's two call sites (`0xE307BF`, `0x151C45D`) pass an allocated pointer, a 0x40000-byte block and app type 0x10000000; the app type and block are not used. `sceVoiceQoSCreateLocalEndpoint` takes four arguments (`int* id, userId, deviceInId, deviceOutId`) as declared in fpPS4 (`src/ps4_libscevoice.pas`, `ps4_sceVoiceQoSCreateLocalEndpoint`); signatures for `DeleteLocalEndpoint(id)`, `CreateRemoteEndpoint(int* id)`, `DeleteRemoteEndpoint(id)`, `Connect(int* id, local, remote)`, `Disconnect(connection)`, `SetLocalEndpointAttribute(id, attribute, value, size)`, `SetRemoteEndpointAttribute(id, attribute, value, size)`, and `GetLocalEndpointAttribute(id, attribute, value, size)` were verified against call sites in Aliens: Fireteam Elite (PPSA03393, e.g. `0x76D132`, `0x76D16E`, `0x76CECB`). Endpoints and connections are tracked by allocated id; duplicate connections and deleting an endpoint while referenced in a connection are permitted as the console is not known to reject them; endpoint attributes are stored as given (including empty or non-positive sizes), as their ids and defaults are unknown. The not-initialized error 0x804E0901 is assumed from the 0x804E09xx numbering, and unknown ids return `ARGUMENT_INVALID`. Aliens: Fireteam Elite (PPSA03393) uses all implemented calls on its load path; it stops with `not implemented` when any unimplemented call throws - [libSceVoiceQoS firmware exports](../../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - `sceVoiceQoSGetRemoteEndpointAttribute`, `sceVoiceQoSGetConnectionAttribute` and `sceVoiceQoSGetStatus` follow public PS4 SDK prototypes in [Orbital](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json#L2436-L2441) and remain throwing stubs as endpoint and connection attribute storage is unverified on hardware. `sceVoiceQoSEnableChat`, `sceVoiceQoSDisableChat`, `sceVoiceQoSDebugGetStatus`, `sceVoiceQoSInitHQ`, `sceVoiceQoSSetMode`, and `sceVoiceQoSTerminate` are declared without parameters from the firmware export database (aerolib.csv); their guest signatures are unverified on hardware and every call throws ### Functional diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 931d7d1746..037f620011 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -4,8 +4,8 @@ - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw -- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw +- [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. @@ -20,7 +20,7 @@ - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error -- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks. `Memalign` is inferred as `(handle, alignment, size, owner)`, delegates to `sceLibcMspaceMemalign`, and is unverified on console. `Create` has an extra options pointer with a partly observed layout (version, type 1 or 2 at +2, field at +0x10); non-null options throw because the field's meaning is unknown. Allocation owners are ignored because block footers are not modelled. On PS5 firmware 13.60, heap-owner calls worked; wrong owners in `Reallocalign` or in-place `Realloc` terminate the process, and `MallocStats` returns 1 if non-heap-owned blocks exist. These owner results are not reproduced. `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` remain unimplemented. +- [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block @@ -54,7 +54,10 @@ ### Functional +- [_FSinh and _FCosh](../../../core/libs/prx/libc/src/Math.cpp) (libc) finite results are not verified bit for bit against the console: the scaled product uses the host's binary64 hyperbolic function before rounding to binary32, rather than the float rational and `_FExp` sequence in PRAGMATA's bundled `libc.prx`. NaN preference and payloads, signed zeros, infinite operands and the range-error thresholds follow that binary's instructions; the system `libSceLibcInternal` implementation and nondefault flush-to-zero modes are unverified. + - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. +- [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. @@ -87,10 +90,11 @@ - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. -- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps the range up to 0x70_0000_0000 unmappable for every process (max protection none). It starts at 0x10_0000_0000 on macOS 26.5 (M1 Pro, arm64 and x86_64 under Rosetta) and at 0xF_C000_0000 on macOS 27 (27.0.1 on an M4 Pro, arm64 and x86_64 under Rosetta; 27.2 on an M2 Max, arm64). With dyld and the Rosetta runtime near 0x2_0200_0000 and 0x3_0400_0000, a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way -- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A host standard stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated +- [readdir and readdir_r](../../../core/libs/prx/libc/src/Directory.cpp) (libc) on Windows report, on a file system whose directory listing carries no file ID (the entry's ID is 0, or the volume refuses `FileIdBothDirectoryInfo`), a nonzero `d_fileno` hashed from the entry name and the directory's file index, which differs from `lstat`'s `st_ino`. Not reproduced: NTFS lists an ID for every entry - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way - [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked @@ -118,9 +122,10 @@ - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. It throws for `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. -- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred -- [Call tracing](../../../core/libs/prx/libc/src/specifics/windows/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) exists on Windows only and records the entry of a call, not its arguments or its result. One process-wide ring keeps the last 65536 calls, so a thread that calls often pushes the others out. Calls are also counted per traced target since start-up; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; arguments in wider vector registers pass only as long as the recording code stays free of AVX instructions, which is not tested +- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) +- [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. - [app0.zar](../../../core/libs/prx/libc/src/AppArchive.cpp) (libc) - a [ZArchive](https://github.com/Exzap/ZArchive) beside `app0/` is a read-only layer under it: a path missing from `app0/` is looked up in the archive, case-insensitively. It is not a console format; titles ship their files in a PFS image instead. Resolving an archive-only path through any call other than the archive-aware libkernel ones throws, so `fopen`, `stat`, `access`, `opendir` and the rest do not read the archive yet, and `opendir` of a directory that exists in both places, `app0/` itself included, and `fdopen` of an archived descriptor throw instead of missing its entries or reading the null device. A damaged compressed block throws instead of returning `EIO` - [fopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - returns the errno of the failed open, as C11 K.3.5.2.1 leaves the nonzero value open and Microsoft's CRT returns errno; the console's Dinkumware `fopen_s` is not verified - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. +- [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), without console verification. `_Thrd_id` throws because whether the console returns `pthread_t` or a kernel thread ID and at what width is unknown; `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 05721a9fc9..6e80c0af8f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -18,8 +18,8 @@ - [sceKernelGetSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports baseline system software version `0x01000000` ("01.000.000") for ProsperoStore (PPSA99000); the console's firmware version is not modeled. Measured on PS5 firmware 13.42: record values 0, 24, 39, 40, 41, 48 and 64 in the leading 64-bit field are accepted and preserved, the 28-byte string at offset 8 and packed version at 0x24 are filled, and bytes from 0x28 onward remain untouched. This API reports "13.590.001" / `0x13590001` in that environment, while the Prospero variant reports "13.420.000" / `0x13420000`; their values must not be assumed identical. A null pointer still returns `SCE_KERNEL_ERROR_EFAULT`, which was not measured; other firmware versions and record values remain unverified. - [kern.rng_pseudo](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` draws a new block of 64 bytes from the host's `std::random_device` for each read (no new value) that passes a buffer and a length, and sizes its answers as a payload process on a PS5 (firmware 12.70) sees it: a size query reports 64; a buffer of 64 bytes or more gets 64 bytes and a length of 64 (tried up to 65536); a smaller buffer gets as many bytes as its length, which is left as given, and a null length gets none, both with -1 and `ENOMEM`; a null buffer with a null length returns 0. `errno` is kept when the call succeeds. A write returns -1 with `EPERM`, which was not tried on the console. The only caller seen is mbedTLS's `mbedtls_hardware_poll` in the catalog's ProsperoLight boilerplate, which asks for 64-byte blocks and fails if fewer come back. The console's generator is unknown, and a title process was not measured. On the console `sysctlnametomib` gives MIB {1, 50} for the name, and `sysctl` with it answers reads of 0, 1, 63, 64, 65 and 128 bytes, a size query, a null length and a null buffer with a null length as the name does; here `sysctlnametomib` maps the name to {1, 50} and `sysctl` {1, 50} answers as the name does; a write through the MIB was not measured and throws - [sysconf](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `_SC_CLK_TCK` returns 128, FreeBSD's `CLK_TCK` as its [`sysconf`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L100-L101) returns it ([`time.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/time.h#L48-L54)); the console's value is not checked, and no `times` reports ticks at that rate. CPython's `posix` module stops its import with "cannot read ticks_per_second" when the value is below 1, as the Python in PS5MC (PPSA99420) does. `_SC_ARG_MAX` returns 262144 (FreeBSD's `NCARGS`), `_SC_OPEN_MAX` returns the `RLIMIT_NOFILE` soft limit via `getrlimit` ([`sysconf.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/sysconf.c#L106-L115)), and `_SC_IOV_MAX` returns 1024 (FreeBSD's `UIO_MAXIOV`). Names other than `_SC_ARG_MAX`, `_SC_CLK_TCK`, `_SC_OPEN_MAX`, `_SC_PAGESIZE`, `_SC_IOV_MAX`, `_SC_NPROCESSORS_CONF`, `_SC_NPROCESSORS_ONLN` and `_SC_PHYS_PAGES` fail with `EINVAL` -- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`; the console's values are not known. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) -- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer; the console's value is not known and a write throws. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up +- [hw.realmem](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_HW`, 12} and `sysctlbyname` report the same 8-byte value as `hw.physmem` (the `_SC_PHYS_PAGES` pages of 16 KiB), read-only with `EPERM` for a write and `ENOMEM` for a short buffer, as FreeBSD's [`sysctl_hw_realmem`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_mib.c#L191-L204) does. FreeBSD reports there the memory the firmware describes, which is at least `hw.physmem`. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for `hw.physmem` and `hw.realmem` (a read, a size query and a null length) and for {`CTL_HW`, 5} and {`CTL_HW`, 12} (a read and a size query); what a title process gets was not measured. Dolphin reads it as the machine's memory size in [`MemPhysical`](https://github.com/dolphin-emu/dolphin/blob/8d2ad1a662472955497e5bb79d7b9ec6999e1f0d/Source/Core/Common/MemoryUtil.cpp#L253-L273) (RetroArch PPSA99169's `dolphin_libretro` core) +- [vm.overcommit](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the 4-byte value 0, the default FreeBSD gives its [`vm.overcommit`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/swap_pager.c#L158-L161) `int`, with `ENOMEM` for a short buffer, and a write throws. A payload process on a PS5 (firmware 12.70) gets -1 with `ENOENT` for it, for a read, a size query and a null length; what a title process gets was not measured. mimalloc reads it in [`unix_detect_overcommit`](https://github.com/microsoft/mimalloc/blob/31d034d94cdb8e22f7d7ed55967f581a2d6e831d/src/prim/unix/prim.c#L129-L153) and takes 0 as no overcommit, as PS5MC (PPSA99420) does at start-up - [kern.sdk_version](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown - [kern.ostype, kern.osrelease, kern.version, kern.hostname, hw.machine](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` (1.1, 1.2, 1.4, 1.10, 6.1), `sysctlbyname` and `sysctlnametomib` serve the names `uname` reads as read-only strings: "FreeBSD", "11.4-RELEASE" and "FreeBSD 11.4-RELEASE\n", the values FreeBSD 11.4-RELEASE builds without source control information ([newvers.sh](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L45-L52), [L293-L294](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/conf/newvers.sh#L293-L294)), the host name `gethostname` reports ("AnyPS5") and "amd64"; a short buffer returns `ENOMEM` and a write `EPERM`. The console's strings are not measured - [sceKernelGetProsperoSystemSwVersion](../../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it @@ -53,14 +53,16 @@ - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [Protecting a host thread's stack](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - on Windows `sceKernelIsStack` reports a host thread's whole stack reservation, which Windows commits on demand from the top down behind a guard page. A protect over that range changes only the pages above the guard; the guard page and the reserved pages below it are left as they are, so the stack still grows and an overflow still raises a stack overflow. A protect that starts at the bottom of the reservation and stops short of the guard page, as HotSpot makes its stack guard zones, is committed and protected. Windows moves the guard page down by committing the pages below it as read/write guard pages, whether they were reserved or committed, so those pages come in read/write as the stack grows to within one guard region of them (three pages on GitHub's runners). A protection other than read/write on them, `PROT_NONE` included, does not hold once the stack is that close, where on the console every page of the reported range takes the protection. Holding it would mean removing the thread's guard page and keeping the stack limit in its TEB by hand. On Linux the reported range of the main thread is mostly unmapped; the unmapped pages from the bottom of a protect up to the top of the stack are mapped read/write in place, then the protect is applied, so the stack never has to grow down to the protected pages, which Linux refuses within its stack guard gap (1 MiB by default) above a readable mapping. The pages below a protect stay unmapped and out of the stack's reach, where on the console the stack goes on below them - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. A contended operation 0 or 1 was not measured; uncontended, both fail with `EOPNOTSUPP` and leave the word unchanged, as FreeBSD 11's [`__umtx_op_unimpl`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3349-L3353) does and as a payload process on a PS5 (firmware 12.70) answered operation 1 for four word values and operation 0 for a word of 0. Operations below 0 or above 26 fail with `EINVAL`, as in FreeBSD 11. `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. The other operations throw, and so does a misaligned object. The same payload process answered `UMTX_OP_SEM_WAIT` (19) and `UMTX_OP_SEM_WAKE` (20) as FreeBSD 11's [`do_sem_wait` and `do_sem_wake`](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L3100-L3192), built under `COMPAT_FREEBSD9` or `COMPAT_FREEBSD10` ([op_table](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c#L4015-L4021)), do: 0 for a count of 1 with `_has_waiters` set, `ETIMEDOUT` for a count of 0 with a 100 ms relative timeout, and 0 for a wake without a waiter; it answered `UMTX_OP_SEM2_WAIT` (23) with 0, not `ETIMEDOUT`, for a count of 0 with the same timeout, leaving the count 0 where FreeBSD 11 would set `USEM_HAS_WAITERS`, and its log ends before the result of `UMTX_OP_SEM2_WAKE` (24) without a waiter. PPSA12544's `libc.prx` and Unity's `PS5Util.prx` (PPSA18194) pass `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and wait for a running call_once with operation 1; what those callers do with the `EOPNOTSUPP` was not run, and whether the console reads a microsecond timeout there in all cases is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count +- [_umtx_op](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the mutex, condition variable, read-write lock, legacy lock and `SEM2` operations throw, and so does a misaligned object. Implemented as FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE`, `UMTX_OP_NWAKE_PRIVATE`, `UMTX_OP_SEM_WAIT` and `UMTX_OP_SEM_WAKE` (the last two built under `COMPAT_FREEBSD9`/`10`). Operations 0 and 1 fail with `EOPNOTSUPP`, as measured on a PS5 (firmware 12.70). The `SEM2` operations, 23 and 24, were measured on the console and returned `0` for a count of 0 where FreeBSD would time out, so they stay unimplemented. - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked +- [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it @@ -107,6 +109,7 @@ - [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) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: 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 the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. 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 +- [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional @@ -114,7 +117,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws -- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw +- [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end and `EVFILT_WRITE` on the write end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe off Linux (see the kevent EV_CLEAR entry), `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event fires once the send buffer's free space reaches the mark, as `filt_sowrite` does with `sbspace`; the host reports that space as `SO_SNDBUF` less `SIOCOUTQ` on Linux and as `SO_SNDBUF` alone elsewhere, so off Linux it does not shrink while data is queued. Marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here @@ -146,7 +149,7 @@ - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest -- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for the whole 1 s retry limit, such as a long HLE or driver call, costs the raising thread up to 1 s per raise and is then redirected in host code anyway, with the same hazard. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented +- [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows suspends the target and redirects it to the handler. A target running host code (any module other than the executable and the `*.guest.prx` modules, so libkernel, libc and the other HLE modules count too) is resumed and sampled again until it is back in guest code, since a handler that waits there would keep the host locks that code holds; a target blocked in an ntdll wait is redirected in place, as are NtContinue and winpthreads after their retry limit. A target that stays in host code without waiting for 50 ms after the raise began, such as a long HLE or driver call, is then redirected in host code anyway, with the same hazard. A target seen right after a `syscall` instruction in ntdll, inside a system call that is not a wait such as the `NtWriteFile` of an `fwrite` holding its stream lock, restarts those 50 ms, so a raise can cost the raising thread up to the 1 s retry limit. While the target is suspended only `VirtualQuery` and a lock-free cache of image bases are consulted; an image not yet in the cache is classified with `GetModuleFileNameW` after the target is resumed. An APC queued at the first host-code sample, which an alertable guest wait would run sooner, is not implemented - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) does not consult the target thread's signal mask - [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 @@ -166,7 +169,7 @@ - [sysarch](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - `AMD64_GET_FSBASE`, `AMD64_GET_GSBASE` and `AMD64_SET_GSBASE` follow FreeBSD 11's amd64 [`sysarch`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c) on Linux x86-64 hosts, through the host's `arch_prctl`. The GS base belongs to the calling thread, a base at or above `0x800000000000` (`VM_MAXUSER_ADDRESS`) returns `EINVAL`, and a null argument returns `EFAULT`. `AMD64_SET_FSBASE` throws, since guest and host code share the thread's FS base for their TLS. In a payload process on a PS5 (firmware 12.70), `I386_GET_FSBASE` (7) and `I386_GET_GSBASE` (9) wrote 4 bytes, the low 32 bits of the FS and GS bases, and failed with `EFAULT` for a null argument; here they do the same on Linux x86-64 hosts. There a GS base set by one thread left the main thread's unchanged, and threads made with `pthread_create` and `scePthreadCreate` started with their creator's GS base; here a guest thread's host thread is created by the creating thread, and Linux's [`copy_thread`](https://github.com/torvalds/linux/blob/v6.6/arch/x86/kernel/process.c#L182-L186) gives a new thread its creator's GS base. The operation numbers FreeBSD 11's amd64 `sysarch` has no case for returned `EINVAL` there for the nine tried, each with a null argument (2, 5, 6, 12, 127, 133, 134, 1000 and -1; 6 is `I386_VM86`, which its header names), as its [default case](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/amd64/amd64/sys_machdep.c#L324-L325) does; here every such number returns `EINVAL` on every host; the numbers other than those nine follow that default case and were not measured. `AMD64_GET_XFPUSTATE` (132) returned 0 there for lengths 0, 64, 128, 256 and 512 (512 was also the largest length a search accepted) and failed with `EINVAL` for 1024, 2048, 4096 and 8192 and with `EFAULT` for a null argument or address, and `I386_GET_XFPUSTATE` (11) failed with `EFAULT` for a null argument; both throw here. The other operations FreeBSD 11's amd64 `sysarch` handles, and every operation it handles on other hosts (Windows keeps its TEB in GS), throw. - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [machdep.cacheline_size](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns -1 with `ENOENT`, matching FreeBSD amd64 where this sysctl OID is not defined. +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) @@ -202,8 +205,8 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console -- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also duplicates the descriptor (`F_DUPFD`, `F_DUPFD_CLOEXEC`) and handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2` and an accepted socket start without it, `SOCK_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. - [sendfile](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it @@ -219,6 +222,7 @@ - [pthread_atfork](../../../core/libs/prx/libkernel/Pthread/Posix/Atfork.cpp) (libkernel) appends the three handlers to a process-wide list and returns 0, or `ENOMEM` when the entry cannot be allocated, as FreeBSD 11.4's [`_pthread_atfork`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libthr/thread/thr_fork.c#L78-L100) does; null handlers are accepted. The handlers never run: `fork` throws and `vfork` is absent, so no child process is created. Handlers of a module that is unloaded stay in the list, where FreeBSD's `__pthread_cxa_finalize` removes them. PPSA97358 has a source-level call from the constructor `rcu_init` ([rcu.c](https://github.com/ZiZc3/XPSemu/blob/8eec07b4e31f66dce8e0b05c1ca1281af303e933/util/rcu.c#L488-L495)), but current-main runtime reachability is unverified; XPSemu currently stops first at the unresolved `mlockall` import (#4386) - [sceKernelGetPageTableStats](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) reports 8192 entries on each side and counts them as a PS5 (firmware 13.60) counted them for a payload process: one cpu entry for each 2 MiB granule that holds a committed mapping, whatever its protection (none included), and one gpu entry for each 1 GiB region that holds a GPU-accessible mapping, plus one for each 2 MiB granule of such a mapping that is not a whole granule of direct memory at a 2 MiB-aligned physical address. A granule or region shared by several mappings counts once, and reservations do not count. On the console the first GPU mapping of a process also takes three gpu entries and one cpu entry that stay after it is unmapped, which is not modelled, and only the ranges whose protection is recorded here are counted. A null pointer returns `SCE_KERNEL_ERROR_EFAULT`, where the console faults. Resonance: A Plague Tale Legacy (PPSA30601) calls it at start-up (#4067) - [clock_settime](../../../core/libs/prx/libkernel/Time/Time.cpp) (libkernel) validates like FreeBSD 11.4's [`kern_clock_settime`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_time.c#L396-L412) for a root caller: `EFAULT` for a null time, `EINVAL` for any clock but `CLOCK_REALTIME`, a negative second count or a nanosecond count outside 0 to 999999999. A valid `CLOCK_REALTIME` request throws, naming the requested time, since the host clock is never set and no guest clock offset is kept. FreeBSD checks `PRIV_CLOCK_SETTIME` first, which a jailed process fails with `EPERM`; whether a title holds it on the console is unverified. PS5MC (PPSA99420) imports it; no call site is known +- [Short sleeps ended by GPU and flip completions](../../../core/libs/prx/libkernel/Time/SleepWake.cpp) (libkernel) - a `sceKernelUsleep`, `usleep`, `nanosleep` or `sceKernelNanosleep` of 1 ms or less returns before the requested time, which FreeBSD and the console do not do: it ends once the AGC driver reports a completed GPU batch or batch completion action, or VideoOut completes a flip, so a title that polls a GPU label or the flip status between sleeps does not sleep the rest. A title that paces frames with such a sleep can wake up to 1 ms early. A thread returns early at most 8 times in a run of sleeps; the run restarts when the thread submits AGC work or did not sleep the requested time since its last sleep, so a loop that counts sleeps to time out still sleeps all but 8 in full. `APS5_NO_SLEEP_WAKE=1` keeps every sleep in full - [chflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) follows FreeBSD 11.4's [`kern_chflagsat`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2366-L2383) and UFS's [`ufs_setattr`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/ufs/ufs/ufs_vnops.c#L509-L556) for a root caller: the path lookup reports `EFAULT`, `ENOENT` and `ENOTDIR`, a flag outside the 17 UFS stores (`VNOVAL` included) `EOPNOTSUPP`, and `SF_SNAPSHOT` `EPERM`. Neither host keeps BSD file flags, so every file is taken to have none (`stat` reports `st_flags` 0): flags 0 succeed without touching the file, and any other flag throws, naming the flags. `UF_READONLY`, `UF_HIDDEN`, `UF_SYSTEM` and `UF_ARCHIVE` are FreeBSD's names for the Windows attributes but are not mapped to them: UFS stores them without enforcing them, `FILE_ATTRIBUTE_READONLY` already carries the owner write bit of `chmod` on Windows, and Linux has no equivalent a process can set without `CAP_LINUX_IMMUTABLE`. The console's filesystem types, `EROFS` on read-only mounts and the change-time update are not modelled - [fchflags](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) has the gaps of `chflags`: a valid non-zero flag throws, and the console's filesystem types, `EROFS` and the change-time update are not modelled. Character devices, the random device, other host descriptors and files read from app0.zar throw, since their console filesystems and the privilege check for devices are not modelled. On Windows, anonymous shared memory objects are temporary files that are not told apart from regular files, so they are treated as files instead of reporting `EINVAL` as FreeBSD 11.4's [`getvnode`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L3850-L3877) does. - [getpriority](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) follows FreeBSD 11.4's [`sys_getpriority`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_resource.c#L93-L165) for the only guest process, which is taken to run at nice 0: `PRIO_PROCESS` and `PRIO_PGRP` with 0 or its own pid (the guest is taken to lead its own process group, see `kill`) and `PRIO_USER` with 0 or its user id 0 return 0, any other id fails with `ESRCH` and any other `which` with `EINVAL`. As in FreeBSD's libc, where `getpriority` is the bare system call, errno is left unchanged on success. The nice value of a title process on the console is unverified, and the host nice of the process or of the threads `APS5_THREAD_PRIORITY=high` raises is not reported. `setpriority` accepts the same target ids (any other id fails with `ESRCH`, any other `which` with `EINVAL`), clamps the value to FreeBSD's `PRIO_MIN`-`PRIO_MAX` (-20 to 20) and stores it as the guest process's nice value, which `getpriority` returns; the host scheduling class is not changed, and whether a title process may lower the value without privilege is unverified diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 53d901c2c6..e496bc72a6 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -4,7 +4,7 @@ - [Executable guest imports](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - on Linux and macOS, a relocated executable import resolved to one guest provider requires that module and its dependencies at startup, even when it was discovered through `--module-dir` without a DT_NEEDED entry. Unreferenced symbols and no-op relocations do not require an otherwise deferred module. Console module-start ordering is unmeasured. -- Windows [input and output collision checks](../../../core/relinker/main.cpp) use temporary hard links when MinGW reports different permissions for two multiply linked files, as with `.elf` and `.exe` names. The comparison fails if either file is read-only or either parent directory refuses the temporary link, even when the files are distinct. +- Windows [file identity checks](../../../core/relinker/io/src/FileIdentity.cpp) (input and output collisions, the input skipped during guest module discovery, executables converted twice) compare the volume serial number and the 64-bit file index from `GetFileInformationByHandle`. ReFS file IDs are 128-bit, so two distinct files on ReFS can in principle compare equal: a collision or a repeated executable is reported, or a module is skipped; `FILE_ID_INFO` is not used. - [Runtime-loaded modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) (`--module-dir`) that another module needs are initialized at startup with no start arguments, before the module that needs them is loaded; the console is assumed to start them when that module is loaded. - The executable file that [relinker](../../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens a console when launched, unless it is relinked with `--windows-gui`, which selects the GUI subsystem. A GUI build started from Explorer has no standard output or error handle; the [entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp) gives each missing one `anyps5.log` beside the executable, replacing the previous run's file (#3282). Errors about the executable path or about waiting for the previous LoadExec process are written before the log is opened and are lost when the standard error handle is missing. If the log cannot be created, output to the missing handles is lost. - [Guest import registries](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) - declared host imports of bundled modules are recorded with their module name and relocation target, even when another guest module exports the same stripped NID. No-op relocations are omitted. Registries describe import bindings, not implementation correctness or runtime reachability. @@ -20,12 +20,14 @@ - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - The [syscall scan of `sce_module` modules](../../../core/relinker/relinker/src/guest/GuestModuleBuilder.cpp) with a `PT_SCE_DYNLIBDATA` segment stops at the same end, or, when the PLT comes before the code, at the end of the last function of the unwind table; code after it is not scanned. - In the same executables and modules, the bytes between unwind functions are 16-byte slots of function tails and padding, and a tail can begin inside an instruction. The [instruction scanner](../../../core/relinker/codegen/src/InstructionScanner.cpp) of the syscall and call-site scans therefore resumes at the next byte when bytes outside every unwind function do not decode, and ends a decoding outside them at the start of the next unwind function. Code without an unwind entry that does not decode from its first byte is skipped by these scans instead of failing the relink; inside an unwind function a decode failure still fails it. +- Read-only data can also sit between the unwind functions of such images. There, a `syscall`, `sysenter` or `int 0x80` pattern fails the [syscall scan](../../../core/relinker/relinker/src/analysis/SyscallScanner.cpp) only when a constant syscall number is set before it; the others, and every `sysret`, are skipped and counted in one message. The number is looked for in the decoded instructions and, whatever alignment the decoding took, in the bytes before the pattern: a `mov eax, imm32`, `mov rax, imm32`, `mov rax, imm64` or `xor eax, eax`, optionally followed by `mov al, imm8` or `mov ax, imm16`, then by any number of register moves or zeroings that leave `rax` alone. A real syscall without an unwind entry whose number is set otherwise is not refused and runs on the host: from memory or another register (`mov eax, ecx`), by `push`/`pop` or arithmetic (`or eax, imm32`), or with any other instruction between the load and the pattern. +- In the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs for such images, a relocation or `lea` target outside every unwind function is followed as code only when its linear decoding, up to the first jump, return or trap, the next unwind function or 256 instructions, holds an `fs:[disp32]` access, or when it is 16-byte aligned, starts less than 256 bytes after an unwind function and ends in a jump, return or trap. Every other such target is taken as data and counted in one message, and the Windows TLS rewrite fails instead of overwriting one. A TLS access of another form, such as `mov rax, fs:[rax]`, or a `gs` instruction in code reached only through such a target is therefore not rewritten or trapped. Other fs and gs instructions do not tell code from data there: string bytes decode as these prefixes, and in two measured titles about 2,500 of about 5,500 such targets decode to one before their first jump. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, or to the end of that function when the PLT comes before the code, as in the `libSceFios2.prx` and `libc.prx` that CUSA02636 bundles (PLT at 0xc8); the pipeline's scans of executables above stop at this same end, also when the PLT comes before the code. A guest module with PLT relocations and no PLT before or after its unwind functions, or with a PLT that overlaps one, fails as the executable does. Code after that end is not analyzed, such as the `DT_FINI` routine without an unwind entry that follows the last unwind function in both modules and that the Windows and macOS outputs call when the program exits: a TLS access in it is not rewritten, the ELF entry point, `DT_INIT`, `DT_FINI`, a branch or a pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - In macOS output, a guest instruction with a GS segment prefix, including one with no memory operand (`65 90`, `65 48 89 c8`), which does not touch GS, becomes `UD2` and NOP padding of its own length ([macOS image writer](../../../core/relinker/elfpatcher/src/macos/MacOsImage.cpp)): the host owns GS, which holds the pthread TSD base the guest TLS stubs read, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). -- `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time +- `--trace-imports` ([Windows entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp), [Linux entry stub](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp)) exists for Windows and Linux output, not macOS. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time. On Linux the entry stub makes the page of every traced slot readable, writable and executable with `mprotect` before it stores the thunk, so a slot inside `PT_GNU_RELRO` can be replaced; those pages stay writable, and data pages become executable, while tracing is on - [ANDN lowering](../../../core/relinker/codegen/src/x86/AndnLowering.cpp) uses scalar NOT and AND for 32- and 64-bit register and memory operands under `--to-rosetta`, following the ANDN entry in [Intel SDM volume 2A](https://cdrdv2-public.intel.com/812383/253666-sdm-vol-2a.pdf). SF and ZF follow the result, CF and OF clear, and AF and PF are undefined. The scratch register is saved below the 128-byte red zone. A 64-bit stack-relative displacement that exceeds signed disp32 after reserving the spill frame throws; other BMI instructions are not lowered. The M1 test covers register overlap, RSP operands, memory addressing, RIP relocation, flags and red-zone preservation; precise fault delivery inside a spill sequence has not been verified. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b8e3f28aab..723ef2a960 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -122,7 +122,7 @@ - [POPS collision SGPRs](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - the collision wave ID and intrawave collision mask read 0, so a title skips its POPS wait and runs its per-collision loop once. - [Image loads](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) (shader recompiler) - an image_load reads a sampled binding, also of an image the shader stores to, so in a primitive-ordered pixel shader it misses an earlier overlapping primitive's image_store in the same draw. - [Tessellation rings](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) of local, hull and domain programs become Vulkan tessellation interfaces and are never written to guest memory; a title that reads the off-chip or factor ring back sees no data. -- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point. On the console an index past the patch reads the LDS slot of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. +- [Hull input control points](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a hull LDS read at a control-point index computed from data reads `gl_in` at that index clamped to the patch's last input control point, and a domain off-chip read at a computed index reads `gl_in` clamped to the patch's last output control point. On the console an index past the patch reads the LDS slot or off-chip record of a neighbouring patch or of no patch; the host has no such memory, and Vulkan leaves an out-of-range `gl_in` index undefined. The lowering refuses a computed offset that is not a whole number of control points, but the index itself is only known when the program runs. - [Inner tessellation factors](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) of integer, fractional-even or fractional-odd triangle patches that are NaN tessellate as 1 (fractional-even then raises them to 2, as Vulkan does), as in the D3D11 reference tessellator; the console is not measured. - [Hull programs](../../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) start from the hull launch registers (user data, ring bases, v0, v1); on the console they start with what the local program leaves, and that it leaves them unchanged is not checked. - [Quad tessellation factors](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) are taken as outer[0..3] then inner[0..1], 24 bytes per patch, as [Mesa stores them](https://github.com/mihawk-99/PS5_Mesa/blob/b3588f78dd7fd46fbadc7ecd487e5f14d4213e4b/src/amd/common/nir/ac_nir_lower_tess_io_to_mem.c); unmeasured on the console. @@ -131,7 +131,7 @@ - [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) partitioning throws. - [Fractional-even partitioning](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (`VGT_TF_PARAM.PARTITIONING` 3) is drawn with Vulkan's fractional-even spacing, which [radeonsi programs as `PART_FRAC_EVEN`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L536-537), with factors bounded at 64. Vulkan raises every factor that is not culled to at least 2 after `VGT_HOS_MIN/MAX_TESS_LEVEL`, so a `VGT_HOS_MAX_TESS_LEVEL` below 2 still tessellates at 2; the D3D11 range for this mode is also [2, 64], but the console's order of the HOS clamp and that minimum is not measured. - 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 [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. `v_cubeid_f32`, `v_cubesc_f32`, `v_cubetc_f32` and `v_cubema_f32`, measured on RDNA2 (gfx1036) with `DX10_CLAMP`=1, apply it whenever IEEE mode is off, in all four f32 denormal modes (`v_mul_f32_e64` in the same measurement follows the rule above), with the same bit-level result as the f32 path, and ignore it with IEEE mode on; clamp is applied after it. Only round to nearest even is modelled: in the other round modes (measured with IEEE mode off) gfx1036 rounds an overflow as the mode says (to the largest finite value when rounding toward zero), in `v_cubema_f32`'s own result and in the `mul:2` and `mul:4` scaling, where the translation gives infinity, and when rounding toward negative infinity a zero `v_cubema_f32` result with a NaN input is -0, where the translation gives +0. - The [clamp modifier](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows `DX10_CLAMP`, measured on RDNA2 (gfx1035) for f32, f16, packed f16 and f64 results at IEEE=0 and IEEE=1: with `DX10_CLAMP`=1 (and in shaders without a known float mode) a NaN result clamps to +0, including the quieted NaN that `v_pk_min_f16` and `v_pk_max_f16` return for a signaling NaN operand with IEEE=1 (measured on RDNA2 gfx1036 at both `IEEE_MODE` and both `DX10_CLAMP` settings); with `DX10_CLAMP`=0 a NaN result is written unchanged and every other value clamps to [0, 1] as before. f64 results follow the same rule on hardware, but a clamped f64 result with `DX10_CLAMP`=0 throws until it has an execution test. With `DX10_CLAMP`=0 the NaN's bits are those the translation already produces for the instruction without clamp, so where those are left to the driver (f32 and scalar f16 arithmetic) the payload is the driver's, not the hardware's input propagation. `v_dot2_f32_f16` and the integer clamps are unchanged. - [FP16_OVFL](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follows the shader's float mode as measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with f16 denormals kept and flushed: with `FP16_OVFL`=1 an f16 result that rounds to ±infinity is written as ±0x7bff unless a source operand is infinite, so `v_rcp_f16`, `v_rsq_f16` and `v_log_f16` of ±0 and `v_exp_f16` of 16 or more are clamped while an infinite input passes through, and `v_div_fixup_f16` keeps the infinity it writes for a zero denominator or an infinite numerator and clamps any other infinite quotient, a NaN one included. `v_cvt_pkrtz_f16_f32` rounds toward zero and never overflows. The sources are checked as the f32 values the translation computes with; the rule is applied to `v_fma_mix*_f16` with f32 sources and to `v_interp_p2_f16` without a measurement of infinite f32 sources or parameters. Round modes other than nearest even are not modelled, so the overflow threshold is the nearest-even one (65520). - [Directed f16 rounding](../../../core/shader/recompiler/Translation/src/TranslationContext/SubwordPacking.cpp) for `v_cvt_f16_f32`, `v_cvt_f16_i16`, `v_cvt_f16_u16` and `v_ldexp_f16` follows `FLOAT_MODE` bits 3:2 in every f16 denormal mode, measured on RDNA2 (gfx1036 with f16 denormals preserved, gfx1030 in all sixteen combinations of f16 rounding and denormal mode). When bit 7 flushes f16 results, `v_cvt_f16_f32`, `v_cvt_f16_i16` and `v_cvt_f16_u16` write a zero of the result's sign for a value that, rounded in that mode with an unbounded exponent, is below 2^-14, so a value just below 2^-14 that rounds up to it stays 0x0400; `v_ldexp_f16` keeps its rule above. The `clamp` modifier is applied before the rounding. On gfx1030 every row matches: 8,996 f32 inputs around the half denormal, normal and overflow boundaries with and without `clamp`, every 16-bit integer, and 2,470 `v_ldexp_f16` operands. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index ae5f525a05..1ea392b91f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -63,7 +63,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--windows-diagnostics` | Include startup dependency diagnostics. Requires `--windows`. | | `--windows-gui` | Select the Windows GUI subsystem instead of the console subsystem. Requires `--windows`. | | `--windows-fixed-base
` | Map the lowest ELF load page at this fixed Windows address, given in decimal or hexadecimal (`0x`). Requires `--windows`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and a complete image fitting in the user address space. The main executable has no ASLR or base relocations; an occupied address fails to load. Bundled modules keep their normal placement. | -| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | +| `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Windows and Linux output only; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--macos-fixed-base
` | Map the lowest ELF load page of the macOS executable at this fixed address, given in decimal or hexadecimal (`0x`). Requires `--macos`, a 64 KiB-aligned address from `0x20000` through `0x7fffffff0000`, and an image that ends in the macOS user address space. `__PAGEZERO` ends 64 KiB below the address and the Mach-O header occupies those 64 KiB. The executable has no ASLR; an address the process cannot map fails to load. Bundled modules keep their normal placement. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | @@ -228,7 +228,7 @@ Set `APS5_VIDEOOUT_TELEMETRY_DIR` to a directory before launching the applicatio ### System fonts -Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`); copy the files named above into `anyps5-fonts/`. +Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai), `NotoSansArabic-{Light,Regular,Medium,Bold}.ttf` (Arabic) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese, Chinese and Korean). A set that covers several scripts also opens the files of its other scripts (`SSTThai-*.otf`, `SSTArabic-*.otf`, `SSTJpPro-*.otf`, `DFHEI5-SONY.ttf`, `SCEPS4Yoongd-*.otf` or their substitutes) for the characters its main file lacks; a missing one leaves those characters without a glyph. Without either, opening a system font set fails (the log shows `sceFontOpenFontSet` with `file=(none)`), and what happens next depends on the game: some show no text in those fonts, and others stop at start-up because they assert that the open succeeded. The substitutes come from the [Noto project](https://github.com/notofonts/notofonts.github.io) under the SIL Open Font License (the Noto Sans files are in `fonts/NotoSans/full/ttf`, the Arabic ones in `fonts/NotoSansArabic/full/ttf`); copy the files named above into `anyps5-fonts/`. ### Console language @@ -308,7 +308,7 @@ To measure how a title runs on a machine, start it with `APS5_TRACE_SUMMARY_EVER ### Tracing system library calls -Windows only. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls: sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. +Windows and Linux. An executable converted with `--trace-imports` records the calls it makes to the libraries named in `APS5_TRACE_CALLS`, a comma-separated list such as `libkernel,libSceSaveData`, or `all`. Without the variable nothing is recorded. The crash and abort reports then end with the last 64 calls (on Linux, which has no crash report, only an abort prints them): sequence number, thread, library, import name as the executable imports it (a NID) and the caller's address. They are followed by the 32 most called imports since start-up, with their number of calls. ``` last traced system library calls (APS5_TRACE_CALLS): diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 465ad3b15d..d7c8218f67 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -37,7 +37,11 @@ def main(): cache, seed = Path(args.cache), Path(args.seed) if cache.resolve() == seed.resolve() or (cache.exists() and seed.exists() and cache.samefile(seed)): parser.error("seed output would overwrite the input cache") - found = {name: entry for name, entry in checks(cache).items() + try: + results = checks(cache) + except (OSError, UnicodeDecodeError) as error: + parser.error(f"{cache}: {error}") + found = {name: entry for name, entry in results.items() if (not args.include or any(fnmatch.fnmatchcase(name, pattern) for pattern in args.include)) and not any(fnmatch.fnmatchcase(name, pattern) for pattern in args.exclude)} seed.write_text("".join(f"set({quote(name)} {quote(value)} CACHE INTERNAL {quote(doc)})\n" diff --git a/tools/debt_split.py b/tools/debt_split.py index 728521a76a..1460f46bca 100644 --- a/tools/debt_split.py +++ b/tools/debt_split.py @@ -115,7 +115,7 @@ def split(text): def render_index(index): out = [] for kind, item in index: - if kind == "heading" and out and out[-1] != "": + if kind in ("heading", "text") and out and out[-1] != "": out.append("") out.append(item) if kind == "heading" or kind == "text": diff --git a/tools/import_audit.py b/tools/import_audit.py index be6cdf0efc..0e47d38e5d 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -733,6 +733,8 @@ def main(argv=None): parser.add_argument("--source", default=str(PRX), help="core/libs/prx tree used to find throwing stubs (default: this repository's)") parser.add_argument("--names", help="aerolib.csv style file (NID, name per line) used to name absent imports") parser.add_argument("--json", type=Path, help="write the full result to this file") + parser.add_argument("--strict-stubs", action="store_true", + help="also fail when a referenced import resolves to a throwing stub") parser.add_argument("--strict-providers", action="store_true", help="fail when a named import is provided only by another library; retains the existing libc and title-module rules") args = parser.parse_args(argv) try: @@ -776,7 +778,7 @@ def main(argv=None): print(f"FAIL: {error.strerror}: {error.filename}" if error.filename else f"FAIL: {error}", file=sys.stderr) return 2 print(render(summary, records, missing, stops, missing_hosts)) - return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) else 0 + return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": diff --git a/tools/select_tests.py b/tools/select_tests.py index 2d1b33ed6e..6e50619f0f 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -166,7 +166,7 @@ def dump(build, out): print(f"{len(tests)} tests, {len(closures)} test executables, {len(deps)} objects with header deps") -def select(graph, changed): +def select(graph, changed, reasons=None): full = [f for f in changed if FULL.search(f)] if full: return None, full @@ -180,15 +180,28 @@ def select(graph, changed): if (test["target"] is None or files & set(test["registered"]) or files & scope or any(f.startswith(dirs) for f in scope | set(test["registered"]))): picked.add(name) + if reasons is not None: + reasons[name] = (["no build executable"] if test["target"] is None else []) + reasons[name] += ["input: " + path for path in sorted(files & scope)] + reasons[name] += ["registration: " + path for path in sorted(files & set(test["registered"]))] + reasons[name] += ["directory: " + directory for directory in dirs + if any(path.startswith(directory) for path in scope | set(test["registered"]))] setups = collections.defaultdict(set) for name, test in tests.items(): for fixture in test["sets"]: setups[fixture].add(name) todo = list(picked) while todo: - test = tests[todo.pop()] + name = todo.pop() + test = tests[name] for extra in set(test["depends"]).union(*(setups[f] for f in test["requires"])): - if extra in tests and extra not in picked: + if extra not in tests: + continue + if reasons is not None: + fixture = set(test["requires"]) & set(tests[extra]["sets"]) + kind = "fixture setup for: " if fixture else "dependency of: " + reasons.setdefault(extra, []).append(kind + name) + if extra not in picked: picked.add(extra) todo.append(extra) return picked, [] @@ -222,6 +235,9 @@ def main(): s.add_argument("build") s.add_argument("graph") s.add_argument("changed", help="file with one changed path per line") + e = sub.add_parser("explain", help="explain saved-graph selections without a configured build; CTest may add fixtures") + e.add_argument("graph", type=Path) + e.add_argument("changed", type=Path, help="file with one changed path per line") c = sub.add_parser("ctest", help="run ctest on a tree, limited to the selected tests if select wrote any") c.add_argument("build") c.add_argument("arguments", nargs=argparse.REMAINDER, help="ctest arguments") @@ -230,6 +246,18 @@ def main(): selected = Path(args.build, "select.tests") extra = ["-I", selected.read_text(encoding="utf-8").strip()] if selected.exists() else [] return subprocess.run(["ctest", "--test-dir", args.build, *args.arguments, *extra]).returncode + if args.command == "explain": + try: + graph = json.loads(args.graph.read_text(encoding="utf-8")) + changed = [line.strip() for line in args.changed.read_text(encoding="utf-8").splitlines() if line.strip()] + reasons = {} + picked, full = select(graph, changed, reasons) + except (OSError, ValueError) as error: + parser.error(str(error)) + print(json.dumps({"tests": sorted(picked) if picked is not None else None, + "full_run_paths": full, "reasons": {name: sorted(set(why)) for name, why in reasons.items()}}, + indent=2, sort_keys=True)) + return 0 build = Build(args.build) if args.command == "dump": dump(build, args.graph) diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 9c1b936b20..6064df202a 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -47,6 +47,15 @@ class ConfigureSeedPathTests(unittest.TestCase): self.assertEqual(self.cache.read_bytes(), self.original) self.assertEqual(Path(seed).read_bytes(), self.original) + def test_unreadable_cache_is_reported_without_a_traceback(self): + missing = self.root / "no cache dir" / "CMakeCache.txt" + result = subprocess.run([sys.executable, str(TOOL), str(missing), str(self.root / "seed.cmake")], + capture_output=True, text=True) + self.assertEqual(result.returncode, 2, result.stderr) + self.assertIn(str(missing), result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse((self.root / "seed.cmake").exists()) + def test_same_path_preserves_cache(self): self.assert_rejected(self.cache) diff --git a/tools/tests/test_debt_split.py b/tools/tests/test_debt_split.py index d2d25e96be..1f2cf31775 100644 --- a/tools/tests/test_debt_split.py +++ b/tools/tests/test_debt_split.py @@ -234,6 +234,13 @@ class DebtSplitTests(unittest.TestCase): self.assertTrue((self.root / "docs/dev/debt/libSceA.md").is_file()) self.assertIn("libSceA technical debt", self.read("docs/dev/debt/libSceA.md")) + def test_split_keeps_blank_line_between_staying_entry_and_paragraph(self): + paragraph = "A paragraph that belongs to no library." + self.write(INDEX, "# Project technical debt\n\n### Silent stubs\n\n" + A_STUB + + "\n- A toolchain limitation\n\n" + paragraph + "\n") + self.split() + self.assertIn("- A toolchain limitation\n\n" + paragraph + "\n", self.read(INDEX)) + if __name__ == "__main__": unittest.main() diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index adf98d6742..166dc4508f 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -1851,6 +1851,26 @@ class ImportAuditTests(unittest.TestCase): self.assertEqual((code, out), (2, "")) self.assertIn(f"FAIL: Permission denied: {library}", err) + def test_strict_stubs_fails_only_for_referenced_throwing_providers(self): + name = "sceExamplePending" + nid = nid_names.compute_nid(name) + registry = self.work.root / "game.json" + output = self.work.root / "audit.json" + (self.work.libs / "libSceExample.prx").write_bytes(exporting(nid)) + write_source(self.work.source, "libSceExample", STUB_SOURCE.format(name=name)) + for weak in (False, True): + with self.subTest(weak=weak): + write_registry(registry, [(nid, "libSceExample.prx", weak)]) + self.assertEqual(self.work.run(registry)[0], 0) + code, out, err = self.work.run(registry, "--strict-stubs", "--json", str(output)) + self.assertEqual((code, err), (1, "")) + self.assertIn("stub", out) + self.assertEqual(json.loads(output.read_text(encoding="utf-8"))["unique_by_class"]["stub"], 1) + write_source(self.work.source, "libSceExample", DONE_SOURCE.format(name=name) + + STUB_SOURCE.format(name="sceUnreferencedPending")) + self.assertEqual(self.work.run(registry, "--strict-stubs")[0], 0) + + class StrictProviderTests(unittest.TestCase): def test_named_provider_mismatch_is_optional_failure(self): diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index 3ff32bc4ee..ca8cfcbba6 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -119,6 +119,28 @@ class SelectTests(unittest.TestCase): subprocess.run(["cmake", "--build", str(self.build)], check=True, capture_output=True) self.assertEqual(built, {"alpha_tests"}) + def test_explain_reports_input_and_fixture_reasons_without_a_build_cache(self): + changed = self.build / "explain-changed.txt" + cache = self.build / "CMakeCache.txt" + saved = self.build / "CMakeCache.saved" + cache.rename(saved) + try: + for path in ("alpha/alpha.h", "beta/main.c", "CMakeLists.txt"): + with self.subTest(path=path): + changed.write_text(path + "\n", encoding="utf-8") + report = json.loads(self.tool("explain", self.graph, changed)) + picked, full = select_tests.select(json.loads(self.graph.read_text(encoding="utf-8")), [path]) + self.assertEqual(report["tests"], sorted(picked) if picked is not None else None) + self.assertEqual(report["full_run_paths"], full) + if path == "alpha/alpha.h": + self.assertIn("input: alpha/alpha.h", report["reasons"]["alpha"]) + self.assertIn("no build executable", report["reasons"]["script"]) + elif path == "beta/main.c": + self.assertIn("fixture setup for: beta", report["reasons"]["beta_setup"]) + finally: + saved.rename(cache) + + @unittest.skipUnless(shutil.which("ninja") and (shutil.which("gcc") or shutil.which("cc")), "needs ninja and a C compiler") class FixtureBuildTests(unittest.TestCase): diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index e888dc2074..a7fa3619db 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -554,6 +554,36 @@ class TraceDigestTests(unittest.TestCase): self.assertEqual(raised.exception.code, 2) self.assertIn("summary file not found", error.getvalue()) + def test_thread_filter_keeps_frame_bounds_and_capture_summary(self): + command = [sys.executable, trace_digest.__file__, str(self.trace)] + result = subprocess.run([*command, "--thread", "queue worker 0x0", "--json"], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + report = json.loads(result.stdout) + self.assertEqual(report["frames"], 3) + self.assertEqual(report["summary"], SUMMARY) + self.assertEqual(report["thread_filter"], ["queue worker 0x0"]) + self.assertEqual([thread["thread"] for thread in report["threads"]], ["queue worker 0x0"]) + self.assertTrue(all("Present" not in entry["name"] for entry in report["top_slices_ms_per_frame"])) + self.assertTrue(all(wait["thread"] == "queue worker 0x0" for wait in report["longest_waits"])) + result = subprocess.run([*command, "--thread", "presenter", "--thread", "queue worker 0x0", "--json"], + capture_output=True, text=True) + unfiltered = trace_digest.digest(trace_digest.load_trace(self.trace), SUMMARY) + report = json.loads(result.stdout) + del report["thread_filter"] + self.assertEqual(report, unfiltered) + result = subprocess.run([*command, "--thread", "presenter"], capture_output=True, text=True) + self.assertIn("Thread filter:", result.stdout) + self.assertIn("capture-wide", result.stdout) + result = subprocess.run([*command, "--thread", "misspelled"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("unknown thread", result.stderr) + self.assertEqual(result.stdout, "") + result = subprocess.run([sys.executable, trace_digest.__file__, str(self.trace.with_suffix(".jsonl")), + "--thread", "presenter"], capture_output=True, text=True) + self.assertEqual(result.returncode, 2) + self.assertIn("event trace", result.stderr) + + def interval_line(start, end, p99, gpu_ms, **changes): line = dict(SUMMARY, window={"spec": "summary_every 30", "first_present": 0, "ms": (end - start) * 1000.0}, interval_start_s=start, interval_end_s=end, diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index f897df3616..dec7a19a84 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -300,6 +300,17 @@ class UnwrapTest(unittest.TestCase): self.assertNotIn("Traceback", result.stderr) self.assertEqual(output.read_bytes(), b"previous output") + def test_unsupported_inner_elf_class_and_byte_order_are_refused(self): + for offset, value in ((4, 1), (5, 2)): + with self.subTest(offset=offset, value=value): + container = bytearray(build_container(default_segments(), default_records(), default_payloads())) + container[ELF_HEADER_OFFSET + offset] = value + result, output = self.convert(bytes(container)) + self.assertEqual(result.returncode, 2) + self.assertIn("inner ELF is not 64-bit little-endian", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(output.exists()) + def test_a_container_whose_load_segments_look_encrypted_is_refused(self): random_like = bytes(range(256)) * 4 segments = [(PT_LOAD, 1, 0x1234, 0x0, 0x400, PAGE), (PT_LOAD, 4, 0x1500, 0x1000, 0x400, PAGE)] diff --git a/tools/trace_digest.py b/tools/trace_digest.py index cdfcbcf8b1..6d03d2200c 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -59,6 +59,17 @@ class Trace: entry[4] = any(is_wait(parent[2]) for parent in stack) stack.append(entry) + def select_threads(self, names): + available = self.threads.keys() | self.slices.keys() | self.folded.keys() + unknown = set(names) - {self.name(tid) for tid in available} + if unknown: + raise ValueError("unknown thread: " + ", ".join(sorted(unknown))) + selected = {tid for tid in available if self.name(tid) in names} + self.threads = {tid: name for tid, name in self.threads.items() if tid in selected} + self.slices = {tid: slices for tid, slices in self.slices.items() if tid in selected} + self.folded = {tid: value for tid, value in self.folded.items() if tid in selected} + self.batched = [entry for entry in self.batched if entry[0] in selected] + def name(self, tid): return self.threads.get(tid, f"tid {tid}") @@ -209,6 +220,9 @@ def table_cell(value): def markdown(result): lines = [] + if result.get("thread_filter"): + lines += ["Thread filter: " + ", ".join(table_cell(name) for name in result["thread_filter"]) + + ". Frame bounds, summary and stack samples remain capture-wide.", ""] summary = result["summary"] if summary: frame = summary["frame_ms"] @@ -328,12 +342,16 @@ def main(arguments=None): parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") parser.add_argument("--stacks", help="collapsed-stack file of APS5_SAMPLE_WORKER (.collapsed.txt)") + parser.add_argument("--thread", action="append", default=[], metavar="NAME", + help="include event tracks with this exact thread name; repeat to select several tracks") parser.add_argument("--frames", type=nonnegative_integer, default=5, help="slowest frames to list (default 5)") parser.add_argument("--top", type=nonnegative_integer, default=10, help="entries per list (default 10)") parser.add_argument("--json", action="store_true", help="print the digest as JSON") options = parser.parse_args(arguments) if options.summary and not Path(options.summary).is_file(): parser.error(f"summary file not found: {options.summary}") + if options.thread and options.trace.endswith(".jsonl"): + parser.error("--thread requires an event trace, not interval summaries") if options.trace.endswith(".jsonl"): result = digest_intervals(load_intervals(options.trace)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown_intervals(result)) @@ -342,8 +360,13 @@ def main(arguments=None): stacks = load_stacks(options.stacks) if options.stacks else None except (OSError, ValueError) as error: parser.error(str(error)) - result = digest(load_trace(options.trace), load_summary(options.summary, options.trace), + trace = load_trace(options.trace) + if options.thread: + trace.select_threads(options.thread) + result = digest(trace, load_summary(options.summary, options.trace), stacks, options.frames, options.top) + if options.thread: + result["thread_filter"] = sorted(set(options.thread)) sys.stdout.write(json.dumps(result, indent=1) + "\n" if options.json else markdown(result)) return 0 diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 80e95c7a16..3b7e1731e7 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -108,6 +108,8 @@ def parse_elf(data): if inner + 0x40 > len(data): raise Refused("inner ELF header is truncated") header = bytearray(data[inner:inner + 0x40]) + if header[4] != 2 or header[5] != 1: + raise Refused("inner ELF is not 64-bit little-endian") program_offset = struct.unpack_from("