diff --git a/.github/workflows/pages-progress.yml b/.github/workflows/pages-progress.yml new file mode 100644 index 0000000000..88c1f81d5a --- /dev/null +++ b/.github/workflows/pages-progress.yml @@ -0,0 +1,40 @@ +name: Pages progress + +on: + push: + branches: [main] + paths: + - core/libs/prx/** + - core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp + +permissions: + contents: read + actions: write + pages: read + +concurrency: + group: pages-progress + cancel-in-progress: true + +jobs: + progress: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Deploy Pages if the progress changed + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + python3 tools/progress.py "$RUNNER_TEMP/progress" + site=$(gh api "repos/$GH_REPO/pages" --jq .html_url) + if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" && + cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then + echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY" + else + gh workflow run pages.yml --ref main + echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d789d4de62..29891c5c96 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,6 +3,17 @@ name: Pages on: push: branches: [main] + paths: + - .github/workflows/pages.yml + - CMakeLists.txt + - core/relinker/** + - demo/web/** + - tools/build_web.py + - tools/nid_names.py + - tools/progress.py + - tools/progress_history.py + - tools/rdna_isa.txt + - tools/test_web.py workflow_dispatch: release: types: [published] diff --git a/README.md b/README.md index c63b83abe6..5777022fb4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Tool for automatic executables porting to Linux and Windows. +The relinker-only build runs on macOS, including Apple silicon. + [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. ![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index abce0455b5..e2509c0ad9 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -272,6 +272,7 @@ if(BUILD_TESTING) endif() if(APPLE) add_subdirectory(tests/write_watch) + add_subdirectory(tests/guest_arena) endif() if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 237e9166d9..c5e339bee9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1288,7 +1288,7 @@ struct NetCtlIfStat { std::uint32_t reserved[8]; }; -using NetCtlCallback = void (*)(int, void*); +using NetCtlCallback = void (APS5_VABI *)(int, void*); struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; diff --git a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp index 1a93bdbc22..3fb978fbed 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp @@ -43,7 +43,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicMemGetSize() { } std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() { - return 36; + return 44; } std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 0e8338c3da..351466befa 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -81,6 +81,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_atomic_gds_size_tests EXCLUDE_FROM_ALL tests/AtomicGdsSize.cpp) +target_include_directories(agc_atomic_gds_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_atomic_gds_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_atomic_gds_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_atomic_gds_size_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_atomic_gds_size COMMAND agc_atomic_gds_size_tests) + if(WIN32) + set_tests_properties(agc_atomic_gds_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() configure_windows_unwind(libSceAgc) add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp) diff --git a/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp new file mode 100644 index 0000000000..bbc5416bd3 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp @@ -0,0 +1,25 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include + +extern "C" std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize(); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, + std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, + std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData); + +int main() { + constexpr std::uint32_t expectedSize = 11u * sizeof(std::uint32_t); + if (sceAgcAcbAtomicGdsGetSize() != expectedSize) { + std::fprintf(stderr, "ACB atomic GDS size does not match its 11-dword packet\n"); + return 1; + } + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcAcbAtomicGds_0900(&buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + if (packet != words.data() || buffer.cursor_up != words.data() + expectedSize / sizeof(std::uint32_t)) { + std::fprintf(stderr, "ACB atomic GDS builder wrote a different size than its size accessor\n"); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 561c6b0474..75adcbf3a1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -138,6 +138,23 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcOverlayStage IN ITEMS Vert Frag) + string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) + string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) + set(agcOverlayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PerformanceOverlay.${agcOverlayExtension}) + set(agcOverlayShaderSpv ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}.spv) + set(agcOverlayShaderHeader ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}_spv.h) + add_custom_command( + OUTPUT "${agcOverlayShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcOverlayShaderSpv}" "${agcOverlayShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcOverlayShaderSpv} -DOUTPUT=${agcOverlayShaderHeader} + -DSYMBOL=PERFORMANCE_OVERLAY_${agcOverlaySymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcOverlayShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcOverlayShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -212,6 +229,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -1413,6 +1431,51 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_performance_overlay_statistics_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlayStatistics.cpp +) +target_include_directories(agc_performance_overlay_statistics_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_statistics_tests PRIVATE + libSceAgcDriver + libc +) +set_target_properties(agc_performance_overlay_statistics_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_statistics_tests) + +add_test_executable(agc_performance_overlay_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlay.cpp +) +target_include_directories(agc_performance_overlay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) +set_target_properties(agc_performance_overlay_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_tests) + +if(BUILD_TESTING) + set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests) + add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests) + add_test(NAME agc_performance_overlay_device_selection COMMAND agc_performance_overlay_tests device-selection) + add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu) + set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp ) @@ -1718,8 +1781,10 @@ if(BUILD_TESTING) endforeach() add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + add_test(NAME agc_driver_eop_interrupts COMMAND agc_driver_tests --eop-interrupts) + set_tests_properties(agc_driver_eop_interrupts PROPERTIES TIMEOUT 60) if(WIN32) - set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) @@ -1729,6 +1794,11 @@ if(BUILD_TESTING) endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_selected_records_image_limit COMMAND agc_shader_memory_tests --selected-records-image-limit) + add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit) + add_test(NAME agc_shader_selected_records_direct_image_limit COMMAND agc_shader_memory_tests --selected-records-direct-image-limit) + add_test(NAME agc_shader_selected_records_direct_pair_limit COMMAND agc_shader_memory_tests --selected-records-direct-pair-limit) + set_tests_properties(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 PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_table_snapshot_reuse COMMAND agc_shader_memory_tests --table-snapshot-reuse) @@ -1758,7 +1828,7 @@ if(BUILD_TESTING) add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless 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:$") + 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_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests) if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") @@ -1841,6 +1911,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_image_srgb_sample_copy_forced COMMAND agc_driver_image_srgb_sample_copy_tests) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=copy" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) @@ -1971,14 +2049,22 @@ if(BUILD_TESTING) endif() endforeach() set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_storage_cache_retained_batch COMMAND agc_driver_storage_cache_budget_tests --retained-batch) + set_tests_properties(agc_driver_storage_cache_retained_batch 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_storage_cache_retained_batch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) + set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory 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 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 diff --git a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp index 3cc1ebdff5..2b7bcbe4b0 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp +++ b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp @@ -4,7 +4,7 @@ #include #include "SceTypes.hpp" -void AgcDriverDeliverEopInterrupt(std::uint32_t queue); +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue); extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index 564ec3806d..80774d7216 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -25,17 +25,20 @@ std::vector g_registrations; } -void AgcDriverDeliverEopInterrupt(std::uint32_t queue) { +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { std::vector registrations; { std::lock_guard lock(g_mutex); registrations = g_registrations; } + bool delivered = false; for (const auto& registration : registrations) { if (registration.id == static_cast(queue)) { EqueueTriggerEvent_nid_postfix(registration.eq, static_cast(registration.id), EvfiltGraphicsCore, nullptr); + delivered = true; } } + return delivered; } extern "C" { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index f9af8e57c4..17eabcce01 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -317,6 +317,8 @@ private: FrameTiming* frameTiming(); FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment); void execute(const Submission& submission); + void deliverGuestEopInterrupt(std::uint32_t queue); + void deliverFlipEopInterrupt(std::uint32_t queue); void markCompleted(std::uint64_t serial); static const std::atomic*& workerQueued(); void reapCompletionLabels(); @@ -334,6 +336,9 @@ private: std::atomic flipSerial{0}; std::atomic flipBatchesUnsignaled{0}; + std::mutex eopSelfSignalMutex; + std::unordered_map eopSelfSignalFlips; + alignas(64) std::atomic packetsInFlight{0}; std::atomic packetsDone{0}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp new file mode 100644 index 0000000000..653ff19e76 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -0,0 +1,223 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight }; + +struct OverlaySettings { + bool visible = false; + OverlayCorner corner = OverlayCorner::TopLeft; +}; + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position); + +class PerformanceCounters { +public: + static void AddGpuNanoseconds(std::uint64_t nanoseconds); + static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static void CountPipeline(); + static std::uint64_t GpuNanoseconds(); + static std::uint64_t DriverNanoseconds(); + static std::uint64_t Pipelines(); + static bool GpuEnabled(); + static bool DriverEnabled(); +}; + +class PacketCounters { +public: + struct Totals { + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + }; + + static PacketCounters& ThisThread(); + static Totals Sum(); + + void CountDraw() { draws.store(draws.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + void CountDispatch() { dispatches.store(dispatches.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + +private: + alignas(64) std::atomic draws{0}; + std::atomic dispatches{0}; +}; + +struct OverlayCounters { + std::uint64_t gpuNanoseconds = 0; + std::uint64_t driverNanoseconds = 0; + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + std::uint64_t pipelines = 0; +}; + +class OverlayStatistics { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::size_t FrameHistory = 120; + static constexpr Clock::duration Window = std::chrono::seconds(1); + + struct Range { + double minimum = 0; + double average = 0; + double maximum = 0; + }; + + void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void AddPresent(Clock::time_point at, const OverlayCounters& counters); + void Reset(); + double Fps() const; + double LastFrameMs() const; + std::vector FrameTimes() const; + Range FrameTimeRange() const; + double GpuMsPerFrame() const; + double DriverMsPerFrame() const; + double DrawsPerFrame() const; + double DispatchesPerFrame() const; + std::uint64_t PipelinesInWindow() const; + OverlayCounters Latest() const; + +private: + struct Sample { + Clock::time_point at; + OverlayCounters counters; + }; + + double perFrame(std::uint64_t OverlayCounters::*counter) const; + + std::deque window; + std::optional previous; + std::array frameTimes{}; + std::size_t frameCount = 0; + std::size_t frameNext = 0; +}; + +struct OverlayRect { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t color = 0; +}; + +enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; +inline constexpr std::size_t OverlayColorCount = 3; + +struct OverlayContent { + std::vector above; + std::vector frameTimes; + std::vector below; +}; + +struct OverlayMemory { + std::uint64_t used = 0; + std::uint64_t budget = 0; +}; + +struct OverlayDetails { + std::optional vram; + std::optional ram; + VkExtent2D display{}; + VkExtent2D window{}; +}; + +struct OverlayLayout { + OverlayRect bounds; + std::vector rects; + std::int32_t scale = 1; +}; + +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; +}; + +const std::array& OverlayGlyph(char character); +std::string FormatOverlayValue(double value); +std::string FormatOverlayRate(double value); +std::string FormatOverlayCount(double value); +std::string FormatOverlayGigabytes(std::uint64_t bytes); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details); +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); +std::optional ReadVramBudget(const Graphics::Context& context); +std::optional ReadProcessPrivateBytes(); +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); + +class PerformanceOverlay { +public: + PerformanceOverlay(const Graphics::Context& context, std::size_t slots); + ~PerformanceOverlay(); + PerformanceOverlay(const PerformanceOverlay&) = delete; + PerformanceOverlay& operator=(const PerformanceOverlay&) = delete; + + static bool Visible(); + static void Toggle(); + static OverlayCorner Corner(); + + void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display); + const OverlayStatistics& Statistics() const { return statistics; } + const OverlayDetails& Details() const { return details; } + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); + void Release(std::size_t slot) noexcept; + +private: + struct Slot { + std::unique_ptr rects; + std::size_t capacity = 0; + std::vector> transient; + }; + + VkRenderPass renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout); + VkPipeline pipeline(VkFormat format, VkRenderPass compatible); + void releaseTransient(Slot& slot) noexcept; + void release() noexcept; + void restartIfToggled(); + + Graphics::Context context; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::map, VkRenderPass> renderPasses; + std::map pipelines; + std::vector slots; + OverlayStatistics statistics; + OverlayDetails details; + std::optional sampledAt; + std::uint64_t toggles = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 3f771fbb1a..3cdc977947 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -29,6 +29,7 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix(); extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error); #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 5a5ff651f3..b5928fd583 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -13,10 +13,13 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "ShaderDiskCache.hpp" +#include "prx/libc/include/GuestArena.hpp" #include #include #include +#include #include +#include #include #include #ifdef _WIN32 @@ -26,6 +29,7 @@ #include #else #include +#include #endif namespace AgcDriver::DriverDetail { @@ -48,6 +52,45 @@ void PrintFailure(std::exception_ptr error) { std::fflush(stderr); } +constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull; +constexpr std::size_t DriverMemoryBytes = 2u << 20u; +constexpr std::uint64_t FsTableAddress = 0xfe0040000ull; +constexpr std::uint64_t FsTableSlotBytes = 16u; +constexpr std::uint64_t FsTableSlots = 6u; +constexpr std::uint64_t FsRoutineRecords = 8u; +constexpr std::uint64_t FsRoutineRecordBytes = 16u; +constexpr std::uint64_t FsRoutineTableBytes = FsRoutineRecords * FsRoutineRecordBytes; +constexpr std::uint64_t FsRoutineTableAddress = FsTableAddress + FsTableSlots * FsTableSlotBytes; +constexpr std::uint64_t FsRoutineAddress = FsRoutineTableAddress + FsRoutineTableBytes; +constexpr std::uint32_t ScalarSetpcB64LinkS14 = 0xbe80200eu; +constexpr std::uint32_t ScalarEndProgram = 0xbf810000u; +constexpr std::uint64_t FsTableDword3 = 0x5204u; + +void MapDriverMemory() { + void* mapped; +#ifdef _WIN32 + mapped = reinterpret_cast(DriverMemoryAddress); + GuestArena::GuestArenaCommit_nid_postfix(mapped, DriverMemoryBytes, PAGE_READWRITE, DriverMemoryBytes); +#else + mapped = mmap(reinterpret_cast(DriverMemoryAddress), DriverMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map the AGC driver memory"); +#endif + if (reinterpret_cast(mapped) != DriverMemoryAddress) throw std::runtime_error("the AGC driver memory was not mapped at its fixed address"); + auto* routineTable = reinterpret_cast(FsRoutineTableAddress); + for (std::uint64_t record = 0; record < FsRoutineRecords; ++record) { + routineTable[record * 2u] = FsRoutineAddress; + routineTable[record * 2u + 1u] = 0u; + } + auto* routine = reinterpret_cast(FsRoutineAddress); + routine[0] = ScalarSetpcB64LinkS14; + routine[1] = ScalarEndProgram; + auto* slot = reinterpret_cast(FsTableAddress); + slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u; + slot[1] = FsTableDword3 << 32u | FsRoutineRecords; + GuestAllocations::Mutation mutation; + mutation.Add(mapped, DriverMemoryBytes, true, true, true); +} + bool StackHoldsImport() { #ifdef _WIN32 ULONG_PTR low = 0; @@ -137,6 +180,8 @@ void Driver::stop() { Driver::Driver() { InstallAsyncLog_nid_no_patch(); + static const bool driverMemory = (MapDriverMemory(), true); + static_cast(driverMemory); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 1c35594004..0597878160 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -7,6 +7,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" @@ -18,20 +19,38 @@ namespace AgcDriver::DriverDetail { namespace { +constexpr std::uint32_t eopSelfSignalFlipLimit = 4; + bool orderedFills() { static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; return ordered; } +bool pipelineDma() { + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + return enabled; +} + bool keepsDrawPipeline(std::span packet, std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; + case 0x11: case 0x12: case 0x13: case 0x20: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u; case 0x15: return orderedFills(); case 0x63: case 0x64: case 0x9f: return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x50: { + if (!pipelineDma() || packet.size() < 7) return false; + const auto destination = Pm4::DmaDestination(packet); + if (destination == Pm4::DmaSelectGds || destination == Pm4::DmaDestinationNowhere) return true; + const auto bytes = Pm4::DmaBytes(packet); + if (bytes == 0) return true; + const auto source = Pm4::DmaSource(packet); + if ((source == 0 || source == 3) && DrawPipeline::Queue0().Overlaps(packet[2] | (static_cast(packet[3]) << 32u), bytes)) return false; + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + return !DrawPipeline::Queue0().Overlaps(address, bytes); + } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; @@ -42,6 +61,24 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head } +void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { + if (!AgcDriverDeliverEopInterrupt(queue)) return; + std::lock_guard lock(eopSelfSignalMutex); + eopSelfSignalFlips[queue] = 0; +} + +void Driver::deliverFlipEopInterrupt(std::uint32_t queue) { + { + std::lock_guard lock(eopSelfSignalMutex); + const auto latched = eopSelfSignalFlips.find(queue); + if (latched != eopSelfSignalFlips.end()) { + if (++latched->second <= eopSelfSignalFlipLimit) return; + eopSelfSignalFlips.erase(latched); + } + } + AgcDriverDeliverEopInterrupt(queue); +} + template void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { static thread_local WorkerProfile profile; @@ -54,6 +91,7 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); } + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); } void Driver::execute(const Submission& submission) { @@ -149,6 +187,7 @@ void Driver::execute(const Submission& submission) { thread_local PacketProfile* packetProfileSlot = nullptr; auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot); ++packetProfile.submissions; + auto& packetCounters = PacketCounters::ThisThread(); static const bool epochEach = std::getenv("APS5_SUBMISSION_EPOCH_EACH") != nullptr; if (epochEach || submission.enqueuedAt == std::chrono::steady_clock::time_point{} || submission.enqueuedAt > lastEpochBump()) { @@ -273,7 +312,7 @@ void Driver::execute(const Submission& submission) { Pm4::Execute(packet, queue); if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } - if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); } if (deviceUse.owns_lock()) deviceUse.unlock(); CheckFailure(); @@ -289,10 +328,10 @@ void Driver::execute(const Submission& submission) { if (localDevice != nullptr) { localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); const auto queueId = submission.queue; - flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false); + flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false); } } - if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue); ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; @@ -310,6 +349,7 @@ void Driver::execute(const Submission& submission) { flipHold.Release(); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15 || opcode == 0x16) { + packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; if (gateOpened()) { @@ -343,6 +383,7 @@ void Driver::execute(const Submission& submission) { } } else if (drawPacket) { landPendingWaits(submission.queue); + packetCounters.CountDraw(); bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; @@ -393,7 +434,7 @@ void Driver::execute(const Submission& submission) { if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } } - if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); } if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork(); timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 452c674ab3..bb8a031b38 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -54,7 +54,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } if (reason == 0 || reason == 5) { const auto queueId = submission.queue; - interruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, submission.queue == 0); + interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0); if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0); wroteOnGpu = true; } else if (reason == 1) { @@ -272,6 +272,21 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); constexpr std::size_t immediateLimit = std::size_t{16} << 20u; + constexpr std::size_t gpuStoreLimit = 65536; + if (source == 2 && Pm4::DmaBytes(packet) > gpuStoreLimit && Pm4::DmaDestination(packet) != Pm4::DmaSelectGds) { + const auto fillAddress = packet[4] | (static_cast(packet[5]) << 32u); + const auto fillBytes = Pm4::DmaBytes(packet); + const auto pattern = packet[2]; + if (fillAddress % 16 != 0 || fillBytes % 16 != 0 || !GuestMemory::Accessible(reinterpret_cast(fillAddress), fillBytes, true) || !device.Load()) return false; + DrawPipeline::Queue0().Enqueue([this, queue, fillAddress, fillBytes, pattern] { + GuestMemory::SetCurrentPacket(0x50, queue); + if (const auto localDevice = device.Load()) { + storeBufferFill(queue, fillAddress, fillBytes, {pattern, pattern, pattern, pattern}, localDevice, false, false); + ++dmaFills; + } + }, {{fillAddress, fillAddress + fillBytes}}); + return true; + } if (source == 2) { const auto store = Pm4::ResolveStore(packet, state, immediateLimit); if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast(store->address), store->Bytes().size(), true)) return false; @@ -305,6 +320,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 recordDeferredLabels(localDevice.get(), queue); const auto outcome = localDevice->CopyBuffer(copy.destination, copy.source, copy.bytes, 0, std::numeric_limits::max(), 0, 0, queue, [](std::span, std::uint64_t) {}); if (outcome.path == 1 || outcome.path == 3) return; + if (outcome.path == 2 && !Graphics::RegisteredReadableCovers(copy.destination, copy.bytes) && !localDevice->StoresPendingOver(copy.destination, copy.bytes)) ++copiesToHostMemory; } } std::vector bytes(copy.bytes); @@ -397,10 +413,10 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanAfterRecordedWork([queue] { AgcDriverDeliverEopInterrupt(queue); }, queue == 0); + deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0); if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); } - if (!deferred) AgcDriverDeliverEopInterrupt(queue); + if (!deferred) deliverGuestEopInterrupt(queue); } submitDueAfterCommit(localDevice.get()); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index dcfaf75142..76f71dc937 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -652,7 +652,7 @@ std::vector ReadTableCallees(const ShaderRecompil constexpr std::uint64_t limitBytes = 64u << 20u; constexpr std::size_t limitCallees = 64u; const auto slot = call.userDataRegister - request.context.userDataBaseRegister; - const auto pointer = static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; + const auto pointer = call.constantPointer ? *call.constantPointer : static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor"); ShaderRecompiler::ShaderBufferResource descriptor{}; std::memcpy(descriptor.fields.data(), words.data(), words.size()); @@ -901,7 +901,13 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } else { const auto routing = RegisterValue(state.context, 0x2d5); wave = (routing & 0x00400000u) != 0 ? 32u : 64u; - if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr; + const bool unroutedBarrier = routing == 0 && std::ranges::any_of(decoded.instructions, [](const auto& instruction) { + return instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier; + }); + if (registration && stage == Stage::Vertex && (((routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) || unroutedBarrier)) { + if (unroutedBarrier && deferred != nullptr) return defer("its s_barrier depends on draw routing"); + return nullptr; + } if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) { if (registration) return {}; auto stageState = state; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp new file mode 100644 index 0000000000..e21ddf0599 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -0,0 +1,791 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif + +namespace AgcDriver { +namespace { + +static_assert(sizeof(OverlayRect) == 20, "overlay rectangles are read as one ivec4 and one uint per instance"); + +struct OverlayParameters { + std::array, OverlayColorCount> palette; + std::array extent; +}; + +std::atomic toggleCount{0}; +std::atomic gpuTotal{0}; +std::atomic driverTotal{0}; +std::atomic pipelineTotal{0}; + +struct PacketCounterRegistry { + std::mutex mutex; + std::deque counters; + std::vector unused; +}; + +PacketCounterRegistry& packetCounterRegistry() { + static auto* registry = new PacketCounterRegistry; + return *registry; +} + +struct PacketCounterLease { + PacketCounters* counters = nullptr; + ~PacketCounterLease() { + if (counters == nullptr) return; + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + registry.unused.push_back(counters); + } +}; + +constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0; + +const OverlaySettings& settings() { + static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + return parsed; +} + +std::int32_t panelWidth(std::int32_t scale) { + return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +} + +std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { + const auto lines = static_cast(content.above.size() + content.below.size()); + const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; +} + +bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { + const auto margins = 2 * OverlayMetrics::Margin * scale; + return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); +} + +float srgbDecode(float value) { + return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f); +} + +float pqEncode(float nits) { + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + const double power = std::pow(std::clamp(static_cast(nits) / 10000.0, 0.0, 1.0), m1); + return static_cast(std::pow((c1 + c2 * power) / (1.0 + c3 * power), m2)); +} + +bool srgbFormat(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_A8B8G8R8_SRGB_PACK32; +} + +} + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) { + OverlaySettings parsed; + const std::string_view visible = enabled != nullptr ? enabled : ""; + if (visible == "1") parsed.visible = true; + else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'"); + const std::string_view corner = position != nullptr ? position : ""; + if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft; + else if (corner == "tr") parsed.corner = OverlayCorner::TopRight; + else if (corner == "bl") parsed.corner = OverlayCorner::BottomLeft; + else if (corner == "br") parsed.corner = OverlayCorner::BottomRight; + else throw std::runtime_error("ANYPS5_OVERLAY_POS must be tl, tr, bl or br, not '" + std::string(corner) + "'"); + return parsed; +} + +void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) { + gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { + driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::CountPipeline() { + pipelineTotal.fetch_add(1, std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::GpuNanoseconds() { + return gpuTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::DriverNanoseconds() { + return driverTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::Pipelines() { + return pipelineTotal.load(std::memory_order_relaxed); +} + +PacketCounters& PacketCounters::ThisThread() { + auto& lease = HostThreadLocal(); + if (lease.counters == nullptr) { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.counters.emplace_back(); + } else { + lease.counters = registry.unused.back(); + registry.unused.pop_back(); + } + } + return *lease.counters; +} + +PacketCounters::Totals PacketCounters::Sum() { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + Totals totals; + for (const auto& counters : registry.counters) { + totals.draws += counters.draws.load(std::memory_order_relaxed); + totals.dispatches += counters.dispatches.load(std::memory_order_relaxed); + } + return totals; +} + +bool PerformanceCounters::GpuEnabled() { + return Graphics::Recorder::GpuTimingEnabled(); +} + +bool PerformanceCounters::DriverEnabled() { + static const bool enabled = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return enabled; +} + +void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + AddPresent(at, OverlayCounters{gpuNanoseconds, driverNanoseconds}); +} + +void OverlayStatistics::AddPresent(Clock::time_point at, const OverlayCounters& counters) { + if (!window.empty()) { + Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); + frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); + frameNext = (frameNext + 1) % FrameHistory; + frameCount = std::min(frameCount + 1, FrameHistory); + previous = window.back(); + } + window.push_back({at, counters}); + while (at - window.front().at > Window) window.pop_front(); +} + +void OverlayStatistics::Reset() { + window.clear(); + previous.reset(); + frameTimes = {}; + frameCount = 0; + frameNext = 0; +} + +double OverlayStatistics::Fps() const { + if (window.size() < 2) { + const auto frameMs = LastFrameMs(); + return frameMs > 0 ? 1000.0 / frameMs : 0; + } + const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); + return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; +} + +double OverlayStatistics::LastFrameMs() const { + return frameCount == 0 ? 0 : frameTimes[(frameNext + FrameHistory - 1) % FrameHistory]; +} + +std::vector OverlayStatistics::FrameTimes() const { + std::vector ordered; + ordered.reserve(frameCount); + for (std::size_t i = 0; i < frameCount; ++i) ordered.push_back(frameTimes[(frameNext + FrameHistory - frameCount + i) % FrameHistory]); + return ordered; +} + +OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { + const auto times = FrameTimes(); + if (times.empty()) return {}; + Range range{times.front(), 0, times.front()}; + double sum = 0; + for (const auto time : times) { + range.minimum = std::min(range.minimum, time); + range.maximum = std::max(range.maximum, time); + sum += time; + } + range.average = sum / static_cast(times.size()); + return range; +} + +double OverlayStatistics::perFrame(std::uint64_t OverlayCounters::*counter) const { + const bool windowed = window.size() >= 2; + if (!windowed && !previous) return 0; + const auto first = windowed ? window.front().counters.*counter : previous->counters.*counter; + const auto last = window.back().counters.*counter; + const auto frames = windowed ? window.size() - 1 : std::size_t{1}; + return last >= first ? static_cast(last - first) / static_cast(frames) : 0; +} + +double OverlayStatistics::GpuMsPerFrame() const { + return perFrame(&OverlayCounters::gpuNanoseconds) / 1e6; +} + +double OverlayStatistics::DriverMsPerFrame() const { + return perFrame(&OverlayCounters::driverNanoseconds) / 1e6; +} + +double OverlayStatistics::DrawsPerFrame() const { + return perFrame(&OverlayCounters::draws); +} + +double OverlayStatistics::DispatchesPerFrame() const { + return perFrame(&OverlayCounters::dispatches); +} + +std::uint64_t OverlayStatistics::PipelinesInWindow() const { + if (window.empty()) return 0; + const auto first = window.size() < 2 && previous ? previous->counters.pipelines : window.front().counters.pipelines; + const auto last = window.back().counters.pipelines; + return last >= first ? last - first : 0; +} + +OverlayCounters OverlayStatistics::Latest() const { + return window.empty() ? OverlayCounters{} : window.back().counters; +} + +const std::array& OverlayGlyph(char character) { + using Glyph = std::array; + static constexpr Glyph space{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array digits{{ + {0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e}, + {0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e}, + {0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f}, + {0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e}, + {0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02}, + {0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e}, + {0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e}, + {0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, + {0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e}, + {0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c}, + }}; + static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; + static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; + static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; + static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; + static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; + static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; + static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; + static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; + static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; + static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; + static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; + static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; + static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; + static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; + switch (character) { + case ' ': return space; + case '.': return period; + case ':': return colon; + case '/': return slash; + case 'A': return a; + case 'B': return b; + case 'C': return c; + case 'D': return d; + case 'E': return e; + case 'F': return f; + case 'G': return g; + case 'H': return h; + case 'I': return i; + case 'K': return k; + case 'L': return l; + case 'M': return m; + case 'N': return n; + case 'O': return o; + case 'P': return p; + case 'R': return r; + case 'S': return s; + case 'T': return t; + case 'U': return u; + case 'V': return v; + case 'W': return w; + case 'X': return x; + default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); + } +} + +std::string FormatOverlayValue(double value) { + const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; + char text[16]; + std::snprintf(text, sizeof(text), clamped < 99.95 ? "%.1f" : "%.0f", clamped); + return text; +} + +std::string FormatOverlayRate(double value) { + if (!std::isfinite(value) || value <= 0 || value >= 0.995) return FormatOverlayValue(value); + char text[16]; + std::snprintf(text, sizeof(text), "%.2f", value); + return text; +} + +std::string FormatOverlayCount(double value) { + const long long rounded = std::isfinite(value) && value > 0 ? std::llround(std::min(value, 1e12)) : 0; + char text[24]; + if (rounded < 10000) std::snprintf(text, sizeof(text), "%lld", rounded); + else if (rounded < 1000000) std::snprintf(text, sizeof(text), "%lldK", rounded / 1000); + else std::snprintf(text, sizeof(text), "%lldM", std::min(rounded / 1000000, 999)); + return text; +} + +std::string FormatOverlayGigabytes(std::uint64_t bytes) { + return FormatOverlayValue(static_cast(bytes) / GigabyteBytes); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + return ComposeOverlay(statistics, gpu, driver, OverlayDetails{}); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { + OverlayContent content; + if (gpu || driver) content.above.push_back("PROFILED"); + content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); + content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); + content.frameTimes = statistics.FrameTimes(); + const auto range = statistics.FrameTimeRange(); + content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); + if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); + if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + const auto latest = statistics.Latest(); + if (latest.draws != 0 || latest.dispatches != 0) content.below.push_back("DRAWS: " + FormatOverlayCount(statistics.DrawsPerFrame()) + " DISPATCH: " + FormatOverlayCount(statistics.DispatchesPerFrame())); + if (latest.pipelines != 0) content.below.push_back("SHADERS: " + FormatOverlayCount(static_cast(statistics.PipelinesInWindow())) + " TOTAL: " + FormatOverlayCount(static_cast(latest.pipelines))); + if (details.vram) content.below.push_back("VRAM: " + FormatOverlayGigabytes(details.vram->used) + "/" + FormatOverlayGigabytes(details.vram->budget) + " GB"); + if (details.ram) content.below.push_back("RAM: " + FormatOverlayGigabytes(*details.ram) + " GB"); + if (details.display.width != 0 && details.display.height != 0 && details.window.width != 0 && details.window.height != 0) { + const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; + for (const std::string_view separator : {" / ", "/"}) { + auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); + if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + content.below.push_back(std::move(line)); + break; + } + } + return content; +} + +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now) { + return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); +} + +std::optional ReadVramBudget(const Graphics::Context& context) { + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return std::nullopt; + const auto heap = Graphics::LargestDeviceLocalHeap(context.memory); + if (!heap) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + context.memoryProperties2(context.physical, &properties); + if (reported.heapBudget[*heap] == 0) return std::nullopt; + return OverlayMemory{reported.heapUsage[*heap], reported.heapBudget[*heap]}; +} + +std::optional ReadProcessPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PrivateUsage); +#else + std::FILE* status = std::fopen("/proc/self/status", "r"); + if (status == nullptr) return std::nullopt; + std::optional anonymousKb; + std::uint64_t swapKb = 0; + char line[256]; + while (std::fgets(line, sizeof(line), status) != nullptr) { + unsigned long long value = 0; + if (std::sscanf(line, "RssAnon: %llu kB", &value) == 1) anonymousKb = value; + else if (std::sscanf(line, "VmSwap: %llu kB", &value) == 1) swapKb = value; + } + std::fclose(status); + if (!anonymousKb) return std::nullopt; + return (*anonymousKb + swapKb) * 1024u; +#endif +} + +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { + auto scale = std::max(1, static_cast((extent.height + 240) / 480)); + while (scale > 1 && !fits(extent, content, scale)) --scale; + return scale; +} + +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { + Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); + Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + OverlayLayout layout; + const auto scale = OverlayScale(extent, content); + layout.scale = scale; + const auto width = panelWidth(scale); + const auto height = panelHeight(content, scale); + const auto margin = OverlayMetrics::Margin * scale; + const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; + const bool bottom = corner == OverlayCorner::BottomLeft || corner == OverlayCorner::BottomRight; + const auto left = std::max(0, right ? static_cast(extent.width) - margin - width : margin); + const auto top = std::max(0, bottom ? static_cast(extent.height) - margin - height : margin); + layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; + layout.rects.push_back(layout.bounds); + const auto text = [&](const std::string& line, std::int32_t row) { + Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = OverlayGlyph(line[column]); + const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; + for (std::int32_t y = 0; y < OverlayMetrics::GlyphHeight; ++y) { + const auto bits = glyph[static_cast(y)]; + std::int32_t x = 0; + while (x < OverlayMetrics::GlyphWidth) { + if ((bits & (0x10u >> x)) == 0) { + ++x; + continue; + } + auto end = x; + while (end < OverlayMetrics::GlyphWidth && (bits & (0x10u >> end)) != 0) ++end; + layout.rects.push_back({glyphLeft + x * scale, top + (row + y) * scale, (end - x) * scale, scale, static_cast(OverlayColor::Text)}); + x = end; + } + } + } + }; + auto row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; + if (!content.frameTimes.empty()) { + double maximum = 0; + double sum = 0; + for (const auto time : content.frameTimes) { + maximum = std::max(maximum, time); + sum += time; + } + const double fullScale = std::max(maximum, 2.0 * sum / static_cast(content.frameTimes.size())); + const auto first = static_cast(OverlayStatistics::FrameHistory - content.frameTimes.size()); + for (std::size_t i = 0; i < content.frameTimes.size() && fullScale > 0; ++i) { + const auto pixels = std::clamp(static_cast(std::lround(content.frameTimes[i] / fullScale * OverlayMetrics::GraphHeight * scale)), 1, OverlayMetrics::GraphHeight * scale); + layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); + } + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + return layout; +} + +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace) { + std::array, OverlayColorCount> palette{{{0.0f, 0.0f, 0.0f, 0.6f}, {1.0f, 1.0f, 1.0f, 1.0f}, {0.35f, 0.8f, 0.35f, 1.0f}}}; + if (colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + for (auto& color : palette) { + const float red = srgbDecode(color[0]); + const float green = srgbDecode(color[1]); + const float blue = srgbDecode(color[2]); + const std::array bt2020{ + 0.6274f * red + 0.3293f * green + 0.0433f * blue, + 0.0691f * red + 0.9195f * green + 0.0114f * blue, + 0.0164f * red + 0.0880f * green + 0.8956f * blue}; + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = pqEncode(bt2020[channel] * PqReferenceWhiteNits); + } + } else if (srgbFormat(format)) { + for (auto& color : palette) { + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = srgbDecode(color[channel]); + } + } + return palette; +} + +PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "the performance overlay needs at least one slot"); + try { + const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(OverlayParameters)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + 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"); + } catch (...) { + release(); + throw; + } +} + +PerformanceOverlay::~PerformanceOverlay() { + release(); +} + +void PerformanceOverlay::releaseTransient(Slot& slot) noexcept { + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); +} + +void PerformanceOverlay::Release(std::size_t index) noexcept { + if (index < slots.size()) releaseTransient(slots[index]); +} + +void PerformanceOverlay::release() noexcept { + for (auto& slot : slots) { + releaseTransient(slot); + slot.rects.reset(); + slot.capacity = 0; + } + for (const auto& [format, entry] : pipelines) context.Function("vkDestroyPipeline")(context.device, entry, nullptr); + pipelines.clear(); + for (const auto& [key, entry] : renderPasses) context.Function("vkDestroyRenderPass")(context.device, entry, nullptr); + renderPasses.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + fragment = vertex = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; +} + +bool PerformanceOverlay::Visible() { + return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0); +} + +void PerformanceOverlay::Toggle() { + toggleCount.fetch_add(1, std::memory_order_relaxed); +} + +OverlayCorner PerformanceOverlay::Corner() { + return settings().corner; +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) { + restartIfToggled(); + if (OverlaySampleDue(sampledAt, at)) { + details.vram = ReadVramBudget(context); + details.ram = ReadProcessPrivateBytes(); + sampledAt = at; + } + details.display = display; + const auto packets = PacketCounters::Sum(); + statistics.AddPresent(at, OverlayCounters{PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0, packets.draws, packets.dispatches, PerformanceCounters::Pipelines()}); +} + +void PerformanceOverlay::restartIfToggled() { + const auto current = toggleCount.load(std::memory_order_relaxed); + if (current == toggles) return; + statistics.Reset(); + details = {}; + sampledAt.reset(); + toggles = current; +} + +VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { + const auto key = std::tuple{format, initialLayout, finalLayout}; + if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "the performance overlay cannot blend into this format"); + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = initialLayout; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &created), "vkCreateRenderPass performance overlay"); + return renderPasses.emplace(key, created).first->second; +} + +VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible) { + if (const auto found = pipelines.find(format); found != pipelines.end()) return found->second; + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + const VkVertexInputBindingDescription binding{0, sizeof(OverlayRect), VK_VERTEX_INPUT_RATE_INSTANCE}; + const VkVertexInputAttributeDescription attributes[2] = {{0, 0, VK_FORMAT_R32G32B32A32_SINT, 0}, {1, 0, VK_FORMAT_R32_UINT, 16}}; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = 2; + vertexInput.pVertexAttributeDescriptions = attributes; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewport; + info.pRasterizationState = &rasterization; + info.pMultisampleState = &multisample; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = pipelineLayout; + info.renderPass = compatible; + VkPipeline created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &info, nullptr, &created), "vkCreateGraphicsPipelines performance overlay"); + return pipelines.emplace(format, created).first->second; +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + auto shown = details; + shown.window = extent; + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner()); +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { + Graphics::Require(index < slots.size(), "performance overlay slot is out of range"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "the performance overlay destination is unavailable"); + auto& slot = slots[index]; + releaseTransient(slot); + const auto layout = LayOutOverlay(content, extent, corner); + if (slot.capacity < layout.rects.size()) { + const auto capacity = std::max(layout.rects.size(), 1024); + slot.rects = std::make_unique(context, capacity * sizeof(OverlayRect), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + slot.capacity = capacity; + } + std::memcpy(slot.rects->Bytes().data(), layout.rects.data(), layout.rects.size() * sizeof(OverlayRect)); + const auto pass = renderPass(format, initialLayout, finalLayout); + const auto drawing = pipeline(format, pass); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = destination; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + for (const auto layout : {initialLayout, finalLayout}) { + if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + viewInfo.pNext = &usageInfo; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + const auto created = context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer); + slot.transient.emplace_back(view, framebuffer); + Graphics::Check(created, "vkCreateFramebuffer performance overlay"); + const auto right = std::min(layout.bounds.x + layout.bounds.width, static_cast(extent.width)); + const auto bottom = std::min(layout.bounds.y + layout.bounds.height, static_cast(extent.height)); + const VkRect2D area{{layout.bounds.x, layout.bounds.y}, {static_cast(std::max(1, right - layout.bounds.x)), static_cast(std::max(1, bottom - layout.bounds.y))}}; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = area; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, drawing); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &area); + const OverlayParameters parameters{OverlayPalette(format, colorSpace), {static_cast(extent.width), static_cast(extent.height)}}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), ¶meters); + const VkBuffer buffer = slot.rects->Handle(); + const VkDeviceSize offset = 0; + context.Function("vkCmdBindVertexBuffers")(commands, 0, 1, &buffer, &offset); + context.Function("vkCmdDraw")(commands, 4, static_cast(layout.rects.size()), 0, 0); + context.Function("vkCmdEndRenderPass")(commands); + return layout.bounds; +} + +} + +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix() { + AgcDriver::PerformanceOverlay::Toggle(); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 2a8ef54378..4a7d055706 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -378,9 +378,11 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0x7fffffffu) == 0 && (packet[2] & 0x7fffffffu) == 0, "CONTEXT_CONTROL register loading and shadowing are not implemented"); break; case 0x46: { - require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); const auto eventType = packet[1] & 0x3fu; - const auto eventIndex = (packet[1] >> 8u) & 7u; + const auto eventIndex = (packet[1] >> 8u) & 0xfu; + const auto streamOutStatistics = (eventType >= 0x01u && eventType <= 0x03u) || eventType == 0x20u; + require(!streamOutStatistics || (eventIndex != 3 && (eventIndex < 8 || eventIndex > 11)), "stream-out statistics sampling to memory is not implemented"); + require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); switch (eventType) { case 0x07: case 0x0f: case 0x10: size(2); @@ -401,6 +403,11 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0, "invalid VGT_FLUSH event index"); break; + case 0x01: case 0x02: case 0x03: case 0x20: + graphics(); + size(2); + require(eventIndex == 0, "invalid stream-out statistics event index"); + break; case 0x39: graphics(); size(4); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 470c6c6f5c..6373b4a139 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -19,6 +19,7 @@ #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -118,6 +119,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, info.layout = layout; auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); + PerformanceCounters::CountPipeline(); } bool CaptureComputePipeline(std::size_t words) { @@ -324,6 +326,7 @@ struct VulkanDevice::State { bool pipelineExecutableInfo = false; bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool storageImageWithoutFormat = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; @@ -407,6 +410,7 @@ struct VulkanDevice::State { VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; std::unique_ptr presentation; + std::unique_ptr overlay; VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; bool swapchainColorSpaces = false; @@ -766,6 +770,7 @@ struct VulkanDevice::State { textureCache.reset(); detiler.reset(); colorTransfer.reset(); + overlay.reset(); presentation.reset(); scaler.reset(); hdrScaler.reset(); @@ -1111,6 +1116,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->properties = properties.properties; state->physical = selected; state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected); + state->srgbCopyFormats = Graphics::SrgbCopyFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected, state->srgbDecodeFormats); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) { state->capabilities.push_back(spv::CapabilityGroupNonUniform); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot); @@ -2110,6 +2116,7 @@ void VulkanDevice::StoreIdleResults(bool reapFirst) { if (!Graphics::StorageTexture::AnyPending()) return; GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); Graphics::StorageTexture::FlushAllPending("idle"); SubmitRecorded(reapFirst); } @@ -3169,6 +3176,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; require(!slot.inFlight, "presentation slot is still in flight"); + const bool overlay = PerformanceOverlay::Visible(); if (display != nullptr && resident == nullptr) { static_cast(DisplayBufferSize(*display)); state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); @@ -3260,7 +3268,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (drawn) { state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer); - state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); @@ -3298,7 +3306,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - if (!drawn) { + if (overlay) { + if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); + state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}); + state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; @@ -3508,7 +3520,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; - target.srgbDecodeFormats = state->srgbDecodeFormats; + target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { const auto& mesh = state->meshLimits; @@ -3649,6 +3661,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; @@ -4556,6 +4569,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); recorder.Keep(record.objects); + if (record.resources != nullptr) record.resources->KeepStorageImageBytes(recorder); recorder.Keep(record.resources); if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes()); if (record.bindings != nullptr) { @@ -4863,7 +4877,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec static const bool deferEviction = std::getenv("APS5_NO_DEFERRED_EVICTION") == nullptr; std::vector> evicted; state->resourceCache.Insert(contentKey, resources, deferEviction ? &evicted : nullptr); - for (auto& object : evicted) state->recorder->Keep(std::move(object)); + for (auto& object : evicted) { + object->KeepStorageImageBytes(*state->recorder); + state->recorder->Keep(std::move(object)); + } if (profile) { timer.add(PhaseResourcesInsert, std::chrono::duration(std::chrono::steady_clock::now() - insertStart).count()); if (arguments != 0) d.indirectHold.phaseCounts[PhaseResourcesInsert] += evicted.size(); @@ -4961,6 +4978,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec state->resourceCache.Remove(contentKey, cached.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry (insert()). + cached->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(cached)); } if (profile) { @@ -5139,6 +5157,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu // ordinary path runs with the prepared find (C10: the template goes to the batch). if (verify->cached.get() != hit->resources.get()) { counters.verifyReplaced.fetch_add(1, std::memory_order_relaxed); + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); return RecipeOutcome::Rebuild; } @@ -5175,6 +5194,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu state->resourceCache.Remove(recipe.key, hit->resources.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry. + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); counters.rebuilds[static_cast(report.failure)].fetch_add(1, std::memory_order_relaxed); return RecipeOutcome::Rebuild; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 8eaada9e84..d5f3cd7fee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -229,6 +229,7 @@ struct Context { bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp index 7e9a4dc18b..71575f622a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp @@ -5,6 +5,7 @@ #include "RuntimeAbi.hpp" #include #include +#include namespace AgcDriver::Graphics { @@ -51,6 +52,12 @@ constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) { samples[dimension]}; } +constexpr std::optional PlaceholderImageShapeForDevice(std::uint32_t heap, bool storageImageMultisample) { + const auto shape = PlaceholderImageShapeOf(heap); + if (!storageImageMultisample && (shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT) return std::nullopt; + return shape; +} + class PlaceholderImages { public: explicit PlaceholderImages(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 30e547aaa6..f62ba12556 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -102,10 +102,17 @@ public: static constexpr std::size_t UnwrittenSnapshotEntries = 4096; static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawInputEntries = 16384; - std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp); - void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer); + std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset = nullptr); + void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset = 0); std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); + static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u; + struct UploadSlice { + std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::byte* bytes = nullptr; + }; + UploadSlice DrawUpload(std::size_t bytes); static constexpr std::size_t IndexRangeEntries = 16384; static constexpr std::size_t KeptIndexRangeIndices = 4096; bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest); @@ -812,6 +819,8 @@ private: std::uint64_t submissions = 0; std::size_t inFlightKeptBytes = 0; std::uint64_t writeNoteCount = 0; + std::shared_ptr drawUpload; + VkDeviceSize drawUploadUsed = 0; // Command buffers and fences of completed batches, reused by later ones (hundreds of batches per // frame would otherwise allocate and free their objects each time). std::vector> spare; @@ -836,6 +845,7 @@ private: struct UnwrittenSnapshot { std::size_t bytes; std::shared_ptr buffer; + VkDeviceSize offset = 0; }; std::unordered_map unwrittenSnapshots; std::array unwrittenStamp{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 94624adb85..6567db4c09 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -34,6 +34,7 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureCacheBudget(const Context& context); struct TextureCacheUse { std::size_t sampledEntries = 0; @@ -181,6 +182,9 @@ public: struct Snapshot { std::uint64_t address; std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::size_t bytes = 0; + std::span Bytes() const { return buffer->Bytes().subspan(static_cast(offset), bytes); } }; DescriptorCache* cache = nullptr; VkDescriptorSetLayout layout = VK_NULL_HANDLE; @@ -207,6 +211,7 @@ public: static const char* MoveRefusalName(MoveRefusal refusal); std::optional> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const; static std::size_t MaxDrawSnapshotBytes(); + static std::size_t DrawUploadLimit(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs @@ -233,6 +238,7 @@ public: std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; + bool HoldsReleasedTexture() const; std::uint32_t TableElements() const { return tableElements[0] + tableElements[1]; } const std::vector& LayoutKey() const { return layoutKey; } const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } @@ -261,6 +267,7 @@ public: // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + void KeepStorageImageBytes(Recorder& recorder) const; const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); static std::uint64_t DepthGuardRefusals(); @@ -692,6 +699,7 @@ public: private: void erase(const Key& key); void noteMiss(const Key& key); + void dropReleasedTextures(std::vector>* evicted); void noteEvicted(const Key& key); void noteReturn(const Key& key); void noteUse(const Key& key); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 344d5ee592..6a5b97e7f2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -66,6 +66,9 @@ public: Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); + struct SrgbExpandTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag); + static bool CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); struct BlockCopyTag {}; @@ -113,10 +116,13 @@ public: PerUseCopy CopyKind() const { return perUseCopy; } bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } + bool Released() const { return released.load(std::memory_order_acquire); } + void MarkReleased() { released.store(true, std::memory_order_release); } private: void release() noexcept; bool refreshBlockCopy(); + bool refreshSrgbExpand(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); @@ -146,7 +152,10 @@ private: std::shared_ptr storageSource; std::unique_ptr upload; PerUseCopy perUseCopy = PerUseCopy::None; + std::atomic released{false}; bool copiedOnce = false; + bool srgbExpand = false; + GuestTextureResource sourceDescriptor{}; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat blockFormat = VK_FORMAT_UNDEFINED; @@ -356,6 +365,7 @@ public: static std::uint64_t RefreshesProved(); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; + std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index ff9eaf11df..9271395ec1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -16,6 +16,8 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat); VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats); +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat); bool IsSrgbTextureFormat(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag new file mode 100644 index 0000000000..435fb75d5a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat in uint paletteIndex; +layout(location = 0) out vec4 color; + +void main() { + color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : parameters.palette[0]; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert new file mode 100644 index 0000000000..b59069433c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert @@ -0,0 +1,16 @@ +#version 450 + +layout(location = 0) in ivec4 area; +layout(location = 1) in uint color; +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat out uint paletteIndex; + +void main() { + vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)); + vec2 pixel = vec2(area.xy) + corner * vec2(area.zw); + gl_Position = vec4(pixel / parameters.extent * 2.0 - 1.0, 0.0, 1.0); + paletteIndex = color; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 148b5e4f55..e6365c0aca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -194,6 +194,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges do not ascend by begin"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8ef1050892..9aaa239642 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -257,6 +257,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); + if (resident != nullptr && !resident->Cached()) recorder.KeepBytes(resident.get(), static_cast(resident->BudgetBytes())); recorder.Keep(resident); context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); recorder.NotePendingWrite(address, bytes); @@ -1544,6 +1545,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr kept->scratch = std::move(scratch); kept->targets = std::move(targets); const auto& residents = kept->targets; + resources->KeepStorageImageBytes(recorder); + for (const auto& target : residents) { + if (target != nullptr && !target->Cached()) recorder.KeepBytes(target.get(), static_cast(target->BudgetBytes())); + } recorder.Keep(kept); if (resources != nullptr) recorder.KeepBytes(resources.get(), resources->CopiedBytes()); if (checkRecords) recorder.OnComplete(std::move(checkRecords)); @@ -1702,15 +1707,15 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }; if (bindings != nullptr) { for (const auto& snapshot : bindings->snapshots) { - const auto bytes = snapshot.buffer->Bytes(); - addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data()); + const auto bytes = snapshot.Bytes(); + addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), snapshot.offset, bytes.data()); } } for (const auto& [begin, end] : resources.InPlaceReads()) { Require(end >= begin, "invalid capture input range"); const auto bytes = static_cast(end - begin); if (bytes == 0 || bytes > 512) continue; - if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue; + if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.bytes; })) continue; if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) { CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast(begin), bytes); continue; @@ -2678,6 +2683,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (!proved) { SharedResourceCache().Remove(recipe.key, resources.get()); countCache(&DrawProfile::cacheInvalidated); + resources->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resources)); timer.phase(PhaseLookup); return miss(DrawRecipeMiss::Proof); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 7864d0f83b..3d233e6868 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1400,7 +1400,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; } else if (!fresh) { - if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current; + if (const auto* current = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + current->lastUse = ++state.useCounter; + return current; + } } auto run = importRun(lease, address); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the @@ -1431,7 +1434,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); return nullptr; } - if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second; + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) { + found->second.lastUse = ++state.useCounter; + return &found->second; + } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; if (const auto failed = failures.find(base); failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index c404531663..24403fa2bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" @@ -566,6 +567,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); timing.Mark("create"); + PerformanceCounters::CountPipeline(); LogPipelineStatistics_nid_no_patch(context, pipeline); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp index cc352d0d9c..68487401c7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -21,18 +21,18 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) try { std::vector toClear; for (std::uint32_t index = 0; index < images.size(); ++index) { - const auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); - if (!Supported(context, shape)) continue; + const auto shape = PlaceholderImageShapeForDevice(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index, context.storageImageMultisample); + if (!shape || !Supported(context, *shape)) continue; auto& image = images[index]; VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - info.imageType = shape.type; - info.format = shape.format; + info.imageType = shape->type; + info.format = shape->format; info.extent = {1, 1, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = shape.samples; + info.samples = shape->samples; info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = shape.usage; + info.usage = shape->usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image.image), "vkCreateImage placeholder"); @@ -45,9 +45,9 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) Check(context.Function("vkBindImageMemory")(context.device, image.image, image.memory, 0), "vkBindImageMemory placeholder"); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image.image; - viewInfo.viewType = shape.viewType; - viewInfo.format = shape.format; - viewInfo.subresourceRange = {shape.aspect, 0, 1, 0, 1}; + viewInfo.viewType = shape->viewType; + viewInfo.format = shape->format; + viewInfo.subresourceRange = {shape->aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &image.view), "vkCreateImageView placeholder"); VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 1f03eea443..43b90a9f1c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -10,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" @@ -2414,6 +2415,7 @@ void Recorder::readGpuTiming(Batch& batch) { batch.gpuEndNs = static_cast(stamps[i * 2 + 1]) * period; PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId); timingBatchMs += ns / 1e6; + AgcDriver::PerformanceCounters::AddGpuNanoseconds(static_cast(ns)); continue; } const auto key = batch.timedKeys[i]; @@ -2540,6 +2542,17 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } +Recorder::UploadSlice Recorder::DrawUpload(std::size_t bytes) { + const VkDeviceSize alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 16); + auto offset = (drawUploadUsed + alignment - 1) & ~(alignment - 1); + if (drawUpload == nullptr || offset + bytes > drawUpload->Bytes().size()) { + drawUpload = std::make_shared(context, std::max(DrawUploadChunk, bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + offset = 0; + } + drawUploadUsed = offset + bytes; + return {drawUpload, offset, drawUpload->Bytes().data() + offset}; +} + void Recorder::retireUnwrittenSnapshots(const std::array& stamp) { if (stamp == unwrittenStamp) return; unwrittenSnapshots.clear(); @@ -2547,21 +2560,22 @@ void Recorder::retireUnwrittenSnapshots(const std::array& stam unwrittenStamp = stamp; } -std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp) { +std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset) { retireUnwrittenSnapshots(stamp); const auto found = unwrittenSnapshots.find(address); if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {}; + if (offset != nullptr) *offset = found->second.offset; return found->second.buffer; } -void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer) { +void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset) { retireUnwrittenSnapshots(stamp); if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) { unwrittenBytes -= found->second.bytes; unwrittenSnapshots.erase(found); } if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return; - unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer)}); + unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer), offset}); unwrittenBytes += bytes; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 85025d5cf2..fb213c0220 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -194,6 +194,16 @@ void noteUncached(const std::shared_ptr& image) { uncached.any.store(true, std::memory_order_release); } +std::atomic& TexturesReleased() { + static std::atomic released{false}; + return released; +} + +void noteReleased(const std::shared_ptr& texture) { + texture->MarkReleased(); + TexturesReleased().store(true, std::memory_order_release); +} + bool SamplerCacheDisabled() { static const bool disabled = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; return disabled; @@ -225,6 +235,7 @@ void eraseTexture(TextureCache& cache, std::list::iterator it) { } } } + if (it->texture.use_count() > 1) noteReleased(it->texture); cache.index.erase(it->key); cache.entries.erase(it); ++cache.erasures; @@ -424,7 +435,7 @@ void logLookup(const LookupRecord& record) { std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); std::uint64_t heldBytes(const StorageTexture& image) { - return std::max(image.AllocationBytes(), image.GuestBytes()); + return image.BudgetBytes(); } bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) { @@ -639,10 +650,11 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format); auto source = StorageTexture::FindPending(address, guestBytes); - const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); + const bool blockCopy = source != nullptr && !depthCompare && !srgbExpand && Texture::CanBlockCopyFrom(*source, resource); const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); - if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : srgbExpand ? !Texture::CanSrgbExpandFrom(context, *source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -674,6 +686,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && srgbExpand) { + auto texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -816,6 +834,9 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::RepackCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (srgbExpand) { + entry.texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared(context, source, resource, components); } @@ -840,6 +861,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedStorageTexture(const Context& context, std std::vector> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); + if (image.use_count() > 1) recorder->KeepBytes(image.get(), static_cast(heldBytes(*image))); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } if (!evicted.empty()) { @@ -1078,6 +1104,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + return largestDeviceLocalHeap(memory); +} + std::uint64_t StorageTextureCacheBudget(const Context& context) { return storageBudget(context); } @@ -1156,7 +1186,10 @@ void ClearCachedTextures(VkDevice device) { std::vector> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } } @@ -1333,6 +1366,16 @@ bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& bin ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast(vertex.pushConstants.size())}}}, target, 1, indexAddress, indexBytes) {} +void ShaderResources::KeepStorageImageBytes(Recorder& recorder) const { + const auto keep = [&](const std::shared_ptr& image) { + if (image != nullptr && !image->Cached()) recorder.KeepBytes(image.get(), static_cast(image->BudgetBytes())); + }; + for (const auto& image : storageTextures) keep(image); + for (const auto& texture : textures) { + if (texture != nullptr) keep(texture->SharedStorageSource()); + } +} + ShaderResources::ShaderResources(const Context& context, std::span shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes, std::span snapshots) : context(context), guestMemory(context) { drawBuild = true; prepareAddressBindings(shaders, snapshots); @@ -2381,6 +2424,7 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.imageAtomic); packBits(binding.samplerUnnormalized); packBits(binding.samplerIntegerBorder); + packBits(binding.samplerCoordinatesIgnored); packBits(binding.imageUnnormalized); packBits(binding.imageUnnormalizedBaseLevel); // Read-only elements are bound without a write set: an object built for one written set @@ -2402,11 +2446,11 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha words += count; }; for (const auto& binding : shader.program->bindings) { - addWords(19); + addWords(20); addWords(binding.imageSamplers.size()); addWords(binding.imageShapes.size()); if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size()); - for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); + for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.samplerCoordinatesIgnored, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); } key.reserve(words); VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); @@ -3538,7 +3582,8 @@ void ResourceCache::noteMiss(const Key& key) { void ResourceCache::Insert(const Key& key, std::shared_ptr resources, std::vector>* evicted) { std::lock_guard lock(mutex); - if (!resources->HasCachedStorageImages()) return; + dropReleasedTextures(evicted); + if (!resources->HasCachedStorageImages() || resources->HoldsReleasedTexture()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -3597,6 +3642,19 @@ void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { + if (!TexturesReleased().exchange(false, std::memory_order_acq_rel)) return; + for (auto it = entries.begin(); it != entries.end();) { + if (!it->second->HoldsReleasedTexture()) { + ++it; + continue; + } + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = entries.erase(it); + } +} + bool ResourceCache::Touch(const Key& key) { resourceCacheTouches.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(mutex); @@ -4137,7 +4195,14 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + auto guestWords = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array normalizedWords{}; + if (element < binding.samplerCoordinatesIgnored.size() && binding.samplerCoordinatesIgnored[element]) { + std::copy(guestWords.begin(), guestWords.end(), normalizedWords.begin()); + normalizedWords[0] &= ~(1u << 15u); + guestWords = normalizedWords; + } + const auto words = guestWords; const bool compareEnable = binding.samplerDepthCompare.at(element); const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; const bool integerBorder = element < binding.samplerIntegerBorder.size() && binding.samplerIntegerBorder[element]; @@ -5030,10 +5095,19 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { return static_cast(mib * 1024ull * 1024ull); } +std::size_t ShaderResources::DrawUploadLimit() { + static const std::size_t limit = [] { + const char* value = std::getenv("APS5_DRAW_UPLOAD_MAX"); + return value != nullptr ? static_cast(std::strtoull(value, nullptr, 10)) : std::size_t{4096}; + }(); + return limit; +} + std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved, bool movedOnly) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = maxDrawSnapshotBytes; + const auto uploadLimit = DrawUploadLimit(); std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); thread_local std::vector* selectedSlot = nullptr; @@ -5061,7 +5135,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); } selected.push_back(index); - result->snapshots.push_back({0, std::move(buffer)}); + result->snapshots.push_back({0, std::move(buffer), 0, override->size}); continue; } std::uint64_t address = item.address; @@ -5082,28 +5156,38 @@ std::shared_ptr ShaderResources::PrepareDrawBindi stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration}; stamped = true; } - auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp); + VkDeviceSize offset = 0; + auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp, &offset); static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr; if (verifySnapshots && buffer != nullptr) { static std::atomic checked{0}, differing{0}; + const auto fast = buffer->Bytes().subspan(static_cast(offset), bytes); const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); - const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == buffer->Bytes().size() && std::memcmp(slow->Bytes().data(), buffer->Bytes().data(), buffer->Bytes().size()) == 0) || (slow == nullptr && std::memcmp(buffer->Bytes().data(), reinterpret_cast(begin), bytes) == 0); + const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast(begin), bytes) == 0); if (!same) differing.fetch_add(1); if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); } if (buffer == nullptr) { - const auto generation = GuestMemory::CollectWrites(begin, bytes); - buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); - if (buffer == nullptr) { - buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); - recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + const bool uploaded = bytes != 0 && bytes <= uploadLimit; + if (uploaded) { + auto slice = recorder.DrawUpload(bytes); + std::memcpy(slice.bytes, reinterpret_cast(begin), bytes); + buffer = std::move(slice.buffer); + offset = slice.offset; + } else { + const auto generation = GuestMemory::CollectWrites(begin, bytes); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (buffer == nullptr) { + buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); + recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + } } - if (GuestMemory::TrackerGeneration() == stamp[0]) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer); - else stamped = false; + if (GuestMemory::TrackerGeneration() != stamp[0]) stamped = false; + else if (!uploaded || stamp[1] != 0) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer, offset); } selected.push_back(index); - result->snapshots.push_back({begin, std::move(buffer)}); + result->snapshots.push_back({begin, std::move(buffer), offset, bytes}); totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } @@ -5152,7 +5236,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto snapshotIndex = snapshotIndices[binding.allocations[element]]; if (snapshotIndex == selected.size()) continue; const auto& snapshot = result->snapshots[snapshotIndex]; - buffers[offset + element] = {snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()}; + buffers[offset + element] = {snapshot.buffer->Handle(), snapshot.offset, snapshot.bytes}; } } } @@ -5255,6 +5339,10 @@ bool ShaderResources::HasCachedStorageImages() const { return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); } +bool ShaderResources::HoldsReleasedTexture() const { + return std::any_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->Released(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 961b103030..aa903ba8fa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -17,6 +17,7 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { @@ -50,6 +51,25 @@ std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* n return std::string(name) + " is unsupported" + detail; } +std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) { + static constexpr std::array Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"}; + static constexpr std::array Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"}; + static constexpr std::array Topologies{"OUTPUT_POINT", "OUTPUT_LINE", "OUTPUT_TRIANGLE_CW", "OUTPUT_TRIANGLE_CCW"}; + std::ostringstream message; + const auto field = [&](std::string_view name, std::uint32_t value, std::span names) { + message << name << '=' << value; + if (value < names.size()) message << ' ' << names[value]; + }; + message << "VGT_TF_PARAM=0x" << std::hex << parameters << std::dec << " ("; + field("TYPE", tess.domain, Types); + message << ", "; + field("PARTITIONING", tess.partitioning, Partitionings); + message << ", "; + field("TOPOLOGY", tess.outputTopology, Topologies); + message << "), VGT_LS_HS_CONFIG=0x" << std::hex << config << std::dec << " (NUM_PATCHES=" << tess.patchesPerGroup << ", HS_NUM_INPUT_CP=" << tess.inputControlPoints << ", HS_NUM_OUTPUT_CP=" << tess.outputControlPoints << "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"; + return message.str(); +} + void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, RegisterBank bank = RegisterBank::Context) { const auto value = read(registers, offset, bank); if ((value & mask) != 0) throw std::runtime_error(zeroMessage(offset, value, name)); @@ -777,6 +797,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, const auto mapping = static_cast(alternate ? 0xecu : 0xe4u); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2, mapping}; if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2, mapping}; + if (number == uint) return {VK_FORMAT_R8G8_UINT, 2, mapping}; return fail(); } case 4: @@ -915,7 +936,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + if (!((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3))) validate(false, tessellationMessage(config, parameters, tess).c_str()); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); @@ -1096,7 +1117,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; - Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); + if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); result.customSampleLocations = true; result.sampleLocations.fill(0); result.sampleLocationGrid = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 6437d57931..63b983fa4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -64,6 +64,12 @@ TextureProfile& Profile() { return profile; } +void keepStorageTexture(Recorder& recorder, std::shared_ptr texture) { + if (texture == nullptr) return; + if (!texture->Cached()) recorder.KeepBytes(texture.get(), static_cast(texture->BudgetBytes())); + recorder.Keep(std::move(texture)); +} + // APS5_PROFILE_DRAW, every 10 s on the [storage] line: storage image bytes uploaded by path // (recorded DCC clear, direct detile from the import, CPU) and written back by the reason of the // FlushPending that forced the write-back (the other callers name themselves: "refresh", "cache @@ -232,6 +238,53 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor, viewInfo.pNext = &minLod; } +struct ExpandImage { + VkImage image = VK_NULL_HANDLE; + std::shared_ptr owned; + VkDeviceSize bytes = 0; +}; + +ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + ExpandImage result; + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u); + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent = {descriptor.width, descriptor.height, depth}; + imageInfo.mipLevels = descriptor.mipCount; + imageInfo.arrayLayers = layers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &result.image), "vkCreateImage sRGB copy"); + result.owned = std::make_shared(context, result.image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, result.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &result.owned->memory), "vkAllocateMemory sRGB copy"); + Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); + result.bytes = requirements.size; + return result; +} + +void RecordExpandBlit(const Context& context, VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage target, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + std::vector regions; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const VkOffset3D end{static_cast(std::max(descriptor.width >> level, 1u)), static_cast(std::max(descriptor.height >> level, 1u)), static_cast(descriptor.dimension == TextureDimension::k3D ? std::max(depth >> level, 1u) : 1u)}; + VkImageBlit region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers}; + region.srcOffsets[1] = end; + region.dstSubresource = region.srcSubresource; + region.dstOffsets[1] = end; + regions.push_back(region); + } + context.Function("vkCmdBlitImage")(commands, source, sourceLayout, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + std::atomic& SampledMemoryCounter() { static std::atomic bytes{0}; return bytes; @@ -249,6 +302,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex PhaseTimer timer; try { const auto colorFormat = SampledTextureFormat(context, descriptor.format); + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format); Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture"); Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures"); const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat; @@ -281,7 +335,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u); imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage"); @@ -296,6 +350,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); + const VkImage uploadImage = image; + std::shared_ptr uploadOwned; + if (srgbExpand) { + auto expanded = CreateExpandImage(context, descriptor, geometry.imageDepth, geometry.imageLayers); + uploadOwned = std::move(owned); + owned = std::move(expanded.owned); + image = expanded.image; + allocationBytes = expanded.bytes; + } + { // Debug aid: APS5_DUMP_TEXTURE= saves the detiled first mip // of those sampled textures on their first 8 uploads as texture_
_.raw (u32 @@ -334,6 +398,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex recorder->Keep(tiled, static_cast(guestBytes)); recorder->Keep(linear, static_cast(linearBytes)); recorder->Keep(owned); + if (uploadOwned != nullptr) recorder->Keep(uploadOwned); } else { batch = std::make_unique(context); commands = batch->Handle(); @@ -379,7 +444,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransferDst.image = image; + toTransferDst.image = uploadImage; toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst); @@ -400,7 +465,19 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex regions.push_back(region); } } - context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + if (srgbExpand) { + std::array toBlit{}; + toBlit[0] = toTransferDst; + toBlit[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toBlit[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toBlit[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toBlit[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toBlit[1] = toTransferDst; + toBlit[1].image = image; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2, toBlit.data()); + RecordExpandBlit(context, commands, uploadImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, descriptor, geometry.imageDepth, geometry.imageLayers); + } std::unique_ptr dump; if (dumpWanted) { dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -451,8 +528,8 @@ 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.format = vkFormat; - viewFormat = vkFormat; + viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; + viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -687,10 +764,96 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } +bool Texture::CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!SrgbExpandsToRgba8(context, descriptor.format) || !CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (!StorageFormatAvailable(context, source.Descriptor().format) || StorageFormatForGuest(context, source.Descriptor().format) != SampledTextureFormat(context, descriptor.format)) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), srgbExpand(true) { + try { + Require(source != nullptr && CanSrgbExpandFrom(context, *source, descriptor), "storage image cannot serve an sRGB copy of the sampled texture"); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u sRGB copy of storage image (guest format %u)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format); + auto expanded = CreateExpandImage(context, descriptor, 1u, copyLayers); + image = expanded.image; + owned = std::move(expanded.owned); + allocationBytes = expanded.bytes; + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; + viewFormat = viewInfo.format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_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 sRGB copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + sourceDescriptor = descriptor; + countedBytes = allocationBytes; + SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::refreshSrgbExpand() { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + RecordExpandBlit(context, commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, sourceDescriptor, 1u, copyLayers); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + bool Texture::RefreshDepthCopy() { if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; if (copiedOnce && storageSource->Version() == copiedVersion) return false; if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); + if (srgbExpand) return refreshSrgbExpand(); std::uint64_t linearBytes = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u)); @@ -2328,7 +2491,7 @@ void StorageTexture::upload(const std::vector* layers) { if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); ++Profile().storageRecordedClears; Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { @@ -2420,7 +2583,7 @@ void StorageTexture::upload(const std::vector* layers) { timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); if (linear) recorder->Keep(linear, linear->Size()); // The image itself must outlive the recorded copy: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); // The detile reads the tiled bytes from the import when the batch runs. recorder->NotePendingRead(descriptor.baseAddress, static_cast(guestBytes), Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); @@ -2848,7 +3011,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< for (const auto& source : sources) { if (source.slab != nullptr) recorder->Keep(source.slab); } - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (!reads.empty()) recorder->NotePendingReads(reads, Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { @@ -3052,7 +3215,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab); for (const auto& slab : padding.slabs) recorder->Keep(slab); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, direct ? 3 : 4); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; @@ -4127,7 +4290,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { } const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { const std::pair clearedImage{image, true}; @@ -4262,7 +4425,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::FillClear); Recorder::CountBarriers(Recorder::CommandClass::FillClear, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (Recorder::BarrierValidate()) { const std::pair cleared{image, true}; recorder->NoteAccess(Recorder::CommandClass::FillClear, Recorder::Access{{}, {}, std::span(&cleared, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4389,8 +4552,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; recorder->NoteAccess(Recorder::CommandClass::Copy, Recorder::Access{{}, {}, images, VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4567,8 +4730,8 @@ std::uint64_t StorageTexture::borrowUnits(StorageTexture& source, const std::vec if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, 2); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; @@ -4869,7 +5032,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& slab : padding.slabs) recorder->Keep(slab); // The image itself must outlive the recorded retile: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, padding.copies.empty() ? 4 : 5); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; @@ -5148,6 +5311,10 @@ std::uint64_t StorageTexture::GuestBytes() const { return guestBytes; } +std::uint64_t StorageTexture::BudgetBytes() const { + return std::max(AllocationBytes(), GuestBytes()); +} + VkImageView StorageTexture::View() const { return view; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 59dd54b906..c9617b896f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -117,7 +117,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); - const bool always = forced != nullptr && std::strcmp(forced, "1") == 0; + const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0); const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) { VkFormatProperties properties{}; formatProperties(physical, ResolveTextureFormat(static_cast(format)), &properties); @@ -137,6 +137,26 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); } +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats) { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::strcmp(forced, "1") == 0) return 0u; + VkFormatProperties target{}; + formatProperties(physical, VK_FORMAT_R8G8B8A8_UNORM, &target); + if ((target.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) return 0u; + std::uint32_t formats = 0; + for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) { + if ((decodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) continue; + VkFormatProperties source{}; + formatProperties(physical, ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))), &source); + if ((source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) formats |= ShaderRecompiler::SrgbDecodeBit(format); + } + return formats; +} + +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat) { + return (context.srgbCopyFormats & ShaderRecompiler::SrgbDecodeBit(static_cast(guestFormat))) != 0u; +} + bool IsSrgbTextureFormat(std::uint32_t guestFormat) { switch (findFormatEntry(guestFormat).vkFormat) { case VK_FORMAT_R8_SRGB: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index a2c51f1e0c..d2fcbf8852 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -383,6 +383,13 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) { + Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented"); + Require(descriptor.dimension == TextureDimension::k2DMsaa && descriptor.baseArray == 0u && descriptor.depthOrLastArray == 0u, "multisampled surface dimension or array range is not implemented"); + Require(descriptor.mipCount == 1u && descriptor.allocatedMipCount == 1u && descriptor.baseLevel == 0u && descriptor.lastLevel == 0u, "multisampled surface mip levels are not implemented"); + Require(descriptor.width != 0u && descriptor.width <= 16384u && descriptor.height != 0u && descriptor.height <= 16384u, "invalid multisampled surface extent"); + Require(!IsBlockCompressed(descriptor.format), "multisampled block-compressed surface is not implemented"); + } if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { const auto depth = descriptor.depthOrLastArray + 1u; geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); @@ -406,6 +413,29 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { geometry.layerBytes = thick.slabBytes; return geometry; } + if (descriptor.dimension == TextureDimension::k2DMsaa && descriptor.tileMode != TextureTileMode::kLinear) { + Require(descriptor.samples > 1u && std::has_single_bit(descriptor.samples), "multisampled texture has an invalid sample count"); + const auto bytesPerElement = BytesPerElement(descriptor.format); + Require(std::has_single_bit(bytesPerElement), "multisampled texture format has an unsupported element size"); + const auto block = GetBlockLayout(descriptor.tileMode, bytesPerElement); + Require(block.blockSize == 4096u || block.blockSize == 65536u, "multisampled surface requires 4 KiB or 64 KiB blocks"); + const auto log2Samples = static_cast(std::countr_zero(descriptor.samples)); + const auto log2Pixels = static_cast(std::countr_zero(block.blockSize)) - static_cast(std::countr_zero(bytesPerElement)) - log2Samples; + const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; + const auto blockWidth = 1u << log2Width; + const auto blockHeight = 1u << (log2Pixels - log2Width); + const auto align = [](std::uint32_t extent, std::uint32_t blockDimension) { + return (static_cast(extent) + blockDimension - 1u) / blockDimension * blockDimension; + }; + geometry.guestBytes = align(descriptor.width, blockWidth) * align(descriptor.height, blockHeight) * bytesPerElement * descriptor.samples; + geometry.layerBytes = geometry.guestBytes; + geometry.layers = 1; + geometry.imageLayers = 1; + geometry.imageDepth = 1; + geometry.sliceLinearBytes = static_cast(descriptor.width) * descriptor.height * descriptor.samples * bytesPerElement; + geometry.mips.push_back({0u, geometry.guestBytes, 0u, geometry.sliceLinearBytes, descriptor.width, descriptor.height, static_cast(align(descriptor.width, blockWidth) / blockWidth), descriptor.width * descriptor.samples * bytesPerElement, false, 0u, 0u}); + return geometry; + } geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube; geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index 312d83bede..daad45d4f1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace { @@ -181,4 +182,8 @@ void RunBdaContractTests() { invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); + const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending BDA ranges were refused"); + AgcDriver::Graphics::Require(ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2f00, 0x3100, 1, 1, 0}}), "BDA ranges ascending by begin and overlapping were refused"); + AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 2804e7e81b..113991ac43 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -213,6 +214,170 @@ static void RunBdaDwordWriteTests(const Context& context) { write(guest + 0x7c, 2, 8, static_cast(0), 64); } +static void RunBdaAnchorTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + constexpr std::uint64_t guest = 0x7fff12340001ULL; + constexpr std::uint64_t limit = 0xffffffff00000000ULL; + constexpr std::uint64_t highest = 0xffffffffffffff00ULL; + constexpr std::uint32_t lanes = 64; + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + Buffer first(context, 3, usage); + Buffer second(context, 2, usage); + Buffer words(context, 16, usage); + Buffer shifted(context, 20, usage); + Buffer large(context, 1024, usage); + Buffer top(context, 16, usage); + Buffer above(context, 16, usage); + Buffer overlap(context, 0x200, usage); + std::uint32_t seed = 0x11; + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above, &overlap}) { + for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); + seed += 0x33; + } + for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); + for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); + std::vector ranges{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, + {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, + {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, + {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {guest + 0x700, guest + 0x900, overlap.DeviceAddress(), Abi::Read, 0}, + {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table does not ascend by begin"); + Buffer table(context, sizeof(Abi::Header) + ranges.size() * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + struct Outcome { + Abi::Fault fault{}; + std::array values{}; + }; + std::uint32_t count = static_cast(ranges.size()); + std::uint32_t permissions = Abi::Read; + const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { + auto copy = ranges; + if (copy.size() > 2u) copy[2].permissions = permissions; + const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), copy.size() * sizeof(Abi::Range)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); + std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; + inputs.insert(inputs.end(), lows.begin(), lows.end()); + std::memcpy(output.Bytes().data() + BdaAnchoredReadInputWord * sizeof(std::uint32_t), inputs.data(), inputs.size() * sizeof(std::uint32_t)); + pipeline.Run(1); + Outcome outcome; + std::memcpy(&outcome.fault, fault.Bytes().data(), sizeof(outcome.fault)); + std::memcpy(outcome.values.data(), output.Bytes().data(), sizeof(outcome.values)); + return outcome; + }; + struct Programs { + Programs(const Context& context, const BdaAnchoredReadProgram& reads, const std::array& buffers) : reads(reads), plain(context, MakeBdaAnchoredReadTestShader(reads, false), buffers), anchored(context, MakeBdaAnchoredReadTestShader(reads, true), buffers) {} + BdaAnchoredReadProgram reads; + Pipeline plain; + Pipeline anchored; + }; + const auto build = [&](const BdaAnchoredReadProgram& reads) { return std::make_unique(context, reads, std::array{&table, &fault, &output}); }; + const auto compare = [&](Programs& programs, std::initializer_list anchors, const std::vector& lows) { + std::vector> batches; + if (programs.reads.lanes == 1u) { + for (const auto low : lows) batches.push_back({low}); + } else { + batches.push_back(lows); + batches.back().resize(programs.reads.lanes, lows.back()); + } + for (std::uint32_t index = 0; index < programs.reads.reads.size(); ++index) { + const auto& read = programs.reads.reads[index]; + for (const auto anchor : anchors) { + for (const auto& batch : batches) { + const auto expected = run(programs.plain, index, anchor, batch); + const auto actual = run(programs.anchored, index, anchor, batch); + const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state; + Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); + } + } + } + }; + std::vector reads; + for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { + for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); + } + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, guest + 0x6f0, guest + 0x700, guest + 0x800, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; + for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); + std::vector strided; + for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto continuing = build({reads, false, false, false, lanes}); + compare(*continuing, anchors, lows); + compare(*continuing, {guest + 0x400}, strided); + auto elected = build({reads, false, true, false, lanes}); + compare(*elected, anchors, lows); + compare(*elected, {guest + 0x400}, strided); + auto coherent = build({{reads[0], reads[2], reads[9], reads[16], reads[22]}, false, false, true, lanes}); + compare(*coherent, anchors, lows); + auto stopping = build({reads, true, true, false, 1}); + compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); + const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + auto direct = std::make_unique(context, MakeBdaAnchoredReadTestShader({reads, true, false, false, 1, true}, true), std::array{&table, &fault, &output}); + const auto dword = [](Buffer& buffer, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, buffer.Bytes().data() + offset, sizeof(value)); + return value; + }; + const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list values) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + loads(2, guest + 0x100, 0, {dword(words, 0)}); + loads(2, guest + 0x100, 0xc, {dword(words, 0xc)}); + loads(3, guest + 0x100, 0, {dword(words, 0), dword(words, 4), dword(words, 8), dword(words, 0xc)}); + loads(0, guest + 0x200, 0xd, {0xceu}); + loads(0, guest + 3, 1, {static_cast(second.Bytes()[1])}); + loads(1, guest, 1, {static_cast(first.Bytes()[1]) | static_cast(first.Bytes()[2]) << 8u}); + loads(2, guest + 0x400, 0x2fc, {dword(large, 0x2fc)}); + loads(2, guest + 0x700, 0x1fc, {dword(overlap, 0x1fc)}); + loads(2, limit - 0x10, 0xc, {dword(top, 0xc)}); + misses(2, guest + 0x100, 0xd); + misses(3, guest + 0x100, 4); + misses(2, guest + 0x200, 0); + misses(1, guest + 2, 0); + misses(0, guest + 3, 2); + misses(2, guest + 0x400, 0x300); + misses(2, guest + 0x400, 0x2fd); + misses(2, limit - 0x10, 0xd); + misses(2, highest, 0); + misses(2, guest - 1, 1); + const auto wide = check(3, guest + 0x100, 0); + Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); + const auto some = check(4, guest + 0x100, 0); + Require(some.fault.state == Abi::FaultState::Empty && some.values[0] == 0u && some.values[1] == 0xa7a6a5a4u && some.values[2] == 0u && some.values[3] == 0xafaeadacu, "an anchored read of some dwords returned wrong data"); + const auto odd = check(2, guest + 0x200, 0); + Require(odd.fault.state == Abi::FaultState::Empty && odd.values[0] == 0xc4c3c2c1u, "an anchored read of an odd device address returned wrong data"); + const auto byte = check(0, guest + 0x200, 0xd); + Require(byte.fault.state == Abi::FaultState::Empty && byte.values[0] == 0xceu, "an anchored byte read returned wrong data"); + const auto below = check(20, guest + 0x108, 4); + Require(below.fault.state == Abi::FaultState::Empty && below.values[0] == 0xa7a6a5a4u, "an anchored read below its base returned wrong data"); + const auto unmapped = check(2, guest + 0x100, 0x10); + Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); + permissions = 0; + misses(2, guest + 0x100, 0); + compare(*stopping, {guest + 0x100}, {0u, 4u}); + compare(*elected, {guest + 0x100}, lows); + permissions = Abi::Read; + count = 3; + misses(2, guest + 0x100, 0); + misses(0, guest, 0); + compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); + compare(*elected, {guest + 0x100, guest}, lows); + ranges = {{0, limit, large.DeviceAddress(), Abi::Read, 0}}; + count = 1; + compare(*stopping, {limit - 1}, {0xffffffffu}); +} + void RunBdaExecutionTests(const Context& context) { namespace Abi = ShaderRecompiler::BdaAbi; Buffer first(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); @@ -333,6 +498,7 @@ void RunBdaExecutionTests(const Context& context) { ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); RunBdaDwordWriteTests(context); + RunBdaAnchorTests(context); } void RunBdaStoreExecutionTests(const Context& context) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index dd9ef7ac25..0fbb1146d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -739,6 +740,151 @@ void importWholeRangeOverWindowTests(const Context& context) { leased.WriteBack(); } +struct ObservedHostImport { + const void* pointer; + VkDeviceSize bytes; +}; + +class ImportAllocationProbe { +public: + explicit ImportAllocationProbe(Context& context) : context(context), original(context.deviceProc) { + Require(active == nullptr && original != nullptr, "invalid import allocation probe scope"); + active = this; + context.deviceProc = Resolve; + } + ~ImportAllocationProbe() { + context.deviceProc = original; + active = nullptr; + } + ImportAllocationProbe(const ImportAllocationProbe&) = delete; + ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete; + + std::vector calls; + +private: + static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + Require(active != nullptr, "import allocation probe escaped its scope"); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(Allocate); + return active->original(device, name); + } + static VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + Require(active != nullptr, "import allocation callback escaped its scope"); + for (const auto* next = static_cast(info->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* imported = reinterpret_cast(next); + active->calls.push_back({imported->pHostPointer, info->allocationSize}); + } + const auto allocate = reinterpret_cast(active->original(device, "vkAllocateMemory")); + Require(allocate != nullptr, "import allocation probe has no original allocator"); + return allocate(device, info, allocator, memory); + } + + Context& context; + PFN_vkGetDeviceProcAddr original; + inline static thread_local ImportAllocationProbe* active = nullptr; +}; + +class UnrelatedImportProbeRange { +public: + UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Add(storage.data(), storage.size(), true, false, false); + } + ~UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Remove(storage.data()); + } + UnrelatedImportProbeRange(const UnrelatedImportProbeRange&) = delete; + UnrelatedImportProbeRange& operator=(const UnrelatedImportProbeRange&) = delete; + +private: + std::array storage{}; +}; + +void importStaleProbeTests(const Context& context, const BdaTestAccess& access) { + for (const auto scenario : {0u, 1u, 2u}) { + const bool held = scenario == 1u; + const bool exactWhole = scenario == 2u; + WindowedRange range(context, 2 * WindowedRange::piece, 2); + ImportAllocationProbe probe(range.importing); + const auto first = range.First(); + auto* guest = reinterpret_cast(first); + for (std::size_t offset = 17u; offset < range.bytes; offset += 4096u) guest[offset] = static_cast((offset / 4096u) & 0xffu); + const std::vector originalGuest(guest, guest + range.bytes); + const auto* window = HostImportFor(range.importing, first, exactWhole ? range.bytes : 32u); + Require(window != nullptr && !window->joined && window->base == first && window->bytes == (exactWhole ? range.bytes : range.window), "the stale-probe fixture did not create its ordinary import extent"); + const auto originalBuffer = window->buffer; + const auto originalUse = window->lastUse; + const auto originalSerial = HostImportSerial(range.importing, first, 32u, false); + auto mockContents = access.bytes(window->buffer); + Require(mockContents.size() == window->bytes, "the mock import buffer has an incorrect extent"); + std::copy_n(originalGuest.begin(), window->bytes, mockContents.begin()); + std::uint32_t scratch = 0x12345678u; + { + GuestBufferMemory copied(range.importing); + copied.AddReadable(reinterpret_cast(&scratch), sizeof(scratch)); + copied.Upload(false); + copied.WriteBack(); + } + Require(window->lastUse == originalUse && HostImportSerial(range.importing, first, 32u, false) == originalSerial, "an unrelated copied build touched or replaced the prefix window"); + GuestBufferMemory reader(range.importing); + VkDescriptorBufferInfo heldView{}; + std::uint32_t heldAdjustment = 0u; + if (held) { + reader.AddReadable(first + 16u, 32u); + reader.Upload(false); + heldView = reader.Descriptor(first + 16u, 32u, heldAdjustment); + Require(heldView.buffer == originalBuffer, "the held control did not bind the prefix window"); + } + GuestAllocations::Mapped mapped; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == first && owner->bytes == range.bytes) mapped.push_back(owner); + } + Require(mapped.size() == 1u, "the stale-probe fixture lost its registered range"); + const auto originalOwner = mapped.front().lock(); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + UnrelatedImportProbeRange unrelated; + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation && HostImportsIdentity(range.importing).device == VK_NULL_HANDLE, + "the unrelated registry change did not make imports stale"); + const auto attempts = access.allocationAttempts(); + probe.calls.clear(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false), "the current device did not admit the map-time import pass"); + } + Require(mapped.front().lock() == originalOwner, "an unrelated registry change replaced the import's memory owner"); + if (held) { + Require(access.allocationAttempts() == attempts && probe.calls.empty(), "a stale probe retired an import held by an actual descriptor"); + const auto* kept = HostImportFor(range.importing, first + 16u, 32u); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->bytes == range.window && HostImportSerial(range.importing, first, 32u, false) == originalSerial, + "the actual held import lost its buffer, extent or serial"); + std::uint32_t adjustment = 0u; + const auto view = reader.Descriptor(first + 16u, 32u, adjustment); + Require(view.buffer == heldView.buffer && view.offset == heldView.offset && view.range == heldView.range && adjustment == heldAdjustment, + "a map-time stale probe invalidated a descriptor that still uses its import"); + const auto contents = access.bytes(view.buffer); + Require(view.offset + adjustment + 32u <= contents.size() && std::memcmp(contents.data() + view.offset + adjustment, reinterpret_cast(first + 16u), 32u) == 0, + "the protected descriptor no longer reads the registered bytes"); + reader.WriteBack(); + } else if (exactWhole) { + Require(access.allocationAttempts() == attempts && probe.calls.empty() && window->lastUse > originalUse, + "accepting the stale exact whole import reallocated it or did not protect its returned pointer"); + const auto* kept = HostImportFor(range.importing, first, range.bytes); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->ranges.size() == 1u && kept->ranges.front() == originalOwner && HostImportSerial(range.importing, first, range.bytes, false) == originalSerial, + "accepting the exact whole import changed its native or registered identity"); + } else { + Require(access.allocationAttempts() == attempts + 1u, "a discarded stale probe made an unheld prefix window block the whole import"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && whole->ranges.size() == 1u && whole->ranges.front() == originalOwner, + "the stale-probe repair did not retain the exact registered owner over the whole import"); + Require(HostImportSerial(range.importing, first, range.bytes, false) != originalSerial, "replacing the unheld window reused its old import identity"); + Require(probe.calls.size() == 1u && probe.calls.front().pointer == reinterpret_cast(first) && probe.calls.front().bytes == range.bytes, + "the replacement native import did not reference the exact whole registered memory"); + Require(access.bytes(whole->buffer).size() == range.bytes, "the replacement import buffer does not cover its registered extent"); + } + Require(scratch == 0x12345678u && std::equal(originalGuest.begin(), originalGuest.end(), guest), "the import probe changed guest bytes or unrelated copied data"); + } +} + // An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before, // every such build retired the window and the next access imported it again. void importRefusedWholeRangeTests(const Context& context) { @@ -2068,6 +2214,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importCoverageHintBudgetTests(context, access); failedImportJoinProbeBudgetTests(context, access); importWholeRangeOverWindowTests(context); + importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 60a2e87853..77707e646e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -133,3 +133,94 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui #endif return shader; } + +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + program.Info().usesDma = true; + if (reads.coherent) { + MemoryInfo memory; + memory.coherent = true; + program.Resources().memoryInfo = {memory}; + } + SpirvEmitterState state(program, {}); + EmitBaseHeader(state.module, program); + if (reads.elect) { + state.module.EmitCapability(spv::CapabilityGroupNonUniform); + state.module.EmitCapability(spv::CapabilityGroupNonUniformBallot); + state.requirements.subgroupBallot = true; + } + const auto define = [&](std::uint32_t binding) { + const auto variable = state.module.DefineGlobalVariable(TypeStorageBufferPointer(state), spv::StorageClassStorageBuffer); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, binding); + return variable; + }; + state.bdaPagetableVariable = define(0); + state.faultBufferVariable = define(1); + const auto output = define(2); + std::vector interfaceVariables{state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)}; + state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex); + state.bdaStopsInvocations = reads.stops; + DefineGetBdaPointer(state); + if (anchored) DefineBdaAnchorFunction(state); + const auto main = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); + EmitLabel(state, state.module.AllocateId()); + SpirvValueEmitContext ctx(state); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto input = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, BdaWord(state, output, word)); + return value; + }; + const auto base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord))), Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord + 1u))), BdaConstant(state, 32u))); + SpirvEmitterState::BdaAnchor anchor{}; + if (anchored) EmitBdaAnchorRange(state, anchor, base); + const auto selector = input(ConstantU32(state, BdaAnchoredReadInputWord + 2u)); + const auto laneIndex = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, laneIndex, interfaceVariables[0]); + const auto low = input(Binary(state, spv::OpIAdd, u32, laneIndex, ConstantU32(state, BdaAnchoredReadInputWord + 3u))); + const auto slot = Binary(state, spv::OpIMul, u32, laneIndex, ConstantU32(state, 4u)); + IrBuilder ir(program); + for (std::uint32_t index = 0; index < reads.reads.size(); ++index) { + const auto& read = reads.reads[index]; + const auto opcode = read.bits == 8u ? IrOpcode::LoadAddressU8 : read.bits == 16u ? IrOpcode::LoadAddressU16 : read.dwords == 2u ? IrOpcode::LoadAddressU32x2 : read.dwords == 3u ? IrOpcode::LoadAddressU32x3 : read.dwords == 4u ? IrOpcode::LoadAddressU32x4 : IrOpcode::LoadAddressU32; + const auto type = read.bits != 32u || read.dwords == 1u ? IrType::U32 : read.dwords == 2u ? IrType::U32x2 : read.dwords == 3u ? IrType::U32x3 : IrType::U32x4; + auto& instruction = program.CreateValue(opcode, type); + MemoryFlags flags{}; + flags.pc = 0x1234; + instruction.SetFlags(flags); + if (read.bits == 32u && read.dwords > 1u) { + const auto extract = read.dwords == 2u ? IrOpcode::CompositeExtractU32x2 : read.dwords == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4; + for (std::uint32_t dword = 0; dword < read.dwords; ++dword) { + if ((read.used & (1u << dword)) == 0u) continue; + auto& user = program.CreateValue(extract, IrType::U32); + user.AddArgument(&instruction); + user.AddArgument(&ir.Constant(dword)); + } + } + EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { + const auto lookup = [&] { + if (reads.sentinel) { + const auto sentinel = ConstantU32(state, BdaAnchoredReadSentinel); + return std::array{sentinel, sentinel, sentinel, sentinel}; + } + const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); + if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); + return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; + }; + const auto values = anchored ? EmitAnchoredBdaRead(state, instruction, anchor, low, read.immediate, read.bits, read.dwords, 0u, lookup) : lookup(); + for (std::uint32_t dword = 0; dword < (read.bits == 32u ? read.dwords : 1u); ++dword) { + state.module.AddFunction(spv::OpStore, BdaWord(state, output, Binary(state, spv::OpIAdd, u32, slot, ConstantU32(state, dword))), values[dword]); + } + }); + } + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, reads.lanes, 1u, 1u); + state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables); + return state.module.Finalize(); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 62a3adcb19..555397c843 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -11,6 +11,23 @@ std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std: std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +inline constexpr std::uint32_t BdaAnchoredReadInputWord = 256; +struct BdaAnchoredRead { + std::int32_t immediate = 0; + std::uint32_t bits = 32; + std::uint32_t dwords = 1; + std::uint32_t used = 1; +}; +struct BdaAnchoredReadProgram { + std::vector reads; + bool stops = true; + bool elect = false; + bool coherent = false; + std::uint32_t lanes = 1; + bool sentinel = false; +}; +inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu; +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1134ee3590..dfbe870a35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -995,6 +995,29 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnsupportedTessellationFieldTests() { + auto queue = makeState(); + queue.context[0x2d5] = 0x0200210d; + queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; + for (const auto [config, parameters, fields] : { + std::tuple{0xc355u, 0x40045u, "VGT_TF_PARAM=0x40045 (TYPE=1 TESS_TRIANGLE, PARTITIONING=1 PART_POW2, TOPOLOGY=2 OUTPUT_TRIANGLE_CW), VGT_LS_HS_CONFIG=0xc355 (NUM_PATCHES=85, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=3)"}, + std::tuple{0x1033fu, 0x40066u, "VGT_TF_PARAM=0x40066 (TYPE=2 TESS_QUAD, PARTITIONING=1 PART_POW2, TOPOLOGY=3 OUTPUT_TRIANGLE_CCW), VGT_LS_HS_CONFIG=0x1033f (NUM_PATCHES=63, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=4)"}, + std::tuple{0xc355u, 0x40048u, "VGT_TF_PARAM=0x40048 (TYPE=0 TESS_ISOLINE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x40029u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=1 OUTPUT_LINE)"}, + std::tuple{0xc355u, 0x4000du, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=3 PART_FRAC_EVEN, TOPOLOGY=0 OUTPUT_POINT)"}, + std::tuple{0xc355u, 0x4004bu, "(TYPE=3, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x400f1u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=4, TOPOLOGY=7)"}}) { + queue.context[0x2d6] = config; + queue.context[0x2db] = parameters; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "VGT_SHADER_STAGES_EN=0x200210d: VGT_TF_PARAM=0x"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, fields); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + } +} + void NggVertexWorkgroupTests() { auto queue = makeState(); queue.context[0x2d5] = 0x12010; @@ -2764,6 +2787,17 @@ void cmaskTests() { Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } +void uint8x2TargetTests() { + auto queue = makeState(); + queue.context[0x1c5] = 7; + queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (3u << 2u) | (4u << 8u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R8G8_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0xe4u && state.color.uintExport, "an 8_8 UINT color target was rejected"); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); + const auto alt = AgcDriver::Graphics::DecodeState(queue); + Require(alt.color.format == VK_FORMAT_R8G8_UINT && alt.color.componentMapping == 0xecu, "an 8_8 UINT SWAP_ALT color target was rejected"); +} + void uint8x4TargetTests() { auto queue = makeState(); queue.context[0x1c5] = 7; @@ -2805,6 +2839,78 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V return depthSampleResult; } +void msaaEnableClearDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto drawB = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x0020c002; + queue.context[0x292] = 2; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u; + queue.context[0x31c] = 0x8028; + queue.context[0x31d] = 0x12000; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + queue.context[0x201] = 0x00010002; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations, what + " did not keep the 4x depth target"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + auto queue = drawB(); + queue.context[0x200] = 0x711; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x333; + queue.context[0x10c] = 0x00ff0000; + queue.context[0x10d] = 0; + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's draw B lost its 4x color target"); + Require(state.stencilTest && !state.depthTest && state.depth && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.stencilFront.compareOp == VK_COMPARE_OP_ALWAYS, "PPSA18216's draw B (DB_DEPTH_CONTROL 0x711) lost its 4x stencil plane"); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.failOp == VK_STENCIL_OP_REPLACE && state.stencilFront.depthFailOp == VK_STENCIL_OP_REPLACE && state.stencilFront.writeMask == 0xffu && state.stencilFront.reference == 0u, "PPSA18216's draw B did not decode as a stencil clear to 0"); + requireCentered(state, "a 4x depth and stencil target rasterized without MSAA_ENABLE (PPSA18216's draw B)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused PPSA18216's draw B"); + queue.context[0x200] = 0x710; + state = DecodeState(queue); + Require(!state.depth && state.samples == 4 && state.colors.size() == 1, "draw B with DB_DEPTH_CONTROL 0x710 bound a depth target"); + queue.context[0x201] = 0x00050002u; + Require(!DecodeState(queue).depth, "DB_EQAA Z interpolation refused a 4x draw without a depth target"); + queue = drawB(); + state = DecodeState(queue); + Require(state.depthTest && state.colors.size() == 1, "a depth-tested 4x depth and color pair lost a target"); + requireCentered(state, "a depth-tested 4x depth and color pair rasterized without MSAA_ENABLE"); + queue.context[0x8e] = 0; + state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); + requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + queue = drawB(); + queue.context[0x201] = eqaa; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"); + } + queue = drawB(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); + queue = drawB(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + queue = drawB(); + queue.context[0x010] = 0xa2900987; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorBuffer; @@ -3065,7 +3171,7 @@ void multisampleTests() { const auto aliased = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); queue = multisampled(); queue.context[0x292] = 2; queue.context[0x302] = 0x44cc; @@ -8799,7 +8905,9 @@ int main(int argc, char** argv) { metadataPassTests(); cmaskTests(); uint16ExportTests(); + uint8x2TargetTests(); uint8x4TargetTests(); + msaaEnableClearDepthTests(); multisampleTests(); MsaaEnableSingleSampleTests(); multisampledDepthTests(); @@ -8809,6 +8917,7 @@ int main(int argc, char** argv) { depthCopyFormatTests(); quadPixelMaskTests(); ShaderStageTests(); + UnsupportedTessellationFieldTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); PolygonPrimitiveTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 34c28d4149..3b88132328 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -270,6 +271,89 @@ void RunGuestTextureResourceTests() { const std::array canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; result = DecodeTextureResource(canvas); Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Require(DescribeSurface(result).guestBytes == 0x190000, "4x render-target geometry must include fragment-aware padding for the PPSA99006 canvas"); + const auto canvasGeometry = DescribeSurface(result); + const std::array sonic{0x405fc080u, 0xc4700000u, 0x821bc3bfu, 0xe1b30facu, 0x00000000u, 0x00400030u, 0x406b0000u, 0x0040819fu}; + const auto sonicResource = DecodeTextureResource(sonic); + const auto sonicGeometry = DescribeSurface(sonicResource); + Require(sonicResource.width == 3840u && sonicResource.height == 2160u && sonicResource.samples == 8u && sonicResource.format == 71u, "the PPSA99902 multisampled color descriptor decoded incorrectly"); + Require(sonicGeometry.guestBytes == 0x1fe00000ull, "the PPSA99902 8x surface footprint omits fragments: " + std::to_string(sonicGeometry.guestBytes)); + Require(canvasGeometry.guestBytes == 0x190000u, "the 297x297 4x surface footprint does not use fragment-aware blocks"); + auto singleSample = sonicResource; + singleSample.samples = 1u; + singleSample.dimension = TextureDimension::k2D; + Require(DescribeSurface(singleSample).guestBytes == 0x3fc0000u, "single-sample surface geometry changed"); + struct FootprintCase { + TextureTileMode tile; + VkFormat format; + std::uint32_t elementBytes, width, height, samples, blocksPerRow; + std::uint64_t bytes; + }; + const std::array footprints{ + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R8_UINT, 1u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R16_UINT, 2u, 65u, 129u, 4u, 1u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 2u, 2u, 0x40000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 297u, 297u, 4u, 5u, 0x190000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32_SFLOAT, 8u, 3840u, 2160u, 8u, 120u, 0x1fe00000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32B32A32_UINT, 16u, 16384u, 16384u, 8u, 1024u, 0x800000000ull}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R16_UNORM, 2u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R32_SFLOAT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kStandard4KB, VK_FORMAT_R32_UINT, 4u, 17u, 33u, 4u, 2u, 0x6000u}, + FootprintCase{TextureTileMode::kStandard64KB, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u} + }; + for (const auto& test : footprints) { + auto resource = sonicResource; + const auto format = FindGuestTextureFormat(test.format, test.elementBytes); + Require(format.has_value(), "the multisample footprint test format has no guest encoding"); + resource.format = format.value(); + resource.width = test.width; + resource.height = test.height; + resource.samples = test.samples; + resource.tileMode = test.tile; + const auto geometry = DescribeSurface(resource); + const auto linearBytes = static_cast(test.width) * test.height * test.samples * test.elementBytes; + Require(geometry.guestBytes == test.bytes && geometry.layerBytes == test.bytes && geometry.sliceLinearBytes == linearBytes, "multisampled surface footprint or linear size disagrees with the reference"); + Require(geometry.layers == 1u && geometry.imageLayers == 1u && geometry.imageDepth == 1u && !geometry.thick && geometry.blockDepth == 1u && geometry.mips.size() == 1u, "multisampled surface geometry is not one thin 2D level"); + const auto& mip = geometry.mips.front(); + Require(mip.tiledOffset == 0u && mip.tiledSize == test.bytes && mip.linearOffset == 0u && mip.linearSize == linearBytes && mip.width == test.width && mip.height == test.height && mip.blocksPerRow == test.blocksPerRow && mip.pitchBytes == test.width * test.samples * test.elementBytes && !mip.tail && mip.tailX == 0u && mip.tailY == 0u, "multisampled mip geometry lost its sample pitch or block layout"); + } + auto unsupportedSurface = sonicResource; + for (const auto samples : {0u, 3u, 16u}) { + unsupportedSurface.samples = samples; + reject([&] { DescribeSurface(unsupportedSurface); }, "fragment count"); + } + unsupportedSurface = sonicResource; + unsupportedSurface.mipCount = 2u; + reject([&] { DescribeSurface(unsupportedSurface); }, "mip levels"); + unsupportedSurface = sonicResource; + unsupportedSurface.dimension = TextureDimension::k2DArray; + reject([&] { DescribeSurface(unsupportedSurface); }, "dimension or array"); + unsupportedSurface = sonicResource; + unsupportedSurface.tileMode = TextureTileMode::kStandard256B; + reject([&] { DescribeSurface(unsupportedSurface); }, "4 KiB or 64 KiB"); + for (const auto format : {56u, 71u}) { + for (const auto samples : {2u, 4u, 8u}) { + for (const auto extent : {std::array{1u, 1u}, std::array{17u, 5u}, std::array{297u, 297u}}) { + auto linear = sonicResource; + linear.tileMode = TextureTileMode::kLinear; + linear.format = format; + linear.samples = samples; + linear.width = extent[0]; + linear.height = extent[1]; + auto single = linear; + single.samples = 1u; + single.dimension = TextureDimension::k2D; + const auto expected = DescribeSurface(single); + const auto actual = DescribeSurface(linear); + Require(actual.guestBytes == expected.guestBytes && actual.layerBytes == expected.layerBytes && actual.sliceLinearBytes == expected.sliceLinearBytes && actual.mips.size() == expected.mips.size(), "linear multisampled descriptors must retain the existing geometry path"); + const auto& mip = actual.mips.front(); + const auto& baseline = expected.mips.front(); + Require(mip.tiledSize == baseline.tiledSize && mip.linearSize == baseline.linearSize && mip.pitchBytes == baseline.pitchBytes && mip.blocksPerRow == baseline.blocksPerRow, "linear multisampled descriptors changed existing mip geometry"); + } + } + } Fields fewerFragments = multisampled; fewerFragments.maxMip = 1; rejectFields(fewerFragments, "same sample count"); diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index 8901e04a35..d409fc1d07 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -56,12 +57,31 @@ std::uint64_t workgroupBytes(std::span words) { return bytes; } -RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { +std::set compareExchangeWidths(std::span words) { + std::map widths; + std::set found; + std::uint32_t faultFunction = 0; + bool inFaultFunction = false; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); + const auto instruction = words.subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffffu); + if (opcode == spv::OpName && std::string_view(reinterpret_cast(instruction.data() + 2)) == "record_bda_fault") faultFunction = instruction[1]; + if (opcode == spv::OpFunction) inFaultFunction = instruction[2] == faultFunction; + if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); + if (opcode == spv::OpAtomicCompareExchange && !inFaultFunction) found.insert(widths.at(instruction[1])); + offset += count; + } + return found; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { static constexpr std::array code{ 0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u, 0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u }; - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; static constexpr std::array userData{}; RecompileRequest request{}; @@ -73,7 +93,7 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; request.target.bdaAbiVersion = 1; - request.target.supportedCapabilities = capabilities; + request.target.supportedCapabilities = std::span(capabilities).first(int64Atomics ? 8u : 7u); request.target.supportedExtensions = extensions; request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; request.target.maxWorkgroupInvocations = 1024; @@ -83,13 +103,18 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { return Recompile(request); } -void verify(std::uint32_t waveSize, std::uint32_t limit) { - const auto result = compile(waveSize, limit); +void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { + const auto result = compile(waveSize, limit, int64Atomics); require(!result.spirv.empty(), "LDS atomic program did not compile"); const auto declared = workgroupBytes(result.spirv.Words()); require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); - require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + require(result.workgroupMemoryDwords == (limit < 68u ? 18u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); + const auto widths = compareExchangeWidths(result.spirv.Words()); + const bool native = int64Atomics && limit < 68u; + require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); + require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); + require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit)); } } @@ -98,14 +123,16 @@ int main() { unsigned failures = 0; for (const auto waveSize : {32u, 64u}) { for (const auto limit : {64u, 68u, 72u}) { - const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); - try { - verify(waveSize, limit); - std::cout << "LDS lock budget: " << name << " passed\n"; - } catch (const std::exception& error) { - ++failures; - const std::string message(error.what()); - std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + for (const bool int64Atomics : {false, true}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit) + (int64Atomics ? " int64 atomics" : ""); + try { + verify(waveSize, limit, int64Atomics); + std::cout << "LDS lock budget: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp new file mode 100644 index 0000000000..9b5b6531f2 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -0,0 +1,724 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::PerformanceOverlay; +using AgcDriver::PresentationPass; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; + +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("performance-overlay device selection CPU tests passed"); +} + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + timestampBits = families[family].timestampValidBits; + const auto enumerateExtensions = function("vkEnumerateDeviceExtensionProperties"); + Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector extensions(count); + Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); + memoryBudget = std::any_of(extensions.begin(), extensions.end(), [](const VkExtensionProperties& extension) { return std::string(extension.extensionName) == VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }); + const char* const budgetExtension = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = memoryBudget ? 1 : 0; + device.ppEnabledExtensionNames = memoryBudget ? &budgetExtension : nullptr; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + if (memoryBudget) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + name = properties.deviceName; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + const std::string& Name() const { return name; } + std::uint32_t TimestampBits() const { return timestampBits; } + bool MemoryBudget() const { return memoryBudget; } + + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Selected GPU name=" << properties.deviceName << " vendor=" << properties.vendorID << " device=" << properties.deviceID << '\n'; + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the performance-overlay fixture physical device does not match ANYPS5_GPU"); + } + +private: + template + TFunction function(const char* functionName) const { + const auto result = reinterpret_cast(instanceProc(instance, functionName)); + Require(result != nullptr, functionName); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; + std::string name; + std::uint32_t timestampBits = 0; + bool memoryBudget = false; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage performance overlay test"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory performance overlay test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory performance overlay test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.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); +} + +std::vector wordsOf(Buffer& buffer, VkExtent2D extent) { + std::vector words(static_cast(extent.width) * extent.height); + std::memcpy(words.data(), buffer.Bytes().data(), words.size() * sizeof(std::uint32_t)); + return words; +} + +void setEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + setenv(name, value, 1); +#endif +} + +bool inside(const OverlayRect& rect, std::int32_t x, std::int32_t y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + +OverlayContent sixtyFps() { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (int i = 0; i <= 60; ++i) statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, 0, 0); + auto content = AgcDriver::ComposeOverlay(statistics, false, false); + Require(content.above.front() == "FPS: 60.0", "60 presents a second compose as '" + content.above.front() + "'"); + return content; +} + +std::vector clearAndOverlay(const Context& context, PerformanceOverlay* overlay, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, const VkClearColorValue& clear, const OverlayContent& content, OverlayCorner corner, OverlayRect* bounds) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + if (overlay != nullptr) { + *bounds = overlay->Record(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, corner); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + if (overlay != nullptr) overlay->Release(0); + return wordsOf(readback, extent); +} + +void onlyInside(const std::vector& reference, const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, const std::string& what, bool everyPixel = true) { + std::size_t changed = 0; + for (std::int32_t y = 0; y < static_cast(extent.height); ++y) { + for (std::int32_t x = 0; x < static_cast(extent.width); ++x) { + const auto index = static_cast(y) * extent.width + static_cast(x); + if (!inside(bounds, x, y)) { + Require(drawn[index] == reference[index], what + ": the overlay changed pixel " + std::to_string(x) + "," + std::to_string(y) + " outside its panel"); + continue; + } + if (drawn[index] != reference[index]) ++changed; + else Require(!everyPixel, what + ": the overlay left pixel " + std::to_string(x) + "," + std::to_string(y) + " of its panel unchanged"); + } + } + Require(changed != 0, what + ": the overlay changed nothing"); +} + +int channel(std::uint32_t word, unsigned shift) { + return static_cast((word >> shift) & 0xffu); +} + +void requireTextDrawn(const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, std::int32_t scale, std::uint32_t frameWord, const std::string& line, std::int32_t row, std::size_t first = 0, std::size_t count = std::string::npos) { + const auto end = count == std::string::npos ? line.size() : std::min(line.size(), first + count); + for (std::size_t index = first; index < end; ++index) { + const auto& glyph = AgcDriver::OverlayGlyph(line[index]); + const auto column = static_cast(index); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (row + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(frameWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } +} + +AgcDriver::OverlayDetails measuredDetails(const Context& context, VkExtent2D display, VkExtent2D window) { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::ReadVramBudget(context); + details.ram = AgcDriver::ReadProcessPrivateBytes(); + details.display = display; + details.window = window; + return details; +} + +AgcDriver::OverlayDetails fixedDetails(VkExtent2D display, VkExtent2D window) { + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = display; + details.window = window; + return details; +} + +OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'200'000u; + counters.driverNanoseconds = i * 1'300'000u; + counters.draws = i * 1240; + counters.dispatches = i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); + } + auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); + Require(content.above.size() == 3 && content.above[0] == "PROFILED" && content.above[1] == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + return content; +} + +void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{1280, 720}; + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); + onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const std::string digits = "60.0"; + const auto firstColumn = content.above.front().find(digits); + Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); + requireTextDrawn(drawn, extent, bounds, scale, reference.front(), content.above.front(), OverlayMetrics::Padding, firstColumn, digits.size()); + std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); +} + +void hdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const auto format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) == 0) { + std::printf("A2B10G10R10 cannot be blended into here; HDR10 overlay not exercised\n"); + return; + } + const VkExtent2D extent{1920, 1080}; + VkClearColorValue clear{}; + clear.float32[0] = 0.4f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.6f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, &bounds); + onlyInside(reference, drawn, extent, bounds, "HDR10 1920x1080"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto& six = AgcDriver::OverlayGlyph('6'); + const auto column = static_cast(content.above.front().find('6')); + std::size_t checked = 0; + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::GlyphWidth; ++gx) { + if ((six[static_cast(gy)] & (0x10u >> gx)) == 0) continue; + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + scale / 2; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + scale / 2; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + for (const unsigned shift : {0u, 10u, 20u}) { + const int code = static_cast((word >> shift) & 0x3ffu); + Require(std::abs(code - 594) <= 1, "HDR10 text is not at 203 nits: PQ code " + std::to_string(code) + " for 594"); + } + Require((word >> 30u) == (reference.front() >> 30u), "HDR10 text changed the alpha"); + ++checked; + } + } + Require(checked != 0, "no HDR10 text pixel was checked"); + std::printf("HDR10 overlay: text at PQ code 594 (203 nits), nothing changed outside the %dx%d panel\n", bounds.width, bounds.height); +} + +void cornerTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{640, 360}; + VkClearColorValue clear{}; + clear.float32[1] = 1.0f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::TopRight, OverlayCorner::BottomLeft, OverlayCorner::BottomRight}) { + OverlayRect bounds{}; + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, corner, &bounds); + const auto expected = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + Require(bounds.x == expected.x && bounds.y == expected.y && bounds.width == expected.width && bounds.height == expected.height, "the drawn panel is not where the layout puts it"); + onlyInside(reference, drawn, extent, bounds, "corner " + std::to_string(static_cast(corner))); + } + std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); +} + +void fullPanelTest(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1920, 1080}; + auto details = measuredDetails(context, extent, extent); + Require(details.ram.has_value(), "the process's private memory cannot be read"); + Require(details.vram.has_value() == device.MemoryBudget(), device.MemoryBudget() ? "VK_EXT_memory_budget gives no device-local budget" : "VRAM is read without VK_EXT_memory_budget"); + auto withoutBudget = context; + withoutBudget.memoryProperties2 = nullptr; + Require(!AgcDriver::ReadVramBudget(withoutBudget).has_value(), "VRAM is read without vkGetPhysicalDeviceMemoryProperties2"); + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = fullPanel(details); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "the full panel is not drawn where its layout puts it"); + onlyInside(reference, drawn, extent, bounds, "full panel 1920x1080"); + std::int32_t row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + std::printf("full panel at 1920x1080: %dx%d at %d,%d (scale %d, %.1f%% of the height), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale, 100.0 * bounds.height / extent.height); + for (const auto& line : content.above) std::printf(" %s\n", line.c_str()); + std::printf(" (graph of %zu frame times)\n", content.frameTimes.size()); + for (const auto& line : content.below) std::printf(" %s\n", line.c_str()); +} + +void notePresentTest(const Device& device) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + PerformanceOverlay overlay(context, 1); + overlay.NotePresent(AgcDriver::OverlayStatistics::Clock::now(), VkExtent2D{640, 360}); + const auto& details = overlay.Details(); + Require(details.ram.has_value() && details.vram.has_value() == device.MemoryBudget() && details.display.width == 640 && details.display.height == 360, "a shown frame did not sample the memory or keep the display buffer size"); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + OverlayRect bounds{}; + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + } + overlay.Release(0); + auto shown = details; + shown.window = extent; + const auto expected = AgcDriver::ComposeOverlay(overlay.Statistics(), AgcDriver::PerformanceCounters::GpuEnabled(), AgcDriver::PerformanceCounters::DriverEnabled(), shown); + Require(expected.below.back() == "RES: 640X360 / 1280X720", "the RES line reads '" + expected.below.back() + "'"); + const auto layout = AgcDriver::LayOutOverlay(expected, extent, PerformanceOverlay::Corner()); + Require(bounds.height == layout.bounds.height && bounds.width == layout.bounds.width, "a shown frame's panel does not hold the sampled VRAM, RAM and RES lines"); + std::printf("shown frame: memory sampled, %zu lines below the graph, the last '%s'\n", expected.below.size(), expected.below.back().c_str()); +} + +class Presenter { +public: + explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { + Buffer upload(context, static_cast(256) * 144 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::uint32_t seed = 0x2545f491u; + for (auto& byte : upload.Bytes()) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {256, 144, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + batch.SubmitAndWait(); + } + + std::vector Drawn(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + pass.Bind(0, source.Handle(), 256, 144, Bgra8, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + overlay.Release(0); + return wordsOf(readback, Extent); + } + + std::vector Blitted(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); + if (visible) { + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + overlay.Release(0); + return wordsOf(readback, Extent); + } + + static constexpr VkExtent2D Extent{400, 300}; + +private: + const Context& context; + PresentationPass pass; + PerformanceOverlay overlay; + Image source; +}; + +void disabledIsUnchangedTest(const Context& context) { + Require(!PerformanceOverlay::Visible(), "the performance overlay is shown without ANYPS5_OVERLAY=1"); + Presenter presenter(context); + OverlayRect unused{}; + const auto drawnBefore = presenter.Drawn(&unused); + const auto blittedBefore = presenter.Blitted(&unused); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Visible(), "F10 does not show the performance overlay"); + OverlayRect bounds{}; + onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false); + onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again"); + Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden"); + Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden"); + std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n"); +} + +void costReport(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + if (device.TimestampBits() == 0) { + std::printf("the graphics queue has no timestamps; overlay cost not measured\n"); + return; + } + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = 2; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + const auto sampled = std::chrono::steady_clock::now(); + for (int run = 0; run < 100; ++run) static_cast(AgcDriver::ReadVramBudget(context)); + const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; + std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); + for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + const auto content = fullPanel(fixedDetails(extent, extent)); + std::vector gpuMs; + std::vector cpuMs; + for (int run = 0; run < 12; ++run) { + CommandBatch batch(context); + const auto commands = batch.Handle(); + context.Function("vkCmdResetQueryPool")(commands, queries, 0, 2); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queries, 0); + const auto recordStart = std::chrono::steady_clock::now(); + static_cast(overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, OverlayCorner::TopLeft)); + cpuMs.push_back(std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count()); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, 1); + batch.SubmitAndWait(); + overlay.Release(0); + std::array stamps{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0, 2, sizeof(stamps), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults overlay cost"); + if (run >= 2) gpuMs.push_back(static_cast(stamps[1] - stamps[0]) * context.limits.timestampPeriod / 1e6); + } + std::sort(gpuMs.begin(), gpuMs.end()); + std::sort(cpuMs.begin(), cpuMs.end()); + std::printf("full panel cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); +} + +} + +int main(int argc, char** argv) { + try { + const std::string_view mode = argc > 1 ? argv[1] : ""; + Require(argc <= 2 && (mode.empty() || mode == "device-selection" || mode == "device-selection-cpu"), "unknown performance-overlay test mode"); + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } + setEnvironment("ANYPS5_OVERLAY", ""); + setEnvironment("ANYPS5_OVERLAY_POS", ""); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + device->VerifySelection(); + if (mode == "device-selection") return 0; + const auto& context = device->GetContext(); + { + PerformanceOverlay overlay(context, 1); + sdrOverlayTest(context, overlay); + hdrOverlayTest(context, overlay); + cornerTest(context, overlay); + fullPanelTest(*device, overlay); + costReport(*device, overlay); + } + notePresentTest(*device); + disabledIsUnchangedTest(context); + std::cout << "performance overlay tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp new file mode 100644 index 0000000000..b829a08c01 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -0,0 +1,500 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::OverlayStatistics; +using Clock = OverlayStatistics::Clock; +using namespace std::chrono_literals; + +constexpr Clock::time_point Start = Clock::time_point{} + std::chrono::hours(1); + +void require(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +bool within(double value, double expected, double tolerance = 1e-9) { + return std::abs(value - expected) <= tolerance; +} + +void fpsWindowTests() { + OverlayStatistics statistics; + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a new overlay shows statistics"); + statistics.AddPresent(Start, 0, 0); + require(statistics.Fps() == 0, "one present reads as a frame rate"); + for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); + require(within(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + statistics.AddPresent(Start + 1700ms, 0, 0); + require(within(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + statistics.AddPresent(Start + 3700ms, 0, 0); + require(within(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); + statistics.AddPresent(Start + 3750ms, 0, 0); + require(within(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + bool refused = false; + try { + statistics.AddPresent(Start + 3740ms, 0, 0); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a present earlier than the previous one was accepted"); + statistics.Reset(); + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a reset kept statistics"); +} + +void frameRingTests() { + OverlayStatistics statistics; + auto at = Start; + statistics.AddPresent(at, 0, 0); + for (int i = 1; i <= 4; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto partial = statistics.FrameTimes(); + require(partial.size() == 4 && within(partial[0], 1) && within(partial[3], 4), "the frame times of a short run are not in present order"); + for (int i = 5; i <= 150; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto times = statistics.FrameTimes(); + require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); + for (std::size_t i = 0; i < times.size(); ++i) require(within(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(within(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + const auto range = statistics.FrameTimeRange(); + require(within(range.minimum, 31) && within(range.maximum, 150) && within(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); +} + +void counterTests() { + OverlayStatistics statistics; + constexpr std::uint64_t Earlier = 1'000'000'000u; + for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + statistics.AddPresent(Start + 61 * 16ms, 5, 5); + require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); +} + +void formatTests() { + const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); + const std::pair rates[] = {{0.5, "0.50"}, {1000.0 / 12000.0, "0.08"}, {0.05, "0.05"}, {0.004, "0.00"}, {0.994, "0.99"}, {0.996, "1.0"}, {1.5, "1.5"}, {59.94, "59.9"}, {144.4, "144"}, {12345, "9999"}, {0, "0.0"}, {-1, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : rates) require(AgcDriver::FormatOverlayRate(value) == expected, "a rate of " + std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayRate(value) + "', not '" + expected + "'"); +} + +void lowRateTests() { + OverlayStatistics slow; + constexpr std::uint64_t Gpu[] = {30'000'000u, 45'000'000u, 20'000'000u}; + constexpr std::uint64_t Driver[] = {12'000'000u, 20'000'000u, 8'000'000u}; + std::uint64_t gpuTotal = 0; + std::uint64_t driverTotal = 0; + slow.AddPresent(Start, 0, 0); + require(slow.Fps() == 0 && slow.GpuMsPerFrame() == 0 && slow.DriverMsPerFrame() == 0, "a single present reads as a frame"); + for (int i = 0; i < 3; ++i) { + gpuTotal += Gpu[i]; + driverTotal += Driver[i]; + slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); + require(within(slow.Fps(), 0.5) && within(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(within(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && within(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + } + const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); + require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); + require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); + + OverlayStatistics glacial; + glacial.AddPresent(Start, 0, 0); + for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); + require(within(glacial.Fps(), 1000.0 / 12000.0) && within(glacial.LastFrameMs(), 12000) && within(glacial.GpuMsPerFrame(), 250) && within(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); + require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); + require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); + require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); + const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); + std::size_t bars = 0; + for (const auto& rect : layout.rects) { + if (rect.color != static_cast(AgcDriver::OverlayColor::Graph)) continue; + ++bars; + require(rect.height == OverlayMetrics::GraphHeight * layout.scale / 2, "steady 12 s frames do not reach half the graph"); + } + require(bars == 5, "the graph has " + std::to_string(bars) + " bars for five 12 s frames"); + + OverlayStatistics rising; + auto at = Start; + std::uint64_t total = 0; + rising.AddPresent(at, 0, 0); + const auto step = [&](Clock::duration interval, std::uint64_t gpuNanoseconds) { + at += interval; + total += gpuNanoseconds; + rising.AddPresent(at, total, 0); + }; + const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { + require(within(rising.Fps(), fps) && within(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + }; + step(2s, 40'000'000u); + check(0.5, 40, "0.50", "a 2 s frame"); + step(1250ms, 30'000'000u); + check(0.8, 30, "0.80", "a 1.25 s frame"); + step(1001ms, 20'000'000u); + check(1000.0 / 1001.0, 20, "1.0", "a 1.001 s frame, the last second holding one present"); + step(1000ms, 10'000'000u); + check(1.0, 10, "1.0", "a 1 s frame, the last second holding two presents"); + step(750ms, 6'000'000u); + check(1.0 / 0.75, 6, "1.3", "a 750 ms frame"); + for (int i = 0; i < 8; ++i) step(250ms, i % 2 == 0 ? 2'000'000u : 6'000'000u); + check(4.0, 4.0, "4.0", "four presents a second, alternating 2 and 6 ms of GPU time"); + step(3s, 9'000'000u); + check(1.0 / 3.0, 9, "0.33", "a 3 s frame after the fast ones"); +} + +void settingsTests() { + const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr); + require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); + require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); + require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay"); + const std::pair corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}}; + for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner"); + const auto refused = [](const char* enabled, const char* position, const char* variable) { + try { + static_cast(AgcDriver::ParseOverlaySettings(enabled, position)); + } catch (const std::exception& error) { + return std::string(error.what()).find(variable) != std::string::npos; + } + return false; + }; + require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named"); + require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named"); +} + +void composeTests() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 100; ++i) statistics.AddPresent(Start + i * 10ms, i * 2'000'000u, i * 500'000u); + const auto plain = AgcDriver::ComposeOverlay(statistics, false, false); + require(plain.above.size() == 2 && plain.above[0] == "FPS: 100" && plain.above[1] == "FRAME: 10.0 MS", "the frame lines read '" + plain.above[0] + "' and '" + plain.above[1] + "'"); + require(plain.below.size() == 1 && plain.below[0] == "MIN/AVG/MAX: 10.0/10.0/10.0", "the graph line reads '" + plain.below[0] + "'"); + require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); + const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); + require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + std::vector marked{"PROFILED"}; + marked.insert(marked.end(), plain.above.begin(), plain.above.end()); + require(profiled.above == marked && AgcDriver::ComposeOverlay(statistics, true, false).above == marked && AgcDriver::ComposeOverlay(statistics, false, true).above == marked, "a profiled panel does not start with PROFILED above the unprofiled lines: it starts with '" + profiled.above.front() + "'"); + require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); +} + +std::vector coverage(const AgcDriver::OverlayLayout& layout, std::uint32_t color) { + const auto& bounds = layout.bounds; + std::vector covered(static_cast(bounds.width) * static_cast(bounds.height), 0); + for (const auto& rect : layout.rects) { + if (rect.color != color) continue; + for (std::int32_t y = rect.y; y < rect.y + rect.height; ++y) { + for (std::int32_t x = rect.x; x < rect.x + rect.width; ++x) ++covered[static_cast(y - bounds.y) * static_cast(bounds.width) + static_cast(x - bounds.x)]; + } + } + return covered; +} + +void requireLineDrawn(const AgcDriver::OverlayLayout& layout, const std::vector& text, const std::string& line, std::int32_t top) { + const auto s = layout.scale; + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } +} + +void layoutTests() { + OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; + for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}, {{2560, 720}, 2}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, content) == scale, std::to_string(extent.width) + "x" + std::to_string(extent.height) + " draws the overlay at scale " + std::to_string(AgcDriver::OverlayScale(extent, content)) + ", not " + std::to_string(scale)); + + const VkExtent2D extent{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + const auto s = layout.scale; + const auto margin = OverlayMetrics::Margin * s; + require(layout.bounds.x == margin && layout.bounds.y == margin && layout.bounds.width == (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * s, "the top-left panel is not at the margin"); + require(layout.rects.front().x == layout.bounds.x && layout.rects.front().width == layout.bounds.width && layout.rects.front().color == static_cast(AgcDriver::OverlayColor::Background), "the background is not drawn first over the whole panel"); + for (const auto& rect : layout.rects) { + require(rect.width > 0 && rect.height > 0 && rect.x >= layout.bounds.x && rect.y >= layout.bounds.y && rect.x + rect.width <= layout.bounds.x + layout.bounds.width && rect.y + rect.height <= layout.bounds.y + layout.bounds.height, "a rectangle leaves the panel"); + } + + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; + requireLineDrawn(layout, text, content.above[0], lineTop(0)); + requireLineDrawn(layout, text, content.above[1], lineTop(1)); + requireLineDrawn(layout, text, content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + + std::vector bars; + for (const auto& rect : layout.rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) bars.push_back(rect); + } + require(bars.size() == content.frameTimes.size(), "the graph has " + std::to_string(bars.size()) + " bars for 40 frames"); + const auto graphBottom = layout.bounds.y + (lineTop(2) + OverlayMetrics::GraphHeight) * s; + const auto full = OverlayMetrics::GraphHeight * s; + require(bars.back().x == layout.bounds.x + (OverlayMetrics::Padding + static_cast(OverlayStatistics::FrameHistory) - 1) * s, "the newest frame is not at the right end of the graph"); + for (std::size_t i = 0; i + 1 < bars.size(); ++i) require(bars[i + 1].x == bars[i].x + s && bars[i].width == s, "the graph bars are not side by side"); + require(bars.back().height == full && bars.back().y + bars.back().height == graphBottom, "the slowest frame does not fill the graph"); + require(bars.front().height == static_cast(std::lround(10.0 / 50.0 * full)) && bars.front().y + bars.front().height == graphBottom, "a 10 ms frame is not a fifth of a 50 ms one"); + OverlayContent steady = content; + std::fill(steady.frameTimes.begin(), steady.frameTimes.end(), 16.0); + for (const auto& rect : AgcDriver::LayOutOverlay(steady, extent, OverlayCorner::TopLeft).rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) require(rect.height == full / 2, "steady frames do not reach half the graph"); + } + + const std::pair> corners[] = {{OverlayCorner::TopRight, {true, false}}, {OverlayCorner::BottomLeft, {false, true}}, {OverlayCorner::BottomRight, {true, true}}}; + for (const auto& [corner, sides] : corners) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + const auto x = sides.first ? static_cast(extent.width) - margin - placed.width : margin; + const auto y = sides.second ? static_cast(extent.height) - margin - placed.height : margin; + require(placed.x == x && placed.y == y && placed.width == layout.bounds.width && placed.height == layout.bounds.height, "a corner places the panel elsewhere"); + } + OverlayContent profiled = content; + profiled.below.push_back("GPU: 2.0 MS"); + require(AgcDriver::LayOutOverlay(profiled, extent, OverlayCorner::TopLeft).bounds.height == layout.bounds.height + OverlayMetrics::LinePitch * s, "a profiled line does not add one line to the panel"); + + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph('q')); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a character without a glyph was drawn"); +} + +constexpr std::uint64_t GiB = 1ull << 30u; + +OverlayStatistics countedStatistics() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'000'000u; + counters.driverNanoseconds = i * 1'500'000u; + counters.draws = 5000 + i * 1240; + counters.dispatches = 70 + i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(Start + i * 16ms, counters); + } + return statistics; +} + +AgcDriver::OverlayDetails fullDetails() { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = {1920, 1080}; + details.window = {2560, 1440}; + return details; +} + +void packetStatisticsTests() { + const auto statistics = countedStatistics(); + require(within(statistics.DrawsPerFrame(), 1240) && within(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + auto stalled = statistics; + auto counters = stalled.Latest(); + counters.draws += 900; + counters.dispatches += 40; + counters.pipelines += 7; + stalled.AddPresent(Start + 3000ms, counters); + require(within(stalled.DrawsPerFrame(), 900) && within(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); + auto restarted = statistics; + counters = restarted.Latest(); + counters.draws = 3; + restarted.AddPresent(Start + 61 * 16ms, counters); + require(restarted.DrawsPerFrame() == 0, "a draw counter that went back is read as draws"); + OverlayStatistics empty; + require(empty.PipelinesInWindow() == 0 && empty.Latest().draws == 0, "an overlay without presents shows packet counts"); +} + +void packetCounterTests() { + const auto before = AgcDriver::PacketCounters::Sum(); + auto& counters = AgcDriver::PacketCounters::ThisThread(); + require(&counters == &AgcDriver::PacketCounters::ThisThread(), "one thread got two packet counters"); + for (int i = 0; i < 1240; ++i) counters.CountDraw(); + for (int i = 0; i < 85; ++i) counters.CountDispatch(); + bool separate = false; + std::thread worker([&] { + auto& own = AgcDriver::PacketCounters::ThisThread(); + separate = &own != &counters; + for (int i = 0; i < 1000; ++i) own.CountDraw(); + own.CountDispatch(); + }); + worker.join(); + require(separate, "two workers share packet counters"); + const auto after = AgcDriver::PacketCounters::Sum(); + require(after.draws - before.draws == 2240 && after.dispatches - before.dispatches == 86, "two workers' 2240 draws and 86 dispatches sum to " + std::to_string(after.draws - before.draws) + " and " + std::to_string(after.dispatches - before.dispatches)); + std::thread next([] { AgcDriver::PacketCounters::ThisThread().CountDraw(); }); + next.join(); + require(AgcDriver::PacketCounters::Sum().draws == after.draws + 1, "the counts of a worker that ended were lost, or a new worker restarted them"); +} + +void countFormatTests() { + const std::pair counts[] = {{0, "0"}, {0.4, "0"}, {0.5, "1"}, {85, "85"}, {1239.6, "1240"}, {9999, "9999"}, {9999.5, "10K"}, {12345, "12K"}, {999999, "999K"}, {1e6, "1M"}, {2.5e9, "999M"}, {-3, "0"}, {std::nan(""), "0"}, {INFINITY, "0"}}; + for (const auto& [value, expected] : counts) require(AgcDriver::FormatOverlayCount(value) == expected, std::to_string(value) + " is counted as '" + AgcDriver::FormatOverlayCount(value) + "', not '" + expected + "'"); + const std::pair sizes[] = {{0, "0.0"}, {static_cast(6.1 * GiB), "6.1"}, {static_cast(7.8 * GiB), "7.8"}, {GiB / 2, "0.5"}, {1536 * GiB, "1536"}, {20000 * GiB, "9999"}}; + for (const auto& [bytes, expected] : sizes) require(AgcDriver::FormatOverlayGigabytes(bytes) == expected, std::to_string(bytes) + " bytes are shown as '" + AgcDriver::FormatOverlayGigabytes(bytes) + "' GB, not '" + expected + "'"); +} + +void detailComposeTests() { + const auto statistics = countedStatistics(); + const auto full = AgcDriver::ComposeOverlay(statistics, true, true, fullDetails()); + const std::vector expected{"MIN/AVG/MAX: 16.0/16.0/16.0", "GPU: 4.0 MS", "DRIVER: 1.5 MS", "DRAWS: 1240 DISPATCH: 85", "SHADERS: 3 TOTAL: 1503", "VRAM: 6.1/7.8 GB", "RAM: 3.2 GB", "RES: 1920X1080 / 2560X1440"}; + require(full.below == expected, "the full panel's lines below the graph read '" + (full.below.empty() ? std::string() : full.below.back()) + "' last, of " + std::to_string(full.below.size())); + for (const auto& line : full.below) require(line.size() <= static_cast(OverlayMetrics::Columns), "'" + line + "' is wider than the panel"); + + OverlayStatistics uncounted; + for (int i = 0; i <= 60; ++i) uncounted.AddPresent(Start + i * 16ms, 0, 0); + const auto plain = AgcDriver::ComposeOverlay(uncounted, false, false); + const auto none = AgcDriver::ComposeOverlay(uncounted, false, false, AgcDriver::OverlayDetails{}); + require(none.above == plain.above && none.below == plain.below && none.below.size() == 1, "a panel without packets, pipelines, memory or a display buffer shows more than the first lines"); + + auto details = fullDetails(); + details.vram.reset(); + details.ram.reset(); + details.display = {}; + const auto partial = AgcDriver::ComposeOverlay(statistics, false, false, details); + require(partial.below.size() == 3 && partial.below.back().rfind("SHADERS", 0) == 0, "a line is shown without its data: the last line is '" + partial.below.back() + "'"); + + details = fullDetails(); + details.display = {3840, 2160}; + details.window = {16384, 16384}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RES: 3840X2160/16384X16384", "a long RES line is not written without spaces"); + details.display = {12345, 12345}; + details.window = {12345, 12345}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "a RES line wider than the panel is shown"); + details = fullDetails(); + details.window = {}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "RES is shown without a window size"); + auto large = countedStatistics(); + auto counters = large.Latest(); + counters.draws += 12345 * 2; + counters.pipelines = 123456; + large.AddPresent(Start + 62 * 16ms, counters); + const auto busy = AgcDriver::ComposeOverlay(large, false, false); + require(busy.below.size() == 3 && busy.below[2] == "SHADERS: 121K TOTAL: 123K", "large counts are not shortened: '" + busy.below.back() + "'"); +} + +void fullPanelLayoutTests() { + auto content = AgcDriver::ComposeOverlay(countedStatistics(), true, true, fullDetails()); + content.below[3] = "DRAWS: 9999 DISPATCH: 12K"; + content.below[4] = "SHADERS: 999M TOTAL: 9999"; + const VkExtent2D hd{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, hd, OverlayCorner::TopLeft); + require(layout.scale == 2 && layout.bounds.height * 3 <= static_cast(hd.height), "the full panel at 1080p is " + std::to_string(layout.bounds.height) + " pixels tall at scale " + std::to_string(layout.scale) + ", more than a third of the window"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + std::int32_t top = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + top += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + require(layout.bounds.height == (top - OverlayMetrics::LinePitch + OverlayMetrics::GlyphHeight + OverlayMetrics::Padding) * layout.scale, "the panel does not end one padding below its last line"); + const std::pair scales[] = {{{1280, 720}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::BottomRight); + require(placed.scale == scale && placed.bounds.x >= 0 && placed.bounds.y >= 0 && placed.bounds.y + placed.bounds.height <= static_cast(extent.height), "the full panel does not fit " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " at scale " + std::to_string(scale)); + } + const std::string added = "BCHKLOTW"; + for (const char letter : added) { + const auto& glyph = AgcDriver::OverlayGlyph(letter); + require(std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }) && std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + letter + "' is empty or wider than 5 pixels"); + for (const char other : std::string("0123456789ADEFGIMNPRSUVX") + added) { + if (other != letter) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + letter + "' and '" + other + "' have the same glyph"); + } + } +} + +void sampleTests() { + require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); + require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); + static std::uint8_t* volatile touched = nullptr; + const auto before = AgcDriver::ReadProcessPrivateBytes(); + require(before.has_value() && *before != 0, "the process's private memory cannot be read"); + constexpr std::size_t Bytes = std::size_t{64} << 20u; + const auto block = std::make_unique(Bytes); + touched = block.get(); + std::memset(touched, 0x5a, Bytes); + const auto after = AgcDriver::ReadProcessPrivateBytes(); + require(after.has_value() && *after >= *before + Bytes * 3 / 4, "touching 64 MiB raised the private memory from " + std::to_string(*before) + " to " + std::to_string(after.value_or(0)) + " bytes"); + require(touched[Bytes - 1] == 0x5a, "the touched block was not written"); +} + +void costReport() { + auto& counters = AgcDriver::PacketCounters::ThisThread(); + constexpr int Increments = 20'000'000; + const auto counted = Clock::now(); + for (int i = 0; i < Increments; ++i) counters.CountDraw(); + const auto countNs = std::chrono::duration(Clock::now() - counted).count() / Increments; + const auto summed = Clock::now(); + for (int i = 0; i < 1000; ++i) static_cast(AgcDriver::PacketCounters::Sum()); + const auto sumUs = std::chrono::duration(Clock::now() - summed).count() / 1000; + const auto sampled = Clock::now(); + for (int i = 0; i < 100; ++i) static_cast(AgcDriver::ReadProcessPrivateBytes()); + const auto ramUs = std::chrono::duration(Clock::now() - sampled).count() / 100; + std::printf("hidden cost: %.2f ns per counted draw or dispatch packet; shown: %.2f us to sum the packet counters per frame, %.1f us to read the private memory once a second\n", countNs, sumUs, ramUs); +} + +void paletteTests() { + const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); + const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(within(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); + for (std::size_t channel = 0; channel < 3; ++channel) require(within(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); +} + +} + +int main() { + try { + fpsWindowTests(); + frameRingTests(); + counterTests(); + formatTests(); + lowRateTests(); + settingsTests(); + composeTests(); + layoutTests(); + paletteTests(); + packetStatisticsTests(); + packetCounterTests(); + countFormatTests(); + detailComposeTests(); + fullPanelLayoutTests(); + sampleTests(); + costReport(); + std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp index e00241e373..f345d9f076 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp @@ -65,6 +65,8 @@ void CheckEveryHeap() { continue; } const auto shape = AgcDriver::Graphics::PlaceholderImageShapeOf(heap); + const auto withoutStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, false); + const auto withStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, true); const auto where = " for heap " + std::to_string(heap); const bool storage = resourceClass == ImageResourceClass::Storage; const bool multisampled = dimension == RdnaImageDimension::Dim2DMsaa || dimension == RdnaImageDimension::Dim2DMsaaArray; @@ -73,6 +75,10 @@ void CheckEveryHeap() { Require(shape.format == Format(image), "placeholder format disagrees with the numeric class" + where); Require(shape.viewType == ViewType(dimension), "placeholder view type disagrees with the dimension" + where); Require((shape.samples != VK_SAMPLE_COUNT_1_BIT) == multisampled, "placeholder sample count disagrees with multisampling" + where); + Require(withStorageMsaa.has_value(), "an enabled device lost a placeholder image shape" + where); + Require(withoutStorageMsaa.has_value() == !(storage && multisampled), "a device without storage-image MSAA exposes an unsupported placeholder" + where); + if (withoutStorageMsaa) Require(withoutStorageMsaa->samples == shape.samples && withoutStorageMsaa->viewType == shape.viewType && withoutStorageMsaa->usage == shape.usage, + "a device-specific placeholder changed the typed heap shape" + where); Require(((shape.features & VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT) != 0) == atomic, "placeholder format features disagree with atomics" + where); heaps.insert(heap); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 00359e947e..64255959a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -805,6 +805,22 @@ void testEventWrite() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30"); + for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) { + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); + for (std::uint32_t index = 1; index < 8; ++index) { + if (index == 3) continue; + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index"); + } + for (const auto index : {3u, 8u, 9u, 10u, 11u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented"); + } + for (const auto index : {12u, 13u, 14u, 15u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits"); + } + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size"); + } } void testAcquireMem() { @@ -909,6 +925,8 @@ void testDriverSubmission() { makePacket(0x46, {0x2a}), makePacket(0x46, {0x72c}), makePacket(0x46, {0x2e}), + makePacket(0x46, {0x01}), + makePacket(0x46, {0x20}), makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}), makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}), makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())}) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2e9cd49f46..ec87d09524 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2531,8 +2531,9 @@ void resourceReadTests(const Device& device, Recorder& recorder) { const auto commands = snapshotRecorder.Commands(); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); const auto copy = snapshotContext.Function("vkCmdCopyBuffer"); - VkBufferCopy region{0, 0, elementBytes}; + VkBufferCopy region{first->snapshots[0].offset, 0, elementBytes}; copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); + region.srcOffset = second->snapshots[0].offset; region.dstOffset = elementBytes; copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); @@ -2878,7 +2879,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { const auto check = [&] { const auto bindings = resources.PrepareDrawBindings(regionRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 1, "the read-only misaligned draw input was not snapshotted"); - const auto contents = bindings->snapshots.front().buffer->Bytes(); + const auto contents = bindings->snapshots.front().Bytes(); Require(bindings->snapshots.front().address == view - adjustment && contents.size() == adjustment + elementBytes, "the draw snapshot does not start at the aligned offset below the view"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's patched offset into the draw snapshot misses the view's bytes"); }; @@ -2963,9 +2964,9 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { Require(push[0] == std::byte{0} && adjustment == offset % alignment, "the inner view's shader offset is not its distance from the binding"); const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 2, "read-only draw inputs were not snapshotted"); - const auto outerContents = bindings->snapshots[0].buffer->Bytes(); + const auto outerContents = bindings->snapshots[0].Bytes(); Require(outerContents.size() >= outerBytes && std::memcmp(outerContents.data(), guest + outer, outerBytes) == 0, "an aligned draw snapshot misses its view's bytes"); - const auto contents = bindings->snapshots[1].buffer->Bytes(); + const auto contents = bindings->snapshots[1].Bytes(); Require(contents.size() >= adjustment + elementBytes, "a draw snapshot ends before the bytes the shader reads"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's offset into a draw snapshot misses the view's bytes"); snapshotRecorder.Sync(); @@ -3185,7 +3186,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { return; } const auto element = address + 4096; - constexpr std::size_t elementBytes = 1024; + constexpr std::size_t elementBytes = 8192; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; @@ -3635,6 +3636,99 @@ void unwrittenSnapshotTests(const Device& device) { for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer); cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer); Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap"); + const std::array later{1, 3, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, later, buffer, 256); + VkDeviceSize offset = 0; + Require(cache.UnwrittenDrawSnapshot(address, 16, later, &offset) == buffer && offset == 256, "an unwritten snapshot lost its offset in its buffer"); +} + +void drawSnapshotUploadTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + const auto limit = ShaderResources::DrawUploadLimit(); + if (limit < 256 || limit > 16384) { + std::cout << "draw upload limit outside 256..16384 bytes: draw snapshot uploads not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot uploads not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + for (std::size_t index = 0; index < bytes; ++index) static_cast(block)[index] = static_cast(index * 7u + 1u); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot uploads not tested\n"; + return; + } + const std::array elements{address + 4096, address + 8192, address + 32768}; + const std::array sizes{256, limit, limit + 256}; + ShaderRecompiler::RecompileResult program; + for (std::uint32_t index = 0; index < elements.size(); ++index) { + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = index; + binding.count = 1; + binding.guestDescriptor = {static_cast(elements[index]), static_cast(elements[index] >> 32u) & 0xffffu, static_cast(sizes[index]), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + } + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto alignment = std::max(snapshotContext.limits.minStorageBufferOffsetAlignment, 16); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted"); + for (std::size_t index = 0; index < elements.size(); ++index) { + const auto& snapshot = bindings->snapshots[index]; + Require(snapshot.address == elements[index] && snapshot.bytes == sizes[index] && snapshot.offset % alignment == 0, "a draw snapshot is not an aligned copy of its input"); + Require(std::memcmp(snapshot.Bytes().data(), reinterpret_cast(elements[index]), sizes[index]) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + } + Require(bindings->snapshots[0].buffer == bindings->snapshots[1].buffer && bindings->snapshots[0].offset != bindings->snapshots[1].offset, "draw inputs up to the upload limit do not share the recorder's upload chunk"); + Require(bindings->snapshots[2].buffer != bindings->snapshots[0].buffer && bindings->snapshots[2].offset == 0, "a draw input above the upload limit was copied into the upload chunk"); + const auto earlier = bindings->snapshots[0].Bytes(); + const std::vector before(earlier.begin(), earlier.end()); + std::memset(reinterpret_cast(elements[0]), 0x5a, sizes[0]); + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted again"); + const auto copied = again->snapshots[0].Bytes(); + Require(again->snapshots[0].buffer != bindings->snapshots[0].buffer || again->snapshots[0].offset != bindings->snapshots[0].offset, "a draw input copied again took the slice an earlier draw reads"); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x5a}; }), "a draw snapshot copied again misses the CPU store"); + Require(std::equal(before.begin(), before.end(), earlier.begin()), "copying a draw input again changed the slice an earlier draw reads"); + const auto first = snapshotRecorder.DrawUpload(16); + const auto second = snapshotRecorder.DrawUpload(16); + Require(second.buffer == first.buffer && second.offset >= first.offset + 16 && second.offset % alignment == 0, "upload slices overlap or are not aligned"); + const auto whole = snapshotRecorder.DrawUpload(Recorder::DrawUploadChunk); + Require(whole.buffer != second.buffer && whole.offset == 0, "a full upload chunk was not replaced"); + snapshotRecorder.Sync(); + } + recorder.Activate(); } void drawInputReuseTests(const Device& device, Recorder& recorder) { @@ -9022,6 +9116,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); std::cout << "Draw snapshot cap tests passed\n"; return 0; } @@ -9071,6 +9166,7 @@ int main(int argc, char** argv) { drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 0af0fe0bfa..0e3bde7eec 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -84,7 +84,7 @@ void removeCacheDirectory(const std::filesystem::path& directory) { } bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { - return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.samplerCoordinatesIgnored == right.samplerCoordinatesIgnored && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -176,6 +176,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.bufferWritten = {false, false, true, true, false}; binding.samplerUnnormalized = {false, true, true}; binding.samplerIntegerBorder = {true, false, seed % 2 == 1}; + binding.samplerCoordinatesIgnored = {false, seed % 2 == 0, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0}; binding.imageSamplers = {0x5u, 0u, 0x80000000u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 4aaddf8dc6..befdf47148 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2615,6 +2615,116 @@ void verifyImageTableUnkeyedAccess() { } } +struct TableManyGuest { + std::array, 128> heap{}; + std::array output{}; + std::array srt{}; +}; + +constexpr std::uint32_t TableManyDirectWord = 96u; +constexpr std::uint32_t TableManyExtraSamplerWord = 112u; + +std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers, std::uint32_t direct = 0u, bool extraSampler = false) { + std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u}; + for (std::uint32_t image = 0; image < direct; image++) code.insert(code.end(), {0xf40c0000u | ((44u + image * 8u) << 6u), 0xfa000000u | ((TableManyDirectWord + image * 8u) * 4u)}); + if (extraSampler) code.insert(code.end(), {0xf4080f00u, 0xfa000000u | (TableManyExtraSamplerWord * 4u)}); + for (std::uint32_t read = 0; read < first + second; read++) { + code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u}); + } + for (std::uint32_t image = 0; image < direct; image++) { + const auto resource = ((44u + image * 8u) / 4u) << 16u; + code.insert(code.end(), {0xf09c0f08u, 0x00400400u | resource, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00600400u | resource, 0x06100908u}); + if (extraSampler && image + 1u == direct) code.insert(code.end(), {0xf09c0f08u, 0x01e00400u | resource, 0x06100908u}); + } + code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); + return code; +} + +void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[4]; + alignas(4096) static TableManyGuest guest; + auto& heap = guest.heap; + for (std::uint32_t record = 0; record < heap.size(); record++) heap[record] = TableT(textures[record % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, record + 1u); + const auto heapV = TableV(heap.data(), 32u, static_cast(heap.size())); + const auto outputV = TableV(guest.output.data(), 0u, 16u); + guest.srt = {}; + std::copy(heapV.begin(), heapV.end(), guest.srt.begin()); + guest.srt[8] = 0x92u; + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + std::copy(heapV.begin(), heapV.end(), guest.srt.begin() + 16); + for (std::uint32_t read = 0; read + 20u < guest.srt.size(); read++) guest.srt[20u + read] = read + 2u; + for (std::uint32_t image = 0; image < 2u; image++) { + const auto direct = TableT(textures[image].bytes.data(), TableFormat8888UNorm, TableType2D, 200u + image); + std::copy(direct.begin(), direct.end(), guest.srt.begin() + TableManyDirectWord + image * 8u); + } + guest.srt[TableManyExtraSamplerWord] = 0x91u; + const auto address = static_cast(reinterpret_cast(guest.srt.data())); + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; }; + const auto selectedTables = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [&](const ImageResource& image) { return image.table != NoTable && columnOf(plan, image).selected; })); + }; + const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit"; + const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages; + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + if (direct) { + const auto directImages = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [](const ImageResource& image) { return image.table == NoTable; })); + }; + const auto reads = limit - 1u; + const auto fittingDirectCode = TableManyKeyCode(reads, 0u, pairs, 1u); + const auto fittingDirect = GetResourcePlan(TableRequest(fittingDirectCode, userData)); + const auto describe = [&](const IrResourcePlan& plan) { + return std::to_string(plan.info.images.size()) + " images (" + std::to_string(selectedTables(plan)) + " selected tables, " + std::to_string(directImages(plan)) + " direct), " + std::to_string(plan.info.sampledPairs.size()) + " pairs"; + }; + require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str()); + std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with 2 samplers" : "") << ": " << describe(*fittingDirect) << '\n'; + const auto pastDirectCode = TableManyKeyCode(reads, 0u, pairs, pairs ? 1u : 2u, pairs); + const auto pastDirectRequest = TableRequest(pastDirectCode, userData); + const auto pastDirect = GetResourcePlan(pastDirectRequest); + const auto mergedDirect = std::find_if(pastDirect->info.images.begin(), pastDirect->info.images.end(), [](const ImageResource& image) { return image.table != NoTable; }); + require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 5u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str()); + const auto mergedColumn = columnOf(*pastDirect, *mergedDirect); + require(!mergedColumn.selected && mergedColumn.selection == 0u && !mergedColumn.unkeyed && mergedColumn.keyDomains.size() == reads, (name + ": the table merged beside the direct " + (pairs ? "sampler" : "images") + " is not the union of the keys").c_str()); + const auto compiledDirect = CompileTable(pastDirectRequest); + const auto& directTables = compiledDirect.capture->snapshot.tables; + require(directTables.tables.size() == 1u, (name + ": the snapshot beside the direct " + (pairs ? "sampler" : "images") + " does not hold one table").c_str()); + const auto& directSnapshot = directTables.tables[mergedDirect->table]; + for (const auto key : {2u, 2u + reads / 2u, 1u + reads}) { + require(key < directSnapshot.codes.size() && (directSnapshot.codes[key] & Abi::PoisonFlag) == 0u && directSnapshot.words.at(directSnapshot.codes[key]).dwords == heap[key], (name + ": a key beside the direct " + (pairs ? "sampler" : "images") + " does not bind its record").c_str()); + } + require(directSnapshot.codes.at(1) == outside && directSnapshot.codes.at(reads + 2u) == outside, (name + ": a key no read selects was bound beside the direct " + (pairs ? "sampler" : "images")).c_str()); + std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with a third sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n"; + return; + } + const auto fitting = TableManyKeyCode(limit, 0u, pairs); + const auto atLimit = GetResourcePlan(TableRequest(fitting, userData)); + require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str()); + const auto past = TableManyKeyCode(limit + 1u, 0u, pairs); + const auto request = TableRequest(past, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == (pairs ? 2u : 1u), (name + ": reads past the limit were not merged into one table").c_str()); + const auto merged = columnOf(*plan, plan->info.images[0]); + require(!merged.selected && merged.selection == 0u && !merged.unkeyed && merged.keyDomains.size() == limit + 1u, (name + ": the merged table is not the union of the keys").c_str()); + const auto compiled = CompileTable(request); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str()); + const auto& snapshot = tables.tables[0]; + for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) { + require(key < snapshot.codes.size() && (snapshot.codes[key] & Abi::PoisonFlag) == 0u && snapshot.words.at(snapshot.codes[key]).dwords == heap[key], (name + ": a key does not bind its record").c_str()); + } + require(snapshot.codes.at(1) == outside && snapshot.codes.at(limit + 3u) == outside, (name + ": a key no read selects was bound").c_str()); + if (pairs) return; + const auto split = TableManyKeyCode(40u, 30u, false); + const auto twoHeaps = GetResourcePlan(TableRequest(split, userData)); + const auto unselected = std::find_if(twoHeaps->info.images.begin(), twoHeaps->info.images.end(), [&](const ImageResource& image) { return !columnOf(*twoHeaps, image).selected; }); + require(twoHeaps->info.images.size() == 31u && selectedTables(*twoHeaps) == 30u && unselected != twoHeaps->info.images.end() && columnOf(*twoHeaps, *unselected).keyDomains.size() == 40u, (name + ": the larger heap was not merged first, or the smaller one lost its split").c_str()); +} + void verifyImageTableSrtKey() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -5323,7 +5433,7 @@ void verifySpecializedBufferAtomicBindings() { } require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); - require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault"); + require(faultCalls == 0, "specialized buffer atomic: a 32-bit atomic records an unaligned fault instead of acting on its dword"); const std::array shaderOnly{spv::CapabilityShader}; expectFailure([&] { static_cast(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64"); } @@ -5696,6 +5806,47 @@ void verifyUnnormalizedSamplers() { require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset"); } +void verifyNullVolumeUnnormalizedSampler() { + using namespace ShaderRecompiler; + ImageResource volume{}; + volume.resourceClass = ImageResourceClass::Sampled; + volume.numericClass = IrTextureNumericClass::Float; + volume.dimension = RdnaImageDimension::Dim3D; + volume.read = true; + ImageResource plane = volume; + plane.dimension = RdnaImageDimension::Dim2D; + SamplerResource sampler{}; + sampler.uses = SamplerUseExplicitLod; + const DescriptorValue nullDescriptor{{0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue volumeDescriptor{{0x00001000u, 0x03800000u, 0x0000c000u, 0xa0000facu, 3u, 0u, 0u, 0u}, 8u}; + const DescriptorValue planeDescriptor{{0x00002000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue unnormalized{{0x00008092u, 0x00fff000u, 0x02500000u, 0u}, 4u}; + const auto populate = [&](std::vector images, std::vector descriptors) { + ShaderInfo info; + info.images = images; + info.samplers = {sampler}; + for (std::uint32_t index = 0; index < images.size(); ++index) { + info.sampledPairs.push_back({index, 0u, 0u}); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(images[index])); + } + ResourceSnapshot snapshot; + snapshot.images = descriptors; + snapshot.samplers = {unnormalized}; + BindingAllocationResult allocation; + for (std::uint32_t index = 0; index < images.size(); ++index) allocation.layout.descriptors.push_back({DescriptorBindingForImage(images[index]), {index}}); + allocation.layout.descriptors.push_back({DescriptorBindingKind::Samplers, {0u}}); + DescriptorBindingBuilder{}.Populate(allocation, info, IrShaderStage::Compute, 0u, snapshot, {}); + return allocation.bindings.back(); + }; + const auto nullOnly = populate({volume}, {nullDescriptor}); + require(nullOnly.samplerUnnormalized == std::vector{false} && nullOnly.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired only with a null 3D T# was refused or kept its unnormalized coordinates"); + const DescriptorValue zeroSwizzle{{0x00003000u, 0x00100000u, 0u, 0xa0000000u, 0u, 0u, 0u, 0u}, 8u}; + const auto constantAndNull = populate({volume, volume}, {zeroSwizzle, nullDescriptor}); + require(constantAndNull.samplerUnnormalized == std::vector{false} && constantAndNull.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired with a null 3D T# and a 3D T# whose channels all select constants was refused or kept its unnormalized coordinates"); + expectFailure([&] { static_cast(populate({volume}, {volumeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); + expectFailure([&] { static_cast(populate({volume, plane}, {nullDescriptor, planeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: an S# shared by a null 3D T# and a 2D T# was accepted"); +} + void verifyUnusedUnnormalizedSampler() { using namespace ShaderRecompiler; ImageResource image{}; @@ -6004,6 +6155,102 @@ void verifyBdaReadFallbackFunctions() { } } +void verifyBdaAnchors() { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t scalarBase, std::uint32_t stride) { + std::vector code{0x7e020200u, 0x7e040201u}; + for (std::uint32_t load = 0; load < scalarBase; ++load) { + code.push_back(0xdc308000u | (stride * load + 4u)); + code.push_back(((3u + load) << 24u) | 0x00040000u); + } + code.push_back(0xdc308000u | 4u); + code.push_back(((3u + scalarBase) << 24u) | 0x007d0001u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 1; load <= scalarBase; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u)); + code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x60000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + for (const std::uint32_t stride : {4u, 32u}) { + for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { + const bool anchored = stride == 32u && loads >= 2u; + const auto words = compile(loads, stride); + std::map names; + std::string function; + std::map mainCalls; + std::size_t anchorFunctions = 0; + std::size_t branchesBeforeAnchor = 0; + std::size_t directLoads = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto length = words[cursor] >> 16u; + require(length != 0 && length <= words.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (op == spv::OpFunction) { + function = names[words[cursor + 2]]; + if (function == "bda_anchor_range") ++anchorFunctions; + } + if (op == spv::OpFunctionCall && function == "main") { + const auto& callee = names[words[cursor + 3]]; + if (callee == "bda_anchor_range") require(mainCalls.empty(), "BDA anchors: the anchor is searched after a read"); + ++mainCalls[callee.starts_with("read_bda_span") ? std::string("read_bda_span") : callee]; + } + if (op == spv::OpBranchConditional && function == "main" && mainCalls["bda_anchor_range"] == 0u) ++branchesBeforeAnchor; + if (op == spv::OpLoad && function == "main" && length > 4u && words[cursor + 4] == spv::MemoryAccessAlignedMask && words[cursor + 5] == 4u) ++directLoads; + cursor += length; + } + const auto spans = loads == 0u ? 1u : stride == 4u ? 2u : loads + 1u; + require(anchorFunctions == (anchored ? 1u : 0u), "BDA anchors: the anchor search is defined without anchored reads, or twice"); + require(mainCalls["bda_anchor_range"] == (anchored ? 1u : 0u), "BDA anchors: the reads of one user data base do not share one anchor"); + require(!anchored || branchesBeforeAnchor == 0u, "BDA anchors: the anchor is not searched at the program's entry"); + require(mainCalls["read_bda_span"] == spans, "BDA anchors: a read lost its lookup path"); + require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); + } + } + RecompileRequest plain{}; + const std::vector endOnly{0xbf810000u}; + plain.shader = {ShaderStage::Compute, 0x60000u, endOnly, 0, {}}; + plain.context.waveSize = 32; + plain.context.userDataBaseRegister = 0; + plain.context.userData = userData; + plain.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + plain.target.vulkanVersion = 0x00401000u; + plain.target.spirvVersion = 0x00010300u; + plain.target.subgroupSize = 32; + plain.target.bdaAbiVersion = BdaAbi::Version; + plain.target.fragmentShaderBarycentricEnabled = false; + plain.layout.pushConstantSizeBytes = 128; + plain.useCache = false; + const auto plainWords = Recompile(plain).spirv; + for (std::size_t cursor = 5; cursor < plainWords.size();) { + const auto length = plainWords[cursor] >> 16u; + require(length != 0 && length <= plainWords.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = plainWords[cursor] & 0xffffu; + require(!(op == spv::OpTypeInt && plainWords[cursor + 2] == 64u), "BDA anchors: a program without anchors declares a 64-bit integer type"); + require(!(op == spv::OpCapability && plainWords[cursor + 1] == spv::CapabilityInt64), "BDA anchors: a program without anchors requires Int64"); + cursor += length; + } +} + void verifyFunctionLdsBound() { using namespace ShaderRecompiler; const auto build = [](const auto& body) { @@ -6671,6 +6918,11 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "1", 1); +#endif using namespace ShaderRecompiler; if (argc == 2 && std::string_view(argv[1]) == "--quad-mask-no-demote") { verifyQuadMaskNeighbourReads(); @@ -6724,6 +6976,26 @@ int main(int argc, char** argv) { verifyTableEdgeStore(); return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-image-limit") { + verifySelectedRecordsWithinLimits(false, false); + std::cout << "Selected image-table records past the image limit merge back into one table per heap\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-pair-limit") { + verifySelectedRecordsWithinLimits(true, false); + std::cout << "Selected image-table records past the sampled pair limit merge back into one table per heap\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-image-limit") { + verifySelectedRecordsWithinLimits(false, true); + std::cout << "Direct images count toward the image limit beside selected image-table records\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-pair-limit") { + verifySelectedRecordsWithinLimits(true, true); + std::cout << "Direct samplers count toward the sampled pair limit beside selected image-table records\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { verifySrtReadPatching(); std::cout << "SRT read patching replaces repeated and multiple uses\n"; @@ -6779,9 +7051,11 @@ int main(int argc, char** argv) { verifyCoherentBufferAccesses(); verifyUnnormalizedSamplers(); verifyUnusedUnnormalizedSampler(); + verifyNullVolumeUnnormalizedSampler(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); + verifyBdaAnchors(); verifyFunctionLdsBound(); verifyFunctionLdsUpdates(); verifySrtReadPatching(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index f4fe928120..5549ae9fb1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -195,11 +195,26 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed"); check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed"); expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); }); + struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { + std::atomic ready{0}; + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } + void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } + void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } + }; + const auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; + Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; + std::array events{}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == 1, "flip-only queue flip was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared"); std::array words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - std::array events{}; check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only"); check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong"); check(sceAgcDriverGetEqContextId(events.data()) == 0, "graphics end-of-pipe event reports another context"); @@ -212,21 +227,39 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed"); AgcDriverWaitIdle_nid_postfix(); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt"); - struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { - std::atomic ready{0}; - std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } - void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } - void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } - }; - const auto output = std::make_shared(); - AgcDriverRegisterVideoOutput_nid_postfix(7, output); - std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; - Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; - check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - check(output->ready.load() == 1, "flip was not handed to its output"); - check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event"); - check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared"); + check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver on a queue whose flip event is suppressed"); + std::uint32_t sustainedFlips = 0; + std::uint32_t flipsUntilEvent = 0; + while (sustainedFlips < 8 && flipsUntilEvent == 0) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "sustained flip submit failed"); + AgcDriverWaitIdle_nid_postfix(); + ++sustainedFlips; + check(output->ready.load() == static_cast(2 + sustainedFlips), "sustained flip was not handed to its output"); + if (owner->GetTriggeredEvents(events.data(), 2) != 0) flipsUntilEvent = sustainedFlips; + } + check(flipsUntilEvent != 0, "a flip never raised an event again on a queue whose own release interrupts stopped"); + check(flipsUntilEvent > 2, "a flip raised an event within two flips of the queue's own release interrupt"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after the latch cleared submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(3 + sustainedFlips), "flip after the latch cleared was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a flip did not raise an event after the latch cleared"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release after a cleared latch submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver after the latch cleared"); + for (int index = 0; index < 2; ++index) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip paired with a release interrupt submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt"); + } AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); @@ -784,6 +817,36 @@ void testRegisteredVertexWorkgroupRouting() { check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted"); } +void testRegisteredFastLaunchWorkgroupVertex() { + alignas(256) static const std::array code{ + 0xd8340000u, 0x00000100u, 0xbf8a0000u, 0xf80008cfu, 0x00000000u, 0xbf810000u, + }; + struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; + }; + alignas(8) static Header header{}; + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 2u; + header.shader.user_data = &header.userData; + header.registers = {{{0x0c8u, static_cast(address >> 8u)}, {0x0c9u, static_cast(address >> 40u)}, {0x08au, 0x022c0245u}, {0x08bu, 0x0009002au}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + AgcDriverRegisterShader_nid_postfix(&header.shader); + const std::array stages{&header.shader}; + const std::array context{{ + {0x2d5u, 0x00482030u}, {0x1ffu, 32u}, {0x2ceu, 32u}, {0x2d3u, 64u}, {0x29bu, 2u}, {0x2abu, 4u}, + }}; + const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -804,6 +867,12 @@ void testWorkerFailure() { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--eop-interrupts") { + testEndOfPipeInterrupts(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC end-of-pipe interrupt queue mode tests passed"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { testReadOnlyResummarize(); LibcRunShutdown_nid_postfix(); @@ -886,6 +955,7 @@ int main(int argc, char** argv) { testProgramWorkgroupUse(); testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); + testRegisteredFastLaunchWorkgroupVertex(); testEvents(); testValidation(); testClearState(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp new file mode 100644 index 0000000000..9bd219d267 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp @@ -0,0 +1,164 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 1024; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t OutputBase = 0x8000; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array Code{ + 0x34040082, 0x34060085, 0x4a0606ff, OutputBase, + 0xdc308010, 0x04000002, 0xdc308014, 0x05000002, 0xdc308018, 0x06000002, + 0xdc308403, 0x07000002, + 0xf4000100, 0xfa000203, 0xf4000140, 0xfa0002f6, + 0xbf8c3f70, 0xbf8cc07f, 0x7e100204, 0x7e120205, + 0xdc708000, 0x00000403, 0xdc708004, 0x00000503, 0xdc708008, 0x00000603, 0xdc70800c, 0x00000703, + 0xdc708010, 0x00000803, 0xdc708014, 0x00000903, 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, "BDA anchor reads: 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; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +std::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x61000000u + dword * 0x00010307u; + return memory; +} + +std::uint32_t Bytes(const std::vector& memory, std::uint32_t address) { + std::uint32_t value = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) { + const std::uint32_t at = address + byte; + value |= ((memory[at / 4u] >> (at % 4u * 8u)) & 0xffu) << (byte * 8u); + } + return value; +} + +std::vector Expected() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase / 4u + tid * OutputDwords]; + out[0] = Bytes(initial, tid * 4u + 0x10u); + out[1] = Bytes(initial, tid * 4u + 0x14u); + out[2] = Bytes(initial, tid * 4u + 0x18u); + out[3] = Bytes(initial, tid * 4u + 0x403u); + out[4] = Bytes(initial, 0x200u); + out[5] = Bytes(initial, 0x2f4u); + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::uint32_t waveSize, std::span userData, std::span guest, const ShaderRecompiler::SpirvTarget& target) { + const std::span code(Code); + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(guest.data()), guest}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +bool SearchesAnchor(const ShaderRecompiler::SharedSpirv& spirv) { + const std::string_view text(reinterpret_cast(spirv.data()), spirv.size() * sizeof(std::uint32_t)); + return text.find("bda_anchor_range") != std::string_view::npos; +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const auto result = Recompile(waveSize, userData, std::span(reinterpret_cast(guest.Data()), InputDwords * sizeof(std::uint32_t)), target); + Require(SearchesAnchor(result.spirv), "BDA anchor reads: " + name + " reads of one user-data base do not share an anchor"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + const auto expected = Expected(); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "BDA anchor reads: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto native = device->Target(); + const auto split = device->ComputeTarget(32); + Run(*device, guest, 32, native, "wave32"); + Run(*device, guest, 64, native, "wave64"); + if (split.subgroupSize != native.subgroupSize) Run(*device, guest, 64, split, "wave64 split"); + std::puts("BDA anchor read 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/BufferAtomicUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp new file mode 100644 index 0000000000..0cfcfe9c2f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp @@ -0,0 +1,92 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Sentinel = 0x5eed0001u; +constexpr std::size_t OffsetWord = 2; +alignas(256) std::array Buffer{}; +alignas(256) std::array Output{}; + +alignas(256) auto Code = std::to_array({ + 0x34020082, + 0x7e040281, + 0xe0c85000, 0x80000201, + 0xbf8c3f70, + 0xe0701000, 0x80010201, + 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}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t offset) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Buffer[lane] = lane * 100u; + Buffer[Threads] = Sentinel; + Output.fill(0xdeadbeefu); + Code[OffsetWord] = 0xe0c85000u | offset; + std::vector userData(8, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t offset) { + const auto what = "buffer_atomic_add at byte offset " + std::to_string(offset) + ": lane "; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == lane * 100u, what + std::to_string(lane) + " returned " + std::to_string(Output[lane]) + ", expected " + std::to_string(lane * 100u)); + Require(Buffer[lane] == lane * 100u + 1u, what + std::to_string(lane) + " left dword " + std::to_string(Buffer[lane]) + ", expected " + std::to_string(lane * 100u + 1u)); + } + Require(Buffer[Threads] == Sentinel, what + "the dword after the last lane changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64)) { + std::puts("skipped, the device has no shaderInt64"); + return VulkanTestSkipped; + } + for (std::uint32_t offset = 0; offset < 4u; ++offset) { + Run(*device, offset); + Check(offset); + } + std::puts("buffer atomic unaligned 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/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index c3520e8346..bc07a0cdbf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp @@ -244,9 +244,13 @@ void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) { for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } -void RunAtomicFault(AgcDriver::VulkanDevice& device) { +void RunAtomicContainingDword(AgcDriver::VulkanDevice& device) { DispatchAtomic(device, 2u, 0u); - for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2"); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word < Lanes ? Fill + word + 1u : Fill; + Require(Output[word] == expected, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected) + ": a 32-bit atomic off a DWORD boundary acts on its containing dword"); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } } @@ -272,7 +276,7 @@ int main() { RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view); RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view); } - RunAtomicFault(*device); + RunAtomicContainingDword(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp new file mode 100644 index 0000000000..923b3b7212 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -0,0 +1,114 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Marker = 0x55u; +constexpr std::uint64_t FsTable = 0xfe0040000ull; + +alignas(256) constexpr std::array Caller{ + 0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u, + 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, 0xbf840006u, 0xf4240404u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xe0701000u, 0x80010102u, + 0xbf810000u, 0xbf9f0000u}; +alignas(256) std::array Output{}; + +std::vector Commands() { + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Caller.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{8u << 1u}; + registers(0x213, resource); + const auto output = reinterpret_cast(Output.data()); + const std::array data{0u, 0u, 0u, 0u, static_cast(output), static_cast((output >> 32u) & 0xffffu), + static_cast(sizeof(Output)), 0x31016facu}; + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + return words; +} + +std::string Dispatch() { + Output.fill(Untouched); + const auto words = Commands(); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + std::string failure; + try { + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + } catch (const std::exception& error) { + failure = error.what(); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + return failure; +} + +void CheckTable() { + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(FsTable), 0x60u), "the FS table at 0xfe0040000 is not mapped"); + std::array slots{}; + std::memcpy(slots.data(), reinterpret_cast(FsTable), sizeof(slots)); + const auto records = slots[0] & 0xffffffffffffull; + Require(slots[0] >> 48u == 16u, "FS table slot 0 does not have a stride of 16"); + Require(slots[1] == 0x0000520400000008ull, "FS table slot 0 does not describe eight records with dword3 0x5204"); + for (std::size_t slot = 2; slot < slots.size(); ++slot) Require(slots[slot] == 0u, "an FS table slot other than the first is not zero"); + Require(records % 16u == 0u && AgcDriver::GuestMemory::Accessible(reinterpret_cast(records), 128u), "the FS table records are not readable"); + std::array table{}; + std::memcpy(table.data(), reinterpret_cast(records), sizeof(table)); + for (std::size_t record = 0; record < 8u; ++record) { + Require(table[record * 2u] != 0u && table[record * 2u] == table[2u] && table[record * 2u + 1u] == 0u, "an FS table record does not hold the routine and a zero"); + } + std::array routine{}; + Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(table[2]), sizeof(routine)), "the FS routine is not readable"); + std::memcpy(routine.data(), reinterpret_cast(table[2]), sizeof(routine)); + Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + CheckTable(); + const auto failure = Dispatch(); + Require(failure.find("not statically resolvable") == std::string::npos, "the call through the FS table did not link: " + failure); + Require(failure.empty(), "the dispatch failed: " + failure); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Marker, "lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + " after the call through the FS table, expected " + std::to_string(Marker)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("compute FS table call tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception&) { + } + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp new file mode 100644 index 0000000000..21fbc192ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp @@ -0,0 +1,231 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t Block = 0x10000; +constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t StoredAgain = 0x0badf00du; +constexpr auto IdleStoreWindow = std::chrono::milliseconds(500); + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreAgainCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Store(const std::uint32_t* texels, std::span code) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{8, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{static_cast(userData.size()) << 1u}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +std::uint32_t* MapSurface() { +#ifdef _WIN32 + auto* texels = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + return texels; +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped == MAP_FAILED) return nullptr; + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); + auto* texels = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes); + return texels; +#endif +} + +void ReleaseSurface(std::uint32_t* texels) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes); +#else + munmap(texels, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes); +#endif +} + +bool Holds(const std::uint32_t* texels, std::uint32_t value) { + const auto* words = static_cast(texels); + for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) { + if (words[index] == value) return true; + } + return false; +} + +bool HoldLive(std::unique_lock& hold, const std::uint32_t* texels, std::uint32_t result) { + const auto address = AddressOf(texels); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + const std::array, 1> range{{{address, address + SurfaceBytes}}}; + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true; + hold.unlock(); + if (Holds(texels, result)) return false; + Require(std::chrono::steady_clock::now() < deadline, "idle store over unmap: the result neither became pending nor reached guest memory"); + std::this_thread::yield(); + } +} + +enum class Outcome { Settled, Missed, NotRun }; + +Outcome Run() { + auto* texels = MapSurface(); + Require(texels != nullptr, "idle store over unmap: cannot allocate the guest surface"); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true); + bool mapped = true; + struct Unmapped { + std::uint32_t* texels; + bool& mapped; + ~Unmapped() { + if (!mapped) return; + AgcDriverWaitIdle_nid_postfix(); + GuestAllocations::Mutation().Remove(texels); + ReleaseSurface(texels); + } + } unmapped{texels, mapped}; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes == SurfaceBytes, "idle store over unmap: unexpected surface size"); + const auto address = AddressOf(texels); + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + std::printf("skipped, the surface is not write-watched\n"); + return Outcome::NotRun; + } + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "idle store over unmap: the surface is still write-watched"); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); + Store(texels, StoreCode); + if (!HoldLive(hold, texels, Stored)) return Outcome::Missed; + StorageTexture::FlushAllPending("idle store over unmap fixture"); + hold.unlock(); + { + std::vector first(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, first); + } + Require(static_cast(texels)[0] == Stored, "idle store over unmap: the first result was not stored"); + hold.lock(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + const bool saved = image != nullptr && image->GuestSnapshotValid(); + hold.unlock(); + Require(image != nullptr, "idle store over unmap: the surface has no storage image"); + if (!saved) { + std::printf("skipped, the first store left no saved guest bytes to compare with\n"); + return Outcome::NotRun; + } + Store(texels, StoreAgainCode); + if (!HoldLive(hold, texels, StoredAgain)) return Outcome::Missed; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(texels, SurfaceBytes, [](const void* piece, std::size_t, const void*, bool) { + ReleaseSurface(static_cast(const_cast(piece))); + }); + mapped = false; + hold.unlock(); + std::this_thread::sleep_for(IdleStoreWindow); + Require(StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store ran while a guest unmap of the surface was in progress"); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (StorageTexture::AnyPending() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(!StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store did not settle the results of the unmapped surface"); + AgcDriverWaitIdle_nid_postfix(); + return Outcome::Settled; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 64u && outcome == Outcome::Missed; ++attempt) outcome = Run(); + Require(outcome != Outcome::Missed, "idle store over unmap: the result was never held pending"); + if (outcome == Outcome::NotRun) return VulkanTestSkipped; + std::puts("idle store over unmap 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/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp new file mode 100644 index 0000000000..4ea7cc7525 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -0,0 +1,352 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Srgb8 = 128; +constexpr std::uint32_t Srgb8_8 = 129; +constexpr std::uint32_t SwizzleX = 0xfacu; +constexpr std::uint32_t SwizzleYAsX = 0xfadu; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::array Codes{0, 255, 128, 64}; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::uint32_t StoreSize = 1024; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) std::array StoreCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x062a28ff, 0x41000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + 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 + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "image sRGB sample copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "image sRGB sample copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint8_t* Data() { return block; } + +private: + std::uint8_t* block = nullptr; +}; + +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(std::uint32_t format, std::uint32_t swizzle, const void* data = Texels.data(), std::uint32_t extent = Size) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((extent - 1u) & 3u) << 30u), + ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t filter) { + return {0x92u, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +} + +double Decode(std::uint32_t code) { + const double encoded = code / 255.0; + return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); +} + +std::uint32_t Code(std::uint32_t format, bool second, std::uint32_t x) { + const std::uint32_t code = Codes.at(std::min(x, Size - 1u)); + return second && format == Srgb8_8 ? 255u - code : code; +} + +void FillTexels(std::uint32_t format) { + const std::uint32_t bytes = format == Srgb8 ? 1u : 2u; + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sRGB sample copy: the texture does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * bytes]; + texel[0] = static_cast(Code(format, false, x)); + if (bytes == 2u) texel[1] = static_cast(Code(format, true, x)); + } + } +} + +void FillInput(bool gather, bool between) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + float u = (static_cast(tid % Size) + 0.5f) / static_cast(Size); + if (gather) u = (static_cast(base) + 0.6f) / static_cast(Size); + else if (between) u = static_cast(base + 1u) / static_cast(Size); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = 0.6f / static_cast(Size); + Input[tid * 3u + 2u] = 0.0f; + } +} + +bool Close(double actual, double expected) { + return std::fabs(actual - expected) <= 0.01 + std::fabs(expected) * 0.04; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::uint32_t bits = 0; + std::memcpy(&bits, &scale, sizeof(bits)); + StoreCode[2] = bits; + const std::array memory{{{reinterpret_cast(StoreCode.data()), std::as_bytes(std::span(StoreCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(StoreCode.data()), std::span(StoreCode), 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(StoreCode.data())); + device.WaitIdle(); +} + +void CheckStored(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = (static_cast(tid) + 0.5f) / static_cast(StoreSize); + Input[tid * 3u + 1u] = 0.5f / static_cast(StoreSize); + Input[tid * 3u + 2u] = 0.0f; + } + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + const auto sampler = SamplerDescriptor(FilterPoint); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(SampleCode.data()), std::as_bytes(std::span(SampleCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(SampleCode.data()), std::span(SampleCode), 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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const double code = std::round(std::min(1.0, static_cast(tid) * scale) * 255.0); + const double expected = Decode(static_cast(code)); + Require(Close(Output[tid], expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void CheckStorageSource(AgcDriver::VulkanDevice& device) { + GuestBlock block; + auto* texels = block.Data(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "8_SRGB storage source: the surface does not fit the guest block or is below the storage image floor"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), surface.guestBytes, nullptr, "test"); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + Store(device, texels, 1.0f / 32.0f); + CheckStored(device, texels, 1.0f / 32.0f, "8_SRGB sample of a pending storage image"); + const auto before = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(before != nullptr, "8_SRGB storage source: no storage image holds the surface's results"); + const auto version = before->Version(); + Store(device, texels, 1.0f / 8.0f); + const auto after = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(after != nullptr && after->Version() > version, "8_SRGB storage source: the second store did not advance the storage image"); + CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); +} + +std::string ForcedMode() { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + return forced == nullptr ? std::string{} : std::string{forced}; +} + +void VKAPI_PTR NativeSrgbFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +void VKAPI_PTR NoSrgbFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R8_SRGB || format == VK_FORMAT_R8G8_SRGB) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +std::uint32_t SrgbBits() { + return AgcDriver::Graphics::SrgbDecodeFormats(NoSrgbFormatProperties, VK_NULL_HANDLE); +} + +void CheckSelection() { + const auto mode = ForcedMode(); + Require(SrgbBits() != 0u, "a device that cannot sample R8_SRGB or R8G8_SRGB selected no sRGB decode formats"); + const auto decode = AgcDriver::Graphics::SrgbDecodeFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE); + const auto copy = AgcDriver::Graphics::SrgbCopyFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE, decode); + const std::uint32_t expected = mode == "copy" ? SrgbBits() : 0u; + Require(copy == expected, "a device that samples R8_SRGB and R8G8_SRGB selected sRGB copy formats " + std::to_string(copy) + " with APS5_SRGB_SHADER_DECODE='" + mode + "', expected " + std::to_string(expected)); +} + +void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { + FillTexels(format); + const bool between = filter == FilterBilinear; + FillInput(gather, between); + const auto& code = gather ? GatherCode : SampleCode; + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format, second ? SwizzleYAsX : SwizzleX); + const auto sampler = SamplerDescriptor(filter); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), std::span(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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + if (gather) { + const double low = Decode(Code(format, second, base)); + const double high = Decode(Code(format, second, base + 1u)); + const std::array expected{low, high, high, low}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const double actual = Output[tid * 4u + component]; + Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); + } + continue; + } + double expected = Decode(Code(format, second, tid % Size)); + if (between) expected = (Decode(Code(format, second, base)) + Decode(Code(format, second, base + 1u))) / 2.0; + const double actual = Output[tid]; + Require(Close(actual, expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + CheckSelection(); + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (ForcedMode() == "copy") Require((device->Target().srgbDecodeFormats & SrgbBits()) == 0u, "APS5_SRGB_SHADER_DECODE=copy: the device cannot blit R8 or RG8 to RGBA8, so 8 and 8_8 sRGB samples have no copy"); + for (const auto format : {Srgb8, Srgb8_8}) { + const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; + Check(*device, format, false, FilterPoint, false, name + " point sample"); + Check(*device, format, false, FilterBilinear, false, name + " bilinear sample, decoded before filtering"); + Check(*device, format, false, FilterPoint, true, name + " gather4"); + if (format == Srgb8_8) { + Check(*device, format, true, FilterPoint, false, name + " point sample of the second channel"); + Check(*device, format, true, FilterBilinear, false, name + " bilinear sample of the second channel, decoded before filtering"); + Check(*device, format, true, FilterPoint, true, name + " gather4 of the second channel"); + } + } + CheckStorageSource(*device); + std::puts("image sRGB sample copy 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/LdsBeyondDeviceLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp index 8b37c4591f..fb6fd2d583 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp @@ -148,7 +148,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe Require(within.workgroupMemoryDwords == 0u, "lds beyond the device limit " + run + ": LDS below the device limit left workgroup memory"); fits(within, "LDS below the device limit"); const auto atLimit = Compile(waveSize, limit, target); - Require(atLimit.workgroupMemoryDwords == limit / 4u + 1u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(atLimit.workgroupMemoryDwords == limit / 4u + 2u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(atLimit, "LDS at the device limit"); const auto bytes = limit + AboveLimitBytes; for (std::uint32_t tid = 0; tid < Threads; ++tid) { @@ -159,7 +159,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe } Output.fill(Untouched); const auto result = Compile(waveSize, bytes, target); - Require(result.workgroupMemoryDwords == bytes / 4u + 1u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(result.workgroupMemoryDwords == bytes / 4u + 2u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(result, "LDS above the device limit"); if (indirect) { IndirectGroups = {groupsX, groupsY, groupsZ, 0u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 7c35530d9f..1f8032e93c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -14,12 +14,61 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + constexpr std::uint32_t Threads = 256; constexpr std::uint32_t Iterations = 16; constexpr std::uint32_t Repeats = 32; @@ -167,6 +216,32 @@ alignas(256) constexpr auto LoadedParityCode = Unrolled<1>( 0xbf810000, })); +alignas(256) constexpr auto MisalignedCode = Unrolled<1>( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + }), + std::to_array({ + 0xd9000004, 0x00000201, + 0xbf8cc07f, + }), + std::to_array({ + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 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}; @@ -213,7 +288,7 @@ std::uint64_t WorkgroupBytes(std::span words) { return total; } -void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t ldsDwords) { std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); @@ -228,10 +303,14 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { + const auto result = Compile(device, code, ldsDwords); if (expectedDeviceDwords) { const auto bytes = WorkgroupBytes(result.spirv.Words()); - const auto limit = request.target.maxWorkgroupSharedMemoryBytes; + const auto limit = device.Target().maxWorkgroupSharedMemoryBytes; Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch"); Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch"); if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock"); @@ -246,6 +325,21 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name); } +void Misaligned(AgcDriver::VulkanDevice& device, std::uint32_t ldsDwords) { + const auto result = Compile(device, MisalignedCode, ldsDwords); + StderrCapture capture; + try { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(MisalignedCode.data())); + device.WaitIdle(); + } catch (...) { + static_cast(capture.Finish()); + throw; + } + const auto log = capture.Finish(); + Require(log.find("BDA access failed: address=0x4 instruction=0x2c bytes=8 ") != std::string::npos && log.find("reason=7") != std::string::npos, "lds lock budget: misaligned ds_add_u64 on device-backed LDS did not fault"); + std::puts("lds lock budget: misaligned ds_add_u64 faulted"); +} + void LockBudget(AgcDriver::VulkanDevice& device) { const auto target = device.Target(); const auto limit = target.maxWorkgroupSharedMemoryBytes; @@ -253,7 +347,9 @@ void LockBudget(AgcDriver::VulkanDevice& device) { if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled"); Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast(Threads) * Iterations; - Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u); + Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u); + const auto& capabilities = target.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != capabilities.end()) Misaligned(device, limit / 4u); Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); std::puts("lds lock budget tests passed"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index c470b4fdc0..d43e135c35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -1,8 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" #include +#include +#include #include #include #include @@ -10,10 +13,13 @@ #include #include #include +#include #include namespace { +using AgcDriver::DriverDetail::DrawPipeline; + void check(bool condition, const std::string& what) { if (!condition) throw std::runtime_error("pipelined DMA: " + what); } @@ -22,11 +28,47 @@ std::array DmaData(bool immediate, std::uint64_t source, std:: return {0xc0055000u, immediate ? (2u << 29u) : 0u, immediate ? data : static_cast(source), immediate ? 0u : static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; } +std::array DmaSelect(std::uint32_t destinationSelect, std::uint64_t source, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, destinationSelect << 20u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +template +void Append(std::vector& words, const std::array& packet) { + words.insert(words.end(), packet.begin(), packet.end()); +} + void Submit(std::vector& words) { Packet packet{words.data(), static_cast(words.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); } +struct Gate { + std::atomic release{false}; + ~Gate() { release.store(true, std::memory_order_release); } +}; + +void Hold(Gate& gate, std::uint64_t address, std::size_t bytes) { + DrawPipeline::Queue0().Enqueue([&gate] { + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {{address, address + bytes}}); +} + +void ExpectDrains(std::vector& words, std::uint32_t opcode, Gate& gate, const char* what) { + const auto before = DrawPipeline::DrainRequestsByOpcode(opcode); + std::thread releaser([&] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (DrawPipeline::DrainRequestsByOpcode(opcode) == before && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gate.release.store(true, std::memory_order_release); + }); + Submit(words); + releaser.join(); + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before + 1, what); +} + +void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) { + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before, what); +} + } int main() { @@ -40,8 +82,12 @@ int main() { try { alignas(256) static std::array first{}; alignas(256) static std::array second{}; + alignas(256) static std::array pending{}; + alignas(256) static std::array elsewhere{}; first.fill(0); second.fill(0); + pending.fill(0); + elsewhere.fill(0); using namespace AgcDriver::DriverDetail; const auto drainedBefore = storesDrained.load(); const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load(); @@ -57,6 +103,69 @@ int main() { const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)"); check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained"); + + const auto source = reinterpret_cast(first.data()); + const auto held = reinterpret_cast(pending.data()); + const auto other = reinterpret_cast(elsewhere.data()); + { + Gate gate; + Hold(gate, held, sizeof(pending)); + const auto dmaBefore = DrawPipeline::DrainRequestsByOpcode(0x50); + const auto predicationBefore = DrawPipeline::DrainRequestsByOpcode(0x20); + const auto nopBefore = DrawPipeline::DrainRequestsByOpcode(0x10); + words.clear(); + Append(words, DmaSelect(2, source, 0, 64)); + Append(words, DmaData(false, source, 0, other, 0)); + Append(words, DmaSelect(1, source, 0, 4)); + Append(words, DmaData(false, source, 0, other, 2)); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc000102cu, 0x00636261u}); + Append(words, std::array{0xc0001030u, 0u}); + Submit(words); + ExpectNoDrain(0x50, dmaBefore, "a prefetch, a zero-byte copy, a GDS copy or an unaligned copy off the pending range drained"); + ExpectNoDrain(0x20, predicationBefore, "SET_PREDICATION drained the pipeline"); + ExpectNoDrain(0x10, nopBefore, "a marker push or pop drained the pipeline"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, source, 0, held, 2)); + ExpectDrains(words, 0x50, gate, "a copy onto an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, held, 0, other, 2)); + ExpectDrains(words, 0x50, gate, "an unaligned copy of an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc0001301u, 32u}); + ExpectDrains(words, 0x13, gate, "a predicated packet whose predicate overlaps an uncommitted write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + const auto resetBefore = DrawPipeline::DrainRequestsByOpcode(0x10); + words.clear(); + Append(words, std::array{0xc0001024u, 0u}); + Submit(words); + ExpectNoDrain(0x10, resetBefore, "DISPATCH_RESET drained the pipeline"); + } + LibcRunShutdown_nid_postfix(); std::puts("pipelined DMA tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index fe436e6b22..881867d747 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -50,7 +50,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u; class Device { public: - explicit Device(bool resourceImages = false) { + explicit Device(bool enableStorageImageMultisample = true) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -96,13 +96,12 @@ public: queue.queueFamilyIndex = family; queue.queueCount = 1; queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; - if (resourceImages) { - VkPhysicalDeviceFeatures supported{}; - function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); - enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; - } + enabled.shaderStorageImageMultisample = enableStorageImageMultisample && supported.shaderStorageImageMultisample; + context.storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { @@ -469,6 +468,48 @@ void heldTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "released textures were not evicted down to the budget"); } +void releasedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + ClearCachedTextures(context.device); + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + Block block(context); + auto pinned = ImageResources(context, block, 0, 1, true); + Require(pinned->SampledTextures().size() == 1 && !pinned->SampledTextures().front()->ViewsStorageImage(), "the pinned descriptor set does not hold a sampled snapshot"); + const std::weak_ptr texture = pinned->SampledTextures().front(); + const std::weak_ptr pinnedSet = pinned; + cache.Insert({1}, pinned); + Require(cache.Find({1}) == pinned, "the resource cache rejected a descriptor set whose texture is cached"); + pinned.reset(); + ClearCachedTextures(context.device); + Require(!texture.expired() && texture.lock()->Released(), "the sampled texture cache dropped a texture the resource cache holds without marking it released"); + auto live = ImageResources(context, block, 1, 1, true); + std::vector> displaced; + cache.Insert({2}, live, &displaced); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == live && cache.Size() == 1, "the resource cache kept an entry holding a released texture"); + Require(displaced.size() == 1 && displaced.front() == pinnedSet.lock(), "the entry holding a released texture was not handed back to the caller"); + displaced.clear(); + Require(pinnedSet.expired() && texture.expired(), "the resource cache kept a released texture alive"); + auto stale = ImageResources(context, block, 2, 1, true); + ClearCachedTextures(context.device); + Require(stale->SampledTextures().front()->Released(), "a held texture the sampled cache dropped is not marked released"); + cache.Insert({3}, stale, &displaced); + Require(cache.Find({3}) == nullptr, "a descriptor set holding a released texture entered the resource cache"); + Require(cache.Size() == 0 && displaced.size() == 1 && displaced.front() == live, "the live entry whose texture was released stayed in the resource cache"); + displaced.clear(); + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + ClearCachedTextures(context.device); + std::puts("resource cache released texture tests passed"); +} + void rescanTests(const Context& context) { constexpr std::uint32_t SmallSide = 128; constexpr std::size_t SmallBytes = std::size_t{SmallSide} * SmallSide * 4u; @@ -678,6 +719,7 @@ int main(int argc, char** argv) { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; + const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); #else @@ -685,13 +727,28 @@ int main(int argc, char** argv) { #endif std::unique_ptr device; try { - device = std::make_unique(resources); + device = std::make_unique(!disableStorageImageMultisample); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } auto context = device->GetContext(); + if (disableStorageImageMultisample) { + PlaceholderImages placeholders(context); + for (std::uint32_t heap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + heap < ShaderRecompiler::RuntimeAbi::FirstImageBinding + ShaderRecompiler::RuntimeAbi::ImageBindingCount; ++heap) { + const auto shape = PlaceholderImageShapeOf(heap); + if (shape.samples == VK_SAMPLE_COUNT_1_BIT) continue; + bool refused = false; + try { + placeholders.View(heap); + } catch (const std::runtime_error&) { + refused = true; + } + Require(refused, "a device without storage-image MSAA returned a placeholder for typed multisample storage heap " + std::to_string(heap)); + } + } TextureDetiler detiler(context); context.detiler = &detiler; if (resources) { @@ -713,6 +770,7 @@ int main(int argc, char** argv) { reportedBudgetTests(context); snapshotTests(context); heldTests(context); + releasedTests(context); rescanTests(context); viewEvictionTests(context); viewTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 09c9c0b2da..a3a273ff50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -1,4 +1,6 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -10,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +20,7 @@ namespace { using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::Recorder; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -51,8 +55,7 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { - const auto address = base + surface * Stride; +void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) { std::vector userData(16, 0u); const auto texture = TextureDescriptor(address); std::copy(texture.begin(), texture.end(), userData.begin() + 4); @@ -67,7 +70,13 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint }; request.useCache = true; const auto result = ShaderRecompiler::Recompile(request); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { + const auto address = base + surface * Stride; + Record(device, address); device.WaitIdle(); if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast(SurfaceBytes), nullptr, "test"); @@ -75,10 +84,57 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint return StorageTexture::FindLive(address, SurfaceBytes); } +int RetainedBatch() { +#ifdef _WIN32 + Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); + Require(_putenv_s("APS5_NO_RESOURCE_CACHE", "1") == 0, "cannot disable the resource cache for the retained-batch test"); +#else + Require(setenv("APS5_TEXTURE_CACHE_MIB", "32", 1) == 0, "cannot set the test cache budget"); + Require(setenv("APS5_NO_RESOURCE_CACHE", "1", 1) == 0, "cannot disable the resource cache for the retained-batch test"); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u}); + const auto base = static_cast(reinterpret_cast(texels)); + const auto blockBytes = static_cast(SurfaceBytes + (MaxSurfaces - 1u) * Stride); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, blockBytes, true, true, true); + } + const auto release = [&] { + device->WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device->Device()); + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + }; + try { + Record(*device, base); + auto retained = StorageTexture::FindLive(base, SurfaceBytes); + Require(retained != nullptr, "surface 0 has no cached storage image"); + const auto retainedBytes = retained->BudgetBytes(); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Recording(), "the surface dispatch was not left in an open batch"); + const auto beforeEviction = recorder->OpenKeptBytes(); + const auto capacity = static_cast(TestBudget / retainedBytes); + Require(capacity + 1u < MaxSurfaces, "the eviction set exceeds the test storage"); + retained.reset(); + for (std::uint32_t surface = 1; surface <= capacity; ++surface) Record(*device, base + surface * Stride); + Require(StorageTexture::FindLive(base, SurfaceBytes) == nullptr, "surface 0 remained cached after the storage budget was exceeded"); + Require(recorder->OpenKeptBytes() >= beforeEviction + retainedBytes, "an evicted storage image held by a batch without a shared resource-cache entry was not charged to that batch"); + } catch (...) { + release(); + throw; + } + release(); + return 0; } -int main() { +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "--retained-batch") return RetainedBatch(); #ifdef _WIN32 Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); #else diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 4c03e4254e..155a95220d 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -198,7 +199,6 @@ private: std::uint32_t outstanding = 0; std::size_t cursor = 0; std::deque items; - std::mutex waiters; std::condition_variable passed; Label label{}; std::uint64_t labelSeen = 0; @@ -221,7 +221,6 @@ public: struct Manager { std::mutex lock; std::condition_variable work; - std::condition_variable nodeFreed; bool running = false; std::uint32_t blockedWaiters = 0; std::uint32_t freeNodes = 0; @@ -251,7 +250,6 @@ bool TakeNode(Manager& manager) { void ReturnNodes(Manager& manager, std::uint32_t count) { manager.freeNodes += count; - manager.nodeFreed.notify_all(); } int Sequence::Latch(int code) { @@ -303,8 +301,8 @@ void Sequence::Advance() { continue; } bool* const waiter = item.passed; - const auto retired = static_cast(cursor) + 1; - items.erase(items.begin(), items.begin() + static_cast(retired)); + const auto retired = static_cast(cursor); + items.erase(items.begin(), items.begin() + static_cast(retired + 1)); cursor = 0; commands.clear(); batchStarted = false; @@ -439,7 +437,8 @@ int Sequence::Wait() { int Sequence::WaitAndClose(std::uint32_t index) { std::unique_lock guard(manager->lock); const int result = WaitLocked(guard); - ReturnNodes(*manager, static_cast(items.size()) + 1); + const auto remaining = static_cast(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; })); + ReturnNodes(*manager, remaining + 1); items.clear(); commands.clear(); cursor = 0; @@ -457,13 +456,7 @@ int Sequence::WaitLocked(std::unique_lock& guard) { } else if (submitted.waits++ >= limits.waits) { result = Latch(SequenceLimitExceeded); } - if (manager->freeNodes == 0 && result == 0) result = Latch(SequenceNoNodes); ++manager->blockedWaiters; - manager->nodeFreed.wait(guard, [this] { return manager->freeNodes != 0; }); - --manager->freeNodes; - guard.unlock(); - const std::lock_guard serial(waiters); - guard.lock(); bool done = false; items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); Advance(); @@ -700,10 +693,16 @@ void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) { Implementation(self) = nullptr; } -void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*) { +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self); + +void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void* self) { + if (Implementation(self) != nullptr) { + static_cast(_ZN3sce3Job10JobManager8shutdownEv(self)); + } } -void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void*) { +void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void* self) { + _ZN3sce3Job10JobManagerD1Ev(self); } std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) { diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 4a1c9fdb5e..cdab277419 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -30,7 +30,7 @@ if(BUILD_TESTING) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(guest_socket_host_write_tests) - foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom sceNetRecvmsg) add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e5537bf33d..2ad1bc257b 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -763,14 +765,23 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) return fd; } +extern "C" int APS5_VABI close_nid_postfix(int d); + int APS5_VABI sceNetSocketClose(int s) { std::shared_ptr native; + bool found = false; { std::lock_guard lk(g_mutex); const auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - native = it->second.native; - g_socks.erase(it); + if (it != g_socks.end()) { + native = it->second.native; + g_socks.erase(it); + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix()); } KernelReleaseSocketNumber_nid_no_patch(s); if (native) { @@ -1089,6 +1100,14 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite messageOutput(msg, sizeof(*msg)); + if (!messageOutput.Open()) return fail(NET_EFAULT); + std::deque destinations; + for (int i = 0; i < msg->iov_length; ++i) { + const auto& entry = msg->iov[i]; + if (entry.length == 0) continue; + if (!destinations.emplace_back(entry.base, static_cast(entry.length)).Open()) return fail(NET_EFAULT); + } const bool datagram = socket.type != NET_SOCK_STREAM; #ifdef _WIN32 if (const int wait = wait_for_local_address(s, socket, total == 0)) { @@ -1152,13 +1171,22 @@ int APS5_VABI sceNetShutdown(int s, int how) { return error == 0 ? 0 : fail(error); } +extern "C" int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length); + int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { Sock socket; + bool found = false; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - socket = it->second; + if (it != g_socks.end()) { + socket = it->second; + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 2541758590..463c9642ae 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -38,6 +38,8 @@ static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x8041210 static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; +static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -61,16 +63,22 @@ static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0 static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; -static std::mutex g_callbackLock; -static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; -static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; +struct CallbackSlot { + NetCtlCallback func = nullptr; + void* arg = nullptr; + int lastEvent = 0; +}; -static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) { +static std::mutex g_callbackLock; +static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; + +static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; std::lock_guard lock(g_callbackLock); for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!callbacks[index]) { - callbacks[index] = func; + if (!callbacks[index].func) { + callbacks[index] = {func, arg, 0}; *cid = index; return 0; } @@ -78,13 +86,21 @@ static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCa return SCE_NET_CTL_ERROR_CALLBACK_MAX; } -static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) { +static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; std::lock_guard lock(g_callbackLock); - callbacks[cid] = nullptr; + callbacks[cid] = {}; return 0; } +static bool HasIpv4Callback() { + std::lock_guard lock(g_callbackLock); + for (const auto& slot : g_callbacks) { + if (slot.func) return true; + } + return false; +} + static bool HostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { @@ -324,6 +340,19 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + if (!HasIpv4Callback()) return 0; + const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; + CallbackSlot due[MAX_CALLBACKS]; + int count = 0; + { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = slot; + } + } + for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); return 0; } @@ -390,14 +419,17 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacks, func, cid); + return RegisterCallback(g_callbacks, func, arg, cid); } int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacksV6, func, cid); + return RegisterCallback(g_callbacksV6, func, arg, cid); } void APS5_VABI sceNetCtlTerm(void) { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) slot = {}; + for (auto& slot : g_callbacksV6) slot = {}; } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 848133acd5..17237a6caf 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -2,6 +2,8 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include @@ -13,6 +15,7 @@ static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; @@ -32,6 +35,27 @@ int AddRequest() { liveRequests.insert(reqId); return reqId; } + +using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); +using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); + +constexpr int NP_STATE_CALLBACK_MAX = 8; + +struct StateCallbackA { + NpStateCallbackA callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +struct StateCallback { + NpStateCallback callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +std::mutex stateMutex; +std::array stateCallbacksA; +StateCallback stateCallback; } extern "C" { @@ -42,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) { } int APS5_VABI sceNpCheckCallback(void) { + std::array callbacksA; + StateCallback legacy; + { + std::lock_guard lock(stateMutex); + callbacksA = stateCallbacksA; + legacy = stateCallback; + for (auto& entry : stateCallbacksA) entry.pending = false; + stateCallback.pending = false; + } + if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata); + for (const auto& entry : callbacksA) { + if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata); + } return 0; } @@ -172,9 +209,11 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) return 0; } -int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + stateCallback = {callback, userdata, true}; return 0; } @@ -208,6 +247,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT } int APS5_VABI sceNpUnregisterStateCallback(void) { + std::lock_guard lock(stateMutex); + if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + stateCallback = {}; return 0; } @@ -223,14 +265,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) { return 0; } -int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; - return 1; + std::lock_guard lock(stateMutex); + for (const auto& entry : stateCallbacksA) { + if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + } + for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) { + if (stateCallbacksA[index].callback) continue; + stateCallbacksA[index] = {callback, userdata, true}; + return index + 1; + } + return SCE_NP_ERROR_CALLBACK_MAX; } -int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { - (void)callback_id; +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { + if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + auto& entry = stateCallbacksA[callbackId - 1]; + if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + entry = {}; return 0; } diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index a8914303d5..70e875fed8 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -35,6 +35,7 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, + InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 53336f6a47..229dd48da9 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -36,6 +36,7 @@ private: struct InputRequests { bool toggleFullscreen = false; + bool togglePerformanceOverlay = false; bool toggleMouse = false; bool releaseMouse = false; }; diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 81842bae72..8f9db0ffb4 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -20,7 +20,8 @@ enum class InputControl { TouchLeft, TouchRight, ToggleMouse, - ToggleFullscreen + ToggleFullscreen, + TogglePerformanceOverlay }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e2aaff9966..b8e4ba2743 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -71,6 +71,16 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_video_out_cursor_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutCursor.cpp) + target_include_directories(guest_video_out_cursor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_cursor_tests PRIVATE libSceVideoOut libkernel libc) + configure_windows_unwind(guest_video_out_cursor_tests) + add_test(NAME guest_video_out_cursor COMMAND guest_video_out_cursor_tests) + set_tests_properties(guest_video_out_cursor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index 7db06b36c4..ecf6a6ce1b 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -17,6 +17,26 @@ int APS5_VABI sceVideoOutClearFlipMaster() { return 0; } +int APS5_VABI sceVideoOutCursorEnable(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorDisable(int32_t, int32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetImageAddress(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); return sceVideoOutAdjustColor(handle, settings); diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index 334984d1f7..2d9e303e1e 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -52,7 +52,8 @@ constexpr auto actions = std::array{ InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, - InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} + InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -111,6 +112,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::ToggleFullscreen) { throw std::runtime_error("ToggleFullscreen can only use a keyboard key"); } + if (action.control == Pad::InputControl::TogglePerformanceOverlay) { + throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index 9b58c374de..3f39dfa80f 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -152,6 +152,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -183,6 +184,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::TouchRight: state.touchRight = true; break; case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; + case InputControl::TogglePerformanceOverlay: break; } } for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 25ccd5af4a..5478ece435 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -11,6 +11,7 @@ #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceMouse/include/MouseState.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -228,6 +229,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { keyboardMouse.SetTitleReadsMouse(MouseIsOpen_nid_no_patch()); const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); + if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(); } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 0f991c0c3c..a7072e5e51 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -681,7 +681,7 @@ void testReleaseVblank() { const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the flip did not complete"); - check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at"); + check(cfg->lastFlipVblank >= 3, "the first flip was released before its configured interval"); } sceVideoOutUnregisterBuffers(handle, 0); sceVideoOutClose(handle); diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index 1fe0f4b30b..0d85e5f293 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -75,6 +75,23 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void performanceOverlayBinding() { + useConfig("Cross = KEY:F\n"); + bool onF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF10 = onF10 || (binding.control == Pad::InputControl::TogglePerformanceOverlay && binding.key == SDL_SCANCODE_F10); + require(onF10, "F10 toggles the performance overlay by default"); + useConfig("togglePerformanceOverlay = KEY:F9\n"); + bool onF9 = false; + bool stillOnF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::TogglePerformanceOverlay) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF10 = stillOnF10 || binding.key == SDL_SCANCODE_F10; + } + require(onF9 && !stillOnF10, "the performance overlay toggle moves to another key"); + require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -180,6 +197,7 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + performanceOverlayBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index fa9b6f7e3a..3bb0b15aa7 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -284,6 +284,9 @@ void testToggleRequests() { check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse"); check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing"); check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); + const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); + check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..98b1ace1b8 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -343,6 +343,16 @@ if(BUILD_TESTING) set_tests_properties(guest_path_case PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_path_drive_component_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathDriveComponent.cpp) + target_include_directories(guest_path_drive_component_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_drive_component_tests PRIVATE libc) + configure_windows_unwind(guest_path_drive_component_tests) + add_test(NAME guest_path_drive_component COMMAND guest_path_drive_component_tests) + set_tests_properties(guest_path_drive_component PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_path_drive_component PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + 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) @@ -463,8 +473,9 @@ if(BUILD_TESTING) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) + add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout) if(WIN32) - set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) @@ -771,6 +782,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_cnd_init_default_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndInitDefaultName.cpp) +target_include_directories(guest_libc_cnd_init_default_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_init_default_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_init_default_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_init_default_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_init_default_name COMMAND guest_libc_cnd_init_default_name_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_init_default_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) @@ -837,5 +860,14 @@ if(MINGW) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:" FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran") + add_test(NAME windows_crash_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") + add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005" + FAIL_REGULAR_EXPRESSION "stack words from rsp;data at stack words") endif() endif() diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 0f51bc7d6d..a72d143805 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include #include #include #include @@ -15,7 +16,10 @@ #include "prx/libc/include/FileLock.hpp" extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +#else +#include #endif +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); struct GuestFilePrefix { unsigned char* position = nullptr; @@ -80,6 +84,31 @@ public: } GuestFilePrefix& GuestState() { return _guest; } + bool IsHostStandard() const { + return _handle == stdin || _handle == stdout || _handle == stderr; + } + + void Detach() { + _handle = nullptr; + _guest.flags = 0; + _guest.descriptor = -1; + } + + void RestoreHostStandard(std::FILE* stream, int kept) { + const int error = errno; +#ifdef _WIN32 + const bool restored = _handle || (::_wfreopen(L"NUL", stream == stdin ? L"r" : L"w", stream) && ::_dup2(kept, ::_fileno(stream)) == 0); + ::_unlock_file(stream); + ::_close(kept); +#else + const bool restored = _handle || (std::freopen("/dev/null", stream == stdin ? "r" : "w", stream) && ::dup2(kept, ::fileno(stream)) >= 0); + ::funlockfile(stream); + ::close(kept); +#endif + if (!restored) throw std::runtime_error("FileStream: cannot restore the host standard stream"); + errno = error; + } + bool Reopen(const std::filesystem::path& filename, const char* mode) { auto* previous = GetHandle(); _guest = {}; @@ -90,11 +119,17 @@ 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); KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1; + if (kept >= 0) ::flockfile(previous); _handle = std::freopen(filename.c_str(), mode, previous); + if (kept >= 0) RestoreHostStandard(previous, kept); #endif if (!_handle) return false; _guest.flags = 0x10; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 34cee2743f..c60a1704fa 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -35,6 +35,7 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); #ifdef _WIN32 extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 436549ba2c..ff0fd39cfa 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -193,11 +193,10 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) { for (unsigned long long rest = value; rest != 0; rest >>= 4) digits.insert(digits.begin(), "0123456789abcdef"[rest & 15]); if (digits.empty() && !(hasPrecision && precision == 0)) digits = "0"; if (digits.size() < precision) digits.insert(0, precision - digits.size(), '0'); - const std::size_t used = digits.size() + 2; - const std::size_t padding = width > used ? width - used : 0; - if (left) return "0x" + digits + std::string(padding, ' '); - if (zero && !hasPrecision) return "0x" + std::string(padding, '0') + digits; - return std::string(padding, ' ') + "0x" + digits; + const std::size_t padding = width > digits.size() ? width - digits.size() : 0; + if (left) return digits + std::string(padding, ' '); + if (zero && !hasPrecision) return std::string(padding, '0') + digits; + return std::string(padding, ' ') + digits; } constexpr size_t maximumArgumentPosition = 99; @@ -380,7 +379,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } } output.Value(spec + 's', utf8.c_str()); - } else if (conversion == 'p' && length.empty()) { + } else if (conversion == 'p' && (length.empty() || length == "l")) { const std::string text = FormatPointer(spec, args.Next()); output.Append(text.data(), text.size()); } else if (conversion == 'm') { diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp index be3916a765..ba32feae07 100644 --- a/core/libs/prx/libc/src/AppArchive.cpp +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -22,8 +22,8 @@ struct Archive { }; Archive& State() { - static Archive archive; - return archive; + static auto* archive = new Archive; + return *archive; } bool SameComponent(std::string_view left, std::string_view right) { diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index fa8e028a98..bd221c5d2b 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -283,7 +283,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode std::filesystem::path path; if (*filename) { if (const int error = ResolvePathError_nid_no_patch(filename, path)) { - stream->Close(); + if (stream->IsHostStandard()) stream->Detach(); + else stream->Close(); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 7a77dad2de..bb4163aa43 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -31,13 +31,14 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { #ifndef _WIN32 -bool HasGuestWideFormat(const char* format) { +bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; - if (*format == 'S' || *format == 'C' || *format == 'm' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true; + if (*format == 'S' || *format == 'C' || *format == 'm' || *format == 'p' || + (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; if (*format) ++format; } return false; @@ -222,15 +223,15 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = text.c_str(); #else std::string guestText; - const bool guestWide = HasGuestWideFormat(format); + const bool guestFormatting = RequiresGuestFormatting(format); char* text = nullptr; std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int count = guestWide ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); + const int count = guestFormatting ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); va_end(copy); std::unique_ptr owner(text, std::free); if (count < 0) return -1; - const char* source = guestWide ? guestText.c_str() : text; + const char* source = guestFormatting ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); if (!output) { @@ -255,7 +256,7 @@ int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaLis const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; #else - if (HasGuestWideFormat(format)) { + if (RequiresGuestFormatting(format)) { std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; @@ -349,7 +350,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -358,7 +359,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -366,7 +367,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -374,7 +375,7 @@ int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); va_end(args); return result; } @@ -441,7 +442,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else - if (HasGuestWideFormat(str)) return LibcDetail::PrintWindows(str, c); + if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vprintf(str, copy); @@ -454,7 +455,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) #ifdef _WIN32 return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsprintf(str, format, copy); @@ -467,7 +468,7 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, size, format, c); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, size, format, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vsnprintf(str, size, format, copy); @@ -512,7 +513,7 @@ int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaLi } int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -521,7 +522,7 @@ int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* for } int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -532,7 +533,7 @@ int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -540,7 +541,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -548,7 +549,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d96670de58..bd7f31df31 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -111,12 +111,39 @@ std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) { return RelinkedModulePath_nid_no_patch(path); } -std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative, bool module = false) { - const auto direct = root / relative; +std::vector SplitNames(std::string_view text) { + std::vector names; + while (!text.empty()) { + const auto separator = text.find('/'); + names.emplace_back(text.substr(0, separator)); + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + if (text.empty()) names.emplace_back(); + } + return names; +} + +std::string JoinNames(const std::vector& names) { + std::string joined; + for (std::size_t i = 0; i < names.size(); ++i) { + if (i != 0) joined += '/'; + joined += names[i]; + } + return joined; +} + +std::filesystem::path Child(std::filesystem::path parent, const std::string& name) { + parent /= ""; + parent += name; + return parent; +} + +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; if (std::filesystem::exists(direct, error)) return direct; for (auto part = relative.begin(); part != relative.end(); ++part) { - const auto candidate = root / *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)) { root = candidate; @@ -124,7 +151,7 @@ std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::fil } const bool last = module && std::next(part) == relative.end(); const auto relinked = RelinkedModuleName(candidate); - NameMatch names[] = {{part->string()}, {relinked.filename().string()}}; + NameMatch names[] = {{*part}, {relinked.filename().string()}}; error = MatchNames(root, std::span(names, last ? 2 : 1)); if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); const bool matched = !error && names[0].path; @@ -152,12 +179,12 @@ std::optional ResolveAlias(const std::string& guestPath, } #ifndef _WIN32 if (guestPath.back() == '/') { - auto result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); result /= ""; return module ? PathOrRelinkedModule(result) : result; } #endif - return ResolveHostPath(host, relative.substr(prefix.size() + 1), module).make_preferred(); + return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; } @@ -171,8 +198,8 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } -std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { - const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); +std::optional ProvidedPath(const WorkingDirectory& state, const std::vector& names) { + const auto relative = TrimSlashes(JoinNames(names).c_str()); PathProvider provide = nullptr; { auto& aliases = Aliases(); @@ -182,20 +209,20 @@ std::optional ProvidedPath(const WorkingDirectory& state, } } if (provide == nullptr) return std::nullopt; - const auto supplied = ResolveHostPath(state.root, guest.relative_path()); + const auto supplied = ResolveHostPath(state.root, names); std::error_code error; if (std::filesystem::exists(supplied, error) || error) return supplied; auto provided = provide(state.root); if (provided.empty()) return std::nullopt; return provided.make_preferred(); } -std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { - if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; - if (auto provided = ProvidedPath(state, guest)) return *provided; - return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); +std::filesystem::path HostPath(WorkingDirectory& state, const std::vector& names, bool module = false) { + if (auto aliased = ResolveAlias(JoinNames(names), module)) return *aliased; + if (auto provided = ProvidedPath(state, names)) return *provided; + return ResolveHostPath(state.root, names, module).make_preferred(); } -void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { - const auto host = HostPath(state, guest); +void RequireHostDirectory(WorkingDirectory& state, const std::vector& names) { + const auto host = HostPath(state, names); std::error_code error; const auto status = std::filesystem::status(host, error); if (std::filesystem::is_directory(status)) return; @@ -213,25 +240,32 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } +std::vector WithTrailingSeparator(std::vector names, bool trailing) { + if (trailing && !names.empty()) names.emplace_back(); + return names; +} std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; - std::filesystem::path input(text); #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. - if (input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; + if (std::filesystem::path input(text); input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; #endif - const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); - const auto relative = (input.is_absolute() ? input : current / input).relative_path(); - std::filesystem::path guest("/"); - for (auto part = relative.begin(); part != relative.end(); ++part) { - if (*part == "..") { - const auto parent = guest.lexically_normal(); - if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent); + if (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text; + std::vector names; + bool trailing = true; + const auto parts = SplitNames(text); + for (std::size_t i = 0; i < parts.size(); ++i) { + const auto& name = parts[i]; + if (name.empty() && i + 1 != parts.size()) continue; + if (name == ".." && !names.empty()) { + if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing)); + names.pop_back(); } - guest /= *part; + trailing = name.empty() || name == "." || name == ".."; + if (!trailing) names.push_back(name); } - return HostPath(state, guest.lexically_normal(), module); + return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); } #ifdef _WIN32 using WriteMark = FILETIME; @@ -356,6 +390,10 @@ extern "C" int KeepHostStandardError_nid_no_patch() { extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } + +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); +} #endif extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { @@ -612,6 +650,12 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); } +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const int kept = ::_dup(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return kept; +} #endif extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index ca580fc5bd..876cf75826 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -11,6 +11,10 @@ #include #include #endif +#ifdef __APPLE__ +#include +#include +#endif #include #include #include @@ -148,8 +152,8 @@ public: private: Arena() { -#if defined(__linux__) - SeedLinuxUsedRanges(); +#if defined(__linux__) || defined(__APPLE__) + SeedHostUsedRanges(); #elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; SYSTEM_INFO system{}; @@ -182,7 +186,8 @@ private: } #if defined(__linux__) - void SeedLinuxUsedRanges() { + static std::vector> HostMappings() { + std::vector> mappings; std::ifstream maps("/proc/self/maps"); if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps"); std::string line; @@ -191,6 +196,48 @@ private: std::uintptr_t begin = 0, end = 0; char dash = 0; if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line); + mappings.emplace_back(begin, end); + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + void* const wanted = reinterpret_cast(address); + void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) return 0; + if (placed == MAP_FAILED) return errno; + munmap(placed, bytes); + return EINVAL; + } +#elif defined(__APPLE__) + static std::vector> HostMappings() { + std::vector> mappings; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (result == KERN_INVALID_ADDRESS) break; + if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed"); + mappings.emplace_back(static_cast(address), static_cast(address + size)); + address += size; + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + mach_vm_address_t wanted = address; + const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result == KERN_SUCCESS) return 0; + return result == KERN_NO_SPACE ? EEXIST : ENOMEM; + } +#endif + +#if defined(__linux__) || defined(__APPLE__) + void SeedHostUsedRanges() { + for (const auto& [begin, end] : HostMappings()) { if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue; const auto first = std::max(begin, ArenaStart); const auto last = std::min(end, ApplicationAreaEnd); @@ -208,12 +255,8 @@ private: constexpr std::uintptr_t kChunk = 1ULL << 30; for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) { const auto end = std::min(cursor + kChunk, to); - void* const wanted = reinterpret_cast(cursor); - void* const placed = mmap(wanted, end - cursor, PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); - if (placed == wanted) continue; - const int failed = placed == MAP_FAILED ? errno : EINVAL; - if (placed != MAP_FAILED) munmap(placed, end - cursor); + const int failed = ReserveNoReplace(cursor, end - cursor); + if (failed == 0) continue; if (failed != EEXIST) { char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", @@ -428,7 +471,7 @@ void GuestArenaForgetSection_nid_postfix(void* section) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) namespace { int PosixProtection(std::uint32_t windowsProtection) { switch (windowsProtection) { @@ -449,7 +492,7 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { if (!Arena::Get().Contains(reinterpret_cast(address), bytes)) throw OutsideArena("set protection", reinterpret_cast(address), bytes); if (mprotect(reinterpret_cast(address), bytes, PosixProtection(protection)) != 0) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ddeea98f8c..a3eb9dc10c 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -319,6 +320,8 @@ long double APS5_VABI fminl_nid_postfix(long double x, long double y) { return G double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); } float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); } long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); } +double APS5_VABI fdim_nid_postfix(double x, double y) { return std::fdim(x, y); } +float APS5_VABI fdimf_nid_postfix(float x, float y) { return std::fdim(x, y); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } long double APS5_VABI acoshl_nid_postfix(long double x) { return std::acosh(x); } long double APS5_VABI asinhl_nid_postfix(long double x) { return std::asinh(x); } @@ -545,6 +548,16 @@ long double APS5_VABI powl_nid_postfix(long double base, long double exp) { retu long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +int APS5_VABI ilogb_nid_postfix(double x) { + if (x == 0.0) { + return INT_MIN; + } + if (!std::isfinite(x)) { + return INT_MAX; + } + return std::ilogb(x); +} +int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast(x)); } double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 9d59cff429..cd486feecf 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -7,10 +7,26 @@ #ifdef _WIN32 #include #else +#include #include #endif -namespace { FileStream input{stdin}; } +namespace { +#ifndef _WIN32 +const bool standardDescriptorsOpen = [] { + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + if (::fcntl(descriptor, F_GETFD) != -1 || errno != EBADF) continue; + const int opened = ::open("/dev/null", descriptor == 0 ? O_RDONLY : O_WRONLY); + if (opened > descriptor) { + ::dup2(opened, descriptor); + ::close(opened); + } + } + return true; +}(); +#endif +FileStream input{stdin}; +} extern "C" { FileStream* __stdinp_nid_postfix = &input; FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix; diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index a5cc181812..167360e72c 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -187,6 +187,10 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); +} + void APS5_VABI _Cnd_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Cnd_destroy: null condition handle pointer"); diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 47cfaed9fc..c23d288aa9 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -256,6 +256,58 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) { return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); } +std::uint64_t EnvironmentNumber(const char* name) { + char text[32]; + const DWORD length = GetEnvironmentVariableA(name, text, sizeof(text)); + if (length == 0 || length >= sizeof(text)) return 0; + return std::strtoull(text, nullptr, 0); +} + +void ReportStackWords(std::uint64_t rsp) { + const auto bytes = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); + if (bytes == 0) return; + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto stackLow = reinterpret_cast(tib->StackLimit); + const auto stackHigh = reinterpret_cast(tib->StackBase); + const auto first = rsp & ~std::uint64_t{7}; + Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast(rsp)); + const auto limit = std::min(first + bytes, stackHigh); + std::uint64_t end = first; + std::uint64_t readablePage = 0; + for (std::uint64_t at = first; at < limit; at += 32) { + std::uint64_t words[4] = {}; + int count = 0; + for (; count < 4; ++count) { + const auto slot = at + static_cast(count) * 8; + const auto page = slot & ~std::uint64_t{0xfff}; + if (page != readablePage) { + if (!IsReadable(slot)) break; + readablePage = page; + } + words[count] = *reinterpret_cast(slot); + } + if (count == 0) break; + static char text[192]; + int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast(at - first)); + for (int k = 0; k < count && used > 0 && used < static_cast(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast(used), " %016llx", static_cast(words[k])); + Report("%s\n", text); + end = at + static_cast(count) * 8; + if (count < 4) break; + } + static std::uint64_t seen[256]; + std::size_t seenCount = 0; + Report(" data at stack words that point outside the stack (8 words each):\n"); + for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) { + const auto value = *reinterpret_cast(slot); + if (value < 0x10000 || (value & 7) != 0 || value > 0x7fffffffffffull || (value >= stackLow && value < stackHigh)) continue; + if (std::find(seen, seen + seenCount, value) != seen + seenCount) continue; + if (IsExecutable(value) || !IsReadable(value) || !IsReadable(value + 0x38)) continue; + seen[seenCount++] = value; + const auto* words = reinterpret_cast(value); + Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast(slot - first), static_cast(value), words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + } +} + void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { const auto* record = info->ExceptionRecord; char type[256] = "of unknown type"; @@ -377,7 +429,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { frame = next; } ReportStackCandidates(context->Rsp, 16, 0x2000); - if (context->Rip == 0 || !IsExecutable(context->Rip)) { + ReportStackWords(context->Rsp); + if (context->Rip == 0 ||!IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. Report(" recently popped return address candidates:\n"); diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp new file mode 100644 index 0000000000..4e828a4970 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP +#define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP + +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" + +#include + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath); + +#endif diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index c2c9b2d2c6..f078143320 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" #include "prx/libc/include/General.hpp" #include @@ -25,6 +26,7 @@ std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; +constexpr const char* AppMetadataParamSfoGuestPath = "/app0/sce_sys/param.sfo"; constexpr const char* AppMetadataIconGuestPath = "/app0/sce_sys/icon0.png"; void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std::string& source) { @@ -33,12 +35,16 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std std::memcpy(destination, source.c_str(), source.size() + 1); } +ParsedParamJson parseParams(const std::filesystem::path& paramJson, const std::filesystem::path& paramSfo) { + if (std::filesystem::exists(paramJson)) return parseParamJson(paramJson); + if (std::filesystem::exists(paramSfo)) return parseParamSfo(paramSfo); + throw std::runtime_error("param.json or param.sfo not found"); +} + void ensureTitleLoaded() { std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; - const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); - if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); - const auto parsed = parseParamJson(resolvedPath); + const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; @@ -50,8 +56,9 @@ void ensureTitleLoaded() { const bool g_downloadDataMounted = [] { try { const auto run = std::filesystem::current_path(); - const auto paramJson = run / "app0" / "sce_sys" / "param.json"; - if (!std::filesystem::exists(paramJson) || parseParamJson(paramJson).downloadDataSizeMiB == 0) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; + if (parseParams(metadata / "param.json", metadata / "param.sfo").downloadDataSizeMiB == 0) return false; std::filesystem::create_directories(run / "download0"); return true; } catch (const std::exception&) { @@ -62,7 +69,8 @@ const bool g_downloadDataMounted = [] { const bool g_dataMounted = [] { try { const auto run = std::filesystem::current_path(); - if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; std::filesystem::create_directories(run / "data"); return true; } catch (const std::exception&) { diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp new file mode 100644 index 0000000000..bfaf32d9f8 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp @@ -0,0 +1,85 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint16_t SfoUtf8 = 0x0204; +constexpr std::uint16_t SfoInteger = 0x0404; +constexpr std::size_t HeaderSize = 20; +constexpr std::size_t EntrySize = 16; + +std::uint16_t readU16(const std::vector& bytes, std::size_t offset) { + std::uint16_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::uint32_t readU32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +} + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath) { + std::ifstream file(paramSfoPath, std::ios::binary); + if (!file.is_open()) throw std::runtime_error("failed to open param.sfo"); + const std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (bytes.size() < HeaderSize || std::memcmp(bytes.data(), "\0PSF", 4) != 0) throw std::runtime_error("param.sfo has no PSF header"); + const std::uint64_t keyTable = readU32(bytes, 8); + const std::uint64_t dataTable = readU32(bytes, 12); + const std::uint64_t count = readU32(bytes, 16); + if (count > (bytes.size() - HeaderSize) / EntrySize) throw std::runtime_error("param.sfo tables are out of bounds"); + std::set keys; + std::map strings; + std::map integers; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = HeaderSize + index * EntrySize; + const auto keyOffset = keyTable + readU16(bytes, entry); + const auto format = readU16(bytes, entry + 2); + const std::uint64_t length = readU32(bytes, entry + 4); + const auto dataOffset = dataTable + readU32(bytes, entry + 12); + if (keyOffset >= bytes.size() || dataOffset > bytes.size() || length > bytes.size() - dataOffset) throw std::runtime_error("param.sfo entry is out of bounds"); + const auto keyEnd = std::find(bytes.begin() + static_cast(keyOffset), bytes.end(), std::uint8_t{0}); + if (keyEnd == bytes.end()) throw std::runtime_error("param.sfo key is not terminated"); + const std::string key(bytes.begin() + static_cast(keyOffset), keyEnd); + if (!keys.insert(key).second) throw std::runtime_error("param.sfo repeats " + key); + if (format == SfoUtf8) { + const auto begin = bytes.begin() + static_cast(dataOffset); + const auto end = std::find(begin, begin + static_cast(length), std::uint8_t{0}); + if (end == begin + static_cast(length)) throw std::runtime_error("param.sfo " + key + " is not a terminated string"); + strings.emplace(key, std::string(begin, end)); + } else if (format == SfoInteger) { + if (length != 4) throw std::runtime_error("param.sfo " + key + " is not a 32-bit integer"); + integers.emplace(key, static_cast(readU32(bytes, dataOffset))); + } + } + const auto text = [&](const char* key) { + const auto found = strings.find(key); + if (found == strings.end() || found->second.empty()) throw std::runtime_error(std::string("param.sfo is missing ") + key); + return found->second; + }; + ParsedParamJson result; + result.title = text("TITLE"); + result.titleId = text("TITLE_ID"); + if (const auto size = integers.find("DOWNLOAD_DATA_SIZE"); size != integers.end()) { + if (size->second < 0) throw std::runtime_error("param.sfo DOWNLOAD_DATA_SIZE is negative"); + result.downloadDataSizeMiB = static_cast(size->second); + } + for (int index = 0; index < 4; ++index) { + const auto param = integers.find("USER_DEFINED_PARAM_" + std::to_string(index + 1)); + if (param != integers.end()) result.userDefinedParams[index] = param->second; + } + return result; +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f3627ea391..2d1502fa1e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -76,6 +76,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) @@ -105,12 +106,12 @@ if(BUILD_TESTING) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) configure_windows_unwind(download_data_mount_tests) - foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0) + foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0 sfo:0) string(REPLACE ":" ";" downloadDataCase ${downloadDataCase}) list(GET downloadDataCase 0 downloadDataName) list(GET downloadDataCase 1 downloadDataSize) set(downloadDataRun ${CMAKE_CURRENT_BINARY_DIR}/download_data_mount_${downloadDataName}) - if(downloadDataName STREQUAL "missing") + if(downloadDataName STREQUAL "missing" OR downloadDataName STREQUAL "sfo") file(MAKE_DIRECTORY ${downloadDataRun}) elseif(downloadDataName STREQUAL "invalid") file(WRITE ${downloadDataRun}/app0/sce_sys/param.json "{\"downloadDataSize\":${downloadDataSize}}") @@ -135,6 +136,15 @@ if(BUILD_TESTING) set_tests_properties(param_json_parser PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(param_sfo_parser_tests ${LIBS_INCLUDE_DIR}/tests/ParamSfoParser.cpp) + target_include_directories(param_sfo_parser_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(param_sfo_parser_tests PRIVATE libkernel libc) + configure_windows_unwind(param_sfo_parser_tests) + add_test(NAME param_sfo_parser COMMAND param_sfo_parser_tests) + if(WIN32) + set_tests_properties(param_sfo_parser PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_posix_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSem.cpp) target_include_directories(guest_posix_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_sem_tests PRIVATE libkernel libc) @@ -275,6 +285,11 @@ if(BUILD_TESTING) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests) set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10) + add_test_executable(guest_closed_standard_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestClosedStandardDescriptors.cpp) + target_include_directories(guest_closed_standard_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_closed_standard_descriptors_tests PRIVATE libkernel libc) + add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) + set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) @@ -420,6 +435,10 @@ if(BUILD_TESTING) add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) + if(WIN32) + target_compile_definitions(guest_thread_self_tests PRIVATE _WIN32_WINNT=0x0602) + target_link_options(guest_thread_self_tests PRIVATE "SHELL:-Xlinker --stack=0x210000,0x200000") + endif() configure_windows_unwind(guest_thread_self_tests) add_test(NAME guest_thread_self COMMAND guest_thread_self_tests) set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20) @@ -681,8 +700,20 @@ if(BUILD_TESTING) configure_windows_unwind(guest_semaphore_tests) add_test(NAME guest_semaphore COMMAND guest_semaphore_tests) set_tests_properties(guest_semaphore PROPERTIES TIMEOUT 20) + if(Python3_Interpreter_FOUND) + add_test_executable(semaphore_trace_tests + ${LIBS_INCLUDE_DIR}/tests/SemaphoreTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp) + target_include_directories(semaphore_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + find_package(Threads REQUIRED) + target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads libc) + add_test(NAME guest_semaphore_trace COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/test_semaphore_trace.py $) + set_tests_properties(guest_semaphore_trace PROPERTIES TIMEOUT 20) + endif() if(WIN32) - set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_semaphore guest_semaphore_trace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_timed_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadTimedLocks.cpp) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 23fd92fc59..9f8639670c 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -244,8 +244,17 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } std::filesystem::path native; const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; - if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) return SceKernelError(error); - return OpenNative(native, flags, mode); + if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) { + const int result = SceKernelError(error); + APS5_LOG_ERR("open failed path=%s resolveError=%d result=0x%08X", path != nullptr ? path : "", error, static_cast(result)); + return result; + } + const int result = OpenNative(native, flags, mode); + if (result < 0) { + const auto resolved = native.u8string(); + APS5_LOG_ERR("open failed path=%s resolvedPath=%s result=0x%08X", path != nullptr ? path : "", reinterpret_cast(resolved.c_str()), static_cast(result)); + } + return result; } int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 0bfedc48da..531c800fbf 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -786,6 +786,10 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { return 0; } +int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb) { + return fstat_nid_disambig1_nid_postfix(d, sb); +} + int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); @@ -1439,4 +1443,12 @@ int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; } +int64_t APS5_VABI _readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return readv_nid_postfix(d, iov, iovcnt); +} + +int64_t APS5_VABI _writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return writev_nid_postfix(d, iov, iovcnt); +} + } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 9b049a94f5..44c987b41e 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +64,7 @@ struct PthreadCondattrPrivate { }; struct PthreadCondPrivate { - TimedWait::Condition _cv; + std::shared_ptr _cv = std::make_shared(); int _clockid = 0; }; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index f0686f555d..557a81aa3c 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -13,6 +14,7 @@ #include #include #include +#include namespace { @@ -31,21 +33,20 @@ PthreadMutex adaptiveInitializer() { return reinterpret_cast(std::uintptr_t{1}); } -PthreadCond resolveCond(PthreadCond* cond) { +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::atomic_ref slot(*cond); - if (const auto current = slot.load(std::memory_order_acquire); current && current != destroyedCond()) - return current; std::lock_guard lock(condInitializationMutex); - const auto current = slot.load(std::memory_order_acquire); + std::atomic_ref slot(*cond); + auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) throw std::runtime_error("Condition variable has been destroyed"); - if (current) - return current; - auto* created = new PthreadCondPrivate(); - slot.store(created, std::memory_order_release); - return created; + if (!current) { + current = new PthreadCondPrivate(); + slot.store(current, std::memory_order_release); + } + if (clockId) *clockId = current->_clockid; + return current->_cv; } int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { @@ -73,8 +74,16 @@ struct WakingLock { } }; -int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { - auto* c = resolveCond(cond); +int finishWait(std::shared_ptr& condition, bool timedOut) { + if (ThreadCancel::Requested()) { + condition->NotifyOne(); + condition.reset(); + scePthreadExit(GUEST_CANCELED); + } + return timedOut ? sceTimedOut : 0; +} + +int waitUntil(std::shared_ptr condition, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { PthreadMutex m = nullptr; if (const int error = mutexOwnership(mutex, &m)) return error; @@ -92,32 +101,32 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional_owner.store(std::thread::id{}, std::memory_order_release); for (int level = 1; level < previousCount; ++level) m->_rmtx.unlock(); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); for (int level = 1; level < previousCount; ++level) m->_rmtx.lock(); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); m->_count = previousCount; lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } WakingLock waking{m, m->_mtx}; std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } } int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { KernelUseconds usec = 0; - if (!PosixThread::RelativeMicroseconds(resolveCond(cond)->_clockid, abstime, &usec)) + int clockId = 0; + auto condition = resolveCond(cond, &clockId); + if (!PosixThread::RelativeMicroseconds(clockId, abstime, &usec)) throw std::invalid_argument("Invalid absolute condition variable timeout"); - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(std::move(condition), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } extern "C" { @@ -165,27 +174,27 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { } int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyOne(); + resolveCond(cond)->NotifyOne(); return 0; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { (void)thread; - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { - return waitUntil(cond, mutex, std::nullopt, __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, std::nullopt, __builtin_return_address(0)); } int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fdbdb8a765..fe9aa84863 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -172,12 +172,32 @@ static void FreeGuestStack(PthreadPrivate*) {} static void RetireGuestStack(PthreadPrivate*) {} #endif +#ifdef _WIN32 +static bool CommittedReadWrite(std::uintptr_t begin, std::uintptr_t end) { + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.RegionSize == 0) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "Querying the host thread stack"); + if (memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE) return false; + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} + +static std::uintptr_t PrecommittedStackStart(std::uintptr_t low, std::uintptr_t high) { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::uintptr_t start = low + system.dwAllocationGranularity; + return start < high && CommittedReadWrite(start, high) ? start : low; +} +#endif + static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 ULONG_PTR lowLimit = 0; ULONG_PTR highLimit = 0; GetCurrentThreadStackLimits(&lowLimit, &highLimit); - *low = lowLimit; + *low = PrecommittedStackStart(lowLimit, highLimit); *high = highLimit; #else pthread_attr_t attr; @@ -674,12 +694,8 @@ static unsigned __stdcall StartNativeThread(void* opaque) { if (high <= low || high - low < self->stackSize) throw std::runtime_error("Cannot query guest thread stack"); self->stackAddress = reinterpret_cast(high - self->stackSize); - for (auto cursor = high - self->stackSize; cursor < high;) { - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE || memory.RegionSize == 0) - throw std::runtime_error("Guest thread stack is not fully committed"); - cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; - } + if (!CommittedReadWrite(high - self->stackSize, high)) + throw std::runtime_error("Guest thread stack is not fully committed"); self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 7375ae7ff2..725f297e67 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,6 +1,8 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include +#include +#include #include #include @@ -10,6 +12,11 @@ KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCou : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } +static bool TraceSync() { + static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; + return enabled; +} + extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt) { @@ -19,6 +26,7 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); + if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast(*sem), name, attr, init, max); return KERNEL_SEMA_OK; } @@ -44,7 +52,9 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { if (count > sem->maxCount - sem->tokenCount) { return SCE_KERNEL_ERROR_EINVAL; } + const auto before = sem->tokenCount; sem->tokenCount += count; + if (TraceSync()) std::fprintf(stderr, "[sema] signal %p '%s' count=%d have=%d->%d\n", static_cast(sem), sem->name.c_str(), count, before, sem->tokenCount); sem->condition.NotifyAll(); return KERNEL_SEMA_OK; } @@ -71,20 +81,28 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } waiterGuard{sem, generation}; const auto waitStart = std::chrono::steady_clock::now(); + const void* caller = __builtin_return_address(0); + if (TraceSync()) std::fprintf(stderr, "[sema] wait begin %p '%s' need=%d have=%d timeout=%lld caller=%p\n", static_cast(sem), sem->name.c_str(), need, sem->tokenCount, time ? static_cast(*time) : -1LL, caller); + const auto traceSyncWaitEnd = [&](int result) { + if (TraceSync()) std::fprintf(stderr, "[sema] wait end %p '%s' result=0x%x have=%d caller=%p\n", static_cast(sem), sem->name.c_str(), result, sem->tokenCount, caller); + }; const auto traceWait = [&](bool timedOut) { - KernelTraceWait_nid_postfix("sema", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); + KernelTraceWait_nid_postfix("sema", caller, static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); }; const auto released = [&] { return sem->tokenCount >= need || sem->deleted || sem->cancelGeneration != generation; }; if (time == nullptr) { sem->condition.Wait(lock, released); traceWait(false); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } @@ -93,15 +111,19 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) *time = static_cast(TimedWait::RemainingMicros(deadline)); traceWait(!acquired); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } if (!acquired) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ETIMEDOUT); return SCE_KERNEL_ERROR_ETIMEDOUT; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5b79792d75..53983f29ed 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -740,6 +741,20 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { return 0; } +namespace { +std::int64_t ListenQueueLength(NativeSocket socket) { +#ifdef __linux__ + tcp_info info{}; + socklen_t length = sizeof(info); + if (getsockopt(socket, IPPROTO_TCP, TCP_INFO, &info, &length) == 0 && length >= offsetof(tcp_info, tcpi_unacked) + sizeof(info.tcpi_unacked)) + return info.tcpi_unacked; +#else + (void)socket; +#endif + return 1; +} +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr socket; { @@ -766,6 +781,8 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e int queued = 0; if (*data > 0 && ::ioctl(socket->value, SIOCOUTQ, &queued) == 0) *data = std::max(0, *data - queued); #endif + } else if (Listening(*socket)) { + *data = ListenQueueLength(socket->value); } else { #ifdef _WIN32 unsigned long available = 0; @@ -926,6 +943,20 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { socket->options.nonblocking = enabled != 0; return 0; } +int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, std::uint64_t); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, std::uint64_t); + va_end(arguments); +#endif + return fcntl_nid_postfix(descriptor, command, argument); +} int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { int netError = 0; @@ -986,6 +1017,10 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { return Fail(12); } } +int APS5_VABI __sys_socketex_nid_postfix(const char* name, int family, int type, int protocol) { + static_cast(name); + return socket_nid_postfix(family, type, protocol); +} int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* descriptors) { const bool nonblocking = (type & GuestNonBlocking) != 0; const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 12523a767b..4c447a1a45 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -318,41 +318,54 @@ int APS5_VABI usleep_nid_postfix(KernelUseconds microseconds) { } #ifdef _WIN32 -static std::uint64_t CpuTimeNanos(bool callingThread, bool countKernel) { +static constexpr int GUEST_EIO = 5; +static constexpr int GUEST_EPERM = 1; + +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error) { + if (error == ERROR_NOACCESS) return GUEST_EFAULT; + if (error == ERROR_ACCESS_DENIED) return GUEST_EPERM; + return GUEST_EIO; +} + +static bool CpuTimeNanos(bool callingThread, bool countKernel, std::uint64_t* nanos) { FILETIME creation{}; FILETIME exitTime{}; FILETIME kernel{}; FILETIME user{}; if (callingThread) { - if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetThreadTimes failed"); + if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } else { - if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetProcessTimes failed"); + if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } const auto toTicks = [](const FILETIME& time) { return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; }; - return (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + *nanos = (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + return true; } -static std::uint64_t CpuTimeResolutionNanos() { +static bool CpuTimeResolutionNanos(std::uint64_t* nanos) { DWORD adjustment = 0; DWORD increment = 0; BOOL adjustmentDisabled = FALSE; - if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) - throw std::system_error(GetLastError(), std::system_category(), "clock_getres: GetSystemTimeAdjustment failed"); + if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } if (increment == 0) throw std::runtime_error("clock_getres: GetSystemTimeAdjustment reported no clock interrupt period"); - return static_cast(increment) * 100ULL; + *nanos = static_cast(increment) * 100ULL; + return true; } -static std::uint64_t ProcessCpuNanos(bool countSystem) { - return CpuTimeNanos(false, countSystem); -} - -static std::uint64_t ProcessCpuResolutionNanos() { - return CpuTimeResolutionNanos(); +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { + return CpuTimeResolutionNanos(nanos); } #else static std::uint64_t ClockTicksPerSecond() { @@ -372,9 +385,10 @@ static std::uint64_t ProcessCpuNanos(bool countSystem) { return ticks / rate * 1000000000ULL + ticks % rate * 1000000000ULL / rate; } -static std::uint64_t ProcessCpuResolutionNanos() { +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { const std::uint64_t rate = ClockTicksPerSecond(); - return (1000000000ULL + rate - 1ULL) / rate; + *nanos = (1000000000ULL + rate - 1ULL) / rate; + return true; } #endif @@ -390,7 +404,12 @@ static int ReadClock(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 1 || clockId == 2) { +#ifdef _WIN32 + std::uint64_t nanos = 0; + if (!CpuTimeNanos(false, clockId == 2, &nanos)) return -1; +#else const std::uint64_t nanos = ProcessCpuNanos(clockId == 2); +#endif tp->tv_sec = static_cast(nanos / 1000000000ULL); tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -423,7 +442,8 @@ static int ReadClock(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeNanos(clockId == 14, true); + std::uint64_t nanos = 0; + if (!CpuTimeNanos(clockId == 14, true, &nanos)) return -1; tp->tv_sec = static_cast(nanos / 1000000000ULL); tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -514,7 +534,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { if (IsCpuClock(clockId)) return clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, res); if (clockId == 1 || clockId == 2) { - const std::uint64_t nanos = ProcessCpuResolutionNanos(); + std::uint64_t nanos = 0; + if (!ProcessCpuResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -542,7 +563,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeResolutionNanos(); + std::uint64_t nanos = 0; + if (!CpuTimeResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; diff --git a/core/libs/prx/libkernel/Time/include/Time.hpp b/core/libs/prx/libkernel/Time/include/Time.hpp index dc1f2a0d85..925140891d 100644 --- a/core/libs/prx/libkernel/Time/include/Time.hpp +++ b/core/libs/prx/libkernel/Time/include/Time.hpp @@ -15,6 +15,9 @@ extern "C" { int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); +#ifdef _WIN32 + int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); // Debug aid: APS5_TRACE_WAITS=1 aggregates blocking waits (event flags, semaphores, condition diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index e8693b984e..2db861b0f5 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -177,23 +177,28 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(threw); } char expected[1024]; - std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, + std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g %llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); snprintf_nid_postfix(buffer, sizeof(buffer), "%#08x %.3e %a %g %p", 42u, 1.25, 1.25, 1.25, static_cast(buffer)); Require(std::strcmp(buffer, expected) == 0); -#ifdef _WIN32 char pointers[128]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - std::strcmp(pointers, "0x0|0x1234|0xdeadbeef12") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%8p][%-8p][%08p][%.6p][%10.4p][%.0p]", - address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0)) == 56 && - std::strcmp(pointers, "[ 0x1a][0x1a ][0x00001a][0x00001a][ 0x001a][0x]") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%+p][% p][%#p][%*p][%-*p]", address(0x1a), address(0x1a), - address(0x1a), 6, address(0xf), -6, address(0xf)) == 34 && - std::strcmp(pointers, "[0x1a][0x1a][0x1a][ 0xf][0xf ]") == 0); - Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 8 && std::strcmp(pointers, "0xabc") == 0); -#endif + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + std::strcmp(pointers, "0|1234|deadbeef12") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%p][%5p][%-5p][%05p][%.8p][%10.6p][%.0p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0)) == 51 && + std::strcmp(pointers, "[1234][ 1234][1234 ][01234][00001234][ 001234][]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%#p][%+p][% p][%lp][%20p][%-20p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234)) == 68 && + std::strcmp(pointers, "[1234][1234][1234][1234][ 1234][1234 ]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%08p][%.6p][%8p]", address(0x1a), address(0x1a), address(0xf)) == 28 && + std::strcmp(pointers, "[0000001a][00001a][ f]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%*p][%-*p]", 6, address(0x1a), -6, address(0x1a)) == 16 && + std::strcmp(pointers, "[ 1a][1a ]") == 0); + Require(FormatList(pointers, sizeof(pointers), "%2$p:%1$d", 7, address(0)) == 3 && std::strcmp(pointers, "0:7") == 0); + Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 6 && std::strcmp(pointers, "abcde") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p", address(0xffffffffffffffffull)) == 16 && + std::strcmp(pointers, "ffffffffffffffff") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.*s", -1, "unlimited"); Require(std::strcmp(buffer, "unlimited") == 0); buffer[5] = '!'; diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e65d891a06..5421462aea 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -26,6 +26,29 @@ struct AppInfo { char unknown2[0x3c]; }; +static void WriteParamSfo(const std::filesystem::path& path) { + const char title[] = "Example"; + const char titleId[] = "CUSA00000"; + const char keys[16] = "TITLE\0TITLE_ID"; + unsigned char bytes[86] = {0, 'P', 'S', 'F', 1, 1, 0, 0, 52, 0, 0, 0, 68, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 4, 2, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 4, 2, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0}; + std::memcpy(bytes + 52, keys, sizeof(keys)); + std::memcpy(bytes + 68, title, sizeof(title)); + std::memcpy(bytes + 76, titleId, sizeof(titleId)); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes), sizeof(bytes)); +} + +static int RunSelf(const char* program, std::string_view mode, const std::filesystem::path& directory) { + const std::string arguments = "\"" + std::string(program) + "\" " + std::string(mode) + " \"" + directory.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + return std::system(command.c_str()); +} + static std::filesystem::path EnterTemporaryRoot(std::string_view name) { const auto root = std::filesystem::temp_directory_path() / (std::string(name) + std::to_string(getpid_nid_postfix())); std::filesystem::remove_all(root); @@ -40,13 +63,21 @@ int main(int argc, char** argv) { AppInfo info{}; try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + return std::string_view(error.what()) == "param.json or param.sfo not found" ? 0 : 1; } return 1; } + if (argc == 3 && std::string_view(argv[1]) == "--with-param-sfo") { + std::filesystem::current_path(argv[2]); + AppInfo info{}; + Require(sceKernelGetAppInfo(getpid_nid_postfix(), &info) == 0); + Require(std::strcmp(info.titleId, "CUSA00000") == 0); + return 0; + } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); std::filesystem::create_directories(root / "app0" / "sce_sys"); + WriteParamSfo(root / "app0" / "sce_sys" / "param.sfo"); std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-PPSA97358_00-XPSEMU0000000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"XPSemu\"}},\n \"titleId\": \"PPSA97358\"\n}\n"; AppInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -63,14 +94,14 @@ int main(int argc, char** argv) { const auto empty = std::filesystem::temp_directory_path() / ("guest_app_info_empty_" + std::to_string(self)); std::filesystem::remove_all(empty); std::filesystem::create_directories(empty); - const std::string arguments = "\"" + std::string(argv[0]) + "\" --without-param-json \"" + empty.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - Require(std::system(command.c_str()) == 0); + Require(RunSelf(argv[0], "--without-param-json", empty) == 0); std::filesystem::remove_all(empty); + const auto sfo = std::filesystem::temp_directory_path() / ("guest_app_info_sfo_" + std::to_string(self)); + std::filesystem::remove_all(sfo); + std::filesystem::create_directories(sfo / "app0" / "sce_sys"); + WriteParamSfo(sfo / "app0" / "sce_sys" / "param.sfo"); + Require(RunSelf(argv[0], "--with-param-sfo", sfo) == 0); + std::filesystem::remove_all(sfo); Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); std::uint32_t first[4]{}; diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp new file mode 100644 index 0000000000..6a9a907466 --- /dev/null +++ b/core/libs/tests/GuestArenaHost.cpp @@ -0,0 +1,122 @@ +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if !defined(__APPLE__) +#error These tests cover the macOS guest arena reservation +#endif + +using namespace GuestArena; + +namespace { + +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uint32_t ProtectionNoAccess = 0x01; +constexpr std::uint32_t ProtectionReadOnly = 0x02; +constexpr std::uint32_t ProtectionReadWrite = 0x04; + +struct HostRegion { + std::uintptr_t start; + std::uintptr_t end; + vm_prot_t maxProtection; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RegionsInArenaRange() { + std::vector regions; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object) != KERN_SUCCESS) break; + if (address >= ApplicationAreaEnd) break; + regions.push_back({static_cast(address), static_cast(address + size), info.max_protection}); + address += size; + } + return regions; +} + +void CheckAvailable() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + std::uintptr_t base = 0; + std::size_t bytes = 0; + GuestArenaRange_nid_postfix(&base, &bytes); + Require(base == ArenaStart, "the guest arena does not start at the PS5 application map base"); + Require(base + bytes == ApplicationAreaEnd, "the guest arena does not end at the PS5 application map limit"); +} + +void CheckSkipsUnmappable() { + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection != VM_PROT_NONE || region.end - region.start < 0x10000) continue; + void* placed = GuestArenaAllocateAtOrAbove_nid_postfix(region.start, 0x10000, 0x10000); + const auto address = reinterpret_cast(placed); + Require(address >= region.end, "an allocation landed inside a range the kernel never lets the process map"); + GuestArenaRelease_nid_postfix(placed, 0x10000); + return; + } + std::puts("no unmappable range inside the arena on this host; nothing to skip"); +} + +void CheckHostRegionsKept() { + std::size_t hostRegions = 0; + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection == VM_PROT_ALL) continue; + ++hostRegions; + bool rejected = false; + try { + GuestArenaMarkUsed_nid_postfix(reinterpret_cast(region.start), region.end - region.start); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "a fixed guest mapping over a host region was accepted"); + } + std::printf("%zu host regions inside the arena range are protected\n", hostRegions); +} + +void CheckCommitAndProtect() { + constexpr std::size_t bytes = 0x10000; + auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); + GuestArenaCommit_nid_postfix(pointer, bytes, ProtectionReadWrite, bytes); + pointer[0] = 0x5a; + pointer[bytes - 1] = 0xa5; + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0xa5, "committed guest memory does not keep writes"); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionReadOnly); + Require(pointer[0] == 0x5a, "read-only guest memory lost its contents"); + GuestArenaReset_nid_postfix(pointer, bytes); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionNoAccess); + GuestArenaRelease_nid_postfix(pointer, bytes); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected available, unmappable, host 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 == "commit") CheckCommitAndProtect(); + else throw std::runtime_error("unknown guest arena test"); + std::printf("guest arena %s passed\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/GuestClosedStandardDescriptors.cpp b/core/libs/tests/GuestClosedStandardDescriptors.cpp new file mode 100644 index 0000000000..4cb0deece1 --- /dev/null +++ b/core/libs/tests/GuestClosedStandardDescriptors.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int GuestGetFlags = 3; + +int Child(const char* closed) { + struct stat null{}; + if (::stat("/dev/null", &null) != 0) return 70; + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0) return 71 + descriptor; + if (closed[descriptor] != '1') continue; + if (!S_ISCHR(status.st_mode) || status.st_rdev != null.st_rdev) return 74 + descriptor; + if ((::fcntl(descriptor, F_GETFL) & O_ACCMODE) != (descriptor == 0 ? O_RDONLY : O_WRONLY)) return 77 + descriptor; + } + if (closed[0] == '1' && fcntl_nid_postfix(0, GuestGetFlags) < 0) return 80; + return 0; +} + +int RunClosed(const char* self, bool closeInput, bool closeOutput, bool closeError) { + const pid_t child = ::fork(); + if (child < 0) return -1; + if (child == 0) { + if (closeInput) ::close(0); + if (closeOutput) ::close(1); + if (closeError) ::close(2); + char closed[] = {closeInput ? '1' : '0', closeOutput ? '1' : '0', closeError ? '1' : '0', '\0'}; + char* const arguments[] = {const_cast(self), const_cast("--closed-child"), closed, nullptr}; + ::execv(self, arguments); + ::_exit(69); + } + int status = 0; + if (::waitpid(child, &status, 0) != child) return -1; + return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); +} + +} + +int main(int argc, char** argv) { + if (argc > 2 && std::strcmp(argv[1], "--closed-child") == 0 && std::strlen(argv[2]) == 3) return Child(argv[2]); + const struct { bool input, output, error; const char* name; } cases[] = { + {true, false, false, "stdin"}, + {false, true, false, "stdout"}, + {true, true, true, "all three"}, + }; + for (const auto& test : cases) { + const int result = RunClosed(argv[0], test.input, test.output, test.error); + if (result != 0) { + std::fprintf(stderr, "closed standard descriptors, %s: child exited with %d\n", test.name, result); + return 1; + } + } + std::puts("closed standard descriptors are reopened on /dev/null"); + return 0; +} diff --git a/core/libs/tests/GuestCpuTimeClocks.cpp b/core/libs/tests/GuestCpuTimeClocks.cpp index 439a643457..cb7405736e 100644 --- a/core/libs/tests/GuestCpuTimeClocks.cpp +++ b/core/libs/tests/GuestCpuTimeClocks.cpp @@ -8,11 +8,20 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +#ifdef _WIN32 +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); } +#ifdef _WIN32 +static constexpr std::uint32_t WINDOWS_ERROR_NOACCESS = 998; +static constexpr std::uint32_t WINDOWS_ERROR_ACCESS_DENIED = 5; +static constexpr std::uint32_t WINDOWS_ERROR_INVALID_HANDLE = 6; +#endif + static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); @@ -98,6 +107,11 @@ static void ProcessClockCountsOtherThreads() { } int main() { +#ifdef _WIN32 + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_NOACCESS) == 14); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_ACCESS_DENIED) == 1); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_INVALID_HANDLE) == 5); +#endif KernelTimespec time{-1, -1}; Require(sceKernelClockGettime(GUEST_CLOCK_THREAD_CPUTIME_ID, &time) == SCE_OK); Require(time.tv_sec >= 0 && time.tv_nsec >= 0 && time.tv_nsec < NANOS_PER_SECOND); diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index a3ebfccef8..1c3abe8c03 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,20 +1,46 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include #include #include +#include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } +static void WriteParamSfo(const std::filesystem::path& path) { + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 68, 104, 3}; + const Entry entries[3] = {{0, 0x0404, 4, 4, 0}, {19, 0x0204, 8, 8, 4}, {25, 0x0204, 10, 10, 12}}; + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(header), sizeof(header)); + file.write(reinterpret_cast(entries), sizeof(entries)); + file << std::string("DOWNLOAD_DATA_SIZE\0TITLE\0TITLE_ID\0\0\0", 36) << std::string("\0\1\0\0Example\0CUSA00000\0", 22); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; - Require(std::filesystem::is_directory("download0") == (mode == "declared")); + Require(std::filesystem::is_directory("download0") == (mode == "declared" || mode == "sfo-child")); const bool data = std::filesystem::is_directory("data"); std::filesystem::remove_all("data"); - Require(data == (mode != "missing")); + Require(data == (mode != "missing" && mode != "sfo")); if (mode == "missing") return 0; + if (mode == "sfo") { + std::filesystem::create_directories("app0/sce_sys"); + WriteParamSfo("app0/sce_sys/param.sfo"); +#ifdef _WIN32 + const std::string command = "\"\"" + std::string(argv[0]) + "\" sfo-child\""; +#else + const std::string command = "\"" + std::string(argv[0]) + "\" sfo-child"; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all("app0"); + Require(status == 0); + return 0; + } if (mode == "invalid") { bool rejected = false; try { @@ -25,6 +51,6 @@ int main(int argc, char** argv) { Require(rejected); return 0; } - Require(std::strcmp(GetAppTitleId_nid_postfix().value, "PPSA00000") == 0); + Require(std::strcmp(GetAppTitleId_nid_postfix().value, mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); std::filesystem::remove_all("download0"); } diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 13c71a958d..b1e91115f8 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -19,6 +19,7 @@ int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); @@ -37,11 +38,12 @@ static bool Same(const KernelTimespec& actual, const KernelTimeval& expected) { } static void CheckTimes(const std::string& path, int fd, const KernelTimeval* expected) { - FileStat byPath{}, byFd{}, byKernel{}; + FileStat byPath{}, byFd{}, byKernel{}, byAlias{}; Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat"); Require(fstat_nid_disambig1_nid_postfix(fd, &byFd) == 0, "fstat"); Require(sceKernelFstat(fd, &byKernel) == 0, "sceKernelFstat"); - for (const auto* status : {&byPath, &byFd, &byKernel}) { + Require(_fstat_nid_postfix(fd, &byAlias) == 0, "_fstat"); + for (const auto* status : {&byPath, &byFd, &byKernel, &byAlias}) { Require(Same(status->st_atim, expected[0]), "access time preserves microseconds"); Require(Same(status->st_mtim, expected[1]), "modification time preserves microseconds"); } diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index f864d0e069..e9e7b46880 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -41,6 +41,8 @@ int APS5_VABI sceKernelLseek(int, std::int64_t, int); std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); @@ -200,6 +202,8 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 7); Require(readv_nid_postfix(file, reads, 2) == 3); Require(readv_nid_postfix(file, reads, 2) == 0); + Require(sceKernelLseek(file, 3, 0) == 3 && _readv_nid_postfix(file, reads, 2) == 7); + Require(std::memcmp(first, "345", 3) == 0 && std::memcmp(second, "6789", 4) == 0); Require(readv_nid_postfix(file, reads, -1) == -1 && *__error_nid_postfix() == 22); Require(writev_nid_postfix(file, nullptr, 1) == -1 && *__error_nid_postfix() == 14); @@ -224,6 +228,9 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelLseek(file, 8, 0) == 8 && _writev_nid_postfix(file, swapped, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); + Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); GuestIovec gapped[3] = {{first, 2}, {nullptr, 0}, {second, 3}}; Require(sceKernelPreadv(file, gapped, 3, 0) == 5); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 238ae84299..a28a0ef2fc 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +115,40 @@ static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags return false; } +static std::string CaptureStderr(const std::function& action) { + std::FILE* capture = std::tmpfile(); + Require(capture != nullptr); + std::fflush(stderr); +#ifdef _WIN32 + const int stream = ::_fileno(stderr); + const int saved = ::_dup(stream); + Require(saved >= 0 && ::_dup2(::_fileno(capture), stream) == 0); +#else + const int stream = ::fileno(stderr); + const int saved = ::dup(stream); + Require(saved >= 0 && ::dup2(::fileno(capture), stream) >= 0); +#endif + action(); + std::fflush(stderr); +#ifdef _WIN32 + Require(::_dup2(saved, stream) == 0); + ::_close(saved); +#else + Require(::dup2(saved, stream) >= 0); + ::close(saved); +#endif + Require(std::fseek(capture, 0, SEEK_SET) == 0); + std::string output; + char buffer[512]; + for (;;) { + const auto bytes = std::fread(buffer, 1, sizeof(buffer), capture); + output.append(buffer, bytes); + if (bytes != sizeof(buffer)) break; + } + std::fclose(capture); + return output; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -219,6 +254,13 @@ static void TestRelinkedModuleNames() { Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); + const auto missing = (root / "missing-content.pak").string(); + int openResult = 0; + const auto openLog = CaptureStderr([&] { openResult = sceKernelOpen(missing.c_str(), 0, 0); }); + Require(openResult == static_cast(0x80020002u)); + Require(openLog.find("path=" + missing) != std::string::npos); + Require(openLog.find("resolvedPath=") != std::string::npos && openLog.find("missing-content.pak") != std::string::npos); + Require(openLog.find("result=0x80020002") != std::string::npos); { std::ofstream file(relinked); file << "relinked"; } Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index e82703be18..7258ae6694 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -354,6 +354,34 @@ static void LimitsCase() { while (accepted < 0x3ff && (result = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted; Require(result == NoNodes && accepted == 33); Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 10, "recycle") == 0); + for (int cycle = 0; cycle < 5; ++cycle) { + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + } + int finalAccepted = 0; + int finalResult = 0; + while (finalAccepted < 0x3ff && (finalResult = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++finalAccepted; + Require(finalResult == NoNodes && finalAccepted == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + for (int round = 0; round < 5; ++round) { + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 2, "recycle_round") == 0); + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + int acceptedInRound = 0; + int resultInRound = 0; + while (acceptedInRound < 0x3ff && (resultInRound = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++acceptedInRound; + Require(resultInRound == NoNodes && acceptedInRound == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + } Require(Shutdown(manager.storage) == 0); } @@ -418,6 +446,11 @@ static void ShutdownCase() { Require(Shutdown(manager.storage) == NotInitialized); Require(Factories(manager.storage, 0) == nullptr); } + { + Block scopedBlock(SizeFor(2, 2, 8, 2, 2)); + ManagerObject scoped; + Require(Initialize(scoped.storage, scopedBlock.memory, scopedBlock.bytes, 2, 2, nullptr) == 0); + } } int main(int argc, char** argv) { diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 912ce45b64..6d555d64eb 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -11,9 +11,11 @@ #include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI __sys_socketex_nid_postfix(const char*, int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI _fcntl_nid_postfix(int, int, ...); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); @@ -110,6 +112,8 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); + const int named = __sys_socketex_nid_postfix("probe", 2, 1, 0); + Require(named >= 0 && sceKernelFcntl(named, GetFlags) == ReadWrite && close_nid_postfix(named) == 0); } static void FileFlagsRoundTrip() { @@ -166,6 +170,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); GuestLock probe{0, 0, 0, 1, 0, 0}; Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + static GuestLock imageProbe{0, 0, 0, 1, 0, 0}; + Require(_fcntl_nid_postfix(file, GetLock, &imageProbe) == 0 && imageProbe.type == 2); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0 && _fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 0) == 0); lock.type = 2; Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); lock.type = 7; diff --git a/core/libs/tests/GuestLibcCndInitDefaultName.cpp b/core/libs/tests/GuestLibcCndInitDefaultName.cpp new file mode 100644 index 0000000000..6d5d046398 --- /dev/null +++ b/core/libs/tests/GuestLibcCndInitDefaultName.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char* name); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; + +int main() { + void* condition = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&condition, "GameCond") == success); + Require(condition != nullptr); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); + Require(condition == nullptr); + + void* unnamed = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&unnamed, nullptr) == success); + Require(unnamed != nullptr); + _Cnd_destroy_nid_postfix(&unnamed); + + RequireThrows([] { + _Cnd_init_with_default_name_override_nid_postfix(nullptr, "NullHandle"); + }); + return 0; +} diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index d095eda0ff..02955cd316 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -69,6 +70,8 @@ double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); +double APS5_VABI fdim_nid_postfix(double, double); +float APS5_VABI fdimf_nid_postfix(float, float); float APS5_VABI tanf_nid_postfix(float); int APS5_VABI rand_nid_postfix(); void APS5_VABI srand_nid_postfix(unsigned int); @@ -83,6 +86,8 @@ float APS5_VABI erff_nid_postfix(float); double APS5_VABI lgamma_nid_postfix(double); extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); +int APS5_VABI ilogb_nid_postfix(double); +int APS5_VABI ilogbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); double APS5_VABI __powidf2_nid_postfix(double, int); @@ -210,6 +215,13 @@ static void CheckExactOperations() { Require(fma_nid_postfix(tenth, 10., -1.) == 0x1p-54); Require(fma_nid_postfix(0x1p1023, 2., -0x1p1023) == 0x1p1023); Require(std::isnan(fma_nid_postfix(INFINITY, 0., 1.))); + Require(fdim_nid_postfix(4.0, 1.0) == 3.0 && fdim_nid_postfix(1.0, 4.0) == 0.0); + Require(fdim_nid_postfix(4.0, 4.0) == 0.0 && !std::signbit(fdim_nid_postfix(1.0, 4.0))); + Require(std::isnan(fdim_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0))); + Require(std::isnan(fdim_nid_postfix(1.0, std::numeric_limits::quiet_NaN()))); + Require(fdimf_nid_postfix(4.0f, 1.0f) == 3.0f && fdimf_nid_postfix(1.0f, 4.0f) == 0.0f); + Require(fdimf_nid_postfix(4.0f, 4.0f) == 0.0f && !std::signbit(fdimf_nid_postfix(1.0f, 4.0f))); + Require(std::isnan(fdimf_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0f))); } static void CheckNearbyint() { @@ -1053,6 +1065,18 @@ int main() { Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); Require(logbf_nid_postfix(-std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(logbf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(ilogb_nid_postfix(8.0) == 3 && ilogb_nid_postfix(-0.75) == -1 && ilogb_nid_postfix(1.0) == 0); + Require(ilogb_nid_postfix(1024.0) == 10 && ilogb_nid_postfix(0.125) == -3); + Require(ilogbf_nid_postfix(8.f) == 3 && ilogbf_nid_postfix(-0.75f) == -1 && ilogbf_nid_postfix(1.f) == 0); + Require(ilogbf_nid_postfix(1024.f) == 10 && ilogbf_nid_postfix(0.125f) == -3); + Require(ilogb_nid_postfix(0.0) == INT_MIN && ilogb_nid_postfix(-0.0) == INT_MIN); + Require(ilogb_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(-std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(0.f) == INT_MIN && ilogbf_nid_postfix(-0.f) == INT_MIN); + Require(ilogbf_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::denorm_min()) == -149); Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5730ecd1a9..0e5a4f6d26 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -19,9 +19,15 @@ #include extern "C" { +int APS5_VABI sceNetCtlCheckCallback(void); int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +void APS5_VABI sceNetCtlTerm(void); +int APS5_VABI sceNetCtlUnregisterCallback(int); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); } namespace { @@ -32,9 +38,100 @@ constexpr int kErrInvalidCode = static_cast(0x80412106); constexpr int kErrInvalidAddr = static_cast(0x80412107); constexpr int kErrNotConnected = static_cast(0x80412108); constexpr int kStateIpObtained = 3; +constexpr int kEventDisconnected = 1; +constexpr int kEventIpObtained = 3; constexpr std::uint8_t kFill = 0xa5; constexpr std::size_t kAddressLength = 16; +struct Delivery { + int calls = 0; + int event = 0; + void* arg = nullptr; +}; + +Delivery g_first; +Delivery g_second; +Delivery g_v6; +Delivery g_self; +int g_selfUnregister = -1; + +void APS5_VABI RecordFirst(int event, void* arg) { + ++g_first.calls; + g_first.event = event; + g_first.arg = arg; +} + +void APS5_VABI RecordSecond(int event, void* arg) { + ++g_second.calls; + g_second.event = event; + g_second.arg = arg; +} + +void APS5_VABI RecordV6(int event, void* arg) { + ++g_v6.calls; + g_v6.event = event; + g_v6.arg = arg; +} + +void APS5_VABI UnregisterSelf(int event, void* arg) { + ++g_self.calls; + g_self.event = event; + g_self.arg = arg; + g_selfUnregister = sceNetCtlUnregisterCallback(*static_cast(arg)); +} + +void CheckCallbacks() { + int state = -1; + Require(sceNetCtlGetState(&state) == 0); + const int event = state == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int firstProbe = 0; + int secondProbe = 0; + int v6Probe = 0; + + int v6 = -1; + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + + int first = -1; + Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + + int second = -1; + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + Require(g_second.calls == 1 && g_second.event == event && g_second.arg == &secondProbe); + + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + int self = -1; + Require(sceNetCtlRegisterCallback(UnregisterSelf, &self, &self) == 0 && self == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1 && g_self.event == event && g_self.arg == &self && g_selfUnregister == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1); + + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + sceNetCtlTerm(); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); + Require(g_second.calls == 1 && g_self.calls == 1 && g_v6.calls == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); +} + NetCtlInfo Query(int code) { NetCtlInfo info; std::memset(&info, kFill, sizeof(info)); @@ -90,6 +187,8 @@ int main() { WSADATA data{}; Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); #endif + CheckCallbacks(); + NetCtlInfo info{}; Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index a89ed3ea2e..4fe07d41f6 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -22,10 +22,22 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } +extern "C" { +int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallback(void); +int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId); +int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); +} + namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int AlreadyRegistered = static_cast(0x80550008u); +constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); void Require(bool condition, const char* message) { @@ -35,6 +47,133 @@ void Require(bool condition, const char* message) { } } +struct StateCall { + int calls; + int userId; + std::uint32_t state; + NpId* npId; + void* userdata; +}; + +constexpr int InitialUserId = 0x10000000; +constexpr std::uint32_t StateSignedOut = 1; +StateCall stateCallsA[9]; +StateCall legacyStateCall; + +template +void APS5_VABI StateCallbackA(int userId, std::uint32_t state, void* userdata) { + stateCallsA[Index] = {stateCallsA[Index].calls + 1, userId, state, nullptr, userdata}; +} + +void APS5_VABI LegacyStateCallback(int userId, std::uint32_t state, NpId* npId, void* userdata) { + legacyStateCall = {legacyStateCall.calls + 1, userId, state, npId, userdata}; +} + +bool SignedOutOnce(const StateCall& call, void* userdata) { + return call.calls == 1 && call.userId == InitialUserId && call.state == StateSignedOut && + call.npId == nullptr && call.userdata == userdata; +} + +void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) { + std::fprintf(stderr, "NpManager: the reachability callback was called\n"); + std::abort(); +} + +struct SelfUnregister { + int id; + int calls; + int result; +}; + +void APS5_VABI SelfUnregisteringCallback(int, std::uint32_t, void* userdata) { + auto* context = static_cast(userdata); + ++context->calls; + context->result = sceNpUnregisterStateCallbackA(context->id); +} + +void CheckStateCallbacksA() { + constexpr int CallbackMax = static_cast(0x8055001Du); + void* const callbacks[] = { + reinterpret_cast(&StateCallbackA<0>), reinterpret_cast(&StateCallbackA<1>), + reinterpret_cast(&StateCallbackA<2>), reinterpret_cast(&StateCallbackA<3>), + reinterpret_cast(&StateCallbackA<4>), reinterpret_cast(&StateCallbackA<5>), + reinterpret_cast(&StateCallbackA<6>), reinterpret_cast(&StateCallbackA<7>), + reinterpret_cast(&StateCallbackA<8>), + }; + int userdata[9]{}; + Require(sceNpRegisterStateCallbackA(nullptr, &userdata[0]) == InvalidArgument, "null state callback"); + int ids[8]{}; + ids[0] = sceNpRegisterStateCallbackA(callbacks[0], &userdata[0]); + Require(ids[0] >= 1 && ids[0] <= 8, "state callback id outside 1..8"); + Require(sceNpRegisterStateCallbackA(callbacks[0], &userdata[1]) == AlreadyRegistered, "duplicate state callback"); + Require(stateCallsA[0].calls == 0, "registering calls the state callback"); + Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast(&AbortingReachabilityCallback), nullptr) == 0, + "registering the reachability callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed"); + Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once"); + Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed"); + Require(stateCallsA[0].calls == 1, "the second sceNpCheckCallback delivered the state again"); + for (int index = 1; index < 8; ++index) { + ids[index] = sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]); + Require(ids[index] >= 1 && ids[index] <= 8, "state callback id outside 1..8"); + for (int other = 0; other < index; ++other) { + Require(ids[index] != ids[other], "two state callbacks got the same id"); + } + } + Require(sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]) == CallbackMax, "ninth state callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with eight callbacks"); + for (int index = 0; index < 8; ++index) { + Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); + } + Require(stateCallsA[8].calls == 0, "a rejected callback was called"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); + for (int index = 0; index < 8; ++index) { + if (index == 3) continue; + Require(sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]) == AlreadyRegistered, + "unregistering one id removed another callback"); + } + Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); + } + for (int index = 0; index < 8; ++index) { + Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); + } + Require(sceNpUnregisterStateCallbackA(ids[0]) == NotRegistered, "unregistering a state callback twice"); + const int removed = sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]); + Require(removed >= 1 && removed <= 8, "a freed slot is reused"); + Require(sceNpUnregisterStateCallbackA(removed) == 0, "unregistering before sceNpCheckCallback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with no callbacks"); + Require(stateCallsA[8].calls == 0, "an unregistered callback was called"); + SelfUnregister self{}; + self.id = sceNpRegisterStateCallbackA(reinterpret_cast(&SelfUnregisteringCallback), &self); + Require(self.id >= 1 && self.id <= 8, "state callback id outside 1..8"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with a callback that unregisters itself"); + Require(self.calls == 1 && self.result == 0, "a state callback must be able to unregister itself"); + Require(sceNpUnregisterStateCallbackA(self.id) == NotRegistered, "the callback did not unregister itself"); +} + +void CheckLegacyStateCallback() { + int userdata = 0; + NpId sentinel{}; + auto* callback = reinterpret_cast(&LegacyStateCallback); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering with no legacy callback"); + Require(sceNpRegisterStateCallback(nullptr, &userdata) == InvalidArgument, "null legacy callback"); + Require(sceNpRegisterStateCallback(callback, &userdata) == 0, "registering the legacy callback"); + Require(sceNpRegisterStateCallback(callback, nullptr) == AlreadyRegistered, "duplicate legacy callback"); + legacyStateCall.npId = &sentinel; + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with the legacy callback"); + Require(SignedOutOnce(legacyStateCall, &userdata), "the legacy callback must get SIGNED_OUT with no NpId"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed with the legacy callback"); + Require(legacyStateCall.calls == 1, "the legacy callback got the state again"); + Require(sceNpUnregisterStateCallback() == 0, "unregistering the legacy callback"); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering the legacy callback twice"); +} + void CheckRequestTimeouts() { Require(sceNpSetTimeout(0x7fffffff, 2, 0, 0, 0, 0) == RequestNotFound, "never created request"); Require(sceNpSetTimeout(0x7fffffff, 0, 0, 0, 0, 0) == InvalidArgument, "arguments checked before the request"); @@ -64,6 +203,8 @@ void CheckRequestTimeouts() { } int main() { + CheckStateCallbacksA(); + CheckLegacyStateCallback(); CheckRequestTimeouts(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/core/libs/tests/GuestPathDriveComponent.cpp b/core/libs/tests/GuestPathDriveComponent.cpp new file mode 100644 index 0000000000..c17bafcbfc --- /dev/null +++ b/core/libs/tests/GuestPathDriveComponent.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string Under(const std::filesystem::path& directory, const std::string& names) { + auto text = directory.string(); + for (std::size_t start = 0; start <= names.size();) { + const auto end = std::min(names.find('/', start), names.size()); + text += static_cast(std::filesystem::path::preferred_separator); + text += names.substr(start, end - start); + start = end + 1; + } + return text; +} + +int main() { + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "AnyPS5-Drive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + const auto mounted = directory / "Mounted"; + Require(std::filesystem::create_directories(mounted / "Nested")); + { std::ofstream file(directory / "Secret.txt"); file << 'x'; } + auto drive = host.root_name().string(); + if (drive.empty()) drive = "C:"; + std::string outside; + for (const auto& part : directory.relative_path()) outside += "/" + part.string(); + AddPathAlias_nid_no_patch("/drive-mount", mounted.string().c_str()); + + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + outside + "/Secret.txt").c_str()).string() == + Under(mounted, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + "Nested").c_str()).string() == Under(mounted, drive + "Nested")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive).c_str()).string() == Under(mounted, drive)); + Require(ResolvePath_nid_no_patch(("/drive-mount/Nested/" + drive + "/x").c_str()).string() == Under(mounted, "Nested/" + drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/../" + drive + outside + "/Secret.txt").c_str()).string() == + Under(host, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive + "/" + name + "/Secret.txt").c_str()).string() == + Under(host, drive + "/" + name + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive).c_str()).string() == Under(host, drive)); + Require(ResolvePath_nid_no_patch(("/" + name + "/" + drive + "/x").c_str()).string() == Under(directory, drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/Secret.txt").c_str()) == directory / "Secret.txt"); + Require(ResolvePath_nid_no_patch("/drive-mount/Nested").string() == Under(mounted, "Nested")); + + RemovePathAlias_nid_no_patch("/drive-mount"); + std::filesystem::remove_all(directory); +} diff --git a/core/libs/tests/GuestPthreadCancelRecovery.cpp b/core/libs/tests/GuestPthreadCancelRecovery.cpp index 59e6056c85..297f761a92 100644 --- a/core/libs/tests/GuestPthreadCancelRecovery.cpp +++ b/core/libs/tests/GuestPthreadCancelRecovery.cpp @@ -1,10 +1,21 @@ #include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include +#include #include #include #include #include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -17,6 +28,9 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); @@ -24,6 +38,10 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* time); int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name); int APS5_VABI scePthreadSemDestroy(PthreadSem* sem); int APS5_VABI scePthreadSemWait(PthreadSem* sem); @@ -226,6 +244,166 @@ static void ConditionRecovery() { Require(scePthreadMutexDestroy(&mutex) == 0); } +enum class ConditionWaitMode { SceTimed, Sce, PosixTimed, Posix }; + +struct SignaledCondition { + PthreadMutex mutex = nullptr; + PthreadCond condition = nullptr; + PthreadCond* slot = &condition; + ConditionWaitMode mode = ConditionWaitMode::SceTimed; + int lockCount = 1; + bool cancellationDisabled = false; + bool released = false; + std::atomic ready{0}; + std::array results{-1, -1}; + std::array cleaned{}; +}; + +struct SignaledConditionWorker { + SignaledCondition* context; + int index; +}; + +static void APS5_VABI UnlockSignaledCondition(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexUnlock(&context.mutex) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == static_cast(0x80020001)); + context.cleaned[worker.index] = true; +} + +static void* APS5_VABI SignaledConditionWaiter(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + if (context.cancellationDisabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexLock(&context.mutex) == 0); + std::uintptr_t cleanup[8]; + __pthread_cleanup_push_imp_nid_postfix(UnlockSignaledCondition, &worker, cleanup); + context.ready.fetch_add(1); + int result = 0; + do { + switch (context.mode) { + case ConditionWaitMode::SceTimed: + result = scePthreadCondTimedwait(context.slot, &context.mutex, 2000000); + break; + case ConditionWaitMode::Sce: + result = scePthreadCondWait(context.slot, &context.mutex); + break; + case ConditionWaitMode::PosixTimed: { + KernelTimespec deadline{}; + Require(clock_gettime_nid_postfix(0, &deadline) == 0); + deadline.tv_sec += 2; + result = pthread_cond_timedwait_nid_postfix(context.slot, &context.mutex, &deadline); + break; + } + case ConditionWaitMode::Posix: + result = pthread_cond_wait_nid_postfix(context.slot, &context.mutex); + break; + } + } while (result == 0 && !context.released); + context.results[worker.index] = result; + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static bool ConditionRegistered(Pthread thread) { + const std::lock_guard lock(thread->cancelLock); + return thread->cancelWait != nullptr; +} + +static void InitializeSignaledCondition(SignaledCondition& context, bool recursive) { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == 0); + Require(scePthreadMutexattrSettype(&attr, recursive ? 2 : 1) == 0); + Require(scePthreadMutexInit(&context.mutex, &attr, nullptr) == 0); + Require(scePthreadMutexattrDestroy(&attr) == 0); + context.lockCount = recursive ? 3 : 1; + Require(scePthreadCondInit(context.slot, nullptr, nullptr) == 0); +} + +static void CanceledConditionSignal(ConditionWaitMode mode, bool recursive) { + SignaledCondition context; + context.mode = mode; + InitializeSignaledCondition(context, recursive); + std::array workers{{{&context, 0}, {&context, 1}}}; + std::array threads{}; + for (int index = 0; index < 2; ++index) { + Require(scePthreadCreate(&threads[index], nullptr, SignaledConditionWaiter, &workers[index], nullptr) == 0); + while (context.ready.load() <= index) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!ConditionRegistered(threads[index]) && std::chrono::steady_clock::now() < deadline) { + Require(scePthreadMutexUnlock(&context.mutex) == 0); + std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + } + Require(ConditionRegistered(threads[index])); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + } + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(&context.condition) == 0); + int selected = -1; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (selected < 0 && std::chrono::steady_clock::now() < deadline) { + for (int index = 0; index < 2; ++index) if (!ConditionRegistered(threads[index])) selected = index; + std::this_thread::yield(); + } + Require(selected >= 0); + Require(scePthreadCancel(threads[selected]) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(threads[selected], CANCELED); + Join(threads[1 - selected], nullptr); + Require(context.results[selected] == -1 && context.results[1 - selected] == 0); + Require(context.cleaned[0] && context.cleaned[1]); + Require(scePthreadCondDestroy(&context.condition) == 0); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + +static void DestroyedSignaledCondition(ConditionWaitMode mode, bool recursive, bool cancel, bool cancellationDisabled) { + SignaledCondition context; + context.mode = mode; + context.cancellationDisabled = cancellationDisabled; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto bytes = static_cast(system.dwPageSize); + context.slot = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(context.slot != nullptr); +#else + const auto bytes = static_cast(sysconf(_SC_PAGESIZE)); + context.slot = static_cast(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(context.slot != MAP_FAILED); +#endif + *context.slot = nullptr; + InitializeSignaledCondition(context, recursive); + const std::weak_ptr retained = (*context.slot)->_cv; + SignaledConditionWorker worker{&context, 0}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SignaledConditionWaiter, &worker, nullptr) == 0); + while (context.ready.load() == 0) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(context.slot) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (ConditionRegistered(thread) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(!ConditionRegistered(thread)); + Require(scePthreadCondDestroy(context.slot) == 0); + Require(!retained.expired()); +#ifdef _WIN32 + Require(VirtualFree(context.slot, 0, MEM_RELEASE) != 0); +#else + Require(munmap(context.slot, bytes) == 0); +#endif + if (cancel) Require(scePthreadCancel(thread) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(thread, cancel && !cancellationDisabled ? CANCELED : nullptr); + Require(context.cleaned[0]); + Require(context.results[0] == (cancel && !cancellationDisabled ? -1 : 0)); + Require(retained.expired()); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + static void* APS5_VABI HeldTarget(void*) { CheckDefaults(); targetRelease.wait(false); @@ -274,6 +452,14 @@ int main() { } Require(scePthreadKeyDelete(key) == 0); ConditionRecovery(); + for (const auto mode : {ConditionWaitMode::SceTimed, ConditionWaitMode::Sce, ConditionWaitMode::PosixTimed, ConditionWaitMode::Posix}) { + for (bool recursive : {false, true}) { + CanceledConditionSignal(mode, recursive); + DestroyedSignaledCondition(mode, recursive, true, false); + DestroyedSignaledCondition(mode, recursive, false, false); + DestroyedSignaledCondition(mode, recursive, true, true); + } + } disabled = true; ConditionRecovery(); JoinRecovery(); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e5b47cdeae..464bcd0e32 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -76,6 +76,7 @@ int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); +bool GuestSocketIsOpen_nid_no_patch(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); struct GuestPollEntry { @@ -123,6 +124,8 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} + static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } @@ -855,6 +858,17 @@ static void CheckStreamAddressLengths() { for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelSocketOption() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0); + const int size = 65536; + Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + const int one = 1; + Require(sceNetSetsockopt(kernel, 6, 1, &one, sizeof(one)) == 0); + Require(close_nid_postfix(kernel) == 0); + Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); +} + static void CheckBufferSizes() { const int socket = sceNetSocket("buffer-sizes", 2, 2, 17); Require(socket >= 0); @@ -991,6 +1005,14 @@ static void CheckSocketError() { Require(sceNetSocketClose(refused) == 0); } +static void CheckCloseKernelSocket() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0 && GuestSocketIsOpen_nid_no_patch(kernel)); + Require(sceNetSocketClose(kernel) == 0); + Require(!GuestSocketIsOpen_nid_no_patch(kernel)); + Require(Failed(sceNetSocketClose(kernel), 9)); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1060,6 +1082,7 @@ int main() { CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); + CheckCloseKernelSocket(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); @@ -1106,6 +1129,7 @@ int main() { CheckStreamAddressLengths(); CheckBufferSizes(); CheckOnesBroadcast(); + CheckKernelSocketOption(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); @@ -1406,20 +1430,19 @@ int main() { Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); } { - const auto dummyCallback = [](int, void*) {}; int cid = -1; Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, nullptr) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); int cids[8] = {}; for (int i = 0; i < 8; ++i) { - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); Require(cids[i] == i); } - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); Require(sceNetCtlUnregisterCallbackV6(-1) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(8) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(3) == 0); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == 0); Require(cid == 3); for (int i = 0; i < 8; ++i) { Require(sceNetCtlUnregisterCallbackV6(i) == 0); diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 94bf0656db..92048bd727 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -73,8 +73,22 @@ int main() { Require(events[0].ident == static_cast(listener)); Require(events[0].filter == EVFILT_READ); Require(events[0].udata == &marker); + Require(events[0].data == 1); + const int second = socket_nid_postfix(2, 1, 0); + Require(second >= 0 && connect_nid_postfix(second, address.data(), address.size()) == 0); +#ifdef __linux__ + const auto queued = std::chrono::steady_clock::now() + std::chrono::seconds(1); + do { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(listener)); + } while (events[0].data != 2 && std::chrono::steady_clock::now() < queued); + Require(events[0].data == 2); +#endif const int accepted = accept_nid_postfix(listener, nullptr, nullptr); Require(accepted >= 0); + const int acceptedSecond = accept_nid_postfix(listener, nullptr, nullptr); + Require(acceptedSecond >= 0); + Require(close_nid_postfix(acceptedSecond) == 0 && close_nid_postfix(second) == 0); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_OK); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_KERNEL_ERROR_ENOENT); diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 982eb625d7..df022d1f8a 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -5,12 +5,29 @@ #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; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, flags) == 44); + extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); @@ -36,6 +53,7 @@ int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetSocketClose(int); int* APS5_VABI sceNetErrnoLoc(); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); @@ -61,7 +79,7 @@ static DWORD Protection(const void* address) { int main(int argc, char** argv) { constexpr std::size_t page = 0x4000; constexpr std::size_t capacity = 32; - constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom", "sceNetRecvmsg"}; Require(argc == 2 || argc == 3, "receive API argument required"); Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); const bool stream = argc == 3; @@ -121,7 +139,13 @@ int main(int argc, char** argv) { case 2: return read_nid_postfix(receiver, buffer, bytes); case 3: return sceKernelRead(receiver, buffer, bytes); case 4: return sceNetRecv(receiver, buffer, bytes, flags); - default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + case 5: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + default: { + const auto first = std::min(bytes, 3); + NetIovec vectors[]{{buffer, first}, {nullptr, 0}, {static_cast(buffer) + first, bytes - first}}; + NetMsghdr message{nullptr, 0, vectors, 3, nullptr, 0, 0}; + return sceNetRecvmsg(receiver, &message, flags); + } } }; const auto waitForPacket = [&] { @@ -302,6 +326,42 @@ int main(int argc, char** argv) { collect(); Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, "failed receive releases the host write guard"); + if (api == 6) { + Require(sceKernelMprotect(static_cast(mapping) + page, page, 1) == 0, + "make the later receive vector read-only"); + send(); + waitForPacket(); + Require(receive(buffer, capacity, 0, nullptr, nullptr) == static_cast(0x8041010eu) && *error == 14, + "a later read-only vector fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "a rejected vector list leaves every destination unchanged"); + collect(); + Require(Protection(buffer) == PAGE_READONLY, "a later vector failure releases earlier guards"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected vector list preserves queued data"); + Require(sceKernelMprotect(static_cast(mapping) + page, page, 3) == 0, + "restore the later receive vector"); + NetIovec vectors[]{{buffer, 3}, {nullptr, 0}, {buffer + 3, capacity - 3}}; + auto* message = reinterpret_cast(static_cast(twin) + 32); + const NetMsghdr initial{nullptr, 0, vectors, 3, nullptr, 123, -1}; + { + const GuestArena::HostWrite destination(message, sizeof(*message)); + Require(destination.Open(), "initialize the receive message"); + std::memcpy(message, &initial, sizeof(initial)); + } + Require(sceKernelMprotect(twin, page, 1) == 0, "make the receive message read-only"); + send(); + waitForPacket(); + Require(sceNetRecvmsg(receiver, message, 0) == static_cast(0x8041010eu) && *error == 14, + "read-only message outputs fail with EFAULT"); + Require(message->flags == -1 && message->controlLength == 123, "rejected message outputs unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected message preserves queued data"); + Require(sceKernelMprotect(twin, page, 3) == 0, "restore receive message write permission"); + reset(); + collect(); + } Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); send(); waitForPacket(); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index dca63ca158..0ba21b86b1 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -566,11 +566,25 @@ static int CheckClosedStandardOutput() { Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); + Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); + Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); + return 0; +} + +static int CheckExclusiveStandardOutput() { + const auto filename = "anyps5-exclusive-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(filename.c_str(), "wx", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(fputs_nid_postfix("exclusive\n", __stdoutp_nid_postfix) >= 0 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(CheckFileBytes(filename, "exclusive\n", "wx", true)); + Require(std::filesystem::remove(filename)); return 0; } int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index c94f557187..87e38f6c0a 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -2,6 +2,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -31,6 +37,8 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); } static bool StackContains(Pthread thread, const void* address) { @@ -58,6 +66,72 @@ static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +static constexpr std::uintptr_t GuestPage = 0x4000; +static constexpr int GuestProtNone = 0; +static constexpr int GuestProtReadWrite = 3; + +static void ReportedStack(Pthread thread, std::uintptr_t* begin, std::uintptr_t* end) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrGet(thread, &attr) == SCE_OK); + void* stack = nullptr; + size_t size = 0; + Require(scePthreadAttrGetstackaddr(&attr, &stack) == SCE_OK && scePthreadAttrGetstacksize(&attr, &size) == SCE_OK); + scePthreadAttrDestroy(&attr); + *begin = reinterpret_cast(stack); + *end = *begin + size; +} + +static void RequireGuardable(std::uintptr_t begin, std::uintptr_t end) { + Require(begin % GuestPage == 0 && end - begin >= GuestPage); + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.State == MEM_COMMIT && memory.Protect == PAGE_READWRITE); + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + auto* lowest = reinterpret_cast(begin); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtNone) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); +} + +static void RequireGuardableStack(Pthread thread) { + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(thread, &begin, &end); + RequireGuardable(begin, end); +} + +static DWORD WINAPI DefaultSizeHostThread(void*) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + RequireGuardable(reinterpret_cast(start), reinterpret_cast(end)); + RequireGuardableStack(scePthreadSelf()); + return 0; +} + +static DWORD WINAPI PartlyCommittedHostThread(void*) { + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(scePthreadSelf(), &begin, &end); + Require(begin == low && end == high); + return 0; +} + +static void RunHostThread(LPTHREAD_START_ROUTINE routine, SIZE_T committed) { + const HANDLE thread = CreateThread(nullptr, committed, routine, nullptr, 0, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + Require(CloseHandle(thread) != 0); +} +#endif + static std::int64_t ThreadIdField(Pthread thread) { std::int64_t tid = 0; std::memcpy(&tid, static_cast(thread), sizeof(tid)); @@ -110,6 +184,11 @@ int main() { Require(pthread_getthreadid_np_nid_postfix() == static_cast(mainTid)); int local = 0; Require(StackContains(mainSelf, &local)); +#ifdef _WIN32 + RequireGuardableStack(mainSelf); + RunHostThread(DefaultSizeHostThread, 0); + RunHostThread(PartlyCommittedHostThread, 0x10000); +#endif Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EDEADLK); Require(pthread_join_nid_postfix(mainSelf, nullptr) == GUEST_EDEADLK); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); diff --git a/core/libs/tests/GuestVideoOutCursor.cpp b/core/libs/tests/GuestVideoOutCursor.cpp new file mode 100644 index 0000000000..214293bdd2 --- /dev/null +++ b/core/libs/tests/GuestVideoOutCursor.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutCursorEnable(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorDisable(std::int32_t handle, std::int32_t index); +int APS5_VABI sceVideoOutCursorSetImageAddress(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorSetPosition(std::int32_t handle, std::int32_t index, std::uint32_t x, std::uint32_t y); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + static const std::uint32_t image[64 * 64] = {}; + bool passed = ReportsNotImplemented("sceVideoOutCursorEnable", [] { return sceVideoOutCursorEnable(1, 0, image); }); + passed = ReportsNotImplemented("sceVideoOutCursorSetImageAddress", [] { return sceVideoOutCursorSetImageAddress(1, 0, image); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorSetPosition", [] { return sceVideoOutCursorSetPosition(1, 0, 960, 540); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorDisable", [] { return sceVideoOutCursorDisable(1, 0); }) && passed; + return passed ? 0 : 1; +} diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index f372fd47e1..818f401ffd 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -114,10 +114,13 @@ static void CheckPositional() { static void CheckPointer() { char16_t buffer[64]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - buffer == std::u16string(u"0x0|0x1234|0xdeadbeef12"), "vswprintf %p uses the 0x form"); - Require(Format(buffer, 64, u"[%8p][%-8p][%08p][%.6p]", address(0x1a), address(0x1a), address(0x1a), address(0x1a)) == 40 && - buffer == std::u16string(u"[ 0x1a][0x1a ][0x00001a][0x00001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + buffer == std::u16string(u"0|1234|deadbeef12"), "vswprintf %p uses the console form"); + Require(Format(buffer, 64, u"[%10p][%-10p][%.8p][%08p]", address(0x1234), address(0x1234), address(0x1234), address(0x1a)) == 44 && + buffer == std::u16string(u"[ 1234][1234 ][00001234][0000001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"[%.0p][%p]", address(0), address(0)) == 5 && + buffer == std::u16string(u"[][0]"), "vswprintf %p with a zero precision prints nothing"); + Require(Format(buffer, 64, u"[%lp]", address(0x1234)) == -1, "vswprintf %lp is refused"); } static void CheckCount() { diff --git a/core/libs/tests/ParamSfoParser.cpp b/core/libs/tests/ParamSfoParser.cpp new file mode 100644 index 0000000000..d4f9ff1c12 --- /dev/null +++ b/core/libs/tests/ParamSfoParser.cpp @@ -0,0 +1,144 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Entry { + std::string key; + std::uint16_t format; + std::vector data; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Entry Text(const std::string& key, const std::string& value) { + std::vector data(value.begin(), value.end()); + data.push_back(0); + return {key, 0x0204, data}; +} + +Entry Integer(const std::string& key, std::int32_t value) { + std::vector data(4); + std::memcpy(data.data(), &value, 4); + return {key, 0x0404, data}; +} + +void Put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { + std::memcpy(bytes.data() + offset, &value, 4); +} + +std::vector Build(const std::vector& entries) { + std::vector keys; + std::vector data; + std::vector bytes(20 + entries.size() * 16); + for (std::size_t index = 0; index < entries.size(); ++index) { + const auto entry = 20 + index * 16; + const auto keyOffset = static_cast(keys.size()); + std::memcpy(bytes.data() + entry, &keyOffset, 2); + std::memcpy(bytes.data() + entry + 2, &entries[index].format, 2); + Put32(bytes, entry + 4, static_cast(entries[index].data.size())); + Put32(bytes, entry + 8, static_cast(entries[index].data.size())); + Put32(bytes, entry + 12, static_cast(data.size())); + keys.insert(keys.end(), entries[index].key.begin(), entries[index].key.end()); + keys.push_back(0); + data.insert(data.end(), entries[index].data.begin(), entries[index].data.end()); + } + while (keys.size() % 4 != 0) keys.push_back(0); + std::memcpy(bytes.data(), "\0PSF", 4); + Put32(bytes, 4, 0x0101); + Put32(bytes, 8, static_cast(bytes.size())); + Put32(bytes, 12, static_cast(bytes.size() + keys.size())); + Put32(bytes, 16, static_cast(entries.size())); + bytes.insert(bytes.end(), keys.begin(), keys.end()); + bytes.insert(bytes.end(), data.begin(), data.end()); + return bytes; +} + +ParsedParamJson Parse(const std::filesystem::path& path, const std::vector& bytes) { + { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file), "Cannot write param.sfo fixture"); + } + return parseParamSfo(path); +} + +void Rejects(const std::filesystem::path& path, const std::vector& bytes, const std::string& expected) { + try { + Parse(path, bytes); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error for " + expected + ": " + error.what()); + return; + } + throw std::runtime_error("Accepted invalid param.sfo: " + expected); +} + +std::vector Valid() { + return {Text("CATEGORY", "gd"), Integer("DOWNLOAD_DATA_SIZE", 64), Text("TITLE", "Example"), Text("TITLE_ID", "CUSA00000"), + Integer("USER_DEFINED_PARAM_1", 23566), Integer("USER_DEFINED_PARAM_3", -7), {"PUBTOOLINFO", 0x0004, {1, 2, 3}}}; +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-sfo-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".sfo"); + int result = 0; + try { + const auto parsed = Parse(path, Build(Valid())); + Require(parsed.title == "Example" && parsed.titleId == "CUSA00000", "Incorrect title metadata"); + Require(parsed.downloadDataSizeMiB == 64, "Incorrect download data size"); + Require(parsed.userDefinedParams[0] == 23566 && parsed.userDefinedParams[1] == 0 && parsed.userDefinedParams[2] == -7 && parsed.userDefinedParams[3] == 0, "Incorrect user defined params"); + + auto entries = Valid(); + entries.erase(entries.begin() + 2); + Rejects(path, Build(entries), "missing TITLE"); + entries = Valid(); + entries.erase(entries.begin() + 3); + Rejects(path, Build(entries), "missing TITLE_ID"); + entries = Valid(); + entries[1] = Integer("DOWNLOAD_DATA_SIZE", -1); + Rejects(path, Build(entries), "DOWNLOAD_DATA_SIZE is negative"); + entries = Valid(); + entries[2].data.back() = 'x'; + Rejects(path, Build(entries), "TITLE is not a terminated string"); + entries = Valid(); + entries[4].data.pop_back(); + Rejects(path, Build(entries), "USER_DEFINED_PARAM_1 is not a 32-bit integer"); + entries = Valid(); + entries.push_back(Text("TITLE", "Again")); + Rejects(path, Build(entries), "repeats TITLE"); + + auto bytes = Build(Valid()); + bytes[1] = 'X'; + Rejects(path, bytes, "no PSF header"); + bytes = Build(Valid()); + Put32(bytes, 16, 1000); + Rejects(path, bytes, "tables are out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 2 * 16 + 12, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 4, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + bytes.resize(bytes.size() - 64); + Rejects(path, bytes, "out of bounds"); + Rejects(path, std::vector(8), "no PSF header"); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + result = 1; + } + std::filesystem::remove(path); + if (result == 0) std::cout << "param.sfo parser tests passed\n"; + return result; +} diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index dda7b2fea4..4dae722b17 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -198,7 +198,25 @@ static IrProgram TranslateCode(std::span code, const Swappc return InstructionTranslator{}.Translate(program, cfg, options); } +static void CheckConstantTableCall() { + const std::vector caller{0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, + 0xbf840004u, 0xf4240402u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u}; + const SwappcInfo swappc{false, 0u, 16u}; + const auto calls = FindTableCalls(RdnaInstructionDecoder{}.Decode(caller), swappc); + Require(calls.size() == 1u); + Require(calls[0].constantPointer == std::optional(0xfe0040000ull) && calls[0].descriptorOffset == 0u && calls[0].entryOffset == 0x20u && calls[0].entryStride == 0u); + Require(calls[0].linkRegister == 14u && calls[0].targetRegister == 14u); + auto changedHigh = caller; + changedHigh[2] = 0xbeeb0380u; + const auto zero = FindTableCalls(RdnaInstructionDecoder{}.Decode(changedHigh), swappc); + Require(zero.size() == 1u && zero[0].constantPointer == std::optional(0xe0040000ull)); + auto computed = caller; + computed[2] = 0xbeeb0302u; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(computed), swappc).empty()); +} + static void CheckTableCalls() { + CheckConstantTableCall(); const SwappcInfo swappc{false, 0u, 16u}; const auto caller = TableCaller(); const auto program = RdnaInstructionDecoder{}.Decode(caller); diff --git a/core/libs/tests/SemaphoreTrace.cpp b/core/libs/tests/SemaphoreTrace.cpp new file mode 100644 index 0000000000..e49947b56e --- /dev/null +++ b/core/libs/tests/SemaphoreTrace.cpp @@ -0,0 +1,34 @@ +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include +#include +#include + +extern "C" void KernelTraceWait_nid_postfix(const char*, const void*, std::uint64_t, bool) {} + +int main() { + KernelSema sem = nullptr; + if (sceKernelCreateSema(&sem, "trace", 0, 0, 1, nullptr) != KERNEL_SEMA_OK) return 1; + + std::atomic result{-1}; + std::thread waiter([&] { result.store(sceKernelWaitSema(sem, 1, nullptr)); }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + for (;;) { + { + std::lock_guard lock(sem->mutex); + if (sem->waiterCount != 0) break; + } + if (std::chrono::steady_clock::now() >= deadline) { + sceKernelSignalSema(sem, 1); + waiter.join(); + sceKernelDeleteSema(sem); + return 2; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + const int signaled = sceKernelSignalSema(sem, 1); + waiter.join(); + const int deleted = sceKernelDeleteSema(sem); + return signaled == KERNEL_SEMA_OK && result.load() == KERNEL_SEMA_OK && deleted == KERNEL_SEMA_OK ? 0 : 3; +} diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index 32871b4f63..d16340e2b3 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" #include "../prx/libc/include/FailureReport.hpp" @@ -16,10 +18,26 @@ void DroppedReport() noexcept { std::fputs("dropped failure report ran\n", stderr); } +LONG WINAPI EndAfterReport(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_CONTINUE_SEARCH; +} + +[[gnu::noinline]] void FaultHoldingObject() { + AddVectoredExceptionHandler(0, EndAfterReport); + auto* object = static_cast(VirtualAlloc(nullptr, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + if (object == nullptr) std::exit(1); + for (std::uint64_t i = 0; i < 8; ++i) object[i] = 0xc0ffee0000000000ull + i; + std::uint64_t* volatile held = object; + volatile int* volatile target = nullptr; + *target = static_cast(reinterpret_cast(held)); +} + } int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "fault") == 0) FaultHoldingObject(); if (argc == 2 && std::strcmp(argv[1], "report") == 0) { LibcRegisterFailureReport_nid_no_patch(DroppedReport); LibcRegisterFailureReport_nid_no_patch(KeptReport); diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt new file mode 100644 index 0000000000..f84f4e6842 --- /dev/null +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(GuestArenaTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(guest_arena_tests ../GuestArenaHost.cpp ../../prx/libc/src/GuestArena.cpp ../../prx/libc/src/GuestWriteWatch.cpp) +target_include_directories(guest_arena_tests PRIVATE ../..) +target_compile_features(guest_arena_tests PRIVATE cxx_std_20) +target_link_libraries(guest_arena_tests PRIVATE Threads::Threads) +set_target_properties(guest_arena_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode available unmappable host 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/libs/tests/test_semaphore_trace.py b/core/libs/tests/test_semaphore_trace.py new file mode 100644 index 0000000000..004692b8ce --- /dev/null +++ b/core/libs/tests/test_semaphore_trace.py @@ -0,0 +1,46 @@ +import os +import re +import subprocess +import sys + + +def run(executable, enabled): + environment = os.environ.copy() + environment.pop("APS5_TRACE_SYNC", None) + if enabled: + environment["APS5_TRACE_SYNC"] = "1" + return subprocess.run([executable], capture_output=True, check=False, env=environment, text=True, timeout=15) + + +def main(): + executable = sys.argv[1] + quiet = run(executable, False) + if quiet.returncode != 0 or "[sema]" in quiet.stderr: + raise SystemExit("semaphore test failed or traced without APS5_TRACE_SYNC") + + traced = run(executable, True) + if traced.returncode != 0: + raise SystemExit(traced.stderr or f"semaphore test exited {traced.returncode}") + + lines = traced.stderr.splitlines() + create = next((line for line in lines if "[sema] create " in line and "'trace'" in line), None) + if create is None: + raise SystemExit("semaphore creation trace is missing") + handle_match = re.search(r"\[sema\] create (\S+)", create) + if handle_match is None: + raise SystemExit("semaphore creation trace has no handle") + handle = handle_match.group(1) + expected = { + "signal": f"[sema] signal {handle} 'trace'", + "wait begin": f"[sema] wait begin {handle} 'trace'", + "wait end": f"[sema] wait end {handle} 'trace'", + } + for event, prefix in expected.items(): + if not any(line.startswith(prefix) for line in lines): + raise SystemExit(f"semaphore {event} trace for the created handle is missing") + if not any(line.startswith(expected["wait begin"]) and "timeout=-1" in line for line in lines): + raise SystemExit("indefinite semaphore wait is not identified in the trace") + + +if __name__ == "__main__": + main() diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index bc368dbda2..c53be3987b 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -438,6 +438,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() + list(APPEND tlsCoverageGroups gs_traps) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) @@ -604,6 +605,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) + add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 41948aec85..f82d8a19f9 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -40,6 +40,7 @@ inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0}; inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; +inline constexpr Entry kRdtscp = {"RDTSCP", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kScalarCompare = {"scalar compare", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp index 798128e36c..4176cc4cff 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -25,6 +25,7 @@ struct DecodedInstruction { [[nodiscard]] bool IsMovntsd() const; [[nodiscard]] bool IsRdseed() const; [[nodiscard]] bool IsRdpid() const; + [[nodiscard]] bool IsRdtscp() const; [[nodiscard]] bool IsClwb() const; private: diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 7b6175bdff..c9128ecbaf 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -134,6 +134,13 @@ std::vector _rdpidReplacement(const DecodedInstruction& instr) { return bytes; } +std::vector _rdtscpReplacement(const DecodedInstruction& instr) { + std::vector bytes(instr.Data, instr.Data + instr.Length); + bytes.push_back(0xB9); + bytes.insert(bytes.end(), 4, 0); + return bytes; +} + bool _validWait(const DecodedInstruction& instr) { const auto opcode = instr.Data + instr.OpcodeOffset(); return instr.Length <= kMaxInstructionLength && std::find(instr.Data, opcode, X64OpcodeConstants::PrefixLock) == opcode; @@ -229,6 +236,8 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (instr.IsRdtscp()) + return _trampoline(kRdtscp.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdtscpReplacement(instr)); }), true); if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length)) @@ -268,6 +277,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c EmitAndn(body, *andn); return "ANDN"; } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdtscp()) { + body.Raw(_rdtscpReplacement(instr)); + return kRdtscp.Name; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; @@ -304,7 +317,7 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && (first.IsRdpid() || first.IsRdtscp())); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } diff --git a/core/relinker/codegen/src/x86/DecodedInstruction.cpp b/core/relinker/codegen/src/x86/DecodedInstruction.cpp index 3bdb906efe..d54ce541bc 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -292,6 +292,23 @@ bool DecodedInstruction::IsRdpid() const { ((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7; } +bool DecodedInstruction::IsRdtscp() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (operandSizeOverride || repnePrefix || repPrefix) { + return false; + } + + if (pos + 2 >= Length) { + return false; + } + + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xF9; +} + bool DecodedInstruction::IsClwb() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 6fbecaf06b..89bc80f07d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -655,8 +655,8 @@ void rosettaSubstitutions() { const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); const auto match = [](const auto& matcher, const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); }; - for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}}) - require(!match(intel, bytes).has_value(), "RDSEED, RDPID or CLWB was converted for an Intel host"); + for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}, Bytes{0x0F, 0x01, 0xF9}}) + require(!match(intel, bytes).has_value(), "RDSEED, RDPID, CLWB or RDTSCP was converted for an Intel host"); const auto rdseed = match(rosetta, {0x48, 0x0F, 0xC7, 0xF9}); require(rdseed && rdseed->Lowering == Amd64OnlyLowering::InPlace && rdseed->ReplacementBytes == Bytes{0x48, 0x0F, 0xC7, 0xF1} && rdseed->InstructionName == "RDSEED", "RDSEED was not rewritten to RDRAND"); const auto rdseed16 = match(rosetta, {0x66, 0x41, 0x0F, 0xC7, 0xFA}); @@ -669,6 +669,11 @@ void rosettaSubstitutions() { const auto rdpid = match(rosetta, {0xF3, 0x41, 0x0F, 0xC7, 0xFB}); require(rdpid && rdpid->Lowering == Amd64OnlyLowering::Trampoline && rdpid->Optional && rdpid->InstructionName == "RDPID", "RDPID was not lowered through an optional stub"); require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); + const auto rdtscp = match(rosetta, {0x0F, 0x01, 0xF9}); + require(rdtscp && rdtscp->Lowering == Amd64OnlyLowering::Trampoline && rdtscp->Optional && rdtscp->InstructionName == "RDTSCP", "RDTSCP was not lowered through an optional stub"); + require(rdtscp->ReturnBranchOffset == 8 && Bytes(rdtscp->StubBody.begin(), rdtscp->StubBody.begin() + 9) == Bytes{0x0F, 0x01, 0xF9, 0xB9, 0, 0, 0, 0, 0xE9}, "RDTSCP stub does not load 0 into ECX after reading the counter"); + require(!match(rosetta, {0x0F, 0x01, 0xF8}).has_value(), "SWAPGS (0F 01 F8) was taken for RDTSCP"); + require(!match(rosetta, {0x66, 0x0F, 0x01, 0xF9}).has_value(), "RDTSCP with an operand size prefix was lowered"); } void wideStoreSplit() { @@ -1745,6 +1750,40 @@ void immediateFormExecution() {} void ripRelativeExecution() {} #endif +#if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) +void rdtscpExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x0F, 0x01, 0xF9}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDTSCP stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + std::uint64_t low = 0; + std::uint64_t high = 0; + std::uint64_t processor = 0; + std::uint8_t carry = 0; + asm volatile( + "mov $-1, %%rcx\n\t" + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(low), "=&d"(high), "=&c"(processor), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(((high << 32) | low) != 0, "RDTSCP stub did not read the time stamp counter"); + require(processor == 0, "RDTSCP stub left a value other than processor 0 in RCX"); + require(carry == 1, "RDTSCP stub changed the flags"); +} +#else +void rdtscpExecution() {} +#endif + void scannerZeroTail() { const auto scanner = Codegen::MakeInstructionScanner(); const Bytes code{0xC3, 0x00, 0x00, 0x00}; @@ -1786,6 +1825,7 @@ int main() { converterRosettaScalarCompare(); rosettaLegacyScalarCompares(); converterRosettaLegacyScalarCompare(); + rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 19ff13e378..edf68e531f 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -90,6 +91,20 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons throw Domain::RelinkerException("TLS instruction is outside the PE image", access.Rva); } +void trapInstruction(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; + const auto offset = rva - section.Rva; + if (length > section.Data.size() - offset) + throw Domain::RelinkerException("Guest GS instruction crosses a PE section boundary", rva); + std::fill_n(section.Data.begin() + offset, length, 0x90); + std::copy(std::begin(kStubOpUd2), std::end(kStubOpUd2), section.Data.begin() + offset); + return; + } + throw Domain::RelinkerException("Guest GS instruction is outside the PE image", 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 { @@ -98,6 +113,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const Codegen::X64InstructionDecoder decoder; std::vector accesses; + std::vector> gsTraps; std::set branchTargets; std::set entryRoots; const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }, &entryRoots); @@ -133,6 +149,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co branchTargets.insert(static_cast(target)); } + if (info.SegmentPrefix == 0x65 && !insidePrevious) { + gsTraps.emplace_back(rva, info.Length); + continue; + } if (info.SegmentPrefix != 0 && !(insidePrevious && info.SegmentPrefix != 0x64)) { const auto position = info.OpcodeOffset; bool hasOperandSizePrefix = false; @@ -173,7 +193,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; branchDisplacementOffset = movedBytes.size(); - movedBytes.insert(movedBytes.end(), 4, 0); + movedBytes.resize(movedBytes.size() + 4); } else { branchDisplacementOffset = next.Length - 4; } @@ -200,6 +220,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } report(9000); + for (const auto& [rva, length] : gsTraps) + trapInstruction(sections, rva, length); if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 325b6f3399..1d5bc3c10c 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -290,6 +290,7 @@ int Run(const int argc, char* argv[]) { for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end()); if (desktopEntry) { outputPaths.push_back(desktopEntry->IconPath); outputPaths.push_back(desktopEntry->EntryPath); diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 8b83ab475e..915607b420 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -104,6 +105,8 @@ std::set CodeInstructionCollector::Collect(const std::ve 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; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 33cc5bdfc2..2d94dcf5dc 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -292,14 +294,27 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (std::size_t index = 0; index < images.size(); ++index) visit(index); std::vector hostLibraries; + std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; - const auto addHost = [&](const std::string& name) { + const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); - if (uniqueHosts.insert(name).second) hostLibraries.push_back(name); + if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name); }; - for (const auto& name : neededNames) addHost(name); - for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); + for (const auto& name : neededNames) addHost(name, true); + for (std::size_t index = 0; index < images.size(); ++index) { + auto& hosts = moduleHostLibraries[index]; + for (const auto& dependency : images[index].Dependencies) { + addHost(dependency, windows || !deferred(index)); + if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); + } + if (!windows && !macos && images[index].Init != 0) { + 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"); + } + if (std::find(hosts.begin(), hosts.end(), "libSceLibcInternal.prx") != hosts.end() && + std::find(hosts.begin(), hosts.end(), "libc.prx") == hosts.end()) hosts.push_back("libc.prx"); + } if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx"); dynamic.NeededLibraries = hostLibraries; dynamic.DynamicSegmentData.clear(); @@ -313,12 +328,6 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; - std::vector moduleHostLibraries; - std::set uniqueModuleHosts; - for (const auto& image : images) for (const auto& dependency : image.Dependencies) { - if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency); - } - if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx"); for (const auto index : order) { auto& image = images[index]; progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); @@ -329,9 +338,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } + const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); + const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { + std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + if (sceImage) + if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) + code.resize(*end - header.MappedAddress); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; @@ -363,7 +377,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end()); + needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index b82abed93f..e915e0f2c5 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -165,6 +166,23 @@ std::vector ElfReader::ReadProgramHeaders() const { } } + bool hasTls = false; + for (const auto& tls : headers) { + if (tls.Type != PT_TLS) continue; + if (hasTls) + throw RelinkerException("Multiple ELF TLS segments", tls.Offset); + hasTls = true; + if (tls.FileSize > tls.MemorySize || (tls.Alignment > 1 && !std::has_single_bit(tls.Alignment))) + throw RelinkerException("Invalid or unsupported ELF TLS layout", tls.Offset); + if (tls.FileSize == 0) continue; + const bool covered = std::any_of(headers.begin(), headers.end(), [&](const ProgramHeader& load) { + return load.Type == PT_LOAD && tls.MappedAddress >= load.MappedAddress && tls.MappedAddress - load.MappedAddress <= load.MemorySize + && tls.FileSize <= load.MemorySize - (tls.MappedAddress - load.MappedAddress); + }); + if (!covered) + throw RelinkerException("PT_TLS initialization image is outside PT_LOAD memory", tls.Offset); + } + return headers; } diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index ef54dd3c96..8b1c17d895 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,22 @@ Bytes Fixture() { return bytes; } +Bytes TlsFixture(const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize, const std::uint64_t alignment) { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x260, bytes.data() + 64, 56); + Write(bytes, 32, 0x260); + Write(bytes, 56, 2); + Write(bytes, 0x298, 7); + Write(bytes, 0x29c, 4); + Write(bytes, 0x2a0, 0x248); + Write(bytes, 0x2a8, address); + Write(bytes, 0x2b0, address); + Write(bytes, 0x2b8, fileSize); + Write(bytes, 0x2c0, memorySize); + Write(bytes, 0x2c8, alignment); + return bytes; +} + void Require(const bool condition) { if (!condition) throw std::runtime_error("Unexpected parsed value"); } @@ -119,6 +136,37 @@ int main() { Write(bytes, 104, 0xd0); RejectProgramHeaders(std::move(bytes)); }}, + {"TLS image inside load memory and empty TLS placeholders", [] { + for (const auto& [address, fileSize, memorySize, alignment] : {std::tuple{0x1008, 8, 16, 8}, {0x1030, 16, 16, 16}, {0x9000, 0, 0, 0}, {0x9000, 0, 16, 16}}) { + const auto headers = Relinker::ElfReader(TlsFixture(address, fileSize, memorySize, alignment)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Type == 7 && headers[1].MappedAddress == address); + } + }}, + {"TLS image inside uncovered SCE RELRO", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + Write(bytes, 0x260, 0x61000010); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[0].Type == 1 && headers[1].Type == 7 && headers[1].MappedAddress == 0x1008); + }}, + {"TLS image outside load memory", [] { + constexpr std::uint64_t addresses[] = {0x0ff8, 0x103c, 0x2000, std::numeric_limits::max() - 3}; + for (const std::uint64_t address : addresses) { + RejectProgramHeaders(TlsFixture(address, 8, 8, 1)); + } + }}, + {"TLS file size exceeds memory size", [] { + RejectProgramHeaders(TlsFixture(0x1008, 16, 8, 8)); + }}, + {"TLS alignment is not a power of two", [] { + RejectProgramHeaders(TlsFixture(0x1008, 8, 8, 3)); + }}, + {"second TLS segment", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + bytes.resize(0x308); + std::memcpy(bytes.data() + 0x2d0, bytes.data() + 0x298, 56); + Write(bytes, 56, 3); + RejectProgramHeaders(std::move(bytes)); + }}, {"section entry size field", [] { const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 6c3de5f3a4..5aee388957 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -10,6 +10,9 @@ import tempfile from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols from test_macos_output import MachO from test_windows_import_modules import executable +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_needed_modules import module_with_needed ARGS = 0x1122334455667788 ARGP = 0x99AABBCCDDEEFF00 @@ -56,6 +59,55 @@ def no_relocation_module(): return image +def check_lifecycle_host(relinker, host, work, linux, deferred): + case = work / f'lifecycle-host-{deferred}' + modules, plugins = case / 'sce_module', case / 'Media/Plugins' + modules.mkdir(parents=True) + plugins.mkdir(parents=True) + image = no_relocation_module() + image[0x1010:0x1013] = b'\x31\xc0\xc3' + tags = [] + for position in range(0x2000, 0x2200, 16): + tag, value = struct.unpack_from(' constantPointer; }; struct TableCallee { diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index a67f30d1ec..fe2c3565a3 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -294,10 +294,11 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std:: return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; - if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; + const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; + if ((IsDirectBranchOpcode(instruction.op) && !skipped) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index, waveSize); })) { writer = position; return true; @@ -1286,10 +1287,10 @@ bool isScalarBufferLoad(RdnaOpcode op) { } } -bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t& load, std::uint32_t& dword) { +bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t skipBranchesPast, std::uint32_t& load, std::uint32_t& dword) { for (std::uint32_t copies = 0; copies < 4u; ++copies) { std::uint32_t writer = 0; - if (!findLastWriter(program, before, {reg}, writer, waveSize)) return false; + if (!findLastWriter(program, before, {reg}, writer, waveSize, skipBranchesPast)) return false; const auto& instruction = program.instructions[writer]; const auto destination = scalarIndex(instruction.destination); const auto source = scalarIndex(instruction.source0); @@ -1445,28 +1446,39 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn const auto target = scalarIndex(instruction.source0); if (link == NoScalarRegister || link % 2u != 0u || link > 104u || target == NoScalarRegister || target % 2u != 0u || target > 104u) continue; std::uint32_t lowLoad = 0, lowDword = 0, highLoad = 0, highDword = 0; - if (!loadedDword(program, target, index, swappc.waveSize, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, highLoad, highDword)) continue; + const auto reader = instruction.programCounter; + if (!loadedDword(program, target, index, swappc.waveSize, reader, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, reader, highLoad, highDword)) continue; if (lowLoad != highLoad || highDword != lowDword + 1u || !sameBlock(lowLoad, index)) continue; const auto& load = program.instructions[lowLoad]; const auto descriptor = scalarIndex(load.source0); if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue; std::uint32_t descriptorLoad = 0; - if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize) || !sameBlock(descriptorLoad, index)) continue; + if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) || !sameBlock(descriptorLoad, index)) continue; const auto& table = program.instructions[descriptorLoad]; const auto pointer = scalarIndex(table.source0); - if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || table.source0.kind != RdnaOperandKind::ScalarRegister || table.source1.kind != RdnaOperandKind::Null || - table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u || !userData(pointer) || !userData(pointer + 1u) || - !unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) || !unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize)) continue; + if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || + table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue; + std::optional constantPointer; + if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) { + std::uint32_t lowWriter = 0, highWriter = 0, low = 0, high = 0; + if (!findLastWriter(program, descriptorLoad, {pointer}, lowWriter, swappc.waveSize, reader) || !findLastWriter(program, descriptorLoad, {pointer + 1u}, highWriter, swappc.waveSize, reader) || + !sameBlock(std::min(lowWriter, highWriter), descriptorLoad)) continue; + const auto& lowMove = program.instructions[lowWriter]; + const auto& highMove = program.instructions[highWriter]; + if (lowMove.op != RdnaOpcode::SMovB32 || highMove.op != RdnaOpcode::SMovB32 || !isScalar(lowMove.destination, pointer) || !isScalar(highMove.destination, pointer + 1u) || + !isImmediate(lowMove.source0, low) || !isImmediate(highMove.source0, high)) continue; + constantPointer = static_cast(high) << 32u | low; + } std::uint32_t stride = 0; if (load.source1.kind != RdnaOperandKind::Null) { const auto offset = scalarIndex(load.source1); std::uint32_t shift = 0, shiftIndex = 0; - if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize) || !sameBlock(shiftIndex, index)) continue; + if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize, reader) || !sameBlock(shiftIndex, index)) continue; const auto& shifted = program.instructions[shiftIndex]; if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue; stride = 1u << shift; } - calls.push_back({instruction.programCounter, link, target, pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride}); + calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer}); } return calls; } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 8879b166be..1440effcb2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -42,7 +42,7 @@ struct ShaderInfo { }; [[nodiscard]] inline std::uint32_t WorkgroupMemoryStrideDwords(const ShaderInfo& info) { - return info.sharedMemoryBytes == 0u ? 0u : info.sharedMemoryBytes / 4u + 1u; + return info.sharedMemoryBytes == 0u ? 0u : (info.sharedMemoryBytes / 4u + 2u) & ~1u; } } diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 528958e6d9..772b61e75c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -68,6 +68,7 @@ public: static void PrepareImageModes(ShaderInfo& info); static const char* UnnormalizedSampleMismatch(const ImageResource& image); static std::shared_ptr ResolveImageTablesShared(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey); + static bool NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor); static ResolvedImageTables ResolveImageTables(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey = 0); [[nodiscard]] IrResourcePlan ExtractPlan(const IrProgram& program) const; static void CompactPlan(IrResourcePlan& plan); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 79e73779e2..e4544a55d9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -182,10 +182,11 @@ struct UnnormalizedProof { std::vector samplers; std::vector images; std::vector imagesAtBaseLevel; + std::vector samplersIgnoreCoordinates; }; UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { - UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size())}; + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size()), std::vector(info.samplers.size())}; for (std::uint32_t r = 0; r < info.samplers.size(); r++) { if (snapshot.samplers.at(r).dwordCount != 4u) fail("sampler descriptor must contain four dwords"); const auto& sampler = info.samplers[r]; @@ -212,6 +213,8 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (sampler.depthCompare) { failUnnormalized("is used with depth comparison"); } + bool sampled = false; + const char* nullMismatch = nullptr; for (const auto& pair : info.sampledPairs) { if (pair.sampler != r) { continue; @@ -221,12 +224,23 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh failUnnormalized("samples an image selected at run time"); } const auto& image = info.runtimeImageModes.at(pair.image).at(ResourceMaterializer::RuntimeImageMode(base, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image))); - if (const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image)) { + const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image); + if (reason != nullptr && ResourceMaterializer::NullImageDescriptor(base, snapshot.images.at(pair.image))) { + if (nullMismatch == nullptr) nullMismatch = reason; + continue; + } + if (reason != nullptr) { failUnnormalized(reason); } if (image.constantSwizzle) continue; + sampled = true; proof.images[pair.image] = true; } + if (!sampled && nullMismatch != nullptr) { + proof.samplersIgnoreCoordinates[r] = true; + continue; + } + if (nullMismatch != nullptr) failUnnormalized(nullMismatch); proof.samplers[r] = true; } for (std::uint32_t resource = 0; resource < info.images.size(); resource++) { @@ -451,6 +465,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l for (const auto& pair : info.sampledPairs) if (pair.sampler == resource) compare |= info.images.at(pair.image).depthCompare && compareStates.at(pair.image) == 0u; physical.samplerDepthCompare.push_back(compare); physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); + physical.samplerCoordinatesIgnored.push_back(unnormalized.samplersIgnoreCoordinates.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); const auto& words = snapshot.samplers.at(resource).dwords; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3484d31426..e05ee2f2aa 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1931,6 +1931,10 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe return modes; } +bool ResourceMaterializer::NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { + return nullImageDescriptor(image, descriptor); +} + const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource& image) { if (image.table != NoTable) return "samples an image selected at run time"; if (image.constantSwizzle) return nullptr; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 73b74dad2a..524fddde19 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -658,7 +658,7 @@ private: column.address = address; column.foldedAddend = folded ? baseAddend : 0u; column.foldedCarry = folded ? carry : 0u; - if (!address && !sampler) { + if (!address && !sampler && std::ranges::find(m_unselectedHandles, &handle) == m_unselectedHandles.end()) { DescriptorSource direct; MakeSource(handle, width, false, false, direct); DescriptorSource selection; @@ -1292,6 +1292,118 @@ private: if (!enabled) { return; } + const auto sources = m_sources; + const auto columns = m_tableColumns; + const auto reads = m_tablePlanningReads; + const auto memory = m_tablePlanningMemory; + PlanTableColumnPass(); + while (!PlannedWithinLimits()) { + const auto group = LargestSelectedGroup(); + if (group.empty()) { + return; + } + m_unselectedHandles.insert(m_unselectedHandles.end(), group.begin(), group.end()); + m_sources = sources; + m_tableColumns = columns; + m_tablePlanningReads = reads; + m_tablePlanningMemory = memory; + PlanTableColumnPass(); + } + } + + bool PlannedWithinLimits() { + std::vector images; + std::vector> pairs; + std::vector directImages; + std::vector> directSamplers; + const auto direct = std::numeric_limits::max() / 2u; + for (auto& block : m_program.Blocks()) { + for (IrValue* inst : block->Instructions()) { + const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); + if (inst->Opcode() == IrOpcode::ImageBvhIntersectRay || imageInfo.access == ImageAccess::None || imageInfo.resourceClass == ImageResourceClass::None || inst->ArgumentCount() == 0u) { + continue; + } + const auto flags = inst->Flags(); + if (flags.index >= m_program.Resources().memoryInfo.size()) { + continue; + } + const auto& memory = m_program.Resources().memoryInfo[flags.index]; + if (memory.kind != ResourceKind::Image || memory.planningOnly || IsTablePlanningMemory(flags.index)) { + continue; + } + const IrValue* handle = inst->Argument(0)->Resolve(); + const TableColumnPlan* column = FindTableColumn(*handle); + std::uint32_t source = 0; + if (column != nullptr) { + source = column->source; + } else { + const auto found = std::ranges::find_if(directImages, [&](const IrValue* other) { return SameHandle(*other, *handle); }); + source = direct + static_cast(found - directImages.begin()); + if (found == directImages.end()) directImages.push_back(handle); + } + const auto key = ImageKey(source, memory, inst->Opcode()); + const auto image = std::ranges::find_if(images, [&](const ImageResource& other) { return SameImageKey(other, key); }); + const auto imageIndex = static_cast(image - images.begin()); + if (image == images.end()) images.push_back(key); + if (!imageInfo.needsSampler || inst->ArgumentCount() < 2u) { + continue; + } + const IrValue* samplerHandle = inst->Argument(1)->Resolve(); + const TableColumnPlan* samplerColumn = FindTableColumn(*samplerHandle); + std::uint32_t sampler = 0; + if (samplerColumn != nullptr) { + sampler = samplerColumn->source; + } else { + const bool sampleAdjust = (memory.imageSampleFlags & RdnaImageSampleFlagAdjust) != 0; + const auto found = std::ranges::find_if(directSamplers, [&](const auto& other) { return other.second == sampleAdjust && SameHandle(*other.first, *samplerHandle); }); + sampler = direct + static_cast(found - directSamplers.begin()); + if (found == directSamplers.end()) directSamplers.emplace_back(samplerHandle, sampleAdjust); + } + if (std::ranges::find(pairs, std::pair{imageIndex, sampler}) == pairs.end()) pairs.emplace_back(imageIndex, sampler); + } + } + return images.size() <= ShaderInfo::MaxImages && pairs.size() <= ShaderInfo::MaxSampledPairs; + } + + std::vector LargestSelectedGroup() const { + const auto unselected = [](TableColumn column) { + column.selected = false; + column.selection = 0u; + return column; + }; + std::vector> groups; + for (const auto& plan : m_tableColumns) { + const auto& column = m_sources[plan.source].tableColumn; + if (!column.has_value()) { + continue; + } + const auto group = std::ranges::find_if(groups, [&](const std::vector& members) { return unselected(*m_sources[members.front()].tableColumn) == unselected(*column); }); + if (group == groups.end()) { + groups.push_back({plan.source}); + } else if (std::ranges::find(*group, plan.source) == group->end()) { + group->push_back(plan.source); + } + } + const std::vector* largest = nullptr; + for (const auto& group : groups) { + const bool selected = std::ranges::any_of(group, [&](std::uint32_t source) { return m_sources[source].tableColumn->selected; }); + if (selected && group.size() > 1u && (largest == nullptr || group.size() > largest->size())) { + largest = &group; + } + } + std::vector handles; + if (largest == nullptr) { + return handles; + } + for (const auto& plan : m_tableColumns) { + if (std::ranges::find(*largest, plan.source) != largest->end()) { + handles.push_back(plan.handle); + } + } + return handles; + } + + void PlanTableColumnPass() { IndexMemoryUsers(); const auto directImages = DirectImageHandles(); for (auto& block : m_program.Blocks()) { @@ -1576,13 +1688,29 @@ private: resource.scalar = resource.scalar || op == IrOpcode::ReadConstBuffer || memory.kind == ResourceKind::ScalarBuffer; } + static ImageResource ImageKey(std::uint32_t source, const MemoryInfo& memory, IrOpcode op) { + 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.r128 = memory.imageR128; + image.packed = memory.imagePacked; + image.byElements = memory.imageByElements; + image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; + return image; + } + + 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; + } + std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { - const auto resourceClass = ImageOpcodeInfoOf(op).resourceClass; - const auto mip = resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; - const bool depth = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + const auto key = ImageKey(source, memory, op); for (std::uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; - if (image.source == source && image.resourceClass == resourceClass && image.dimension == memory.imageDimension && image.mipMode == mip && image.depthCompare == depth && image.r128 == memory.imageR128 && image.packed == memory.imagePacked && image.byElements == memory.imageByElements && image.byComponents == (memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u)) { + if (SameImageKey(image, key)) { Merge(image, memory, op, pc); return i; } @@ -1590,18 +1718,9 @@ private: if (m_info.images.size() >= ShaderInfo::MaxImages) { return std::numeric_limits::max(); } - ImageResource image; + ImageResource image = key; image.singleMipPlaneBiasCompatible = m_program.Resources().stage == IrShaderStage::Pixel; - image.source = source; image.firstUsePc = pc; - image.resourceClass = resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = mip; - image.depthCompare = depth; - image.r128 = memory.imageR128; - image.packed = memory.imagePacked; - image.byElements = memory.imageByElements; - image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; Merge(image, memory, op, pc); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); @@ -1848,6 +1967,7 @@ private: std::vector m_tableColumns; std::vector m_tablePlanningReads; std::vector m_tablePlanningMemory; + std::vector m_unselectedHandles; struct EdgeSelectorEntry { IrBlock* block = nullptr; std::vector edges; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index fac95cb5d4..80871f8ea9 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -535,6 +535,12 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo return inst->Opcode() == IrOpcode::BitwiseXor32; }); }); + const bool ldsAtomic64 = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* inst) { + return SharedAccessOf(inst->Opcode()) == SharedAccess::Atomic && inst->Type() == IrType::U64; + }); + }); + next.usesFaultBuffer = next.usesFaultBuffer || (next.sharedMemoryBytes != 0u && ldsAtomic64); switch (program.Resources().stage) { case IrShaderStage::Vertex: case IrShaderStage::Local: diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 38de551273..9f9133417d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -428,6 +428,7 @@ struct DescriptorBinding { std::vector bufferWritten; std::vector samplerUnnormalized; std::vector samplerIntegerBorder; + std::vector samplerCoordinatesIgnored; std::vector imageUnnormalized; std::vector imageUnnormalizedBaseLevel; std::vector imageSamplers; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 6b5dafb49d..24b7e61a21 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -87,7 +87,7 @@ namespace ShaderRecompiler::ShaderDiskCache { static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 632, "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"); static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: update the parameter encoder"); @@ -427,6 +427,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageUnnormalized); writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); + writer.Flags(binding.samplerCoordinatesIgnored); writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); } @@ -454,6 +455,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageUnnormalized); reader.Flags(binding.imageUnnormalizedBaseLevel); reader.Flags(binding.samplerIntegerBorder); + reader.Flags(binding.samplerCoordinatesIgnored); reader.Values(binding.imageSamplers); reader.Flags(binding.bufferRead); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index f9840a0963..bcb99a770a 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 = 37; +inline constexpr std::uint32_t FormatVersion = 38; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 1b1c53242b..ebfd8888e3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -19,6 +19,7 @@ struct SpirvRequirements { bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; + bool ldsBufferInt64Atomics = false; std::uint32_t functionLdsDwords = 0; std::unordered_map functionLdsAddresses; bool functionScratch = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 58789e09fe..bce467f22c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -39,6 +39,15 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem); +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords); +std::vector BdaReadSites(const SpirvEmitterState& state); +void CollectBdaAnchors(SpirvEmitterState& state); +void DefineBdaAnchorFunction(SpirvEmitterState& state); +void EmitBdaAnchors(SpirvEmitterState& state); +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base); +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst); +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 836b58a04c..e2101177cf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index e293dd87c7..8202209f9d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -143,6 +143,15 @@ struct SpirvEmitterState { std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; + struct BdaAnchor { + std::uint32_t lowRegister = 0; + std::uint32_t highRegister = 0; + std::uint32_t size = 0; + std::uint32_t device = 0; + std::uint32_t offset = 0; + }; + std::vector bdaAnchors; + std::uint32_t bdaAnchorFunction = 0; std::uint32_t bdaStopValue = 0; std::uint32_t bdaPcOverride = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). @@ -164,6 +173,7 @@ struct SpirvEmitterState { std::uint32_t flattenedSrtVariable = 0; std::uint32_t ldsVariable = 0; std::uint32_t ldsBufferVariable = 0; + std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 03bd0330d6..53a851625b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -15,6 +15,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b48e8775b5..c87aa246a2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -10,18 +10,18 @@ namespace ShaderRecompiler { namespace { -void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { +void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t zero) { const auto failed = state.module.AllocateId(); const auto next = state.module.AllocateId(); state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); EmitLabel(state, failed); - state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); + state.module.AddFunction(spv::OpReturnValue, zero); EmitLabel(state, next); } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read, bool rangeResult = false) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -29,7 +29,10 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); - state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); + const auto rangeType = rangeResult ? state.module.Type(spv::OpTypeVector, u64, 3u) : 0u; + const auto resultType = rangeResult ? rangeType : u64; + const auto zero = rangeResult ? state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)) : BdaConstant(state, 0u); + state.module.AddFunction(spv::OpFunction, resultType, function, rangeResult ? spv::FunctionControlMaskNone : spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32)); const auto address = state.module.AllocateId(); const auto bytes = state.module.AllocateId(); const auto instruction = state.module.AllocateId(); @@ -41,7 +44,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r if (recordFaults) { ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); } else { - ReturnBdaZeroIf(state, condition); + ReturnBdaZeroIf(state, condition, zero); } }; const auto failFirst = [&](std::initializer_list> checks) { @@ -51,7 +54,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); if (!recordFaults) { - ReturnBdaZeroIf(state, failed); + ReturnBdaZeroIf(state, failed, zero); return; } const auto failedLabel = state.module.AllocateId(); @@ -124,6 +127,16 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); + if (rangeResult) { + const auto last = binary(spv::OpUGreaterThanEqual, boolean, index, count); + const auto next = BdaLoadAddress(state, binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, Select(state, u32, last, binary(spv::OpISub, u32, index, constant(1)), index), constant(8)))); + const auto bound = Select(state, u64, binary(spv::OpLogicalOr, boolean, last, binary(spv::OpULessThan, boolean, finish, next)), finish, next); + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, bound, base); + state.module.AddFunction(spv::OpReturnValue, range); + state.module.AddFunction(spv::OpFunctionEnd); + return function; + } state.module.AddFunction(spv::OpReturnValue, result); state.module.AddFunction(spv::OpFunctionEnd); return function; @@ -212,6 +225,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { return function; } +void DefineBdaAnchorFunction(SpirvEmitterState& state) { + if (state.bdaAnchorFunction == 0) state.bdaAnchorFunction = DefineBdaLookup(state, "bda_anchor_range", false, BdaAbi::Read, true); +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (state.faultBufferVariable != 0) DefineBdaFaultFunction(state); if (!state.program.Info().usesDma) return; @@ -219,6 +236,8 @@ void DefineGetBdaPointer(SpirvEmitterState& state) { if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); DefineBdaDwordReadFunctions(state); DefineBdaSpanReadFunctions(state); + CollectBdaAnchors(state); + if (!state.bdaAnchors.empty()) DefineBdaAnchorFunction(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); if (!BdaByteReadsForced()) state.bdaWriteProbeFunction = DefineBdaLookup(state, "probe_bda_write_pointer", false, BdaAbi::Write); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 067d57d184..3d4f5d80bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -1,12 +1,16 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" #include #include +#include #include #include +#include #include #include +#include namespace ShaderRecompiler { @@ -101,23 +105,6 @@ std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std: return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); } -// The dwords of a wide load that the program extracts (all of them when the composite is used -// whole). Dead-code elimination has dropped the unused extracts, as it dropped unused dword loads -// when every dword was its own instruction. -std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { - const auto all = (1u << dwords) - 1u; - if (dwords == 1u) return all; - std::uint32_t used = 0; - for (const IrValue* user : inst.Uses()) { - const auto opcode = user->Opcode(); - if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; - const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; - if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; - used |= 1u << index->ImmediateU32(); - } - return used; -} - void EmitBdaStoreAt(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t pointerType, std::uint32_t value, std::uint32_t instruction, std::uint32_t accessMask) { const auto physical = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, bytes), instruction); @@ -375,6 +362,24 @@ bool BdaByteReadsForced() { return forced; } +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { + const auto all = (1u << dwords) - 1u; + if (dwords == 1u) return all; + std::uint32_t used = 0; + for (const IrValue* user : inst.Uses()) { + const auto opcode = user->Opcode(); + if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; + const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; + if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; + used |= 1u << index->ImmediateU32(); + } + return used; +} + +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { + return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; +} + std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst) { return state.bdaPcOverride != 0u ? state.bdaPcOverride : ConstantU32(state, inst.Flags().pc); } @@ -417,6 +422,215 @@ bool BdaInvocationsMayStop(const IrProgram& program) { return true; } +namespace { + +constexpr std::size_t MaxBdaAnchors = 4; +constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; +constexpr std::uint64_t BdaAnchorSpanLimit = std::uint64_t{1} << 62u; + +std::optional> UserDataBase(const SpirvEmitterState& state, const IrValue& inst) { + if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; + const auto index = inst.Flags().index; + const auto& memory = state.program.Resources().memoryInfo; + if (index >= memory.size()) return std::nullopt; + const auto& info = memory[index]; + if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; + if (info.planningOnly || info.addressIsFull || RoutesApertures(state, info)) return std::nullopt; + const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; + if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; + const auto userData = [](const IrValue* value) -> std::optional { + value = value != nullptr ? value->Resolve() : nullptr; + if (value == nullptr || value->Opcode() != IrOpcode::GetUserData || value->ArgumentCount() == 0u || value->Argument(0) == nullptr) return std::nullopt; + return value->Argument(0)->Register().index; + }; + const auto low = userData(handle->Argument(0)); + const auto high = userData(handle->Argument(1)); + if (!low || !high) return std::nullopt; + return std::pair{*low, *high}; +} + +std::uint32_t EmitElectedBdaRange(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, const std::function& search) { + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + const auto before = state.currentLabel; + const auto electedLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, electedLabel, merge); + EmitLabel(state, electedLabel); + const auto found = search(); + const auto electedExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, type, value, found, electedExit, zero, before); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + std::array parts{}; + for (std::uint32_t component = 0; component < parts.size(); ++component) { + const auto part = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, part, value, component); + const auto shared = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, pair, shared, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpBitcast, pair, part)); + parts[component] = Unary(state, spv::OpBitcast, u64, shared); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, parts[0], parts[1], parts[2]); + return result; +} + +std::uint32_t BdaAnchorHolds(SpirvEmitterState& state, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bytes, std::uint32_t& translated) { + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + auto offset = Binary(state, spv::OpIAdd, u64, anchor.offset, Unary(state, spv::OpUConvert, u64, low)); + if (immediate != 0) offset = Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, static_cast(static_cast(immediate)))); + const auto width = BdaConstant(state, bytes); + const auto fits = Binary(state, spv::OpUGreaterThanEqual, boolean, anchor.size, width); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u64, anchor.size, width)); + translated = Binary(state, spv::OpIAdd, u64, anchor.device, offset); + return Binary(state, spv::OpLogicalAnd, boolean, fits, inside); +} + +} + +void CollectBdaAnchors(SpirvEmitterState& state) { + state.bdaAnchors.clear(); + if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; + std::vector, std::size_t>> bases; + for (const IrValue* inst : BdaReadSites(state)) { + const auto base = UserDataBase(state, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); + } + std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); + for (const auto& [base, reads] : bases) { + if (reads < 2u || state.bdaAnchors.size() == MaxBdaAnchors) break; + state.bdaAnchors.push_back({base.first, base.second}); + } +} + +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base) { + if (state.bdaAnchorFunction == 0) throw std::runtime_error("the BDA anchor search function is missing"); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + const auto rangeType = state.module.Type(spv::OpTypeVector, u64, 3u); + const auto search = [&] { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, rangeType, range, state.bdaAnchorFunction, base, ConstantU32(state, 1u), ConstantU32(state, 0u)); + return range; + }; + const auto zero = state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)); + const auto range = state.requirements.subgroupBallot ? EmitElectedBdaRange(state, rangeType, zero, search) : search(); + const auto extract = [&](std::uint32_t component) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, value, range, component); + return value; + }; + const auto begin = extract(0u); + const auto end = extract(1u); + anchor.device = extract(2u); + anchor.offset = Binary(state, spv::OpISub, u64, base, begin); + const auto span = Binary(state, spv::OpISub, u64, end, begin); + const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); + auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, span, BdaConstant(state, BdaAnchorSpanLimit))); + anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); +} + +void EmitBdaAnchors(SpirvEmitterState& state) { + if (state.bdaAnchors.empty()) return; + const auto u64 = TypeScalarU64(state); + for (auto& anchor : state.bdaAnchors) { + const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); + const auto high = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.highRegister))); + EmitBdaAnchorRange(state, anchor, Binary(state, spv::OpBitwiseOr, u64, low, Binary(state, spv::OpShiftLeftLogical, u64, high, BdaConstant(state, 32u)))); + } +} + +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { + if (state.bdaAnchors.empty()) return nullptr; + const auto base = UserDataBase(state, inst); + if (!base) return nullptr; + for (const auto& anchor : state.bdaAnchors) { + const bool emitted = anchor.size != 0u; + if (emitted && anchor.lowRegister == base->first && anchor.highRegister == base->second) return &anchor; + } + return nullptr; +} + +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup) { + if (bits != 8u && bits != 16u && bits != 32u) throw std::runtime_error("unsupported anchored BDA read width"); + if (bits == 32u && (dwords == 0u || dwords > 4u)) throw std::runtime_error("unsupported anchored BDA read dword count"); + const auto used = bits != 32u ? 1u : usedDwords != 0u ? usedDwords : UsedBdaDwords(inst, dwords); + if (used == 0u) return lookup(); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + std::uint32_t first = 0; + std::uint32_t last = 0; + std::uint32_t bytes = bits / 8u; + if (bits == 32u) { + first = static_cast(std::countr_zero(used)); + last = 31u - static_cast(std::countl_zero(used)); + bytes = (last - first + 1u) * 4u; + } + std::uint32_t translated = 0; + auto direct = BdaAnchorHolds(state, anchor, low, static_cast(static_cast(immediate) + first * 4u), bytes, translated); + if (bits == 32u) direct = Binary(state, spv::OpLogicalAnd, boolean, direct, Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, translated))); + const auto directLabel = state.module.AllocateId(); + const auto lookupLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, direct, directLabel, lookupLabel); + EmitLabel(state, directLabel); + std::array directValues{}; + const bool coherent = BdaCoherent(state, inst); + if (bits == 32u) { + for (std::uint32_t dword = first; dword <= last; ++dword) { + if ((used & (1u << dword)) == 0u) continue; + const auto element = dword == first ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, (dword - first) * 4u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); + directValues[dword] = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, directValues[dword], pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, directValues[dword], pointer, BdaAccessMask(false), 4u); + } + } else { + const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); + const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); + auto value = ConstantU32(state, 0u); + for (std::uint32_t byte = 0; byte < bytes; ++byte) { + const auto element = byte == 0u ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, byte)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent), 1u); + value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); + } + directValues[0] = value; + } + const auto directExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, lookupLabel); + const auto lookupValues = lookup(); + const auto lookupExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + std::array values{}; + for (std::uint32_t dword = 0; dword < (bits == 32u ? dwords : 1u); ++dword) { + if ((used & (1u << dword)) == 0u) { + values[dword] = ConstantU32(state, 0u); + continue; + } + values[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, values[dword], directValues[dword], directExit, lookupValues[dword], lookupExit); + } + return values; +} + std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits) { auto& state = ctx.state; if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA read width"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index f30656653d..050316a6f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -262,6 +262,10 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; 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; + state.requirements.ldsBufferInt64Atomics = true; + } if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index d208290cac..192b60cc27 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -111,6 +111,15 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state) { return pointer; } +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index) { + if (!state.requirements.ldsBufferInt64Atomics || state.ldsBufferU64Variable == 0u || state.ldsBufferBase == 0u) { + FailEmit("64-bit device memory LDS atomics were not prepared at function entry"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)), pointer, state.ldsBufferU64Variable, ConstantU32(state, 0u), EmitBinaryU32(state, spv::OpShiftRightLogical, EmitAddU32(state, state.ldsBufferBase, index), ConstantU32(state, 1u))); + return pointer; +} + MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem) { MemoryResourceAccess access; access.kind = mem.kind; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 2f44307dc3..7a704ea52d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -518,6 +518,14 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.requirements.ldsBufferInt64Atomics) { + state.ldsBufferU64Variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU64BlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferU64Variable, "workgroup_memory_u64"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationCoherent); + for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index d561dea75c..97482cdcaa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -202,6 +203,28 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +std::uint32_t GuestAddressLow(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { + const auto low = ctx.Arg(inst, 1); + if (mem.kind != ResourceKind::ScalarAddress) return low; + return Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), low, ConstantU32(ctx.state, ~3u)); +} + +std::array ReadBdaDwords(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t dwords, std::uint32_t used) { + const auto lookup = [&] { return EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, dwords, false, used); }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup(); + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(mem.offset), 32u, dwords, used, lookup); +} + +std::uint32_t ReadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + const auto lookup = [&] { return std::array{EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits)}; }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup()[0]; + auto immediate = mem.offset; + if (mem.kind == ResourceKind::ScalarAddress) immediate &= ~3u; + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(immediate), bits, 1u, 1u, lookup)[0]; +} + bool SameAddressValue(const IrValue* left, const IrValue* right, std::uint32_t depth = 0u) { if (left == right) return true; if (left == nullptr || right == nullptr || depth > 3u || left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; @@ -224,10 +247,10 @@ bool WritesMemory(IrOpcode opcode) { image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; } -std::vector> AdjacentBdaLoads(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { +std::vector> AdjacentBdaLoads(const SpirvEmitterState& state, const IrValue& inst, const MemoryInfo& mem) { std::vector> members; const IrBlock* block = inst.Parent(); - if (block == nullptr || ctx.state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; + if (block == nullptr || state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; const auto& instructions = block->Instructions(); auto next = std::find(instructions.begin(), instructions.end(), &inst); if (next == instructions.end()) return members; @@ -238,7 +261,7 @@ std::vector> AdjacentBdaLoads(SpirvValu if (WritesMemory(other->Opcode())) break; continue; } - const auto& otherMemory = ctx.Memory(*other); + const auto& otherMemory = state.program.Resources().memoryInfo.at(other->Flags().index); if (otherMemory.planningOnly || otherMemory.kind != mem.kind || otherMemory.addressIsFull != mem.addressIsFull || otherMemory.coherent != mem.coherent || other->ArgumentCount() != inst.ArgumentCount()) continue; bool sameArguments = true; for (std::size_t index = 0; index < inst.ArgumentCount(); ++index) { @@ -262,7 +285,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); return value; } - const auto members = AdjacentBdaLoads(ctx, inst, mem); + const auto members = AdjacentBdaLoads(state, inst, mem); if (!members.empty()) { std::uint32_t used = 1u; for (const auto& member : members) used |= 1u << member.second; @@ -271,7 +294,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem const auto none = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { - const auto values = EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, 4u, false, used); + const auto values = ReadBdaDwords(ctx, inst, mem, 4u, used); const auto read = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); return read; @@ -283,7 +306,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem } } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); + return ReadBda(ctx, inst, mem, bits); }); } @@ -414,10 +437,34 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { - return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; + } +std::vector BdaReadSites(const SpirvEmitterState& state) { + std::vector sites; + std::unordered_set grouped; + const auto& memory = state.program.Resources().memoryInfo; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto info = AddressOpcodeInfoOf(inst->Opcode()); + const auto index = inst->Flags().index; + if (info.access != AddressAccess::Read || index >= memory.size() || grouped.contains(inst)) continue; + const auto& mem = memory[index]; + if (mem.planningOnly || RoutesApertures(state, mem)) continue; + if (mem.kind != ResourceKind::ScalarAddress && mem.kind != ResourceKind::Flat && mem.kind != ResourceKind::Global) continue; + if (info.dataBits == 32u && info.components == 1u) { + for (const auto& member : AdjacentBdaLoads(state, *inst, mem)) grouped.insert(member.first); + } else if (info.components > 1u && UsedBdaDwords(*inst, info.components) == 0u) { + continue; + } + sites.push_back(inst); + } + } + return sites; +} + +namespace { + struct ApertureAddress { std::uint32_t low = 0; std::uint32_t shared = 0; @@ -576,7 +623,7 @@ void LoadAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t std::uint32_t LoadBdaWide(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { - return ConstructU32Composite(state, components, EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, components)); + return ConstructU32Composite(state, components, ReadBdaDwords(ctx, inst, mem, components, 0u)); }); } @@ -761,6 +808,11 @@ std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t poin template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); +template +std::uint32_t LdsBufferAtomic64(SpirvEmitterState& state, std::uint32_t low, TNext&& next) { + return Unary(state, spv::OpBitcast, TypeU64(state), AtomicUpdateTyped(state, EmitLdsBufferU64Pointer(state, low), ResourceKind::Lds, TypeScalarU64(state), next)); +} + template std::uint32_t EmitLdsAtomic32(SpirvEmitterState& state, const MemoryInfo& mem, TOperation&& operation) { if (mem.kind != ResourceKind::Lds || !state.requirements.ldsLock) return operation(); @@ -1145,7 +1197,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 7u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 8u), instruction, BdaAbi::FaultReason::Unaligned); }); if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU64(state, 0u); - if (kind == ResourceKind::Lds && !state.requirements.ldsLock) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); + if (kind == ResourceKind::Lds && !state.requirements.ldsLock && !state.requirements.ldsBufferInt64Atomics) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); MemoryInfo storage{}; storage.kind = kind; const auto resource = PrepareMemoryResourceAccess(state, storage); @@ -1155,6 +1207,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto opcode = inst.Opcode(); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&] { return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&] { + if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1387,6 +1440,12 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto low = EmitMemoryElementIndex(state, resource, rawIndex); const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { + if (state.requirements.ldsBufferInt64Atomics) { + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFaultWords(state, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), rawIndex, ConstantU32(state, 2u)), ConstantU32(state, 0u), ConstantU32(state, 8u), ConstantU32(state, inst.Flags().pc), BdaAbi::FaultReason::Unaligned); }); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); + return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); + } const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 763f1a1fa8..ef3e1ac42d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -265,6 +265,10 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.ldsBufferInt64Atomics) { + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + } if (state.requirements.imageInt64Atomics) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index accf51eb13..3ef049b0af 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1187,6 +1187,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); + EmitBdaAnchors(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7820a803c3..fa019cafce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -811,33 +811,39 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa if (!context.Memory(instruction).gpuDescriptor) { const auto storage = directAccess(context, instruction, bits == 64u); const auto byte = EmitAddU32(state, access.guest, storage.byteOffset); - const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); - EmitIfCondition(state, unaligned, [&] { - const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); - RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); - }); - StopBdaInvocationIf(state, unaligned); const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, bits == 64u ? 3u : 2u)); - return EmitValueOrDefaultIfCondition(state, both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), EmitMemoryElementInBounds(state, storage, index)), type, zero, [&] { + auto valid = EmitMemoryElementInBounds(state, storage, index); + if (bits == 64u) { + const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); + EmitIfCondition(state, unaligned, [&] { + const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); + RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); + }); + StopBdaInvocationIf(state, unaligned); + valid = both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), valid); + } + return EmitValueOrDefaultIfCondition(state, valid, type, zero, [&] { const auto pointer = EmitStorageBufferElementPointer(state, storage, index, bits == 64u ? TypeStorageBufferU64ElementPointer(state) : TypeStorageBufferElementPointer(state)); return operation(pointer); }); } - const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); - faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); - return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { + const auto atomicAt = [&](std::uint32_t guest) { const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, access.guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); const auto mapped = Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)); return EmitValueOrDefaultIfCondition(state, mapped, type, zero, [&] { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, scalar), pointer, physical); const auto result = operation(pointer); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, access.guest); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); return result; }); - }); + }; + if (bits == 32u) return atomicAt(Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, ~std::uint64_t{3u}))); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); + faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { return atomicAt(access.guest); }); }); }); } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index ffe74562b5..6f2e3a6366 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -136,7 +136,7 @@ flowchart LR A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders. -A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. +A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. When the selected columns would take a shader past `ShaderInfo::MaxImages` or `ShaderInfo::MaxSampledPairs`, `PlanTableColumns` plans the columns of one heap unselected again, the heap with the most selected columns first, until both fit. At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the column's distinct descriptor words, or a poison code) and the ranges it read. A selected column evaluates its offset and reads only that record, as the scalar loads would: dwords past the heap read zeros, an unmapped record or an offset that carries past 32 bits is poison, and an offset the walker cannot read faults the table instead of throwing. Its heap base moves to the record, so the shader's fault address and the written-heap check cover that record alone. Any other column narrows to the records its keys select, plus record 0, when every access has a key domain: a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`), or a value the SRT walker evaluates from user data, SRT reads and constants (which, for an image, selects its record before it could narrow). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table or key changes neither the SPIR-V nor the variant's disk key. diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 71dcccda64..a735fffdad 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -203,6 +203,12 @@ A class range covers a command together with its own barriers. In `VulkanDevice: - `shadow-publish`: copies of unit shadow slabs into the import, with their barriers (`recordCopies` in `UnitShadow.cpp`, reached from `PublishShadow`). Copied bytes. - `template-refresh`: the `vkCmdUpdateBuffer` updates of a cached dispatch's changed data words, as one range from the first update to the last (`ShaderResources::refreshData`, called from `recordDispatch`). Refreshed bytes. +## Guest clock scale + +Set `APS5_TIME_SCALE=` to scale `sceKernelGetProcessTime`, `sceKernelGetProcessTimeCounter` and `sceKernelReadTsc` from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). On Windows, `sceKernelClockGettime` monotonic clock ids are scaled too; on Linux they return the native host monotonic clock unchanged. A title's own `rdtsc` instruction and `sceKernelGetTscFrequency` are unscaled, so dividing scaled TSC deltas by the reported real frequency gives guest-time intervals. Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + +`APS5_PAD_INPUT_FILE` command durations and reload polling use `sceKernelGetProcessTime`, so they follow the scale. At `0.02`, a 1-second injected press lasts about 50 seconds of real time, and a new command can take up to 5 seconds to be noticed. + ## Driver thread priority Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal. diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7f9c37c9bf..b53549a30f 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -67,6 +67,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | +| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | 1 shows the performance overlay; 0 or unset hides it, and any other value is refused | +| `ANYPS5_OVERLAY_POS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | tl | the overlay's corner: tl, tr, bl or br | | `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | @@ -115,6 +117,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DRAW_KEY_ALL_USER_WORDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | includes all 32 user words in a draw's register key instead of the used count | | `APS5_DRAW_SNAPSHOT_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 64 | upper bound in MiB on the snapshot bytes a draw's bindings may take | | `APS5_DRAW_TRANSITIONS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps the per-draw upload and download barriers and layout transitions instead of the lean recorded path | +| `APS5_DRAW_UPLOAD_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 4096 | largest draw snapshot in bytes copied into the per-recorder upload chunk; 0 turns the chunk off | | `APS5_DUMP_COMPUTE_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | only captures compute pipelines whose SPIR-V has at least this many words | | `APS5_DUMP_COMPUTE_PIPELINES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | directory in which captured compute pipelines are written | | `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address whose draw's shaders are dumped | @@ -343,7 +346,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | -| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | +| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8 | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index e66bf9fcad..b337ab4c98 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -1,6 +1,6 @@ # Progress reporting -[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Progress workflow](../../.github/workflows/progress.yml) publishes them after pushes to `main`. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. +[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Pages workflow](../../.github/workflows/pages.yml) publishes them with the web converter after pushes to `main` that change the converter, the web page or these tools. The [Pages progress workflow](../../.github/workflows/pages-progress.yml) runs after pushes that change `core/libs/prx` or the opcode list, compares the generated `progress.json` with the deployed one and dispatches the Pages workflow only when they differ, so the history chart gains new days only when the progress changes. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. ## Library counts diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f692e267c0..6576a43b73 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -20,7 +20,7 @@ - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots, because Mesa's lowering exports one primitive, possibly null, per emitted vertex; whether the console's compiler does the same is not known, and triangle strips keep `- 2` per input. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` as for triangles. Line output counts as lines for the polygon offset decode, as on the vertex path. The lines are drawn 1 pixel wide with Vulkan's default line rasterization, like lines on the vertex path: `PA_SU_LINE_CNTL` is not read, and neither its width nor the console's line rules were measured. Points, rectangle lists, per-stream types and line strips from a vertex program on the mesh path still throw. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. Other locations of sample 0, absent location registers, points, lines, rectangles, alpha-to-coverage and conservative rasterization throw -- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. Points, lines, a depth target and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target. +- [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw @@ -66,13 +66,14 @@ - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. +- [8_8 UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) decode as `R8G8_UINT` with the same component-swap mapping as 8_8 UNORM/SNORM (`0xe4` / `0xec`). Follows the existing 8 and 8_8_8_8 UINT paths; not measured on the console. RetroArch (PPSA99169) uses it during menu startup. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. - [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. -- [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. +- [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline @@ -90,7 +91,7 @@ - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. -- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. @@ -143,6 +144,7 @@ - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. +- [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. - [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c). Blending into the reverse orders (`COMP_SWAP` 2 and 3) is covered by the reversed-order blending entry, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws @@ -165,16 +167,20 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. A selected column's range is its record and the bytes of the record before the descriptor. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled, and the other five slots stay zero. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. + +- [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index f7effa839a..519f8470dc 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -7,3 +7,7 @@ ### Unknown function info - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the HTTP proxy port (21) is 0; the Wi-Fi codes (5-10) throw + +### Functional + +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index 8076229ee3..e9f15ca299 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -12,3 +12,4 @@ - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests +- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 8a5caa007a..ce95581f16 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -11,6 +11,7 @@ - [sceVideoOutColorSpaceConversionSettingsSetDefaultSdrToHdrConversion_, sceVideoOutColorSpaceConversionSettingsSetSdrToHdrConversion_, sceVideoOutAdjustColorSpaceConversion_, sceVideoOutSetGlobalBlendSpace](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - PPSA30630 imports these to configure color space conversion and global blend space. Color space conversion settings are accepted with size validation (32 bytes) but conversion blocks and blend space parameters are not applied to the presentation swapchain - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it +- [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. - [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d3ac2a4d01..d787587580 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -49,6 +49,7 @@ - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. +- [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported @@ -67,6 +68,7 @@ - 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. - [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 @@ -77,11 +79,13 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx` +- [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount - [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. `gmtime` converts every time whose year fits an `int` on both hosts. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2eb1a7613f..b4adfa1c64 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -52,7 +52,7 @@ - [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 +- [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 - [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 - [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 @@ -125,7 +125,7 @@ - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. -- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer @@ -135,6 +135,7 @@ - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only +- [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) 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 @@ -208,3 +209,4 @@ - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 2a8365c5d3..9c5f5bbaa6 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -13,11 +13,13 @@ - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. +- In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, 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`. - 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. -- 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` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. +- 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 of the PLT; code after it is not scanned. +- 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, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or 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)). - 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, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. 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-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 diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 12e6133a6d..15a96b372e 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -33,7 +33,7 @@ - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. +- An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. @@ -62,8 +62,8 @@ - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. -- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. @@ -129,10 +129,11 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. -- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. +- [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index 882ce0a000..ea37776a37 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, `ToggleFullscreen` (F11), and `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)). `ToggleFullscreen` and `TogglePerformanceOverlay` accept only keyboard keys. ### Default bindings diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 27b1862066..5c1719fe3a 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -62,7 +62,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--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`. | | `--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`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | +| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | @@ -236,12 +236,35 @@ most called traced imports (1 calls in total): Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `PROFILED` | The first line while `APS5_PROFILE_GPU=1` or `APS5_PROFILE_DRAW=1` is set. Those switches add timestamp queries to every batch and timers to every packet, so the numbers of such a run aren't comparable with an unprofiled one. | +| `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. Every worker's time counts in full, so with several workers busy at once the line can exceed `FRAME`. | +| `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | +| `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | +| `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | +| `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | + +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. Counts are whole numbers, in thousands (`12K`) from 10000 and in millions (`3M`) from a million. Memory is in GB of 2^30 bytes. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. Each draw and dispatch packet and each pipeline creation is counted also while the overlay is hidden, with one increment of a counter that belongs to the packet worker or, for pipelines, a shared one; the memory and the counters are read only while the overlay is shown. + ### Windows crash diagnostics When a register points at readable memory that starts with at least four printable ASCII characters, either as UTF-16LE or as single bytes, the crash report prints that text after the register memory dump, as `[r13] u"..."` for UTF-16LE or `[rdi] "..."` for single bytes. The text ends at a NUL, at the first character that is not printable ASCII, a tab, a line feed or a carriage return, after 512 characters or at the end of readable memory. Tabs, line breaks, quotes and backslashes are escaped, and `...` after the closing quote means that no NUL ended the text within those limits. Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. +Set `APS5_CRASH_STACK_BYTES=` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. + ### GPU submission rings and stall dumps Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`.