From 8c8a97af655061ee2640a29247a29d41ae611a11 Mon Sep 17 00:00:00 2001 From: Ranieri Date: Fri, 26 Jun 2026 21:04:39 -0300 Subject: [PATCH] Feature/mpeg decoder (#120) * feat: added ffmepg as dependency * feat: wip decoder video * feat: some perf and cleanup * feat: added generic MPEG stream notification * feat: CMakeLists.txt in ps2xStudio to configure SDL2 build options for static linking. fix: fix ffmpeg setup for linux fix: now MPEG decoder now identify that movie has ended and can play again anytime feat: better audio stub to not block games * feat: fix expansion test * feat: foo * a * feat: finally added a helper to to prevent thread starvation * feat: added basic vu0 code execution * feat: added yield Guest Execution After Wake to prevent deadlock * feat: added options on cmake for logs feat: better input for keyboard pad * feat: small corrections like top and itop vu branches etc * feat: changes * feat: working feature * feat: fatal frame iop * feat: test fix feat: z buffer fix * fix: gix GsPutIMR IMR * feat: added rl imgui * feat: added helper to get snapshot * feat: added debug panel consuming snapshots * feat: added pad snapshot feat: added RCP debug events * feat: final cleanup from old code * feat: added EE timer counter feat: applyed sound driver for Lotr feat: better check for sound driver compat layout feat: enquee and cosumed DMa cause feat: added Pad execCMd feat: update GS vsync signal flag feat: custom IOPs for LotR * feat: small cleanups * fix: fix wrong import --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 2 +- CMakeSettings.json | 27 + ps2xRecomp/CMakeLists.txt | 9 +- ps2xRecomp/src/lib/code_generator.cpp | 146 +- ps2xRuntime/CMakeLists.txt | 243 +- ps2xRuntime/cmake/CopyFfmpegDlls.cmake | 24 + ps2xRuntime/include/ps2_debug_panel.h | 25 + ps2xRuntime/include/ps2_log.h | 122 +- ps2xRuntime/include/ps2_runtime.h | 158 +- ps2xRuntime/include/ps2_stubs.h | 26 - ps2xRuntime/include/runtime/ps2_gs_gpu.h | 108 + ps2xRuntime/include/runtime/ps2_iop.h | 3 + ps2xRuntime/include/runtime/ps2_iop_audio.h | 14 +- ps2xRuntime/include/runtime/ps2_iop_cl.h | 27 + ps2xRuntime/include/runtime/ps2_iop_dbcman.h | 13 +- ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h | 17 + ps2xRuntime/include/runtime/ps2_memory.h | 23 +- ps2xRuntime/include/runtime/ps2_vu1.h | 12 +- ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp | 60 +- ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp | 117 +- ps2xRuntime/src/lib/Kernel/Stubs/CD.h | 36 + ps2xRuntime/src/lib/Kernel/Stubs/Common.h | 1 + ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp | 257 +- .../src/lib/Kernel/Stubs/Helpers/Support.h | 105 +- ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp | 1944 +++++++++++++- ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h | 2 + .../src/lib/Kernel/Stubs/MemoryCard.cpp | 34 + ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.h | 33 + ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp | 144 +- ps2xRuntime/src/lib/Kernel/Stubs/Pad.h | 42 + ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp | 80 +- ps2xRuntime/src/lib/Kernel/Syscalls/Common.h | 9 + .../src/lib/Kernel/Syscalls/Helpers/Runtime.h | 95 +- .../src/lib/Kernel/Syscalls/Helpers/State.h | 43 +- .../src/lib/Kernel/Syscalls/Interrupt.cpp | 37 +- ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp | 340 ++- ps2xRuntime/src/lib/Kernel/Syscalls/Sync.cpp | 227 +- .../src/lib/Kernel/Syscalls/System.cpp | 106 +- .../src/lib/Kernel/Syscalls/Thread.cpp | 186 +- ps2xRuntime/src/lib/game_overrides.cpp | 17 +- ps2xRuntime/src/lib/ps2_debug_panel.cpp | 2246 +++++++++++++++++ ps2xRuntime/src/lib/ps2_gif_arbiter.cpp | 64 - ps2xRuntime/src/lib/ps2_gs_gpu.cpp | 277 +- ps2xRuntime/src/lib/ps2_iop.cpp | 84 +- ps2xRuntime/src/lib/ps2_iop_audio.cpp | 35 +- ps2xRuntime/src/lib/ps2_iop_cl.cpp | 514 ++++ ps2xRuntime/src/lib/ps2_iop_dbcman.cpp | 56 +- ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp | 325 +++ ps2xRuntime/src/lib/ps2_memory.cpp | 359 ++- ps2xRuntime/src/lib/ps2_pad.cpp | 8 +- ps2xRuntime/src/lib/ps2_runtime.cpp | 436 +++- ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp | 295 ++- ps2xRuntime/src/lib/ps2_vu1.cpp | 1322 +++++----- ps2xRuntime/src/main.cpp | 23 + ps2xStudio/CMakeLists.txt | 9 + ps2xTest/CMakeLists.txt | 4 + ps2xTest/src/pad_input_tests.cpp | 43 + ps2xTest/src/ps2_gs_tests.cpp | 8 +- ps2xTest/src/ps2_memory_tests.cpp | 144 +- ps2xTest/src/ps2_runtime_expansion_tests.cpp | 625 ++++- ps2xTest/src/ps2_runtime_interrupt_tests.cpp | 182 ++ ps2xTest/src/ps2_runtime_kernel_tests.cpp | 117 +- ps2xTest/src/ps2_sif_dma_tests.cpp | 126 + ps2xTest/src/ps2_sif_rpc_tests.cpp | 297 +++ 65 files changed, 10749 insertions(+), 1766 deletions(-) create mode 100644 CMakeSettings.json create mode 100644 ps2xRuntime/cmake/CopyFfmpegDlls.cmake create mode 100644 ps2xRuntime/include/ps2_debug_panel.h create mode 100644 ps2xRuntime/include/runtime/ps2_iop_cl.h create mode 100644 ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h create mode 100644 ps2xRuntime/src/lib/ps2_debug_panel.cpp create mode 100644 ps2xRuntime/src/lib/ps2_iop_cl.cpp create mode 100644 ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f433439..eb363c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update - sudo apt-get install -y ninja-build libgl-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev + sudo apt-get install -y ninja-build libgl-dev libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev pkg-config libavcodec-dev libavformat-dev libavutil-dev libswresample-dev libswscale-dev - name: Configure run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index f55bfb2..abe5bf5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VITASDK}) endif() endif() -project("PS2 Retro X") +project(PS2RetroX) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) diff --git a/CMakeSettings.json b/CMakeSettings.json new file mode 100644 index 0000000..0c5fbf9 --- /dev/null +++ b/CMakeSettings.json @@ -0,0 +1,27 @@ +{ + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "" + }, + { + "name": "x64-Release", + "generator": "Ninja", + "configurationType": "RelWithDebInfo", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "msvc_x64_x64" ], + "variables": [] + } + ] +} \ No newline at end of file diff --git a/ps2xRecomp/CMakeLists.txt b/ps2xRecomp/CMakeLists.txt index a053bb4..857102a 100644 --- a/ps2xRecomp/CMakeLists.txt +++ b/ps2xRecomp/CMakeLists.txt @@ -20,7 +20,7 @@ FetchContent_MakeAvailable(elfio) FetchContent_Declare( toml11 GIT_REPOSITORY https://github.com/ToruNiina/toml11.git - GIT_TAG master + GIT_TAG v4.4.0 ) FetchContent_MakeAvailable(toml11) @@ -38,13 +38,6 @@ FetchContent_Declare( GIT_SHALLOW TRUE ) -FetchContent_Declare( - libdwarf - GIT_REPOSITORY https://github.com/davea42/libdwarf-code.git - GIT_TAG v2.2.0 - GIT_SHALLOW TRUE -) - set(BUILD_DWARFDUMP OFF CACHE BOOL "" FORCE) set(BUILD_DWARFEXAMPLE OFF CACHE BOOL "" FORCE) set(BUILD_DWARFGEN OFF CACHE BOOL "" FORCE) diff --git a/ps2xRecomp/src/lib/code_generator.cpp b/ps2xRecomp/src/lib/code_generator.cpp index 9aceed1..f831895 100644 --- a/ps2xRecomp/src/lib/code_generator.cpp +++ b/ps2xRecomp/src/lib/code_generator.cpp @@ -65,6 +65,15 @@ namespace ps2recomp return literal; } + static std::string vuMaskExpr(uint8_t dest_mask) + { + return fmt::format("_mm_castsi128_ps(_mm_set_epi32({}, {}, {}, {}))", + (dest_mask & 0x1) ? -1 : 0, + (dest_mask & 0x2) ? -1 : 0, + (dest_mask & 0x4) ? -1 : 0, + (dest_mask & 0x8) ? -1 : 0); + } + static std::string sanitizeIdentifierBody(const std::string &name) { std::string sanitized; @@ -2532,9 +2541,31 @@ namespace ps2recomp inst.rt, inst.rt); } case VU0_S2_VMOVE: - return fmt::format("ctx->vu0_vf[{}] = ctx->vu0_vf[{}];", inst.rt, inst.rd); + { + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format( + "{{ __m128i mask = _mm_set_epi32({}, {}, {}, {}); " + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _mm_castsi128_ps(mask)); }}", + (dest_mask & 0x1) ? -1 : 0, + (dest_mask & 0x2) ? -1 : 0, + (dest_mask & 0x4) ? -1 : 0, + (dest_mask & 0x8) ? -1 : 0, + inst.rt, inst.rt, inst.rd); + } case VU0_S2_VMR32: - return fmt::format("ctx->vu0_vf[{}] = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,0,0,1));", inst.rt, inst.rd, inst.rd); + { + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format( + "{{ __m128 res = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(0,3,2,1)); " + "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", + inst.rd, inst.rd, + (dest_mask & 0x1) ? -1 : 0, + (dest_mask & 0x2) ? -1 : 0, + (dest_mask & 0x4) ? -1 : 0, + (dest_mask & 0x8) ? -1 : 0, + inst.rt, inst.rt); + } case VU0_S2_VCLIPw: { uint8_t field = inst.function & 0x3; @@ -3519,186 +3550,201 @@ namespace ps2recomp { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; + uint8_t dest_mask = inst.vectorInfo.vectorField; uint8_t field = inst.function & 0x3; std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); return fmt::format("{{ __m128 res = PS2_VADD(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " - "ctx->vu0_acc = res; }}", - vfs, vft, vft, shuffle_pattern); + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vft, shuffle_pattern, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VSUBA_Field(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; + uint8_t dest_mask = inst.vectorInfo.vectorField; uint8_t field = inst.function & 0x3; std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); return fmt::format("{{ __m128 res = PS2_VSUB(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " - "ctx->vu0_acc = res; }}", - vfs, vft, vft, shuffle_pattern); + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vft, shuffle_pattern, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMADDA_Field(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; + uint8_t dest_mask = inst.vectorInfo.vectorField; uint8_t field = inst.function & 0x3; std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); " - "ctx->vu0_acc = res; }}", - vfs, vft, vft, shuffle_pattern); + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vft, shuffle_pattern, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMSUBA_Field(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; + uint8_t dest_mask = inst.vectorInfo.vectorField; uint8_t field = inst.function & 0x3; std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " - "ctx->vu0_acc = res; }}", - vfs, vft, vft, shuffle_pattern); + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vft, shuffle_pattern, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMULA_Field(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; + uint8_t dest_mask = inst.vectorInfo.vectorField; uint8_t field = inst.function & 0x3; std::string shuffle_pattern = fmt::format("_MM_SHUFFLE({},{},{},{})", field, field, field, field); return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " - "ctx->vu0_acc = res; }}", - vfs, vft, vft, shuffle_pattern); + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vft, shuffle_pattern, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VADDA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("ctx->vu0_acc = PS2_VADD(ctx->vu0_vf[{}], ctx->vu0_vf[{}]);", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VADD(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VADDAq(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VADD(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VADD(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VADDAi(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VADD(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VADD(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VSUBA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("ctx->vu0_acc = PS2_VSUB(ctx->vu0_vf[{}], ctx->vu0_vf[{}]);", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VSUB(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VSUBAq(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VSUB(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VSUB(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VSUBAi(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VSUB(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VSUB(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMADDA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " - "ctx->vu0_acc = PS2_VADD(ctx->vu0_acc, mul_res); }}", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); __m128 res = PS2_VADD(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMADDAq(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); " - "ctx->vu0_acc = PS2_VADD(ctx->vu0_acc, mul_res); }}", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); __m128 res = PS2_VADD(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMADDAi(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " - "ctx->vu0_acc = PS2_VADD(ctx->vu0_acc, mul_res); }}", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); __m128 res = PS2_VADD(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMSUBA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " - "ctx->vu0_acc = PS2_VSUB(ctx->vu0_acc, mul_res); }}", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); __m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMSUBAq(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); " - "ctx->vu0_acc = PS2_VSUB(ctx->vu0_acc, mul_res); }}", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); __m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMSUBAi(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " - "ctx->vu0_acc = PS2_VSUB(ctx->vu0_acc, mul_res); }}", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); __m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMULA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("ctx->vu0_acc = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]);", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMULAq(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VMULAi(const Instruction &inst) { uint8_t vfs = inst.rd; - return fmt::format("ctx->vu0_acc = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i));", - vfs); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VOPMULA(const Instruction &inst) { uint8_t vfs = inst.rd; uint8_t vft = inst.rt; - return fmt::format("ctx->vu0_acc = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]);", - vfs, vft); + uint8_t dest_mask = inst.vectorInfo.vectorField; + return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vft, vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VITOF(const Instruction &inst, int shift) diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index ec58e9e..92f141b 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -9,12 +9,18 @@ option(PS2X_ENABLE_RUNNER_UNITY_BUILD "Build ps2EntryRunner with CMake unity bui set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 8 CACHE STRING "Unity build batch size for ps2EntryRunner") option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON) +option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF) +option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF) +option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON) + if(PS2X_ENABLE_SCCACHE) find_program(PS2X_SCCACHE_PROGRAM sccache) + if(PS2X_SCCACHE_PROGRAM) if(CMAKE_C_COMPILER) set(CMAKE_C_COMPILER_LAUNCHER "${PS2X_SCCACHE_PROGRAM}") endif() + set(CMAKE_CXX_COMPILER_LAUNCHER "${PS2X_SCCACHE_PROGRAM}") message(STATUS "Using sccache: ${PS2X_SCCACHE_PROGRAM}") else() @@ -25,8 +31,9 @@ endif() include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ReleaseMode.cmake") set(PS2X_IS_VITA OFF) + if((CMAKE_C_COMPILER MATCHES "arm-vita-eabi") OR - (CMAKE_CXX_COMPILER MATCHES "arm-vita-eabi")) + (CMAKE_CXX_COMPILER MATCHES "arm-vita-eabi")) set(PS2X_IS_VITA ON) endif() @@ -42,6 +49,7 @@ if(PS2X_IS_VITA) endif() set(PS2X_VITASDK "$ENV{VITASDK}") + if(NOT EXISTS "${PS2X_VITASDK}/share/vita.cmake") message(FATAL_ERROR "Could not find vita.cmake under ${PS2X_VITASDK}/share.") endif() @@ -60,11 +68,55 @@ else() GIT_PROGRESS TRUE ) FetchContent_MakeAvailable(raylib) + + FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG "docking" + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(imgui) + + if(NOT imgui_POPULATED) + FetchContent_Populate(imgui) + endif() + + set(PS2X_IMGUI_SOURCE_DIR "${imgui_SOURCE_DIR}") + + add_library(imgui STATIC + "${PS2X_IMGUI_SOURCE_DIR}/imgui.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_draw.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_tables.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_widgets.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_demo.cpp" + ) + target_include_directories(imgui PUBLIC "${PS2X_IMGUI_SOURCE_DIR}") + + FetchContent_Declare( + rlImGui + GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git + GIT_TAG "Raylib_5_5" + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(rlImGui) + + if(NOT rlimgui_POPULATED) + FetchContent_Populate(rlImGui) + endif() + + set(PS2X_RLIMGUI_SOURCE_DIR "${rlimgui_SOURCE_DIR}") + + add_library(rlImGui STATIC + "${PS2X_RLIMGUI_SOURCE_DIR}/rlImGui.cpp" + ) + target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}") + target_link_libraries(rlImGui PUBLIC raylib imgui) endif() add_library(ps2_host_backend INTERFACE) set(PS2X_DEFAULT_BOOT_ELF_DEFINE "") + if(PS2X_DEFAULT_BOOT_ELF) set(PS2X_DEFAULT_BOOT_ELF_DEFINE "${PS2X_DEFAULT_BOOT_ELF}") string(REPLACE "\\" "\\\\" PS2X_DEFAULT_BOOT_ELF_DEFINE "${PS2X_DEFAULT_BOOT_ELF_DEFINE}") @@ -107,13 +159,15 @@ if(PS2X_IS_VITA) set(PS2X_VITA_INCLUDE_DIRS "${PS2X_VITA_PREFIX}/include" ) + foreach(PS2X_VITA_OPTIONAL_INCLUDE_DIR IN ITEMS - "${PS2X_VITA_PREFIX}/include/raylib" - "${PS2X_VITA_PREFIX}/include/SDL2") + "${PS2X_VITA_PREFIX}/include/raylib" + "${PS2X_VITA_PREFIX}/include/SDL2") if(EXISTS "${PS2X_VITA_OPTIONAL_INCLUDE_DIR}") list(APPEND PS2X_VITA_INCLUDE_DIRS "${PS2X_VITA_OPTIONAL_INCLUDE_DIR}") endif() endforeach() + if(PS2X_VITA_EXTRA_INCLUDE_DIRS) list(APPEND PS2X_VITA_INCLUDE_DIRS ${PS2X_VITA_EXTRA_INCLUDE_DIRS}) endif() @@ -135,15 +189,18 @@ if(PS2X_IS_VITA) PATHS "${PS2X_VITA_LIBRARY_DIR}" NO_DEFAULT_PATH ) + if(NOT ${out_var}) message(FATAL_ERROR "Could not find Vita library '${library_name}' under ${PS2X_VITA_LIBRARY_DIR}. " "Install Quenom/raylib-5.5-vita and its SDL2/PVR dependencies into VitaSDK first.") endif() + set(${out_var} "${${out_var}}" PARENT_SCOPE) endfunction() set(PS2X_VITA_RESOLVED_LIBRARIES "") + foreach(PS2X_VITA_LIBRARY_NAME IN LISTS PS2X_VITA_REQUIRED_LIBRARY_NAMES) ps2x_find_vita_library(PS2X_VITA_LIBRARY_PATH "${PS2X_VITA_LIBRARY_NAME}") list(APPEND PS2X_VITA_RESOLVED_LIBRARIES "${PS2X_VITA_LIBRARY_PATH}") @@ -161,6 +218,128 @@ else() target_link_libraries(ps2_host_backend INTERFACE raylib) endif() +if(WIN32) + include(ExternalProject) + + set(FFMPEG_PREBUILT_TAG "n7.1-241205") + set(FFMPEG_PREBUILT_URL + "https://github.com/System233/ffmpeg-msvc-prebuilt/releases/download/${FFMPEG_PREBUILT_TAG}/ffmpeg-${FFMPEG_PREBUILT_TAG}-lgpl-amd64-shared.zip") + + set(FFMPEG_PREFIX_DIR "${CMAKE_BINARY_DIR}/ThirdParty/ffmpeg-prefix") + set(FFMPEG_SOURCE_DIR "${FFMPEG_PREFIX_DIR}/src/ffmpeg_external") + set(FFMPEG_INCLUDE_DIR "${FFMPEG_SOURCE_DIR}/include") + set(FFMPEG_LIB_DIR "${FFMPEG_SOURCE_DIR}/bin") + set(FFMPEG_BIN_DIR "${FFMPEG_SOURCE_DIR}/bin") + + ExternalProject_Add(ffmpeg_external + URL "${FFMPEG_PREBUILT_URL}" + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + PREFIX "${FFMPEG_PREFIX_DIR}" + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND "" + BUILD_BYPRODUCTS + "${FFMPEG_LIB_DIR}/avcodec.lib" + "${FFMPEG_LIB_DIR}/avformat.lib" + "${FFMPEG_LIB_DIR}/avutil.lib" + "${FFMPEG_LIB_DIR}/swresample.lib" + "${FFMPEG_LIB_DIR}/swscale.lib" + ) + + # Link against the import libraries shipped with the Windows DLLs. + add_library(ffmpeg_avcodec STATIC IMPORTED GLOBAL) + set_target_properties(ffmpeg_avcodec PROPERTIES + IMPORTED_LOCATION "${FFMPEG_LIB_DIR}/avcodec.lib" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_INCLUDE_DIR}" + ) + add_dependencies(ffmpeg_avcodec ffmpeg_external) + + add_library(ffmpeg_avformat STATIC IMPORTED GLOBAL) + set_target_properties(ffmpeg_avformat PROPERTIES + IMPORTED_LOCATION "${FFMPEG_LIB_DIR}/avformat.lib" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_INCLUDE_DIR}" + ) + add_dependencies(ffmpeg_avformat ffmpeg_external) + + add_library(ffmpeg_avutil STATIC IMPORTED GLOBAL) + set_target_properties(ffmpeg_avutil PROPERTIES + IMPORTED_LOCATION "${FFMPEG_LIB_DIR}/avutil.lib" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_INCLUDE_DIR}" + ) + add_dependencies(ffmpeg_avutil ffmpeg_external) + + add_library(ffmpeg_swresample STATIC IMPORTED GLOBAL) + set_target_properties(ffmpeg_swresample PROPERTIES + IMPORTED_LOCATION "${FFMPEG_LIB_DIR}/swresample.lib" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_INCLUDE_DIR}" + ) + add_dependencies(ffmpeg_swresample ffmpeg_external) + + add_library(ffmpeg_swscale STATIC IMPORTED GLOBAL) + set_target_properties(ffmpeg_swscale PROPERTIES + IMPORTED_LOCATION "${FFMPEG_LIB_DIR}/swscale.lib" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_INCLUDE_DIR}" + ) + add_dependencies(ffmpeg_swscale ffmpeg_external) + + add_library(ffmpeg INTERFACE) + target_link_libraries(ffmpeg INTERFACE + ffmpeg_avcodec + ffmpeg_avformat + ffmpeg_avutil + ffmpeg_swresample + ffmpeg_swscale + bcrypt + secur32 + ws2_32 + user32 + advapi32 + ole32 + shell32 + ) + + set(PS2X_FFMPEG_BIN_DIR "${FFMPEG_BIN_DIR}" CACHE INTERNAL "Directory containing FFmpeg runtime DLLs") + set(PS2X_COPY_FFMPEG_DLLS_SCRIPT + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/CopyFfmpegDlls.cmake" + CACHE INTERNAL + "Script used to stage FFmpeg runtime DLLs beside executables") +else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET GLOBAL + libavcodec + libavformat + libavutil + libswresample + libswscale + ) + + add_library(ffmpeg INTERFACE) + target_link_libraries(ffmpeg INTERFACE PkgConfig::FFMPEG) +endif() + +function(ps2x_stage_ffmpeg_runtime_dlls target_name) + if(NOT WIN32) + return() + endif() + + if(NOT PS2X_FFMPEG_BIN_DIR OR NOT PS2X_COPY_FFMPEG_DLLS_SCRIPT) + return() + endif() + + if(TARGET ffmpeg_external) + add_dependencies(${target_name} ffmpeg_external) + endif() + + add_custom_command( + TARGET ${target_name} POST_BUILD + COMMAND "${CMAKE_COMMAND}" + "-Dsource_dir=${PS2X_FFMPEG_BIN_DIR}" + "-Ddest_dir=$" + -P "${PS2X_COPY_FFMPEG_DLLS_SCRIPT}" + VERBATIM + ) +endfunction() + add_library(ps2_runtime STATIC src/lib/game_overrides.cpp src/lib/ps2_gif_arbiter.cpp @@ -172,6 +351,8 @@ add_library(ps2_runtime STATIC src/lib/ps2_iop.cpp src/lib/ps2_iop_audio.cpp src/lib/ps2_iop_dbcman.cpp + src/lib/ps2_iop_sdrdrv.cpp + src/lib/ps2_iop_cl.cpp src/lib/ps2_memory.cpp src/lib/ps2_pad.cpp src/lib/ps2_runtime.cpp @@ -180,6 +361,18 @@ add_library(ps2_runtime STATIC src/lib/games_database.cpp ) +if(PS2X_ENABLE_RUNTIME_LOGS) + target_compile_definitions(ps2_runtime PUBLIC + PS2_RUNTIME_LOGS=1 + ) +endif() + +if(PS2X_ENABLE_AGRESSIVE_LOGS) + target_compile_definitions(ps2_runtime PUBLIC + AGRESSIVE_LOGS=1 + ) +endif() + file(GLOB_RECURSE KERNEL_SRC_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/lib/Kernel/*.cpp" ) @@ -227,12 +420,22 @@ target_include_directories(ps2_runtime PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src/lib/Kernel ) -target_link_libraries(ps2_runtime PUBLIC ps2_host_backend) +target_link_libraries(ps2_runtime PUBLIC ps2_host_backend ffmpeg) target_link_libraries(ps2EntryRunner PRIVATE ps2_runtime ) +if(NOT PS2X_IS_VITA) + target_sources(ps2EntryRunner PRIVATE + src/lib/ps2_debug_panel.cpp + ) + target_link_libraries(ps2EntryRunner PRIVATE rlImGui imgui) + target_compile_definitions(ps2EntryRunner PRIVATE PS2X_ENABLE_DEBUG_UI=1) +endif() + +ps2x_stage_ffmpeg_runtime_dlls(ps2EntryRunner) + if(PS2X_IS_VITA) set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d PARENTAL_LEVEL=1") @@ -249,17 +452,18 @@ if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI EnableFastReleaseMode(ps2_runtime) EnableFastReleaseMode(ps2EntryRunner) - if(MSVC AND WIN32) - target_link_options(ps2EntryRunner PRIVATE - $<$,$>:/SUBSYSTEM:WINDOWS> - $<$,$>:/ENTRY:mainCRTStartup> - ) - elseif(MINGW AND WIN32) - target_link_options(ps2EntryRunner PRIVATE - $<$,$>:-mwindows> - ) + if(WIN32 AND NOT PS2X_SHOW_WINDOWS_CONSOLE) + if(MSVC) + target_link_options(ps2EntryRunner PRIVATE + $<$,$>:/SUBSYSTEM:WINDOWS> + $<$,$>:/ENTRY:mainCRTStartup> + ) + elseif(MINGW) + target_link_options(ps2EntryRunner PRIVATE + $<$,$>:-mwindows> + ) + endif() endif() - endif() # Work around WinAPI vs raylib symbol clash for CloseWindow on x64 @@ -267,11 +471,20 @@ if(MSVC) target_link_options(ps2EntryRunner PRIVATE "/FORCE:MULTIPLE") endif() -install(TARGETS ps2_runtime +install(TARGETS ps2_runtime ps2EntryRunner + RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib ) +if(WIN32) + install(DIRECTORY "${FFMPEG_BIN_DIR}/" + DESTINATION bin + FILES_MATCHING + PATTERN "*.dll" + ) +endif() + install(DIRECTORY include/ DESTINATION include ) diff --git a/ps2xRuntime/cmake/CopyFfmpegDlls.cmake b/ps2xRuntime/cmake/CopyFfmpegDlls.cmake new file mode 100644 index 0000000..4a45275 --- /dev/null +++ b/ps2xRuntime/cmake/CopyFfmpegDlls.cmake @@ -0,0 +1,24 @@ +if(NOT DEFINED source_dir OR source_dir STREQUAL "") + message(FATAL_ERROR "CopyFfmpegDlls.cmake requires -Dsource_dir=") +endif() + +if(NOT DEFINED dest_dir OR dest_dir STREQUAL "") + message(FATAL_ERROR "CopyFfmpegDlls.cmake requires -Ddest_dir=") +endif() + +file(GLOB ffmpeg_runtime_dlls "${source_dir}/*.dll") +if(NOT ffmpeg_runtime_dlls) + message(FATAL_ERROR "No FFmpeg runtime DLLs found in '${source_dir}'") +endif() + +file(MAKE_DIRECTORY "${dest_dir}") + +foreach(ffmpeg_runtime_dll IN LISTS ffmpeg_runtime_dlls) + execute_process( + COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${ffmpeg_runtime_dll}" "${dest_dir}" + RESULT_VARIABLE copy_result + ) + if(NOT copy_result EQUAL 0) + message(FATAL_ERROR "Failed to copy '${ffmpeg_runtime_dll}' to '${dest_dir}'") + endif() +endforeach() diff --git a/ps2xRuntime/include/ps2_debug_panel.h b/ps2xRuntime/include/ps2_debug_panel.h new file mode 100644 index 0000000..b9933d5 --- /dev/null +++ b/ps2xRuntime/include/ps2_debug_panel.h @@ -0,0 +1,25 @@ +#ifndef PS2_DEBUG_PANEL_H +#define PS2_DEBUG_PANEL_H + +class PS2Runtime; + +class PS2DebugPanel +{ +public: + void initialize(); + void shutdown(); + void draw(PS2Runtime &runtime); + + bool isVisible() const { return m_visible; } + void setVisible(bool visible) { m_visible = visible; } + void toggleVisible() { m_visible = !m_visible; } + +private: + bool m_initialized = false; + bool m_visible = true; + bool m_showRegisters = true; + unsigned int m_memoryAddress = 0x00100000u; + unsigned int m_memoryBytes = 0x100u; +}; + +#endif // PS2_DEBUG_PANEL_H diff --git a/ps2xRuntime/include/ps2_log.h b/ps2xRuntime/include/ps2_log.h index ef32c8f..395b44d 100644 --- a/ps2xRuntime/include/ps2_log.h +++ b/ps2xRuntime/include/ps2_log.h @@ -1,9 +1,19 @@ #ifndef PS2_LOG_H #define PS2_LOG_H +#include +#include +#include #include #include -#include +#include +#include +#include +#include + +#ifndef PS2_RUNTIME_LOGS +#define PS2_RUNTIME_LOGS 1 +#endif #if defined(AGRESSIVE_LOGS) #define PS2_AGRESSIVE_LOGS_ENABLED 1 @@ -11,13 +21,113 @@ #define PS2_AGRESSIVE_LOGS_ENABLED 0 #endif -#if defined(_DEBUG) -#define RUNTIME_LOG(x) do { std::cout << x; } while (0) +namespace ps2_log +{ +struct RuntimeLogEntry +{ + uint64_t seq = 0; + std::string text; +}; + +inline constexpr size_t kMaxRuntimeLogEntries = 4096; + +inline std::mutex &runtime_log_mutex() +{ + static std::mutex m; + return m; +} + +inline std::deque &runtime_log_entries() +{ + static std::deque entries; + return entries; +} + +inline uint64_t &runtime_log_next_seq() +{ + static uint64_t seq = 1; + return seq; +} + +inline bool &runtime_log_paused() +{ + static bool paused = false; + return paused; +} + +inline void set_runtime_log_paused(bool paused) +{ + std::lock_guard lock(runtime_log_mutex()); + runtime_log_paused() = paused; +} + +inline bool is_runtime_log_paused() +{ + std::lock_guard lock(runtime_log_mutex()); + return runtime_log_paused(); +} + +inline void append_runtime_log_text(const std::string &text) +{ + if (text.empty()) + { + return; + } + + std::lock_guard lock(runtime_log_mutex()); + if (runtime_log_paused()) + { + return; + } + + auto &entries = runtime_log_entries(); + RuntimeLogEntry entry{}; + entry.seq = runtime_log_next_seq()++; + entry.text = text; + entries.push_back(std::move(entry)); + + while (entries.size() > kMaxRuntimeLogEntries) + { + entries.pop_front(); + } +} + +inline std::vector snapshot_runtime_log_entries() +{ + std::lock_guard lock(runtime_log_mutex()); + const auto &entries = runtime_log_entries(); + return std::vector(entries.begin(), entries.end()); +} + +inline void clear_runtime_log_entries() +{ + std::lock_guard lock(runtime_log_mutex()); + runtime_log_entries().clear(); +} +} + +#if defined(PS2_RUNTIME_LOGS) || defined(AGRESSIVE_LOGS) +#define RUNTIME_LOG(x) \ + do \ + { \ + std::ostringstream _ps2_runtime_log_stream; \ + _ps2_runtime_log_stream << x; \ + const std::string _ps2_runtime_log_text = _ps2_runtime_log_stream.str(); \ + if (_ps2_runtime_log_text.empty()) \ + { \ + std::cout.flush(); \ + } \ + else \ + { \ + std::cout << _ps2_runtime_log_text; \ + ps2_log::append_runtime_log_text(_ps2_runtime_log_text); \ + } \ + } while (0) #else #define RUNTIME_LOG(x) do {} while(0) #endif -#ifdef neverDone +#ifdef AGRESSIVE_LOGS namespace ps2_log { @@ -82,6 +192,10 @@ inline void print_saved_location() namespace ps2_log { inline constexpr bool agressive_logs_enabled = false; +inline std::string log_path() +{ + return (std::filesystem::current_path() / "ps2_log.txt").string(); +} inline void print_saved_location() {} } #define PS_LOG_ENTRY(name) ((void)0) diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 4c4f051..a64c5f6 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -206,45 +207,12 @@ inline void setReturnU64(R5900Context *ctx, uint64_t value) inline constexpr uint32_t PS2_PATH_WATCH_ADDR = 0x01EFFFA0u; inline constexpr uint32_t PS2_PATH_WATCH_BYTES = 0x200u; -inline constexpr uint32_t PS2_PATH_WATCH_MAX_LOGS = 4096u; -inline std::atomic g_ps2PathWatchLogCount{0}; inline uint32_t ps2PathWatchPhysAddr() { return PS2_PATH_WATCH_ADDR & PS2_RAM_MASK; } -inline bool ps2PathWatchIntersects(uint32_t writeAddr, uint32_t writeSize) -{ - const uint64_t writeStart = writeAddr; - const uint64_t writeEnd = writeStart + static_cast(writeSize); - const uint64_t watchStart = ps2PathWatchPhysAddr(); - const uint64_t watchEnd = watchStart + static_cast(PS2_PATH_WATCH_BYTES); - return writeEnd > watchStart && writeStart < watchEnd; -} - -inline void ps2PathWatchDumpPrefix(const uint8_t *rdram) -{ - if (!rdram) - { - return; - } - - const uint32_t base = ps2PathWatchPhysAddr(); - auto flags = std::cout.flags(); - std::cout << " buf=" << std::hex; - for (uint32_t i = 0; i < 16u; ++i) - { - const uint32_t addr = (base + i) & PS2_RAM_MASK; - std::cout << static_cast(rdram[addr]); - if (i + 1u < 16u) - { - std::cout << '.'; - } - } - std::cout.flags(flags); -} - inline uint8_t ps2PathWatchExtractByteFromWrite(uint32_t writeAddr, uint32_t watchAddr, uint64_t valueLo, uint64_t valueHi) { const uint32_t byteIndex = watchAddr - writeAddr; @@ -263,57 +231,14 @@ inline void ps2TraceGuestWrite(uint8_t *rdram, const char *op, const R5900Context *ctx) { - if (!rdram || size == 0u) - { - return; - } - - const uint32_t writeAddr = guestAddr & PS2_RAM_MASK; - if (!ps2PathWatchIntersects(writeAddr, size)) - { - return; - } - - const uint32_t logIndex = g_ps2PathWatchLogCount.fetch_add(1, std::memory_order_relaxed); - if (logIndex >= PS2_PATH_WATCH_MAX_LOGS) - { - return; - } - - const uint32_t watchAddr = ps2PathWatchPhysAddr(); - const bool touchesFirstByte = (watchAddr >= writeAddr) && (watchAddr < writeAddr + size); - const uint8_t oldByte = rdram[watchAddr]; - const uint8_t newByte = touchesFirstByte ? ps2PathWatchExtractByteFromWrite(writeAddr, watchAddr, valueLo, valueHi) : oldByte; - - const uint32_t pc = ctx ? ctx->pc : 0u; - const uint32_t ra = ctx ? static_cast(_mm_extract_epi32(ctx->r[31], 0)) : 0u; - const uint32_t sp = ctx ? static_cast(_mm_extract_epi32(ctx->r[29], 0)) : 0u; - - auto flags = std::cout.flags(); - std::cout << "[watch:path-write] #" << (logIndex + 1u) - << " op=" << op - << " addr=0x" << std::hex << writeAddr - << " size=0x" << size - << " pc=0x" << pc - << " ra=0x" << ra - << " sp=0x" << sp - << " vLo=0x" << valueLo; - if (size > 8u) - { - std::cout << " vHi=0x" << valueHi; - } - if (touchesFirstByte) - { - std::cout << " firstByte:" << static_cast(oldByte) - << "->" << static_cast(newByte); - if (oldByte != 0u && newByte == 0u) - { - std::cout << " (ZEROED)"; - } - } - ps2PathWatchDumpPrefix(rdram); - std::cout.flags(flags); - std::cout << std::endl; + (void)rdram; + (void)guestAddr; + (void)size; + (void)valueLo; + (void)valueHi; + (void)op; + (void)ctx; + // TODO we dont need this anymore so on next release it will be deleted } inline void ps2TraceGuestRangeWrite(uint8_t *rdram, @@ -322,40 +247,12 @@ inline void ps2TraceGuestRangeWrite(uint8_t *rdram, const char *op, const R5900Context *ctx) { - if (!rdram || size == 0u) - { - return; - } - - const uint32_t writeAddr = guestAddr & PS2_RAM_MASK; - if (!ps2PathWatchIntersects(writeAddr, size)) - { - return; - } - - const uint32_t logIndex = g_ps2PathWatchLogCount.fetch_add(1, std::memory_order_relaxed); - if (logIndex >= PS2_PATH_WATCH_MAX_LOGS) - { - return; - } - - const uint32_t pc = ctx ? ctx->pc : 0u; - const uint32_t ra = ctx ? static_cast(_mm_extract_epi32(ctx->r[31], 0)) : 0u; - const uint32_t sp = ctx ? static_cast(_mm_extract_epi32(ctx->r[29], 0)) : 0u; - const uint8_t firstByte = rdram[ps2PathWatchPhysAddr()]; - - auto flags = std::cout.flags(); - std::cout << "[watch:path-range] #" << (logIndex + 1u) - << " op=" << op - << " addr=0x" << std::hex << writeAddr - << " size=0x" << size - << " pc=0x" << pc - << " ra=0x" << ra - << " sp=0x" << sp - << " firstByte=" << static_cast(firstByte); - ps2PathWatchDumpPrefix(rdram); - std::cout.flags(flags); - std::cout << std::endl; + (void)rdram; + (void)guestAddr; + (void)size; + (void)op; + (void)ctx; + // TODO we dont need this anymore so on next release it will be deleted } struct PS2SoundDriverCompatLayout @@ -451,6 +348,12 @@ public: bool loadELF(const std::string &elfPath); void run(); + using DebugUiCallback = void (*)(PS2Runtime &runtime, void *userData); + void setDebugUiCallbacks(DebugUiCallback initCallback, + DebugUiCallback drawCallback, + DebugUiCallback shutdownCallback, + void *userData); + using RecompiledFunction = void (*)(uint8_t *, R5900Context *, PS2Runtime *); class GuestExecutionScope @@ -514,7 +417,9 @@ public: uint32_t guestHeapLimit() const; uint32_t reserveAsyncCallbackStack(uint32_t size, uint32_t alignment = 16u); void dispatchLoop(uint8_t *rdram, R5900Context *ctx); + void drainCompletedDmacHandlers(uint8_t *rdram); bool shouldPreemptGuestExecution(); + void yieldGuestExecutionAfterWake(); void requestStop(); bool isStopRequested() const; uint32_t guestExecutionWaiterCountForTesting() const @@ -551,7 +456,7 @@ public: return true; if (inRange(physAddr, PS2_GS_PRIV_REG_BASE, PS2_GS_PRIV_REG_SIZE)) return true; - if (physAddr >= PS2_VU0_CODE_BASE && physAddr < (PS2_VU1_DATA_BASE + PS2_VU1_DATA_SIZE)) + if (physAddr >= PS2_VU0_DATA_BASE && physAddr < (PS2_VU1_CODE_BASE + PS2_VU1_CODE_SIZE)) return true; return false; }; @@ -576,6 +481,8 @@ public: inline const GS &gs() const { return m_gs; } inline GifArbiter &gifArbiter() { return m_gifArbiter; } inline const GifArbiter &gifArbiter() const { return m_gifArbiter; } + inline VU1Interpreter &vu0() { return m_vu0; } + inline const VU1Interpreter &vu0() const { return m_vu0; } inline VU1Interpreter &vu1() { return m_vu1; } inline const VU1Interpreter &vu1() const { return m_vu1; } @@ -609,6 +516,7 @@ private: void leaveGuestExecution(); uint32_t releaseGuestExecution(); void reacquireGuestExecution(uint32_t depth); + void markGuestExecutionAcquired(); void HandleIntegerOverflow(R5900Context *ctx); @@ -622,10 +530,14 @@ private: ps2_iop m_iop; PS2AudioBackend m_audioBackend; PSPadBackend m_padBackend; + VU1Interpreter m_vu0; VU1Interpreter m_vu1; R5900Context m_cpuContext; mutable std::recursive_mutex m_guestExecutionMutex; mutable std::atomic m_guestExecutionWaiters{0u}; + mutable std::mutex m_guestExecutionHandoffMutex; + mutable std::condition_variable m_guestExecutionHandoffCv; + std::atomic m_guestExecutionHandoffEpoch{0u}; mutable std::mutex m_guestHeapMutex; mutable std::mutex m_asyncCallbackStackMutex; std::vector m_guestHeapBlocks; @@ -639,13 +551,19 @@ private: std::unordered_map m_functionTable; std::atomic m_stopRequested{false}; + DebugUiCallback m_debugUiInitCallback = nullptr; + DebugUiCallback m_debugUiDrawCallback = nullptr; + DebugUiCallback m_debugUiShutdownCallback = nullptr; + void *m_debugUiUserData = nullptr; + bool m_debugUiInitialized = false; - // TODO remove this later +public: std::atomic m_debugPc{0}; std::atomic m_debugRa{0}; std::atomic m_debugSp{0}; std::atomic m_debugGp{0}; +private: struct LoadedModule { std::string name; diff --git a/ps2xRuntime/include/ps2_stubs.h b/ps2xRuntime/include/ps2_stubs.h index a99beca..4492ce1 100644 --- a/ps2xRuntime/include/ps2_stubs.h +++ b/ps2xRuntime/include/ps2_stubs.h @@ -8,29 +8,6 @@ class PS2Runtime; #include "runtime/ps2_memory.h" #include "Stubs/Unimplemented.h" -struct PS2MpegCompatLayout -{ - uint32_t mpegObjectAddr = 0; - uint32_t videoStateAddr = 0; - uint32_t movieStateAddr = 0; - uint32_t syntheticFramesBeforeEnd = 1u; - uint32_t playingVideoStateValue = 0u; - uint32_t playingMovieStateValue = 2u; - uint32_t finishedVideoStateValue = 3u; - uint32_t finishedMovieStateValue = 3u; - - [[nodiscard]] bool matchesMpegObject(uint32_t addr) const - { - return mpegObjectAddr != 0u && ((addr & PS2_RAM_MASK) == (mpegObjectAddr & PS2_RAM_MASK)); - } - - [[nodiscard]] bool hasFinishTargets() const - { - return videoStateAddr != 0u || movieStateAddr != 0u; - } -}; - - namespace ps2_stubs { #define PS2_DECLARE_STUB(name) void name(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); @@ -38,7 +15,4 @@ namespace ps2_stubs #undef PS2_DECLARE_STUB void resetSifState(); - - void setMpegCompatLayout(const PS2MpegCompatLayout &layout); - void clearMpegCompatLayout(); } diff --git a/ps2xRuntime/include/runtime/ps2_gs_gpu.h b/ps2xRuntime/include/runtime/ps2_gs_gpu.h index c63f88d..6991524 100644 --- a/ps2xRuntime/include/runtime/ps2_gs_gpu.h +++ b/ps2xRuntime/include/runtime/ps2_gs_gpu.h @@ -1,6 +1,8 @@ #ifndef PS2_GS_GPU_H #define PS2_GS_GPU_H +#include +#include #include #include #include @@ -223,6 +225,88 @@ struct GSTrxReg uint16_t rrw, rrh; }; +struct GSDebugSnapshot +{ + GSContext ctx[2]{}; + GSPrimReg prim{}; + GSTexaReg texa{}; + GSTexClutReg texclut{}; + GSBitBltBuf bitbltbuf{}; + GSTrxPos trxpos{}; + GSTrxReg trxreg{}; + uint32_t trxdir = 0; + uint32_t transferX = 0; + uint32_t transferY = 0; + uint32_t transferTotalPixels = 0; + uint32_t transferCopiedPixels = 0; + uint32_t lastDisplayBaseBytes = 0; + GSFrameReg preferredDisplaySourceFrame{}; + uint32_t preferredDisplayDestFbp = 0; + bool hasPreferredDisplaySource = false; + uint32_t hostPresentationWidth = 0; + uint32_t hostPresentationHeight = 0; + uint32_t hostPresentationDisplayFbp = 0; + uint32_t hostPresentationSourceFbp = 0; + bool hostPresentationUsedPreferred = false; + bool hasHostPresentationFrame = false; + size_t localToHostPendingBytes = 0; +}; + +enum class GSDebugEventKind : uint8_t +{ + GifTag = 0, + Register = 1, + Draw = 2, + Transfer = 3, + Present = 4, +}; + +struct GSDebugHistoryEntry +{ + uint64_t seq = 0; + uint64_t vsyncTick = 0; + uint32_t frameIndex = 0; + GSDebugEventKind kind = GSDebugEventKind::Register; + + uint8_t reg = 0; + uint64_t regValue = 0; + + uint32_t gifSizeBytes = 0; + uint32_t gifNloop = 0; + uint8_t gifFlg = 0; + uint8_t gifNreg = 0; + + GSPrimReg prim{}; + GSFrameReg frame{}; + GSZbufReg zbuf{}; + GSTex0Reg tex0{}; + GSScissorReg scissor{}; + uint64_t test = 0; + uint64_t alpha = 0; + + uint32_t vertexCount = 0; + float xMin = 0.0f; + float xMax = 0.0f; + float yMin = 0.0f; + float yMax = 0.0f; + double zMin = 0.0; + double zMax = 0.0; + uint8_t aMin = 0; + uint8_t aMax = 0; + + GSBitBltBuf bitbltbuf{}; + GSTrxPos trxpos{}; + GSTrxReg trxreg{}; + uint32_t trxdir = 0; + uint32_t transferPixels = 0; + + uint32_t displayFbp = 0; + uint32_t sourceFbp = 0; + uint32_t width = 0; + uint32_t height = 0; + bool usedPreferred = false; +}; + class GSRasterizer; class GS @@ -246,8 +330,14 @@ public: { return m_ctx[(index != 0) ? 1 : 0].frame; } + GSDebugSnapshot getDebugSnapshot() const; + std::vector getDebugHistory() const; + void clearDebugHistory(); + bool isDebugHistoryPaused() const; + void setDebugHistoryPaused(bool paused); bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const; void latchHostPresentationFrame(); + bool tryLatchHostPresentationFrame(); bool copyLatchedHostPresentationFrame(std::vector &outPixels, uint32_t &outWidth, uint32_t &outHeight, @@ -268,6 +358,15 @@ private: void snapshotVRAM(); void writeRegisterPacked(uint8_t regDesc, uint64_t lo, uint64_t hi); void vertexKick(bool drawing); + void latchHostPresentationFrameUnlocked(); + + void recordDebugEventUnlocked(GSDebugHistoryEntry entry); + GSDebugHistoryEntry makeDebugEventUnlocked(GSDebugEventKind kind) const; + void recordGifTagDebugEventUnlocked(uint32_t sizeBytes, uint32_t nloop, uint8_t flg, uint32_t nreg); + void recordRegisterDebugEventUnlocked(uint8_t regAddr, uint64_t value); + void recordDrawDebugEventUnlocked(int vertexCount); + void recordTransferDebugEventUnlocked(); + void recordPresentDebugEventUnlocked(uint32_t displayFbp, uint32_t sourceFbp, uint32_t width, uint32_t height, bool usedPreferred); void processImageData(const uint8_t *data, uint32_t sizeBytes); void performLocalToLocalTransfer(); @@ -338,6 +437,15 @@ private: std::vector m_localToHostBuffer; size_t m_localToHostReadPos = 0; + static constexpr size_t kDebugHistoryCapacity = 512; + std::array m_debugHistory{}; + size_t m_debugHistoryWrite = 0; + size_t m_debugHistoryCount = 0; + uint64_t m_debugNextSeq = 1; + uint32_t m_debugFrameIndex = 0; + uint64_t m_debugLastVsyncTick = UINT64_MAX; + bool m_debugHistoryPaused = false; + GSRasterizer m_rasterizer; using WriteVramFunc = std::function; diff --git a/ps2xRuntime/include/runtime/ps2_iop.h b/ps2xRuntime/include/runtime/ps2_iop.h index 5f78d3d..e3fda43 100644 --- a/ps2xRuntime/include/runtime/ps2_iop.h +++ b/ps2xRuntime/include/runtime/ps2_iop.h @@ -7,7 +7,10 @@ class PS2Runtime; constexpr uint32_t IOP_SID_SNDDRV_COMMAND = 0x00000000u; constexpr uint32_t IOP_SID_SNDDRV_STATE = 0x00000001u; +constexpr uint32_t IOP_SID_LOTR_CLFILE = 0x0000FF01u; +constexpr uint32_t IOP_SID_LOTR_SOUND = 0x00012345u; constexpr uint32_t IOP_SID_LIBSD = 0x80000701u; +constexpr uint32_t IOP_SID_FATAL_FRAME_SDRDRV = 0x19740512u; constexpr uint32_t IOP_RPC_SNDDRV_SUBMIT = 0x00000000u; constexpr uint32_t IOP_RPC_SNDDRV_GET_STATUS_ADDR = 0x00000012u; diff --git a/ps2xRuntime/include/runtime/ps2_iop_audio.h b/ps2xRuntime/include/runtime/ps2_iop_audio.h index bb54d31..4629528 100644 --- a/ps2xRuntime/include/runtime/ps2_iop_audio.h +++ b/ps2xRuntime/include/runtime/ps2_iop_audio.h @@ -7,9 +7,17 @@ class PS2Runtime; namespace ps2_iop_audio { -void handleLibSdRpc(PS2Runtime *runtime, uint32_t sid, uint32_t rpcNum, - const uint8_t *sendBuf, uint32_t sendSize, - uint8_t *recvBuf, uint32_t recvSize); + void reset(); + + bool handleLibSdRpc(uint8_t* rdram, + PS2Runtime* runtime, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t& resultPtr); } #endif diff --git a/ps2xRuntime/include/runtime/ps2_iop_cl.h b/ps2xRuntime/include/runtime/ps2_iop_cl.h new file mode 100644 index 0000000..8e344d1 --- /dev/null +++ b/ps2xRuntime/include/runtime/ps2_iop_cl.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +namespace ps2_iop_cl +{ + void reset(); + + bool handleClFileRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr); + + bool handleSoundRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr, + bool &signalNowaitCompletion); +} diff --git a/ps2xRuntime/include/runtime/ps2_iop_dbcman.h b/ps2xRuntime/include/runtime/ps2_iop_dbcman.h index 6d1aa48..4becf40 100644 --- a/ps2xRuntime/include/runtime/ps2_iop_dbcman.h +++ b/ps2xRuntime/include/runtime/ps2_iop_dbcman.h @@ -2,13 +2,16 @@ #include -class PS2Runtime; - namespace ps2_iop_dbcman { + void reset(); + bool handleDbcManRpc(uint8_t *rdram, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBufAddr, uint32_t sendSize, - uint32_t recvBufAddr, uint32_t recvSize, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, uint32_t &resultPtr); } diff --git a/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h b/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h new file mode 100644 index 0000000..84fa2a2 --- /dev/null +++ b/ps2xRuntime/include/runtime/ps2_iop_sdrdrv.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace ps2_iop_sdrdrv +{ + void reset(); + + bool handleSdrdrvRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr); +} diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index 6dfc4cc..79495e8 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "ps2_gif_arbiter.h" #if defined(_MSC_VER) @@ -303,15 +304,19 @@ public: void setGifPacketCallback(GifPacketCallback cb) { m_gifPacketCallback = std::move(cb); } void setGifArbiter(GifArbiter *arbiter) { m_gifArbiter = arbiter; } - using Vu1MscalCallback = std::function; + using Vu1MscalCallback = std::function; void setVu1MscalCallback(Vu1MscalCallback cb) { m_vu1MscalCallback = std::move(cb); } - using Vu1MscntCallback = std::function; + using Vu1MscntCallback = std::function; void setVu1MscntCallback(Vu1MscntCallback cb) { m_vu1MscntCallback = std::move(cb); } uint8_t *getVU1Code() { return m_vu1Code; } const uint8_t *getVU1Code() const { return m_vu1Code; } uint8_t *getVU1Data() { return m_vu1Data; } const uint8_t *getVU1Data() const { return m_vu1Data; } + uint8_t *getVU0Code() { return m_vu0Code; } + const uint8_t *getVU0Code() const { return m_vu0Code; } + uint8_t *getVU0Data() { return m_vu0Data; } + const uint8_t *getVU0Data() const { return m_vu0Data; } bool isPath3Masked() const { return m_path3Masked; } void flushMaskedPath3Packets(bool drainImmediately = true); @@ -319,9 +324,12 @@ public: void submitGifPacket(GifPathId pathId, const uint8_t *data, uint32_t sizeBytes, bool drainImmediately = true, bool path2DirectHl = false); void processGIFPacket(uint32_t srcPhysAddr, uint32_t qwCount); void processGIFPacket(const uint8_t *data, uint32_t sizeBytes); + void processVIF0Data(uint32_t srcPhysAddr, uint32_t sizeBytes); + void processVIF0Data(const uint8_t *data, uint32_t sizeBytes); void processVIF1Data(uint32_t srcPhysAddr, uint32_t sizeBytes); void processVIF1Data(const uint8_t *data, uint32_t sizeBytes); void processPendingTransfers(); + std::vector consumeCompletedDmacCauses(); int pollDmaRegisters(); @@ -377,6 +385,8 @@ public: Vu1MscalCallback m_vu1MscalCallback; Vu1MscntCallback m_vu1MscntCallback; + uint8_t *m_vu0Code = nullptr; + uint8_t *m_vu0Data = nullptr; uint8_t *m_vu1Code = nullptr; uint8_t *m_vu1Data = nullptr; bool m_path3Masked = false; @@ -392,7 +402,10 @@ public: std::vector chainData; }; std::vector m_pendingGifTransfers; + std::vector m_pendingVif0Transfers; std::vector m_pendingVif1Transfers; + std::mutex m_completedDmacMutex; + std::vector m_completedDmacCauses; struct CodeRegion { @@ -405,6 +418,12 @@ public: bool isAddressInRegion(uint32_t address, const CodeRegion ®ion); void markModified(uint32_t address, uint32_t size); bool isScratchpad(uint32_t address) const; + uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit); + const uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) const; + void updateEeTimer0Counter(); + void queueCompletedDmacCause(uint32_t cause); + uint64_t m_timer0LastHostNs = 0; + uint64_t m_timer0FractionNs = 0; }; #endif // PS2_MEMORY_H diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h index 1e2ad59..102bc9e 100644 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ b/ps2xRuntime/include/runtime/ps2_vu1.h @@ -19,8 +19,12 @@ struct VU1State uint32_t clip; uint32_t status; bool ebit; - uint32_t itop; - uint32_t xitop; + uint32_t top; // VIF1 TOP visible to VU1 XTOP + uint32_t itop; // VIF1 ITOP visible to VU1 XITOP + + bool branchPending; + uint32_t branchTarget; + uint32_t branchDelay; }; class VU1Interpreter @@ -33,13 +37,13 @@ public: void execute(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory = nullptr, - uint32_t startPC = 0, uint32_t itop = 0, + uint32_t startPC = 0, uint32_t top = 0, uint32_t itop = 0, uint32_t maxCycles = 65536); void resume(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory = nullptr, - uint32_t itop = 0, uint32_t maxCycles = 65536); + uint32_t top = 0, uint32_t itop = 0, uint32_t maxCycles = 65536); VU1State &state() { return m_state; } const VU1State &state() const { return m_state; } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp index 19f55b2..71829cd 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp @@ -8,7 +8,9 @@ namespace ps2_stubs constexpr uint32_t kLibSdCmdSetParam = 0x8010u; constexpr uint32_t kLibSdCmdBlockTrans = 0x80D0u; constexpr uint32_t kLibSdCmdBlockTransAlt = 0x80E0u; + constexpr uint32_t kLibSdCmdBlockTransStatus = 0x80F0u; constexpr uint32_t kAudioPositionMask = 0x00FFFFFFu; + constexpr uint32_t kAudioTransferUnit = 1024u; struct AudioStubState { @@ -16,6 +18,9 @@ namespace ps2_stubs uint32_t currentBlockBase = 0u; uint32_t currentBlockSize = 0u; uint32_t currentPauseBase = 0u; + uint32_t currentBlockOffset = 0u; + uint32_t blockStatusTraceCount = 0u; + bool blockTransferActive = false; }; std::mutex g_audio_stub_mutex; @@ -25,6 +30,12 @@ namespace ps2_stubs { g_audio_stub_state = {}; } + + uint32_t currentBlockPosition() + { + return (g_audio_stub_state.currentBlockBase + g_audio_stub_state.currentBlockOffset) & + kAudioPositionMask; + } } void resetAudioStubState() @@ -61,17 +72,50 @@ namespace ps2_stubs if (cmd == kLibSdCmdBlockTrans || cmd == kLibSdCmdBlockTransAlt) { - if (arg4 != 0u) + if (arg4 != 0u && arg5 != 0u) { g_audio_stub_state.currentBlockBase = arg4 & kAudioPositionMask; - } - if (arg5 != 0u) - { g_audio_stub_state.currentBlockSize = arg5; + g_audio_stub_state.currentPauseBase = + ((arg6 != 0u) ? arg6 : arg4) & kAudioPositionMask; + + const uint32_t pauseOffset = + (g_audio_stub_state.currentPauseBase - g_audio_stub_state.currentBlockBase) & + kAudioPositionMask; + g_audio_stub_state.currentBlockOffset = + (pauseOffset < g_audio_stub_state.currentBlockSize) ? pauseOffset : 0u; + g_audio_stub_state.blockStatusTraceCount = 0u; + g_audio_stub_state.blockTransferActive = true; } - if (arg6 != 0u) + else { - g_audio_stub_state.currentPauseBase = arg6 & kAudioPositionMask; + g_audio_stub_state.blockTransferActive = false; + } + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[Audio:BlockTrans] active=" << g_audio_stub_state.blockTransferActive + << " base=0x" << std::hex << g_audio_stub_state.currentBlockBase + << " size=0x" << g_audio_stub_state.currentBlockSize + << " pause=0x" << g_audio_stub_state.currentPauseBase + << " offset=0x" << g_audio_stub_state.currentBlockOffset + << std::dec << std::endl; + }); + } + else if (cmd == kLibSdCmdBlockTransStatus && + g_audio_stub_state.blockTransferActive && + g_audio_stub_state.currentBlockSize != 0u) + { + g_audio_stub_state.currentBlockOffset = + (g_audio_stub_state.currentBlockOffset + kAudioTransferUnit) % + g_audio_stub_state.currentBlockSize; + if (g_audio_stub_state.blockStatusTraceCount < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[Audio:BlockStatus] position=0x" << std::hex << currentBlockPosition() + << " offset=0x" << g_audio_stub_state.currentBlockOffset + << " size=0x" << g_audio_stub_state.currentBlockSize + << std::dec << std::endl; + }); + ++g_audio_stub_state.blockStatusTraceCount; } } else if (cmd == kLibSdCmdSetParam) @@ -81,9 +125,7 @@ namespace ps2_stubs } // Some games only sample the low 24 bits of the reported SPU transfer head. - // Returning the last configured transfer base keeps the ring-buffer math - // stable without emulating SPU DMA progress. - setReturnU32(ctx, g_audio_stub_state.currentBlockBase & kAudioPositionMask); + setReturnU32(ctx, currentBlockPosition()); } void sceSdRemoteInit(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp index 286886f..9228e83 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -1,8 +1,52 @@ #include "Common.h" #include "CD.h" +#include "MPEG.h" namespace ps2_stubs { + namespace + { + uint32_t g_cdStReadTraceCount = 0u; + } + + + CdDebugSnapshot getCdDebugSnapshot() + { + CdDebugSnapshot snapshot{}; + snapshot.initialized = g_cdInitialized; + snapshot.lastError = g_lastCdError; + snapshot.mode = g_cdMode; + snapshot.streamingLbn = g_cdStreamingLbn; + snapshot.streamingEndLbn = g_cdStreamingEndLbn; + snapshot.nextPseudoLbn = g_nextPseudoLbn; + snapshot.imageSizeBytes = g_cdImageSizeBytes; + snapshot.imageSizeValid = g_cdImageSizeValid; + snapshot.cdRoot = getCdRootPath(); + snapshot.cdImage = getCdImagePath(); + snapshot.imageSizePath = g_cdImageSizePath; + snapshot.leafIndexRoot = g_cdLeafIndexRoot; + snapshot.leafIndexBuilt = g_cdLeafIndexBuilt; + snapshot.leafIndexCount = g_cdLeafIndex.size(); + snapshot.loosePathIndexCount = g_cdLoosePathIndex.size(); + + snapshot.files.reserve(g_cdFilesByKey.size()); + for (const auto &[key, entry] : g_cdFilesByKey) + { + CdDebugFileEntry row{}; + row.key = key; + row.hostPath = entry.hostPath; + row.sizeBytes = entry.sizeBytes; + row.baseLbn = entry.baseLbn; + row.sectors = entry.sectors; + snapshot.files.push_back(std::move(row)); + } + std::sort(snapshot.files.begin(), snapshot.files.end(), [](const CdDebugFileEntry &a, const CdDebugFileEntry &b) + { + return a.baseLbn < b.baseLbn; + }); + return snapshot; + } + void sceCdRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t a0 = getRegU32(ctx, 4); // usually lbn @@ -434,6 +478,7 @@ namespace ps2_stubs } g_cdStreamingLbn = resolvedEntry.baseLbn; + g_cdStreamingEndLbn = resolvedEntry.baseLbn + resolvedEntry.sectors; if (shouldTrace) { RUNTIME_LOG("[sceCdSearchFile:ok] path=\"" << sanitizeForLog(path) @@ -448,6 +493,7 @@ namespace ps2_stubs void sceCdSeek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { g_cdStreamingLbn = getRegU32(ctx, 4); + g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); setReturnS32(ctx, 1); } @@ -478,22 +524,65 @@ namespace ps2_stubs void sceCdStRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - uint32_t sectors = getRegU32(ctx, 4); + uint32_t requestedSectors = getRegU32(ctx, 4); + uint32_t sectors = requestedSectors; uint32_t buf = getRegU32(ctx, 5); uint32_t errAddr = getRegU32(ctx, 7); uint32_t offset = buf & PS2_RAM_MASK; - size_t bytes = static_cast(sectors) * kCdSectorSize; + size_t requestedBytes = static_cast(requestedSectors) * kCdSectorSize; const size_t maxBytes = PS2_RAM_SIZE - offset; + if (requestedBytes > maxBytes) + { + requestedBytes = maxBytes; + } + + bool hitStreamEnd = false; + if (g_cdStreamingEndLbn != 0xFFFFFFFFu) + { + if (g_cdStreamingLbn >= g_cdStreamingEndLbn) + { + sectors = 0u; + hitStreamEnd = true; + } + else + { + const uint32_t remaining = g_cdStreamingEndLbn - g_cdStreamingLbn; + if (sectors > remaining) + { + sectors = remaining; + hitStreamEnd = true; + } + } + } + + size_t bytes = static_cast(sectors) * kCdSectorSize; if (bytes > maxBytes) { bytes = maxBytes; } - const bool ok = readCdSectors(g_cdStreamingLbn, sectors, rdram + offset, bytes); + const uint32_t readLbn = g_cdStreamingLbn; + const bool ok = (sectors > 0u) && readCdSectors(readLbn, sectors, rdram + offset, bytes); if (ok) { g_cdStreamingLbn += sectors; + if (requestedBytes > bytes) + { + std::memset(rdram + offset + bytes, 0, requestedBytes - bytes); + } + if (hitStreamEnd || g_cdStreamingLbn == g_cdStreamingEndLbn) + { + notifyMpegCdStreamEof(); + } + } + else + { + if (requestedBytes > 0u) + { + std::memset(rdram + offset, 0, requestedBytes); + } + notifyMpegCdStreamEof(); } if (int32_t *err = reinterpret_cast(getMemPtr(rdram, errAddr)); err) @@ -501,6 +590,18 @@ namespace ps2_stubs *err = ok ? 0 : g_lastCdError; } + if (g_cdStReadTraceCount < 32u) + { + std::cerr << "[sceCdStRead] sectors=" << requestedSectors + << " read=" << sectors + << " buf=0x" << std::hex << buf + << " lbn=0x" << readLbn + << " end=0x" << g_cdStreamingEndLbn + << std::dec << " ok=" << ok + << " bytes=" << bytes << std::endl; + ++g_cdStReadTraceCount; + } + setReturnS32(ctx, ok ? static_cast(sectors) : 0); } @@ -517,18 +618,27 @@ namespace ps2_stubs void sceCdStSeek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { g_cdStreamingLbn = getRegU32(ctx, 4); + g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); setReturnS32(ctx, 1); } void sceCdStSeekF(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { g_cdStreamingLbn = getRegU32(ctx, 4); + g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); setReturnS32(ctx, 1); } void sceCdStStart(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { g_cdStreamingLbn = getRegU32(ctx, 4); + g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); + g_cdStReadTraceCount = 0u; + + notifyMpegCdStreamStart(); + + std::cerr << "[sceCdStStart] lbn=0x" << std::hex << g_cdStreamingLbn + << " endLbn=0x" << g_cdStreamingEndLbn << std::dec << std::endl; setReturnS32(ctx, 1); } @@ -539,6 +649,7 @@ namespace ps2_stubs void sceCdStStop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + notifyMpegCdStreamEof(); setReturnS32(ctx, 1); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.h b/ps2xRuntime/src/lib/Kernel/Stubs/CD.h index 9bf8760..dd5bae9 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.h @@ -2,8 +2,44 @@ #include "ps2_stubs.h" +#include +#include +#include +#include + namespace ps2_stubs { + + struct CdDebugFileEntry + { + std::string key; + std::filesystem::path hostPath; + uint32_t sizeBytes = 0; + uint32_t baseLbn = 0; + uint32_t sectors = 0; + }; + + struct CdDebugSnapshot + { + bool initialized = false; + int32_t lastError = 0; + uint32_t mode = 0; + uint32_t streamingLbn = 0; + uint32_t streamingEndLbn = 0; + uint32_t nextPseudoLbn = 0; + uint64_t imageSizeBytes = 0; + bool imageSizeValid = false; + std::filesystem::path cdRoot; + std::filesystem::path cdImage; + std::filesystem::path imageSizePath; + std::filesystem::path leafIndexRoot; + bool leafIndexBuilt = false; + size_t leafIndexCount = 0; + size_t loosePathIndexCount = 0; + std::vector files; + }; + + CdDebugSnapshot getCdDebugSnapshot(); void sceCdRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceCdSync(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceCdGetError(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Common.h b/ps2xRuntime/src/lib/Kernel/Stubs/Common.h index 59045a1..76a237d 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Common.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Common.h @@ -1,6 +1,7 @@ #pragma once #include "ps2_stubs.h" +#include "ps2_log.h" #include "ps2_runtime.h" #include "ps2_runtime_macros.h" #include "ps2_syscalls.h" diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 2d1b2aa..7e46972 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -17,22 +17,6 @@ namespace ps2_stubs uint32_t g_gs_sync_v_callback_stack_base = 0u; uint32_t g_gs_sync_v_callback_stack_top = 0u; uint32_t g_gs_sync_v_callback_bad_pc_logs = 0u; - struct GsDebugProbePoint - { - uint32_t x; - uint32_t y; - }; - - constexpr GsDebugProbePoint kGhostProbePoints[] = { - {220u, 176u}, - {260u, 208u}, - {320u, 208u}, - {260u, 240u}, - {320u, 240u}, - {260u, 272u}, - {320u, 272u}, - }; - uint64_t makeClearPrim(bool useContext2) { return static_cast(GS_PRIM_SPRITE) | @@ -131,142 +115,9 @@ namespace ps2_stubs return {stack0, stack1, stack2}; } - bool sampleFramebufferPixel(uint8_t *vram, - uint32_t vramSize, - uint32_t fbp, - uint32_t fbw, - uint32_t psm, - uint32_t x, - uint32_t y, - uint32_t &outPixel) - { - if (!vram || fbw == 0u) - { - return false; - } - - const uint32_t bytesPerPixel = - (psm == GS_PSM_CT16 || psm == GS_PSM_CT16S) ? 2u : (psm == GS_PSM_CT32 || psm == GS_PSM_CT24) ? 4u - : 0u; - if (bytesPerPixel == 0u) - { - return false; - } - - const uint32_t widthBlocks = (fbw != 0u) ? fbw : 1u; - const uint32_t strideBytes = fbw * 64u * bytesPerPixel; - const uint32_t baseBytes = fbp * 8192u; - uint32_t offset = baseBytes + (y * strideBytes) + (x * bytesPerPixel); - if (psm == GS_PSM_CT16) - { - offset = GSPSMCT16::addrPSMCT16(GSInternal::framePageBaseToBlock(fbp), widthBlocks, x, y); - } - else if (psm == GS_PSM_CT16S) - { - offset = GSPSMCT16::addrPSMCT16S(GSInternal::framePageBaseToBlock(fbp), widthBlocks, x, y); - } - if (offset + bytesPerPixel > vramSize) - { - return false; - } - - if (bytesPerPixel == 4u) - { - std::memcpy(&outPixel, vram + offset, sizeof(outPixel)); - if (psm == GS_PSM_CT24) - { - outPixel |= 0xFF000000u; - } - return true; - } - - uint16_t packed = 0u; - std::memcpy(&packed, vram + offset, sizeof(packed)); - const uint32_t r = ((packed >> 0) & 0x1Fu) << 3; - const uint32_t g = ((packed >> 5) & 0x1Fu) << 3; - const uint32_t b = ((packed >> 10) & 0x1Fu) << 3; - const uint32_t a = (packed & 0x8000u) ? 0x80u : 0x00u; - outPixel = r | (g << 8) | (b << 16) | (a << 24); - return true; - } - - bool sampleFrameRegPixel(PS2Runtime *runtime, - uint64_t frameReg, - uint32_t x, - uint32_t y, - uint32_t &outPixel) - { - if (!runtime) - { - return false; - } - - const uint32_t fbp = static_cast(frameReg & 0x1FFu); - const uint32_t fbw = static_cast((frameReg >> 16) & 0x3Fu); - const uint32_t psm = static_cast((frameReg >> 24) & 0x3Fu); - return sampleFramebufferPixel(runtime->memory().getGSVRAM(), PS2_GS_VRAM_SIZE, fbp, fbw, psm, x, y, outPixel); - } - - bool sampleDispFbPixel(PS2Runtime *runtime, - uint64_t dispfb, - uint32_t x, - uint32_t y, - uint32_t &outPixel) - { - if (!runtime) - { - return false; - } - - const uint32_t fbp = static_cast(dispfb & 0x1FFu); - const uint32_t fbw = static_cast((dispfb >> 9) & 0x3Fu); - const uint32_t psm = static_cast((dispfb >> 15) & 0x1Fu); - return sampleFramebufferPixel(runtime->memory().getGSVRAM(), PS2_GS_VRAM_SIZE, fbp, fbw, psm, x, y, outPixel); - } - - void logSwapProbeStage(PS2Runtime *runtime, - const char *stage, - uint32_t which, - uint64_t drawFrameReg, - uint64_t dispfb, - bool hasClearPacket) - { - static uint32_t s_swapProbeCount = 0u; - if (!runtime || s_swapProbeCount >= 24u) - { - return; - } - - PS2_IF_AGRESSIVE_LOGS({ - RUNTIME_LOG("[gs:probe] stage=" << stage - << " which=" << which - << " clear=" << static_cast(hasClearPacket ? 1u : 0u)); - - for (const auto &probe : kGhostProbePoints) - { - uint32_t page0Pixel = 0u; - uint32_t page150Pixel = 0u; - const bool havePage0 = sampleFrameRegPixel(runtime, drawFrameReg, probe.x, probe.y, page0Pixel); - const bool havePage150 = sampleDispFbPixel(runtime, dispfb, probe.x, probe.y, page150Pixel); - if (havePage0) - { - RUNTIME_LOG(" p0[" << probe.x << "," << probe.y << "]=0x" - << std::hex << page0Pixel << std::dec); - } - if (havePage150) - { - RUNTIME_LOG(" p150[" << probe.x << "," << probe.y << "]=0x" - << std::hex << page150Pixel << std::dec); - } - } - RUNTIME_LOG(std::endl); - ++s_swapProbeCount; - }); - } - void applyGsClearPacket(PS2Runtime *runtime, const GsClearMem &clear) { - if (!runtime || !runtime->syncCoreSubsystems() || !hasSeededGsClearPacket(clear)) + if (!runtime->syncCoreSubsystems() || !hasSeededGsClearPacket(clear)) { return; } @@ -281,30 +132,6 @@ namespace ps2_stubs void refreshPacketBuilderPendingCount(uint8_t *rdram, PS2Runtime *runtime, uint32_t stateAddr); void writePacketBuilderCurrent(uint8_t *rdram, PS2Runtime *runtime, uint32_t stateAddr, uint32_t currentAddr); - void logVif1PacketStateOp(const char *op, - R5900Context *ctx, - uint32_t stateAddr, - uint32_t currentAddr, - uint32_t aux0, - uint32_t aux1) - { - static uint32_t s_vif1PacketOpLogCount = 0u; - if (s_vif1PacketOpLogCount >= 96u) - { - return; - } - - RUNTIME_LOG("[vif1:packet] idx=" << s_vif1PacketOpLogCount - << " op=" << op - << " pc=0x" << std::hex << ctx->pc - << " ra=0x" << getRegU32(ctx, 31) - << " state=0x" << stateAddr - << " current=0x" << currentAddr - << " aux0=0x" << aux0 - << " aux1=0x" << aux1 - << std::dec << std::endl); - ++s_vif1PacketOpLogCount; - } void initPacketBuilderState(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { @@ -826,10 +653,12 @@ namespace ps2_stubs static uint32_t s_missingCallbackLogCount = 0u; if (s_missingCallbackLogCount < 32u) { - std::cerr << "[sceGsSyncVCallback:missing] cb=0x" << std::hex << callback - << " gp=0x" << gp - << " tick=0x" << callbackTick - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[sceGsSyncVCallback:missing] cb=0x" << std::hex << callback + << " gp=0x" << gp + << " tick=0x" << callbackTick + << std::dec << std::endl; + }); ++s_missingCallbackLogCount; } return; @@ -864,11 +693,13 @@ namespace ps2_stubs const bool shouldLogDispatch = (s_dispatchLogCount < 64u); if (shouldLogDispatch) { - RUNTIME_LOG("[sceGsSyncVCallback:dispatch] cb=0x" << std::hex << callback - << " gp=0x" << gp - << " sp=0x" << getRegU32(&callbackCtx, 29) - << " tick=0x" << callbackTick - << std::dec << std::endl); + PS2_IF_AGRESSIVE_LOGS({ + RUNTIME_LOG("[sceGsSyncVCallback:dispatch] cb=0x" << std::hex << callback + << " gp=0x" << gp + << " sp=0x" << getRegU32(&callbackCtx, 29) + << " tick=0x" << callbackTick + << std::dec << std::endl); + }); } uint32_t steps = 0u; @@ -900,11 +731,13 @@ namespace ps2_stubs if (shouldLogDispatch) { - RUNTIME_LOG("[sceGsSyncVCallback:return] cb=0x" << std::hex << callback - << " finalPc=0x" << callbackCtx.pc - << " ra=0x" << getRegU32(&callbackCtx, 31) - << " steps=0x" << steps - << std::dec << std::endl); + PS2_IF_AGRESSIVE_LOGS({ + RUNTIME_LOG("[sceGsSyncVCallback:return] cb=0x" << std::hex << callback + << " finalPc=0x" << callbackCtx.pc + << " ra=0x" << getRegU32(&callbackCtx, 31) + << " steps=0x" << steps + << std::dec << std::endl); + }); ++s_dispatchLogCount; } } @@ -1470,11 +1303,6 @@ namespace ps2_stubs return; } - const bool hasClearPacket = (which == 0u) ? hasSeededGsClearPacket(db.clear0) - : hasSeededGsClearPacket(db.clear1); - const uint64_t debugDrawFrameReg = (which == 0u) ? db.draw01.frame1.value - : db.draw11.frame1.value; - applyGsDispEnv(runtime, db.disp[which]); static uint32_t s_swapDbuffLogCount = 0u; if (s_swapDbuffLogCount < 32u) @@ -1483,17 +1311,18 @@ namespace ps2_stubs const uint32_t clearContext = (which == 0u) ? static_cast((db.clear0.prim.value >> 9) & 0x1u) : static_cast((db.clear1.prim.value >> 9) & 0x1u); - RUNTIME_LOG("[gs:swapdbuff] which=" << which - << " env=0x" << std::hex << envAddr - << " dispfb=0x" << db.disp[which].dispfb - << " display=0x" << db.disp[which].display - << " pmode=0x" << db.disp[which].pmode - << " dispFbp=" << dispFbp - << " clearCtxt=" << clearContext - << std::dec << std::endl); + PS2_IF_AGRESSIVE_LOGS({ + RUNTIME_LOG("[gs:swapdbuff] which=" << which + << " env=0x" << std::hex << envAddr + << " dispfb=0x" << db.disp[which].dispfb + << " display=0x" << db.disp[which].display + << " pmode=0x" << db.disp[which].pmode + << " dispFbp=" << dispFbp + << " clearCtxt=" << clearContext + << std::dec << std::endl); + }); ++s_swapDbuffLogCount; } - logSwapProbeStage(runtime, "swap-pre", which, debugDrawFrameReg, db.disp[which].dispfb, hasClearPacket); if (which == 0u) { applyGsRegPairs(runtime, reinterpret_cast(&db.draw01), 8u); @@ -1502,10 +1331,8 @@ namespace ps2_stubs { const uint32_t clearContext = static_cast((db.clear0.prim.value >> 9) & 0x1u); runtime->gs().clearFramebufferContext(clearContext, static_cast(db.clear0.rgbaq.value)); - logSwapProbeStage(runtime, "swap-post-clear", which, db.draw01.frame1.value, db.disp[which].dispfb, true); } applyGsClearPacket(runtime, db.clear0); - logSwapProbeStage(runtime, "swap-post", which, db.draw01.frame1.value, db.disp[which].dispfb, hasClearPacket); } else { @@ -1515,10 +1342,8 @@ namespace ps2_stubs { const uint32_t clearContext = static_cast((db.clear1.prim.value >> 9) & 0x1u); runtime->gs().clearFramebufferContext(clearContext, static_cast(db.clear1.rgbaq.value)); - logSwapProbeStage(runtime, "swap-post-clear", which, db.draw11.frame1.value, db.disp[which].dispfb, true); } applyGsClearPacket(runtime, db.clear1); - logSwapProbeStage(runtime, "swap-post", which, db.draw11.frame1.value, db.disp[which].dispfb, hasClearPacket); } setReturnS32(ctx, static_cast(which ^ 1u)); @@ -1651,13 +1476,15 @@ namespace ps2_stubs static uint32_t s_syncVCallbackLogCount = 0u; if (s_syncVCallbackLogCount < 128u) { - RUNTIME_LOG("[sceGsSyncVCallback:set] new=0x" << std::hex << newCallback - << " old=0x" << oldCallback - << " callerPc=0x" << callerPc - << " callerRa=0x" << callerRa - << " gp=0x" << gp - << " sp=0x" << sp - << std::dec << std::endl); + PS2_IF_AGRESSIVE_LOGS({ + RUNTIME_LOG("[sceGsSyncVCallback:set] new=0x" << std::hex << newCallback + << " old=0x" << oldCallback + << " callerPc=0x" << callerPc + << " callerRa=0x" << callerRa + << " gp=0x" << gp + << " sp=0x" << sp + << std::dec << std::endl); + }); ++s_syncVCallbackLogCount; } @@ -1798,8 +1625,6 @@ namespace ps2_stubs return; } - logVif1PacketStateOp("close-direct", ctx, stateAddr, currentAddr, openAddr, qwordCount); - tagWord += qwordCount; writeGuestU32(rdram, runtime, stateAddr + 12u, 0u); writeGuestU32(rdram, runtime, openAddr, tagWord); @@ -1866,7 +1691,6 @@ namespace ps2_stubs writeGuestU32(rdram, runtime, currentAddr, tagWord); writePacketBuilderCurrent(rdram, runtime, stateAddr, currentAddr + 4u); writeGuestU32(rdram, runtime, stateAddr + 12u, currentAddr); - logVif1PacketStateOp("open-direct", ctx, stateAddr, currentAddr + 4u, currentAddr, tagWord); } void sceVif1PkOpenGifTag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -1887,7 +1711,6 @@ namespace ps2_stubs uint32_t currentAddr = 0u; tryReadWordFromGuest(rdram, runtime, stateAddr, currentAddr); const uint32_t reservedAddr = reservePacketBuilderWords(rdram, runtime, stateAddr, wordCount); - logVif1PacketStateOp("reserve", ctx, stateAddr, currentAddr, reservedAddr, wordCount); setReturnU32(ctx, reservedAddr); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index 0b746cd..a98e548 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -26,6 +26,7 @@ namespace int32_t g_lastCdError = 0; uint32_t g_cdMode = 0; uint32_t g_cdStreamingLbn = 0; + uint32_t g_cdStreamingEndLbn = 0xFFFFFFFFu; bool g_cdInitialized = false; constexpr uint32_t kIopHeapBase = 0x01A00000; @@ -497,6 +498,30 @@ namespace return false; } + bool findRegisteredCdFileForLbn(uint32_t lbn, CdFileEntry &entryOut) + { + for (const auto &[key, entry] : g_cdFilesByKey) + { + const uint32_t endLbn = entry.baseLbn + entry.sectors; + if (lbn >= entry.baseLbn && lbn < endLbn) + { + entryOut = entry; + return true; + } + } + return false; + } + + uint32_t cdStreamingEndLbnForStart(uint32_t lbn) + { + CdFileEntry entry{}; + if (findRegisteredCdFileForLbn(lbn, entry)) + { + return entry.baseLbn + entry.sectors; + } + return 0xFFFFFFFFu; + } + bool writeCdSearchResult(uint8_t *rdram, uint32_t fileAddr, const std::string &ps2Path, const CdFileEntry &entry) { // sceCdlFILE layout: u32 lsn, u32 size, char name[16], u8 date[8] @@ -1354,37 +1379,61 @@ namespace mem.writeIORegister(channelBase + 0x00u, chcr); mem.processPendingTransfers(); - std::lock_guard lock(g_dmaStubMutex); - g_dmaPendingPolls[channelBase] = 1; - if (g_dmaStubLogCount < kMaxDmaStubLogs) + std::vector completedCauses = mem.consumeCompletedDmacCauses(); + if (completedCauses.empty() && (mem.readIORegister(channelBase + 0x00u) & 0x100u) == 0u) { - RUNTIME_LOG("[sceDmaSend] ch=0x" << std::hex << channelBase - << " madr=0x" << madr - << " qwc=0x" << qwc - << " tadr=0x" << tadr - << " chcr=0x" << chcr << std::dec << std::endl); - - if (!preferNormalCount && (channelBase == 0x10009000u || channelBase == 0x1000A000u)) + if (channelBase == 0x10008000u) { - if (const uint8_t *tagPtr = getConstMemPtr(rdram, tadr)) - { - uint64_t tagLo = 0u; - std::memcpy(&tagLo, tagPtr, sizeof(tagLo)); - uint32_t w2 = 0u; - uint32_t w3 = 0u; - std::memcpy(&w2, tagPtr + 8u, sizeof(w2)); - std::memcpy(&w3, tagPtr + 12u, sizeof(w3)); - RUNTIME_LOG("[sceDmaSend:head] ch=0x" << std::hex << channelBase - << " tagQwc=0x" << static_cast(tagLo & 0xFFFFu) - << " id=0x" << static_cast((tagLo >> 28u) & 0x7u) - << " irq=0x" << static_cast((tagLo >> 31u) & 0x1u) - << " addr=0x" << static_cast((tagLo >> 32u) & 0x7FFFFFFFu) - << " w2=0x" << w2 - << " w3=0x" << w3 - << std::dec << std::endl); - } + completedCauses.push_back(0u); } - ++g_dmaStubLogCount; + else if (channelBase == 0x10009000u) + { + completedCauses.push_back(1u); + } + else if (channelBase == 0x1000A000u) + { + completedCauses.push_back(2u); + } + } + + { + std::lock_guard lock(g_dmaStubMutex); + g_dmaPendingPolls[channelBase] = 1; + if (g_dmaStubLogCount < kMaxDmaStubLogs) + { + RUNTIME_LOG("[sceDmaSend] ch=0x" << std::hex << channelBase + << " madr=0x" << madr + << " qwc=0x" << qwc + << " tadr=0x" << tadr + << " chcr=0x" << chcr << std::dec << std::endl); + + if (!preferNormalCount && (channelBase == 0x10009000u || channelBase == 0x1000A000u)) + { + if (const uint8_t *tagPtr = getConstMemPtr(rdram, tadr)) + { + uint64_t tagLo = 0u; + std::memcpy(&tagLo, tagPtr, sizeof(tagLo)); + uint32_t w2 = 0u; + uint32_t w3 = 0u; + std::memcpy(&w2, tagPtr + 8u, sizeof(w2)); + std::memcpy(&w3, tagPtr + 12u, sizeof(w3)); + RUNTIME_LOG("[sceDmaSend:head] ch=0x" << std::hex << channelBase + << " tagQwc=0x" << static_cast(tagLo & 0xFFFFu) + << " id=0x" << static_cast((tagLo >> 28u) & 0x7u) + << " irq=0x" << static_cast((tagLo >> 31u) & 0x1u) + << " addr=0x" << static_cast((tagLo >> 32u) & 0x7FFFFFFFu) + << " w2=0x" << w2 + << " w3=0x" << w3 + << std::dec << std::endl); + } + } + ++g_dmaStubLogCount; + } + } + + for (const uint32_t completedCause : completedCauses) + { + ps2_syscalls::dispatchDmacHandlersForCause(rdram, runtime, completedCause); } return 0; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index d06f5a6..8bd093f 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -1,41 +1,526 @@ #include "Common.h" #include "MPEG.h" +extern "C" +{ +#include +#include +#include +#include +} + +#include +#include +#include +#include + +#include "Syscalls/Helpers/State.h" + namespace ps2_stubs { namespace { + struct MpegDecodedFrame + { + int width = 0; + int height = 0; + std::vector rgba; + }; + + std::string ffmpegErrorString(int err) + { + std::array buffer{}; + if (av_strerror(err, buffer.data(), buffer.size()) < 0) + { + return "unknown FFmpeg error"; + } + return std::string(buffer.data()); + } + + void configureFfmpegLogLevel() + { + static std::once_flag s_once; + std::call_once(s_once, [] { + av_log_set_level(ps2_log::agressive_logs_enabled ? AV_LOG_WARNING : AV_LOG_ERROR); + }); + } + + class MpegFfmpegDecoder + { + public: + MpegFfmpegDecoder() = default; + + ~MpegFfmpegDecoder() + { + reset(); + } + + MpegFfmpegDecoder(const MpegFfmpegDecoder &) = delete; + MpegFfmpegDecoder &operator=(const MpegFfmpegDecoder &) = delete; + + bool feed(const uint8_t *data, size_t size, std::deque &frames) + { + if (!data || size == 0) + { + return true; + } + + if (!ensureInitialized()) + { + return false; + } + + static uint32_t s_feedLogCount = 0u; + const bool shouldLog = (s_feedLogCount < 32u); + if (shouldLog) + ++s_feedLogCount; + + size_t totalParsed = 0u; + size_t totalPacketsSent = 0u; + size_t framesBefore = frames.size(); + + const uint8_t *cursor = data; + size_t remaining = size; + while (remaining > 0) + { + uint8_t *packetData = nullptr; + int packetSize = 0; + const int chunk = static_cast(std::min( + remaining, static_cast(std::numeric_limits::max()))); + const int used = av_parser_parse2( + m_parser, + m_codecCtx, + &packetData, + &packetSize, + cursor, + chunk, + AV_NOPTS_VALUE, + AV_NOPTS_VALUE, + 0); + if (used < 0) + { + std::cerr << "[MPEG] parser failed: " << ffmpegErrorString(used) << std::endl; + return false; + } + if (used == 0 && packetSize == 0) + { + break; + } + + totalParsed += static_cast(used); + cursor += used; + remaining -= static_cast(used); + + if (packetSize > 0) + { + ++totalPacketsSent; + if (!sendPacket(packetData, static_cast(packetSize), frames)) + { + return false; + } + } + } + + if (shouldLog) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:feed] inSize=" << size + << " parsed=" << totalParsed + << " packets=" << totalPacketsSent + << " newFrames=" << (frames.size() - framesBefore) + << " totalFrames=" << frames.size() + << std::endl; + }); + } + + return true; + } + + bool flush(std::deque &frames) + { + if (!m_initialized || m_drained) + { + return true; + } + + if (m_parser) + { + uint8_t *packetData = nullptr; + int packetSize = 0; + const int used = av_parser_parse2( + m_parser, + m_codecCtx, + &packetData, + &packetSize, + nullptr, + 0, + AV_NOPTS_VALUE, + AV_NOPTS_VALUE, + 0); + (void)used; + if (packetSize > 0 && !sendPacket(packetData, static_cast(packetSize), frames)) + { + return false; + } + } + + const int sendRet = avcodec_send_packet(m_codecCtx, nullptr); + if (sendRet < 0 && sendRet != AVERROR_EOF) + { + std::cerr << "[MPEG] decoder flush failed: " << ffmpegErrorString(sendRet) << std::endl; + return false; + } + + const bool ok = receiveFrames(frames); + m_drained = true; + return ok; + } + + void reset() + { + if (m_swsCtx) + { + sws_freeContext(m_swsCtx); + m_swsCtx = nullptr; + } + if (m_frame) + { + av_frame_free(&m_frame); + } + if (m_packet) + { + av_packet_free(&m_packet); + } + if (m_codecCtx) + { + avcodec_free_context(&m_codecCtx); + } + if (m_parser) + { + av_parser_close(m_parser); + m_parser = nullptr; + } + + m_swsWidth = 0; + m_swsHeight = 0; + m_swsFormat = AV_PIX_FMT_NONE; + m_initialized = false; + m_drained = false; + } + + private: + bool ensureInitialized() + { + if (m_initialized) + { + return true; + } + + configureFfmpegLogLevel(); + + const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MPEG2VIDEO); + if (!codec) + { + std::cerr << "[MPEG] FFmpeg MPEG-2 decoder not found." << std::endl; + return false; + } + + m_parser = av_parser_init(AV_CODEC_ID_MPEG2VIDEO); + if (!m_parser) + { + std::cerr << "[MPEG] FFmpeg MPEG-video parser not found." << std::endl; + return false; + } + + m_codecCtx = avcodec_alloc_context3(codec); + m_frame = av_frame_alloc(); + m_packet = av_packet_alloc(); + if (!m_codecCtx || !m_frame || !m_packet) + { + std::cerr << "[MPEG] failed to allocate FFmpeg decoder state." << std::endl; + reset(); + return false; + } + + m_codecCtx->thread_count = 1; + m_codecCtx->skip_frame = AVDISCARD_NONKEY; + m_codecCtx->err_recognition = 0; + const int ret = avcodec_open2(m_codecCtx, codec, nullptr); + if (ret < 0) + { + std::cerr << "[MPEG] failed to open MPEG decoder: " << ffmpegErrorString(ret) << std::endl; + reset(); + return false; + } + + m_initialized = true; + m_drained = false; + m_seenKeyframe = false; + return true; + } + + bool sendPacket(const uint8_t *data, size_t size, std::deque &frames) + { + if (!data || size == 0) + { + return true; + } + + av_packet_unref(m_packet); + const int allocRet = av_new_packet(m_packet, static_cast(size)); + if (allocRet < 0) + { + std::cerr << "[MPEG] failed to allocate packet: " << ffmpegErrorString(allocRet) << std::endl; + return false; + } + std::memcpy(m_packet->data, data, size); + + int ret = avcodec_send_packet(m_codecCtx, m_packet); + if (ret == AVERROR(EAGAIN)) + { + if (!receiveFrames(frames)) + { + av_packet_unref(m_packet); + return false; + } + ret = avcodec_send_packet(m_codecCtx, m_packet); + } + av_packet_unref(m_packet); + if (ret < 0 && ret != AVERROR(EAGAIN)) + { + static uint32_t s_rejectedPacketLogCount = 0u; + if (s_rejectedPacketLogCount < 32u) + { + std::cerr << "[MPEG] decoder rejected packet, dropping: " + << ffmpegErrorString(ret) << std::endl; + ++s_rejectedPacketLogCount; + } + return true; + } + + return receiveFrames(frames); + } + + bool receiveFrames(std::deque &frames) + { + while (true) + { + const int ret = avcodec_receive_frame(m_codecCtx, m_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) + { + return true; + } + if (ret < 0) + { + static uint32_t s_receiveErrorLogCount = 0u; + if (s_receiveErrorLogCount < 32u) + { + std::cerr << "[MPEG] decoder receive failed, dropping: " + << ffmpegErrorString(ret) << std::endl; + ++s_receiveErrorLogCount; + } + return true; + } + + if (!m_seenKeyframe) + { + m_seenKeyframe = true; + m_codecCtx->skip_frame = AVDISCARD_DEFAULT; + } + + if (!convertFrame(frames)) + { + av_frame_unref(m_frame); + return false; + } + av_frame_unref(m_frame); + } + } + + bool convertFrame(std::deque &frames) + { + const int width = m_frame->width; + const int height = m_frame->height; + const AVPixelFormat srcFormat = static_cast(m_frame->format); + if (width <= 0 || height <= 0 || srcFormat == AV_PIX_FMT_NONE) + { + return false; + } + + if (!m_swsCtx || + m_swsWidth != width || + m_swsHeight != height || + m_swsFormat != srcFormat) + { + if (m_swsCtx) + { + sws_freeContext(m_swsCtx); + m_swsCtx = nullptr; + } + m_swsCtx = sws_getContext( + width, + height, + srcFormat, + width, + height, + AV_PIX_FMT_RGBA, + SWS_BILINEAR, + nullptr, + nullptr, + nullptr); + if (!m_swsCtx) + { + std::cerr << "[MPEG] failed to create FFmpeg scaler." << std::endl; + return false; + } + m_swsWidth = width; + m_swsHeight = height; + m_swsFormat = srcFormat; + } + + MpegDecodedFrame decoded; + decoded.width = width; + decoded.height = height; + decoded.rgba.resize(static_cast(width) * static_cast(height) * 4u); + + uint8_t *dstData[4] = {decoded.rgba.data(), nullptr, nullptr, nullptr}; + int dstLinesize[4] = {width * 4, 0, 0, 0}; + const int scaledRows = sws_scale( + m_swsCtx, + m_frame->data, + m_frame->linesize, + 0, + height, + dstData, + dstLinesize); + if (scaledRows <= 0) + { + std::cerr << "[MPEG] FFmpeg scaler produced no rows." << std::endl; + return false; + } + + frames.push_back(std::move(decoded)); + return true; + } + + AVCodecParserContext *m_parser = nullptr; + AVCodecContext *m_codecCtx = nullptr; + AVFrame *m_frame = nullptr; + AVPacket *m_packet = nullptr; + SwsContext *m_swsCtx = nullptr; + int m_swsWidth = 0; + int m_swsHeight = 0; + AVPixelFormat m_swsFormat = AV_PIX_FMT_NONE; + bool m_initialized = false; + bool m_drained = false; + bool m_seenKeyframe = false; + }; + struct MpegRegisteredCallback { uint32_t type = 0u; + uint32_t streamId = 0u; uint32_t func = 0u; uint32_t data = 0u; uint32_t handle = 0u; + bool stream = false; }; struct MpegPlaybackState { uint32_t picturesServed = 0u; + uint32_t width = 320u; + uint32_t height = 240u; + uint32_t decodeMode = 0u; + uint32_t imageBufferAddr = 0u; + bool sawInput = false; + bool streamEnded = false; + bool decoderFailed = false; + uint64_t cdStreamGeneration = 0u; + bool noFrameStallArmed = false; + std::chrono::steady_clock::time_point noFrameStallStart{}; + uint32_t consecutiveEmptyGetPicture = 0u; + bool waitingForVideoSequenceHeader = true; + std::vector videoSequenceSyncBuffer; + std::vector pssBuffer; + std::vector pssGuestAddrs; + std::deque decodedFrames; + bool hasLastFrame = false; + MpegDecodedFrame lastFrame; + std::unique_ptr decoder; + }; + + struct MpegStreamCallbackEvent + { + uint32_t mpegAddr = 0u; + uint32_t streamType = 0u; + uint32_t dataAddr = 0u; + uint32_t len = 0u; + uint64_t pts = 0xFFFFFFFFFFFFFFFFull; + uint64_t dts = 0xFFFFFFFFFFFFFFFFull; + std::vector callbacks; }; struct MpegStubState { bool initialized = false; uint32_t nextCallbackHandle = 1u; + uint64_t cdStreamGeneration = 0u; + bool currentCdStreamEofSeen = false; + uint32_t feedEsTraceCount = 0u; + uint32_t demuxPssTraceCount = 0u; + uint32_t demuxRingTraceCount = 0u; + uint32_t getPictureWaitTraceCount = 0u; + uint32_t pictureTraceCount = 0u; + uint32_t isEndTraceCount = 0u; std::unordered_map> callbacksByMpeg; std::unordered_map playbackByMpeg; - PS2MpegCompatLayout compat; }; std::mutex g_mpeg_stub_mutex; + std::condition_variable g_mpeg_cv; MpegStubState g_mpeg_stub_state; + // TODO this resolution should follow runtime resolution constexpr uint32_t kStubMovieWidth = 320u; constexpr uint32_t kStubMovieHeight = 240u; + constexpr uint32_t kMpegStrM2V = 0u; + constexpr uint32_t kMpegStrPCM = 1u; + constexpr uint32_t kMpegStrADPCM = 2u; + constexpr uint8_t kMpegPackHeader = 0xBAu; + constexpr uint8_t kMpegSystemHeader = 0xBBu; + constexpr uint8_t kMpegProgramEnd = 0xB9u; + constexpr uint8_t kMpegSequenceEnd = 0xB7u; + constexpr uint8_t kMpegPrivateStream1 = 0xBDu; + constexpr size_t kStartCodeNotFound = std::numeric_limits::max(); + constexpr uint32_t kMpegCallbackDataSize = 0x20u; + constexpr uint32_t kMpegCallbackMaxSteps = 0x4000u; + constexpr std::chrono::milliseconds kMpegGetPictureNoFrameWaitTimeout{64}; + constexpr std::chrono::milliseconds kMpegNoFrameEndTimeout{500}; + constexpr uint32_t kMpegMaxConsecutiveEmptyGetPicture = 60u; - uint32_t mpegCompatSyntheticFrames(const PS2MpegCompatLayout &layout) + uint32_t align16(uint32_t value) { - return layout.syntheticFramesBeforeEnd != 0u ? layout.syntheticFramesBeforeEnd : 1u; + return (value + 15u) & ~15u; + } + + uint32_t readStackArg(uint8_t *rdram, R5900Context *ctx, uint32_t offset) + { + if (!rdram || !ctx) + { + return 0u; + } + return FAST_READ32(getRegU32(ctx, 29) + offset); + } + + uint32_t readAbiArg4(uint8_t *rdram, R5900Context *ctx) + { + const uint32_t regArg = getRegU32(ctx, 8); + if (regArg != 0u) + { + return regArg; + } + return readStackArg(rdram, ctx, 0x10u); } MpegPlaybackState &getPlaybackState(uint32_t mpegAddr) @@ -43,43 +528,1076 @@ namespace ps2_stubs return g_mpeg_stub_state.playbackByMpeg[mpegAddr]; } + MpegPlaybackState makeFreshPlaybackState() + { + MpegPlaybackState playback{}; + playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + return playback; + } + + MpegPlaybackState makeFreshPlaybackStatePreservingConfig(const MpegPlaybackState &oldPlayback) + { + MpegPlaybackState playback = makeFreshPlaybackState(); + playback.decodeMode = oldPlayback.decodeMode; + playback.imageBufferAddr = oldPlayback.imageBufferAddr; + playback.width = oldPlayback.width; + playback.height = oldPlayback.height; + return playback; + } + + uint16_t readBe16(const uint8_t *p) + { + return static_cast((static_cast(p[0]) << 8u) | + static_cast(p[1])); + } + + bool isVideoStreamId(uint8_t streamId) + { + return streamId >= 0xE0u && streamId <= 0xEFu; + } + + bool isAudioStreamId(uint8_t streamId) + { + return streamId == kMpegPrivateStream1 || (streamId >= 0xC0u && streamId <= 0xDFu); + } + + bool isLengthPrefixedHeader(uint8_t streamId) + { + switch (streamId) + { + case kMpegSystemHeader: + case 0xBCu: // program_stream_map + case 0xBEu: // padding_stream + case 0xBFu: // private_stream_2 + case 0xF0u: // ECM + case 0xF1u: // EMM + case 0xF2u: // DSMCC + case 0xF8u: // ITU-T H.222.1 type E + case 0xFFu: // program_stream_directory + return true; + default: + return false; + } + } + + size_t findStartCode(const std::vector &buffer, size_t from) + { + if (buffer.size() < 4 || from >= buffer.size() - 3u) + { + return kStartCodeNotFound; + } + + for (size_t i = from; i + 3u < buffer.size(); ++i) + { + if (buffer[i] == 0x00u && buffer[i + 1u] == 0x00u && buffer[i + 2u] == 0x01u) + { + return i; + } + } + return kStartCodeNotFound; + } + + bool containsMpegSequenceEnd(const uint8_t *data, size_t size) + { + if (!data || size < 4) + { + return false; + } + + for (size_t i = 0; i + 3u < size; ++i) + { + if (data[i] == 0x00u && + data[i + 1u] == 0x00u && + data[i + 2u] == 0x01u && + data[i + 3u] == kMpegSequenceEnd) + { + return true; + } + } + return false; + } + + size_t findMpegSequenceHeader(const uint8_t *data, size_t size) + { + if (!data || size < 7u) + { + return kStartCodeNotFound; + } + + for (size_t i = 0; i + 6u < size; ++i) + { + if (data[i] == 0x00u && + data[i + 1u] == 0x00u && + data[i + 2u] == 0x01u && + data[i + 3u] == 0xB3u) + { + const uint32_t width = (static_cast(data[i + 4u]) << 4u) | + (static_cast(data[i + 5u]) >> 4u); + const uint32_t height = ((static_cast(data[i + 5u]) & 0x0Fu) << 8u) | + static_cast(data[i + 6u]); + if (width != 0u && height != 0u && width <= 4096u && height <= 4096u) + { + return i; + } + } + } + return kStartCodeNotFound; + } + + size_t parsePesPayloadOffset(const uint8_t *packet, size_t packetSize) + { + if (!packet || packetSize <= 6u) + { + return packetSize; + } + + size_t pos = 6u; + if (packetSize >= 9u && (packet[pos] & 0xC0u) == 0x80u) + { + return std::min(packetSize, 9u + static_cast(packet[pos + 2u])); + } + + while (pos < packetSize && packet[pos] == 0xFFu) + { + ++pos; + } + + if (pos + 1u < packetSize && (packet[pos] & 0xC0u) == 0x40u) + { + pos += 2u; + } + + if (pos >= packetSize) + { + return packetSize; + } + + const uint8_t flags = packet[pos]; + if ((flags & 0xF0u) == 0x20u) + { + pos += 5u; + } + else if ((flags & 0xF0u) == 0x30u) + { + pos += 10u; + } + else if (flags == 0x0Fu) + { + pos += 1u; + } + + return std::min(packetSize, pos); + } + + void flushDecoderIfEnded(MpegPlaybackState &playback) + { + if (playback.streamEnded && playback.decoder) + { + playback.decoder->flush(playback.decodedFrames); + } + } + + void clearNoFrameStall(MpegPlaybackState &playback) + { + playback.noFrameStallArmed = false; + playback.noFrameStallStart = {}; + } + + void finishPlaybackStream(uint32_t mpegAddr, MpegPlaybackState &playback); + + bool maybeFinishNoFrameStall(uint32_t mpegAddr, MpegPlaybackState &playback) + { + if (playback.streamEnded || playback.decoderFailed || !playback.decodedFrames.empty()) + { + clearNoFrameStall(playback); + playback.consecutiveEmptyGetPicture = 0u; + return false; + } + + if (!playback.sawInput || playback.picturesServed == 0u) + { + return false; + } + + const auto now = std::chrono::steady_clock::now(); + const bool cdStreamEofSeen = g_mpeg_stub_state.currentCdStreamEofSeen; + + bool stallByNoFrame = false; + if (cdStreamEofSeen) + { + if (!playback.noFrameStallArmed) + { + playback.noFrameStallArmed = true; + playback.noFrameStallStart = now; + } + else if (now - playback.noFrameStallStart >= kMpegNoFrameEndTimeout) + { + stallByNoFrame = true; + } + } + else + { + clearNoFrameStall(playback); + } + + const bool stallByConsecutive = + cdStreamEofSeen && (playback.consecutiveEmptyGetPicture >= kMpegMaxConsecutiveEmptyGetPicture); + + if (!stallByNoFrame && !stallByConsecutive) + { + return false; + } + + finishPlaybackStream(mpegAddr, playback); + + static uint32_t s_noFrameEofLogCount = 0u; + if (s_noFrameEofLogCount < 16u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:no-frame-eof] mpeg=0x" << std::hex << mpegAddr + << std::dec << " served=" << playback.picturesServed + << " sawInput=" << playback.sawInput + << " cdEof=" << cdStreamEofSeen + << " reason=" + << (stallByNoFrame ? "no-frame" : "consecutive") + << " consecutiveEmpty=" << playback.consecutiveEmptyGetPicture + << std::endl; + }); + ++s_noFrameEofLogCount; + } + return true; + } + + void feedElementaryStream(MpegPlaybackState &playback, const uint8_t *data, size_t size) + { + if (!data || size == 0) + { + return; + } + + const uint32_t feedEsIdx = g_mpeg_stub_state.feedEsTraceCount++; + if (feedEsIdx < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + char hexBuf[16] = {}; + for (size_t i = 0; i < std::min(4u, size); ++i) + { + ::snprintf(hexBuf + i * 2, 3, "%02x", data[i]); + } + std::cerr << "[MPEG:feedES] #" << feedEsIdx + << " size=" << size + << " first4=" << hexBuf + << " decoderFailed=" << playback.decoderFailed + << " waitSeq=" << playback.waitingForVideoSequenceHeader + << std::endl; + }); + } + + playback.sawInput = true; + if (playback.waitingForVideoSequenceHeader) + { + playback.videoSequenceSyncBuffer.insert( + playback.videoSequenceSyncBuffer.end(), + data, + data + size); + + constexpr size_t kMaxVideoSequenceSyncBytes = 2u * 1024u * 1024u; + if (playback.videoSequenceSyncBuffer.size() > kMaxVideoSequenceSyncBytes) + { + const size_t keepFrom = playback.videoSequenceSyncBuffer.size() - 3u; + playback.videoSequenceSyncBuffer.erase( + playback.videoSequenceSyncBuffer.begin(), + playback.videoSequenceSyncBuffer.begin() + static_cast(keepFrom)); + } + + const size_t sequenceHeader = findMpegSequenceHeader( + playback.videoSequenceSyncBuffer.data(), + playback.videoSequenceSyncBuffer.size()); + if (sequenceHeader == kStartCodeNotFound) + { + return; + } + + if (sequenceHeader != 0u) + { + playback.videoSequenceSyncBuffer.erase( + playback.videoSequenceSyncBuffer.begin(), + playback.videoSequenceSyncBuffer.begin() + static_cast(sequenceHeader)); + } + + data = playback.videoSequenceSyncBuffer.data(); + size = playback.videoSequenceSyncBuffer.size(); + playback.waitingForVideoSequenceHeader = false; + playback.decoderFailed = false; + playback.decoder.reset(); + playback.decodedFrames.clear(); + } + + if (containsMpegSequenceEnd(data, size)) + { + playback.streamEnded = true; + playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + } + + if (!playback.decoder) + { + playback.decoder = std::make_unique(); + } + + if (!playback.decoder->feed(data, size, playback.decodedFrames)) + { + playback.decoder.reset(); + playback.waitingForVideoSequenceHeader = true; + playback.videoSequenceSyncBuffer.clear(); + playback.decoderFailed = false; + return; + } + + playback.videoSequenceSyncBuffer.clear(); + flushDecoderIfEnded(playback); + if (!playback.decodedFrames.empty()) + { + clearNoFrameStall(playback); + } + } + + void erasePssPrefix(MpegPlaybackState &playback, size_t count) + { + std::vector &buffer = playback.pssBuffer; + std::vector &guestAddrs = playback.pssGuestAddrs; + const size_t clamped = std::min(count, buffer.size()); + if (clamped == 0u) + { + return; + } + + buffer.erase(buffer.begin(), buffer.begin() + static_cast(clamped)); + if (guestAddrs.size() >= clamped) + { + guestAddrs.erase(guestAddrs.begin(), guestAddrs.begin() + static_cast(clamped)); + } + else + { + guestAddrs.clear(); + } + } + + std::vector matchingStreamCallbacks(uint32_t mpegAddr, uint32_t streamType) + { + std::vector out; + auto it = g_mpeg_stub_state.callbacksByMpeg.find(mpegAddr); + if (it == g_mpeg_stub_state.callbacksByMpeg.end()) + { + return out; + } + + for (const MpegRegisteredCallback &callback : it->second) + { + if (callback.stream && callback.type == streamType) + { + out.push_back(callback); + } + } + return out; + } + + void queueStreamCallbackEvent(uint32_t mpegAddr, + uint32_t streamType, + uint32_t dataAddr, + uint32_t len, + std::vector &callbackEvents) + { + MpegStreamCallbackEvent event{}; + event.mpegAddr = mpegAddr; + event.streamType = streamType; + event.dataAddr = dataAddr; + event.len = len; + event.callbacks = matchingStreamCallbacks(mpegAddr, streamType); + if (!event.callbacks.empty()) + { + callbackEvents.push_back(std::move(event)); + } + } + + void processPssBuffer(uint32_t mpegAddr, + MpegPlaybackState &playback, + std::vector &callbackEvents, + bool finalChunk = false) + { + std::vector &buffer = playback.pssBuffer; + + while (true) + { + if (playback.streamEnded) + { + erasePssPrefix(playback, buffer.size()); + return; + } + + const size_t start = findStartCode(buffer, 0u); + if (start == kStartCodeNotFound) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + return; + } + if (buffer.size() > 3u) + { + erasePssPrefix(playback, buffer.size() - 3u); + } + return; + } + + if (start > 0u) + { + erasePssPrefix(playback, start); + } + + if (buffer.size() < 4u) + { + return; + } + + const uint8_t streamId = buffer[3u]; + + if (streamId == kMpegProgramEnd) + { + playback.streamEnded = true; + playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + flushDecoderIfEnded(playback); + erasePssPrefix(playback, buffer.size()); + return; + } + + if (streamId == kMpegPackHeader) + { + if (buffer.size() < 12u) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + } + return; + } + + size_t packSize = 12u; + if ((buffer[4u] & 0xC0u) == 0x40u) + { + if (buffer.size() < 14u) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + } + return; + } + packSize = 14u + static_cast(buffer[13u] & 0x07u); + } + if (buffer.size() < packSize) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + } + return; + } + erasePssPrefix(playback, packSize); + continue; + } + + if (buffer.size() < 6u) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + } + return; + } + + const uint16_t packetLength = readBe16(buffer.data() + 4u); + if (isLengthPrefixedHeader(streamId)) + { + const size_t packetEnd = 6u + static_cast(packetLength); + if (buffer.size() < packetEnd) + { + if (finalChunk) + { + erasePssPrefix(playback, buffer.size()); + } + return; + } + erasePssPrefix(playback, packetEnd); + continue; + } + + size_t packetEnd = 0u; + if (packetLength != 0u) + { + packetEnd = 6u + static_cast(packetLength); + if (buffer.size() < packetEnd) + { + if (!finalChunk) + { + return; + } + packetEnd = buffer.size(); + } + } + else + { + const size_t next = findStartCode(buffer, 6u); + if (next == kStartCodeNotFound) + { + if (!finalChunk) + { + return; + } + packetEnd = buffer.size(); + } + else + { + packetEnd = next; + } + } + + if (isVideoStreamId(streamId)) + { + const size_t payloadStart = parsePesPayloadOffset(buffer.data(), packetEnd); + if (payloadStart < packetEnd) + { + if (payloadStart < playback.pssGuestAddrs.size()) + { + queueStreamCallbackEvent( + mpegAddr, + kMpegStrM2V, + playback.pssGuestAddrs[payloadStart], + static_cast(packetEnd - payloadStart), + callbackEvents); + } + feedElementaryStream( + playback, + buffer.data() + payloadStart, + packetEnd - payloadStart); + } + } + else if (isAudioStreamId(streamId)) + { + const size_t payloadStart = parsePesPayloadOffset(buffer.data(), packetEnd); + if (payloadStart < packetEnd && payloadStart < playback.pssGuestAddrs.size()) + { + queueStreamCallbackEvent( + mpegAddr, + kMpegStrPCM, + playback.pssGuestAddrs[payloadStart], + static_cast(packetEnd - payloadStart), + callbackEvents); + queueStreamCallbackEvent( + mpegAddr, + kMpegStrADPCM, + playback.pssGuestAddrs[payloadStart], + static_cast(packetEnd - payloadStart), + callbackEvents); + } + } + + erasePssPrefix(playback, packetEnd); + } + } + + void finishPlaybackStream(uint32_t mpegAddr, MpegPlaybackState &playback) + { + std::vector ignoredCallbacks; + processPssBuffer(mpegAddr, playback, ignoredCallbacks, true); + playback.streamEnded = true; + playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + flushDecoderIfEnded(playback); + } + + void appendPssBytes(uint32_t mpegAddr, + MpegPlaybackState &playback, + const uint8_t *data, + size_t size, + uint32_t guestAddr, + std::vector &callbackEvents) + { + if (!data || size == 0) + { + return; + } + + if (playback.sawInput && playback.cdStreamGeneration != g_mpeg_stub_state.cdStreamGeneration) + { + playback = makeFreshPlaybackState(); + } + + if (playback.streamEnded) + { + if (playback.cdStreamGeneration == g_mpeg_stub_state.cdStreamGeneration) + { + playback.sawInput = true; + return; + } + + playback = makeFreshPlaybackState(); + } + + if (!playback.sawInput) + { + playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + } + playback.sawInput = true; + + playback.pssBuffer.insert(playback.pssBuffer.end(), data, data + size); + playback.pssGuestAddrs.reserve(playback.pssGuestAddrs.size() + size); + for (size_t i = 0; i < size; ++i) + { + playback.pssGuestAddrs.push_back(guestAddr + static_cast(i)); + } + processPssBuffer(mpegAddr, playback, callbackEvents); + if (!playback.decodedFrames.empty()) + { + clearNoFrameStall(playback); + } + } + + size_t appendGuestBytes(uint32_t mpegAddr, + MpegPlaybackState &playback, + const uint8_t *rdram, + uint32_t addr, + size_t size, + std::vector &callbackEvents) + { + size_t copied = 0u; + while (copied < size) + { + const uint32_t curAddr = addr + static_cast(copied); + const uint32_t offset = curAddr & PS2_RAM_MASK; + size_t chunk = std::min(size - copied, PS2_RAM_SIZE - offset); + if (chunk == 0u) + { + break; + } + + const uint8_t *src = getConstMemPtr(rdram, curAddr); + if (!src) + { + break; + } + + appendPssBytes( + mpegAddr, + playback, + src, + chunk, + curAddr, + callbackEvents); + copied += chunk; + } + return copied; + } + + size_t appendGuestRingBytes(uint32_t mpegAddr, + MpegPlaybackState &playback, + const uint8_t *rdram, + uint32_t dataAddr, + uint32_t byteCount, + uint32_t ringBaseAddr, + uint32_t ringSize, + std::vector &callbackEvents) + { + if (byteCount == 0u) + { + return 0u; + } + + const uint32_t base = ringBaseAddr & PS2_RAM_MASK; + const uint32_t data = dataAddr & PS2_RAM_MASK; + if (ringBaseAddr != 0u && ringSize != 0u && ringSize <= PS2_RAM_SIZE) + { + const uint32_t ringOffset = (data - base) & PS2_RAM_MASK; + if (ringOffset < ringSize) + { + const uint32_t first = std::min(byteCount, ringSize - ringOffset); + size_t copied = appendGuestBytes( + mpegAddr, + playback, + rdram, + dataAddr, + first, + callbackEvents); + if (copied < first) + { + return copied; + } + + const uint32_t remaining = byteCount - first; + if (remaining != 0u) + { + copied += appendGuestBytes( + mpegAddr, + playback, + rdram, + ringBaseAddr, + remaining, + callbackEvents); + } + return copied; + } + } + + return appendGuestBytes(mpegAddr, playback, rdram, dataAddr, byteCount, callbackEvents); + } + + bool writeMpegCallbackData(uint8_t *rdram, uint32_t addr, const MpegStreamCallbackEvent &event) + { + if (!rdram || addr == 0u) + { + return false; + } + + uint8_t *data = getMemPtr(rdram, addr); + if (!data) + { + return false; + } + + std::memset(data, 0, kMpegCallbackDataSize); + *reinterpret_cast(data + 0x00u) = event.streamType; + *reinterpret_cast(data + 0x08u) = event.dataAddr; + *reinterpret_cast(data + 0x0Cu) = event.len; + *reinterpret_cast(data + 0x10u) = event.pts; + *reinterpret_cast(data + 0x18u) = event.dts; + return true; + } + + void dispatchGuestStreamCallback(uint8_t *rdram, + R5900Context *callerCtx, + PS2Runtime *runtime, + const MpegStreamCallbackEvent &event, + const MpegRegisteredCallback &callback) + { + if (!rdram || !callerCtx || !runtime || callback.func == 0u || !runtime->hasFunction(callback.func)) + { + return; + } + + thread_local PS2Runtime *s_callbackStackRuntime = nullptr; + thread_local uint32_t s_callbackStackTop = 0u; + if (s_callbackStackRuntime != runtime || s_callbackStackTop == 0u) + { + constexpr uint32_t kCallbackStackSize = 0x4000u; + s_callbackStackRuntime = runtime; + s_callbackStackTop = runtime->reserveAsyncCallbackStack(kCallbackStackSize, 16u); + } + + const uint32_t cbDataAddr = runtime->guestMalloc(kMpegCallbackDataSize, 16u); + if (cbDataAddr == 0u) + { + return; + } + if (!writeMpegCallbackData(rdram, cbDataAddr, event)) + { + runtime->guestFree(cbDataAddr); + return; + } + + R5900Context callbackCtx = *callerCtx; + SET_GPR_U32(&callbackCtx, 4, event.mpegAddr); + SET_GPR_U32(&callbackCtx, 5, cbDataAddr); + SET_GPR_U32(&callbackCtx, 6, callback.data); + SET_GPR_U32(&callbackCtx, 7, 0u); + SET_GPR_U32(&callbackCtx, 29, (s_callbackStackTop != 0u) ? s_callbackStackTop : (PS2_RAM_SIZE - 0x10u)); + SET_GPR_U32(&callbackCtx, 31, 0u); + callbackCtx.pc = callback.func; + + uint32_t steps = 0u; + while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < kMpegCallbackMaxSteps) + { + if (!runtime->hasFunction(callbackCtx.pc)) + { + static uint32_t badPcLogCount = 0u; + if (badPcLogCount < 16u) + { + std::cerr << "[MPEG:callback:bad-pc] cb=0x" << std::hex << callback.func + << " pc=0x" << callbackCtx.pc + << " ra=0x" << getRegU32(&callbackCtx, 31) + << std::dec << std::endl; + ++badPcLogCount; + } + break; + } + + PS2Runtime::RecompiledFunction step = runtime->lookupFunction(callbackCtx.pc); + if (!step) + { + break; + } + + { + PS2Runtime::GuestExecutionScope guestExecution(runtime); + step(rdram, &callbackCtx, runtime); + } + ++steps; + } + + if (steps >= kMpegCallbackMaxSteps) + { + static uint32_t stepLimitLogCount = 0u; + if (stepLimitLogCount < 16u) + { + std::cerr << "[MPEG:callback:step-limit] cb=0x" << std::hex << callback.func + << " pc=0x" << callbackCtx.pc << std::dec << std::endl; + ++stepLimitLogCount; + } + } + + runtime->guestFree(cbDataAddr); + } + + void dispatchStreamCallbacks(uint8_t *rdram, + R5900Context *ctx, + PS2Runtime *runtime, + const std::vector &events) + { + if (events.empty()) + { + return; + } + + for (const MpegStreamCallbackEvent &event : events) + { + for (const MpegRegisteredCallback &callback : event.callbacks) + { + dispatchGuestStreamCallback(rdram, ctx, runtime, event, callback); + } + } + } + + void dispatchStreamCallbacksUnlocked(uint8_t *rdram, + R5900Context *ctx, + PS2Runtime *runtime, + const std::vector &events) + { + if (events.empty()) + { + return; + } + + PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); + dispatchStreamCallbacks(rdram, ctx, runtime, events); + } + + void writeBlankMpegFrame(uint8_t *rdram, uint32_t destAddr, uint32_t width, uint32_t height) + { + if (!rdram || destAddr == 0u) + { + return; + } + + const uint32_t outWidth = align16(width == 0u ? kStubMovieWidth : width); + const uint32_t outHeight = align16(height == 0u ? kStubMovieHeight : height); + const uint32_t macroblockColumns = outWidth / 16u; + for (uint32_t mbx = 0u; mbx < macroblockColumns; ++mbx) + { + const size_t stripOffset = + static_cast(mbx) * static_cast(outHeight) * 16u * 4u; + for (uint32_t y = 0u; y < outHeight; ++y) + { + uint8_t *dst = getMemPtr( + rdram, + destAddr + static_cast(stripOffset + static_cast(y) * 16u * 4u)); + if (!dst) + { + continue; + } + for (uint32_t x = 0u; x < 16u; ++x) + { + dst[x * 4u + 0u] = 0u; + dst[x * 4u + 1u] = 0u; + dst[x * 4u + 2u] = 0u; + dst[x * 4u + 3u] = 0x80u; + } + } + } + } + + void writeDecodedFrameToGuest(uint8_t *rdram, uint32_t destAddr, const MpegDecodedFrame &frame) + { + if (!rdram || destAddr == 0u || frame.rgba.empty() || frame.width <= 0 || frame.height <= 0) + { + return; + } + + const uint32_t width = static_cast(frame.width); + const uint32_t height = static_cast(frame.height); + const uint32_t outWidth = align16(width); + const uint32_t outHeight = align16(height); + const uint32_t macroblockColumns = outWidth / 16u; + + for (uint32_t mbx = 0u; mbx < macroblockColumns; ++mbx) + { + const size_t stripOffset = + static_cast(mbx) * static_cast(outHeight) * 16u * 4u; + for (uint32_t y = 0u; y < outHeight; ++y) + { + uint8_t *dst = getMemPtr( + rdram, + destAddr + static_cast(stripOffset + static_cast(y) * 16u * 4u)); + if (!dst) + { + continue; + } + + for (uint32_t x = 0u; x < 16u; ++x) + { + const uint32_t srcX = mbx * 16u + x; + const uint8_t *src = nullptr; + if (srcX < width && y < height) + { + src = frame.rgba.data() + + (static_cast(y) * static_cast(width) + srcX) * 4u; + } + + if (src) + { + dst[x * 4u + 0u] = src[0u]; + dst[x * 4u + 1u] = src[1u]; + dst[x * 4u + 2u] = src[2u]; + dst[x * 4u + 3u] = 0x80u; + } + else + { + dst[x * 4u + 0u] = 0u; + dst[x * 4u + 1u] = 0u; + dst[x * 4u + 2u] = 0u; + dst[x * 4u + 3u] = 0x80u; + } + } + } + } + } + void resetMpegStubStateUnlocked() { - const PS2MpegCompatLayout compat = g_mpeg_stub_state.compat; g_mpeg_stub_state.initialized = false; g_mpeg_stub_state.nextCallbackHandle = 1u; + g_mpeg_stub_state.cdStreamGeneration = 0u; + g_mpeg_stub_state.currentCdStreamEofSeen = false; + g_mpeg_stub_state.feedEsTraceCount = 0u; + g_mpeg_stub_state.demuxPssTraceCount = 0u; + g_mpeg_stub_state.demuxRingTraceCount = 0u; + g_mpeg_stub_state.getPictureWaitTraceCount = 0u; + g_mpeg_stub_state.pictureTraceCount = 0u; + g_mpeg_stub_state.isEndTraceCount = 0u; g_mpeg_stub_state.callbacksByMpeg.clear(); g_mpeg_stub_state.playbackByMpeg.clear(); - g_mpeg_stub_state.compat = compat; } } - void setMpegCompatLayout(const PS2MpegCompatLayout &layout) - { - std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.compat = layout; - } - - void clearMpegCompatLayout() - { - std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.compat = {}; - } - void resetMpegStubState() { std::lock_guard lock(g_mpeg_stub_mutex); resetMpegStubStateUnlocked(); + g_mpeg_cv.notify_all(); + } + + void notifyMpegCdStreamStart() + { + std::lock_guard lock(g_mpeg_stub_mutex); + ++g_mpeg_stub_state.cdStreamGeneration; + g_mpeg_stub_state.currentCdStreamEofSeen = false; + g_mpeg_stub_state.feedEsTraceCount = 0u; + g_mpeg_stub_state.demuxPssTraceCount = 0u; + g_mpeg_stub_state.demuxRingTraceCount = 0u; + g_mpeg_stub_state.getPictureWaitTraceCount = 0u; + g_mpeg_stub_state.pictureTraceCount = 0u; + g_mpeg_stub_state.isEndTraceCount = 0u; + + for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) + { + playback = makeFreshPlaybackStatePreservingConfig(playback); + } + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:CdStreamStart] generation=" << g_mpeg_stub_state.cdStreamGeneration + << " reopened=" << g_mpeg_stub_state.playbackByMpeg.size() << std::endl; + }); + g_mpeg_cv.notify_all(); + } + + void notifyMpegCdStreamEof() + { + std::lock_guard lock(g_mpeg_stub_mutex); + g_mpeg_stub_state.currentCdStreamEofSeen = true; + bool changed = false; + for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) + { + if (!playback.sawInput || playback.streamEnded) + { + continue; + } + + finishPlaybackStream(mpegAddr, playback); + changed = true; + } + + if (changed) + { + static uint32_t s_eofLogCount = 0u; + if (s_eofLogCount < 8u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:CdStreamEof] finalized active MPEG playback" << std::endl; + }); + ++s_eofLogCount; + } + g_mpeg_cv.notify_all(); + } } void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegFlush", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + + const uint32_t mpegAddr = getRegU32(ctx, 4); + std::lock_guard lock(g_mpeg_stub_mutex); + MpegPlaybackState &playback = getPlaybackState(mpegAddr); + if (playback.decoder) + { + playback.decoder->flush(playback.decodedFrames); + } + g_mpeg_cv.notify_all(); + setReturnS32(ctx, 0); } void sceMpegAddBs(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegAddBs", rdram, ctx, runtime); + (void)runtime; + + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t dataAddr = getRegU32(ctx, 5); + const uint32_t byteCount = getRegU32(ctx, 6); + + std::lock_guard lock(g_mpeg_stub_mutex); + MpegPlaybackState &playback = getPlaybackState(mpegAddr); + size_t copied = 0u; + while (copied < byteCount) + { + const uint32_t curAddr = dataAddr + static_cast(copied); + const uint32_t offset = curAddr & PS2_RAM_MASK; + const size_t chunk = std::min(static_cast(byteCount) - copied, PS2_RAM_SIZE - offset); + const uint8_t *src = getConstMemPtr(rdram, curAddr); + if (!src || chunk == 0u) + { + break; + } + feedElementaryStream(playback, src, chunk); + copied += chunk; + } + + g_mpeg_cv.notify_all(); + setReturnS32(ctx, static_cast(copied)); } void sceMpegAddCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -98,15 +1616,26 @@ namespace ps2_stubs const uint32_t handle = g_mpeg_stub_state.nextCallbackHandle++; g_mpeg_stub_state.callbacksByMpeg[mpegAddr].push_back( - MpegRegisteredCallback{callbackType, callbackFunc, callbackData, handle}); + MpegRegisteredCallback{callbackType, 0u, callbackFunc, callbackData, handle, false}); setReturnU32(ctx, handle); } void sceMpegAddStrCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - (void)rdram; (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t streamType = getRegU32(ctx, 5); + const uint32_t streamId = getRegU32(ctx, 6); + const uint32_t callbackFunc = getRegU32(ctx, 7); + const uint32_t callbackData = readAbiArg4(rdram, ctx); + + std::lock_guard lock(g_mpeg_stub_mutex); + g_mpeg_stub_state.initialized = true; + (void)getPlaybackState(mpegAddr); + const uint32_t handle = g_mpeg_stub_state.nextCallbackHandle++; + g_mpeg_stub_state.callbacksByMpeg[mpegAddr].push_back( + MpegRegisteredCallback{streamType, streamId, callbackFunc, callbackData, handle, true}); setReturnU32(ctx, 0u); } @@ -163,7 +1692,7 @@ namespace ps2_stubs { std::lock_guard lock(g_mpeg_stub_mutex); - getPlaybackState(param_1) = {}; + getPlaybackState(param_1) = makeFreshPlaybackState(); } const uint32_t puVar4 = uVar3 + 0x108u; @@ -239,60 +1768,303 @@ namespace ps2_stubs void sceMpegDemuxPss(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegDemuxPss", rdram, ctx, runtime); + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t dataAddr = getRegU32(ctx, 5); + const uint32_t byteCount = getRegU32(ctx, 6); + + std::vector callbackEvents; + size_t consumed = 0u; + size_t decodedCount = 0u; + uint32_t traceIdx = 0u; + { + PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); + std::lock_guard lock(g_mpeg_stub_mutex); + MpegPlaybackState &playback = getPlaybackState(mpegAddr); + consumed = appendGuestBytes(mpegAddr, playback, rdram, dataAddr, byteCount, callbackEvents); + decodedCount = playback.decodedFrames.size(); + traceIdx = g_mpeg_stub_state.demuxPssTraceCount++; + } + g_mpeg_cv.notify_all(); + + if (traceIdx < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:DemuxPss] mpeg=0x" << std::hex << mpegAddr + << " data=0x" << dataAddr << std::dec + << " bytes=" << byteCount + << " consumed=" << consumed + << " decoded=" << decodedCount + << " callbacks=" << callbackEvents.size() + << std::endl; + }); + } + + dispatchStreamCallbacksUnlocked(rdram, ctx, runtime, callbackEvents); + setReturnS32(ctx, static_cast(consumed)); } void sceMpegDemuxPssRing(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - (void)rdram; - (void)runtime; + static std::atomic s_demuxRingEntryCount{0u}; + const uint32_t entryIdx = s_demuxRingEntryCount.fetch_add(1u, std::memory_order_relaxed); + if (entryIdx < 4u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:DemuxPssRing:ENTER] call #" << entryIdx + << " pc=0x" << std::hex << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + }); + } + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t dataAddr = getRegU32(ctx, 5); const uint32_t availableBytes = getRegU32(ctx, 6); - setReturnS32(ctx, static_cast(availableBytes)); + const uint32_t ringBaseAddr = getRegU32(ctx, 7); + const uint32_t ringSize = readAbiArg4(rdram, ctx); + + std::vector callbackEvents; + size_t consumed = 0u; + size_t decodedCount = 0u; + uint32_t traceIdx = 0u; + { + // This prevents an ABBA deadlock with sceMpegGetPicture on thread 5, need investigation on other games + PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); + std::lock_guard lock(g_mpeg_stub_mutex); + MpegPlaybackState &playback = getPlaybackState(mpegAddr); + consumed = appendGuestRingBytes( + mpegAddr, + playback, + rdram, + dataAddr, + availableBytes, + ringBaseAddr, + ringSize, + callbackEvents); + decodedCount = playback.decodedFrames.size(); + traceIdx = g_mpeg_stub_state.demuxRingTraceCount++; + } + g_mpeg_cv.notify_all(); + + if (traceIdx < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:DemuxPssRing] mpeg=0x" << std::hex << mpegAddr + << " data=0x" << dataAddr + << " ring=0x" << ringBaseAddr << std::dec + << " avail=" << availableBytes + << " consumed=" << consumed + << " decoded=" << decodedCount + << " callbacks=" << callbackEvents.size() + << std::endl; + }); + } + + dispatchStreamCallbacksUnlocked(rdram, ctx, runtime, callbackEvents); + setReturnS32(ctx, static_cast(consumed)); } void sceMpegDispCenterOffX(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegDispCenterOffX", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + setReturnS32(ctx, 0); } void sceMpegDispCenterOffY(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegDispCenterOffY", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + setReturnS32(ctx, 0); } void sceMpegDispHeight(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegDispHeight", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + std::lock_guard lock(g_mpeg_stub_mutex); + setReturnU32(ctx, getPlaybackState(mpegAddr).height); } void sceMpegDispWidth(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegDispWidth", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + std::lock_guard lock(g_mpeg_stub_mutex); + setReturnU32(ctx, getPlaybackState(mpegAddr).width); } void sceMpegGetDecodeMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegGetDecodeMode", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + std::lock_guard lock(g_mpeg_stub_mutex); + setReturnU32(ctx, getPlaybackState(mpegAddr).decodeMode); } void sceMpegGetPicture(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - (void)runtime; const uint32_t mpegAddr = getRegU32(ctx, 4); - uint32_t picturesServed = 0u; - PS2MpegCompatLayout compat{}; + const uint32_t imageAddr = getRegU32(ctx, 5); + uint32_t width = kStubMovieWidth; + uint32_t height = kStubMovieHeight; + uint32_t frameCount = 0u; + bool haveFrame = false; + MpegDecodedFrame frame; { - std::lock_guard lock(g_mpeg_stub_mutex); + PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); + std::unique_lock lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); - mpegGuestWrite32(rdram, mpegAddr + 0x00u, kStubMovieWidth); - mpegGuestWrite32(rdram, mpegAddr + 0x04u, kStubMovieHeight); - mpegGuestWrite32(rdram, mpegAddr + 0x08u, playback.picturesServed); - picturesServed = playback.picturesServed; - compat = g_mpeg_stub_state.compat; - playback.picturesServed += 1u; + const uint64_t waitCdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + + if (playback.decodedFrames.empty()) + { + playback.consecutiveEmptyGetPicture++; + if (g_mpeg_stub_state.getPictureWaitTraceCount < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:GetPicture] waiting for frames, mpeg=0x" << std::hex << mpegAddr + << std::dec << " ended=" << playback.streamEnded + << " failed=" << playback.decoderFailed + << " sawInput=" << playback.sawInput + << " consec=" << playback.consecutiveEmptyGetPicture << std::endl; + }); + ++g_mpeg_stub_state.getPictureWaitTraceCount; + } + } + + std::shared_ptr currentThreadInfo = nullptr; + { + std::lock_guard mapLock(g_thread_map_mutex); + auto it = g_threads.find(g_currentThreadId); + if (it != g_threads.end()) + currentThreadInfo = it->second; + } + + const auto noFrameWaitStart = std::chrono::steady_clock::now(); + while (runtime && + g_mpeg_stub_state.playbackByMpeg.find(mpegAddr) != g_mpeg_stub_state.playbackByMpeg.end() && + getPlaybackState(mpegAddr).decodedFrames.empty() && + !getPlaybackState(mpegAddr).streamEnded && + !getPlaybackState(mpegAddr).decoderFailed && + g_mpeg_stub_state.cdStreamGeneration == waitCdStreamGeneration && + !runtime->isStopRequested() && + (!currentThreadInfo || !currentThreadInfo->terminated.load(std::memory_order_relaxed))) + { + g_mpeg_cv.wait_for(lock, std::chrono::milliseconds(8)); + + auto playbackIt = g_mpeg_stub_state.playbackByMpeg.find(mpegAddr); + if (playbackIt == g_mpeg_stub_state.playbackByMpeg.end()) + { + break; + } + + MpegPlaybackState &waitPlayback = playbackIt->second; + if (maybeFinishNoFrameStall(mpegAddr, waitPlayback)) + { + break; + } + + if (!g_mpeg_stub_state.currentCdStreamEofSeen && + std::chrono::steady_clock::now() - noFrameWaitStart >= kMpegGetPictureNoFrameWaitTimeout) + { + static uint32_t s_noFrameYieldLogCount = 0u; + if (s_noFrameYieldLogCount < 16u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:GetPicture:yield] mpeg=0x" << std::hex << mpegAddr + << std::dec << " generation=" << g_mpeg_stub_state.cdStreamGeneration + << " sawInput=" << waitPlayback.sawInput + << " served=" << waitPlayback.picturesServed + << " cdEof=" << g_mpeg_stub_state.currentCdStreamEofSeen + << std::endl; + }); + ++s_noFrameYieldLogCount; + } + break; + } + } + + if (g_mpeg_stub_state.playbackByMpeg.find(mpegAddr) == g_mpeg_stub_state.playbackByMpeg.end()) + { + // The MPEG decoder was deleted while we were waiting. + setReturnS32(ctx, -1); + return; + } + + if (!playback.decodedFrames.empty()) + { + frame = std::move(playback.decodedFrames.front()); + playback.decodedFrames.pop_front(); + playback.width = static_cast(frame.width); + playback.height = static_cast(frame.height); + width = playback.width; + height = playback.height; + frameCount = playback.picturesServed; + playback.picturesServed += 1u; + playback.consecutiveEmptyGetPicture = 0u; + playback.lastFrame = frame; + playback.hasLastFrame = true; + haveFrame = true; + if (g_mpeg_stub_state.pictureTraceCount < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:GetPicture:FRAME] mpeg=0x" << std::hex << mpegAddr + << std::dec << " generation=" << g_mpeg_stub_state.cdStreamGeneration + << " frame=" << frameCount + << " queued=" << playback.decodedFrames.size() + << " size=" << width << "x" << height << std::endl; + }); + ++g_mpeg_stub_state.pictureTraceCount; + } + if (!playback.decodedFrames.empty()) + { + clearNoFrameStall(playback); + } + } + else if (!g_mpeg_stub_state.currentCdStreamEofSeen && + playback.sawInput && + playback.hasLastFrame && + playback.picturesServed > 0u && + !playback.streamEnded && + !playback.decoderFailed) + { + frame = playback.lastFrame; + width = static_cast(frame.width); + height = static_cast(frame.height); + frameCount = playback.picturesServed; + playback.picturesServed += 1u; + playback.consecutiveEmptyGetPicture = 0u; + haveFrame = true; + + static uint32_t s_duplicateFrameLogCount = 0u; + if (s_duplicateFrameLogCount < 16u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:GetPicture:DUP] mpeg=0x" << std::hex << mpegAddr + << std::dec << " generation=" << g_mpeg_stub_state.cdStreamGeneration + << " frame=" << frameCount + << " size=" << width << "x" << height << std::endl; + }); + ++s_duplicateFrameLogCount; + } + } + else + { + maybeFinishNoFrameStall(mpegAddr, playback); + width = playback.width; + height = playback.height; + frameCount = playback.picturesServed; + } } + mpegGuestWrite32(rdram, mpegAddr + 0x00u, width); + mpegGuestWrite32(rdram, mpegAddr + 0x04u, height); + mpegGuestWrite32(rdram, mpegAddr + 0x08u, frameCount); + if (uint8_t *base = getMemPtr(rdram, mpegAddr)) { const uint32_t iVar1 = *reinterpret_cast(base + 0x40); @@ -306,23 +2078,16 @@ namespace ps2_stubs } } - if (compat.matchesMpegObject(mpegAddr) && - compat.hasFinishTargets() && - (picturesServed + 1u) >= mpegCompatSyntheticFrames(compat)) + if (haveFrame) { - // No decoder yet: synthesize a safe frame so the guest can - // initialize its movie presentation path, then mark playback finished. - if (compat.videoStateAddr != 0u) - { - mpegGuestWrite32(rdram, compat.videoStateAddr, compat.finishedVideoStateValue); - } - if (compat.movieStateAddr != 0u) - { - mpegGuestWrite32(rdram, compat.movieStateAddr, compat.finishedMovieStateValue); - } + writeDecodedFrameToGuest(rdram, imageAddr, frame); + } + else if (frameCount == 0u) + { + writeBlankMpegFrame(rdram, imageAddr, width, height); } - setReturnU32(ctx, 0u); + setReturnS32(ctx, 0); } void sceMpegGetPictureRAW8(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -341,34 +2106,51 @@ namespace ps2_stubs (void)runtime; std::lock_guard lock(g_mpeg_stub_mutex); + const uint64_t cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + const bool currentCdStreamEofSeen = g_mpeg_stub_state.currentCdStreamEofSeen; resetMpegStubStateUnlocked(); g_mpeg_stub_state.initialized = true; + g_mpeg_stub_state.cdStreamGeneration = cdStreamGeneration; + g_mpeg_stub_state.currentCdStreamEofSeen = currentCdStreamEofSeen; + g_mpeg_cv.notify_all(); setReturnU32(ctx, 0u); } void sceMpegIsEnd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; - (void)runtime; + // runtime used below for GuestExecutionReleaseScope const uint32_t mpegAddr = getRegU32(ctx, 4); + PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); std::lock_guard lock(g_mpeg_stub_mutex); g_mpeg_stub_state.initialized = true; - const MpegPlaybackState &playback = getPlaybackState(mpegAddr); - if (g_mpeg_stub_state.compat.matchesMpegObject(mpegAddr)) + MpegPlaybackState &playback = getPlaybackState(mpegAddr); + maybeFinishNoFrameStall(mpegAddr, playback); + const bool ended = playback.streamEnded || (playback.decoderFailed && playback.sawInput); + + if (g_mpeg_stub_state.isEndTraceCount < 16u) { - setReturnS32(ctx, playback.picturesServed >= mpegCompatSyntheticFrames(g_mpeg_stub_state.compat) ? 1 : 0); - return; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:IsEnd] mpeg=0x" << std::hex << mpegAddr << std::dec + << " ended=" << ended + << " frames=" << playback.decodedFrames.size() + << " sawInput=" << playback.sawInput << std::endl; + }); + ++g_mpeg_stub_state.isEndTraceCount; } - // Generic fallback: keep decode threads alive until a game-specific path - // decides to stop playback. - setReturnS32(ctx, 0); + setReturnS32(ctx, (ended && playback.decodedFrames.empty()) ? 1 : 0); } void sceMpegIsRefBuffEmpty(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegIsRefBuffEmpty", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + std::lock_guard lock(g_mpeg_stub_mutex); + const MpegPlaybackState &playback = getPlaybackState(mpegAddr); + setReturnS32(ctx, playback.decodedFrames.empty() ? 1 : 0); } void sceMpegReset(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -377,7 +2159,15 @@ namespace ps2_stubs const uint32_t param_1 = getRegU32(ctx, 4); { std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.playbackByMpeg[param_1] = {}; + MpegPlaybackState &playback = getPlaybackState(param_1); + MpegPlaybackState resetState = makeFreshPlaybackStatePreservingConfig(playback); + if (playback.streamEnded || playback.decoderFailed) + { + resetState.sawInput = true; + resetState.streamEnded = true; + resetState.cdStreamGeneration = playback.cdStreamGeneration; + } + playback = std::move(resetState); } uint8_t *base = getMemPtr(rdram, param_1); if (!base) @@ -401,21 +2191,37 @@ namespace ps2_stubs void sceMpegResetDefaultPtsGap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegResetDefaultPtsGap", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + setReturnS32(ctx, 0); } void sceMpegSetDecodeMode(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegSetDecodeMode", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t mode = getRegU32(ctx, 5); + std::lock_guard lock(g_mpeg_stub_mutex); + getPlaybackState(mpegAddr).decodeMode = mode; + setReturnS32(ctx, 0); } void sceMpegSetDefaultPtsGap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegSetDefaultPtsGap", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + setReturnS32(ctx, 0); } void sceMpegSetImageBuff(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - TODO_NAMED("sceMpegSetImageBuff", rdram, ctx, runtime); + (void)rdram; + (void)runtime; + const uint32_t mpegAddr = getRegU32(ctx, 4); + const uint32_t imageBufferAddr = getRegU32(ctx, 5); + std::lock_guard lock(g_mpeg_stub_mutex); + getPlaybackState(mpegAddr).imageBufferAddr = imageBufferAddr; + setReturnS32(ctx, 0); } } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h index a77dacc..bd9a8d5 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h @@ -5,6 +5,8 @@ namespace ps2_stubs { void resetMpegStubState(); + void notifyMpegCdStreamStart(); + void notifyMpegCdStreamEof(); void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMpegAddBs(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMpegAddCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.cpp index 1f82c35..8524162 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.cpp @@ -464,6 +464,40 @@ namespace ps2_stubs } } + + MemoryCardDebugSnapshot getMemoryCardDebugSnapshot() + { + MemoryCardDebugSnapshot snapshot{}; + std::lock_guard lock(g_mcStateMutex); + snapshot.nextFd = g_mcNextFd; + snapshot.lastCmd = g_mcLastCmd; + snapshot.lastResult = g_mcLastResult; + snapshot.cvFileCursor = g_cvMcFileCursor; + + for (size_t i = 0; i < g_mcPorts.size(); ++i) + { + snapshot.ports[i].port = static_cast(i); + snapshot.ports[i].currentDir = g_mcPorts[i].currentDir; + snapshot.ports[i].formatted = g_mcPorts[i].formatted; + snapshot.ports[i].rootPath = getMcRootPath(static_cast(i)); + } + + snapshot.openFiles.reserve(g_mcFiles.size()); + for (const auto &[fd, file] : g_mcFiles) + { + MemoryCardDebugOpenFile row{}; + row.fd = fd; + row.port = file.port; + row.hostPath = file.hostPath; + snapshot.openFiles.push_back(std::move(row)); + } + std::sort(snapshot.openFiles.begin(), snapshot.openFiles.end(), [](const MemoryCardDebugOpenFile &a, const MemoryCardDebugOpenFile &b) + { + return a.fd < b.fd; + }); + return snapshot; + } + void sceMcChangeThreadPriority(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { setReturnS32(ctx, 0); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.h b/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.h index 8cb953d..2a71bb2 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MemoryCard.h @@ -2,8 +2,41 @@ #include "ps2_stubs.h" +#include +#include +#include +#include +#include + namespace ps2_stubs { + + struct MemoryCardDebugOpenFile + { + int32_t fd = 0; + int32_t port = 0; + std::filesystem::path hostPath; + }; + + struct MemoryCardDebugPort + { + int32_t port = 0; + std::string currentDir; + bool formatted = false; + std::filesystem::path rootPath; + }; + + struct MemoryCardDebugSnapshot + { + int32_t nextFd = 0; + int32_t lastCmd = 0; + int32_t lastResult = 0; + int32_t cvFileCursor = 0; + std::array ports{}; + std::vector openFiles; + }; + + MemoryCardDebugSnapshot getMemoryCardDebugSnapshot(); void sceMcChangeThreadPriority(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMcChdir(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMcClose(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp index 170ffd0..b40edd5 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.cpp @@ -11,6 +11,7 @@ namespace ps2_stubs constexpr int32_t kPadTypeDigital = 4; constexpr int32_t kPadTypeDualShock = 7; constexpr int32_t kPadStateDisconnected = 0; + constexpr int32_t kPadStateExecCmd = 5; constexpr int32_t kPadStateStable = 6; constexpr size_t kPadPortCount = 2; constexpr size_t kPadSlotCount = 1; @@ -46,9 +47,17 @@ namespace ps2_stubs bool open = false; bool analogMode = true; bool pressureEnabled = false; + bool lastUsedOverride = false; + bool lastUsedBackend = false; + bool lastReadOk = false; uint16_t buttonMask = 0xFFFFu; uint32_t dmaAddr = 0u; uint32_t reqState = 0u; + uint32_t transientState = 0u; + PadInputState lastInput{}; + uint8_t lastData[32]{}; + uint32_t readCount = 0u; + uint32_t lastReadDataAddr = 0u; }; std::mutex g_padOverrideMutex; @@ -212,6 +221,12 @@ namespace ps2_stubs portState.buttonMask = 0xFFFFu; portState.dmaAddr = dmaAddr; portState.reqState = 0u; + portState.transientState = 0u; + } + + void queueExecCmdStateLocked(PadPortState &portState) + { + portState.transientState = static_cast(kPadStateExecCmd); } uint8_t pressureValue(const PadInputState &state, const PadPortState &portState, uint16_t mask) @@ -251,7 +266,7 @@ namespace ps2_stubs data[19] = pressureValue(state, portState, kPadBtnR2); } - bool readPadPortData(int port, int slot, PS2Runtime *runtime, uint8_t *outData) + bool readPadPortData(int port, int slot, PS2Runtime *runtime, uint8_t *outData, uint32_t dataAddr) { if (!outData) { @@ -280,6 +295,7 @@ namespace ps2_stubs } } + bool usedBackend = false; if (!useOverride) { uint8_t backendData[32]{}; @@ -290,6 +306,7 @@ namespace ps2_stubs state.ry = backendData[5]; state.lx = backendData[6]; state.ly = backendData[7]; + usedBackend = true; } else { @@ -300,6 +317,20 @@ namespace ps2_stubs fillPadStatus(outData, state, portState); + { + std::lock_guard lock(g_padStateMutex); + if (PadPortState *sharedPortState = lookupPadPortStateLocked(port, slot)) + { + sharedPortState->lastInput = state; + std::memcpy(sharedPortState->lastData, outData, sizeof(sharedPortState->lastData)); + sharedPortState->lastUsedOverride = useOverride; + sharedPortState->lastUsedBackend = usedBackend; + sharedPortState->lastReadOk = true; + sharedPortState->lastReadDataAddr = dataAddr; + ++sharedPortState->readCount; + } + } + return true; } } @@ -334,6 +365,7 @@ namespace ps2_stubs portState->pressureEnabled = true; portState->reqState = 0u; + queueExecCmdStateLocked(*portState); setReturnS32(ctx, 1); } @@ -351,6 +383,7 @@ namespace ps2_stubs portState->pressureEnabled = false; portState->reqState = 0u; + queueExecCmdStateLocked(*portState); setReturnS32(ctx, 1); } @@ -422,9 +455,22 @@ namespace ps2_stubs (void)rdram; (void)runtime; std::lock_guard lock(g_padStateMutex); - const PadPortState *portState = lookupPadPortStateLocked(static_cast(getRegU32(ctx, 4)), - static_cast(getRegU32(ctx, 5))); - setReturnS32(ctx, (portState && portState->open) ? kPadStateStable : kPadStateDisconnected); + PadPortState *portState = lookupPadPortStateLocked(static_cast(getRegU32(ctx, 4)), + static_cast(getRegU32(ctx, 5))); + int32_t state = kPadStateDisconnected; + if (portState && portState->open) + { + if (portState->transientState != 0u) + { + state = static_cast(portState->transientState); + portState->transientState = 0u; + } + else + { + state = kPadStateStable; + } + } + setReturnS32(ctx, state); } void scePadInfoAct(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -545,6 +591,7 @@ namespace ps2_stubs portState->open = false; portState->pressureEnabled = false; portState->reqState = 0u; + portState->transientState = 0u; setReturnS32(ctx, 1); } @@ -568,6 +615,7 @@ namespace ps2_stubs portState->buttonMask = 0xFFFFu; portState->dmaAddr = dmaAddr; portState->reqState = 0u; + portState->transientState = 0u; if (dmaStr) { std::memset(dmaStr, 0, 32); @@ -587,30 +635,32 @@ namespace ps2_stubs return; } - if (!readPadPortData(port, slot, runtime, data)) + if (!readPadPortData(port, slot, runtime, data, dataAddr)) { setReturnS32(ctx, 0); return; } - if (g_padReadLogCount < 48) - { - const int gamepad = findFirstGamepad(); - const bool gamepadStartPressed = - (gamepad >= 0) && IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_RIGHT); - const bool startPressed = (data[2] != 0xFFu || data[3] != 0xFFu || - IsKeyDown(KEY_ENTER) || gamepadStartPressed); - if (startPressed) + PS2_IF_AGRESSIVE_LOGS({ + if (g_padReadLogCount < 48) { - const uint32_t guestButtons = - (static_cast(static_cast(data[2] ^ 0xFFu)) << 8) | - static_cast(static_cast(data[3] ^ 0xFFu)); - std::printf("[padread] port=%d slot=%d data2=0x%02x data3=0x%02x guestButtons=0x%04x enter=%d gamepadStart=%d\n", - port, slot, data[2], data[3], guestButtons, - IsKeyDown(KEY_ENTER) ? 1 : 0, gamepadStartPressed ? 1 : 0); - ++g_padReadLogCount; + const int gamepad = findFirstGamepad(); + const bool gamepadStartPressed = + (gamepad >= 0) && IsGamepadButtonDown(gamepad, GAMEPAD_BUTTON_MIDDLE_RIGHT); + const bool startPressed = (data[2] != 0xFFu || data[3] != 0xFFu || + IsKeyDown(KEY_ENTER) || gamepadStartPressed); + if (startPressed) + { + const uint32_t guestButtons = + (static_cast(static_cast(data[2] ^ 0xFFu)) << 8) | + static_cast(static_cast(data[3] ^ 0xFFu)); + std::printf("[padread] port=%d slot=%d data2=0x%02x data3=0x%02x guestButtons=0x%04x enter=%d gamepadStart=%d\n", + port, slot, data[2], data[3], guestButtons, + IsKeyDown(KEY_ENTER) ? 1 : 0, gamepadStartPressed ? 1 : 0); + ++g_padReadLogCount; + } } - } + }); setReturnS32(ctx, 1); } @@ -658,6 +708,7 @@ namespace ps2_stubs { portState->buttonMask = static_cast(getRegU32(ctx, 6)); portState->reqState = 0u; + queueExecCmdStateLocked(*portState); } setReturnS32(ctx, 1); } @@ -677,6 +728,7 @@ namespace ps2_stubs portState->analogMode = (getRegU32(ctx, 6) != 0u); portState->reqState = 0u; + queueExecCmdStateLocked(*portState); setReturnS32(ctx, 1); } @@ -690,6 +742,7 @@ namespace ps2_stubs if (portState && portState->open) { portState->reqState = static_cast(getRegU32(ctx, 6) ? 1u : 0u); + queueExecCmdStateLocked(*portState); } setReturnS32(ctx, 1); } @@ -729,6 +782,10 @@ namespace ps2_stubs { text = "FINDPAD"; } + else if (state == 5) + { + text = "EXECCMD"; + } else if (state == 0) { text = "DISCONNECTED"; @@ -739,6 +796,51 @@ namespace ps2_stubs setReturnS32(ctx, 0); } + PadDebugSnapshot getPadDebugSnapshot() + { + PadDebugSnapshot snapshot{}; + { + std::lock_guard lock(g_padOverrideMutex); + snapshot.overrideEnabled = g_padOverrideEnabled; + snapshot.overrideButtons = g_padOverrideState.buttons; + snapshot.overrideRx = g_padOverrideState.rx; + snapshot.overrideRy = g_padOverrideState.ry; + snapshot.overrideLx = g_padOverrideState.lx; + snapshot.overrideLy = g_padOverrideState.ly; + } + + { + std::lock_guard lock(g_padStateMutex); + snapshot.readLogCount = g_padReadLogCount; + for (size_t port = 0; port < kPadDebugPortCount; ++port) + { + for (size_t slot = 0; slot < kPadDebugSlotCount; ++slot) + { + const PadPortState &src = g_padPorts[port]; + PadDebugPortSnapshot &dst = snapshot.ports[port][slot]; + dst.open = src.open; + dst.analogMode = src.analogMode; + dst.pressureEnabled = src.pressureEnabled; + dst.lastUsedOverride = src.lastUsedOverride; + dst.lastUsedBackend = src.lastUsedBackend; + dst.lastReadOk = src.lastReadOk; + dst.buttonMask = src.buttonMask; + dst.lastButtons = src.lastInput.buttons; + dst.dmaAddr = src.dmaAddr; + dst.reqState = src.reqState; + dst.readCount = src.readCount; + dst.lastReadDataAddr = src.lastReadDataAddr; + dst.rx = src.lastInput.rx; + dst.ry = src.lastInput.ry; + dst.lx = src.lastInput.lx; + dst.ly = src.lastInput.ly; + std::memcpy(dst.lastData, src.lastData, sizeof(dst.lastData)); + } + } + } + return snapshot; + } + void setPadOverrideState(uint16_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) { std::lock_guard lock(g_padOverrideMutex); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.h b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.h index 5c284d1..c4de234 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Pad.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Pad.h @@ -2,6 +2,9 @@ #include "ps2_stubs.h" +#include +#include + namespace ps2_stubs { void PadSyncCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); @@ -34,6 +37,45 @@ namespace ps2_stubs void scePadSetVrefParam(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void scePadSetWarningLevel(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void scePadStateIntToStr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); + + static constexpr size_t kPadDebugPortCount = 2u; + static constexpr size_t kPadDebugSlotCount = 1u; + static constexpr size_t kPadDebugDataSize = 32u; + + struct PadDebugPortSnapshot + { + bool open = false; + bool analogMode = false; + bool pressureEnabled = false; + bool lastUsedOverride = false; + bool lastUsedBackend = false; + bool lastReadOk = false; + uint16_t buttonMask = 0xFFFFu; + uint16_t lastButtons = 0xFFFFu; + uint32_t dmaAddr = 0u; + uint32_t reqState = 0u; + uint32_t readCount = 0u; + uint32_t lastReadDataAddr = 0u; + uint8_t rx = 0x80u; + uint8_t ry = 0x80u; + uint8_t lx = 0x80u; + uint8_t ly = 0x80u; + uint8_t lastData[kPadDebugDataSize]{}; + }; + + struct PadDebugSnapshot + { + bool overrideEnabled = false; + uint16_t overrideButtons = 0xFFFFu; + uint8_t overrideRx = 0x80u; + uint8_t overrideRy = 0x80u; + uint8_t overrideLx = 0x80u; + uint8_t overrideLy = 0x80u; + int readLogCount = 0; + PadDebugPortSnapshot ports[kPadDebugPortCount][kPadDebugSlotCount]{}; + }; + + PadDebugSnapshot getPadDebugSnapshot(); void setPadOverrideState(uint16_t buttons, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry); void clearPadOverrideState(); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp index 780591d..6402190 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp @@ -430,10 +430,12 @@ namespace ps2_stubs static uint32_t warnCount = 0; if (warnCount < 32u) { - std::cerr << "sceSifGetOtherData copy failed src=0x" << std::hex << srcAddr - << " dst=0x" << dstAddr - << " size=0x" << size - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "sceSifGetOtherData copy failed src=0x" << std::hex << srcAddr + << " dst=0x" << dstAddr + << " size=0x" << size + << std::dec << std::endl; + }); ++warnCount; } setReturnS32(ctx, -1); @@ -473,13 +475,15 @@ namespace ps2_stubs } if (shouldLog) { - auto flags = std::cerr.flags(); - std::cerr << "[sceSifGetReg] reg=0x" << std::hex << reg - << " value=0x" << value - << " pc=0x" << (ctx ? ctx->pc : 0u) - << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) - << std::dec << std::endl; - std::cerr.flags(flags); + PS2_IF_AGRESSIVE_LOGS({ + auto flags = std::cerr.flags(); + std::cerr << "[sceSifGetReg] reg=0x" << std::hex << reg + << " value=0x" << value + << " pc=0x" << (ctx ? ctx->pc : 0u) + << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) + << std::dec << std::endl; + std::cerr.flags(flags); + }); } setReturnU32(ctx, value); } @@ -620,6 +624,34 @@ namespace ps2_stubs const uint32_t dmatAddr = getRegU32(ctx, 4); const uint32_t count = getRegU32(ctx, 5); + + const uint32_t listAddr = getRegU32(ctx, 4); + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[sceSifSetDma:CALL] pc=0x" << std::hex << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << " list=0x" << listAddr + << " count=" << std::dec << count + << std::endl; + + for (uint32_t i = 0; i < count; ++i) + { + const uint32_t desc = listAddr + i * 16; + const uint32_t src = READ32(desc + 0); + const uint32_t dst = READ32(desc + 4); + const uint32_t size = READ32(desc + 8); + const uint32_t attr = READ32(desc + 12); + + std::cerr << "[sceSifSetDma:DESC] i=" << i + << " src=0x" << std::hex << src + << " dst=0x" << dst + << " size=0x" << size + << " attr=0x" << attr + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + } + }); + if (!dmatAddr || count == 0u || count > 32u) { setReturnS32(ctx, 0); @@ -685,9 +717,11 @@ namespace ps2_stubs static uint32_t warnCount = 0; if (warnCount < 32u) { - std::cerr << "sceSifSetDma failed dmat=0x" << std::hex << dmatAddr - << " count=0x" << count - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "sceSifSetDma failed dmat=0x" << std::hex << dmatAddr + << " count=0x" << count + << std::dec << std::endl; + }); ++warnCount; } setReturnS32(ctx, 0); @@ -726,14 +760,16 @@ namespace ps2_stubs } if (shouldLog) { - auto flags = std::cerr.flags(); - std::cerr << "[sceSifSetReg] reg=0x" << std::hex << reg - << " prev=0x" << prev - << " value=0x" << value - << " pc=0x" << (ctx ? ctx->pc : 0u) - << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) - << std::dec << std::endl; - std::cerr.flags(flags); + PS2_IF_AGRESSIVE_LOGS({ + auto flags = std::cerr.flags(); + std::cerr << "[sceSifSetReg] reg=0x" << std::hex << reg + << " prev=0x" << prev + << " value=0x" << value + << " pc=0x" << (ctx ? ctx->pc : 0u) + << " ra=0x" << (ctx ? getRegU32(ctx, 31) : 0u) + << std::dec << std::endl; + std::cerr.flags(flags); + }); } setReturnU32(ctx, prev); } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h index 5a9f392..8ad6cdc 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h @@ -1,4 +1,5 @@ #include "ps2_syscalls.h" +#include "ps2_log.h" #include "ps2_runtime.h" #include "runtime/ps2_iop_audio.h" #include "ps2_runtime_macros.h" @@ -34,3 +35,11 @@ std::string translatePs2Path(const char *ps2Path); #include "Helpers/State.h" #include "Helpers/Loader.h" #include "Helpers/Runtime.h" + +namespace ps2_syscalls +{ + inline void yieldGuestExecutionAfterWake(PS2Runtime *runtime) + { + runtime->yieldGuestExecutionAfterWake(); + } +} diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h index fd9a2a5..47b560a 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h @@ -1,13 +1,10 @@ -namespace +struct ThreadExitException final : public std::exception { - struct ThreadExitException final : public std::exception + const char *what() const noexcept override { - const char *what() const noexcept override - { - return "PS2 Thread Exit"; - } - }; -} + return "PS2 Thread Exit"; + } +}; static void throwIfTerminated(const std::shared_ptr &info) { @@ -17,6 +14,42 @@ static void throwIfTerminated(const std::shared_ptr &info) } } +// Condition-variable waits in the EE runtime must release the global guest +// execution mutex, but must not reacquire it while still holding the local +// wait-object mutex. Reacquiring guest execution while holding a semaphore, +// thread, event-flag, or vsync mutex can create an ABBA deadlock: +// +// awakened thread: local wait mutex -> waiting for GuestExecutionScope +// running thread: GuestExecutionScope -> waiting for local wait mutex +// +// This helper keeps guest execution released for the whole host wait and for +// any post-wake bookkeeping that needs the local mutex, then unlocks the local +// mutex before the GuestExecutionReleaseScope destructor reacquires guest code. +template +static void waitWithGuestExecutionReleasedUntilUnlocked(PS2Runtime *runtime, + Lock &lock, + WaitFn waitFn, + FinishFn finishFn) +{ + auto releaseGuestExecution = std::make_unique(runtime); + + waitFn(); + finishFn(); + + if (lock.owns_lock()) + { + lock.unlock(); + } + + releaseGuestExecution.reset(); +} + +template +static void waitWithGuestExecutionReleasedUntilUnlocked(PS2Runtime *runtime, Lock &lock, WaitFn waitFn) +{ + waitWithGuestExecutionReleasedUntilUnlocked(runtime, lock, waitFn, []() {}); +} + static void waitWhileSuspended(const std::shared_ptr &info, PS2Runtime *runtime = nullptr) { if (!info) @@ -28,16 +61,29 @@ static void waitWhileSuspended(const std::shared_ptr &info, PS2Runti info->status = THS_SUSPEND; info->waitType = TSW_NONE; info->waitId = 0; - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - info->cv.wait(lock, [&]() - { return info->suspendCount == 0 || info->terminated.load(); }); - } - if (info->terminated.load()) + + bool terminated = false; + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() + { + info->cv.wait(lock, [&]() + { return info->suspendCount == 0 || info->terminated.load(); }); + }, + [&]() + { + terminated = info->terminated.load(); + if (!terminated) + { + info->status = THS_RUN; + } + }); + + if (terminated) { throw ThreadExitException(); } - info->status = THS_RUN; } } @@ -406,14 +452,16 @@ static bool rpcInvokeFunction(uint8_t *rdram, R5900Context *ctx, PS2Runtime *run const uint32_t logIndex = s_rpcInvokeFailureLogs.fetch_add(1u, std::memory_order_relaxed); if (logIndex < kMaxRpcInvokeFailureLogs) { - std::cerr << "[SyscallOverride:invoke-failed]" - << " func=0x" << std::hex << funcAddr - << " exitPc=0x" << tmp.pc - << " ra=0x" << getRegU32(&tmp, 31) - << std::dec - << " steps=" << steps - << " reason=" << rpcInvokeExitReasonName(exitReason) - << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[SyscallOverride:invoke-failed]" + << " func=0x" << std::hex << funcAddr + << " exitPc=0x" << tmp.pc + << " ra=0x" << getRegU32(&tmp, 31) + << std::dec + << " steps=" << steps + << " reason=" << rpcInvokeExitReasonName(exitReason) + << std::endl; + }); } return false; @@ -769,4 +817,3 @@ static void ensureBootModeTable(uint8_t *rdram) g_bootmode_initialized = true; } - diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h index eb3e182..c955176 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h @@ -1,3 +1,8 @@ +#pragma once + +#include +#include + inline std::unordered_map g_fileDescriptors; inline int g_nextFd = 3; // Start after stdin, stdout, stderr @@ -22,6 +27,7 @@ struct ThreadInfo int wakeupCount = 0; int currentPriority = 0; int suspendCount = 0; + std::atomic currentPc{0}; std::mutex m; std::condition_variable cv; @@ -340,6 +346,38 @@ struct RpcClientState uint32_t sid = 0; }; +struct SifRpcDebugEvent +{ + uint64_t seq = 0; + const char *op = ""; + uint32_t pc = 0; + uint32_t ra = 0; + uint32_t threadId = 0; + uint32_t sid = 0; + uint32_t rpcNum = 0; + uint32_t clientPtr = 0; + uint32_t serverPtr = 0; + uint32_t sendBuf = 0; + uint32_t sendSize = 0; + uint32_t recvBuf = 0; + uint32_t recvSize = 0; + uint32_t resultPtr = 0; + uint32_t mode = 0; + uint32_t endFunc = 0; + uint32_t endParam = 0; + uint32_t semaId = 0; + uint32_t flags = 0; + int32_t result = 0; +}; + +static constexpr size_t kSifRpcDebugHistoryCount = 256u; +static constexpr uint32_t kSifRpcDebugFlagNowait = 1u << 0; +static constexpr uint32_t kSifRpcDebugFlagHandledByHle = 1u << 1; +static constexpr uint32_t kSifRpcDebugFlagCallback = 1u << 2; +static constexpr uint32_t kSifRpcDebugFlagMissingClient = 1u << 3; +static constexpr uint32_t kSifRpcDebugFlagServerDispatch = 1u << 4; +static constexpr uint32_t kSifRpcDebugFlagDtx = 1u << 5; + struct SoundDriverRpcState { uintptr_t ownerRuntime = 0; @@ -355,6 +393,8 @@ struct SoundDriverRpcState inline std::unordered_map g_rpc_servers; inline std::unordered_map g_rpc_clients; +inline SifRpcDebugEvent g_sif_rpc_debug_history[kSifRpcDebugHistoryCount]{}; +inline uint64_t g_sif_rpc_debug_next_seq = 0; inline std::mutex g_rpc_mutex; inline std::recursive_mutex g_sif_call_rpc_mutex; inline bool g_rpc_initialized = false; @@ -642,5 +682,4 @@ inline std::mutex g_sif_module_mutex; inline std::unordered_map g_sif_modules_by_id; inline std::unordered_map g_sif_module_id_by_path; inline int32_t g_next_sif_module_id = 1; -inline uint32_t g_sif_module_log_count = 0; - +inline uint32_t g_sif_module_log_count = 0; \ No newline at end of file diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp index ce14411..761befd 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Interrupt.cpp @@ -278,7 +278,19 @@ namespace ps2_syscalls } } - static uint64_t signalVSyncFlag(uint8_t *rdram) + static void updateGsCsrFieldForVSync(PS2Runtime *runtime, uint64_t tickValue) + { + if (!runtime) + { + return; + } + + constexpr uint64_t kGsCsrFieldMask = 0x2000ull; + uint64_t &csr = runtime->memory().gs().csr; + csr = (csr & ~kGsCsrFieldMask) | ((tickValue & 1ull) ? kGsCsrFieldMask : 0ull); + } + + static uint64_t signalVSyncFlag(uint8_t *rdram, PS2Runtime *runtime) { VSyncFlagRegistration reg{}; uint64_t tickValue = 0u; @@ -289,6 +301,7 @@ namespace ps2_syscalls } g_vsync_cv.notify_all(); + updateGsCsrFieldForVSync(runtime, tickValue); if (reg.flagAddr != 0u) { @@ -333,7 +346,7 @@ namespace ps2_syscalls for (int i = 0; i < ticksToProcess; ++i) { - const uint64_t tickValue = signalVSyncFlag(rdram); + const uint64_t tickValue = signalVSyncFlag(rdram, runtime); ps2_stubs::dispatchGsSyncVCallback(rdram, runtime, tickValue); dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankStart); std::this_thread::sleep_for(std::chrono::microseconds(500)); @@ -396,12 +409,20 @@ namespace ps2_syscalls ensureInterruptWorkerRunning(rdram, runtime); std::unique_lock lock(g_vsync_flag_mutex); uint64_t current = g_vsync_tick_counter; - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - g_vsync_cv.wait(lock, [current, runtime]() - { return g_vsync_tick_counter > current || (runtime != nullptr && runtime->isStopRequested()); }); - } - return g_vsync_tick_counter; + uint64_t result = current; + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() + { + g_vsync_cv.wait(lock, [current, runtime]() + { return g_vsync_tick_counter > current || (runtime != nullptr && runtime->isStopRequested()); }); + }, + [&]() + { + result = g_vsync_tick_counter; + }); + return result; } void WaitVSyncTick(uint8_t *rdram, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index f9f26ea..4f0fc7f 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -20,6 +20,28 @@ namespace ps2_syscalls constexpr uint32_t kSoundDriverGuestPoolBase = 0x00120000u; constexpr uint32_t kSoundDriverGuestPoolLimit = 0x00200000u; + SifRpcDebugEvent makeRpcDebugEvent(const char *op, R5900Context *ctx) + { + SifRpcDebugEvent event{}; + event.op = op; + event.pc = ctx ? ctx->pc : 0u; + event.ra = ctx ? getRegU32(ctx, 31) : 0u; + event.threadId = static_cast(g_currentThreadId); + return event; + } + + void pushSifRpcDebugEventLocked(SifRpcDebugEvent event) + { + event.seq = ++g_sif_rpc_debug_next_seq; + g_sif_rpc_debug_history[event.seq % kSifRpcDebugHistoryCount] = event; + } + + void pushSifRpcDebugEvent(SifRpcDebugEvent event) + { + std::lock_guard lock(g_rpc_mutex); + pushSifRpcDebugEventLocked(event); + } + void resetDtxRpcStateUnlocked() { g_dtx_remote_by_id.clear(); @@ -676,6 +698,167 @@ namespace ps2_syscalls state.wkSize == sizeBytes; } + bool dtxHasReadableRange(const uint8_t *rdram, uint32_t addr, uint32_t len) + { + if (len == 0u) + { + return true; + } + if (addr > (addr + len - 1u)) + { + return false; + } + return getConstMemPtr(rdram, addr) && getConstMemPtr(rdram, addr + len - 1u); + } + + bool dtxReadValidCommandCount(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize, + uint32_t &commandCount) + { + constexpr uint32_t kDtxHeaderSize = 16u; + constexpr uint32_t kDtxCommandSize = 16u; + + commandCount = 0u; + if (workSize < 64u || !readGuestU32(rdram, workAddr, commandCount)) + { + return false; + } + if (commandCount == 0u || commandCount > 128u) + { + return false; + } + + const uint64_t commandBytes = static_cast(commandCount) * kDtxCommandSize; + const uint64_t requiredBytes = kDtxHeaderSize + commandBytes + sizeof(uint32_t); + return requiredBytes <= workSize; + } + + bool dtxLooksLikeSjxPayloadLocked(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize) + { + constexpr uint32_t kSjxHeaderSize = 16u; + constexpr uint32_t kSjxCommandSize = 16u; + + uint32_t commandCount = 0u; + if (!dtxReadValidCommandCount(rdram, workAddr, workSize, commandCount)) + { + return false; + } + + for (uint32_t i = 0; i < commandCount; ++i) + { + const uint32_t cmdAddr = workAddr + kSjxHeaderSize + (i * kSjxCommandSize); + const uint8_t *cmdPtr = getConstMemPtr(rdram, cmdAddr); + if (!cmdPtr) + { + break; + } + + const uint8_t cmdNo = cmdPtr[0]; + const uint8_t cmdLine = cmdPtr[1]; + uint16_t cmdXid = 0u; + uint32_t sjxHandle = 0u; + uint32_t chunkDataAddr = 0u; + uint32_t chunkLen = 0u; + std::memcpy(&cmdXid, cmdPtr + 2u, sizeof(cmdXid)); + std::memcpy(&sjxHandle, cmdPtr + 4u, sizeof(sjxHandle)); + std::memcpy(&chunkDataAddr, cmdPtr + 8u, sizeof(chunkDataAddr)); + std::memcpy(&chunkLen, cmdPtr + 12u, sizeof(chunkLen)); + + if (cmdNo != 0u || chunkLen == 0u || !dtxHasReadableRange(rdram, chunkDataAddr, chunkLen)) + { + continue; + } + + auto sjxIt = g_dtx_sjx_by_handle.find(sjxHandle); + if (sjxIt == g_dtx_sjx_by_handle.end()) + { + continue; + } + + const DtxSjxState &sjx = sjxIt->second; + if (sjx.xid != 0u && sjx.xid != cmdXid) + { + continue; + } + if (cmdLine != sjx.line) + { + continue; + } + if (g_dtx_sjrmt_by_handle.find(sjx.dstSjHandle) == g_dtx_sjrmt_by_handle.end()) + { + continue; + } + + return true; + } + + return false; + } + + bool dtxLooksLikePs2RnaPayloadLocked(const uint8_t *rdram, uint32_t workAddr, uint32_t workSize) + { + constexpr uint32_t kPs2RnaHeaderSize = 16u; + constexpr uint32_t kPs2RnaCommandSize = 16u; + + uint32_t commandCount = 0u; + if (!dtxReadValidCommandCount(rdram, workAddr, workSize, commandCount)) + { + return false; + } + + for (uint32_t i = 0; i < commandCount; ++i) + { + const uint32_t cmdAddr = workAddr + kPs2RnaHeaderSize + (i * kPs2RnaCommandSize); + const uint8_t *cmdPtr = getConstMemPtr(rdram, cmdAddr); + if (!cmdPtr) + { + break; + } + + uint16_t cmdNo = 0u; + uint32_t rnaHandle = 0u; + std::memcpy(&cmdNo, cmdPtr, sizeof(cmdNo)); + std::memcpy(&rnaHandle, cmdPtr + 4u, sizeof(rnaHandle)); + + if (cmdNo <= 5u && g_dtx_ps2rna_by_handle.find(rnaHandle) != g_dtx_ps2rna_by_handle.end()) + { + return true; + } + } + + return false; + } + + bool dtxInferTransferFromPayloadLocked(const uint8_t *rdram, uint32_t srcAddr, uint32_t dstAddr, + uint32_t sizeBytes, DtxTransferState &out) + { + if (g_dtx_transfer_by_id.empty()) + { + return false; + } + + if (dtxLooksLikeSjxPayloadLocked(rdram, srcAddr, sizeBytes)) + { + out = {}; + out.dtxId = 0u; + out.eeWorkAddr = srcAddr; + out.iopWorkAddr = dstAddr; + out.wkSize = sizeBytes; + return true; + } + + if (dtxLooksLikePs2RnaPayloadLocked(rdram, srcAddr, sizeBytes)) + { + out = {}; + out.dtxId = 1u; + out.eeWorkAddr = srcAddr; + out.iopWorkAddr = dstAddr; + out.wkSize = sizeBytes; + return true; + } + + return false; + } + bool dtxCopyBytes(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t len) { for (uint32_t i = 0; i < len; ++i) @@ -942,6 +1125,7 @@ namespace ps2_syscalls DtxTransferState matched{}; bool found = false; + bool inferredPayload = false; { std::lock_guard lock(g_dtx_rpc_mutex); if (!g_dtxCompatLayout.isConfigured()) @@ -957,10 +1141,17 @@ namespace ps2_syscalls break; } } + if (!found && dtxInferTransferFromPayloadLocked(rdram, normalizedSrc, normalizedDst, sizeBytes, matched)) + { + found = true; + inferredPayload = true; + } } if (!found) { + const uint8_t *missPtr = getConstMemPtr(rdram, normalizedSrc); + static uint32_t dtxMissLogCount = 0u; if (dtxMissLogCount < 64u) { @@ -968,6 +1159,7 @@ namespace ps2_syscalls uint32_t sampleDtxId = 0u; uint32_t sampleSrc = 0u; uint32_t sampleSize = 0u; + { std::lock_guard lock(g_dtx_rpc_mutex); knownTransfers = static_cast(g_dtx_transfer_by_id.size()); @@ -980,19 +1172,31 @@ namespace ps2_syscalls } } - if (knownTransfers != 0u) + if (missPtr) { - RUNTIME_LOG("[sceSifSetDma:DTX_MISS] src=0x" << std::hex << normalizedSrc - << " dst=0x" << normalizedDst - << " size=0x" << sizeBytes - << " known=" << std::dec << knownTransfers - << " sampleDtxId=0x" << std::hex << sampleDtxId - << " sampleSrc=0x" << sampleSrc - << " sampleSize=0x" << sampleSize - << std::dec << std::endl); - ++dtxMissLogCount; + RUNTIME_LOG("[sceSifSetDma:DTX_MISS_DUMP] src=0x" << std::hex << normalizedSrc + << " dst=0x" << normalizedDst + << " size=0x" << sizeBytes + << " known=" << std::dec << knownTransfers + << " sampleDtxId=0x" << std::hex << sampleDtxId + << " sampleSrc=0x" << sampleSrc + << " sampleSize=0x" << sampleSize + << " first=" + << std::setw(2) << std::setfill('0') << static_cast(missPtr[0]) << " " + << std::setw(2) << static_cast(missPtr[1]) << " " + << std::setw(2) << static_cast(missPtr[2]) << " " + << std::setw(2) << static_cast(missPtr[3]) << " " + << std::setw(2) << static_cast(missPtr[4]) << " " + << std::setw(2) << static_cast(missPtr[5]) << " " + << std::setw(2) << static_cast(missPtr[6]) << " " + << std::setw(2) << static_cast(missPtr[7]) + << std::setfill(' ') + << std::dec << std::endl); } + + ++dtxMissLogCount; } + return; } @@ -1023,6 +1227,10 @@ namespace ps2_syscalls << " size=0x" << matched.wkSize << " ticket=0x" << ticketNo << "->0x" << (ticketNo + 1u)); + if (inferredPayload) + { + RUNTIME_LOG(" inferred=1"); + } if (matched.dtxId == 0u) { uint32_t totalData = 0u; @@ -1170,6 +1378,11 @@ namespace ps2_syscalls g_rpc_packet_index = 0; g_rpc_server_index = 0; g_rpc_active_queue = 0; + g_sif_rpc_debug_next_seq = 0; + for (size_t i = 0; i < kSifRpcDebugHistoryCount; ++i) + { + g_sif_rpc_debug_history[i] = SifRpcDebugEvent{}; + } resetDtxRpcStateUnlocked(); g_soundDriverRpcState.initialized = false; g_rpc_initialized = true; @@ -1180,6 +1393,10 @@ namespace ps2_syscalls resetDtxRpcStateUnlocked(); g_soundDriverRpcState.initialized = false; } + + SifRpcDebugEvent event = makeRpcDebugEvent("InitRpc", ctx); + event.result = 0; + pushSifRpcDebugEventLocked(event); setReturnS32(ctx, 0); } @@ -1193,6 +1410,13 @@ namespace ps2_syscalls if (!client) { + SifRpcDebugEvent event = makeRpcDebugEvent("BindRpc", ctx); + event.clientPtr = clientPtr; + event.sid = rpcId; + event.mode = mode; + event.flags = kSifRpcDebugFlagMissingClient; + event.result = -1; + pushSifRpcDebugEvent(event); setReturnS32(ctx, -1); return; } @@ -1251,6 +1475,13 @@ namespace ps2_syscalls client->cbuf = 0; } + SifRpcDebugEvent event = makeRpcDebugEvent("BindRpc", ctx); + event.clientPtr = clientPtr; + event.serverPtr = serverPtr; + event.sid = rpcId; + event.mode = mode; + event.result = 0; + pushSifRpcDebugEvent(event); setReturnS32(ctx, 0); } @@ -1388,6 +1619,20 @@ namespace ps2_syscalls if (!client) { + SifRpcDebugEvent event = makeRpcDebugEvent("CallRpc", ctx); + event.clientPtr = clientPtr; + event.rpcNum = rpcNum; + event.sid = boundSidHint; + event.mode = mode; + event.sendBuf = sendBuf; + event.sendSize = sendSize; + event.recvBuf = recvBuf; + event.recvSize = recvSize; + event.endFunc = endFunc; + event.endParam = endParam; + event.flags = kSifRpcDebugFlagMissingClient | ((mode & kSifRpcModeNowait) ? kSifRpcDebugFlagNowait : 0u); + event.result = -1; + pushSifRpcDebugEvent(event); setReturnS32(ctx, -1); return; } @@ -1448,6 +1693,8 @@ namespace ps2_syscalls uint32_t resultPtr = 0; bool handled = false; + bool handledByIop = false; + bool callbackInvokedForDebug = false; auto readRpcU32 = [&](uint32_t addr, uint32_t &out) -> bool { @@ -1492,6 +1739,7 @@ namespace ps2_syscalls iopSignalNowaitCompletion)) { handled = true; + handledByIop = true; resultPtr = iopResultPtr; if (iopSignalNowaitCompletion && (mode & kSifRpcModeNowait) != 0u) { @@ -2148,23 +2396,32 @@ namespace ps2_syscalls if (endFunc) { - bool callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, endFunc, endParam, 0, 0, 0, nullptr); - - if (!callbackInvoked && endFunc >= 0x10000u) - { - const uint32_t normalizedEndFunc = endFunc - 0x10000u; - if (runtime->hasFunction(normalizedEndFunc)) - { - callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, normalizedEndFunc, endParam, 0, 0, 0, nullptr); - } - } - PS2SoundDriverCompatLayout soundCompat{}; { std::lock_guard lock(g_rpc_mutex); soundCompat = g_soundDriverCompatLayout; } + const bool hleCompletedEndCallback = handledByIop && soundCompat.matchesCompletionCallback(endFunc); + bool callbackInvoked = hleCompletedEndCallback; + callbackInvokedForDebug = hleCompletedEndCallback; + + if (!hleCompletedEndCallback) + { + callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, endFunc, endParam, 0, 0, 0, nullptr); + callbackInvokedForDebug = callbackInvoked; + + if (!callbackInvoked && endFunc >= 0x10000u) + { + const uint32_t normalizedEndFunc = endFunc - 0x10000u; + if (runtime->hasFunction(normalizedEndFunc)) + { + callbackInvoked = rpcInvokeFunction(rdram, ctx, runtime, normalizedEndFunc, endParam, 0, 0, 0, nullptr); + callbackInvokedForDebug = callbackInvokedForDebug || callbackInvoked; + } + } + } + if (soundCompat.matchesCompletionCallback(endFunc)) { uint32_t semaId = static_cast(client->hdr.sema_id); @@ -2222,6 +2479,28 @@ namespace ps2_syscalls g_rpc_clients[clientPtr].busy = false; } + SifRpcDebugEvent event = makeRpcDebugEvent("CallRpc", ctx); + event.clientPtr = clientPtr; + event.serverPtr = serverPtr; + event.sid = sid; + event.rpcNum = rpcNum; + event.mode = mode; + event.sendBuf = sendBuf; + event.sendSize = sendSize; + event.recvBuf = recvBuf; + event.recvSize = recvSize; + event.resultPtr = resultPtr; + event.endFunc = endFunc; + event.endParam = endParam; + event.semaId = client ? static_cast(client->hdr.sema_id) : 0u; + event.flags = ((mode & kSifRpcModeNowait) ? kSifRpcDebugFlagNowait : 0u) | + (handledByIop ? kSifRpcDebugFlagHandledByHle : 0u) | + (callbackInvokedForDebug ? kSifRpcDebugFlagCallback : 0u) | + ((sd && sd->func) ? kSifRpcDebugFlagServerDispatch : 0u) | + ((isDtxSid || isDtxUrpc) ? kSifRpcDebugFlagDtx : 0u); + event.result = 0; + pushSifRpcDebugEvent(event); + setReturnS32(ctx, 0); } @@ -2243,6 +2522,16 @@ namespace ps2_syscalls t_SifRpcServerData *sd = reinterpret_cast(getMemPtr(rdram, sdPtr)); if (!sd) { + SifRpcDebugEvent event = makeRpcDebugEvent("RegisterRpc", ctx); + event.serverPtr = sdPtr; + event.sid = sid; + event.sendBuf = buf; + event.recvBuf = cbuf; + event.resultPtr = qd; + event.endFunc = cfunc; + event.flags = kSifRpcDebugFlagMissingClient; + event.result = -1; + pushSifRpcDebugEvent(event); setReturnS32(ctx, -1); return; } @@ -2315,6 +2604,15 @@ namespace ps2_syscalls } RUNTIME_LOG("[SifRegisterRpc] sid=0x" << std::hex << sid << " sd=0x" << sdPtr << std::dec); + SifRpcDebugEvent event = makeRpcDebugEvent("RegisterRpc", ctx); + event.serverPtr = sdPtr; + event.sid = sid; + event.sendBuf = buf; + event.recvBuf = cbuf; + event.resultPtr = qd; + event.endFunc = cfunc; + event.result = 0; + pushSifRpcDebugEvent(event); setReturnS32(ctx, 0); } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Sync.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Sync.cpp index 6004715..8243c48 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Sync.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Sync.cpp @@ -241,6 +241,7 @@ namespace ps2_syscalls int ret = sid; int beforeCount = 0; int afterCount = 0; + bool wokeWaiter = false; { std::lock_guard lock(sema->m); beforeCount = sema->count; @@ -251,6 +252,7 @@ namespace ps2_syscalls else { sema->count++; + wokeWaiter = sema->waiters > 0; sema->cv.notify_one(); } afterCount = sema->count; @@ -268,6 +270,10 @@ namespace ps2_syscalls } setReturnS32(ctx, ret); + if (wokeWaiter) + { + yieldGuestExecutionAfterWake(runtime); + } } void iSignalSema(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -287,8 +293,14 @@ namespace ps2_syscalls auto info = ensureCurrentThreadInfo(ctx); throwIfTerminated(info); + std::unique_lock lock(sema->m); - int ret = sid; // PS2 EE BIOS returns sid on success. + + // PS2 EE BIOS returns sid on success. + int ret = sid; + int countAfter = sema->count; + bool terminated = false; + bool consumed = false; if (sema->count == 0) { @@ -314,43 +326,62 @@ namespace ps2_syscalls } sema->waiters++; - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - sema->cv.wait(lock, [&]() - { - bool forced = info ? info->forceRelease.load() : false; - bool terminated = info ? info->terminated.load() : false; - return sema->count > 0 || sema->deleted || forced || terminated; - }); - } - sema->waiters--; - if (sema->deleted) - { - ret = KE_WAIT_DELETE; - } - - if (info) - { - std::lock_guard tLock(info->m); - info->status = (info->suspendCount > 0) ? THS_SUSPEND : THS_RUN; - info->waitType = TSW_NONE; - info->waitId = 0; - if (info->forceRelease) + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() { - info->forceRelease = false; - ret = KE_RELEASE_WAIT; - } - } + sema->cv.wait(lock, [&]() + { + const bool forced = info ? info->forceRelease.load() : false; + const bool isTerminated = info ? info->terminated.load() : false; + return sema->count > 0 || sema->deleted || forced || isTerminated; + }); + }, + [&]() + { + sema->waiters--; + if (sema->deleted) + { + ret = KE_WAIT_DELETE; + } - if (info && info->terminated.load()) - { - throw ThreadExitException(); - } + if (info) + { + std::lock_guard tLock(info->m); + terminated = info->terminated.load(); + info->status = (info->suspendCount > 0) ? THS_SUSPEND : THS_RUN; + info->waitType = TSW_NONE; + info->waitId = 0; + if (info->forceRelease) + { + info->forceRelease = false; + ret = KE_RELEASE_WAIT; + } + } + + if (!terminated && ret == sid && sema->count > 0) + { + sema->count--; + consumed = true; + } + countAfter = sema->count; + }); } - if (ret == sid && sema->count > 0) + if (terminated) { - sema->count--; + throw ThreadExitException(); + } + + if (!consumed && lock.owns_lock()) + { + if (ret == sid && sema->count > 0) + { + sema->count--; + consumed = true; + } + countAfter = sema->count; } static std::atomic s_waitSemaWakeLogs{0}; @@ -360,10 +391,13 @@ namespace ps2_syscalls RUNTIME_LOG("[WaitSema:wake] tid=" << g_currentThreadId << " sid=" << sid << " ret=" << ret - << " count=" << sema->count + << " count=" << countAfter << std::endl); } - lock.unlock(); + if (lock.owns_lock()) + { + lock.unlock(); + } waitWhileSuspended(info, runtime); setReturnS32(ctx, ret); } @@ -505,10 +539,12 @@ namespace ps2_syscalls } uint32_t newBits = 0u; + bool hadWaiters = false; { std::lock_guard lock(info->m); info->bits |= bits; newBits = info->bits; + hadWaiters = info->waiters > 0; } static std::atomic s_setEventFlagLogs{0}; @@ -523,6 +559,10 @@ namespace ps2_syscalls } info->cv.notify_all(); setReturnS32(ctx, 0); + if (hadWaiters) + { + yieldGuestExecutionAfterWake(runtime); + } } void iSetEventFlag(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -610,6 +650,37 @@ namespace ps2_syscalls return (info->bits & waitBits) == waitBits; }; + bool waitedWithGuestRelease = false; + bool terminated = false; + uint32_t bitsAfter = info->bits; + + auto finishEventFlagWaitLocked = [&]() + { + if (ret == KE_OK && info->deleted) + { + ret = KE_WAIT_DELETE; + } + + if (ret == KE_OK) + { + if (resBitsPtr) + { + *resBitsPtr = info->bits; + } + + if (mode & WEF_CLEAR_ALL) + { + info->bits = 0; + } + else if (mode & WEF_CLEAR) + { + info->bits &= ~waitBits; + } + } + + bitsAfter = info->bits; + }; + if (!satisfied()) { static std::atomic s_waitEventBlockLogs{0}; @@ -637,56 +708,51 @@ namespace ps2_syscalls } info->waiters++; - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - info->cv.wait(lock, satisfied); - } - info->waiters--; - - if (tInfo) - { - std::lock_guard tLock(tInfo->m); - tInfo->status = (tInfo->suspendCount > 0) ? THS_SUSPEND : THS_RUN; - tInfo->waitType = TSW_NONE; - tInfo->waitId = 0; - if (tInfo->forceRelease) + waitedWithGuestRelease = true; + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() { - tInfo->forceRelease = false; - ret = KE_RELEASE_WAIT; - } - } + info->cv.wait(lock, satisfied); + }, + [&]() + { + info->waiters--; - if (tInfo && tInfo->terminated.load()) - { - throw ThreadExitException(); - } + if (tInfo) + { + std::lock_guard tLock(tInfo->m); + terminated = tInfo->terminated.load(); + tInfo->status = (tInfo->suspendCount > 0) ? THS_SUSPEND : THS_RUN; + tInfo->waitType = TSW_NONE; + tInfo->waitId = 0; + if (tInfo->forceRelease) + { + tInfo->forceRelease = false; + ret = KE_RELEASE_WAIT; + } + } + + if (!terminated) + { + finishEventFlagWaitLocked(); + } + else + { + bitsAfter = info->bits; + } + }); } - if (ret == KE_OK && info->deleted) + if (terminated) { - ret = KE_WAIT_DELETE; + throw ThreadExitException(); } - if (ret == KE_OK && resBitsPtr) + if (!waitedWithGuestRelease) { - *resBitsPtr = info->bits; - } - - if (ret == KE_OK) - { - if (resBitsPtr) - { - *resBitsPtr = info->bits; - } - - if (mode & WEF_CLEAR_ALL) - { - info->bits = 0; - } - else if (mode & WEF_CLEAR) - { - info->bits &= ~waitBits; - } + finishEventFlagWaitLocked(); } static std::atomic s_waitEventWakeLogs{0}; @@ -696,12 +762,15 @@ namespace ps2_syscalls RUNTIME_LOG("[WaitEventFlag:wake] tid=" << g_currentThreadId << " eid=" << eid << " ret=" << ret - << " bits=0x" << std::hex << info->bits + << " bits=0x" << std::hex << bitsAfter << std::dec << std::endl); } - lock.unlock(); + if (lock.owns_lock()) + { + lock.unlock(); + } waitWhileSuspended(tInfo, runtime); setReturnS32(ctx, ret); } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index bd808a2..e6ec466 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -45,7 +45,10 @@ namespace ps2_syscalls imr = runtime->memory().gs().imr; } - RUNTIME_LOG("PS2 GsGetIMR: Returning IMR=0x" << std::hex << imr << std::dec); + RUNTIME_LOG("PS2 GsGetIMR: Returning IMR=0x" << std::hex << imr + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl); setReturnU64(ctx, imr); // Return in $v0/$v1 } @@ -57,14 +60,20 @@ namespace ps2_syscalls void GsPutIMR(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - uint64_t newImr = getRegU32(ctx, 4) | ((uint64_t)getRegU32(ctx, 5) << 32); // $a0 = lower 32 bits, $a1 = upper 32 bits + const uint64_t newImr = GPR_U64(ctx, 4); uint64_t oldImr = 0; if (runtime) { oldImr = runtime->memory().gs().imr; runtime->memory().gs().imr = newImr; } - RUNTIME_LOG("PS2 GsPutIMR: Setting IMR=0x" << std::hex << newImr << std::dec); + RUNTIME_LOG("PS2 GsPutIMR: " << " new=0x" << newImr + << " a0_64=0x" << GPR_U64(ctx, 4) + << " a0_32=0x" << getRegU32(ctx, 4) + << " a1_32=0x" << getRegU32(ctx, 5) + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl); setReturnU64(ctx, oldImr); } @@ -434,12 +443,14 @@ namespace ps2_syscalls const uint32_t logIndex = s_reentrantLogs.fetch_add(1u, std::memory_order_relaxed); if (logIndex < kMaxReentrantLogs) { - std::cerr << "[SyscallOverride:reentrant]" - << " syscall=0x" << std::hex << syscallNumber - << " handler=0x" << handler - << " pc=0x" << ctx->pc - << " ra=0x" << getRegU32(ctx, 31) - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[SyscallOverride:reentrant]" + << " syscall=0x" << std::hex << syscallNumber + << " handler=0x" << handler + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + }); } return false; } @@ -490,23 +501,25 @@ namespace ps2_syscalls const uint32_t builtinMinus20c = (builtinRet != 0u) ? (builtinRet - 0x20Cu) : 0u; const uint32_t builtinMinus168 = (builtinRet != 0u) ? (builtinRet - 0x168u) : 0u; - std::cerr << "[Syscall83:override]" - << " handler=0x" << std::hex << handler - << " invoked=" << (invoked ? "true" : "false") - << " pc=0x" << overridePc - << " ra=0x" << overrideRa - << " a0=0x" << overrideA0 - << " a1=0x" << overrideA1 - << " a2=0x" << overrideA2 - << " a3=0x" << overrideA3 - << " guestRet=0x" << retV0 - << " builtinRet=0x" << builtinRet - << " guest-20c=0x" << guestMinus20c - << " builtin-20c=0x" << builtinMinus20c - << " guest-168=0x" << guestMinus168 - << " builtin-168=0x" << builtinMinus168 - << " match=" << (mismatch ? "false" : "true") - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[Syscall83:override]" + << " handler=0x" << std::hex << handler + << " invoked=" << (invoked ? "true" : "false") + << " pc=0x" << overridePc + << " ra=0x" << overrideRa + << " a0=0x" << overrideA0 + << " a1=0x" << overrideA1 + << " a2=0x" << overrideA2 + << " a3=0x" << overrideA3 + << " guestRet=0x" << retV0 + << " builtinRet=0x" << builtinRet + << " guest-20c=0x" << guestMinus20c + << " builtin-20c=0x" << builtinMinus20c + << " guest-168=0x" << guestMinus168 + << " builtin-168=0x" << builtinMinus168 + << " match=" << (mismatch ? "false" : "true") + << std::dec << std::endl; + }); } } @@ -517,12 +530,14 @@ namespace ps2_syscalls const uint32_t logIndex = s_fallbackLogs.fetch_add(1u, std::memory_order_relaxed); if (logIndex < kMaxFallbackLogs) { - std::cerr << "[SyscallOverride:fallback]" - << " syscall=0x" << std::hex << syscallNumber - << " handler=0x" << handler - << " pc=0x" << ctx->pc - << " ra=0x" << getRegU32(ctx, 31) - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[SyscallOverride:fallback]" + << " syscall=0x" << std::hex << syscallNumber + << " handler=0x" << handler + << " pc=0x" << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) + << std::dec << std::endl; + }); } return false; } @@ -683,7 +698,7 @@ namespace ps2_syscalls void SetupHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t heapBaseRaw = getRegU32(ctx, 4); // $a0 - const uint32_t heapSize = getRegU32(ctx, 5); // $a1 (optional size) + const uint32_t heapSize = getRegU32(ctx, 5); // $a1 (optional size) const uint32_t heapBase = (heapBaseRaw + 0xFu) & ~0xFu; @@ -706,13 +721,15 @@ namespace ps2_syscalls { runtime->configureGuestHeap(heapBase, heapLimit); - std::cerr << "[SetupHeap]" - << " base=0x" << std::hex << heapBaseRaw - << " alignedBase=0x" << heapBase - << " size=0x" << heapSize - << " runtimeBase=0x" << runtime->guestHeapBase() - << " runtimeEnd=0x" << runtime->guestHeapEnd() - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[SetupHeap]" + << " base=0x" << std::hex << heapBaseRaw + << " alignedBase=0x" << heapBase + << " size=0x" << heapSize + << " runtimeBase=0x" << runtime->guestHeapBase() + << " runtimeEnd=0x" << runtime->guestHeapEnd() + << std::dec << std::endl; + }); setReturnU32(ctx, runtime->guestHeapBase()); return; @@ -729,8 +746,8 @@ namespace ps2_syscalls static constexpr uint32_t kDefaultGuestHeapEnd = 0x01F00000u; const uint32_t ret = runtime - ? runtime->guestHeapLimit() - : kDefaultGuestHeapEnd; + ? runtime->guestHeapLimit() + : kDefaultGuestHeapEnd; setReturnU32(ctx, ret); } @@ -840,6 +857,11 @@ namespace ps2_syscalls const FindAddressMatchSample *matches, uint32_t matchCount) { + if constexpr (!ps2_log::agressive_logs_enabled) + { + return; + } + static std::atomic s_findAddressHitLogs{0u}; static std::atomic s_findAddressMissLogs{0u}; constexpr uint32_t kMaxFindAddressHitLogs = 16u; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp index 83324f6..7807fc7 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp @@ -15,6 +15,26 @@ namespace ps2_syscalls } } + static void notifyThreadWaitObject(int waitType, int waitId) + { + if (waitType == TSW_SEMA) + { + auto sema = lookupSemaInfo(waitId); + if (sema) + { + sema->cv.notify_all(); + } + } + else if (waitType == TSW_EVENT) + { + auto eventFlag = lookupEventFlagInfo(waitId); + if (eventFlag) + { + eventFlag->cv.notify_all(); + } + } + } + static void runExitHandlersForThread(int tid, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { if (!runtime || !ctx) @@ -375,7 +395,7 @@ namespace ps2_syscalls { uint32_t lastPc = 0xFFFFFFFFu; uint32_t samePcCount = 0; - constexpr uint32_t kSamePcYieldMask = 0x3FFFu; + constexpr uint32_t kSamePcYieldMask = 0xFFu; constexpr uint32_t kSamePcWarnInterval = 0x20000u; uint64_t stepCount = 0u; @@ -390,6 +410,7 @@ namespace ps2_syscalls waitWhileSuspended(info, runtime); const uint32_t pc = threadCtx->pc; + info->currentPc.store(pc, std::memory_order_relaxed); if (pc == 0u) { break; @@ -410,14 +431,20 @@ namespace ps2_syscalls ++samePcCount; if ((samePcCount & kSamePcYieldMask) == 0u) { - std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - if ((samePcCount % kSamePcWarnInterval) == 0u) + if (samePcCount > kSamePcWarnInterval) { - RUNTIME_LOG("[StartThread] id=" << tid - << " spinning at pc=0x" << std::hex << pc - << " ra=0x" << GPR_U32(threadCtx, 31) - << std::dec << std::endl); + // If a thread is spinning for an extremely long time (e.g. idle thread), + // force a 1ms sleep to prevent host CPU starvation. + if ((samePcCount % (kSamePcWarnInterval * 8u)) == 0u) + { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + else if ((samePcCount % (kSamePcWarnInterval)) == 0u) + { + std::this_thread::yield(); + } } } else @@ -582,6 +609,8 @@ namespace ps2_syscalls return; } + int waitType = TSW_NONE; + int waitId = 0; { std::lock_guard lock(info->m); if (info->status == THS_DORMANT) @@ -589,10 +618,13 @@ namespace ps2_syscalls setReturnS32(ctx, KE_DORMANT); return; } + waitType = info->waitType; + waitId = info->waitId; info->terminated = true; info->forceRelease = true; } info->cv.notify_all(); + notifyThreadWaitObject(waitType, waitId); if (tid == g_currentThreadId) { @@ -601,13 +633,17 @@ namespace ps2_syscalls } else { - // Block until the target thread actually finishes unwinding and becomes dormant + // Block until the target thread actually finishes unwinding and becomes dormant. + // Drop the thread mutex before reacquiring GuestExecutionScope to avoid lock inversion. std::unique_lock lock(info->m); - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - info->cv.wait(lock, [&]() - { return !info->started && info->status == THS_DORMANT; }); - } + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() + { + info->cv.wait(lock, [&]() + { return !info->started && info->status == THS_DORMANT; }); + }); } setReturnS32(ctx, KE_OK); @@ -641,16 +677,28 @@ namespace ps2_syscalls if (tid == g_currentThreadId) { std::unique_lock lock(info->m); - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - info->cv.wait(lock, [&]() - { return info->suspendCount == 0 || info->terminated.load(); }); - } - if (info->terminated.load()) + bool terminated = false; + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() + { + info->cv.wait(lock, [&]() + { return info->suspendCount == 0 || info->terminated.load(); }); + }, + [&]() + { + terminated = info->terminated.load(); + if (!terminated) + { + info->status = THS_RUN; + } + }); + + if (terminated) { throw ThreadExitException(); } - info->status = THS_RUN; } setReturnS32(ctx, KE_OK); @@ -760,6 +808,8 @@ namespace ps2_syscalls throwIfTerminated(info); int ret = 0; + int wakeupCountAfter = 0; + bool terminated = false; std::unique_lock lock(info->m); if (info->wakeupCount > 0) @@ -769,6 +819,7 @@ namespace ps2_syscalls info->waitType = TSW_NONE; info->waitId = 0; ret = 0; + wakeupCountAfter = info->wakeupCount; } else { @@ -787,32 +838,46 @@ namespace ps2_syscalls info->waitId = 0; info->forceRelease = false; - { - PS2Runtime::GuestExecutionReleaseScope releaseGuestExecution(runtime); - info->cv.wait(lock, [&]() - { return info->wakeupCount > 0 || info->forceRelease.load() || info->terminated.load(); }); - } + waitWithGuestExecutionReleasedUntilUnlocked( + runtime, + lock, + [&]() + { + info->cv.wait(lock, [&]() + { return info->wakeupCount > 0 || info->forceRelease.load() || info->terminated.load(); }); + }, + [&]() + { + terminated = info->terminated.load(); + if (terminated) + { + return; + } - if (info->terminated.load()) - { - throw ThreadExitException(); - } + info->status = THS_RUN; + info->waitType = TSW_NONE; + info->waitId = 0; - info->status = THS_RUN; - info->waitType = TSW_NONE; - info->waitId = 0; + if (info->forceRelease.load()) + { + info->forceRelease = false; + ret = KE_RELEASE_WAIT; + } + else + { + if (info->wakeupCount > 0) + { + info->wakeupCount--; + } + ret = 0; + } + wakeupCountAfter = info->wakeupCount; + }); + } - if (info->forceRelease.load()) - { - info->forceRelease = false; - ret = KE_RELEASE_WAIT; - } - else - { - if (info->wakeupCount > 0) - info->wakeupCount--; - ret = 0; - } + if (terminated) + { + throw ThreadExitException(); } static std::atomic s_sleepWakeLogs{0}; @@ -821,11 +886,14 @@ namespace ps2_syscalls { RUNTIME_LOG("[SleepThread:wake] tid=" << g_currentThreadId << " ret=" << ret - << " wakeupCount=" << info->wakeupCount + << " wakeupCount=" << wakeupCountAfter << std::endl); } - lock.unlock(); + if (lock.owns_lock()) + { + lock.unlock(); + } waitWhileSuspended(info, runtime); setReturnS32(ctx, ret); } @@ -853,6 +921,7 @@ namespace ps2_syscalls int newWakeupCount = 0; int statusAfter = THS_DORMANT; + bool wokeSleeper = false; { std::lock_guard lock(info->m); if (info->status == THS_DORMANT) @@ -874,6 +943,7 @@ namespace ps2_syscalls info->waitId = 0; info->wakeupCount++; info->cv.notify_one(); + wokeSleeper = true; } else { @@ -894,6 +964,10 @@ namespace ps2_syscalls << std::endl); } setReturnS32(ctx, KE_OK); + if (wokeSleeper) + { + yieldGuestExecutionAfterWake(runtime); + } } void iWakeupThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -1016,9 +1090,8 @@ namespace ps2_syscalls return; } - std::this_thread::yield(); - setReturnS32(ctx, KE_OK); + yieldGuestExecutionAfterWake(runtime); } void iRotateThreadReadyQueue(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -1074,24 +1147,9 @@ namespace ps2_syscalls } info->cv.notify_all(); - - if (waitType == TSW_SEMA) - { - auto sema = lookupSemaInfo(waitId); - if (sema) - { - sema->cv.notify_all(); - } - } - else if (waitType == TSW_EVENT) - { - auto eventFlag = lookupEventFlagInfo(waitId); - if (eventFlag) - { - eventFlag->cv.notify_all(); - } - } + notifyThreadWaitObject(waitType, waitId); setReturnS32(ctx, KE_OK); + yieldGuestExecutionAfterWake(runtime); } void iReleaseWaitThread(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/game_overrides.cpp b/ps2xRuntime/src/lib/game_overrides.cpp index bc55790..b363696 100644 --- a/ps2xRuntime/src/lib/game_overrides.cpp +++ b/ps2xRuntime/src/lib/game_overrides.cpp @@ -178,7 +178,6 @@ namespace ps2_game_overrides { ps2_syscalls::clearSoundDriverCompatLayout(); ps2_syscalls::clearDtxCompatLayout(); - ps2_stubs::clearMpegCompatLayout(); std::vector descriptors; { @@ -292,22 +291,16 @@ namespace ps2_syscalls::setDtxCompatLayout(layout); } - void applyRecvxMpegCompat(PS2Runtime &runtime) + void applyLotrSoundRpcCompat(PS2Runtime &runtime) { (void)runtime; - // this is temporary so ignore for now - PS2MpegCompatLayout layout{}; - layout.mpegObjectAddr = 0x01E27140u; - layout.videoStateAddr = 0x01E271E8u; - layout.movieStateAddr = 0x01E21914u; - layout.syntheticFramesBeforeEnd = 1u; - layout.finishedVideoStateValue = 3u; - layout.finishedMovieStateValue = 3u; - ps2_stubs::setMpegCompatLayout(layout); + PS2SoundDriverCompatLayout layout{}; + layout.completionCallbacks = {0x001FFD70u, 0u, 0u, 0u}; + ps2_syscalls::setSoundDriverCompatLayout(layout); } PS2_REGISTER_GAME_OVERRIDE("RECVX sound-driver compat", "slus_201.84", 0u, 0u, &applyRecvxSoundDriverCompat); PS2_REGISTER_GAME_OVERRIDE("RECVX DTX compat", "slus_201.84", 0u, 0u, &applyRecvxDtxCompat); - PS2_REGISTER_GAME_OVERRIDE("RECVX MPEG compat", "slus_201.84", 0u, 0u, &applyRecvxMpegCompat); + PS2_REGISTER_GAME_OVERRIDE("LotR sound RPC compat", "SLUS_205.78", 0u, 0u, &applyLotrSoundRpcCompat); } diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp new file mode 100644 index 0000000..37766d1 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -0,0 +1,2246 @@ +#include "ps2_debug_panel.h" +#include "ps2_runtime.h" +#include "ps2_runtime_macros.h" +#include "ps2_log.h" + +#include +#include "Kernel/Syscalls/Helpers/State.h" +#include "Kernel/Stubs/CD.h" +#include "Kernel/Stubs/MemoryCard.h" +#include "Kernel/Stubs/Pad.h" +#include "runtime/ps2_iop.h" + +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) +#include "imgui.h" +#include "rlImGui.h" +#include "raylib.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) + const char *threadStatusName(int status) + { + switch (status) + { + case THS_RUN: + return "RUN"; + case THS_READY: + return "READY"; + case THS_WAIT: + return "WAIT"; + case THS_SUSPEND: + return "SUSPEND"; + case THS_WAITSUSPEND: + return "WAITSUSP"; + case THS_DORMANT: + return "DORMANT"; + default: + return "?"; + } + } + + const char *waitTypeName(int waitType) + { + switch (waitType) + { + case TSW_NONE: + return "NONE"; + case TSW_SLEEP: + return "SLEEP"; + case TSW_SEMA: + return "SEMA"; + case TSW_EVENT: + return "EVENT"; + default: + return "?"; + } + } + + const char *iopRpcSidName(uint32_t sid) + { + switch (sid) + { + case IOP_SID_SNDDRV_COMMAND: + return "SNDDRV command"; + case IOP_SID_SNDDRV_STATE: + return "SNDDRV state"; + case IOP_SID_LIBSD: + return "LIBSD"; + case IOP_SID_FATAL_FRAME_SDRDRV: + return "Fatal Frame SDRDRV"; + case 0x80001300u: + return "DBCMAN"; + default: + return ""; + } + } + + std::string pressedPadButtons(uint16_t activeLowButtons) + { + struct ButtonName + { + uint16_t mask; + const char *name; + }; + static constexpr ButtonName buttons[] = { + {1u << 0, "Select"}, + {1u << 1, "L3"}, + {1u << 2, "R3"}, + {1u << 3, "Start"}, + {1u << 4, "Up"}, + {1u << 5, "Right"}, + {1u << 6, "Down"}, + {1u << 7, "Left"}, + {1u << 8, "L2"}, + {1u << 9, "R2"}, + {1u << 10, "L1"}, + {1u << 11, "R1"}, + {1u << 12, "Triangle"}, + {1u << 13, "Circle"}, + {1u << 14, "Cross"}, + {1u << 15, "Square"}, + }; + + std::string out; + for (const ButtonName &button : buttons) + { + if ((activeLowButtons & button.mask) == 0u) + { + if (!out.empty()) + { + out += ", "; + } + out += button.name; + } + } + return out.empty() ? std::string("none") : out; + } + + std::string rpcDebugFlags(uint32_t flags) + { + struct FlagName + { + uint32_t mask; + const char *name; + }; + static constexpr FlagName names[] = { + {kSifRpcDebugFlagNowait, "nowait"}, + {kSifRpcDebugFlagHandledByHle, "hle"}, + {kSifRpcDebugFlagCallback, "callback"}, + {kSifRpcDebugFlagMissingClient, "bad-client"}, + {kSifRpcDebugFlagServerDispatch, "server"}, + {kSifRpcDebugFlagDtx, "dtx"}, + }; + + std::string out; + for (const FlagName &name : names) + { + if ((flags & name.mask) != 0u) + { + if (!out.empty()) + { + out += ","; + } + out += name.name; + } + } + return out; + } + + template + bool readGuestObjectNoThrow(uint8_t *rdram, uint32_t addr, T &out) + { + if (!rdram || addr == 0u) + { + return false; + } + + uint8_t *ptr = getMemPtr(rdram, addr); + if (!ptr) + { + return false; + } + + std::memcpy(&out, ptr, sizeof(T)); + return true; + } + + const char *primTypeName(GSPrimType type) + { + switch (type) + { + case GS_PRIM_POINT: + return "POINT"; + case GS_PRIM_LINE: + return "LINE"; + case GS_PRIM_LINESTRIP: + return "LINE_STRIP"; + case GS_PRIM_TRIANGLE: + return "TRI"; + case GS_PRIM_TRISTRIP: + return "TRI_STRIP"; + case GS_PRIM_TRIFAN: + return "TRI_FAN"; + case GS_PRIM_SPRITE: + return "SPRITE"; + default: + return "?"; + } + } + + const char *gsDebugEventKindName(GSDebugEventKind kind) + { + switch (kind) + { + case GSDebugEventKind::GifTag: + return "GIF"; + case GSDebugEventKind::Register: + return "REG"; + case GSDebugEventKind::Draw: + return "DRAW"; + case GSDebugEventKind::Transfer: + return "XFER"; + case GSDebugEventKind::Present: + return "PRESENT"; + default: + return "?"; + } + } + + const char *gifFormatName(uint8_t flg) + { + switch (flg) + { + case GIF_FMT_PACKED: + return "PACKED"; + case GIF_FMT_REGLIST: + return "REGLIST"; + case GIF_FMT_IMAGE: + return "IMAGE"; + case GIF_FMT_DISABLED: + return "DISABLED"; + default: + return "?"; + } + } + + const char *gsRegName(uint8_t reg) + { + switch (reg) + { + case GS_REG_PRIM: + return "PRIM"; + case GS_REG_RGBAQ: + return "RGBAQ"; + case GS_REG_ST: + return "ST"; + case GS_REG_UV: + return "UV"; + case GS_REG_XYZF2: + return "XYZF2"; + case GS_REG_XYZ2: + return "XYZ2"; + case GS_REG_TEX0_1: + return "TEX0_1"; + case GS_REG_TEX0_2: + return "TEX0_2"; + case GS_REG_CLAMP_1: + return "CLAMP_1"; + case GS_REG_CLAMP_2: + return "CLAMP_2"; + case GS_REG_XYZF3: + return "XYZF3"; + case GS_REG_XYZ3: + return "XYZ3"; + case GS_REG_TEX1_1: + return "TEX1_1"; + case GS_REG_TEX1_2: + return "TEX1_2"; + case GS_REG_TEX2_1: + return "TEX2_1"; + case GS_REG_TEX2_2: + return "TEX2_2"; + case GS_REG_XYOFFSET_1: + return "XYOFFSET_1"; + case GS_REG_XYOFFSET_2: + return "XYOFFSET_2"; + case GS_REG_PRMODECONT: + return "PRMODECONT"; + case GS_REG_PRMODE: + return "PRMODE"; + case GS_REG_TEXCLUT: + return "TEXCLUT"; + case GS_REG_TEXA: + return "TEXA"; + case GS_REG_SCISSOR_1: + return "SCISSOR_1"; + case GS_REG_SCISSOR_2: + return "SCISSOR_2"; + case GS_REG_ALPHA_1: + return "ALPHA_1"; + case GS_REG_ALPHA_2: + return "ALPHA_2"; + case GS_REG_TEST_1: + return "TEST_1"; + case GS_REG_TEST_2: + return "TEST_2"; + case GS_REG_FBA_1: + return "FBA_1"; + case GS_REG_FBA_2: + return "FBA_2"; + case GS_REG_FRAME_1: + return "FRAME_1"; + case GS_REG_FRAME_2: + return "FRAME_2"; + case GS_REG_ZBUF_1: + return "ZBUF_1"; + case GS_REG_ZBUF_2: + return "ZBUF_2"; + case GS_REG_BITBLTBUF: + return "BITBLTBUF"; + case GS_REG_TRXPOS: + return "TRXPOS"; + case GS_REG_TRXREG: + return "TRXREG"; + case GS_REG_TRXDIR: + return "TRXDIR"; + case GS_REG_HWREG: + return "HWREG"; + case GS_REG_SIGNAL: + return "SIGNAL"; + case GS_REG_FINISH: + return "FINISH"; + case GS_REG_LABEL: + return "LABEL"; + default: + return "?"; + } + } + + void textHex32(const char *label, uint32_t value) + { + ImGui::Text("%s: 0x%08X", label, value); + } + + void textHex64(const char *label, uint64_t value) + { + ImGui::Text("%s: 0x%016llX", label, static_cast(value)); + } + + uint32_t readGuestU32NoThrow(uint8_t *rdram, uint32_t addr) + { + if (!rdram || addr == 0u) + { + return 0u; + } + + uint8_t *ptr = getMemPtr(rdram, addr); + if (!ptr) + { + return 0u; + } + + uint32_t value = 0u; + std::memcpy(&value, ptr, sizeof(value)); + return value; + } + + std::string pathToString(const std::filesystem::path &path) + { + if (path.empty()) + { + return std::string(""); + } + return path.string(); + } + + void textPath(const char *label, const std::filesystem::path &path) + { + const std::string value = pathToString(path); + ImGui::Text("%s: %s", label, value.c_str()); + } + + std::filesystem::path makeDebugDumpPath(const char *name, uint32_t frameIndex) + { + static uint32_t s_dumpCounter = 0u; + std::filesystem::path dir = std::filesystem::current_path() / "ps2_debug_dumps"; + std::filesystem::create_directories(dir); + + char fileName[128]{}; + std::snprintf(fileName, + sizeof(fileName), + "%s_frame_%08X_%04u.txt", + name, + frameIndex, + ++s_dumpCounter); + return dir / fileName; + } + + void dumpGsContext(std::ostream &out, const char *label, const GSContext &ctx) + { + out << label << "\n"; + out << " FRAME fbp=" << ctx.frame.fbp + << " fbw=" << static_cast(ctx.frame.fbw) + << " psm=0x" << std::hex << static_cast(ctx.frame.psm) + << " fbmsk=0x" << ctx.frame.fbmsk << std::dec << "\n"; + out << " ZBUF zbp=" << ctx.zbuf.zbp + << " psm=0x" << std::hex << static_cast(ctx.zbuf.psm) << std::dec + << " zmask=" << (ctx.zbuf.zmask ? 1u : 0u) << "\n"; + out << " SCISSOR x=" << ctx.scissor.x0 << ".." << ctx.scissor.x1 + << " y=" << ctx.scissor.y0 << ".." << ctx.scissor.y1 << "\n"; + out << " TEX0 tbp0=" << ctx.tex0.tbp0 + << " tbw=" << static_cast(ctx.tex0.tbw) + << " psm=0x" << std::hex << static_cast(ctx.tex0.psm) << std::dec + << " size=" << (1u << ctx.tex0.tw) << "x" << (1u << ctx.tex0.th) + << " tcc=" << static_cast(ctx.tex0.tcc) + << " tfx=" << static_cast(ctx.tex0.tfx) + << " cbp=" << ctx.tex0.cbp + << " cpsm=0x" << std::hex << static_cast(ctx.tex0.cpsm) << std::dec + << " csm=" << static_cast(ctx.tex0.csm) + << " csa=" << static_cast(ctx.tex0.csa) + << " cld=" << static_cast(ctx.tex0.cld) << "\n"; + out << " XYOFFSET ofx=" << ctx.xyoffset.ofx << " ofy=" << ctx.xyoffset.ofy << "\n"; + out << " TEST 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.test << std::setfill(' ') << std::dec + << " ATE=" << ((ctx.test >> 0) & 1ull) + << " ATST=" << ((ctx.test >> 1) & 7ull) + << " AREF=0x" << std::hex << ((ctx.test >> 4) & 0xffull) << std::dec + << " AFAIL=" << ((ctx.test >> 12) & 3ull) + << " DATE=" << ((ctx.test >> 14) & 1ull) + << " DATM=" << ((ctx.test >> 15) & 1ull) + << " ZTE=" << ((ctx.test >> 16) & 1ull) + << " ZTST=" << ((ctx.test >> 17) & 3ull) << "\n"; + out << " ALPHA 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.alpha << std::setfill(' ') << std::dec << "\n"; + out << " TEX1 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.tex1 << std::setfill(' ') << std::dec << "\n"; + out << " CLAMP 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.clamp << std::setfill(' ') << std::dec << "\n"; + out << " FBA 0x" << std::hex << std::setw(16) << std::setfill('0') << ctx.fba << std::setfill(' ') << std::dec << "\n"; + } + + bool writeGsDebugDump(const std::filesystem::path &path, + const GSRegisters ®s, + const GSDebugSnapshot &gs, + const std::vector &history, + bool currentFrameOnly, + bool drawsOnly, + uint32_t latestFrame, + std::string &error) + { + try + { + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path, std::ios::out | std::ios::trunc); + if (!out) + { + error = "failed to open output file"; + return false; + } + + out << "PS2 Runtime GS debug dump\n"; + out << "path: " << path.string() << "\n"; + out << "latestFrame: " << latestFrame << "\n"; + out << "filters: currentFrameOnly=" << (currentFrameOnly ? 1u : 0u) + << " drawsOnly=" << (drawsOnly ? 1u : 0u) << "\n\n"; + + out << "[GS private registers]\n"; + out << std::hex << std::setfill('0'); + out << "PMODE 0x" << std::setw(16) << regs.pmode << "\n"; + out << "SMODE2 0x" << std::setw(16) << regs.smode2 << "\n"; + out << "DISPFB1 0x" << std::setw(16) << regs.dispfb1 << "\n"; + out << "DISPLAY1 0x" << std::setw(16) << regs.display1 << "\n"; + out << "DISPFB2 0x" << std::setw(16) << regs.dispfb2 << "\n"; + out << "DISPLAY2 0x" << std::setw(16) << regs.display2 << "\n"; + out << "BGCOLOR 0x" << std::setw(16) << regs.bgcolor << "\n"; + out << "CSR 0x" << std::setw(16) << regs.csr << "\n"; + out << "IMR 0x" << std::setw(16) << regs.imr << "\n"; + out << "BUSDIR 0x" << std::setw(16) << regs.busdir << "\n"; + out << "SIGLBLID 0x" << std::setw(16) << regs.siglblid << "\n\n"; + out << std::dec << std::setfill(' '); + + out << "[GS draw state]\n"; + out << "PRIM type=" << primTypeName(gs.prim.type) + << " iip=" << gs.prim.iip + << " tme=" << gs.prim.tme + << " fge=" << gs.prim.fge + << " abe=" << gs.prim.abe + << " aa1=" << gs.prim.aa1 + << " fst=" << gs.prim.fst + << " ctxt=" << gs.prim.ctxt + << " fix=" << gs.prim.fix << "\n"; + out << "TEXA ta0=0x" << std::hex << static_cast(gs.texa.ta0) << std::dec + << " aem=" << (gs.texa.aem ? 1u : 0u) + << " ta1=0x" << std::hex << static_cast(gs.texa.ta1) << std::dec + << " TEXCLUT cbw=" << gs.texclut.cbw + << " cou=" << gs.texclut.cou + << " cov=" << gs.texclut.cov << "\n"; + out << "BITBLT sbp=" << gs.bitbltbuf.sbp + << " sbw=" << gs.bitbltbuf.sbw + << " spsm=0x" << std::hex << static_cast(gs.bitbltbuf.spsm) << std::dec + << " dbp=" << gs.bitbltbuf.dbp + << " dbw=" << gs.bitbltbuf.dbw + << " dpsm=0x" << std::hex << static_cast(gs.bitbltbuf.dpsm) << std::dec << "\n"; + out << "TRXPOS ss=(" << gs.trxpos.ssax << "," << gs.trxpos.ssay << ")" + << " ds=(" << gs.trxpos.dsax << "," << gs.trxpos.dsay << ")" + << " dir=" << gs.trxpos.dir + << " TRXREG=" << gs.trxreg.rrw << "x" << gs.trxreg.rrh + << " TRXDIR=" << gs.trxdir + << " copied=" << gs.transferCopiedPixels << "/" << gs.transferTotalPixels + << " at=(" << gs.transferX << "," << gs.transferY << ")\n"; + out << "Host presentation has=" << (gs.hasHostPresentationFrame ? 1u : 0u) + << " size=" << gs.hostPresentationWidth << "x" << gs.hostPresentationHeight + << " displayFbp=" << gs.hostPresentationDisplayFbp + << " sourceFbp=" << gs.hostPresentationSourceFbp + << " preferred=" << (gs.hostPresentationUsedPreferred ? 1u : 0u) + << " lastDisplayBaseBytes=0x" << std::hex << gs.lastDisplayBaseBytes << std::dec + << " localToHostPending=" << gs.localToHostPendingBytes << "\n"; + out << "Preferred source has=" << (gs.hasPreferredDisplaySource ? 1u : 0u) + << " frame fbp=" << gs.preferredDisplaySourceFrame.fbp + << " fbw=" << gs.preferredDisplaySourceFrame.fbw + << " psm=0x" << std::hex << static_cast(gs.preferredDisplaySourceFrame.psm) << std::dec + << " destFbp=" << gs.preferredDisplayDestFbp << "\n\n"; + + out << "[GS context state]\n"; + dumpGsContext(out, "Context 0", gs.ctx[0]); + dumpGsContext(out, "Context 1", gs.ctx[1]); + out << "\n"; + + out << "[GS history]\n"; + out << "seq\tframe\ttick\tkind\tprim\tctx\tframe\tzbuf\ttest\talpha\ttex0\txy\tz\ta\tdetail\tpresent\n"; + for (auto it = history.rbegin(); it != history.rend(); ++it) + { + const GSDebugHistoryEntry &row = *it; + if (currentFrameOnly && row.frameIndex != latestFrame) + { + continue; + } + if (drawsOnly && row.kind != GSDebugEventKind::Draw) + { + continue; + } + + out << row.seq << '\t' + << row.frameIndex << '\t' + << row.vsyncTick << '\t' + << gsDebugEventKindName(row.kind) << '\t' + << primTypeName(row.prim.type) << '\t' + << (row.prim.ctxt ? 1u : 0u) << '\t' + << "fbp=" << row.frame.fbp << "/fbw=" << static_cast(row.frame.fbw) << "/psm=0x" << std::hex << static_cast(row.frame.psm) << std::dec << '\t' + << "zbp=" << row.zbuf.zbp << "/psm=0x" << std::hex << static_cast(row.zbuf.psm) << std::dec << "/m=" << (row.zbuf.zmask ? 1u : 0u) << '\t' + << "0x" << std::hex << row.test << std::dec << '\t' + << "0x" << std::hex << row.alpha << std::dec << '\t' + << "tbp=" << row.tex0.tbp0 << "/psm=0x" << std::hex << static_cast(row.tex0.psm) << std::dec << "/size=" << (1u << row.tex0.tw) << "x" << (1u << row.tex0.th) << "/cbp=" << row.tex0.cbp << '\t'; + + if (row.kind == GSDebugEventKind::Draw) + { + out << row.xMin << ".." << row.xMax << "/" << row.yMin << ".." << row.yMax << '\t' + << row.zMin << ".." << row.zMax << '\t' + << static_cast(row.aMin) << ".." << static_cast(row.aMax) << '\t'; + } + else + { + out << "-\t-\t-\t"; + } + + if (row.kind == GSDebugEventKind::GifTag) + { + out << gifFormatName(row.gifFlg) << " nloop=" << row.gifNloop << " nreg=" << row.gifNreg << " size=" << row.gifSizeBytes; + } + else if (row.kind == GSDebugEventKind::Register) + { + out << gsRegName(row.reg) << "(0x" << std::hex << static_cast(row.reg) << ")=0x" << row.regValue << std::dec; + } + else if (row.kind == GSDebugEventKind::Transfer) + { + out << "TRXDIR=" << row.trxdir << " " << row.trxreg.rrw << "x" << row.trxreg.rrh + << " pix=" << row.transferPixels << " sbp=" << row.bitbltbuf.sbp << " dbp=" << row.bitbltbuf.dbp; + } + else + { + out << "-"; + } + out << '\t'; + if (row.kind == GSDebugEventKind::Present) + { + out << row.width << "x" << row.height + << " display=" << row.displayFbp + << " source=" << row.sourceFbp + << " pref=" << (row.usedPreferred ? 1u : 0u); + } + else + { + out << "-"; + } + out << '\n'; + } + + return true; + } + catch (const std::exception &ex) + { + error = ex.what(); + return false; + } + } + + void drawGsContext(const char *label, const GSContext &ctx) + { + if (!ImGui::TreeNode(label)) + { + return; + } + + ImGui::Text("FRAME fbp=%u fbw=%u psm=0x%02X fbmsk=0x%08X", + ctx.frame.fbp, + ctx.frame.fbw, + static_cast(ctx.frame.psm), + ctx.frame.fbmsk); + ImGui::Text("ZBUF zbp=%u psm=0x%02X zmask=%u", + ctx.zbuf.zbp, + static_cast(ctx.zbuf.psm), + ctx.zbuf.zmask ? 1u : 0u); + ImGui::Text("SCISSOR x=%u..%u y=%u..%u", + ctx.scissor.x0, + ctx.scissor.x1, + ctx.scissor.y0, + ctx.scissor.y1); + ImGui::Text("TEX0 tbp0=%u tbw=%u psm=0x%02X tw=%u th=%u tcc=%u tfx=%u cbp=%u cpsm=0x%02X csm=%u csa=%u cld=%u", + ctx.tex0.tbp0, + static_cast(ctx.tex0.tbw), + static_cast(ctx.tex0.psm), + static_cast(ctx.tex0.tw), + static_cast(ctx.tex0.th), + static_cast(ctx.tex0.tcc), + static_cast(ctx.tex0.tfx), + ctx.tex0.cbp, + static_cast(ctx.tex0.cpsm), + static_cast(ctx.tex0.csm), + static_cast(ctx.tex0.csa), + static_cast(ctx.tex0.cld)); + ImGui::Text("XYOFFSET ofx=%u ofy=%u", ctx.xyoffset.ofx, ctx.xyoffset.ofy); + textHex64("TEST", ctx.test); + ImGui::SameLine(); + ImGui::Text("ATE=%llu ATST=%llu AREF=0x%02llX AFAIL=%llu DATE=%llu DATM=%llu ZTE=%llu ZTST=%llu", + (ctx.test >> 0) & 1ull, + (ctx.test >> 1) & 7ull, + (ctx.test >> 4) & 0xffull, + (ctx.test >> 12) & 3ull, + (ctx.test >> 14) & 1ull, + (ctx.test >> 15) & 1ull, + (ctx.test >> 16) & 1ull, + (ctx.test >> 17) & 3ull); + textHex64("ALPHA", ctx.alpha); + textHex64("TEX1", ctx.tex1); + textHex64("CLAMP", ctx.clamp); + textHex64("FBA", ctx.fba); + ImGui::TreePop(); + } + + void drawHexDump(uint8_t *rdram, uint32_t startAddr, uint32_t bytes) + { + bytes = std::clamp(bytes, 16u, 0x1000u); + const uint32_t alignedStart = startAddr & ~0x0fu; + for (uint32_t off = 0u; off < bytes; off += 16u) + { + const uint32_t addr = alignedStart + off; + uint8_t *ptr = getMemPtr(rdram, addr); + if (!ptr) + { + ImGui::Text("%08X: ", addr); + continue; + } + + char hex[16 * 3 + 1]{}; + char ascii[17]{}; + for (uint32_t i = 0u; i < 16u; ++i) + { + const uint8_t b = ptr[i]; + std::snprintf(hex + i * 3, sizeof(hex) - i * 3, "%02X ", b); + ascii[i] = (b >= 32u && b < 127u) ? static_cast(b) : '.'; + } + ImGui::Text("%08X: %s |%s|", addr, hex, ascii); + } + } + + void drawCpuTab(PS2Runtime &runtime, bool showRegisters) + { + const uint32_t pc = runtime.m_debugPc.load(std::memory_order_relaxed); + const uint32_t ra = runtime.m_debugRa.load(std::memory_order_relaxed); + const uint32_t sp = runtime.m_debugSp.load(std::memory_order_relaxed); + const uint32_t gp = runtime.m_debugGp.load(std::memory_order_relaxed); + + ImGui::Text("Runtime: %s", runtime.isStopRequested() ? "stop requested" : "running"); + ImGui::Text("Guest execution waiters: %u", runtime.guestExecutionWaiterCountForTesting()); + ImGui::Separator(); + textHex32("PC", pc); + ImGui::SameLine(); + textHex32("RA", ra); + textHex32("SP", sp); + ImGui::SameLine(); + textHex32("GP", gp); + + if (ImGui::Button("Copy PC/RA/SP/GP")) + { + char buf[256]{}; + std::snprintf(buf, sizeof(buf), "pc=0x%08X ra=0x%08X sp=0x%08X gp=0x%08X", pc, ra, sp, gp); + ImGui::SetClipboardText(buf); + } + + ImGui::TextDisabled("full context snapshot is best-effort while guest is running"); + + if (showRegisters) + { + const R5900Context &cpu = runtime.cpu(); + if (ImGui::BeginTable("gpr", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("Reg"); + ImGui::TableSetupColumn("Lo32"); + ImGui::TableSetupColumn("Hi32[1]"); + ImGui::TableSetupColumn("Hi64[2:3]"); + ImGui::TableHeadersRow(); + for (int i = 0; i < 32; ++i) + { + const uint32_t w0 = static_cast(_mm_extract_epi32(cpu.r[i], 0)); + const uint32_t w1 = static_cast(_mm_extract_epi32(cpu.r[i], 1)); + const uint32_t w2 = static_cast(_mm_extract_epi32(cpu.r[i], 2)); + const uint32_t w3 = static_cast(_mm_extract_epi32(cpu.r[i], 3)); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("r%d", i); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", w0); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", w1); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X_%08X", w3, w2); + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("COP0"); + ImGui::Text("Status=0x%08X Cause=0x%08X EPC=0x%08X Count=0x%08X Compare=0x%08X BadVAddr=0x%08X", + cpu.cop0_status, + cpu.cop0_cause, + cpu.cop0_epc, + cpu.cop0_count, + cpu.cop0_compare, + cpu.cop0_badvaddr); + } + } + + void drawThreadsTab() + { + struct ThreadRow + { + int id = 0; + uint32_t entry = 0; + uint32_t stack = 0; + uint32_t stackSize = 0; + uint32_t gp = 0; + uint32_t priority = 0; + int status = 0; + int waitType = 0; + int waitId = 0; + int currentPriority = 0; + int wakeupCount = 0; + int suspendCount = 0; + uint32_t currentPc = 0u; + bool terminated = false; + }; + + std::vector rows; + { + std::lock_guard lock(g_thread_map_mutex); + rows.reserve(g_threads.size()); + for (const auto &[id, ptr] : g_threads) + { + if (!ptr) + { + continue; + } + ThreadRow row{}; + row.id = id; + { + std::lock_guard threadLock(ptr->m); + row.entry = ptr->entry; + row.stack = ptr->stack; + row.stackSize = ptr->stackSize; + row.gp = ptr->gp; + row.priority = ptr->priority; + row.currentPriority = ptr->currentPriority; + row.status = ptr->status; + row.waitType = ptr->waitType; + row.waitId = ptr->waitId; + row.wakeupCount = ptr->wakeupCount; + row.suspendCount = ptr->suspendCount; + } + row.currentPc = ptr->currentPc.load(std::memory_order_relaxed); + row.terminated = ptr->terminated.load(std::memory_order_relaxed); + rows.push_back(row); + } + } + + ImGui::Text("Threads: %zu activeThreads=%d", rows.size(), g_activeThreads.load(std::memory_order_relaxed)); + if (ImGui::BeginTable("threads", 12, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 320))) + { + ImGui::TableSetupColumn("ID"); + ImGui::TableSetupColumn("Status"); + ImGui::TableSetupColumn("Wait"); + ImGui::TableSetupColumn("WaitId"); + ImGui::TableSetupColumn("PC"); + ImGui::TableSetupColumn("Entry"); + ImGui::TableSetupColumn("Stack"); + ImGui::TableSetupColumn("GP"); + ImGui::TableSetupColumn("Prio"); + ImGui::TableSetupColumn("Wake"); + ImGui::TableSetupColumn("Susp"); + ImGui::TableSetupColumn("Term"); + ImGui::TableHeadersRow(); + for (const ThreadRow &row : rows) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.id); + ImGui::TableNextColumn(); + ImGui::Text("%s", threadStatusName(row.status)); + ImGui::TableNextColumn(); + ImGui::Text("%s", waitTypeName(row.waitType)); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.waitId); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.currentPc); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.entry); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X/%u", row.stack, row.stackSize); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.gp); + ImGui::TableNextColumn(); + ImGui::Text("%d/%u", row.currentPriority, row.priority); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.wakeupCount); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.suspendCount); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.terminated ? 1u : 0u); + } + ImGui::EndTable(); + } + } + + void drawKernelTab() + { + ImGui::SeparatorText("Semaphores"); + { + std::lock_guard lock(g_sema_map_mutex); + if (ImGui::BeginTable("semas", 7, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("ID"); + ImGui::TableSetupColumn("Count"); + ImGui::TableSetupColumn("Max"); + ImGui::TableSetupColumn("Init"); + ImGui::TableSetupColumn("Waiters"); + ImGui::TableSetupColumn("Attr"); + ImGui::TableSetupColumn("Deleted"); + ImGui::TableHeadersRow(); + for (const auto &[id, sema] : g_semas) + { + if (!sema) + continue; + std::lock_guard semaLock(sema->m); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", id); + ImGui::TableNextColumn(); + ImGui::Text("%d", sema->count); + ImGui::TableNextColumn(); + ImGui::Text("%d", sema->maxCount); + ImGui::TableNextColumn(); + ImGui::Text("%d", sema->initCount); + ImGui::TableNextColumn(); + ImGui::Text("%d", sema->waiters); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", sema->attr); + ImGui::TableNextColumn(); + ImGui::Text("%u", sema->deleted ? 1u : 0u); + } + ImGui::EndTable(); + } + } + + ImGui::SeparatorText("Event flags"); + { + std::lock_guard lock(g_event_flag_map_mutex); + if (ImGui::BeginTable("evf", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("ID"); + ImGui::TableSetupColumn("Bits"); + ImGui::TableSetupColumn("Init"); + ImGui::TableSetupColumn("Waiters"); + ImGui::TableSetupColumn("Attr"); + ImGui::TableSetupColumn("Deleted"); + ImGui::TableHeadersRow(); + for (const auto &[id, evf] : g_eventFlags) + { + if (!evf) + continue; + std::lock_guard evfLock(evf->m); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", id); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", evf->bits); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", evf->initBits); + ImGui::TableNextColumn(); + ImGui::Text("%d", evf->waiters); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", evf->attr); + ImGui::TableNextColumn(); + ImGui::Text("%u", evf->deleted ? 1u : 0u); + } + ImGui::EndTable(); + } + } + } + + void drawIopTab(PS2Runtime &runtime) + { + uint8_t *rdram = runtime.memory().getRDRAM(); + + struct ModuleRow + { + int32_t id = 0; + std::string path; + std::string pathKey; + uint32_t refCount = 0; + bool loaded = false; + }; + + std::vector modules; + uint32_t moduleLogCount = 0; + int32_t nextModuleId = 0; + { + std::lock_guard lock(g_sif_module_mutex); + modules.reserve(g_sif_modules_by_id.size()); + for (const auto &[id, record] : g_sif_modules_by_id) + { + ModuleRow row{}; + row.id = id; + row.path = record.path; + row.pathKey = record.pathKey; + row.refCount = record.refCount; + row.loaded = record.loaded; + modules.push_back(std::move(row)); + } + moduleLogCount = g_sif_module_log_count; + nextModuleId = g_next_sif_module_id; + } + std::sort(modules.begin(), modules.end(), [](const ModuleRow &a, const ModuleRow &b) + { return a.id < b.id; }); + + struct RpcServerRow + { + uint32_t sid = 0; + uint32_t sdPtr = 0; + bool readable = false; + t_SifRpcServerData sd{}; + }; + + struct RpcClientRow + { + uint32_t clientPtr = 0; + bool busy = false; + uint32_t lastRpc = 0; + uint32_t sid = 0; + bool readable = false; + t_SifRpcClientData cd{}; + }; + + std::vector servers; + std::vector clients; + bool rpcInitialized = false; + uint32_t rpcNextId = 0; + uint32_t rpcPacketIndex = 0; + uint32_t rpcServerIndex = 0; + uint32_t rpcActiveQueue = 0; + SoundDriverRpcState soundState{}; + { + std::lock_guard lock(g_rpc_mutex); + rpcInitialized = g_rpc_initialized; + rpcNextId = g_rpc_next_id; + rpcPacketIndex = g_rpc_packet_index; + rpcServerIndex = g_rpc_server_index; + rpcActiveQueue = g_rpc_active_queue; + soundState = g_soundDriverRpcState; + + servers.reserve(g_rpc_servers.size()); + for (const auto &[sid, state] : g_rpc_servers) + { + RpcServerRow row{}; + row.sid = sid; + row.sdPtr = state.sd_ptr; + row.readable = readGuestObjectNoThrow(rdram, state.sd_ptr, row.sd); + servers.push_back(row); + } + + clients.reserve(g_rpc_clients.size()); + for (const auto &[clientPtr, state] : g_rpc_clients) + { + RpcClientRow row{}; + row.clientPtr = clientPtr; + row.busy = state.busy; + row.lastRpc = state.last_rpc; + row.sid = state.sid; + row.readable = readGuestObjectNoThrow(rdram, clientPtr, row.cd); + clients.push_back(row); + } + } + std::sort(servers.begin(), servers.end(), [](const RpcServerRow &a, const RpcServerRow &b) + { return a.sid < b.sid; }); + std::sort(clients.begin(), clients.end(), [](const RpcClientRow &a, const RpcClientRow &b) + { return a.clientPtr < b.clientPtr; }); + + PS2DtxCompatLayout dtxLayout{}; + size_t dtxRemoteCount = 0; + size_t dtxTransferCount = 0; + size_t dtxSjxCount = 0; + size_t dtxRnaCount = 0; + size_t dtxSjrmtCount = 0; + uint32_t dtxNextUrpcObj = 0; + { + std::lock_guard lock(g_dtx_rpc_mutex); + dtxLayout = g_dtxCompatLayout; + dtxRemoteCount = g_dtx_remote_by_id.size(); + dtxTransferCount = g_dtx_transfer_by_id.size(); + dtxSjxCount = g_dtx_sjx_by_handle.size(); + dtxRnaCount = g_dtx_ps2rna_by_handle.size(); + dtxSjrmtCount = g_dtx_sjrmt_by_handle.size(); + dtxNextUrpcObj = g_dtx_next_urpc_obj; + } + + auto hasServer = [&](uint32_t sid) + { + return std::any_of(servers.begin(), servers.end(), [sid](const RpcServerRow &row) + { return row.sid == sid; }); + }; + + ImGui::SeparatorText("IOP / SIF modules"); + ImGui::Text("Tracked modules: %zu loaded=%zu nextId=%d moduleLogs=%u", + modules.size(), + static_cast(std::count_if(modules.begin(), modules.end(), [](const ModuleRow &row) + { return row.loaded; })), + nextModuleId, + moduleLogCount); + ImGui::TextDisabled("This table is fed by SifLoadModule/SifLoadModuleBuffer/SifStopModule tracking."); + + if (ImGui::BeginTable("iop_modules", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180))) + { + ImGui::TableSetupColumn("ID"); + ImGui::TableSetupColumn("Loaded"); + ImGui::TableSetupColumn("Refs"); + ImGui::TableSetupColumn("Key"); + ImGui::TableSetupColumn("Path"); + ImGui::TableHeadersRow(); + for (const ModuleRow &row : modules) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.id); + ImGui::TableNextColumn(); + ImGui::Text("%s", row.loaded ? "yes" : "no"); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.refCount); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.pathKey.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.path.c_str()); + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("Known HLE IOP RPC services"); + if (ImGui::BeginTable("iop_hle_services", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + struct ServiceRow + { + const char *name; + uint32_t sid; + bool dynamic; + }; + const ServiceRow services[] = { + {"SNDDRV command", IOP_SID_SNDDRV_COMMAND, false}, + {"SNDDRV state", IOP_SID_SNDDRV_STATE, false}, + {"LIBSD", IOP_SID_LIBSD, false}, + {"Fatal Frame SDRDRV", IOP_SID_FATAL_FRAME_SDRDRV, false}, + {"DBCMAN", 0x80001300u, false}, + {"DTX compat", dtxLayout.rpcSid, true}, + }; + + ImGui::TableSetupColumn("Service"); + ImGui::TableSetupColumn("SID"); + ImGui::TableSetupColumn("Configured"); + ImGui::TableSetupColumn("EE server registered"); + ImGui::TableHeadersRow(); + for (const ServiceRow &service : services) + { + if (service.dynamic && service.sid == 0u) + { + continue; + } + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(service.name); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", service.sid); + ImGui::TableNextColumn(); + ImGui::Text("%s", (!service.dynamic || service.sid != 0u) ? "yes" : "no"); + ImGui::TableNextColumn(); + ImGui::Text("%s", hasServer(service.sid) ? "yes" : "no"); + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("SIF RPC state"); + ImGui::Text("initialized=%u servers=%zu clients=%zu nextId=%u packetIndex=%u serverIndex=%u activeQueue=0x%08X", + rpcInitialized ? 1u : 0u, + servers.size(), + clients.size(), + rpcNextId, + rpcPacketIndex, + rpcServerIndex, + rpcActiveQueue); + + if (ImGui::BeginTable("rpc_servers", 10, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180))) + { + ImGui::TableSetupColumn("SID"); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("sd*"); + ImGui::TableSetupColumn("func"); + ImGui::TableSetupColumn("buf"); + ImGui::TableSetupColumn("size"); + ImGui::TableSetupColumn("cfunc"); + ImGui::TableSetupColumn("queue"); + ImGui::TableSetupColumn("recv"); + ImGui::TableSetupColumn("rpc#"); + ImGui::TableHeadersRow(); + for (const RpcServerRow &row : servers) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.sid); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(iopRpcSidName(row.sid)); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.sdPtr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.sd.func : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.sd.buf : 0u); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.readable ? row.sd.size : 0); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.sd.cfunc : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.sd.base : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X/%d", row.readable ? row.sd.recvbuf : 0u, row.readable ? row.sd.rsize : 0); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.readable ? row.sd.rpc_number : 0); + } + ImGui::EndTable(); + } + + if (ImGui::BeginTable("rpc_clients", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 160))) + { + ImGui::TableSetupColumn("client*"); + ImGui::TableSetupColumn("SID"); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Busy"); + ImGui::TableSetupColumn("LastRpc"); + ImGui::TableSetupColumn("cmd"); + ImGui::TableSetupColumn("buf"); + ImGui::TableSetupColumn("cbuf"); + ImGui::TableSetupColumn("server*"); + ImGui::TableHeadersRow(); + for (const RpcClientRow &row : clients) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.clientPtr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.sid); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(iopRpcSidName(row.sid)); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.busy ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.lastRpc); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.cd.command : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.cd.buf : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.cd.cbuf : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.readable ? row.cd.server : 0u); + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("Sound driver / DTX compat"); + ImGui::Text("SoundDriver initialized=%u owner=0x%p storage=0x%08X/%u status=0x%08X addrTable=0x%08X hd=0x%08X sq=0x%08X data=0x%08X", + soundState.initialized ? 1u : 0u, + reinterpret_cast(soundState.ownerRuntime), + soundState.storageBaseAddr, + soundState.storageSize, + soundState.statusAddr, + soundState.addrTableAddr, + soundState.hdBaseAddr, + soundState.sqBaseAddr, + soundState.dataBaseAddr); + ImGui::Text("DTX configured=%u sid=0x%08X obj=[0x%08X,0x%08X) stride=0x%X fnTable=0x%08X objTable=0x%08X dispatcher=0x%08X nextObj=0x%08X", + dtxLayout.isConfigured() ? 1u : 0u, + dtxLayout.rpcSid, + dtxLayout.urpcObjBase, + dtxLayout.urpcObjLimit, + dtxLayout.urpcObjStride, + dtxLayout.urpcFnTableBase, + dtxLayout.urpcObjTableBase, + dtxLayout.dispatcherFuncAddr, + dtxNextUrpcObj); + ImGui::Text("DTX states: remote=%zu transfer=%zu sjx=%zu ps2rna=%zu sjrmt=%zu", + dtxRemoteCount, + dtxTransferCount, + dtxSjxCount, + dtxRnaCount, + dtxSjrmtCount); + } + + void drawRpcHistoryTab() + { + std::vector events; + uint64_t nextSeq = 0; + { + std::lock_guard lock(g_rpc_mutex); + nextSeq = g_sif_rpc_debug_next_seq; + events.reserve(kSifRpcDebugHistoryCount); + for (size_t i = 0; i < kSifRpcDebugHistoryCount; ++i) + { + const SifRpcDebugEvent &event = g_sif_rpc_debug_history[i]; + if (event.seq != 0u) + { + events.push_back(event); + } + } + } + std::sort(events.begin(), events.end(), [](const SifRpcDebugEvent &a, const SifRpcDebugEvent &b) + { return a.seq > b.seq; }); + + ImGui::Text("RPC events: %zu captured, nextSeq=%llu", events.size(), static_cast(nextSeq)); + ImGui::TextDisabled("Ring buffer fed by SifInitRpc/SifBindRpc/SifRegisterRpc/SifCallRpc."); + + static bool onlyCalls = false; + static bool hideBindNoise = false; + ImGui::Checkbox("Only CallRpc", &onlyCalls); + ImGui::SameLine(); + ImGui::Checkbox("Hide bind/register/init", &hideBindNoise); + + if (ImGui::BeginTable("rpc_history", 20, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | + ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp, + ImVec2(0, 360))) + { + ImGui::TableSetupColumn("Seq"); + ImGui::TableSetupColumn("Op"); + ImGui::TableSetupColumn("TID"); + ImGui::TableSetupColumn("PC"); + ImGui::TableSetupColumn("RA"); + ImGui::TableSetupColumn("SID"); + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Rpc#"); + ImGui::TableSetupColumn("Client"); + ImGui::TableSetupColumn("Server"); + ImGui::TableSetupColumn("Send"); + ImGui::TableSetupColumn("SSize"); + ImGui::TableSetupColumn("Recv"); + ImGui::TableSetupColumn("RSize"); + ImGui::TableSetupColumn("ResultPtr"); + ImGui::TableSetupColumn("Mode"); + ImGui::TableSetupColumn("EndFunc"); + ImGui::TableSetupColumn("EndParam"); + ImGui::TableSetupColumn("Sema"); + ImGui::TableSetupColumn("Flags"); + ImGui::TableHeadersRow(); + + for (const SifRpcDebugEvent &event : events) + { + const char *op = event.op ? event.op : ""; + if (onlyCalls && std::strcmp(op, "CallRpc") != 0) + { + continue; + } + if (hideBindNoise && std::strcmp(op, "CallRpc") != 0) + { + continue; + } + + const std::string flags = rpcDebugFlags(event.flags); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(event.seq)); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(op); + ImGui::TableNextColumn(); + ImGui::Text("%u", event.threadId); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.pc); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.ra); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.sid); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(iopRpcSidName(event.sid)); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.rpcNum); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.clientPtr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.serverPtr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.sendBuf); + ImGui::TableNextColumn(); + ImGui::Text("%u", event.sendSize); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.recvBuf); + ImGui::TableNextColumn(); + ImGui::Text("%u", event.recvSize); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.resultPtr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.mode); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.endFunc); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.endParam); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", event.semaId); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(flags.c_str()); + } + ImGui::EndTable(); + } + } + + void drawPadBytes(const uint8_t *data, size_t size) + { + char bytes[3 * 32 + 1]{}; + const size_t count = std::min(size, 32u); + for (size_t i = 0; i < count; ++i) + { + std::snprintf(bytes + (i * 3), sizeof(bytes) - (i * 3), "%02X ", data[i]); + } + ImGui::TextUnformatted(bytes); + } + + void drawPadTab() + { + const ps2_stubs::PadDebugSnapshot snapshot = ps2_stubs::getPadDebugSnapshot(); + + ImGui::SeparatorText("PAD global state"); + ImGui::Text("override=%u readLogCount=%d", snapshot.overrideEnabled ? 1u : 0u, snapshot.readLogCount); + ImGui::SameLine(); + ImGui::Text("override buttons=0x%04X lx=%u ly=%u rx=%u ry=%u", + snapshot.overrideButtons, + snapshot.overrideLx, + snapshot.overrideLy, + snapshot.overrideRx, + snapshot.overrideRy); + ImGui::Text("override pressed: %s", pressedPadButtons(snapshot.overrideButtons).c_str()); + + if (ImGui::BeginTable("pad_ports", 15, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("Port"); + ImGui::TableSetupColumn("Slot"); + ImGui::TableSetupColumn("Open"); + ImGui::TableSetupColumn("Mode"); + ImGui::TableSetupColumn("Pressure"); + ImGui::TableSetupColumn("Req"); + ImGui::TableSetupColumn("DMA"); + ImGui::TableSetupColumn("Read#"); + ImGui::TableSetupColumn("ReadAddr"); + ImGui::TableSetupColumn("Source"); + ImGui::TableSetupColumn("Buttons raw"); + ImGui::TableSetupColumn("Pressed"); + ImGui::TableSetupColumn("LX/LY"); + ImGui::TableSetupColumn("RX/RY"); + ImGui::TableSetupColumn("Mask"); + ImGui::TableHeadersRow(); + + for (size_t port = 0; port < ps2_stubs::kPadDebugPortCount; ++port) + { + for (size_t slot = 0; slot < ps2_stubs::kPadDebugSlotCount; ++slot) + { + const ps2_stubs::PadDebugPortSnapshot &row = snapshot.ports[port][slot]; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%zu", port); + ImGui::TableNextColumn(); + ImGui::Text("%zu", slot); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.open ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.analogMode ? "analog" : "digital"); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.pressureEnabled ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.reqState); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.dmaAddr); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.readCount); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.lastReadDataAddr); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.lastUsedOverride ? "override" : (row.lastUsedBackend ? "backend" : "fallback")); + ImGui::TableNextColumn(); + ImGui::Text("0x%04X", row.lastButtons); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(pressedPadButtons(row.lastButtons).c_str()); + ImGui::TableNextColumn(); + ImGui::Text("%u/%u", row.lx, row.ly); + ImGui::TableNextColumn(); + ImGui::Text("%u/%u", row.rx, row.ry); + ImGui::TableNextColumn(); + ImGui::Text("0x%04X", row.buttonMask); + } + } + ImGui::EndTable(); + } + + ImGui::SeparatorText("Last scePadRead bytes"); + for (size_t port = 0; port < ps2_stubs::kPadDebugPortCount; ++port) + { + for (size_t slot = 0; slot < ps2_stubs::kPadDebugSlotCount; ++slot) + { + const ps2_stubs::PadDebugPortSnapshot &row = snapshot.ports[port][slot]; + ImGui::Text("port=%zu slot=%zu ok=%u data:", port, slot, row.lastReadOk ? 1u : 0u); + ImGui::SameLine(); + drawPadBytes(row.lastData, ps2_stubs::kPadDebugDataSize); + } + } + + ImGui::SeparatorText("PS2 active-low button layout"); + ImGui::TextUnformatted("byte[2:3] raw is active-low: 0 means pressed, 1 means released."); + ImGui::TextUnformatted("bits: Select,L3,R3,Start,Up,Right,Down,Left,L2,R2,L1,R1,Triangle,Circle,Cross,Square"); + } + + void drawGsTab(PS2Runtime &runtime) + { + GSRegisters ®s = runtime.memory().gs(); + const GSDebugSnapshot gs = runtime.gs().getDebugSnapshot(); + + ImGui::SeparatorText("GS private registers"); + if (ImGui::BeginTable("gspriv", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + auto row64 = [](const char *name, uint64_t value) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(name); + ImGui::TableNextColumn(); + ImGui::Text("0x%016llX", static_cast(value)); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(value)); + ImGui::TableNextColumn(); + ImGui::Text("lo=0x%08X hi=0x%08X", static_cast(value), static_cast(value >> 32)); + }; + ImGui::TableSetupColumn("Reg"); + ImGui::TableSetupColumn("Hex64"); + ImGui::TableSetupColumn("Dec"); + ImGui::TableSetupColumn("Split"); + ImGui::TableHeadersRow(); + row64("PMODE", regs.pmode); + row64("SMODE2", regs.smode2); + row64("DISPFB1", regs.dispfb1); + row64("DISPLAY1", regs.display1); + row64("DISPFB2", regs.dispfb2); + row64("DISPLAY2", regs.display2); + row64("BGCOLOR", regs.bgcolor); + row64("CSR", regs.csr); + row64("IMR", regs.imr); + row64("BUSDIR", regs.busdir); + row64("SIGLBLID", regs.siglblid); + ImGui::EndTable(); + } + + ImGui::SeparatorText("GS draw state"); + ImGui::Text("PRIM type=%s iip=%u tme=%u fge=%u abe=%u aa1=%u fst=%u ctxt=%u fix=%u", + primTypeName(gs.prim.type), gs.prim.iip, gs.prim.tme, gs.prim.fge, gs.prim.abe, + gs.prim.aa1, gs.prim.fst, gs.prim.ctxt, gs.prim.fix); + ImGui::Text("TEXA ta0=0x%02X aem=%u ta1=0x%02X | TEXCLUT cbw=%u cou=%u cov=%u", + gs.texa.ta0, gs.texa.aem ? 1u : 0u, gs.texa.ta1, gs.texclut.cbw, gs.texclut.cou, gs.texclut.cov); + ImGui::Text("BITBLT sbp=%u sbw=%u spsm=0x%02X dbp=%u dbw=%u dpsm=0x%02X", + gs.bitbltbuf.sbp, gs.bitbltbuf.sbw, gs.bitbltbuf.spsm, gs.bitbltbuf.dbp, gs.bitbltbuf.dbw, gs.bitbltbuf.dpsm); + ImGui::Text("TRXPOS ss=(%u,%u) ds=(%u,%u) dir=%u | TRXREG %ux%u TRXDIR=%u copied=%u/%u at=(%u,%u)", + gs.trxpos.ssax, gs.trxpos.ssay, gs.trxpos.dsax, gs.trxpos.dsay, gs.trxpos.dir, + gs.trxreg.rrw, gs.trxreg.rrh, gs.trxdir, gs.transferCopiedPixels, gs.transferTotalPixels, + gs.transferX, gs.transferY); + ImGui::Text("Host presentation: has=%u size=%ux%u displayFbp=%u sourceFbp=%u preferred=%u lastDisplayBaseBytes=0x%08X localToHostPending=%zu", + gs.hasHostPresentationFrame ? 1u : 0u, + gs.hostPresentationWidth, + gs.hostPresentationHeight, + gs.hostPresentationDisplayFbp, + gs.hostPresentationSourceFbp, + gs.hostPresentationUsedPreferred ? 1u : 0u, + gs.lastDisplayBaseBytes, + gs.localToHostPendingBytes); + ImGui::Text("Preferred source: has=%u frame fbp=%u fbw=%u psm=0x%02X destFbp=%u", + gs.hasPreferredDisplaySource ? 1u : 0u, + gs.preferredDisplaySourceFrame.fbp, + gs.preferredDisplaySourceFrame.fbw, + gs.preferredDisplaySourceFrame.psm, + gs.preferredDisplayDestFbp); + + drawGsContext("Context 0", gs.ctx[0]); + drawGsContext("Context 1", gs.ctx[1]); + + ImGui::SeparatorText("GS history"); + bool gsHistoryPaused = runtime.gs().isDebugHistoryPaused(); + std::vector history = runtime.gs().getDebugHistory(); + uint32_t latestFrame = 0u; + if (!history.empty()) + { + latestFrame = history.back().frameIndex; + } + + static bool s_gsHistoryCurrentFrameOnly = false; + static bool s_gsHistoryDrawsOnly = false; + static std::string s_lastGsDumpPath; + static std::string s_lastGsDumpError; + + ImGui::Text("Ring entries: %zu / 512%s", history.size(), gsHistoryPaused ? " (paused)" : ""); + ImGui::SameLine(); + if (ImGui::Button("Clear GS history")) + { + runtime.gs().clearDebugHistory(); + history.clear(); + latestFrame = 0u; + } + ImGui::SameLine(); + if (ImGui::Checkbox("Pause capture", &gsHistoryPaused)) + { + runtime.gs().setDebugHistoryPaused(gsHistoryPaused); + } + ImGui::SameLine(); + if (ImGui::Button("Dump GS TXT")) + { + s_lastGsDumpError.clear(); + const std::filesystem::path dumpPath = makeDebugDumpPath("gs_history", latestFrame); + if (writeGsDebugDump(dumpPath, regs, gs, history, s_gsHistoryCurrentFrameOnly, s_gsHistoryDrawsOnly, latestFrame, s_lastGsDumpError)) + { + s_lastGsDumpPath = dumpPath.string(); + ImGui::SetClipboardText(s_lastGsDumpPath.c_str()); + } + else + { + s_lastGsDumpPath.clear(); + } + } + if (gsHistoryPaused) + { + ImGui::SameLine(); + ImGui::TextDisabled("history capture stopped"); + } + + ImGui::Checkbox("Current frame only", &s_gsHistoryCurrentFrameOnly); + ImGui::SameLine(); + ImGui::Checkbox("Draws only", &s_gsHistoryDrawsOnly); + if (!s_lastGsDumpPath.empty()) + { + ImGui::TextDisabled("Last dump copied to clipboard: %s", s_lastGsDumpPath.c_str()); + } + if (!s_lastGsDumpError.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Dump failed: %s", s_lastGsDumpError.c_str()); + } + + if (ImGui::BeginTable("gs_history", 16, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX, ImVec2(0, 260))) + { + ImGui::TableSetupColumn("Seq"); + ImGui::TableSetupColumn("Frame"); + ImGui::TableSetupColumn("Tick"); + ImGui::TableSetupColumn("Kind"); + ImGui::TableSetupColumn("Prim"); + ImGui::TableSetupColumn("Ctx"); + ImGui::TableSetupColumn("FBP/PSM"); + ImGui::TableSetupColumn("ZBP/PSM"); + ImGui::TableSetupColumn("TEST"); + ImGui::TableSetupColumn("ALPHA"); + ImGui::TableSetupColumn("TEX0"); + ImGui::TableSetupColumn("XY"); + ImGui::TableSetupColumn("Z"); + ImGui::TableSetupColumn("A"); + ImGui::TableSetupColumn("GIF/REG/XFER"); + ImGui::TableSetupColumn("Present"); + ImGui::TableHeadersRow(); + + for (auto it = history.rbegin(); it != history.rend(); ++it) + { + const GSDebugHistoryEntry &row = *it; + if (s_gsHistoryCurrentFrameOnly && row.frameIndex != latestFrame) + { + continue; + } + if (s_gsHistoryDrawsOnly && row.kind != GSDebugEventKind::Draw) + { + continue; + } + + const uint64_t test = row.test; + const uint64_t alpha = row.alpha; + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(row.seq)); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.frameIndex); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(row.vsyncTick)); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(gsDebugEventKindName(row.kind)); + ImGui::TableNextColumn(); + ImGui::Text("%s", primTypeName(row.prim.type)); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.prim.ctxt ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::Text("%u/%u/0x%02X", row.frame.fbp, row.frame.fbw, row.frame.psm); + ImGui::TableNextColumn(); + ImGui::Text("%u/0x%02X m=%u", row.zbuf.zbp, row.zbuf.psm, row.zbuf.zmask ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::Text("ATE=%llu ATST=%llu AREF=%02llX AFAIL=%llu ZTE=%llu ZTST=%llu", + (test >> 0) & 1ull, + (test >> 1) & 7ull, + (test >> 4) & 0xffull, + (test >> 12) & 3ull, + (test >> 16) & 1ull, + (test >> 17) & 3ull); + ImGui::TableNextColumn(); + ImGui::Text("0x%016llX", static_cast(alpha)); + ImGui::TableNextColumn(); + ImGui::Text("tbp=%u psm=0x%02X %ux%u cbp=%u cpsm=0x%02X", + row.tex0.tbp0, + row.tex0.psm, + 1u << row.tex0.tw, + 1u << row.tex0.th, + row.tex0.cbp, + row.tex0.cpsm); + ImGui::TableNextColumn(); + if (row.kind == GSDebugEventKind::Draw) + ImGui::Text("%.1f..%.1f / %.1f..%.1f", row.xMin, row.xMax, row.yMin, row.yMax); + else + ImGui::TextUnformatted("-"); + ImGui::TableNextColumn(); + if (row.kind == GSDebugEventKind::Draw) + ImGui::Text("%.0f..%.0f", row.zMin, row.zMax); + else + ImGui::TextUnformatted("-"); + ImGui::TableNextColumn(); + if (row.kind == GSDebugEventKind::Draw) + ImGui::Text("%u..%u", row.aMin, row.aMax); + else + ImGui::TextUnformatted("-"); + ImGui::TableNextColumn(); + if (row.kind == GSDebugEventKind::GifTag) + { + ImGui::Text("%s nloop=%u nreg=%u size=%u", gifFormatName(row.gifFlg), row.gifNloop, row.gifNreg, row.gifSizeBytes); + } + else if (row.kind == GSDebugEventKind::Register) + { + ImGui::Text("%s(0x%02X)=0x%016llX", gsRegName(row.reg), row.reg, static_cast(row.regValue)); + } + else if (row.kind == GSDebugEventKind::Transfer) + { + ImGui::Text("TRXDIR=%u %ux%u pix=%u sbp=%u dbp=%u", row.trxdir, row.trxreg.rrw, row.trxreg.rrh, row.transferPixels, row.bitbltbuf.sbp, row.bitbltbuf.dbp); + } + else + { + ImGui::TextUnformatted("-"); + } + ImGui::TableNextColumn(); + if (row.kind == GSDebugEventKind::Present) + { + ImGui::Text("%ux%u display=%u source=%u pref=%u", row.width, row.height, row.displayFbp, row.sourceFbp, row.usedPreferred ? 1u : 0u); + } + else + { + ImGui::TextUnformatted("-"); + } + } + ImGui::EndTable(); + } + } + + void drawFileCdTab(PS2Runtime &runtime) + { + (void)runtime; + + const PS2Runtime::IoPaths &ioPaths = PS2Runtime::getIoPaths(); + ImGui::SeparatorText("Runtime IO paths"); + textPath("ELF", ioPaths.elfPath); + textPath("ELF dir", ioPaths.elfDirectory); + textPath("Host root", ioPaths.hostRoot); + textPath("CD root", ioPaths.cdRoot); + textPath("CD image", ioPaths.cdImage); + textPath("MC root", ioPaths.mcRoot); + + bool pathsInitialized = false; + std::filesystem::path hostBase; + std::filesystem::path cdromBase; + std::filesystem::path hostCwd; + std::filesystem::path cdromCwd; + std::string cwdDevice; + { + std::lock_guard lock(g_ps2_path_mutex); + pathsInitialized = g_ps2_paths_initialized; + hostBase = g_host_base; + cdromBase = g_cdrom_base; + hostCwd = g_host_cwd; + cdromCwd = g_cdrom_cwd; + cwdDevice = g_ps2_cwd_device; + } + + ImGui::SeparatorText("PS2 path resolver"); + ImGui::Text("initialized=%u cwdDevice=%s", pathsInitialized ? 1u : 0u, cwdDevice.c_str()); + textPath("host base", hostBase); + textPath("cdrom base", cdromBase); + textPath("host cwd", hostCwd); + textPath("cdrom cwd", cdromCwd); + + struct FdRow + { + int fd = 0; + FILE *file = nullptr; + }; + + std::vector fds; + { + std::lock_guard lock(g_fd_mutex); + fds.reserve(g_fileDescriptors.size()); + for (const auto &[fd, file] : g_fileDescriptors) + { + fds.push_back(FdRow{fd, file}); + } + } + std::sort(fds.begin(), fds.end(), [](const FdRow &a, const FdRow &b) + { return a.fd < b.fd; }); + + ImGui::SeparatorText("FileIO descriptors"); + ImGui::Text("Open host FILE* descriptors: %zu", fds.size()); + if (ImGui::BeginTable("fileio_fds", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable, ImVec2(0, 90))) + { + ImGui::TableSetupColumn("FD"); + ImGui::TableSetupColumn("FILE*"); + ImGui::TableHeadersRow(); + for (const FdRow &row : fds) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.fd); + ImGui::TableNextColumn(); + ImGui::Text("%p", static_cast(row.file)); + } + ImGui::EndTable(); + } + + const ps2_stubs::CdDebugSnapshot cd = ps2_stubs::getCdDebugSnapshot(); + ImGui::SeparatorText("CDVD / sceCd state"); + ImGui::Text("initialized=%u lastError=%d mode=0x%08X streamingLbn=0x%08X endLbn=0x%08X nextPseudoLbn=0x%08X", + cd.initialized ? 1u : 0u, + cd.lastError, + cd.mode, + cd.streamingLbn, + cd.streamingEndLbn, + cd.nextPseudoLbn); + ImGui::Text("imageValid=%u imageSize=%llu leafIndexBuilt=%u leafIndex=%zu looseIndex=%zu registeredFiles=%zu", + cd.imageSizeValid ? 1u : 0u, + static_cast(cd.imageSizeBytes), + cd.leafIndexBuilt ? 1u : 0u, + cd.leafIndexCount, + cd.loosePathIndexCount, + cd.files.size()); + textPath("CD resolved root", cd.cdRoot); + textPath("CD image path", cd.cdImage); + textPath("CD image size path", cd.imageSizePath); + textPath("CD leaf index root", cd.leafIndexRoot); + + if (ImGui::BeginTable("cd_files", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 180))) + { + ImGui::TableSetupColumn("Key"); + ImGui::TableSetupColumn("Base LBN"); + ImGui::TableSetupColumn("End LBN"); + ImGui::TableSetupColumn("Sectors"); + ImGui::TableSetupColumn("Bytes"); + ImGui::TableSetupColumn("Host path"); + ImGui::TableHeadersRow(); + for (const ps2_stubs::CdDebugFileEntry &row : cd.files) + { + const uint32_t endLbn = row.baseLbn + row.sectors; + const std::string hostPath = pathToString(row.hostPath); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.key.c_str()); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.baseLbn); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", endLbn); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.sectors); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.sizeBytes); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(hostPath.c_str()); + } + ImGui::EndTable(); + } + + const ps2_stubs::MemoryCardDebugSnapshot mc = ps2_stubs::getMemoryCardDebugSnapshot(); + ImGui::SeparatorText("Memory card state"); + ImGui::Text("lastCmd=0x%X lastResult=%d nextFd=%d cvCursor=%d openFiles=%zu", + mc.lastCmd, + mc.lastResult, + mc.nextFd, + mc.cvFileCursor, + mc.openFiles.size()); + + if (ImGui::BeginTable("mc_ports", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("Port"); + ImGui::TableSetupColumn("Formatted"); + ImGui::TableSetupColumn("Current dir"); + ImGui::TableSetupColumn("Host root"); + ImGui::TableHeadersRow(); + for (const ps2_stubs::MemoryCardDebugPort &port : mc.ports) + { + const std::string root = pathToString(port.rootPath); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", port.port); + ImGui::TableNextColumn(); + ImGui::Text("%u", port.formatted ? 1u : 0u); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(port.currentDir.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(root.c_str()); + } + ImGui::EndTable(); + } + + if (ImGui::BeginTable("mc_open_files", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 120))) + { + ImGui::TableSetupColumn("FD"); + ImGui::TableSetupColumn("Port"); + ImGui::TableSetupColumn("Host path"); + ImGui::TableHeadersRow(); + for (const ps2_stubs::MemoryCardDebugOpenFile &row : mc.openFiles) + { + const std::string path = pathToString(row.hostPath); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.fd); + ImGui::TableNextColumn(); + ImGui::Text("%d", row.port); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(path.c_str()); + } + ImGui::EndTable(); + } + } + + void drawIoTab(PS2Runtime &runtime) + { + PS2Memory &mem = runtime.memory(); + ImGui::SeparatorText("DMA/VIF/GIF IO registers"); + struct RegRow + { + const char *name; + uint32_t addr; + }; + static constexpr RegRow rows[] = { + {"D_STAT", 0x1000e010u}, + {"VIF0_STAT", 0x10003800u}, + {"VIF0_FBRST", 0x10003810u}, + {"VIF0_ERR", 0x10003830u}, + {"VIF1_STAT", 0x10003c00u}, + {"VIF1_FBRST", 0x10003c10u}, + {"VIF1_ERR", 0x10003c30u}, + {"VIF0_CHCR", 0x10008000u}, + {"VIF0_MADR", 0x10008010u}, + {"VIF0_QWC", 0x10008020u}, + {"VIF0_TADR", 0x10008030u}, + {"VIF1_CHCR", 0x10009000u}, + {"VIF1_MADR", 0x10009010u}, + {"VIF1_QWC", 0x10009020u}, + {"VIF1_TADR", 0x10009030u}, + {"GIF_CHCR", 0x1000a000u}, + {"GIF_MADR", 0x1000a010u}, + {"GIF_QWC", 0x1000a020u}, + {"GIF_TADR", 0x1000a030u}, + {"IPU_FROM_CHCR", 0x1000b000u}, + {"IPU_TO_CHCR", 0x1000b400u}, + }; + + if (ImGui::BeginTable("ioregs", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) + { + ImGui::TableSetupColumn("Name"); + ImGui::TableSetupColumn("Addr"); + ImGui::TableSetupColumn("Value"); + ImGui::TableHeadersRow(); + for (const auto &row : rows) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.name); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.addr); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", mem.readIORegister(row.addr)); + } + ImGui::EndTable(); + } + } + + bool writeRuntimeLogDump(const std::filesystem::path &path, + const std::vector &entries, + ImGuiTextFilter *filter, + std::string &error) + { + try + { + std::ofstream out(path, std::ios::out | std::ios::trunc); + if (!out.is_open()) + { + error = "failed to open dump path"; + return false; + } + + out << "PS2 runtime log dump\n"; + out << "entries=" << entries.size() << "\n"; + out << "filter=" << (filter && filter->IsActive() ? filter->InputBuf : "") << "\n"; + out << "ps2_log_txt=" << ps2_log::log_path() << "\n"; + out << "\n"; + out << "seq\ttext\n"; + + for (const ps2_log::RuntimeLogEntry &entry : entries) + { + if (filter && filter->IsActive() && !filter->PassFilter(entry.text.c_str())) + { + continue; + } + out << entry.seq << '\t' << entry.text; + if (entry.text.empty() || entry.text.back() != '\n') + { + out << '\n'; + } + } + return true; + } + catch (const std::exception &e) + { + error = e.what(); + return false; + } + } + + void drawLogsTab() + { + static ImGuiTextFilter s_logFilter; + static bool s_autoScroll = true; + static bool s_wrapText = false; + static bool s_latestFirst = false; + static std::string s_lastLogDumpPath; + static std::string s_lastLogDumpError; + + bool paused = ps2_log::is_runtime_log_paused(); + std::vector entries = ps2_log::snapshot_runtime_log_entries(); + + ImGui::SeparatorText("Runtime log ring buffer"); + ImGui::Text("Entries: %zu / %zu%s", entries.size(), ps2_log::kMaxRuntimeLogEntries, paused ? " (paused)" : ""); + ImGui::TextDisabled("Captures RUNTIME_LOG(...) calls. Direct std::cerr/std::cout writes are still console-only unless routed through RUNTIME_LOG."); + ImGui::TextDisabled("Aggressive call trace file: %s", ps2_log::log_path().c_str()); + + if (ImGui::Button("Clear logs")) + { + ps2_log::clear_runtime_log_entries(); + entries.clear(); + } + ImGui::SameLine(); + if (ImGui::Checkbox("Pause capture", &paused)) + { + ps2_log::set_runtime_log_paused(paused); + } + ImGui::SameLine(); + ImGui::Checkbox("Auto-scroll", &s_autoScroll); + ImGui::SameLine(); + ImGui::Checkbox("Wrap", &s_wrapText); + ImGui::SameLine(); + ImGui::Checkbox("Latest first", &s_latestFirst); + + s_logFilter.Draw("Filter", 260.0f); + ImGui::SameLine(); + if (ImGui::Button("Copy visible")) + { + std::ostringstream out; + auto appendEntry = [&](const ps2_log::RuntimeLogEntry &entry) + { + if (s_logFilter.IsActive() && !s_logFilter.PassFilter(entry.text.c_str())) + { + return; + } + out << entry.seq << '\t' << entry.text; + if (entry.text.empty() || entry.text.back() != '\n') + { + out << '\n'; + } + }; + + if (s_latestFirst) + { + for (auto it = entries.rbegin(); it != entries.rend(); ++it) + { + appendEntry(*it); + } + } + else + { + for (const ps2_log::RuntimeLogEntry &entry : entries) + { + appendEntry(entry); + } + } + const std::string text = out.str(); + ImGui::SetClipboardText(text.c_str()); + } + ImGui::SameLine(); + if (ImGui::Button("Dump logs TXT")) + { + s_lastLogDumpError.clear(); + const std::filesystem::path dumpPath = makeDebugDumpPath("runtime_log", 0u); + if (writeRuntimeLogDump(dumpPath, entries, &s_logFilter, s_lastLogDumpError)) + { + s_lastLogDumpPath = dumpPath.string(); + ImGui::SetClipboardText(s_lastLogDumpPath.c_str()); + } + else + { + s_lastLogDumpPath.clear(); + } + } + + if (!s_lastLogDumpPath.empty()) + { + ImGui::TextDisabled("Last dump: %s", s_lastLogDumpPath.c_str()); + } + if (!s_lastLogDumpError.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.35f, 1.0f), "Dump failed: %s", s_lastLogDumpError.c_str()); + } + + ImGui::Separator(); + const ImGuiTableFlags tableFlags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX; + if (ImGui::BeginTable("runtime_logs", 2, tableFlags, ImVec2(0, 430))) + { + ImGui::TableSetupColumn("Seq", ImGuiTableColumnFlags_WidthFixed, 80.0f); + ImGui::TableSetupColumn("Text", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + auto drawEntry = [&](const ps2_log::RuntimeLogEntry &entry) + { + if (s_logFilter.IsActive() && !s_logFilter.PassFilter(entry.text.c_str())) + { + return; + } + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%llu", static_cast(entry.seq)); + ImGui::TableNextColumn(); + if (s_wrapText) + { + ImGui::TextWrapped("%s", entry.text.c_str()); + } + else + { + ImGui::TextUnformatted(entry.text.c_str()); + } + }; + + if (s_latestFirst) + { + for (auto it = entries.rbegin(); it != entries.rend(); ++it) + { + drawEntry(*it); + } + } + else + { + for (const ps2_log::RuntimeLogEntry &entry : entries) + { + drawEntry(entry); + } + } + + if (s_autoScroll && !s_latestFirst && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f) + { + ImGui::SetScrollHereY(1.0f); + } + ImGui::EndTable(); + } + } + +#endif +} + +void PS2DebugPanel::initialize() +{ +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) + if (!m_initialized) + { + rlImGuiSetup(true); + m_initialized = true; + } +#endif +} + +void PS2DebugPanel::shutdown() +{ +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) + if (m_initialized) + { + rlImGuiShutdown(); + m_initialized = false; + } +#endif +} + +void PS2DebugPanel::draw(PS2Runtime &runtime) +{ +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) + if (!m_initialized) + { + return; + } + + if (IsKeyPressed(KEY_F1)) + { + toggleVisible(); + } + + if (!m_visible) + { + return; + } + + rlImGuiBegin(); + + ImGui::SetNextWindowSize(ImVec2(780.0f, 620.0f), ImGuiCond_FirstUseEver); + if (ImGui::Begin("Runtime Debugger", &m_visible, ImGuiWindowFlags_MenuBar)) + { + if (ImGui::BeginMenuBar()) + { + ImGui::MenuItem("Registers", nullptr, &m_showRegisters); + ImGui::EndMenuBar(); + } + + if (ImGui::BeginTabBar("debug-tabs")) + { + if (ImGui::BeginTabItem("CPU")) + { + drawCpuTab(runtime, m_showRegisters); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Threads")) + { + drawThreadsTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Kernel")) + { + drawKernelTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("IOP/SIF")) + { + drawIopTab(runtime); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("RPC History")) + { + drawRpcHistoryTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("PAD")) + { + drawPadTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("File/CD")) + { + drawFileCdTab(runtime); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("GS")) + { + drawGsTab(runtime); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("DMA/IO")) + { + drawIoTab(runtime); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Logs")) + { + drawLogsTab(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Memory")) + { + uint32_t bytes = m_memoryBytes; + ImGui::InputScalar("Address", ImGuiDataType_U32, &m_memoryAddress, nullptr, nullptr, "%08X", ImGuiInputTextFlags_CharsHexadecimal); + ImGui::InputScalar("Bytes", ImGuiDataType_U32, &bytes, nullptr, nullptr, "%u"); + m_memoryBytes = std::clamp(bytes, 16u, 0x1000u); + ImGui::SameLine(); + if (ImGui::Button("PC")) + m_memoryAddress = runtime.m_debugPc.load(std::memory_order_relaxed); + ImGui::SameLine(); + if (ImGui::Button("SP")) + m_memoryAddress = runtime.m_debugSp.load(std::memory_order_relaxed); + ImGui::SameLine(); + if (ImGui::Button("GP")) + m_memoryAddress = runtime.m_debugGp.load(std::memory_order_relaxed); + ImGui::Separator(); + drawHexDump(runtime.memory().getRDRAM(), m_memoryAddress, m_memoryBytes); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + ImGui::End(); + + rlImGuiEnd(); +#else + (void)runtime; +#endif +} diff --git a/ps2xRuntime/src/lib/ps2_gif_arbiter.cpp b/ps2xRuntime/src/lib/ps2_gif_arbiter.cpp index 3f5f292..1b83711 100644 --- a/ps2xRuntime/src/lib/ps2_gif_arbiter.cpp +++ b/ps2xRuntime/src/lib/ps2_gif_arbiter.cpp @@ -1,30 +1,6 @@ #include "runtime/ps2_gif_arbiter.h" -#include "ps2_log.h" #include -#include #include -#include - -namespace -{ - std::atomic s_debugGifArbiterSubmitCount{0}; - std::atomic s_debugGifArbiterDrainCount{0}; - - const char *pathName(GifPathId id) - { - switch (id) - { - case GifPathId::Path1: - return "path1"; - case GifPathId::Path2: - return "path2"; - case GifPathId::Path3: - return "path3"; - default: - return "path?"; - } - } -} GifArbiter::GifArbiter(ProcessPacketFn processFn) : m_processFn(std::move(processFn)) @@ -47,26 +23,6 @@ void GifArbiter::submit(GifPathId pathId, const uint8_t *data, uint32_t sizeByte if (!data || sizeBytes < 16 || !m_processFn) return; - const uint32_t debugIndex = s_debugGifArbiterSubmitCount.fetch_add(1, std::memory_order_relaxed); - if (debugIndex < 96u) - { - uint64_t tagLo = 0; - std::memcpy(&tagLo, data, sizeof(tagLo)); - const uint32_t nloop = static_cast(tagLo & 0x7FFFu); - const uint8_t flg = static_cast((tagLo >> 58) & 0x3u); - uint32_t nreg = static_cast((tagLo >> 60) & 0xFu); - if (nreg == 0u) - nreg = 16u; - RUNTIME_LOG("[gif:submit] idx=" << debugIndex - << " path=" << pathName(pathId) - << " size=" << sizeBytes - << " nloop=" << nloop - << " flg=" << static_cast(flg) - << " nreg=" << nreg - << " directhl=" << static_cast(path2DirectHl ? 1u : 0u) - << std::endl); - } - GifArbiterPacket pkt; pkt.pathId = pathId; pkt.path2DirectHl = (pathId == GifPathId::Path2) && path2DirectHl; @@ -100,26 +56,6 @@ void GifArbiter::drain() auto &pkt = m_queue[i]; if (!pkt.data.empty()) { - const uint32_t debugIndex = s_debugGifArbiterDrainCount.fetch_add(1, std::memory_order_relaxed); - if (debugIndex < 96u) - { - uint64_t tagLo = 0; - std::memcpy(&tagLo, pkt.data.data(), sizeof(tagLo)); - const uint32_t nloop = static_cast(tagLo & 0x7FFFu); - const uint8_t flg = static_cast((tagLo >> 58) & 0x3u); - uint32_t nreg = static_cast((tagLo >> 60) & 0xFu); - if (nreg == 0u) - nreg = 16u; - RUNTIME_LOG("[gif:drain] idx=" << debugIndex - << " path=" << pathName(pkt.pathId) - << " size=" << pkt.data.size() - << " nloop=" << nloop - << " flg=" << static_cast(flg) - << " nreg=" << nreg - << " directhl=" << static_cast(pkt.path2DirectHl ? 1u : 0u) - << " path3image=" << static_cast(pkt.path3Image ? 1u : 0u) - << std::endl); - } m_processFn(pkt.data.data(), static_cast(pkt.data.size())); } } diff --git a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp index f69a986..257f407 100644 --- a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp @@ -420,6 +420,12 @@ void GS::reset() m_hostPresentationUsedPreferred = false; m_hasHostPresentationFrame = false; + m_debugHistoryWrite = 0; + m_debugHistoryCount = 0; + m_debugNextSeq = 1; + m_debugFrameIndex = 0; + m_debugLastVsyncTick = UINT64_MAX; + for (int i = 0; i < 2; ++i) { m_ctx[i].frame.fbw = 10; @@ -456,6 +462,225 @@ const uint8_t *GS::lockDisplaySnapshot(uint32_t &outSize) return m_displaySnapshot.data(); } +GSDebugSnapshot GS::getDebugSnapshot() const +{ + std::lock_guard lock(m_stateMutex); + + GSDebugSnapshot snapshot{}; + snapshot.ctx[0] = m_ctx[0]; + snapshot.ctx[1] = m_ctx[1]; + snapshot.prim = m_prim; + snapshot.texa = m_texa; + snapshot.texclut = m_texclut; + snapshot.bitbltbuf = m_bitbltbuf; + snapshot.trxpos = m_trxpos; + snapshot.trxreg = m_trxreg; + snapshot.trxdir = m_trxdir; + snapshot.transferX = m_transferState.x; + snapshot.transferY = m_transferState.y; + snapshot.transferTotalPixels = m_transferState.total_pixels; + snapshot.transferCopiedPixels = m_transferState.copied_pixels; + snapshot.lastDisplayBaseBytes = m_lastDisplayBaseBytes; + snapshot.preferredDisplaySourceFrame = m_preferredDisplaySourceFrame; + snapshot.preferredDisplayDestFbp = m_preferredDisplayDestFbp; + snapshot.hasPreferredDisplaySource = m_hasPreferredDisplaySource; + snapshot.hostPresentationWidth = m_hostPresentationWidth; + snapshot.hostPresentationHeight = m_hostPresentationHeight; + snapshot.hostPresentationDisplayFbp = m_hostPresentationDisplayFbp; + snapshot.hostPresentationSourceFbp = m_hostPresentationSourceFbp; + snapshot.hostPresentationUsedPreferred = m_hostPresentationUsedPreferred; + snapshot.hasHostPresentationFrame = m_hasHostPresentationFrame; + snapshot.localToHostPendingBytes = (m_localToHostReadPos < m_localToHostBuffer.size()) + ? (m_localToHostBuffer.size() - m_localToHostReadPos) + : 0u; + return snapshot; +} + + +std::vector GS::getDebugHistory() const +{ + std::lock_guard lock(m_stateMutex); + + std::vector out; + out.reserve(m_debugHistoryCount); + const size_t first = (m_debugHistoryWrite + kDebugHistoryCapacity - m_debugHistoryCount) % kDebugHistoryCapacity; + for (size_t i = 0; i < m_debugHistoryCount; ++i) + { + out.push_back(m_debugHistory[(first + i) % kDebugHistoryCapacity]); + } + return out; +} + +void GS::clearDebugHistory() +{ + std::lock_guard lock(m_stateMutex); + m_debugHistoryWrite = 0; + m_debugHistoryCount = 0; + m_debugNextSeq = 1; + m_debugFrameIndex = 0; + m_debugLastVsyncTick = UINT64_MAX; +} + +bool GS::isDebugHistoryPaused() const +{ + std::lock_guard lock(m_stateMutex); + return m_debugHistoryPaused; +} + +void GS::setDebugHistoryPaused(bool paused) +{ + std::lock_guard lock(m_stateMutex); + m_debugHistoryPaused = paused; +} + +GSDebugHistoryEntry GS::makeDebugEventUnlocked(GSDebugEventKind kind) const +{ + GSDebugHistoryEntry entry{}; + entry.kind = kind; + entry.prim = m_prim; + const uint32_t ci = m_prim.ctxt ? 1u : 0u; + entry.frame = m_ctx[ci].frame; + entry.zbuf = m_ctx[ci].zbuf; + entry.tex0 = m_ctx[ci].tex0; + entry.scissor = m_ctx[ci].scissor; + entry.test = m_ctx[ci].test; + entry.alpha = m_ctx[ci].alpha; + entry.bitbltbuf = m_bitbltbuf; + entry.trxpos = m_trxpos; + entry.trxreg = m_trxreg; + entry.trxdir = m_trxdir; + entry.transferPixels = m_transferState.total_pixels; + return entry; +} + +void GS::recordDebugEventUnlocked(GSDebugHistoryEntry entry) +{ + if (m_debugHistoryPaused) + { + return; + } + + const uint64_t tick = ps2_syscalls::GetCurrentVSyncTick(); + if (m_debugLastVsyncTick == UINT64_MAX) + { + m_debugLastVsyncTick = tick; + } + else if (tick != m_debugLastVsyncTick) + { + ++m_debugFrameIndex; + m_debugLastVsyncTick = tick; + } + + entry.seq = m_debugNextSeq++; + entry.vsyncTick = tick; + entry.frameIndex = m_debugFrameIndex; + + m_debugHistory[m_debugHistoryWrite] = entry; + m_debugHistoryWrite = (m_debugHistoryWrite + 1u) % kDebugHistoryCapacity; + if (m_debugHistoryCount < kDebugHistoryCapacity) + { + ++m_debugHistoryCount; + } +} + +void GS::recordGifTagDebugEventUnlocked(uint32_t sizeBytes, uint32_t nloop, uint8_t flg, uint32_t nreg) +{ + GSDebugHistoryEntry entry = makeDebugEventUnlocked(GSDebugEventKind::GifTag); + entry.gifSizeBytes = sizeBytes; + entry.gifNloop = nloop; + entry.gifFlg = flg; + entry.gifNreg = static_cast(std::min(nreg, 16u)); + recordDebugEventUnlocked(entry); +} + +void GS::recordRegisterDebugEventUnlocked(uint8_t regAddr, uint64_t value) +{ + switch (regAddr) + { + case GS_REG_PRIM: + case GS_REG_TEX0_1: + case GS_REG_TEX0_2: + case GS_REG_TEX2_1: + case GS_REG_TEX2_2: + case GS_REG_TEXA: + case GS_REG_TEXCLUT: + case GS_REG_FRAME_1: + case GS_REG_FRAME_2: + case GS_REG_ZBUF_1: + case GS_REG_ZBUF_2: + case GS_REG_ALPHA_1: + case GS_REG_ALPHA_2: + case GS_REG_TEST_1: + case GS_REG_TEST_2: + case GS_REG_SCISSOR_1: + case GS_REG_SCISSOR_2: + case GS_REG_XYOFFSET_1: + case GS_REG_XYOFFSET_2: + case GS_REG_BITBLTBUF: + case GS_REG_TRXPOS: + case GS_REG_TRXREG: + case GS_REG_TRXDIR: + break; + default: + return; + } + + GSDebugHistoryEntry entry = makeDebugEventUnlocked(GSDebugEventKind::Register); + entry.reg = regAddr; + entry.regValue = value; + recordDebugEventUnlocked(entry); +} + +void GS::recordDrawDebugEventUnlocked(int vertexCount) +{ + if (vertexCount <= 0) + { + return; + } + + GSDebugHistoryEntry entry = makeDebugEventUnlocked(GSDebugEventKind::Draw); + entry.vertexCount = static_cast(vertexCount); + + const int count = std::min(vertexCount, kMaxVerts); + entry.xMin = entry.xMax = m_vtxQueue[0].x; + entry.yMin = entry.yMax = m_vtxQueue[0].y; + entry.zMin = entry.zMax = m_vtxQueue[0].z; + entry.aMin = entry.aMax = m_vtxQueue[0].a; + + for (int i = 1; i < count; ++i) + { + const GSVertex &v = m_vtxQueue[i]; + entry.xMin = std::min(entry.xMin, v.x); + entry.xMax = std::max(entry.xMax, v.x); + entry.yMin = std::min(entry.yMin, v.y); + entry.yMax = std::max(entry.yMax, v.y); + entry.zMin = std::min(entry.zMin, v.z); + entry.zMax = std::max(entry.zMax, v.z); + entry.aMin = std::min(entry.aMin, v.a); + entry.aMax = std::max(entry.aMax, v.a); + } + + recordDebugEventUnlocked(entry); +} + +void GS::recordTransferDebugEventUnlocked() +{ + GSDebugHistoryEntry entry = makeDebugEventUnlocked(GSDebugEventKind::Transfer); + entry.transferPixels = m_transferState.total_pixels; + recordDebugEventUnlocked(entry); +} + +void GS::recordPresentDebugEventUnlocked(uint32_t displayFbp, uint32_t sourceFbp, uint32_t width, uint32_t height, bool usedPreferred) +{ + GSDebugHistoryEntry entry = makeDebugEventUnlocked(GSDebugEventKind::Present); + entry.displayFbp = displayFbp; + entry.sourceFbp = sourceFbp; + entry.width = width; + entry.height = height; + entry.usedPreferred = usedPreferred; + recordDebugEventUnlocked(entry); +} + bool GS::getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const { std::lock_guard lock(m_stateMutex); @@ -501,7 +726,12 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, return false; } - outPixels.assign(kHostFrameWidth * kHostFrameHeight * 4u, 0u); + outPixels.resize(kHostFrameWidth * kHostFrameHeight * 4u); + auto failCopy = [&outPixels]() -> bool + { + outPixels.clear(); + return false; + }; const uint32_t baseBytes = frameBaseIsPages ? (frame.fbp * 8192u) : (frame.fbp * 256u); const uint32_t basePtr = frameBaseIsPages ? GSInternal::framePageBaseToBlock(frame.fbp) : frame.fbp; @@ -554,7 +784,7 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, const uint32_t srcOff = baseBytes + (srcY * strideBytes) + (srcX * srcPixelBytes); if (srcOff + srcPixelBytes > m_vramSize) { - return false; + return failCopy(); } dstRow[x * 4u + 0u] = m_vram[srcOff + 0u]; @@ -605,7 +835,7 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, const uint32_t srcOff = baseBytes + (srcY * strideBytes) + (srcX * 2u); if (srcOff + sizeof(uint16_t) > m_vramSize) { - return false; + return failCopy(); } uint16_t pixel = 0u; @@ -622,13 +852,29 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, return true; } - return false; + return failCopy(); } void GS::latchHostPresentationFrame() { std::lock_guard lock(m_stateMutex); + latchHostPresentationFrameUnlocked(); +} +bool GS::tryLatchHostPresentationFrame() +{ + if (!m_stateMutex.try_lock()) + { + return false; + } + + std::lock_guard lock(m_stateMutex, std::adopt_lock); + latchHostPresentationFrameUnlocked(); + return true; +} + +void GS::latchHostPresentationFrameUnlocked() +{ if (!m_privRegs || !m_vram || m_vramSize == 0u) { m_hostPresentationFrame.clear(); @@ -856,6 +1102,11 @@ void GS::latchHostPresentationFrame() m_hostPresentationSourceFbp = selectedFrame1.fbp; m_hostPresentationUsedPreferred = false; m_hasHostPresentationFrame = true; + recordPresentDebugEventUnlocked(m_hostPresentationDisplayFbp, + m_hostPresentationSourceFbp, + m_hostPresentationWidth, + m_hostPresentationHeight, + m_hostPresentationUsedPreferred); return; } } @@ -894,6 +1145,11 @@ void GS::latchHostPresentationFrame() m_hostPresentationSourceFbp = selectedFrame.fbp; m_hostPresentationUsedPreferred = usedPreferred; m_hasHostPresentationFrame = true; + recordPresentDebugEventUnlocked(m_hostPresentationDisplayFbp, + m_hostPresentationSourceFbp, + m_hostPresentationWidth, + m_hostPresentationHeight, + m_hostPresentationUsedPreferred); } bool GS::copyLatchedHostPresentationFrame(std::vector &outPixels, @@ -928,7 +1184,7 @@ bool GS::copyLatchedHostPresentationFrame(std::vector &outPixels, *outUsedPreferred = m_hostPresentationUsedPreferred; const size_t packedRowBytes = static_cast(outWidth) * 4u; - outPixels.assign(packedRowBytes * static_cast(outHeight), 0u); + outPixels.resize(packedRowBytes * static_cast(outHeight)); if (outWidth != 0u && outHeight != 0u) { const size_t sourceRowBytes = static_cast(kHostFrameWidth) * 4u; @@ -986,6 +1242,7 @@ void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes) } }); + uint32_t offset = 0; while (offset + 16 <= sizeBytes) { @@ -1001,6 +1258,8 @@ void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes) if (nreg == 0) nreg = 16; + recordGifTagDebugEventUnlocked(sizeBytes, nloop, flg, nreg); + bool pre = ((tagLo >> 46) & 1) != 0; if (pre) { @@ -1078,8 +1337,8 @@ void GS::writeRegisterPacked(uint8_t regDesc, uint64_t lo, uint64_t hi) break; } case 0x03: - m_curU = static_cast(lo & 0xFFFFu); - m_curV = static_cast((lo >> 32) & 0xFFFFu); + m_curU = static_cast(lo & 0x3FFFu); + m_curV = static_cast((lo >> 32) & 0x3FFFu); break; case 0x04: { @@ -1566,6 +1825,7 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) { performLocalToHostToBuffer(); } + recordTransferDebugEventUnlocked(); break; } case GS_REG_HWREG: @@ -1663,6 +1923,8 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) default: break; } + + recordRegisterDebugEventUnlocked(regAddr, value); } void GS::performLocalToLocalTransfer() @@ -1841,6 +2103,7 @@ void GS::vertexKick(bool drawing) return; m_rasterizer.drawPrimitive(this); + recordDrawDebugEventUnlocked(needed); switch (m_prim.type) { diff --git a/ps2xRuntime/src/lib/ps2_iop.cpp b/ps2xRuntime/src/lib/ps2_iop.cpp index 62cc5ac..f4db8ab 100644 --- a/ps2xRuntime/src/lib/ps2_iop.cpp +++ b/ps2xRuntime/src/lib/ps2_iop.cpp @@ -1,15 +1,12 @@ #include "runtime/ps2_iop.h" #include "runtime/ps2_iop_audio.h" +#include "runtime/ps2_iop_cl.h" #include "runtime/ps2_iop_dbcman.h" +#include "runtime/ps2_iop_sdrdrv.h" #include "runtime/ps2_memory.h" #include "ps2_runtime.h" #include "Kernel/Syscalls/RPC.h" -// ps2_iop.cpp - -namespace -{ -} ps2_iop::ps2_iop() { reset(); @@ -22,27 +19,32 @@ void ps2_iop::init(uint8_t *rdram) void ps2_iop::reset() { + ps2_iop_cl::reset(); + ps2_iop_dbcman::reset(); + ps2_iop_sdrdrv::reset(); } bool ps2_iop::handleRPC(PS2Runtime *runtime, - uint32_t sid, uint32_t rpcNum, - uint32_t sendBufAddr, uint32_t sendSize, - uint32_t recvBufAddr, uint32_t recvSize, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, uint32_t &resultPtr, bool &signalNowaitCompletion) { resultPtr = 0u; signalNowaitCompletion = false; - if (!runtime || !m_rdram) - { - return false; - } - - if (ps2_syscalls::handleSoundDriverRpcService(m_rdram, runtime, - sid, rpcNum, - sendBufAddr, sendSize, - recvBufAddr, recvSize, + if (ps2_syscalls::handleSoundDriverRpcService(m_rdram, + runtime, + sid, + rpcNum, + sendBufAddr, + sendSize, + recvBufAddr, + recvSize, resultPtr, signalNowaitCompletion)) { @@ -50,20 +52,52 @@ bool ps2_iop::handleRPC(PS2Runtime *runtime, } if (ps2_iop_dbcman::handleDbcManRpc(m_rdram, - sid, rpcNum, - sendBufAddr, sendSize, - recvBufAddr, recvSize, + sid, + rpcNum, + sendBufAddr, + sendSize, + recvBufAddr, + recvSize, resultPtr)) { return true; } - if (sid == IOP_SID_LIBSD) + if (ps2_iop_audio::handleLibSdRpc(m_rdram, + runtime, + sid, + rpcNum, + sendBufAddr, + sendSize, + recvBufAddr, + recvSize, + resultPtr)) + { + return true; + } + + if (ps2_iop_cl::handleSoundRpc(m_rdram, sid, + rpcNum, sendBufAddr, + sendSize, recvBufAddr, + recvSize, resultPtr, + signalNowaitCompletion)) + { + return true; + } + + if (ps2_iop_cl::handleClFileRpc(m_rdram, sid, + rpcNum, sendBufAddr, + sendSize, recvBufAddr, + recvSize, resultPtr)) + { + return true; + } + + if (ps2_iop_sdrdrv::handleSdrdrvRpc(m_rdram, sid, + rpcNum, sendBufAddr, + sendSize, recvBufAddr, + recvSize, resultPtr)) { - const uint8_t *sendPtr = sendBufAddr ? getConstMemPtr(m_rdram, sendBufAddr) : nullptr; - uint8_t *recvPtr = recvBufAddr ? getMemPtr(m_rdram, recvBufAddr) : nullptr; - ps2_iop_audio::handleLibSdRpc(runtime, sid, rpcNum, sendPtr, sendSize, recvPtr, recvSize); - resultPtr = recvBufAddr; return true; } diff --git a/ps2xRuntime/src/lib/ps2_iop_audio.cpp b/ps2xRuntime/src/lib/ps2_iop_audio.cpp index a3fa0ee..0268796 100644 --- a/ps2xRuntime/src/lib/ps2_iop_audio.cpp +++ b/ps2xRuntime/src/lib/ps2_iop_audio.cpp @@ -3,12 +3,35 @@ namespace ps2_iop_audio { - void handleLibSdRpc(PS2Runtime *runtime, uint32_t sid, uint32_t rpcNum, - const uint8_t *sendBuf, uint32_t sendSize, - uint8_t *recvBuf, uint32_t recvSize) + void reset() { - if (!runtime) - return; - runtime->audioBackend().onSoundCommand(sid, rpcNum, sendBuf, sendSize, recvBuf, recvSize); + // Noop. + } + + bool handleLibSdRpc(uint8_t *rdram, + PS2Runtime *runtime, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr) + { + if (sid != IOP_SID_LIBSD) + return false; + + const uint8_t *sendPtr = sendBufAddr ? getConstMemPtr(rdram, sendBufAddr) : nullptr; + uint8_t *recvPtr = recvBufAddr ? getMemPtr(rdram, recvBufAddr) : nullptr; + + runtime->audioBackend().onSoundCommand(sid, + rpcNum, + sendPtr, + sendSize, + recvPtr, + recvSize); + + resultPtr = recvBufAddr; + return true; } } diff --git a/ps2xRuntime/src/lib/ps2_iop_cl.cpp b/ps2xRuntime/src/lib/ps2_iop_cl.cpp new file mode 100644 index 0000000..ff6fea6 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_iop_cl.cpp @@ -0,0 +1,514 @@ +#include "runtime/ps2_iop_cl.h" +#include "runtime/ps2_iop.h" +#include "runtime/ps2_memory.h" +#include "Kernel/Syscalls/Common.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr uint32_t kClFileBufferBytes = 0x2000u; + constexpr uint32_t kClFileLoadResultQueued = 5u; + constexpr uint32_t kClFileLoadStatusFailed = 3u; + constexpr uint32_t kClFileLoadStatusComplete = 7u; + constexpr uint32_t kClFileFirstLoadHandle = 0x00010000u; + constexpr uint32_t kLotrSoundResponseCounterOffset = 4u; + + struct ClFileHandle + { + FILE *file = nullptr; + uint32_t size = 0u; + }; + + struct ClFileLoad + { + uint32_t status = kClFileLoadStatusFailed; + uint32_t size = 0u; + }; + + std::mutex g_clFileMutex; + std::unordered_map g_clFileHandles; + std::unordered_map g_clFileLoads; + uint32_t g_nextFileHandle = 1u; + uint32_t g_nextLoadHandle = kClFileFirstLoadHandle; + std::string g_clFileRoot; + + std::mutex g_lotrSoundMutex; + uint32_t g_lotrSoundCounter = 0u; + + bool readGuestU32(const uint8_t *rdram, uint32_t addr, uint32_t &out) + { + const uint8_t *ptr = getConstMemPtr(rdram, addr); + if (!ptr) + { + return false; + } + + std::memcpy(&out, ptr, sizeof(out)); + return true; + } + + bool writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value) + { + uint8_t *ptr = getMemPtr(rdram, addr); + if (!ptr) + { + return false; + } + + std::memcpy(ptr, &value, sizeof(value)); + return true; + } + + std::string readGuestString(uint8_t *rdram, uint32_t addr, size_t maxBytes) + { + const char *ptr = reinterpret_cast(getConstMemPtr(rdram, addr)); + if (!ptr || maxBytes == 0u) + { + return {}; + } + + size_t length = 0u; + while (length < maxBytes && ptr[length] != '\0') + { + ++length; + } + + return std::string(ptr, length); + } + + bool hasPs2PathDevice(const std::string &path) + { + return path.find(':') != std::string::npos; + } + + std::string joinPs2Path(const std::string &root, const std::string &leaf) + { + if (root.empty() || leaf.empty() || hasPs2PathDevice(leaf)) + { + return leaf; + } + + const char tail = root.back(); + if (tail == '/' || tail == '\\' || tail == ':') + { + return root + leaf; + } + return root + "/" + leaf; + } + + std::filesystem::path resolveClFilePath(const std::string &path) + { + std::string root; + { + std::lock_guard lock(g_clFileMutex); + root = g_clFileRoot; + } + + const std::string rootedPath = joinPs2Path(root, path); + const std::string translated = translatePs2Path(rootedPath.c_str()); + return translated.empty() ? std::filesystem::path{} : std::filesystem::path(translated); + } + + uint32_t allocateClFileHandleLocked(FILE *file, uint32_t size) + { + if (!file) + { + return 0u; + } + + for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) + { + uint32_t handle = g_nextFileHandle++; + if (handle == 0u) + { + handle = g_nextFileHandle++; + } + + if (g_clFileHandles.find(handle) == g_clFileHandles.end() && + g_clFileLoads.find(handle) == g_clFileLoads.end()) + { + g_clFileHandles.emplace(handle, ClFileHandle{file, size}); + return handle; + } + } + + return 0u; + } + + uint32_t allocateClFileLoadLocked(uint32_t status, uint32_t size) + { + for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) + { + uint32_t handle = g_nextLoadHandle++; + if (handle < 3u) + { + handle = kClFileFirstLoadHandle; + g_nextLoadHandle = kClFileFirstLoadHandle + 1u; + } + + if (g_clFileLoads.find(handle) == g_clFileLoads.end() && + g_clFileHandles.find(handle) == g_clFileHandles.end()) + { + g_clFileLoads.emplace(handle, ClFileLoad{status, size}); + return handle; + } + } + + return 0u; + } +} + +namespace ps2_iop_cl +{ + void reset() + { + { + std::lock_guard lock(g_clFileMutex); + for (auto &entry : g_clFileHandles) + { + if (entry.second.file) + { + std::fclose(entry.second.file); + } + } + + g_clFileHandles.clear(); + g_clFileLoads.clear(); + g_clFileRoot.clear(); + g_nextFileHandle = 1u; + g_nextLoadHandle = kClFileFirstLoadHandle; + } + + { + std::lock_guard lock(g_lotrSoundMutex); + g_lotrSoundCounter = 0u; + } + } + + bool handleClFileRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr) + { + if (sid != IOP_SID_LOTR_CLFILE) + { + return false; + } + + resultPtr = recvBufAddr; + if (recvBufAddr && recvSize > 0u) + { + if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) + { + std::memset(recv, 0, std::min(recvSize, 0x40u)); + } + } + + auto writeResult = [&](int32_t status, uint32_t value) { + if (recvBufAddr && recvSize >= sizeof(uint32_t)) + { + writeGuestU32(rdram, recvBufAddr + 0u, static_cast(status)); + } + if (recvBufAddr && recvSize >= sizeof(uint32_t) * 2u) + { + writeGuestU32(rdram, recvBufAddr + 4u, value); + } + }; + + switch (rpcNum) + { + case 0x01u: + { + const std::string guestPath = readGuestString(rdram, sendBufAddr, sendSize ? std::min(sendSize, 0x100u) : 0x100u); + uint32_t requestedBytes = 0u; + uint32_t dstAddr = 0u; + (void)readGuestU32(rdram, sendBufAddr + 0x100u, requestedBytes); + (void)readGuestU32(rdram, sendBufAddr + 0x104u, dstAddr); + + uint32_t status = kClFileLoadStatusFailed; + uint32_t fileSize = 0u; + const std::filesystem::path hostPath = resolveClFilePath(guestPath); + + if (!hostPath.empty()) + { + std::ifstream file(hostPath, std::ios::binary | std::ios::ate); + if (file.is_open()) + { + const std::streampos end = file.tellg(); + if (end >= std::streampos(0)) + { + const uint64_t hostFileSize = static_cast(end); + fileSize = static_cast(std::min(hostFileSize, 0xFFFFFFFFull)); + + uint32_t dstOffset = 0u; + bool scratch = false; + if (ps2ResolveGuestPointer(dstAddr, dstOffset, scratch) && !scratch && dstOffset < PS2_RAM_SIZE) + { + uint8_t *dst = getMemPtr(rdram, dstAddr); + const uint64_t maxGuestBytes = static_cast(PS2_RAM_SIZE - dstOffset); + const uint64_t maxRequestedBytes = requestedBytes ? requestedBytes : hostFileSize; + const size_t bytesToCopy = static_cast( + std::min({hostFileSize, maxRequestedBytes, maxGuestBytes})); + + status = kClFileLoadStatusComplete; + if (dst && bytesToCopy > 0u) + { + file.seekg(0, std::ios::beg); + file.read(reinterpret_cast(dst), static_cast(bytesToCopy)); + if (static_cast(file.gcount()) != bytesToCopy) + { + status = kClFileLoadStatusFailed; + } + } + } + } + } + } + + uint32_t loadHandle = 0u; + { + std::lock_guard lock(g_clFileMutex); + loadHandle = allocateClFileLoadLocked(status, fileSize); + } + + writeResult(kClFileLoadResultQueued, loadHandle); + return true; + } + + case 0x04u: + writeResult(0, 1u); + return true; + + case 0x05u: + writeResult(0, 0u); + return true; + + case 0x15u: + writeResult(0, 0u); + return true; + + case 0x16u: + { + const std::string root = readGuestString(rdram, sendBufAddr, sendSize ? sendSize : 0x100u); + std::lock_guard lock(g_clFileMutex); + g_clFileRoot = root; + writeResult(0, 1u); + return true; + } + + case 0x08u: + { + const std::string guestPath = readGuestString(rdram, sendBufAddr, sendSize ? sendSize : 0x100u); + const std::filesystem::path hostPath = resolveClFilePath(guestPath); + if (hostPath.empty()) + { + writeResult(-1, 0u); + return true; + } + + FILE *file = std::fopen(hostPath.string().c_str(), "rb"); + if (!file) + { + writeResult(-1, 0u); + return true; + } + + uint32_t size = 0u; + if (std::fseek(file, 0, SEEK_END) == 0) + { + const long end = std::ftell(file); + if (end > 0) + { + size = static_cast(std::min(end, 0x7FFFFFFF)); + } + std::fseek(file, 0, SEEK_SET); + } + + uint32_t handle = 0u; + { + std::lock_guard lock(g_clFileMutex); + handle = allocateClFileHandleLocked(file, size); + } + if (!handle) + { + std::fclose(file); + writeResult(-1, 0u); + return true; + } + + writeResult(0, handle); + return true; + } + + case 0x09u: + { + uint32_t handle = 0u; + readGuestU32(rdram, sendBufAddr, handle); + + FILE *file = nullptr; + bool closedLoad = false; + { + std::lock_guard lock(g_clFileMutex); + auto it = g_clFileHandles.find(handle); + if (it != g_clFileHandles.end()) + { + file = it->second.file; + g_clFileHandles.erase(it); + } + + auto loadIt = g_clFileLoads.find(handle); + if (loadIt != g_clFileLoads.end()) + { + g_clFileLoads.erase(loadIt); + closedLoad = true; + } + } + if (file) + { + std::fclose(file); + } + + const bool closed = file || closedLoad; + writeResult(closed ? 0 : -1, closed ? 1u : 0u); + return true; + } + + case 0x0Au: + { + uint32_t handle = 0u; + uint32_t requested = 0u; + uint32_t dstAddr = 0u; + readGuestU32(rdram, sendBufAddr + 0u, handle); + readGuestU32(rdram, sendBufAddr + 4u, requested); + readGuestU32(rdram, sendBufAddr + 8u, dstAddr); + + FILE *file = nullptr; + { + std::lock_guard lock(g_clFileMutex); + auto it = g_clFileHandles.find(handle); + if (it != g_clFileHandles.end()) + { + file = it->second.file; + } + } + + uint8_t *dst = getMemPtr(rdram, dstAddr); + if (!file || !dst) + { + writeResult(-1, 0u); + return true; + } + + const uint32_t bytesToRead = std::min(requested, kClFileBufferBytes); + const size_t bytesRead = std::fread(dst, 1u, bytesToRead, file); + if (bytesRead < bytesToRead && std::ferror(file)) + { + std::clearerr(file); + writeResult(-1, 0u); + return true; + } + + writeResult(0, static_cast(bytesRead)); + return true; + } + + case 0x03u: + { + uint32_t handle = 0u; + readGuestU32(rdram, sendBufAddr, handle); + + std::lock_guard lock(g_clFileMutex); + auto loadIt = g_clFileLoads.find(handle); + if (loadIt != g_clFileLoads.end()) + { + writeResult(static_cast(loadIt->second.status), 0u); + return true; + } + + writeResult(0, g_clFileHandles.find(handle) != g_clFileHandles.end() ? 0u : 9u); + return true; + } + + case 0x06u: + { + uint32_t handle = 0u; + readGuestU32(rdram, sendBufAddr, handle); + + std::lock_guard lock(g_clFileMutex); + auto loadIt = g_clFileLoads.find(handle); + if (loadIt != g_clFileLoads.end()) + { + writeResult(0, loadIt->second.size); + return true; + } + + auto it = g_clFileHandles.find(handle); + writeResult(it != g_clFileHandles.end() ? 0 : -1, + it != g_clFileHandles.end() ? it->second.size : 0u); + return true; + } + + default: + writeResult(0, 0u); + return true; + } + } + + bool handleSoundRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr, + bool &signalNowaitCompletion) + { + if (sid != IOP_SID_LOTR_SOUND) + { + return false; + } + + (void)rpcNum; + (void)sendBufAddr; + (void)sendSize; + + resultPtr = recvBufAddr; + signalNowaitCompletion = true; + + if (recvBufAddr && recvSize > 0u) + { + if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) + { + std::memset(recv, 0, recvSize); + } + } + + uint32_t counter = 0u; + { + std::lock_guard lock(g_lotrSoundMutex); + counter = ++g_lotrSoundCounter; + } + + if (recvBufAddr && recvSize >= kLotrSoundResponseCounterOffset + sizeof(uint32_t)) + { + // LotR's SOUND_JP callback reads word 0 as the number of active + // stream records and word 1 as the IOP update counter. + writeGuestU32(rdram, recvBufAddr + kLotrSoundResponseCounterOffset, counter); + } + + return true; + } +} diff --git a/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp b/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp index 309d943..e0f3b1d 100644 --- a/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp +++ b/ps2xRuntime/src/lib/ps2_iop_dbcman.cpp @@ -1,14 +1,19 @@ #include "runtime/ps2_iop_dbcman.h" #include "runtime/ps2_memory.h" + #include #include namespace { - constexpr uint32_t IOP_SID_DBCMAN = 0x80001300u; - constexpr uint32_t DBCMAN_RPC_CHECK_VERSION = 0x80001363u; + constexpr uint32_t kDbcManSid = 0x80001300u; + constexpr uint32_t kRpcCheckVersion = 0x80001363u; + constexpr uint32_t kDbcManVersion = 0x0320u; + constexpr uint32_t kMaxUnknownRpcLogs = 32u; - static bool writeIopU32(uint8_t *rdram, uint32_t addr, uint32_t value) + uint32_t g_unknownRpcLogCount = 0u; + + bool writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value) { uint8_t *ptr = getMemPtr(rdram, addr); if (!ptr) @@ -19,10 +24,25 @@ namespace std::memcpy(ptr, &value, sizeof(value)); return true; } + + void writeVersionResponse(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize) + { + const uint32_t words = recvSize / sizeof(uint32_t); + const uint32_t count = words < 4u ? words : 4u; + for (uint32_t index = 0; index < count; ++index) + { + writeGuestU32(rdram, recvBufAddr + (index * sizeof(uint32_t)), kDbcManVersion); + } + } } namespace ps2_iop_dbcman { + void reset() + { + g_unknownRpcLogCount = 0u; + } + bool handleDbcManRpc(uint8_t *rdram, uint32_t sid, uint32_t rpcNum, @@ -32,37 +52,26 @@ namespace ps2_iop_dbcman uint32_t recvSize, uint32_t &resultPtr) { - if (sid != IOP_SID_DBCMAN) + if (sid != kDbcManSid) { return false; } + resultPtr = recvBufAddr; if (recvBufAddr == 0u || recvSize == 0u) { - resultPtr = recvBufAddr; return true; } switch (rpcNum) { - case DBCMAN_RPC_CHECK_VERSION: - { - // TODO move this to compile flag - // libdbc expects dbcman.irx version 3.20 - constexpr uint32_t DBCMAN_VERSION = 0x0320; - - writeIopU32(rdram, recvBufAddr + 0x00u, DBCMAN_VERSION); - writeIopU32(rdram, recvBufAddr + 0x04u, DBCMAN_VERSION); - writeIopU32(rdram, recvBufAddr + 0x08u, DBCMAN_VERSION); - writeIopU32(rdram, recvBufAddr + 0x0Cu, DBCMAN_VERSION); - - resultPtr = recvBufAddr; + case kRpcCheckVersion: + // libdbc expects dbcman.irx version 3.20. + writeVersionResponse(rdram, recvBufAddr, recvSize); return true; - } + default: - { - static uint32_t dbcLogCount = 0; - if (dbcLogCount < 32) + if (g_unknownRpcLogCount < kMaxUnknownRpcLogs) { std::cerr << "[DBCMAN:stub]" << " sid=0x" << std::hex << sid @@ -72,12 +81,9 @@ namespace ps2_iop_dbcman << " recv=0x" << recvBufAddr << " recvSize=0x" << recvSize << std::dec << std::endl; - ++dbcLogCount; + ++g_unknownRpcLogCount; } - - resultPtr = recvBufAddr; return true; } - } } } diff --git a/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp b/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp new file mode 100644 index 0000000..9dae347 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_iop_sdrdrv.cpp @@ -0,0 +1,325 @@ +#include "runtime/ps2_iop_sdrdrv.h" +#include "runtime/ps2_iop.h" +#include "runtime/ps2_memory.h" +#include "ps2_runtime.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ + constexpr uint32_t kImgHeaderAddr = 0x012F0000u; + constexpr uint32_t kSectorSize = 2048u; + constexpr uint32_t kStatusOffset = 0x6Cu; + constexpr uint32_t kStatusStride = 8u; + + uint32_t g_headerWarnCount = 0u; + uint32_t g_bodyWarnCount = 0u; + + bool readGuestU32(const uint8_t *rdram, uint32_t addr, uint32_t &out) + { + const uint8_t *ptr = getConstMemPtr(rdram, addr); + if (!ptr) + { + return false; + } + + std::memcpy(&out, ptr, sizeof(out)); + return true; + } + + bool tryExistingFile(const std::filesystem::path &path, std::filesystem::path &out) + { + std::error_code ec; + if (!path.empty() && std::filesystem::is_regular_file(path, ec) && !ec) + { + out = path; + return true; + } + return false; + } + + bool findSiblingFile(const char *lowerName, const char *upperName, std::filesystem::path &out) + { + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + const std::array roots = { + paths.cdRoot, + paths.elfDirectory, + }; + + for (const std::filesystem::path &root : roots) + { + if (tryExistingFile(root / lowerName, out) || + tryExistingFile(root / upperName, out)) + { + return true; + } + } + + return false; + } + + bool readHostRange(const std::filesystem::path &path, uint64_t offsetBytes, uint8_t *dst, size_t byteCount) + { + if (!dst) + { + return false; + } + if (byteCount == 0) + { + return true; + } + + std::memset(dst, 0, byteCount); + + std::ifstream file(path, std::ios::binary); + if (!file.is_open()) + { + return false; + } + + file.seekg(static_cast(offsetBytes), std::ios::beg); + if (!file.good()) + { + return false; + } + + file.read(reinterpret_cast(dst), static_cast(byteCount)); + return true; + } + + size_t clampGuestRdramBytes(uint32_t addr, uint32_t requested) + { + uint32_t offset = 0u; + bool scratch = false; + if (!ps2ResolveGuestPointer(addr, offset, scratch) || scratch || offset >= PS2_RAM_SIZE) + { + return 0u; + } + + return static_cast( + std::min(requested, static_cast(PS2_RAM_SIZE - offset))); + } + + bool loadImageHeader(uint8_t *rdram) + { + std::filesystem::path headerPath; + if (!findSiblingFile("img_hd.bin", "IMG_HD.BIN", headerPath)) + { + return false; + } + + std::error_code ec; + const uint64_t fileBytes = static_cast(std::filesystem::file_size(headerPath, ec)); + if (ec) + { + return false; + } + + uint8_t *dst = getMemPtr(rdram, kImgHeaderAddr); + if (!dst) + { + return false; + } + + const size_t bytes = static_cast(std::min(fileBytes, PS2_RAM_SIZE - (kImgHeaderAddr & PS2_RAM_MASK))); + return readHostRange(headerPath, 0u, dst, bytes); + } + + void warnImageHeaderLoadFailed() + { + if (g_headerWarnCount < 4u) + { + std::cerr << "[FatalFrame SDRDRV] failed to load img_hd.bin" << std::endl; + ++g_headerWarnCount; + } + } + + bool readBody(uint8_t *rdram, uint32_t lbn, uint32_t byteCount, uint32_t dstAddr) + { + if (byteCount == 0) + { + return true; + } + + uint32_t dstOffset = 0u; + bool scratch = false; + if (!ps2ResolveGuestPointer(dstAddr, dstOffset, scratch) || scratch || dstOffset >= PS2_RAM_SIZE) + { + return false; + } + + uint8_t *dst = getMemPtr(rdram, dstAddr); + if (!dst) + { + return false; + } + + const size_t bytes = static_cast( + std::min(byteCount, static_cast(PS2_RAM_SIZE - dstOffset))); + + std::filesystem::path bodyPath; + if (!findSiblingFile("img_bd.bin", "IMG_BD.BIN", bodyPath)) + { + bodyPath = PS2Runtime::getIoPaths().cdImage; + } + if (bodyPath.empty()) + { + return false; + } + + const uint64_t offsetBytes = static_cast(lbn) * kSectorSize; + return readHostRange(bodyPath, offsetBytes, dst, bytes); + } + + void clearRecv(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize) + { + if (recvBufAddr == 0u || recvSize == 0u) + { + return; + } + + if (uint8_t *recv = getMemPtr(rdram, recvBufAddr)) + { + const size_t bytes = clampGuestRdramBytes(recvBufAddr, recvSize); + std::memset(recv, 0, bytes); + } + } + + void markLoadComplete(uint8_t *rdram, uint32_t recvBufAddr, uint32_t recvSize, uint32_t loadId) + { + const uint32_t statusOffset = kStatusOffset + ((loadId & 0x1Fu) * kStatusStride); + if (recvBufAddr == 0u || statusOffset >= recvSize) + { + return; + } + + if (uint8_t *status = getMemPtr(rdram, recvBufAddr + statusOffset)) + { + *status = 0u; + } + } +} + +namespace ps2_iop_sdrdrv +{ + void reset() + { + g_headerWarnCount = 0u; + g_bodyWarnCount = 0u; + } + + bool handleSdrdrvRpc(uint8_t *rdram, + uint32_t sid, + uint32_t rpcNum, + uint32_t sendBufAddr, + uint32_t sendSize, + uint32_t recvBufAddr, + uint32_t recvSize, + uint32_t &resultPtr) + { + if (sid != IOP_SID_FATAL_FRAME_SDRDRV) + { + return false; + } + + resultPtr = recvBufAddr; + clearRecv(rdram, recvBufAddr, recvSize); + + if (rpcNum == 0u) + { + if (!loadImageHeader(rdram)) + { + warnImageHeaderLoadFailed(); + } + return true; + } + + if (rpcNum == 2u) + { + return true; + } + + if (rpcNum != 1u) + { + return true; + } + + constexpr uint32_t kCommandBytes = 32u; + const uint32_t commandCount = std::min(sendSize / kCommandBytes, 32u); + for (uint32_t commandIndex = 0; commandIndex < commandCount; ++commandIndex) + { + std::array words{}; + const uint32_t commandAddr = sendBufAddr + (commandIndex * kCommandBytes); + bool validCommand = true; + for (uint32_t wordIndex = 0; wordIndex < words.size(); ++wordIndex) + { + if (!readGuestU32(rdram, commandAddr + (wordIndex * sizeof(uint32_t)), words[wordIndex])) + { + validCommand = false; + break; + } + } + if (!validCommand) + { + continue; + } + + switch (words[0]) + { + case 0x0Cu: + if (!loadImageHeader(rdram)) + { + warnImageHeaderLoadFailed(); + } + break; + + case 0x0Eu: + { + const uint32_t lbn = words[2]; + const uint32_t byteCount = words[3]; + const uint32_t dstAddr = words[4]; + const uint32_t loadId = words[6]; + const bool eeLoad = words[5] == 0u; + const bool bodyLoaded = !eeLoad || readBody(rdram, lbn, byteCount, dstAddr); + + if (eeLoad && !bodyLoaded) + { + if (uint8_t *dst = getMemPtr(rdram, dstAddr)) + { + uint32_t dstOffset = 0u; + bool scratch = false; + (void)ps2ResolveGuestPointer(dstAddr, dstOffset, scratch); + if (!scratch && dstOffset < PS2_RAM_SIZE) + { + const size_t bytes = static_cast( + std::min(byteCount, static_cast(PS2_RAM_SIZE - dstOffset))); + std::memset(dst, 0, bytes); + } + } + + if (g_bodyWarnCount < 8u) + { + std::cerr << "[FatalFrame SDRDRV] failed data read lbn=0x" << std::hex << lbn + << " bytes=0x" << byteCount + << " dst=0x" << dstAddr << std::dec << std::endl; + ++g_bodyWarnCount; + } + } + + markLoadComplete(rdram, recvBufAddr, recvSize, loadId); + break; + } + + default: + break; + } + } + + return true; + } +} diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 907e6b3..8cbfd00 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -1,7 +1,7 @@ #include "runtime/ps2_memory.h" #include "ps2_log.h" #include -#include +#include #include #include #include @@ -88,6 +88,28 @@ namespace } } + constexpr uint32_t kEeTimer0Count = 0x10000000u; + constexpr uint32_t kEeTimer0Mode = 0x10000010u; + constexpr uint32_t kEeTimer0Compare = 0x10000020u; + constexpr uint32_t kEeTimer0Hold = 0x10000030u; + constexpr uint32_t kEeTimerModeCue = 1u << 7; + constexpr uint64_t kEeTimer0TicksPerSecond = 15720ull; + constexpr uint64_t kNanosecondsPerSecond = 1000000000ull; + + inline bool isEeTimer0Register(uint32_t address) + { + return address == kEeTimer0Count || + address == kEeTimer0Mode || + address == kEeTimer0Compare || + address == kEeTimer0Hold; + } + + inline uint64_t steadyClockNs() + { + using namespace std::chrono; + return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); + } + } // Helpers for GS VRAM addressing (PSMCT32 path). @@ -135,6 +157,16 @@ PS2Memory::~PS2Memory() delete[] m_vu1Data; m_vu1Data = nullptr; } + if (m_vu0Code) + { + delete[] m_vu0Code; + m_vu0Code = nullptr; + } + if (m_vu0Data) + { + delete[] m_vu0Data; + m_vu0Data = nullptr; + } if (iop_ram) { @@ -151,6 +183,8 @@ bool PS2Memory::initialize(size_t ramSize) delete[] m_scratchpad; delete[] iop_ram; delete[] m_gsVRAM; + delete[] m_vu0Code; + delete[] m_vu0Data; delete[] m_vu1Code; delete[] m_vu1Data; m_rdram = nullptr; @@ -158,6 +192,8 @@ bool PS2Memory::initialize(size_t ramSize) ps2SetScratchpadHostPtr(nullptr); iop_ram = nullptr; m_gsVRAM = nullptr; + m_vu0Code = nullptr; + m_vu0Data = nullptr; m_vu1Code = nullptr; m_vu1Data = nullptr; }; @@ -168,11 +204,17 @@ bool PS2Memory::initialize(size_t ramSize) m_gifCopyCount.store(0, std::memory_order_relaxed); m_gsWriteCount.store(0, std::memory_order_relaxed); m_vifWriteCount.store(0, std::memory_order_relaxed); + { + std::lock_guard lock(m_completedDmacMutex); + m_completedDmacCauses.clear(); + } m_codeRegions.clear(); m_path3Masked = false; m_path3MaskedFifo.clear(); m_vif1PendingPath2ImageQwc = 0u; m_vif1PendingPath2DirectHl = false; + m_timer0LastHostNs = 0; + m_timer0FractionNs = 0; try { @@ -208,6 +250,11 @@ bool PS2Memory::initialize(size_t ramSize) m_gsVRAM = new uint8_t[PS2_GS_VRAM_SIZE]; std::memset(m_gsVRAM, 0, PS2_GS_VRAM_SIZE); + m_vu0Code = new uint8_t[PS2_VU0_CODE_SIZE]; + m_vu0Data = new uint8_t[PS2_VU0_DATA_SIZE]; + std::memset(m_vu0Code, 0, PS2_VU0_CODE_SIZE); + std::memset(m_vu0Data, 0, PS2_VU0_DATA_SIZE); + m_vu1Code = new uint8_t[PS2_VU1_CODE_SIZE]; m_vu1Data = new uint8_t[PS2_VU1_DATA_SIZE]; std::memset(m_vu1Code, 0, PS2_VU1_CODE_SIZE); @@ -230,11 +277,82 @@ bool PS2Memory::initialize(size_t ramSize) } } +void PS2Memory::updateEeTimer0Counter() +{ + const uint64_t nowNs = steadyClockNs(); + if (m_timer0LastHostNs == 0u) + { + m_timer0LastHostNs = nowNs; + return; + } + + const uint32_t mode = m_ioRegisters.count(kEeTimer0Mode) ? m_ioRegisters[kEeTimer0Mode] : 0u; + if ((mode & kEeTimerModeCue) == 0u) + { + m_timer0LastHostNs = nowNs; + m_timer0FractionNs = 0u; + return; + } + + const uint64_t elapsedNs = nowNs - m_timer0LastHostNs; + m_timer0LastHostNs = nowNs; + if (elapsedNs == 0u) + { + return; + } + + const uint64_t scaled = elapsedNs * kEeTimer0TicksPerSecond + m_timer0FractionNs; + const uint64_t ticks = scaled / kNanosecondsPerSecond; + m_timer0FractionNs = scaled % kNanosecondsPerSecond; + if (ticks != 0u) + { + m_ioRegisters[kEeTimer0Count] = m_ioRegisters[kEeTimer0Count] + static_cast(ticks); + } +} + bool PS2Memory::isScratchpad(uint32_t address) const { return ps2IsScratchpadAddress(address); } +uint8_t *PS2Memory::mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) +{ + return const_cast(static_cast(this)->mapVuMemory(physAddr, size, offset, limit)); +} + +const uint8_t *PS2Memory::mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) const +{ + auto mapRange = [&](uint32_t base, uint32_t rangeSize, const uint8_t *ptr) -> const uint8_t * + { + if (!ptr || physAddr < base) + { + return nullptr; + } + const uint32_t local = physAddr - base; + if (local >= rangeSize || size > (rangeSize - local)) + { + return nullptr; + } + offset = local; + limit = rangeSize; + return ptr; + }; + + if (const uint8_t *ptr = mapRange(PS2_VU0_CODE_BASE, PS2_VU0_CODE_SIZE, m_vu0Code)) + { + return ptr; + } + if (const uint8_t *ptr = mapRange(PS2_VU0_DATA_BASE, PS2_VU0_DATA_SIZE, m_vu0Data)) + { + return ptr; + } + if (const uint8_t *ptr = mapRange(PS2_VU1_CODE_BASE, PS2_VU1_CODE_SIZE, m_vu1Code)) + { + return ptr; + } + return mapRange(PS2_VU1_DATA_BASE, PS2_VU1_DATA_SIZE, m_vu1Data); +} + uint32_t PS2Memory::translateAddress(uint32_t virtualAddress) { if (isScratchpad(virtualAddress)) @@ -352,6 +470,13 @@ uint8_t PS2Memory::read8(uint32_t address) { return m_rdram[physAddr]; } + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (const uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint8_t), vuOffset, vuLimit)) + { + (void)vuLimit; + return vuMem[vuOffset]; + } else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { uint32_t regAddr = physAddr & ~0x3; @@ -381,6 +506,12 @@ uint16_t PS2Memory::read16(uint32_t address) { return loadScalar(m_rdram, physAddr, PS2_RAM_SIZE, "read16 rdram", address); } + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (const uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint16_t), vuOffset, vuLimit)) + { + return loadScalar(vuMem, vuOffset, vuLimit, "read16 vu", address); + } else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { uint32_t regAddr = physAddr & ~0x3; @@ -420,6 +551,12 @@ uint32_t PS2Memory::read32(uint32_t address) { return loadScalar(m_rdram, physAddr, PS2_RAM_SIZE, "read32 rdram", address); } + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (const uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint32_t), vuOffset, vuLimit)) + { + return loadScalar(vuMem, vuOffset, vuLimit, "read32 vu", address); + } else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { return readIORegister(physAddr); @@ -452,6 +589,12 @@ uint64_t PS2Memory::read64(uint32_t address) { return loadScalar(m_rdram, physAddr, PS2_RAM_SIZE, "read64 rdram", address); } + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (const uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint64_t), vuOffset, vuLimit)) + { + return loadScalar(vuMem, vuOffset, vuLimit, "read64 vu", address); + } // 64-bit IO read: compose from the two adjacent 32-bit IO register slots // to avoid any side-effects from read32 handlers. @@ -484,6 +627,13 @@ __m128i PS2Memory::read128(uint32_t address) inRange(physAddr, sizeof(__m128i), PS2_RAM_SIZE, "read128 rdram", address); return _mm_loadu_si128(reinterpret_cast<__m128i *>(&m_rdram[physAddr])); } + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (const uint8_t *vuMem = mapVuMemory(physAddr, sizeof(__m128i), vuOffset, vuLimit)) + { + inRange(vuOffset, sizeof(__m128i), vuLimit, "read128 vu", address); + return _mm_loadu_si128(reinterpret_cast(vuMem + vuOffset)); + } // 128-bit reads are primarily for quad-word loads in the EE, which are only valid for RAM areas // Return zeroes for unsupported areas @@ -503,7 +653,18 @@ void PS2Memory::write8(uint32_t address, uint8_t value) { m_rdram[physAddr] = value; } - else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) + else + { + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint8_t), vuOffset, vuLimit)) + { + (void)vuLimit; + vuMem[vuOffset] = value; + return; + } + } + if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { // IO registers - handle byte writes by modifying the appropriate byte in the word uint32_t regAddr = physAddr & ~0x3; @@ -532,7 +693,17 @@ void PS2Memory::write16(uint32_t address, uint16_t value) { storeScalar(m_rdram, physAddr, PS2_RAM_SIZE, value, "write16 rdram", address); } - else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) + else + { + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint16_t), vuOffset, vuLimit)) + { + storeScalar(vuMem, vuOffset, vuLimit, value, "write16 vu", address); + return; + } + } + if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { uint32_t regAddr = physAddr & ~0x3; uint32_t shift = (physAddr & 2) * 8; @@ -591,7 +762,17 @@ void PS2Memory::write32(uint32_t address, uint32_t value) storeScalar(m_rdram, physAddr, PS2_RAM_SIZE, value, "write32 rdram", address); } - else if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) + else + { + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint32_t), vuOffset, vuLimit)) + { + storeScalar(vuMem, vuOffset, vuLimit, value, "write32 vu", address); + return; + } + } + if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { writeIORegister(physAddr, value); } @@ -639,6 +820,16 @@ void PS2Memory::write64(uint32_t address, uint64_t value) storeScalar(m_rdram, physAddr, PS2_RAM_SIZE, value, "write64 rdram", address); } else + { + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint64_t), vuOffset, vuLimit)) + { + storeScalar(vuMem, vuOffset, vuLimit, value, "write64 vu", address); + return; + } + } + if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { write32(address, (uint32_t)value); write32(address + 4, (uint32_t)(value >> 32)); @@ -667,6 +858,17 @@ void PS2Memory::write128(uint32_t address, __m128i value) _mm_storeu_si128(reinterpret_cast<__m128i *>(&m_rdram[physAddr]), value); } else + { + uint32_t vuOffset = 0; + uint32_t vuLimit = 0; + if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(__m128i), vuOffset, vuLimit)) + { + inRange(vuOffset, sizeof(__m128i), vuLimit, "write128 vu", address); + _mm_storeu_si128(reinterpret_cast<__m128i *>(vuMem + vuOffset), value); + return; + } + } + if (physAddr >= PS2_IO_BASE && physAddr < PS2_IO_BASE + PS2_IO_SIZE) { // Non-RAM 128-bit stores are modeled as two 64-bit stores. uint64_t lo = _mm_extract_epi64(value, 0); @@ -679,6 +881,26 @@ void PS2Memory::write128(uint32_t address, __m128i value) bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) { + if (isEeTimer0Register(address)) + { + if (address == kEeTimer0Count) + { + m_ioRegisters[address] = value; + m_timer0LastHostNs = steadyClockNs(); + m_timer0FractionNs = 0u; + return true; + } + + updateEeTimer0Counter(); + m_ioRegisters[address] = value; + m_timer0LastHostNs = steadyClockNs(); + if (address == kEeTimer0Mode) + { + m_timer0FractionNs = 0u; + } + return true; + } + if (isGsPrivReg(address)) { m_ioRegisters[address] = value; @@ -829,7 +1051,8 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) const uint32_t qwc = m_ioRegisters[channelBase + 0x20]; m_dmaStartCount.fetch_add(1, std::memory_order_relaxed); - if ((channelBase == 0x1000A000 || channelBase == 0x10009000) && m_gsVRAM) + if ((channelBase == 0x1000A000u || channelBase == 0x10009000u || channelBase == 0x10008000u) && + (m_gsVRAM || channelBase == 0x10008000u)) { auto enqueueTransfer = [&](uint32_t srcAddr, uint32_t qwCount) { @@ -840,10 +1063,12 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) pt.fromScratchpad = scratch; pt.srcAddr = srcAddr; pt.qwc = qwCount; - if (channelBase == 0x1000A000) + if (channelBase == 0x1000A000u) m_pendingGifTransfers.push_back(pt); - else if (channelBase == 0x10009000) + else if (channelBase == 0x10009000u) m_pendingVif1Transfers.push_back(pt); + else if (channelBase == 0x10008000u) + m_pendingVif0Transfers.push_back(pt); }; uint32_t chcr = value; @@ -868,8 +1093,8 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) const uint64_t bytes64 = static_cast(qwCount) * 16ull; uint32_t bytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast(bytes64); const bool scratch = isScratchpad(srcAddr); - uint32_t src = 0; - src = translateAddress(srcAddr); + uint32_t src = 0; + src = translateAddress(srcAddr); const uint8_t *base2; uint32_t maxSz2; if (scratch) @@ -882,6 +1107,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) base2 = m_rdram; maxSz2 = PS2_RAM_SIZE; } + while (bytes > 0) { if (src >= maxSz2) @@ -908,12 +1134,13 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) if (tagPhys + 16u > localMax) return; - // VIF1 packet helpers embed 8 bytes of VIF stream in the DMAtag's upper half. + // VIF packet helpers embed 8 bytes of VIF stream in the DMAtag's upper half. chainBuf.insert(chainBuf.end(), localBase + tagPhys + 8u, localBase + tagPhys + 16u); appendData(localTagAddr + 16u, qwCount); }; int tagsProcessed = 0; + uint32_t lastTagUpper = (chcr >> 16) & 0xFFFFu; while (tagsProcessed < kMaxChainTags) { @@ -949,6 +1176,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) uint32_t id = static_cast((tag >> 28) & 0x7); const bool irq = ((tag >> 31) & 0x1ull) != 0ull; uint32_t addr = static_cast((tag >> 32) & 0x7FFFFFFF); + lastTagUpper = static_cast((tag >> 16) & 0xFFFFu); ++tagsProcessed; uint32_t dataAddr = 0; @@ -1019,13 +1247,16 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) break; } + const bool compactVifLocalTag = + (channelBase == 0x10009000u || channelBase == 0x10008000u) && + (id == 1u || id == 2u || id == 5u || id == 6u || id == 7u); + if (compactVifLocalTag) + appendCompactVif1TagData(currentTagAddr, 0u); + if (hasPayload) { - const bool compactVif1LocalPayload = - (channelBase == 0x10009000u) && - (id == 1u || id == 2u || id == 5u || id == 6u || id == 7u); - if (compactVif1LocalPayload) - appendCompactVif1TagData(currentTagAddr, tagQwc); + if (compactVifLocalTag) + appendData(currentTagAddr + 16u, tagQwc); else appendData(dataAddr, tagQwc); } @@ -1039,6 +1270,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) m_ioRegisters[channelBase + 0x40] = asr0; m_ioRegisters[channelBase + 0x50] = asr1; chcr = (chcr & ~(0x3u << 4)) | ((asp & 0x3u) << 4); + chcr = (chcr & 0x0000FFFFu) | (lastTagUpper << 16); m_ioRegisters[channelBase + 0x00] = chcr; if (!chainBuf.empty()) @@ -1052,10 +1284,14 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) { m_pendingGifTransfers.push_back(std::move(pt)); } - else if (channelBase == 0x10009000) + else if (channelBase == 0x10009000u) { m_pendingVif1Transfers.push_back(std::move(pt)); } + else if (channelBase == 0x10008000u) + { + m_pendingVif0Transfers.push_back(std::move(pt)); + } } // else if (channelBase == 0x10009000u) // { @@ -1160,6 +1396,64 @@ void PS2Memory::processPendingTransfers() } m_pendingGifTransfers.clear(); + const bool hadVif0 = !m_pendingVif0Transfers.empty(); + for (auto &p : m_pendingVif0Transfers) + { + if (!p.chainData.empty()) + { + processVIF0Data(p.chainData.data(), static_cast(p.chainData.size())); + } + else if (p.qwc > 0) + { + uint32_t srcPhys = 0; + const uint64_t bytes64 = static_cast(p.qwc) * 16ull; + uint32_t sizeBytes = (bytes64 > 0xFFFFFFFFull) ? 0xFFFFFFFFu : static_cast(bytes64); + try + { + srcPhys = translateAddress(p.srcAddr); + } + catch (const std::exception &) + { + continue; + } + if (p.fromScratchpad) + { + uint32_t bytesLeft = sizeBytes; + while (bytesLeft > 0) + { + if (srcPhys >= PS2_SCRATCHPAD_SIZE) + srcPhys = 0; + uint32_t chunk = bytesLeft; + if (srcPhys + chunk > PS2_SCRATCHPAD_SIZE) + chunk = PS2_SCRATCHPAD_SIZE - srcPhys; + if (chunk == 0) + break; + processVIF0Data(m_scratchpad + srcPhys, chunk); + bytesLeft -= chunk; + srcPhys += chunk; + } + } + else + { + uint32_t bytesLeft = sizeBytes; + while (bytesLeft > 0) + { + if (srcPhys >= PS2_RAM_SIZE) + srcPhys = 0; + uint32_t chunk = bytesLeft; + if (srcPhys + chunk > PS2_RAM_SIZE) + chunk = PS2_RAM_SIZE - srcPhys; + if (chunk == 0) + break; + processVIF0Data(srcPhys, chunk); + bytesLeft -= chunk; + srcPhys += chunk; + } + } + } + } + m_pendingVif0Transfers.clear(); + const bool hadVif1 = !m_pendingVif1Transfers.empty(); for (auto &p : m_pendingVif1Transfers) { @@ -1222,6 +1516,7 @@ void PS2Memory::processPendingTransfers() m_gifArbiter->drain(); static constexpr uint32_t GIF_CHANNEL = 0x1000A000; + static constexpr uint32_t VIF0_CHANNEL = 0x10008000; static constexpr uint32_t VIF1_CHANNEL = 0x10009000; static constexpr uint32_t D_STAT = 0x1000E010u; @@ -1243,17 +1538,40 @@ void PS2Memory::processPendingTransfers() if (hadGif) { raiseDStatChannel(2u); // GIF channel + queueCompletedDmacCause(2u); m_ioRegisters[GIF_CHANNEL + 0x00] &= ~0x100u; m_ioRegisters[GIF_CHANNEL + 0x20] = 0; } + if (hadVif0) + { + raiseDStatChannel(0u); // VIF0 channel + queueCompletedDmacCause(0u); + m_ioRegisters[VIF0_CHANNEL + 0x00] &= ~0x100u; + m_ioRegisters[VIF0_CHANNEL + 0x20] = 0; + } if (hadVif1) { raiseDStatChannel(1u); // VIF1 channel + queueCompletedDmacCause(1u); m_ioRegisters[VIF1_CHANNEL + 0x00] &= ~0x100u; m_ioRegisters[VIF1_CHANNEL + 0x20] = 0; } } +void PS2Memory::queueCompletedDmacCause(uint32_t cause) +{ + std::lock_guard lock(m_completedDmacMutex); + m_completedDmacCauses.push_back(cause); +} + +std::vector PS2Memory::consumeCompletedDmacCauses() +{ + std::lock_guard lock(m_completedDmacMutex); + std::vector causes; + causes.swap(m_completedDmacCauses); + return causes; +} + void PS2Memory::flushMaskedPath3Packets(bool drainImmediately) { if (m_path3Masked || m_path3MaskedFifo.empty()) @@ -1378,9 +1696,14 @@ uint32_t PS2Memory::readIORegister(uint32_t address) { if (address >= 0x10000000 && address < 0x10000100) { - if ((address & 0xF) == 0x00) + if (isEeTimer0Register(address)) { - return 0; + if (address == kEeTimer0Count) + { + updateEeTimer0Counter(); + } + auto timerIt = m_ioRegisters.find(address); + return timerIt != m_ioRegisters.end() ? timerIt->second : 0u; } } diff --git a/ps2xRuntime/src/lib/ps2_pad.cpp b/ps2xRuntime/src/lib/ps2_pad.cpp index 10e3baf..8590f14 100644 --- a/ps2xRuntime/src/lib/ps2_pad.cpp +++ b/ps2xRuntime/src/lib/ps2_pad.cpp @@ -97,13 +97,13 @@ bool PSPadBackend::readState(int /*port*/, int /*slot*/, uint8_t *data, size_t s clearBit(PAD_LEFT); if (IsKeyDown(KEY_RIGHT) || IsKeyDown(KEY_D)) clearBit(PAD_RIGHT); - if (IsKeyDown(KEY_ENTER) || IsKeyDown(KEY_SPACE)) + if (IsKeyDown(KEY_X) || IsKeyDown(KEY_SPACE)) clearBit(PAD_CROSS); - if (IsKeyDown(KEY_ESCAPE)) + if (IsKeyDown(KEY_C) || IsKeyDown(KEY_ESCAPE)) clearBit(PAD_CIRCLE); - if (IsKeyDown(KEY_KP_0) || IsKeyDown(KEY_Z)) + if (IsKeyDown(KEY_Z) || IsKeyDown(KEY_KP_0)) clearBit(PAD_SQUARE); - if (IsKeyDown(KEY_KP_1) || IsKeyDown(KEY_X)) + if (IsKeyDown(KEY_V) || IsKeyDown(KEY_KP_1)) clearBit(PAD_TRIANGLE); if (IsKeyDown(KEY_Q)) clearBit(PAD_L1); diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 06ecc57..54e579e 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -97,45 +97,6 @@ namespace constexpr uint32_t EXCEPTION_VECTOR_TLB_REFILL = 0x80000000u; constexpr uint32_t EXCEPTION_VECTOR_BOOT = 0xBFC00200u; - struct HostFrameProbePoint - { - uint32_t x; - uint32_t y; - }; - - constexpr HostFrameProbePoint kGhostProbePoints[] = { - {220u, 176u}, - {260u, 208u}, - {320u, 208u}, - {260u, 240u}, - {320u, 240u}, - {260u, 272u}, - {320u, 272u}, - }; - - uint32_t sampleHostFramePixel(const std::vector &pixels, - uint32_t width, - uint32_t height, - uint32_t x, - uint32_t y) - { - if (x >= width || y >= height) - { - return 0u; - } - - const size_t offset = (static_cast(y) * static_cast(width) + static_cast(x)) * 4u; - if (offset + 4u > pixels.size()) - { - return 0u; - } - - return static_cast(pixels[offset + 0u]) | - (static_cast(pixels[offset + 1u]) << 8) | - (static_cast(pixels[offset + 2u]) << 16) | - (static_cast(pixels[offset + 3u]) << 24); - } - struct DispatchHistory { std::array pcs{}; @@ -145,6 +106,8 @@ namespace thread_local DispatchHistory g_dispatchHistory; thread_local std::unordered_map g_guestExecutionDepths; + std::mutex g_functionStartsMutex; + std::unordered_map> g_functionStartsByRuntime; void pushDispatchPc(uint32_t pc) { @@ -225,6 +188,82 @@ namespace return tlbRefill ? EXCEPTION_VECTOR_TLB_REFILL : EXCEPTION_VECTOR_GENERAL; } + void seedVu0IdleSuccess(R5900Context *ctx) + { + if (!ctx) + { + return; + } + + ctx->vu0_clip_flags = 0; + ctx->vu0_clip_flags2 = 0; + ctx->vu0_mac_flags = 0; + ctx->vu0_status = 0; + ctx->vu0_q = 1.0f; + ctx->vu0_vpu_stat = 0; + ctx->vu0_vpu_stat2 = 0; + } + + + void copyVu0ContextToState(const R5900Context *ctx, VU1State &state) + { + std::memset(&state, 0, sizeof(state)); + + for (uint32_t i = 0; i < 32u; ++i) + { + _mm_storeu_ps(state.vf[i], ctx->vu0_vf[i]); + } + for (uint32_t i = 0; i < 16u; ++i) + { + state.vi[i] = static_cast(ctx->vi[i]); + } + + _mm_storeu_ps(state.acc, ctx->vu0_acc); + state.q = ctx->vu0_q; + state.p = ctx->vu0_p; + state.i = ctx->vu0_i; + state.pc = ctx->vu0_pc; + state.mac = ctx->vu0_mac_flags; + state.clip = ctx->vu0_clip_flags; + state.status = ctx->vu0_status; + state.itop = ctx->vu0_itop; + + state.vf[0][0] = 0.0f; + state.vf[0][1] = 0.0f; + state.vf[0][2] = 0.0f; + state.vf[0][3] = 1.0f; + state.vi[0] = 0; + } + + void copyVu0StateToContext(const VU1State &state, R5900Context *ctx) + { + for (uint32_t i = 0; i < 32u; ++i) + { + ctx->vu0_vf[i] = _mm_loadu_ps(state.vf[i]); + } + for (uint32_t i = 0; i < 16u; ++i) + { + ctx->vi[i] = static_cast(state.vi[i]); + } + + ctx->vu0_acc = _mm_loadu_ps(state.acc); + ctx->vu0_q = state.q; + ctx->vu0_p = state.p; + ctx->vu0_i = state.i; + ctx->vu0_mac_flags = state.mac; + ctx->vu0_clip_flags = state.clip; + ctx->vu0_clip_flags2 = state.clip; + ctx->vu0_status = static_cast(state.status); + ctx->vu0_itop = state.itop; + ctx->vu0_pc = state.pc; + ctx->vu0_tpc = state.pc; + ctx->vu0_vpu_stat = 0; + ctx->vu0_vpu_stat2 = 0; + + ctx->vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f); + ctx->vi[0] = 0; + } + void raiseCop0Exception(R5900Context *ctx, uint32_t exceptionCode, bool tlbRefill = false) { if (ctx->in_delay_slot) @@ -357,6 +396,34 @@ namespace return 0u; } + void clearFunctionStarts(const PS2Runtime *runtime) + { + std::lock_guard lock(g_functionStartsMutex); + g_functionStartsByRuntime.erase(runtime); + } + + std::vector snapshotFunctionStarts(const PS2Runtime *runtime) + { + std::lock_guard lock(g_functionStartsMutex); + auto it = g_functionStartsByRuntime.find(runtime); + if (it == g_functionStartsByRuntime.end()) + { + return {}; + } + return it->second; + } + + void registerFunctionStart(const PS2Runtime *runtime, uint32_t address) + { + std::lock_guard lock(g_functionStartsMutex); + auto &starts = g_functionStartsByRuntime[runtime]; + auto insertPos = std::lower_bound(starts.begin(), starts.end(), address); + if (insertPos == starts.end() || *insertPos != address) + { + starts.insert(insertPos, address); + } + } + std::string readGuestPrintableString(const uint8_t *rdram, uint32_t addr, size_t maxLen) { std::string out; @@ -429,22 +496,39 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint static bool s_lastPreferred = false; static uint32_t s_lastWidth = 0u; static uint32_t s_lastHeight = 0u; + static bool s_hasUploadedFrame = false; + static std::vector s_scratch; + static std::vector s_uploadBuffer(DEFAULT_FB_SIZE, 0u); const uint64_t currentTick = ps2_syscalls::GetCurrentVSyncTick(); - if (!s_hasLatchedInitialFrame || currentTick != s_lastPresentationTick) + bool latchedThisCall = false; + if (!s_hasLatchedInitialFrame) { rt->gs().latchHostPresentationFrame(); s_lastPresentationTick = currentTick; s_hasLatchedInitialFrame = true; + latchedThisCall = true; + } + else if (currentTick != s_lastPresentationTick && rt->gs().tryLatchHostPresentationFrame()) + { + s_lastPresentationTick = currentTick; + latchedThisCall = true; } - std::vector scratch; + if (!latchedThisCall && s_hasUploadedFrame) + { + outWidth = (s_lastWidth != 0u) ? s_lastWidth : FB_WIDTH; + outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT; + return; + } + + s_scratch.clear(); uint32_t width = 0u; uint32_t height = 0u; uint32_t displayFbp = 0u; uint32_t sourceFbp = 0u; bool usedPreferredDisplaySource = false; - if (!rt->gs().copyLatchedHostPresentationFrame(scratch, + if (!rt->gs().copyLatchedHostPresentationFrame(s_scratch, width, height, &displayFbp, @@ -456,6 +540,9 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint UnloadImage(blank); outWidth = FB_WIDTH; outHeight = DEFAULT_DISPLAY_HEIGHT; + s_lastWidth = outWidth; + s_lastHeight = outHeight; + s_hasUploadedFrame = true; return; } @@ -476,31 +563,6 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint << " preferred=" << static_cast(usedPreferredDisplaySource ? 1u : 0u) << std::endl; } - static uint32_t s_probeDebugCount = 0u; - if (s_probeDebugCount < 32u || - displayFbp != s_lastDisplayFbp || - sourceFbp != s_lastSourceFbp || - usedPreferredDisplaySource != s_lastPreferred) - { - std::cout << "[frame:probe] idx=" << s_probeDebugCount - << " tick=" << currentTick - << " displayFbp=" << displayFbp - << " sourceFbp=" << sourceFbp - << " preferred=" << static_cast(usedPreferredDisplaySource ? 1u : 0u); - for (const auto &probe : kGhostProbePoints) - { - if (probe.x >= width || probe.y >= height) - { - continue; - } - - const uint32_t pixel = sampleHostFramePixel(scratch, width, height, probe.x, probe.y); - std::cout << " host[" << probe.x << "," << probe.y << "]=0x" - << std::hex << pixel << std::dec; - } - std::cout << std::endl; - ++s_probeDebugCount; - } ++s_uploadDebugCount; }); s_lastDisplayFbp = displayFbp; @@ -509,8 +571,8 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint s_lastWidth = width; s_lastHeight = height; - std::vector uploadBuffer(DEFAULT_FB_SIZE, 0u); - if (!scratch.empty() && width != 0u && height != 0u) + std::fill(s_uploadBuffer.begin(), s_uploadBuffer.end(), 0u); + if (!s_scratch.empty() && width != 0u && height != 0u) { const uint32_t copyWidth = std::min(width, FB_WIDTH); const uint32_t copyHeight = std::min(height, FB_HEIGHT); @@ -521,18 +583,19 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint { const size_t srcOffset = static_cast(y) * srcRowBytes; const size_t dstOffset = static_cast(y) * dstRowBytes; - if (srcOffset + copyRowBytes > scratch.size() || - dstOffset + copyRowBytes > uploadBuffer.size()) + if (srcOffset + copyRowBytes > s_scratch.size() || + dstOffset + copyRowBytes > s_uploadBuffer.size()) { break; } - std::memcpy(uploadBuffer.data() + dstOffset, scratch.data() + srcOffset, copyRowBytes); + std::memcpy(s_uploadBuffer.data() + dstOffset, s_scratch.data() + srcOffset, copyRowBytes); } } - UpdateTexture(tex, uploadBuffer.data()); + UpdateTexture(tex, s_uploadBuffer.data()); outWidth = width; outHeight = height; + s_hasUploadedFrame = true; } PS2Runtime::PS2Runtime() @@ -545,6 +608,7 @@ PS2Runtime::PS2Runtime() // Stack pointer (SP) and global pointer (GP) will be set by the loaded ELF m_functionTable.clear(); + clearFunctionStarts(this); m_loadedModules.clear(); m_guestHeapBlocks.clear(); @@ -557,6 +621,23 @@ PS2Runtime::PS2Runtime() m_asyncCallbackStackTop = PS2_RAM_SIZE; } +void PS2Runtime::setDebugUiCallbacks(DebugUiCallback initCallback, + DebugUiCallback drawCallback, + DebugUiCallback shutdownCallback, + void *userData) +{ + if (m_debugUiInitialized && m_debugUiShutdownCallback) + { + m_debugUiShutdownCallback(*this, m_debugUiUserData); + m_debugUiInitialized = false; + } + + m_debugUiInitCallback = initCallback; + m_debugUiDrawCallback = drawCallback; + m_debugUiShutdownCallback = shutdownCallback; + m_debugUiUserData = userData; +} + PS2Runtime::~PS2Runtime() { try @@ -573,6 +654,12 @@ PS2Runtime::~PS2Runtime() m_audioBackend.setAudioReady(false); } #endif + if (m_debugUiInitialized && m_debugUiShutdownCallback) + { + m_debugUiShutdownCallback(*this, m_debugUiUserData); + m_debugUiInitialized = false; + } + if (IsWindowReady()) { CloseWindow(); @@ -581,6 +668,7 @@ PS2Runtime::~PS2Runtime() m_loadedModules.clear(); m_functionTable.clear(); + clearFunctionStarts(this); } catch (const std::exception &e) { @@ -610,16 +698,17 @@ bool PS2Runtime::syncCoreSubsystems() m_gifArbiter.setProcessPacketFn([this](const uint8_t *data, uint32_t size) { m_gs.processGIFPacket(data, size); }); m_memory.setGifArbiter(&m_gifArbiter); - m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t itop) + m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop) { m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, - m_gs, &m_memory, startPC, itop, 65536); }); - m_memory.setVu1MscntCallback([this](uint32_t itop) + m_gs, &m_memory, startPC, top, itop, 65536); }); + m_memory.setVu1MscntCallback([this](uint32_t top, uint32_t itop) { m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, - m_gs, &m_memory, itop, 65536); }); + m_gs, &m_memory, top, itop, 65536); }); m_iop.init(rdram); m_iop.reset(); + m_vu0.reset(); m_vu1.reset(); m_boundRdram = rdram; @@ -652,6 +741,11 @@ bool PS2Runtime::initialize(const char *title) m_audioBackend.setAudioReady(IsAudioDeviceReady()); #endif SetTargetFPS(60); + if (m_debugUiInitCallback) + { + m_debugUiInitCallback(*this, m_debugUiUserData); + m_debugUiInitialized = true; + } return true; } @@ -957,6 +1051,7 @@ void PS2Runtime::configureIoPathsFromElf(const std::string &elfPath) void PS2Runtime::registerFunction(uint32_t address, RecompiledFunction func) { + registerFunctionStart(this, address); m_functionTable[address] = func; } @@ -981,7 +1076,89 @@ PS2Runtime::RecompiledFunction PS2Runtime::lookupFunction(uint32_t address) return it->second; } - std::cerr << "Warning: Function at address 0x" << std::hex << address << std::dec << " not found" << std::endl; + const std::vector functionStarts = snapshotFunctionStarts(this); + auto aliasOwner = [&](uint32_t ownerAddress) -> RecompiledFunction + { + auto owner = m_functionTable.find(ownerAddress); + if (owner == m_functionTable.end()) + { + return nullptr; + } + + auto ownerStart = std::lower_bound(functionStarts.begin(), functionStarts.end(), ownerAddress); + if (ownerStart == functionStarts.end() || *ownerStart != ownerAddress) + { + return nullptr; + } + + auto nextStart = ownerStart; + ++nextStart; + if (nextStart != functionStarts.end()) + { + if (address >= *nextStart) + { + return nullptr; + } + } + else if (!m_memory.isCodeAddress(address)) + { + return nullptr; + } + + return owner->second; + }; + + if (!functionStarts.empty()) + { + const DispatchHistory &history = g_dispatchHistory; + const uint32_t count = history.wrapped ? static_cast(history.pcs.size()) : history.next; + for (uint32_t step = 1u; step <= count; ++step) + { + const uint32_t idx = (history.next + static_cast(history.pcs.size()) - step) % + static_cast(history.pcs.size()); + const uint32_t previousPc = history.pcs[idx]; + if (previousPc == address) + { + continue; + } + if (RecompiledFunction owner = aliasOwner(previousPc)) + { + return owner; + } + } + + auto nextStart = std::upper_bound(functionStarts.begin(), functionStarts.end(), address); + if (nextStart != functionStarts.begin()) + { + auto ownerStart = nextStart; + --ownerStart; + if (RecompiledFunction owner = aliasOwner(*ownerStart)) + { + return owner; + } + } + } + + std::cerr << "Warning: Function at address 0x" << std::hex << address; + if (!functionStarts.empty()) + { + auto nextStart = std::upper_bound(functionStarts.begin(), functionStarts.end(), address); + if (nextStart != functionStarts.begin()) + { + auto ownerStart = nextStart; + --ownerStart; + std::cerr << " nearestStart=0x" << *ownerStart; + } + if (nextStart != functionStarts.end()) + { + std::cerr << " nextStart=0x" << *nextStart; + } + else + { + std::cerr << " nextStart="; + } + } + std::cerr << std::dec << " not found" << std::endl; static RecompiledFunction defaultFunction = [](uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { @@ -1125,23 +1302,25 @@ void PS2Runtime::SignalException(R5900Context *ctx, PS2Exception exception) void PS2Runtime::executeVU0Microprogram(uint8_t *rdram, R5900Context *ctx, uint32_t address) { - static std::unordered_map seen; - int &count = seen[address]; - if (count < 3) - { - RUNTIME_LOG("[VU0] microprogram @0x" << std::hex << address - << " pc=0x" << ctx->pc - << " ra=0x" << static_cast(_mm_extract_epi32(ctx->r[31], 0)) - << std::dec << std::endl); - } - ++count; + (void)rdram; - // Seed status so dependent code sees success. - ctx->vu0_clip_flags = 0; - ctx->vu0_clip_flags2 = 0; - ctx->vu0_mac_flags = 0; - ctx->vu0_status = 0; - ctx->vu0_q = 1.0f; + uint8_t *const vu0Code = m_memory.getVU0Code(); + uint8_t *const vu0Data = m_memory.getVU0Data(); + const uint32_t startPC = address & ~0x7u; + + if (!vu0Code || !vu0Data || startPC + 8u > PS2_VU0_CODE_SIZE) + { + seedVu0IdleSuccess(ctx); + return; + } + + m_vu0.reset(); + copyVu0ContextToState(ctx, m_vu0.state()); + m_vu0.execute(vu0Code, PS2_VU0_CODE_SIZE, + vu0Data, PS2_VU0_DATA_SIZE, + m_gs, &m_memory, + startPC, 0u, ctx->vu0_itop, 4096); + copyVu0StateToContext(m_vu0.state(), ctx); } void PS2Runtime::vu0StartMicroProgram(uint8_t *rdram, R5900Context *ctx, uint32_t address) @@ -1181,6 +1360,14 @@ void PS2Runtime::handleBreak(uint8_t *rdram, R5900Context *ctx) raiseCop0Exception(ctx, EXCEPTION_BREAKPOINT); } +void PS2Runtime::drainCompletedDmacHandlers(uint8_t *rdram) +{ + for (uint32_t cause : m_memory.consumeCompletedDmacCauses()) + { + ps2_syscalls::dispatchDmacHandlersForCause(rdram, this, cause); + } +} + void PS2Runtime::handleTrap(uint8_t *rdram, R5900Context *ctx) { raiseCop0Exception(ctx, EXCEPTION_TRAP); @@ -1749,13 +1936,15 @@ void PS2Runtime::dispatchLoop(uint8_t *rdram, R5900Context *ctx) const uint32_t ra = static_cast(_mm_extract_epi32(ctx->r[31], 0)); const uint32_t sp = static_cast(_mm_extract_epi32(ctx->r[29], 0)); const uint32_t gp = static_cast(_mm_extract_epi32(ctx->r[28], 0)); - std::cerr << "[dispatch:pc-zero] from=0x" << std::hex << dispatchedPc - << " fromRa=0x" << dispatchedRa - << " ra=0x" << ra - << " sp=0x" << sp - << " gp=0x" << gp - << " trace=" << formatDispatchHistory() - << std::dec << std::endl; + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[dispatch:pc-zero] from=0x" << std::hex << dispatchedPc + << " fromRa=0x" << dispatchedRa + << " ra=0x" << ra + << " sp=0x" << sp + << " gp=0x" << gp + << " trace=" << formatDispatchHistory() + << std::dec << std::endl; + }); // PC=0 means this guest thread returned (usually via jr $ra with RA=0). // Do not request a global runtime stop here: other guest threads may still run. @@ -1770,6 +1959,7 @@ void PS2Runtime::enterGuestExecution() m_guestExecutionMutex.lock(); m_guestExecutionWaiters.fetch_sub(1u, std::memory_order_acq_rel); ++g_guestExecutionDepths[this]; + markGuestExecutionAcquired(); } void PS2Runtime::leaveGuestExecution() @@ -1819,6 +2009,36 @@ void PS2Runtime::reacquireGuestExecution(uint32_t depth) m_guestExecutionMutex.lock(); m_guestExecutionWaiters.fetch_sub(1u, std::memory_order_acq_rel); ++heldDepth; + markGuestExecutionAcquired(); + } +} + +void PS2Runtime::markGuestExecutionAcquired() +{ + { + std::lock_guard lock(m_guestExecutionHandoffMutex); + m_guestExecutionHandoffEpoch.fetch_add(1u, std::memory_order_acq_rel); + } + m_guestExecutionHandoffCv.notify_all(); +} + +void PS2Runtime::yieldGuestExecutionAfterWake() +{ + auto it = g_guestExecutionDepths.find(this); + if (it == g_guestExecutionDepths.end() || it->second == 0u) + { + std::this_thread::yield(); + return; + } + + const uint64_t handoffEpoch = m_guestExecutionHandoffEpoch.load(std::memory_order_acquire); + { + GuestExecutionReleaseScope releaseGuestExecution(this); + std::unique_lock lock(m_guestExecutionHandoffMutex); + m_guestExecutionHandoffCv.wait_for(lock, std::chrono::milliseconds(2), [&]() + { + return m_guestExecutionHandoffEpoch.load(std::memory_order_acquire) != handoffEpoch; + }); } } @@ -1933,6 +2153,7 @@ void PS2Runtime::Store32(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr, uint try { m_memory.write32(vaddr, value); + drainCompletedDmacHandlers(rdram); } catch (const std::exception &) { @@ -2052,7 +2273,7 @@ void PS2Runtime::run() const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed); const int activeThreads = g_activeThreads.load(std::memory_order_relaxed); - std::cout << "[run:tick] tick=" << tick + RUNTIME_LOG("[run:tick] tick=" << tick << " pc=0x" << std::hex << dbgPc << " ra=0x" << dbgRa << " sp=0x" << dbgSp @@ -2065,7 +2286,7 @@ void PS2Runtime::run() << " gif=" << curGif << " gsw=" << curGs << " vif=" << curVif - << std::endl; + << std::endl); } }); uint32_t presentWidth = FB_WIDTH; @@ -2088,6 +2309,10 @@ void PS2Runtime::run() dstWidth, dstHeight}; DrawTexturePro(frameTex, srcRect, dstRect, Vector2{0.0f, 0.0f}, 0.0f, WHITE); + if (m_debugUiInitialized && m_debugUiDrawCallback) + { + m_debugUiDrawCallback(*this, m_debugUiUserData); + } EndDrawing(); if (WindowShouldClose()) @@ -2149,6 +2374,11 @@ void PS2Runtime::run() ps2_syscalls::detachAllGuestHostThreads(); } + if (m_debugUiInitialized && m_debugUiShutdownCallback) + { + m_debugUiShutdownCallback(*this, m_debugUiUserData); + m_debugUiInitialized = false; + } UnloadTexture(frameTex); CloseWindow(); diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index e478fa4..5fefa7f 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -1,8 +1,6 @@ // Based on Blackline Interactive implementation #include "runtime/ps2_memory.h" -#include #include -#include "ps2_log.h" enum VIFCmd : uint8_t { @@ -30,8 +28,6 @@ enum VIFCmd : uint8_t namespace { - std::atomic s_debugVu1KickCount{0}; - std::atomic s_debugVif1OpcodeCount{0}; constexpr uint8_t kGifFmtImage = 2u; uint32_t gifImageQwcFromTag(const uint8_t *data, uint32_t sizeBytes) @@ -49,11 +45,208 @@ namespace } } +void PS2Memory::processVIF0Data(uint32_t srcPhys, uint32_t sizeBytes) +{ + if (sizeBytes == 0u || srcPhys >= PS2_RAM_SIZE) + return; + + const uint64_t requestedEnd = static_cast(srcPhys) + static_cast(sizeBytes); + if (requestedEnd > static_cast(PS2_RAM_SIZE)) + sizeBytes = PS2_RAM_SIZE - srcPhys; + + processVIF0Data(m_rdram + srcPhys, sizeBytes); +} + +void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes) +{ + if (sizeBytes == 0u) + return; + + uint32_t pos = 0; + while (pos + 4 <= sizeBytes) + { + uint32_t cmd = 0u; + std::memcpy(&cmd, data + pos, sizeof(cmd)); + pos += 4u; + + const uint8_t opcode = static_cast((cmd >> 24) & 0x7Fu); + const uint16_t imm = static_cast(cmd & 0xFFFFu); + const uint8_t num = static_cast((cmd >> 16) & 0xFFu); + const bool irq = (cmd & 0x80000000u) != 0u; + + vif0_regs.code = cmd; + vif0_regs.num = num; + if (irq) + vif0_regs.stat |= (1u << 11); + + if (opcode == VIF_NOP) + { + continue; + } + else if (opcode == VIF_STCYCL) + { + vif0_regs.cycle = imm; + continue; + } + else if (opcode == VIF_ITOP) + { + vif0_regs.itops = imm & 0x3FFu; + continue; + } + else if (opcode == VIF_STMOD) + { + vif0_regs.mode = imm & 3u; + continue; + } + else if (opcode == VIF_MARK) + { + vif0_regs.mark = imm; + vif0_regs.stat |= (1u << 6); + continue; + } + else if (opcode == VIF_FLUSHE || opcode == VIF_FLUSH || opcode == VIF_FLUSHA) + { + continue; + } + else if (opcode == VIF_STMASK) + { + if (pos + 4u > sizeBytes) + break; + std::memcpy(&vif0_regs.mask, data + pos, sizeof(vif0_regs.mask)); + pos += 4u; + continue; + } + else if (opcode == VIF_STROW) + { + if (pos + 16u > sizeBytes) + break; + std::memcpy(vif0_regs.row, data + pos, 16u); + pos += 16u; + continue; + } + else if (opcode == VIF_STCOL) + { + if (pos + 16u > sizeBytes) + break; + std::memcpy(vif0_regs.col, data + pos, 16u); + pos += 16u; + continue; + } + else if (opcode == VIF_MPG) + { + const uint32_t destAddr = static_cast(imm & 0x1FFu) * 8u; + const uint32_t instructionCount = (num == 0u) ? 256u : static_cast(num); + const uint32_t mpgBytes = instructionCount * 8u; + uint32_t copyBytes = 0u; + if (m_vu0Code && destAddr < PS2_VU0_CODE_SIZE && mpgBytes > 0u) + { + copyBytes = mpgBytes; + if (destAddr + copyBytes > PS2_VU0_CODE_SIZE) + copyBytes = PS2_VU0_CODE_SIZE - destAddr; + if (pos + copyBytes <= sizeBytes) + std::memcpy(m_vu0Code + destAddr, data + pos, copyBytes); + } + + pos += mpgBytes; + if (pos > sizeBytes) + break; + continue; + } + else if ((opcode & 0x60u) == 0x60u) + { + const uint8_t vn = static_cast((opcode >> 2) & 0x3u); + const uint8_t vl = static_cast(opcode & 0x3u); + const int components = static_cast(vn) + 1; + int bitsPerComponent = 32; + switch (vl) + { + case 0: + bitsPerComponent = 32; + break; + case 1: + bitsPerComponent = 16; + break; + case 2: + bitsPerComponent = 8; + break; + case 3: + bitsPerComponent = (vn == 3u) ? 4 : 16; + break; + default: + break; + } + const int bitsPerVector = (vl == 3u && vn == 3u) ? 16 : (components * bitsPerComponent); + uint32_t bytesPerVector = static_cast((bitsPerVector + 7) / 8); + const uint32_t writeVectorCount = (num == 0u) ? 256u : static_cast(num); + uint32_t cl = vif0_regs.cycle & 0xFFu; + uint32_t wl = (vif0_regs.cycle >> 8) & 0xFFu; + if (cl == 0u) + cl = 1u; + if (wl == 0u) + wl = 1u; + uint32_t sourceVectorCount = writeVectorCount; + if (cl < wl) + { + const uint32_t fullBlocks = writeVectorCount / wl; + uint32_t remainder = writeVectorCount % wl; + if (remainder > cl) + remainder = cl; + sourceVectorCount = fullBlocks * cl + remainder; + } + uint32_t totalBytes = sourceVectorCount * bytesPerVector; + totalBytes = (totalBytes + 3u) & ~3u; + + if (m_vu0Data && pos + totalBytes <= sizeBytes && vl == 0u) + { + uint32_t vuAddr = static_cast(imm & 0x3FFu); + if ((imm & 0x8000u) != 0u) + vuAddr = (vuAddr + (vif0_regs.tops & 0x3FFu)) & 0x3FFu; + const uint8_t *srcBase = data + pos; + uint32_t srcIndex = 0u; + for (uint32_t writeIndex = 0; writeIndex < writeVectorCount; ++writeIndex) + { + const uint32_t cyclePos = writeIndex % wl; + const bool sourceAvailable = (cl >= wl) || (cyclePos < cl); + uint32_t destVec = (cl >= wl) ? ((vuAddr + (writeIndex / wl) * cl + cyclePos) & 0x3FFu) + : ((vuAddr + writeIndex) & 0x3FFu); + const uint32_t destOff = destVec * 16u; + if (destOff + 16u > PS2_VU0_DATA_SIZE) + { + if (sourceAvailable && srcIndex < sourceVectorCount) + ++srcIndex; + continue; + } + if (!sourceAvailable || srcIndex >= sourceVectorCount) + continue; + const uint8_t *srcVec = srcBase + srcIndex * bytesPerVector; + ++srcIndex; + uint32_t lanes[4] = {0u, 0u, 0u, 0u}; + std::memcpy(lanes, m_vu0Data + destOff, sizeof(lanes)); + const uint32_t limit = (components > 4) ? 4u : static_cast(components); + for (uint32_t c = 0; c < limit; ++c) + { + uint32_t scalar = 0u; + std::memcpy(&scalar, srcVec + c * 4u, sizeof(scalar)); + lanes[c] = scalar; + } + _mm_storeu_si128(reinterpret_cast<__m128i *>(m_vu0Data + destOff), _mm_loadu_si128(reinterpret_cast(lanes))); + } + } + pos += totalBytes; + if (pos > sizeBytes) + break; + continue; + } + else + { + break; + } + } +} + void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes) { - if (!m_rdram || !m_gsVRAM || sizeBytes == 0u) - return; - if (srcPhys >= PS2_RAM_SIZE) + if (sizeBytes == 0u || srcPhys >= PS2_RAM_SIZE) return; const uint64_t requestedEnd = static_cast(srcPhys) + static_cast(sizeBytes); @@ -65,17 +258,9 @@ void PS2Memory::processVIF1Data(uint32_t srcPhys, uint32_t sizeBytes) void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) { - if (!data || !m_gsVRAM || sizeBytes == 0u) + if (sizeBytes == 0u) return; - auto recomputeVif1Tops = [&]() - { - const bool dbf = (vif1_regs.stat & (1u << 7)) != 0u; - const uint32_t base = vif1_regs.base & 0x3FFu; - const uint32_t ofst = vif1_regs.ofst & 0x3FFu; - vif1_regs.tops = dbf ? ((base + ofst) & 0x3FFu) : base; - }; - uint32_t pos = 0; while (pos + 4 <= sizeBytes) @@ -120,18 +305,6 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) uint8_t num = (cmd >> 16) & 0xFF; const bool irq = (cmd & 0x80000000u) != 0u; - const uint32_t opcodeIndex = s_debugVif1OpcodeCount.fetch_add(1, std::memory_order_relaxed); - if (opcodeIndex < 160u) - { - RUNTIME_LOG("[vif1:cmd] idx=" << opcodeIndex - << " opcode=0x" << std::hex << static_cast(opcode) - << " imm=0x" << imm - << std::dec - << " num=" << static_cast(num) - << " irq=" << static_cast(irq ? 1u : 0u) - << std::endl); - } - // Track most-recent command for VIFn_CODE emulation. vif1_regs.code = cmd; vif1_regs.num = num; @@ -149,22 +322,23 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_OFFSET) { - const uint32_t oldTops = vif1_regs.tops & 0x3FFu; + // VIF double-buffer setup. OFFSET clears DBF and resets TOPS to BASE. + // Do not rewrite BASE from the previous TOPS value. vif1_regs.ofst = imm & 0x3FFu; - vif1_regs.base = oldTops; + vif1_regs.tops = vif1_regs.base & 0x3FFu; vif1_regs.stat &= ~(1u << 7); // clear DBF - recomputeVif1Tops(); continue; } else if (opcode == VIF_BASE) { + // BASE only updates the base register. TOPS changes on OFFSET/MSCAL. vif1_regs.base = imm & 0x3FFu; - recomputeVif1Tops(); continue; } else if (opcode == VIF_ITOP) { - vif1_regs.itop = imm & 0x3FFu; + // ITOP VIFcode writes pending ITOPS; VU XITOP observes it after MSCAL/MSCNT. + vif1_regs.itops = imm & 0x3FFu; continue; } else if (opcode == VIF_STMOD) @@ -193,39 +367,43 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_MSCAL || opcode == VIF_MSCALF) { - vif1_regs.itops = vif1_regs.itop & 0x3FFu; - vif1_regs.stat ^= (1u << 7); // toggle DBF - recomputeVif1Tops(); uint32_t startPC = (uint32_t)imm * 8u; - const uint32_t kickIndex = s_debugVu1KickCount.fetch_add(1, std::memory_order_relaxed); - if (kickIndex < 48u) - { - RUNTIME_LOG("[vif1:mscal] idx=" << kickIndex - << " opcode=0x" << std::hex << static_cast(opcode) - << " imm=0x" << imm - << " startPc=0x" << startPC - << " itop=0x" << vif1_regs.itop - << std::dec << std::endl); - } + + // Values visible to the VU program for this MSCAL. + // DobieStation semantics: ITOP = ITOPS; TOP = current TOPS; + // then TOPS/DBF are prepared for the next buffer. + const uint32_t runTop = vif1_regs.tops & 0x3FFu; + const uint32_t runItop = vif1_regs.itops & 0x3FFu; + vif1_regs.top = runTop; + vif1_regs.itop = runItop; + + const bool dbf = (vif1_regs.stat & (1u << 7)) != 0u; + if (dbf) + vif1_regs.tops = vif1_regs.base & 0x3FFu; + else + vif1_regs.tops = (vif1_regs.base + vif1_regs.ofst) & 0x3FFu; + vif1_regs.stat ^= (1u << 7); // toggle DBF + if (m_vu1MscalCallback) - m_vu1MscalCallback(startPC, vif1_regs.itop); + m_vu1MscalCallback(startPC, runTop, runItop); continue; } else if (opcode == VIF_MSCNT) { - vif1_regs.itops = vif1_regs.itop & 0x3FFu; + const uint32_t runTop = vif1_regs.tops & 0x3FFu; + const uint32_t runItop = vif1_regs.itops & 0x3FFu; + vif1_regs.top = runTop; + vif1_regs.itop = runItop; + + const bool dbf = (vif1_regs.stat & (1u << 7)) != 0u; + if (dbf) + vif1_regs.tops = vif1_regs.base & 0x3FFu; + else + vif1_regs.tops = (vif1_regs.base + vif1_regs.ofst) & 0x3FFu; vif1_regs.stat ^= (1u << 7); // toggle DBF - recomputeVif1Tops(); - const uint32_t kickIndex = s_debugVu1KickCount.fetch_add(1, std::memory_order_relaxed); - if (kickIndex < 48u) - { - RUNTIME_LOG("[vif1:mscnt] idx=" << kickIndex - << " itop=0x" << std::hex << vif1_regs.itop - << " pc=resume" - << std::dec << std::endl); - } + if (m_vu1MscntCallback) - m_vu1MscntCallback(vif1_regs.itop); + m_vu1MscntCallback(runTop, runItop); continue; } else if (opcode == VIF_STMASK) @@ -364,6 +542,7 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) vuAddr = (vuAddr + (vif1_regs.tops & 0x3FFu)) & 0x3FFu; const bool zeroExtend = (imm & 0x4000u) != 0u; + if (m_vu1Data && totalBytes > 0 && pos + totalBytes <= sizeBytes) { const uint8_t *srcBase = data + pos; diff --git a/ps2xRuntime/src/lib/ps2_vu1.cpp b/ps2xRuntime/src/lib/ps2_vu1.cpp index 10b7dd8..7c3b281 100644 --- a/ps2xRuntime/src/lib/ps2_vu1.cpp +++ b/ps2xRuntime/src/lib/ps2_vu1.cpp @@ -2,20 +2,12 @@ #include "runtime/ps2_gs_gpu.h" #include "runtime/ps2_gif_arbiter.h" #include "runtime/ps2_memory.h" -#include "ps2_log.h" -#include #include #include #include -#include #include #include -namespace -{ - std::atomic s_debugVu1XgkickCount{0}; -} - // Instruction field extraction helpers static inline uint8_t DEST(uint32_t i) { return (uint8_t)((i >> 21) & 0xF); } static inline uint8_t FT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } @@ -27,6 +19,9 @@ static inline uint8_t BC(uint32_t i) { return (uint8_t)(i & 0x3); } static inline uint8_t LIT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } static inline uint8_t LIS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } static inline uint8_t LID(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } +static inline uint8_t VIT(uint32_t i) { return (uint8_t)((i >> 16) & 0xF); } +static inline uint8_t VIS(uint32_t i) { return (uint8_t)((i >> 11) & 0xF); } +static inline uint8_t VID(uint32_t i) { return (uint8_t)((i >> 6) & 0xF); } static inline int16_t IMM11(uint32_t i) { return (int16_t)(int32_t)((int32_t)(i << 21) >> 21); } static inline int16_t IMM15(uint32_t i) { @@ -36,6 +31,135 @@ static inline int16_t IMM15(uint32_t i) return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); } + +static inline uint8_t vuUpperVfWriteReg(uint32_t upper) +{ + const uint8_t op = upper & 0x3Fu; + const uint8_t dest = DEST(upper); + const uint8_t ft = FT(upper); + const uint8_t fd = FD(upper); + + if (dest == 0u) + return 0u; + + if (op <= 0x2Fu) + return fd; + + if (op >= 0x3Cu) + { + const uint8_t specialOp = static_cast((upper & 0x3u) | ((upper >> 4) & 0x7Cu)); + switch (specialOp) + { + // Upper special ops that write a VF register use FT as destination. + case 0x10: // ITOF0 + case 0x11: // ITOF4 + case 0x12: // ITOF12 + case 0x13: // ITOF15 + case 0x14: // FTOI0 + case 0x15: // FTOI4 + case 0x16: // FTOI12 + case 0x17: // FTOI15 + case 0x1D: // ABS + return ft; + default: + return 0u; // ACC/NOP/CLIP/etc. + } + } + + return 0u; +} + +static inline void vuSetRegBit(uint32_t &mask, uint8_t reg) +{ + if (reg != 0u && reg < 32u) + mask |= (1u << reg); +} + +static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, uint32_t &writeMask) +{ + readMask = 0u; + writeMask = 0u; + + if (lower == 0u || lower == 0x8000033Cu) + return; + + const uint8_t opHi = static_cast((lower >> 25) & 0x7Fu); + const uint8_t it = LIT(lower); + const uint8_t is = LIS(lower); + + if ((lower & 0x80000000u) != 0u) + { + const uint8_t funct = lower & 0x3Fu; + if (funct >= 0x3Cu && funct <= 0x3Fu) + { + const uint8_t specialOp = static_cast((lower & 0x3u) | ((lower >> 4) & 0x7Cu)); + switch (specialOp) + { + case 0x30: // MOVE + case 0x31: // MR32 + vuSetRegBit(readMask, is); + vuSetRegBit(writeMask, it); + return; + case 0x34: // LQI + case 0x36: // LQD + vuSetRegBit(writeMask, it); + return; + case 0x35: // SQI + case 0x37: // SQD + vuSetRegBit(readMask, is); + return; + case 0x38: // DIV + case 0x3A: // RSQRT + vuSetRegBit(readMask, is); + vuSetRegBit(readMask, it); + return; + case 0x39: // SQRT + vuSetRegBit(readMask, it); + return; + case 0x3C: // MTIR + case 0x3E: // ILWR source base is integer, but field source is VF for MTIR only. + if (specialOp == 0x3C) + vuSetRegBit(readMask, is); + return; + case 0x3D: // MFIR + case 0x64: // MFP + vuSetRegBit(writeMask, it); + return; + default: + return; + } + } + return; + } + + switch (opHi) + { + case 0x00: // LQ + vuSetRegBit(writeMask, it); + return; + case 0x01: // SQ + vuSetRegBit(readMask, is); + return; + default: + return; + } +} + +static inline bool vuLowerShouldRunBeforeUpper(uint32_t upper, uint32_t lower) +{ + const uint8_t upperWrite = vuUpperVfWriteReg(upper); + if (upperWrite == 0u) + return false; + + uint32_t lowerReads = 0u; + uint32_t lowerWrites = 0u; + vuLowerVfReadWriteMasks(lower, lowerReads, lowerWrites); + + const uint32_t upperBit = (1u << upperWrite); + return ((lowerReads | lowerWrites) & upperBit) != 0u; +} + + VU1Interpreter::VU1Interpreter() { reset(); @@ -73,12 +197,16 @@ void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, - uint32_t startPC, uint32_t itop, + uint32_t startPC, uint32_t top, uint32_t itop, uint32_t maxCycles) { - m_state.pc = startPC; + m_state.pc = startPC & 0x3FFFu; m_state.ebit = false; + m_state.top = top; m_state.itop = itop; + m_state.branchPending = false; + m_state.branchTarget = 0; + m_state.branchDelay = 0; m_state.vf[0][0] = 0.0f; m_state.vf[0][1] = 0.0f; m_state.vf[0][2] = 0.0f; @@ -89,9 +217,10 @@ void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, void VU1Interpreter::resume(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, - uint32_t itop, uint32_t maxCycles) + uint32_t top, uint32_t itop, uint32_t maxCycles) { m_state.ebit = false; + m_state.top = top; m_state.itop = itop; run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); } @@ -109,16 +238,27 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, std::memcpy(&lower, vuCode + m_state.pc, 4); std::memcpy(&upper, vuCode + m_state.pc + 4, 4); - bool eBit = (upper >> 30) & 1; - bool mBit = (upper >> 31) & 1; + const bool iBit = ((upper >> 31) & 1u) != 0u; + const bool eBit = ((upper >> 30) & 1u) != 0u; + const bool mBit = ((upper >> 29) & 1u) != 0u; (void)mBit; - // LOI uses a dedicated lower opcode (0x8000033C). Do not key on bit31 alone: - // opHi=0x40 instructions (including XGKICK) also have bit31 set. - bool loi = (lower == 0x8000033Cu); + // LOI is controlled by the upper I-bit. The lower word is the float immediate. + // DobieStation executes the upper instruction first, then commits lower into I. + const bool loi = iBit; if (loi) { - std::memcpy(&m_state.i, &upper, 4); + // LOI is special: the upper instruction sees the old I value, then LOI loads I. + execUpper(upper); + std::memcpy(&m_state.i, &lower, 4); + } + else if (vuLowerShouldRunBeforeUpper(upper, lower)) + { + // VU upper/lower execute as a pair. If the upper op writes a VF register + // that the lower op reads or also writes, Dobie runs the lower side first + // so it observes the old VF value and the upper write has priority. + execLower(lower, vuData, dataSize, gs, memory, upper); + execUpper(upper); } else { @@ -139,6 +279,21 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, nextPC = 0; m_state.pc = nextPC; + // VU branch/jump has a delay slot. Branch handlers set a pending target; + // we execute one sequential instruction before committing the branch. + if (m_state.branchPending) + { + if (m_state.branchDelay == 0) + { + m_state.pc = m_state.branchTarget & 0x3FFFu; + m_state.branchPending = false; + } + else + { + --m_state.branchDelay; + } + } + if (m_state.ebit) break; @@ -346,283 +501,256 @@ void VU1Interpreter::execUpper(uint32_t instr) applyDest(vd, result, dest); return; - // Special1 group (0x3C..0x3F with secondary field) + // Upper special group (low op 0x3C..0x3F). + // Like lower1 special, the real selector is not just bits 5:0. Dobie decodes: + // op = (instr & 0x3) | ((instr >> 4) & 0x7C) + // Several instructions in this group also use FT as the destination, not FD. case 0x3C: case 0x3D: case 0x3E: case 0x3F: { - uint8_t special = (instr >> 6) & 0x1F; - uint8_t sop = (instr & 0x3) | ((instr >> 4) & 0x3C); - (void)sop; + const uint8_t specialOp = static_cast((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); + float *vtDest = m_state.vf[ft]; - switch (instr & 0x3F) + switch (specialOp) { - case 0x3C: // Special1 (ADDAx..ADDAw, SUBAx..SUBAw, MADDAx..MADDAw, MSUBAx..MSUBAw, etc.) + case 0x00: + case 0x01: + case 0x02: + case 0x03: // ADDAbc { - uint8_t funct = (instr >> 6) & 0x1F; - uint8_t bc2 = (instr >> 0) & 0x3; - (void)bc2; - switch (funct) - { - case 0x00: - case 0x01: - case 0x02: - case 0x03: // ADDAbc - { - float bc = broadcast(vt, funct & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] + bc; - applyDestAcc(result, dest); - return; - } - case 0x04: - case 0x05: - case 0x06: - case 0x07: // SUBAbc - { - float bc = broadcast(vt, funct & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] - bc; - applyDestAcc(result, dest); - return; - } - case 0x08: - case 0x09: - case 0x0A: - case 0x0B: // MADDAbc - { - float bc = broadcast(vt, funct & 3); - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] + vs[c] * bc; - applyDestAcc(result, dest); - return; - } - case 0x0C: - case 0x0D: - case 0x0E: - case 0x0F: // MSUBAbc - { - float bc = broadcast(vt, funct & 3); - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] - vs[c] * bc; - applyDestAcc(result, dest); - return; - } - case 0x10: // ITOF0 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = (float)iv; - } - applyDest(vd, result, dest); - return; - case 0x11: // ITOF4 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = (float)iv / 16.0f; - } - applyDest(vd, result, dest); - return; - case 0x12: // ITOF12 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = (float)iv / 4096.0f; - } - applyDest(vd, result, dest); - return; - case 0x13: // ITOF15 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = (float)iv / 32768.0f; - } - applyDest(vd, result, dest); - return; - case 0x14: // FTOI0 - for (int c = 0; c < 4; c++) - { - int32_t iv = (int32_t)vs[c]; - std::memcpy(&result[c], &iv, 4); - } - applyDest(vd, result, dest); - return; - case 0x15: // FTOI4 - for (int c = 0; c < 4; c++) - { - int32_t iv = (int32_t)(vs[c] * 16.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vd, result, dest); - return; - case 0x16: // FTOI12 - for (int c = 0; c < 4; c++) - { - int32_t iv = (int32_t)(vs[c] * 4096.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vd, result, dest); - return; - case 0x17: // FTOI15 - for (int c = 0; c < 4; c++) - { - int32_t iv = (int32_t)(vs[c] * 32768.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vd, result, dest); - return; - case 0x18: - case 0x19: - case 0x1A: - case 0x1B: // MULAbc - { - float bc = broadcast(vt, funct & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] * bc; - applyDestAcc(result, dest); - return; - } - case 0x1C: // MULAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.q; - applyDestAcc(result, dest); - return; - case 0x1D: // ABS - for (int c = 0; c < 4; c++) - result[c] = std::fabs(vs[c]); - applyDest(vd, result, dest); - return; - case 0x1E: // MULAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.i; - applyDestAcc(result, dest); - return; - case 0x1F: // CLIP - { - float w = std::fabs(vt[3]); - uint32_t flags = 0; - if (vs[0] > +w) - flags |= 0x01; - if (vs[0] < -w) - flags |= 0x02; - if (vs[1] > +w) - flags |= 0x04; - if (vs[1] < -w) - flags |= 0x08; - if (vs[2] > +w) - flags |= 0x10; - if (vs[2] < -w) - flags |= 0x20; - m_state.clip = (m_state.clip << 6) | flags; - return; - } - default: - return; - } - } - case 0x3D: // Special2 (ADDAq, MADDAq, ADDAi, MADDAi, ADDA, MADDA, MULA, OPMULA, ...) - { - uint8_t funct = (instr >> 6) & 0x1F; - switch (funct) - { - case 0x00: // ADDAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.q; - applyDestAcc(result, dest); - return; - case 0x01: // MADDAq - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] + vs[c] * m_state.q; - applyDestAcc(result, dest); - return; - case 0x02: // ADDAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.i; - applyDestAcc(result, dest); - return; - case 0x03: // MADDAi - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] + vs[c] * m_state.i; - applyDestAcc(result, dest); - return; - case 0x04: // SUBAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.q; - applyDestAcc(result, dest); - return; - case 0x05: // MSUBAq - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] - vs[c] * m_state.q; - applyDestAcc(result, dest); - return; - case 0x06: // SUBAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.i; - applyDestAcc(result, dest); - return; - case 0x07: // MSUBAi - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] - vs[c] * m_state.i; - applyDestAcc(result, dest); - return; - case 0x08: // ADDA - for (int c = 0; c < 4; c++) - result[c] = vs[c] + vt[c]; - applyDestAcc(result, dest); - return; - case 0x09: // MADDA - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] + vs[c] * vt[c]; - applyDestAcc(result, dest); - return; - case 0x0A: // MULA - for (int c = 0; c < 4; c++) - result[c] = vs[c] * vt[c]; - applyDestAcc(result, dest); - return; - case 0x0C: // SUBA - for (int c = 0; c < 4; c++) - result[c] = vs[c] - vt[c]; - applyDestAcc(result, dest); - return; - case 0x0D: // MSUBA - for (int c = 0; c < 4; c++) - result[c] = m_state.acc[c] - vs[c] * vt[c]; - applyDestAcc(result, dest); - return; - case 0x0E: // OPMULA - result[0] = vs[1] * vt[2]; - result[1] = vs[2] * vt[0]; - result[2] = vs[0] * vt[1]; - result[3] = 0.0f; - applyDestAcc(result, dest); - return; - case 0x0F: // NOP - return; - default: - return; - } - } - case 0x3E: // Special (more upper ops, rarely used) - return; - case 0x3F: // Special (upper NOP typically) + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] + bc; + applyDestAcc(result, dest); + return; + } + case 0x04: + case 0x05: + case 0x06: + case 0x07: // SUBAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] - bc; + applyDestAcc(result, dest); + return; + } + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: // MADDAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] + vs[c] * bc; + applyDestAcc(result, dest); + return; + } + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: // MSUBAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] - vs[c] * bc; + applyDestAcc(result, dest); + return; + } + case 0x10: // ITOF0 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv); + } + applyDest(vtDest, result, dest); + return; + case 0x11: // ITOF4 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 16.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x12: // ITOF12 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 4096.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x13: // ITOF15 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 32768.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x14: // FTOI0 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c]); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x15: // FTOI4 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 16.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x16: // FTOI12 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 4096.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x17: // FTOI15 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 32768.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: // MULAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] * bc; + applyDestAcc(result, dest); + return; + } + case 0x1C: // MULAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] * m_state.q; + applyDestAcc(result, dest); + return; + case 0x1D: // ABS + for (int c = 0; c < 4; c++) + result[c] = std::fabs(vs[c]); + applyDest(vtDest, result, dest); + return; + case 0x1E: // MULAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] * m_state.i; + applyDestAcc(result, dest); + return; + case 0x1F: // CLIP + { + float w = std::fabs(vt[3]); + uint32_t flags = 0; + if (vs[0] > +w) flags |= 0x01; + if (vs[0] < -w) flags |= 0x02; + if (vs[1] > +w) flags |= 0x04; + if (vs[1] < -w) flags |= 0x08; + if (vs[2] > +w) flags |= 0x10; + if (vs[2] < -w) flags |= 0x20; + m_state.clip = (m_state.clip << 6) | flags; + return; + } + case 0x20: // ADDAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] + m_state.q; + applyDestAcc(result, dest); + return; + case 0x21: // MADDAq + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] + vs[c] * m_state.q; + applyDestAcc(result, dest); + return; + case 0x22: // ADDAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] + m_state.i; + applyDestAcc(result, dest); + return; + case 0x23: // MADDAi + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] + vs[c] * m_state.i; + applyDestAcc(result, dest); + return; + case 0x24: // SUBAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.q; + applyDestAcc(result, dest); + return; + case 0x25: // MSUBAq + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] - vs[c] * m_state.q; + applyDestAcc(result, dest); + return; + case 0x26: // SUBAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.i; + applyDestAcc(result, dest); + return; + case 0x27: // MSUBAi + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] - vs[c] * m_state.i; + applyDestAcc(result, dest); + return; + case 0x28: // ADDA + for (int c = 0; c < 4; c++) + result[c] = vs[c] + vt[c]; + applyDestAcc(result, dest); + return; + case 0x29: // MADDA + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] + vs[c] * vt[c]; + applyDestAcc(result, dest); + return; + case 0x2A: // MULA + for (int c = 0; c < 4; c++) + result[c] = vs[c] * vt[c]; + applyDestAcc(result, dest); + return; + case 0x2C: // SUBA + for (int c = 0; c < 4; c++) + result[c] = vs[c] - vt[c]; + applyDestAcc(result, dest); + return; + case 0x2D: // MSUBA + for (int c = 0; c < 4; c++) + result[c] = m_state.acc[c] - vs[c] * vt[c]; + applyDestAcc(result, dest); + return; + case 0x2E: // OPMULA + result[0] = vs[1] * vt[2]; + result[1] = vs[2] * vt[0]; + result[2] = vs[0] * vt[1]; + result[3] = 0.0f; + applyDestAcc(result, dest); + return; + case 0x2F: + case 0x30: // NOP + return; + default: return; } - return; } case 0x30: case 0x31: case 0x32: - case 0x33: // iadd-like upper? No, these are valid upper ops + case 0x33: default: - // NOP / unimplemented upper return; } } @@ -643,8 +771,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { case 0x00: // LQ (Load Quadword from VU data memory) { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = FT(instr); // VF destination + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; @@ -659,8 +787,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x01: // SQ (Store Quadword to VU data memory) { - uint8_t is = LIS(instr); - uint8_t it = LIT(instr); + uint8_t is = FS(instr); // VF source + uint8_t it = VIT(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[it] + imm)) * 16u; @@ -683,8 +811,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x04: // ILW (Integer Load Word from VU data memory) { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); // VI destination + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; @@ -709,8 +837,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x05: // ISW (Integer Store Word to VU data memory) { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); // VI source + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; @@ -731,8 +859,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x08: // IADDIU { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); if (it != 0) m_state.vi[it] = (int16_t)(m_state.vi[is] + imm); @@ -740,8 +868,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x09: // ISUBIU { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); if (it != 0) m_state.vi[it] = (int16_t)(m_state.vi[is] - imm); @@ -801,24 +929,24 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x18: // FMAND { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); if (it != 0) m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); return; } case 0x1A: // FMEQ { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); if (it != 0) m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; return; } case 0x1C: // FMOR { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); if (it != 0) m_state.vi[it] = (int32_t)(m_state.mac | (uint32_t)(uint16_t)m_state.vi[is]); return; @@ -827,336 +955,136 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { int16_t imm = IMM11(instr); uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - // Simplified branch delay: set PC so next iteration lands on target - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; return; } case 0x21: // BAL (Branch and link) { - uint8_t it = LIT(instr); + uint8_t it = VIT(instr); int16_t imm = IMM11(instr); uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; if (it != 0) m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; return; } case 0x24: // JR { - uint8_t is = LIS(instr); + uint8_t is = VIS(instr); uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; return; } case 0x25: // JALR { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; if (it != 0) m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; return; } case 0x28: // IBEQ { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] == (int16_t)m_state.vi[it]) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } case 0x29: // IBNE { - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] != (int16_t)m_state.vi[it]) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } case 0x2C: // IBLTZ { - uint8_t is = LIS(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] < 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } case 0x2D: // IBGTZ { - uint8_t is = LIS(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] > 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } case 0x2E: // IBLEZ { - uint8_t is = LIS(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] <= 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } case 0x2F: // IBGEZ { - uint8_t is = LIS(instr); + uint8_t is = VIS(instr); int16_t imm = IMM11(instr); if ((int16_t)m_state.vi[is] >= 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.pc = target - 8; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } - case 0x40: // Lower special (opcode in bits 5:0) + case 0x40: // Lower1 / lower special. Bit31 set; low 6 bits select integer or special op. { - uint8_t funct = instr & 0x3F; - uint8_t it = LIT(instr); - uint8_t is = LIS(instr); - uint8_t id = LID(instr); - uint8_t dest = (instr >> 21) & 0xF; + const uint8_t funct = instr & 0x3Fu; + const uint8_t vfT = FT(instr); + const uint8_t vfS = FS(instr); + const uint8_t viT = VIT(instr); + const uint8_t viS = VIS(instr); + const uint8_t viD = VID(instr); + const uint8_t dest = (instr >> 21) & 0xF; - switch (funct) - { - case 0x30: // IADD - if (id != 0) - m_state.vi[id] = (int16_t)(m_state.vi[is] + m_state.vi[it]); - return; - case 0x31: // ISUB - if (id != 0) - m_state.vi[id] = (int16_t)(m_state.vi[is] - m_state.vi[it]); - return; - case 0x32: // IADDI - { - int16_t imm5 = (int16_t)((int32_t)((instr >> 6) & 0x1F) << 27 >> 27); - if (it != 0) - m_state.vi[it] = (int16_t)(m_state.vi[is] + imm5); - return; - } - case 0x34: // IAND - if (id != 0) - m_state.vi[id] = m_state.vi[is] & m_state.vi[it]; - return; - case 0x35: // IOR - if (id != 0) - m_state.vi[id] = m_state.vi[is] | m_state.vi[it]; - return; - - case 0x3C: // Lower special2 - { - uint8_t funct2 = (instr >> 6) & 0x1F; - switch (funct2) - { - case 0x00: // MOVE - { - float tmp[4]; - std::memcpy(tmp, m_state.vf[is], 16); - applyDest(m_state.vf[it], tmp, dest); - return; - } - case 0x01: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) - { - float tmp[4] = {m_state.vf[is][1], m_state.vf[is][2], m_state.vf[is][3], m_state.vf[is][0]}; - applyDest(m_state.vf[it], tmp, dest); - return; - } - case 0x03: // MFIR (Move From Integer Register) - { - float result[4]; - int32_t val = (int32_t)(int16_t)(m_state.vi[is] & 0xFFFF); - std::memcpy(&result[0], &val, 4); - result[1] = result[0]; - result[2] = result[0]; - result[3] = result[0]; - applyDest(m_state.vf[it], result, dest); - return; - } - case 0x04: // MTIR (Move To Integer Register) - { - int comp = 0; - if (dest & 0x8) - comp = 0; - else if (dest & 0x4) - comp = 1; - else if (dest & 0x2) - comp = 2; - else - comp = 3; - uint32_t fval; - std::memcpy(&fval, &m_state.vf[is][comp], 4); - if (it != 0) - m_state.vi[it] = (int32_t)(int16_t)(fval & 0xFFFF); - return; - } - case 0x05: // RNEXT - return; - case 0x06: // RGET - return; - case 0x07: // RINIT - return; - case 0x10: // LQI (Load Quadword, post-increment) - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[is]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[it], tmp, dest); - } - if (is != 0) - m_state.vi[is] = (int16_t)(m_state.vi[is] + 1); - return; - } - case 0x11: // SQI (Store Quadword, post-increment) - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[it]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - if (dest & 0x8) - tmp[0] = m_state.vf[is][0]; - if (dest & 0x4) - tmp[1] = m_state.vf[is][1]; - if (dest & 0x2) - tmp[2] = m_state.vf[is][2]; - if (dest & 0x1) - tmp[3] = m_state.vf[is][3]; - std::memcpy(vuData + addr, tmp, 16); - } - if (it != 0) - m_state.vi[it] = (int16_t)(m_state.vi[it] + 1); - return; - } - case 0x12: // LQD (Load Quadword, pre-decrement) - { - if (is != 0) - m_state.vi[is] = (int16_t)(m_state.vi[is] - 1); - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[is]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[it], tmp, dest); - } - return; - } - case 0x13: // SQD (Store Quadword, pre-decrement) - { - if (it != 0) - m_state.vi[it] = (int16_t)(m_state.vi[it] - 1); - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[it]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - if (dest & 0x8) - tmp[0] = m_state.vf[is][0]; - if (dest & 0x4) - tmp[1] = m_state.vf[is][1]; - if (dest & 0x2) - tmp[2] = m_state.vf[is][2]; - if (dest & 0x1) - tmp[3] = m_state.vf[is][3]; - std::memcpy(vuData + addr, tmp, 16); - } - return; - } - case 0x14: // DIV - { - int fsf = (instr >> 21) & 0x3; - int ftf = (instr >> 23) & 0x3; - float num = m_state.vf[is][fsf]; - float den = m_state.vf[it][ftf]; - if (den != 0.0f) - m_state.q = num / den; - else - m_state.q = (num >= 0.0f) ? std::numeric_limits::max() : -std::numeric_limits::max(); - return; - } - case 0x15: // SQRT - { - int ftf = (instr >> 23) & 0x3; - float val = m_state.vf[it][ftf]; - m_state.q = std::sqrt(std::fabs(val)); - return; - } - case 0x16: // RSQRT - { - int fsf = (instr >> 21) & 0x3; - int ftf = (instr >> 23) & 0x3; - float num = m_state.vf[is][fsf]; - float den = std::sqrt(std::fabs(m_state.vf[it][ftf])); - if (den != 0.0f) - m_state.q = num / den; - else - m_state.q = std::numeric_limits::max(); - return; - } - case 0x17: // WAITQ - return; - case 0x18: // ESADD - return; - case 0x19: // ERSADD - return; - case 0x1B: // ELENG - { - float s = m_state.vf[is][0] * m_state.vf[is][0] + m_state.vf[is][1] * m_state.vf[is][1] + m_state.vf[is][2] * m_state.vf[is][2]; - m_state.p = std::sqrt(s); - return; - } - case 0x1C: // ERCPR - { - int fsf = (instr >> 21) & 0x3; - float val = m_state.vf[is][fsf]; - m_state.p = (val != 0.0f) ? (1.0f / val) : std::numeric_limits::max(); - return; - } - case 0x1D: // ERLENG - { - float s = m_state.vf[is][0] * m_state.vf[is][0] + m_state.vf[is][1] * m_state.vf[is][1] + m_state.vf[is][2] * m_state.vf[is][2]; - float len = std::sqrt(s); - m_state.p = (len != 0.0f) ? (1.0f / len) : std::numeric_limits::max(); - return; - } - case 0x1E: // WAITP - return; - case 0x1A: // EATAN / EATANxy / EATANxz - return; - case 0x1F: // MFP (Move From P register) - { - float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; - applyDest(m_state.vf[it], result, dest); - return; - } - default: - return; - } - } - case 0x3D: // XGKICK - send GIF packet from VU1 data memory + auto doXgkick = [&]() { if (!vuData || dataSize < 16u) return; @@ -1178,7 +1106,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz return value; }; - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[is]) * 16u; + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; addr = wrapOffset(addr); uint32_t pktOff = addr; uint32_t totalBytes = 0u; @@ -1223,17 +1151,6 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz if (totalBytes == 0u) return; - const uint32_t debugIndex = s_debugVu1XgkickCount.fetch_add(1, std::memory_order_relaxed); - if (debugIndex < 64u) - { - RUNTIME_LOG("[vu1:xgkick] idx=" << debugIndex - << " addr=0x" << std::hex << addr - << " totalBytes=0x" << totalBytes - << std::dec - << " wrap=" << static_cast((addr + totalBytes > dataSize) ? 1u : 0u) - << std::endl); - } - if (addr + totalBytes <= dataSize) { if (memory) @@ -1254,19 +1171,288 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz else gs.processGIFPacket(wrappedPacket.data(), totalBytes); } + }; + + switch (funct) + { + case 0x30: // IADD + if (viD != 0) + m_state.vi[viD] = (int16_t)(m_state.vi[viS] + m_state.vi[viT]); + return; + case 0x31: // ISUB + if (viD != 0) + m_state.vi[viD] = (int16_t)(m_state.vi[viS] - m_state.vi[viT]); + return; + case 0x32: // IADDI + { + int16_t imm5 = (int16_t)((int32_t)((instr >> 6) & 0x1F) << 27 >> 27); + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viS] + imm5); return; } - case 0x3E: // XTOP - { - if (it != 0) - m_state.vi[it] = (int32_t)m_state.itop; + case 0x34: // IAND + if (viD != 0) + m_state.vi[viD] = m_state.vi[viS] & m_state.vi[viT]; return; - } - case 0x3F: // XITOP - { - if (it != 0) - m_state.vi[it] = (int32_t)m_state.itop; + case 0x35: // IOR + if (viD != 0) + m_state.vi[viD] = m_state.vi[viS] | m_state.vi[viT]; return; + + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: // Lower1 special. Dobie decodes this as (instr & 3) | ((instr >> 4) & 0x7C). + { + const uint8_t funct2 = (uint8_t)((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); + switch (funct2) + { + case 0x30: // MOVE + { + float tmp[4]; + std::memcpy(tmp, m_state.vf[vfS], 16); + applyDest(m_state.vf[vfT], tmp, dest); + return; + } + case 0x31: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) + { + float tmp[4] = {m_state.vf[vfS][1], m_state.vf[vfS][2], m_state.vf[vfS][3], m_state.vf[vfS][0]}; + applyDest(m_state.vf[vfT], tmp, dest); + return; + } + case 0x34: // LQI (Load Quadword, post-increment) + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + applyDest(m_state.vf[vfT], tmp, dest); + } + if (viS != 0) + m_state.vi[viS] = (int16_t)(m_state.vi[viS] + 1); + return; + } + case 0x35: // SQI (Store Quadword, post-increment) + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + if (dest & 0x8) + tmp[0] = m_state.vf[vfS][0]; + if (dest & 0x4) + tmp[1] = m_state.vf[vfS][1]; + if (dest & 0x2) + tmp[2] = m_state.vf[vfS][2]; + if (dest & 0x1) + tmp[3] = m_state.vf[vfS][3]; + std::memcpy(vuData + addr, tmp, 16); + } + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1); + return; + } + case 0x36: // LQD (Load Quadword, pre-decrement) + { + if (viS != 0) + m_state.vi[viS] = (int16_t)(m_state.vi[viS] - 1); + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + applyDest(m_state.vf[vfT], tmp, dest); + } + return; + } + case 0x37: // SQD (Store Quadword, pre-decrement) + { + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viT] - 1); + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + if (dest & 0x8) + tmp[0] = m_state.vf[vfS][0]; + if (dest & 0x4) + tmp[1] = m_state.vf[vfS][1]; + if (dest & 0x2) + tmp[2] = m_state.vf[vfS][2]; + if (dest & 0x1) + tmp[3] = m_state.vf[vfS][3]; + std::memcpy(vuData + addr, tmp, 16); + } + return; + } + case 0x38: // DIV + { + int fsf = (instr >> 21) & 0x3; + int ftf = (instr >> 23) & 0x3; + float num = m_state.vf[vfS][fsf]; + float den = m_state.vf[vfT][ftf]; + if (den != 0.0f) + m_state.q = num / den; + else + m_state.q = (num >= 0.0f) ? std::numeric_limits::max() : -std::numeric_limits::max(); + return; + } + case 0x39: // SQRT + { + int ftf = (instr >> 23) & 0x3; + float val = m_state.vf[vfT][ftf]; + m_state.q = std::sqrt(std::fabs(val)); + return; + } + case 0x3A: // RSQRT + { + int fsf = (instr >> 21) & 0x3; + int ftf = (instr >> 23) & 0x3; + float num = m_state.vf[vfS][fsf]; + float den = std::sqrt(std::fabs(m_state.vf[vfT][ftf])); + if (den != 0.0f) + m_state.q = num / den; + else + m_state.q = std::numeric_limits::max(); + return; + } + case 0x3B: // WAITQ + return; + case 0x3C: // MTIR (Move To Integer Register) + { + int comp = 0; + if (dest & 0x8) + comp = 0; + else if (dest & 0x4) + comp = 1; + else if (dest & 0x2) + comp = 2; + else + comp = 3; + uint32_t fval; + std::memcpy(&fval, &m_state.vf[vfS][comp], 4); + if (viT != 0) + m_state.vi[viT] = (int32_t)(int16_t)(fval & 0xFFFF); + return; + } + case 0x3D: // MFIR (Move From Integer Register) + { + float result[4]; + int32_t val = (int32_t)(int16_t)(m_state.vi[viS] & 0xFFFF); + std::memcpy(&result[0], &val, 4); + result[1] = result[0]; + result[2] = result[0]; + result[3] = result[0]; + applyDest(m_state.vf[vfT], result, dest); + return; + } + case 0x3E: // ILWR - integer load word from address in VI[is] + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + int comp = 0; + if (dest & 0x8) + comp = 0; + else if (dest & 0x4) + comp = 1; + else if (dest & 0x2) + comp = 2; + else + comp = 3; + uint32_t v; + std::memcpy(&v, vuData + addr + comp * 4, 4); + if (viT != 0) + m_state.vi[viT] = (int32_t)(int16_t)(v & 0xFFFF); + } + return; + } + case 0x3F: // ISWR - integer store word to address in VI[is] + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + uint32_t val = (uint32_t)(uint16_t)(m_state.vi[viT] & 0xFFFF); + if (dest & 0x8) + std::memcpy(vuData + addr + 0, &val, 4); + if (dest & 0x4) + std::memcpy(vuData + addr + 4, &val, 4); + if (dest & 0x2) + std::memcpy(vuData + addr + 8, &val, 4); + if (dest & 0x1) + std::memcpy(vuData + addr + 12, &val, 4); + } + return; + } + case 0x40: // RNEXT + return; + case 0x41: // RGET + return; + case 0x42: // RINIT + return; + case 0x43: // RXOR + return; + case 0x64: // MFP (Move From P register) + { + float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; + applyDest(m_state.vf[vfT], result, dest); + return; + } + case 0x68: // XTOP - move current VIF1 TOP into VI register + { + if (viT != 0) + m_state.vi[viT] = (int32_t)(m_state.top & 0x3FFu); + return; + } + case 0x69: // XITOP - move current VIF1 ITOP into VI register + { + if (viT != 0) + m_state.vi[viT] = (int32_t)(m_state.itop & 0x3FFu); + return; + } + case 0x6C: // XGKICK - send GIF packet from VU1 data memory + doXgkick(); + return; + case 0x70: // ESADD + return; + case 0x71: // ERSADD + return; + case 0x72: // ELENG + { + float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2]; + m_state.p = std::sqrt(s); + return; + } + case 0x73: // ERLENG + { + float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2]; + float len = std::sqrt(s); + m_state.p = (len != 0.0f) ? (1.0f / len) : std::numeric_limits::max(); + return; + } + case 0x7A: // ERCPR + { + int fsf = (instr >> 21) & 0x3; + float val = m_state.vf[vfS][fsf]; + m_state.p = (val != 0.0f) ? (1.0f / val) : std::numeric_limits::max(); + return; + } + case 0x7B: // WAITP + return; + case 0x7D: // EATAN / EATANxy / EATANxz placeholder + return; + default: + return; + } } default: return; diff --git a/ps2xRuntime/src/main.cpp b/ps2xRuntime/src/main.cpp index 21cd2c6..a2fce86 100644 --- a/ps2xRuntime/src/main.cpp +++ b/ps2xRuntime/src/main.cpp @@ -1,6 +1,9 @@ #include "ps2_runtime.h" #include "register_functions.h" #include "games_database.h" +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) +#include "ps2_debug_panel.h" +#endif #ifdef _DEBUG #include "ps2_log.h" @@ -114,6 +117,26 @@ int main(int argc, char *argv[]) } PS2Runtime runtime; +#if defined(PS2X_ENABLE_DEBUG_UI) && !defined(PLATFORM_VITA) + // This hook is to prevent leak rlimgui deps to recompiler etc + PS2DebugPanel debugPanel; + runtime.setDebugUiCallbacks( + [](PS2Runtime &rt, void *userData) + { + (void)rt; + static_cast(userData)->initialize(); + }, + [](PS2Runtime &rt, void *userData) + { + static_cast(userData)->draw(rt); + }, + [](PS2Runtime &rt, void *userData) + { + (void)rt; + static_cast(userData)->shutdown(); + }, + &debugPanel); +#endif if (!runtime.initialize(windowTitle.c_str())) { std::cerr << "Failed to initialize PS2 runtime" << std::endl; diff --git a/ps2xStudio/CMakeLists.txt b/ps2xStudio/CMakeLists.txt index 7d739a2..b199430 100644 --- a/ps2xStudio/CMakeLists.txt +++ b/ps2xStudio/CMakeLists.txt @@ -36,6 +36,11 @@ FetchContent_Declare( GIT_SHALLOW TRUE ) +set(SDL_TEST OFF CACHE BOOL "" FORCE) +set(SDL_TESTS OFF CACHE BOOL "" FORCE) +set(SDL_SHARED OFF CACHE BOOL "" FORCE) +set(SDL_STATIC ON CACHE BOOL "" FORCE) + FetchContent_Declare( SDL2 GIT_REPOSITORY https://github.com/libsdl-org/SDL.git @@ -129,6 +134,10 @@ if(WIN32) target_link_libraries(ps2xStudio PRIVATE SDL2::SDL2main) endif() +if(COMMAND ps2x_stage_ffmpeg_runtime_dlls) + ps2x_stage_ffmpeg_runtime_dlls(ps2xStudio) +endif() + include("${CMAKE_SOURCE_DIR}/ps2xRuntime/cmake/ReleaseMode.cmake") if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index a429123..8f65a72 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -60,6 +60,10 @@ target_link_libraries(ps2x_tests PRIVATE ps2_runtime ) +if(COMMAND ps2x_stage_ffmpeg_runtime_dlls) + ps2x_stage_ffmpeg_runtime_dlls(ps2x_tests) +endif() + include("${CMAKE_SOURCE_DIR}/ps2xRuntime/cmake/ReleaseMode.cmake") if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") diff --git a/ps2xTest/src/pad_input_tests.cpp b/ps2xTest/src/pad_input_tests.cpp index 4d97352..3a3c2aa 100644 --- a/ps2xTest/src/pad_input_tests.cpp +++ b/ps2xTest/src/pad_input_tests.cpp @@ -195,6 +195,49 @@ void register_pad_input_tests() t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(0), "closed port should return DISCONNECTED after close"); }); + tc.Run("pad command state reports EXECCMD once before returning STABLE", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0); + R5900Context ctx; + + ps2_stubs::scePadInit(rdram.data(), &ctx, nullptr); + openPadPort(ctx, rdram); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + setRegU32(ctx, 6, 1u); + setRegU32(ctx, 7, 3u); + ps2_stubs::scePadSetMainMode(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(1), "scePadSetMainMode should succeed"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadGetState(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(5), "first state after mode command should be EXECCMD"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadGetState(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(6), "second state after mode command should return STABLE"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadEnterPressMode(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(1), "scePadEnterPressMode should succeed"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadGetState(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(5), "first state after press-mode command should be EXECCMD"); + + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, 0u); + ps2_stubs::scePadGetState(rdram.data(), &ctx, nullptr); + t.Equals(static_cast(getRegU32(&ctx, 2)), static_cast(6), "second state after press-mode command should return STABLE"); + + closePadPort(ctx, rdram); + }); + tc.Run("pad info and mode helpers return consistent values", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0); diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index b1efb83..4b0edc0 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -31,6 +31,11 @@ namespace ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); } + void setRegU64(R5900Context &ctx, int reg, uint64_t value) + { + ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); + } + uint32_t getRegU32Test(const R5900Context &ctx, int reg) { return ::getRegU32(&ctx, reg); @@ -359,8 +364,7 @@ void register_ps2_gs_tests() std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context ctx{}; - setRegU32(ctx, 4, 0x11112222u); // new IMR low - setRegU32(ctx, 5, 0x33334444u); // new IMR high + setRegU64(ctx, 4, 0x3333444411112222ull); GsPutIMR(rdram.data(), &ctx, &runtime); const uint64_t oldImr = getReturnU64(ctx); diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 0e0f2fc..9e94d89 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -9,8 +9,10 @@ #include "Stubs/GS.h" #include +#include #include #include +#include #include namespace @@ -111,14 +113,16 @@ namespace return tag; } - uint32_t makeVuLowerSpecial(uint8_t funct, uint8_t is, uint8_t it = 0u, uint8_t id = 0u, uint8_t dest = 0u) + uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is, uint8_t it = 0u, uint8_t id = 0u, uint8_t dest = 0u) { return (0x40u << 25) | (static_cast(dest & 0xFu) << 21) | (static_cast(it & 0x1Fu) << 16) | (static_cast(is & 0x1Fu) << 11) | (static_cast(id & 0x1Fu) << 6) | - static_cast(funct & 0x3Fu); + (static_cast(specialOp & 0x7Cu) << 4) | + static_cast(specialOp & 0x3u) | + 0x3Cu; } } @@ -159,6 +163,29 @@ void register_ps2_memory_tests() t.Equals(mem.translateAddress(PS2_SCRATCHPAD_ALIAS_BASE + 0x123u), 0x123u, "0xF000 scratchpad alias should translate to local offset"); }); + tc.Run("EE timer0 count advances while enabled and can be reset", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kTimer0Count = 0x10000000u; + constexpr uint32_t kTimer0Mode = 0x10000010u; + constexpr uint32_t kTimer0Compare = 0x10000020u; + + t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count reset write should succeed"); + t.IsTrue(mem.writeIORegister(kTimer0Compare, 1u), "timer compare write should succeed"); + t.IsTrue(mem.writeIORegister(kTimer0Mode, 0x283u), "timer mode write should be retained"); + t.Equals(mem.readIORegister(kTimer0Mode), 0x283u, "timer mode should be readable"); + + std::this_thread::sleep_for(std::chrono::milliseconds(3)); + const uint32_t firstCount = mem.readIORegister(kTimer0Count); + t.IsTrue(firstCount > 0u, "enabled timer count should advance from host time"); + + t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count second reset should succeed"); + const uint32_t resetCount = mem.readIORegister(kTimer0Count); + t.IsTrue(resetCount <= firstCount, "timer reset should restart the count window"); + }); + tc.Run("scratchpad alias accesses the same bytes as base", [](TestCase &t) { PS2Memory mem; @@ -173,6 +200,33 @@ void register_ps2_memory_tests() t.Equals(mem.read32(kScratchAliasAddr), 0xCAFEBABEu, "reads through 0xF000 scratchpad alias should see scratchpad bytes"); }); + tc.Run("VU0 code and data windows map through EE addresses", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kCodeAddr = PS2_VU0_CODE_BASE + 0x20u; + mem.write32(kCodeAddr, 0x11223344u); + t.Equals(mem.read32(kCodeAddr), 0x11223344u, "VU0 code readback should match written word"); + + uint32_t codeWord = 0u; + std::memcpy(&codeWord, mem.getVU0Code() + 0x20u, sizeof(codeWord)); + t.Equals(codeWord, 0x11223344u, "VU0 code write should land in micro memory buffer"); + + constexpr uint32_t kDataAddr = PS2_VU0_DATA_BASE + 0x30u; + const __m128i value = _mm_set_epi32(0x44556677u, 0x01234567u, 0x89ABCDEFu, 0xCAFEBABEu); + mem.write128(kDataAddr, value); + + alignas(16) uint32_t words[4]{}; + const __m128i readback = mem.read128(kDataAddr); + _mm_storeu_si128(reinterpret_cast<__m128i *>(words), readback); + + t.Equals(words[0], 0xCAFEBABEu, "VU0 data lane 0 should match"); + t.Equals(words[1], 0x89ABCDEFu, "VU0 data lane 1 should match"); + t.Equals(words[2], 0x01234567u, "VU0 data lane 2 should match"); + t.Equals(words[3], 0x44556677u, "VU0 data lane 3 should match"); + }); + tc.Run("fast memory helpers wrap safely at RAM boundary", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0u); @@ -621,13 +675,20 @@ void register_ps2_memory_tests() PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + mem.vif1_regs.base = 0x120u; mem.vif1_regs.tops = 0x120u; mem.vif1_regs.stat = (1u << 7); // DBF=1 before OFFSET - std::vector> mscalCalls; - mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t itop) + struct MscalCall { - mscalCalls.emplace_back(startPC, itop); + uint32_t startPC; + uint32_t top; + uint32_t itop; + }; + std::vector mscalCalls; + mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) + { + mscalCalls.push_back({startPC, top, itop}); }); const uint32_t offsetCmd = makeVifCmd(0x02u, 0u, 0x0022u); @@ -640,18 +701,20 @@ void register_ps2_memory_tests() const uint32_t baseCmd = makeVifCmd(0x03u, 0u, 0x0030u); mem.processVIF1Data(reinterpret_cast(&baseCmd), sizeof(baseCmd)); t.Equals(mem.vif1_regs.base, 0x30u, "BASE should update BASE register"); - t.Equals(mem.vif1_regs.tops, 0x30u, "DBF=0 keeps TOPS equal to BASE"); + t.Equals(mem.vif1_regs.tops, 0x120u, "BASE should not rewrite current TOPS"); const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x0044u); mem.processVIF1Data(reinterpret_cast(&itopCmd), sizeof(itopCmd)); - t.Equals(mem.vif1_regs.itop, 0x44u, "ITOP should update ITOP register"); + t.Equals(mem.vif1_regs.itops, 0x44u, "ITOP VIFcode should update pending ITOPS register"); const uint32_t mscalCmd = makeVifCmd(0x14u, 0u, 0x0003u); mem.processVIF1Data(reinterpret_cast(&mscalCmd), sizeof(mscalCmd)); t.Equals(mscalCalls.size(), static_cast(1u), "MSCAL should invoke callback once"); - t.Equals(mscalCalls[0].first, 0x18u, "MSCAL callback startPC should be IMMEDIATE*8"); - t.Equals(mscalCalls[0].second, 0x44u, "MSCAL callback should receive current ITOP"); - t.Equals(mem.vif1_regs.itops, 0x44u, "MSCAL should latch ITOPS from ITOP"); + t.Equals(mscalCalls[0].startPC, 0x18u, "MSCAL callback startPC should be IMMEDIATE*8"); + t.Equals(mscalCalls[0].top, 0x120u, "MSCAL callback should receive current TOPS"); + t.Equals(mscalCalls[0].itop, 0x44u, "MSCAL callback should receive pending ITOPS"); + t.Equals(mem.vif1_regs.top, 0x120u, "MSCAL should latch TOP from TOPS"); + t.Equals(mem.vif1_regs.itop, 0x44u, "MSCAL should latch ITOP from ITOPS"); t.IsTrue((mem.vif1_regs.stat & (1u << 7)) != 0u, "MSCAL should toggle DBF"); t.Equals(mem.vif1_regs.tops, 0x52u, "DBF=1 should set TOPS to BASE+OFST"); @@ -1111,6 +1174,34 @@ void register_ps2_memory_tests() "compact VIF1 chain should clear the STR bit after drain"); }); + tc.Run("VIF1 DMA chain preserves compact tag high bytes when qwc is zero", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag = 0x00025100u; + + uint8_t *rdram = mem.getRDRAM(); + std::memset(rdram + kTag, 0, 16u); + + const uint64_t endTag = makeDmaTag(0u, 7u, 0u, false); + std::memcpy(rdram + kTag, &endTag, sizeof(endTag)); + + const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x44u); + std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd)); + + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed"); + + mem.processPendingTransfers(); + + t.Equals(mem.vif1_regs.itops, 0x44u, + "qwc-zero compact VIF1 chain should still process high-half VIFcodes"); + t.IsTrue((mem.readIORegister(kVif1Ch + 0x00u) & 0x100u) == 0u, + "qwc-zero compact VIF1 chain should clear the STR bit after drain"); + }); + tc.Run("VIF1 packet builders keep chain qwc live before terminate", [](TestCase &t) { PS2Memory mem; @@ -1182,6 +1273,31 @@ void register_ps2_memory_tests() t.IsTrue(payloadOk, "live VIF1 packet payload should reach the GIF callback"); }); + tc.Run("VIF1 DMA chain latches terminal tag bits in CHCR", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag0 = 0x00027400u; + constexpr uint32_t kTag1 = kTag0 + 0x20u; + + uint8_t *rdram = mem.getRDRAM(); + writeDmaTag(rdram, kTag0, makeDmaTag(1u, 1u, 0u, false)); // CNT + std::memset(rdram + kTag0 + 0x10u, 0, 0x10u); + writeDmaTag(rdram, kTag1, makeDmaTag(0u, 7u, 0u, false)); // END + + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag0), "write VIF1 TADR should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x185u), "write VIF1 CHCR chain start should succeed"); + + mem.processPendingTransfers(); + + const uint32_t chcr = mem.readIORegister(kVif1Ch + 0x00u); + t.Equals(chcr & 0x100u, 0u, "VIF1 STR should clear after DMA chain drain"); + t.Equals(chcr & 0x70000000u, 0x70000000u, "VIF1 CHCR should expose the terminal END tag id"); + t.IsTrue((mem.readIORegister(0x1000E010u) & 0x2u) != 0u, "VIF1 DMA completion should raise D_STAT channel bit"); + }); + tc.Run("GIF DMA chain CALL sources payload from TADR+16", [](TestCase &t) { PS2Memory mem; @@ -1484,7 +1600,7 @@ void register_ps2_memory_tests() vuData[i] = static_cast(0xC0u + i); } - const uint32_t lower = makeVuLowerSpecial(0x3Du, 1u); + const uint32_t lower = makeVuLowerSpecial(0x6Cu, 1u); std::memcpy(vuCode + 0u, &lower, sizeof(lower)); const uint32_t upper = 0u; std::memcpy(vuCode + 4u, &upper, sizeof(upper)); @@ -1499,6 +1615,7 @@ void register_ps2_memory_tests() &mem, 0u, 0u, + 0u, 1u); t.Equals(captured.size(), static_cast(1u), "XGKICK should emit one wrapped GIF packet"); @@ -1669,7 +1786,7 @@ void register_ps2_memory_tests() std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); std::memset(vuData, 0, PS2_VU1_DATA_SIZE); - const uint32_t lower = makeVuLowerSpecial(0x3Du, 0u); + const uint32_t lower = makeVuLowerSpecial(0x6Cu, 0u); std::memcpy(vuCode + 0u, &lower, sizeof(lower)); const uint32_t upper = 0u; std::memcpy(vuCode + 4u, &upper, sizeof(upper)); @@ -1684,7 +1801,7 @@ void register_ps2_memory_tests() } VU1Interpreter vu1; - mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t itop) + mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) { vu1.execute(vuCode, PS2_VU1_CODE_SIZE, @@ -1693,6 +1810,7 @@ void register_ps2_memory_tests() gs, &mem, startPC, + top, itop, 1u); }); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index bb5a1a1..b3a3d00 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -11,6 +11,7 @@ #include "runtime/ps2_gs_psmct32.h" #include "ps2_runtime_macros.h" #include "Stubs/MPEG.h" +#include "Stubs/CD.h" #include "Stubs/Audio.h" #include "Stubs/GS.h" #include "Stubs/VU.h" @@ -55,6 +56,38 @@ namespace static_cast(imm); } + uint32_t makeVuLq(uint8_t dest, uint8_t targetVf, uint8_t baseVi, int16_t imm) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(targetVf & 0x1Fu) << 16) | + (static_cast(baseVi & 0x1Fu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuSq(uint8_t dest, uint8_t sourceVf, uint8_t baseVi, int16_t imm) + { + return (0x01u << 25) | + (static_cast(dest & 0xFu) << 21) | + (static_cast(baseVi & 0x1Fu) << 16) | + (static_cast(sourceVf & 0x1Fu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuAdd(uint8_t dest, uint8_t fd, uint8_t fs, uint8_t ft) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(ft & 0x1Fu) << 16) | + (static_cast(fs & 0x1Fu) << 11) | + (static_cast(fd & 0x1Fu) << 6) | + 0x28u; + } + + void writeVuInstructionPair(uint8_t *code, uint32_t pc, uint32_t lower, uint32_t upper) + { + std::memcpy(code + pc, &lower, sizeof(lower)); + std::memcpy(code + pc + sizeof(lower), &upper, sizeof(upper)); + } + bool hasSignedRdWrite(const std::string &generated, uint8_t rd) { if (rd == 0u) @@ -166,6 +199,24 @@ namespace ctx->pc = 0u; } + void testResumeOwnerFallbackHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + if (ctx) + { + setRegU32(*ctx, 2, 0x00ABC123u); + ctx->pc = 0u; + } + } + + void testResumeNextFunctionHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + if (ctx) + { + setRegU32(*ctx, 2, 0x00555555u); + ctx->pc = 0u; + } + } + constexpr uint32_t kAsyncCounterAddr = 0x2400u; void testWaitForAsyncCounter(uint8_t *rdram, R5900Context *ctx, PS2Runtime *) @@ -217,6 +268,37 @@ namespace ctx->pc = 0u; } + std::atomic gMpegStreamCallbackCount{0u}; + std::atomic gMpegStreamCallbackMpeg{0u}; + std::atomic gMpegStreamCallbackType{0u}; + std::atomic gMpegStreamCallbackDataAddr{0u}; + std::atomic gMpegStreamCallbackLen{0u}; + std::atomic gMpegStreamCallbackUserData{0u}; + + void testRecordMpegStreamCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *) + { + if (!rdram || !ctx) + { + return; + } + + const uint32_t cbData = ::getRegU32(ctx, 5); + uint32_t type = 0u; + uint32_t dataAddr = 0u; + uint32_t len = 0u; + std::memcpy(&type, rdram + cbData + 0x00u, sizeof(type)); + std::memcpy(&dataAddr, rdram + cbData + 0x08u, sizeof(dataAddr)); + std::memcpy(&len, rdram + cbData + 0x0Cu, sizeof(len)); + + gMpegStreamCallbackMpeg.store(::getRegU32(ctx, 4), std::memory_order_release); + gMpegStreamCallbackType.store(type, std::memory_order_release); + gMpegStreamCallbackDataAddr.store(dataAddr, std::memory_order_release); + gMpegStreamCallbackLen.store(len, std::memory_order_release); + gMpegStreamCallbackUserData.store(::getRegU32(ctx, 6), std::memory_order_release); + gMpegStreamCallbackCount.fetch_add(1u, std::memory_order_acq_rel); + ctx->pc = 0u; + } + } void register_ps2_runtime_expansion_tests() @@ -299,6 +381,43 @@ void register_ps2_runtime_expansion_tests() "dispatchLoop should not execute guest code concurrently on one runtime"); }); + tc.Run("wake handoff lets a contending guest thread acquire before returning", [](TestCase &t) + { + PS2Runtime runtime; + std::atomic peerRan{false}; + std::thread peer; + bool peerWaiting = false; + bool peerRanWhileMainHeld = false; + bool peerRanAfterHandoff = false; + + { + PS2Runtime::GuestExecutionScope mainScope(&runtime); + peer = std::thread([&]() + { + PS2Runtime::GuestExecutionScope peerScope(&runtime); + peerRan.store(true, std::memory_order_release); + }); + + peerWaiting = waitUntil([&]() + { + return runtime.guestExecutionWaiterCountForTesting() > 0u; + }, std::chrono::milliseconds(100)); + + peerRanWhileMainHeld = peerRan.load(std::memory_order_acquire); + runtime.yieldGuestExecutionAfterWake(); + peerRanAfterHandoff = peerRan.load(std::memory_order_acquire); + } + + if (peer.joinable()) + { + peer.join(); + } + + t.IsTrue(peerWaiting, "peer guest thread should contend while the waker owns guest execution"); + t.IsFalse(peerRanWhileMainHeld, "peer guest thread should not run before the waker yields execution"); + t.IsTrue(peerRanAfterHandoff, "wake handoff should let the peer acquire guest execution before returning"); + }); + tc.Run("guest preemption policy requests a dispatcher handoff when another guest thread contends", [](TestCase &t) { PS2Runtime runtime; @@ -357,6 +476,36 @@ void register_ps2_runtime_expansion_tests() "first guest worker should observe that the runtime requested preemption under contention"); }); + tc.Run("lookupFunction aliases internal resume PCs to nearest owner", [](TestCase &t) + { + PS2Runtime runtime; + runtime.registerFunction(0x1000u, &testResumeOwnerFallbackHandler); + runtime.registerFunction(0x1100u, &testResumeNextFunctionHandler); + + R5900Context ctx{}; + ctx.pc = 0x1010u; + auto fn = runtime.lookupFunction(ctx.pc); + fn(nullptr, &ctx, &runtime); + + t.Equals(::getRegU32(&ctx, 2), 0x00ABC123u, + "internal resume PC should dispatch to its owner function"); + }); + + tc.Run("lookupFunction aliases final-function resume PCs inside code regions", [](TestCase &t) + { + PS2Runtime runtime; + runtime.memory().registerCodeRegion(0x2000u, 0x2100u); + runtime.registerFunction(0x2000u, &testResumeOwnerFallbackHandler); + + R5900Context ctx{}; + ctx.pc = 0x2010u; + auto fn = runtime.lookupFunction(ctx.pc); + fn(nullptr, &ctx, &runtime); + + t.Equals(::getRegU32(&ctx, 2), 0x00ABC123u, + "last function should own resumable PCs within its code region"); + }); + tc.Run("vblank intc handlers can preempt serialized guest execution", [](TestCase &t) { notifyRuntimeStop(); @@ -506,10 +655,341 @@ void register_ps2_runtime_expansion_tests() "sceMpegInit should reset MPEG callback bookkeeping between runs"); }); + tc.Run("sceMpegDemuxPssRing dispatches registered video and audio stream callbacks", [](TestCase &t) + { + PS2Runtime runtime; + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kCallbackEntry = 0x00124000u; + constexpr uint32_t kVideoUserData = 0x11223344u; + constexpr uint32_t kAudioUserData = 0x55667788u; + constexpr uint32_t kVideoPacketAddr = 0x00128000u; + constexpr uint32_t kAudioPacketAddr = 0x00129000u; + + runtime.registerFunction(kCallbackEntry, &testRecordMpegStreamCallback); + + auto registerGenericCallback = [&](uint32_t callbackType, uint32_t userData) + { + R5900Context addCtx{}; + setRegU32(addCtx, 4, kMpegAddr); + setRegU32(addCtx, 5, callbackType); + setRegU32(addCtx, 6, kCallbackEntry); + setRegU32(addCtx, 7, userData); + ps2_stubs::sceMpegAddCallback(rdram.data(), &addCtx, &runtime); + }; + + auto registerStreamCallback = [&](uint32_t streamType, uint32_t userData) + { + R5900Context addCtx{}; + setRegU32(addCtx, 4, kMpegAddr); + setRegU32(addCtx, 5, streamType); + setRegU32(addCtx, 6, 0u); + setRegU32(addCtx, 7, kCallbackEntry); + setRegU32(addCtx, 8, userData); + ps2_stubs::sceMpegAddStrCallback(rdram.data(), &addCtx, &runtime); + }; + + auto writePesPacket = [&](uint32_t addr, uint8_t streamId, const std::vector &payload) + { + const uint16_t packetLen = static_cast(payload.size() + 3u); + std::vector packet = { + 0x00u, 0x00u, 0x01u, streamId, + static_cast(packetLen >> 8u), + static_cast(packetLen & 0xFFu), + 0x80u, 0x00u, 0x00u}; + packet.insert(packet.end(), payload.begin(), payload.end()); + std::memcpy(rdram.data() + addr, packet.data(), packet.size()); + return static_cast(packet.size()); + }; + + registerGenericCallback(0u, 0xDEAD0000u); + registerGenericCallback(2u, 0xDEAD0002u); + registerStreamCallback(0u, kVideoUserData); + registerStreamCallback(2u, kAudioUserData); + + const std::vector videoPayload = { + 0x00u, 0x00u, 0x01u, 0xB3u, 0x14u, 0x00u, 0xF0u, 0x13u}; + const uint32_t videoPacketSize = writePesPacket(kVideoPacketAddr, 0xE0u, videoPayload); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context videoDemuxCtx{}; + setRegU32(videoDemuxCtx, 4, kMpegAddr); + setRegU32(videoDemuxCtx, 5, kVideoPacketAddr); + setRegU32(videoDemuxCtx, 6, videoPacketSize); + setRegU32(videoDemuxCtx, 7, kVideoPacketAddr); + setRegU32(videoDemuxCtx, 8, videoPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &videoDemuxCtx, &runtime); + + t.Equals(getRegS32(videoDemuxCtx, 2), static_cast(videoPacketSize), + "sceMpegDemuxPssRing should consume the video PES packet"); + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 1u, + "registered video stream callback should be invoked"); + t.Equals(gMpegStreamCallbackMpeg.load(std::memory_order_acquire), kMpegAddr, + "video callback should receive the MPEG handle"); + t.Equals(gMpegStreamCallbackType.load(std::memory_order_acquire), 0u, + "video callback data should report M2V stream type"); + t.Equals(gMpegStreamCallbackDataAddr.load(std::memory_order_acquire), kVideoPacketAddr + 9u, + "video callback data should point at PES payload"); + t.Equals(gMpegStreamCallbackLen.load(std::memory_order_acquire), + static_cast(videoPayload.size()), + "video callback data should report PES payload length"); + t.Equals(gMpegStreamCallbackUserData.load(std::memory_order_acquire), kVideoUserData, + "video callback should receive registered user data"); + + const std::vector audioPayload = {0x80u, 0x01u, 0x02u, 0x03u, 0x04u, 0x05u}; + const uint32_t audioPacketSize = writePesPacket(kAudioPacketAddr, 0xBDu, audioPayload); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context audioDemuxCtx{}; + setRegU32(audioDemuxCtx, 4, kMpegAddr); + setRegU32(audioDemuxCtx, 5, kAudioPacketAddr); + setRegU32(audioDemuxCtx, 6, audioPacketSize); + setRegU32(audioDemuxCtx, 7, kAudioPacketAddr); + setRegU32(audioDemuxCtx, 8, audioPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &audioDemuxCtx, &runtime); + + t.Equals(getRegS32(audioDemuxCtx, 2), static_cast(audioPacketSize), + "sceMpegDemuxPssRing should consume the audio PES packet"); + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 1u, + "registered audio stream callback should be invoked"); + t.Equals(gMpegStreamCallbackType.load(std::memory_order_acquire), 2u, + "audio callback data should report PCM stream type"); + t.Equals(gMpegStreamCallbackDataAddr.load(std::memory_order_acquire), kAudioPacketAddr + 9u, + "audio callback data should point at PES payload"); + t.Equals(gMpegStreamCallbackLen.load(std::memory_order_acquire), + static_cast(audioPayload.size()), + "audio callback data should report PES payload length"); + t.Equals(gMpegStreamCallbackUserData.load(std::memory_order_acquire), kAudioUserData, + "audio callback should receive registered user data"); + + ps2_stubs::notifyMpegCdStreamEof(); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context afterEofDemuxCtx{}; + setRegU32(afterEofDemuxCtx, 4, kMpegAddr); + setRegU32(afterEofDemuxCtx, 5, kVideoPacketAddr); + setRegU32(afterEofDemuxCtx, 6, videoPacketSize); + setRegU32(afterEofDemuxCtx, 7, kVideoPacketAddr); + setRegU32(afterEofDemuxCtx, 8, videoPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &afterEofDemuxCtx, &runtime); + + t.Equals(getRegS32(afterEofDemuxCtx, 2), static_cast(videoPacketSize), + "post-EOF demux should continue consuming caller data"); + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 0u, + "post-EOF demux should not feed callbacks again"); + + R5900Context resetCtx{}; + setRegU32(resetCtx, 4, kMpegAddr); + ps2_stubs::sceMpegReset(rdram.data(), &resetCtx, &runtime); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context afterResetDemuxCtx{}; + setRegU32(afterResetDemuxCtx, 4, kMpegAddr); + setRegU32(afterResetDemuxCtx, 5, kVideoPacketAddr); + setRegU32(afterResetDemuxCtx, 6, videoPacketSize); + setRegU32(afterResetDemuxCtx, 7, kVideoPacketAddr); + setRegU32(afterResetDemuxCtx, 8, videoPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &afterResetDemuxCtx, &runtime); + + t.Equals(getRegS32(afterResetDemuxCtx, 2), static_cast(videoPacketSize), + "post-EOF reset demux should still drain caller data"); + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 0u, + "post-EOF reset demux should not restart callbacks on stale data"); + + ps2_stubs::notifyMpegCdStreamStart(); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context afterNewStreamDemuxCtx{}; + setRegU32(afterNewStreamDemuxCtx, 4, kMpegAddr); + setRegU32(afterNewStreamDemuxCtx, 5, kVideoPacketAddr); + setRegU32(afterNewStreamDemuxCtx, 6, videoPacketSize); + setRegU32(afterNewStreamDemuxCtx, 7, kVideoPacketAddr); + setRegU32(afterNewStreamDemuxCtx, 8, videoPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &afterNewStreamDemuxCtx, &runtime); + + t.Equals(getRegS32(afterNewStreamDemuxCtx, 2), static_cast(videoPacketSize), + "new CD stream demux should reopen an ended MPEG handle"); + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 1u, + "new CD stream demux should allow callbacks on a reused MPEG handle"); + + constexpr uint32_t kMpegWorkAddr = 0x00130000u; + R5900Context createCtx{}; + setRegU32(createCtx, 4, kMpegAddr); + setRegU32(createCtx, 5, kMpegWorkAddr); + setRegU32(createCtx, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtx, &runtime); + t.IsTrue(::getRegU32(&createCtx, 2) != 0u, + "sceMpegCreate should reopen the MPEG handle after an ended reset"); + + gMpegStreamCallbackCount.store(0u, std::memory_order_release); + R5900Context afterCreateDemuxCtx{}; + setRegU32(afterCreateDemuxCtx, 4, kMpegAddr); + setRegU32(afterCreateDemuxCtx, 5, kVideoPacketAddr); + setRegU32(afterCreateDemuxCtx, 6, videoPacketSize); + setRegU32(afterCreateDemuxCtx, 7, kVideoPacketAddr); + setRegU32(afterCreateDemuxCtx, 8, videoPacketSize); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &afterCreateDemuxCtx, &runtime); + + t.Equals(gMpegStreamCallbackCount.load(std::memory_order_acquire), 1u, + "new MPEG create should allow callbacks for the next stream"); + + runtime.requestStop(); + }); + + tc.Run("MPEG playback stays active during a temporary demux pause before CD EOF", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kPacketAddr = 0x00128000u; + constexpr uint32_t kImageAddr = 0x00130000u; + const std::vector es = { + 0x00u, 0x00u, 0x01u, 0xB3u, 0x01u, 0x00u, 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, + 0x00u, 0x00u, 0x01u, 0xB5u, 0x14u, 0x8Au, 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, + 0x01u, 0xB8u, 0x00u, 0x08u, 0x00u, 0x40u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, + 0xFFu, 0xF8u, 0x00u, 0x00u, 0x01u, 0xB5u, 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, + 0x00u, 0x01u, 0x01u, 0x13u, 0xF8u, 0x7Du, 0x29u, 0x48u, 0x88u, 0x00u, 0x00u, 0x01u, + 0xB3u, 0x01u, 0x00u, 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, 0x00u, 0x00u, 0x01u, + 0xB5u, 0x14u, 0x8Au, 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, 0x01u, 0xB8u, 0x00u, + 0x08u, 0x00u, 0xC0u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, 0xFFu, 0xF8u, 0x00u, + 0x00u, 0x01u, 0xB5u, 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, 0x00u, 0x01u, 0x01u, + 0x13u, 0xF8u, 0x7Du, 0x29u, 0x48u, 0x88u, 0x00u, 0x00u, 0x01u, 0xB3u, 0x01u, 0x00u, + 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, 0x00u, 0x00u, 0x01u, 0xB5u, 0x14u, 0x8Au, + 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, 0x01u, 0xB8u, 0x00u, 0x08u, 0x01u, 0x40u, + 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, 0xFFu, 0xF8u, 0x00u, 0x00u, 0x01u, 0xB5u, + 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, 0x00u, 0x01u, 0x01u, 0x13u, 0xF8u, 0x7Du, + 0x29u, 0x48u, 0x88u}; + + std::vector packet = { + 0x00u, 0x00u, 0x01u, 0xE0u, + 0x00u, static_cast(es.size() + 3u), + 0x80u, 0x00u, 0x00u}; + packet.insert(packet.end(), es.begin(), es.end()); + std::memcpy(rdram.data() + kPacketAddr, packet.data(), packet.size()); + + R5900Context demuxCtx{}; + setRegU32(demuxCtx, 4, kMpegAddr); + setRegU32(demuxCtx, 5, kPacketAddr); + setRegU32(demuxCtx, 6, static_cast(packet.size())); + setRegU32(demuxCtx, 7, kPacketAddr); + setRegU32(demuxCtx, 8, static_cast(packet.size())); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxCtx, nullptr); + + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 16u, + "test stream should decode one frame before the pause"); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x08u), 0u, + "first decoded frame should report frame index zero"); + + std::this_thread::sleep_for(std::chrono::milliseconds(650)); + + R5900Context isEndCtx{}; + setRegU32(isEndCtx, 4, kMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &isEndCtx, nullptr); + t.Equals(getRegS32(isEndCtx, 2), 0, + "a temporary demux pause must not end an active stream before CD EOF"); + + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x08u), 1u, + "temporary non-EOF starvation should keep movie frame progress moving"); + + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x08u), 2u, + "repeated temporary starvation should continue advancing from the held frame"); + }); + + tc.Run("sceMpegGetPicture releases an old waiter when the CD stream restarts", [](TestCase &t) + { + PS2Runtime runtime; + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + ps2_stubs::notifyMpegCdStreamStart(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kImageAddr = 0x00130000u; + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + + std::atomic returned{false}; + std::thread waiter([&]() + { + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, &runtime); + returned.store(true, std::memory_order_release); + }); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + t.IsFalse(returned.load(std::memory_order_acquire), + "sceMpegGetPicture should wait while the current stream still has no frame"); + + ps2_stubs::notifyMpegCdStreamStart(); + const bool released = waitUntil( + [&]() { return returned.load(std::memory_order_acquire); }, + std::chrono::milliseconds(30)); + + runtime.requestStop(); + waiter.join(); + t.IsTrue(released, + "a new sceCdStStart generation should release a waiter owned by the previous movie"); + }); + + tc.Run("sceMpegGetPicture yields during active stream starvation before CD EOF", [](TestCase &t) + { + PS2Runtime runtime; + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + ps2_stubs::notifyMpegCdStreamStart(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kPssAddr = 0x0012C000u; + constexpr uint32_t kImageAddr = 0x00130000u; + const uint8_t incompletePssStart[] = {0x00u, 0x00u, 0x01u}; + std::memcpy(rdram.data() + kPssAddr, incompletePssStart, sizeof(incompletePssStart)); + + R5900Context demuxCtx{}; + setRegU32(demuxCtx, 4, kMpegAddr); + setRegU32(demuxCtx, 5, kPssAddr); + setRegU32(demuxCtx, 6, sizeof(incompletePssStart)); + setRegU32(demuxCtx, 7, kPssAddr); + setRegU32(demuxCtx, 8, sizeof(incompletePssStart)); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxCtx, &runtime); + + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + + std::atomic returned{false}; + std::thread waiter([&]() + { + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, &runtime); + returned.store(true, std::memory_order_release); + }); + + const bool yielded = waitUntil( + [&]() { return returned.load(std::memory_order_acquire); }, + std::chrono::milliseconds(200)); + + runtime.requestStop(); + waiter.join(); + t.IsTrue(yielded, + "active non-EOF streams must return control when no decoded frame is currently available"); + + R5900Context isEndCtx{}; + setRegU32(isEndCtx, 4, kMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &isEndCtx, nullptr); + t.Equals(getRegS32(isEndCtx, 2), 0, + "yielding without a frame must not mark the active stream ended"); + }); + tc.Run("movie startup MPEG and audio stubs return safe progress values", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0u); - ps2_stubs::clearMpegCompatLayout(); ps2_stubs::resetMpegStubState(); ps2_stubs::resetAudioStubState(); @@ -546,6 +1026,78 @@ void register_ps2_runtime_expansion_tests() t.Equals(getRegS32(secondIsEndCtx, 2), 0, "sceMpegIsEnd should keep the decode thread alive and let the guest stop playback"); + constexpr uint32_t pssEndAddr = 0x00128000u; + constexpr uint32_t stackAddr = 0x00129000u; + const uint8_t programEnd[] = {0x00u, 0x00u, 0x01u, 0xB9u}; + std::memcpy(rdram.data() + pssEndAddr, programEnd, sizeof(programEnd)); + std::memcpy(rdram.data() + stackAddr + 0x10u, "\x04\x00\x00\x00", 4u); + + R5900Context endDemuxCtx{}; + setRegU32(endDemuxCtx, 29, stackAddr); + setRegU32(endDemuxCtx, 4, 0x00123000u); + setRegU32(endDemuxCtx, 5, pssEndAddr); + setRegU32(endDemuxCtx, 6, sizeof(programEnd)); + setRegU32(endDemuxCtx, 7, pssEndAddr); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &endDemuxCtx, nullptr); + + R5900Context endIsEndCtx{}; + setRegU32(endIsEndCtx, 4, 0x00123000u); + ps2_stubs::sceMpegIsEnd(rdram.data(), &endIsEndCtx, nullptr); + t.Equals(getRegS32(endIsEndCtx, 2), 1, + "sceMpegIsEnd should report end after a demuxed MPEG program end code"); + + ps2_stubs::resetMpegStubState(); + constexpr uint32_t wrappedEndBase = 0x0012A000u; + rdram[wrappedEndBase + 0u] = 0x00u; + rdram[wrappedEndBase + 1u] = 0x01u; + rdram[wrappedEndBase + 2u] = 0xB9u; + rdram[wrappedEndBase + 3u] = 0x00u; + + R5900Context wrappedEndDemuxCtx{}; + setRegU32(wrappedEndDemuxCtx, 4, 0x00123000u); + setRegU32(wrappedEndDemuxCtx, 5, wrappedEndBase + 3u); + setRegU32(wrappedEndDemuxCtx, 6, 4u); + setRegU32(wrappedEndDemuxCtx, 7, wrappedEndBase); + setRegU32(wrappedEndDemuxCtx, 8, 4u); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &wrappedEndDemuxCtx, nullptr); + + R5900Context wrappedEndIsEndCtx{}; + setRegU32(wrappedEndIsEndCtx, 4, 0x00123000u); + ps2_stubs::sceMpegIsEnd(rdram.data(), &wrappedEndIsEndCtx, nullptr); + t.Equals(getRegS32(wrappedEndIsEndCtx, 2), 1, + "sceMpegDemuxPssRing should use the ABI fifth argument in t0 for wrapped rings"); + + ps2_stubs::resetMpegStubState(); + constexpr uint32_t eofMpegAddr = 0x0012B000u; + constexpr uint32_t eofPssAddr = 0x0012C000u; + const uint8_t incompletePssStart[] = {0x00u, 0x00u, 0x01u}; + std::memcpy(rdram.data() + eofPssAddr, incompletePssStart, sizeof(incompletePssStart)); + + R5900Context eofDemuxCtx{}; + setRegU32(eofDemuxCtx, 4, eofMpegAddr); + setRegU32(eofDemuxCtx, 5, eofPssAddr); + setRegU32(eofDemuxCtx, 6, sizeof(incompletePssStart)); + setRegU32(eofDemuxCtx, 7, eofPssAddr); + setRegU32(eofDemuxCtx, 8, sizeof(incompletePssStart)); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &eofDemuxCtx, nullptr); + t.Equals(getRegS32(eofDemuxCtx, 2), static_cast(sizeof(incompletePssStart)), + "sceMpegDemuxPssRing should accept partial trailing stream data"); + + R5900Context eofBeforeStopCtx{}; + setRegU32(eofBeforeStopCtx, 4, eofMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &eofBeforeStopCtx, nullptr); + t.Equals(getRegS32(eofBeforeStopCtx, 2), 0, + "sceMpegIsEnd should not report end until the CD stream terminates"); + + R5900Context cdStopCtx{}; + ps2_stubs::sceCdStStop(rdram.data(), &cdStopCtx, nullptr); + + R5900Context eofAfterStopCtx{}; + setRegU32(eofAfterStopCtx, 4, eofMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &eofAfterStopCtx, nullptr); + t.Equals(getRegS32(eofAfterStopCtx, 2), 1, + "sceCdStStop should finalize active MPEG playback so movie threads can advance"); + R5900Context remoteInitCtx{}; ps2_stubs::sceSdRemoteInit(rdram.data(), &remoteInitCtx, nullptr); t.Equals(getRegS32(remoteInitCtx, 2), 0, @@ -562,8 +1114,8 @@ void register_ps2_runtime_expansion_tests() std::memcpy(rdram.data() + blockTransSp + 0x14u, "\x00\x30\x00\x00", 4u); std::memcpy(rdram.data() + blockTransSp + 0x18u, "\x40\x27\x01\x00", 4u); ps2_stubs::sceSdRemote(rdram.data(), &blockTransCtx, nullptr); - t.Equals(getRegU32(&blockTransCtx, 2), 0x00012340u, - "sceSdRemote block transfer should publish the current IOP ring position"); + t.Equals(getRegU32(&blockTransCtx, 2), 0x00012740u, + "sceSdRemote block transfer should resume from the configured IOP pause position"); R5900Context statusCtx{}; setRegU32(statusCtx, 29, blockTransSp); @@ -573,8 +1125,15 @@ void register_ps2_runtime_expansion_tests() setRegU32(statusCtx, 7, 0u); std::memset(rdram.data() + blockTransSp + 0x10u, 0, 12u); ps2_stubs::sceSdRemote(rdram.data(), &statusCtx, nullptr); - t.Equals(getRegU32(&statusCtx, 2), 0x00012340u, - "sceSdRemote status polling should reuse the last configured transfer base"); + t.Equals(getRegU32(&statusCtx, 2), 0x00012B40u, + "sceSdRemote status polling should advance the emulated SPU transfer head"); + + for (uint32_t i = 0u; i < 11u; ++i) + { + ps2_stubs::sceSdRemote(rdram.data(), &statusCtx, nullptr); + } + t.Equals(getRegU32(&statusCtx, 2), 0x00012740u, + "sceSdRemote status polling should wrap inside the configured IOP ring"); R5900Context setParamCtx{}; setRegU32(setParamCtx, 29, blockTransSp); @@ -583,7 +1142,7 @@ void register_ps2_runtime_expansion_tests() setRegU32(setParamCtx, 6, 0x0F81u); setRegU32(setParamCtx, 7, 0u); ps2_stubs::sceSdRemote(rdram.data(), &setParamCtx, nullptr); - t.Equals(getRegU32(&setParamCtx, 2), 0x00012340u, + t.Equals(getRegU32(&setParamCtx, 2), 0x00012740u, "sceSdRemote set-param calls should not trap or disturb the movie audio state"); }); @@ -729,17 +1288,20 @@ void register_ps2_runtime_expansion_tests() mem.vif1_regs.base = 4u; mem.vif1_regs.ofst = 2u; - mem.vif1_regs.itop = 0x21u; + mem.vif1_regs.tops = 4u; + mem.vif1_regs.itops = 0x21u; mem.vif1_regs.stat &= ~(1u << 7); // DBF = 0 uint32_t callbackPc = 0xFFFFFFFFu; + uint32_t callbackTop = 0xFFFFFFFFu; uint32_t callbackItop = 0xFFFFFFFFu; uint32_t callbackCount = 0u; - mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t itop) + mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) { callbackPc = startPC; + callbackTop = top; callbackItop = itop; - callbackCount++; + ++callbackCount; }); const uint32_t mscal = makeVifCmd(0x14u, 0u, 3u); // start PC = 3 * 8 @@ -747,8 +1309,10 @@ void register_ps2_runtime_expansion_tests() t.Equals(callbackCount, 1u, "MSCAL should invoke VU1 callback exactly once"); t.Equals(callbackPc, 24u, "MSCAL should pass startPC=imm*8"); - t.Equals(callbackItop, 0x21u, "MSCAL callback should receive current ITOP"); - t.Equals(mem.vif1_regs.itops, 0x21u, "MSCAL should latch ITOPS from ITOP"); + t.Equals(callbackTop, 4u, "MSCAL callback should receive current TOPS"); + t.Equals(callbackItop, 0x21u, "MSCAL callback should receive pending ITOPS"); + t.Equals(mem.vif1_regs.top, 4u, "MSCAL should latch TOP from TOPS"); + t.Equals(mem.vif1_regs.itop, 0x21u, "MSCAL should latch ITOP from ITOPS"); t.IsTrue((mem.vif1_regs.stat & (1u << 7)) != 0u, "MSCAL should toggle DBF on"); t.Equals(mem.vif1_regs.tops, 6u, "DBF=1 should make TOPS=BASE+OFST"); @@ -758,7 +1322,44 @@ void register_ps2_runtime_expansion_tests() t.Equals(callbackCount, 1u, "MSCNT should not invoke MSCAL callback"); t.IsTrue((mem.vif1_regs.stat & (1u << 7)) == 0u, "MSCNT should toggle DBF back off"); t.Equals(mem.vif1_regs.tops, 4u, "DBF=0 should make TOPS=BASE"); - t.Equals(mem.vif1_regs.itops, 0x21u, "MSCNT should refresh ITOPS from ITOP"); + t.Equals(mem.vif1_regs.top, 6u, "MSCNT should latch TOP from current TOPS before toggling"); + t.Equals(mem.vif1_regs.itop, 0x21u, "MSCNT should keep latching ITOP from ITOPS"); + }); + + tc.Run("VU0 microprogram executes against VU0 code and data memory", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); + t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind"); + + uint8_t *const code = runtime.memory().getVU0Code(); + uint8_t *const data = runtime.memory().getVU0Data(); + std::memset(code, 0, PS2_VU0_CODE_SIZE); + std::memset(data, 0, PS2_VU0_DATA_SIZE); + + const float input[4] = {1.0f, 2.0f, 3.0f, 4.0f}; + std::memcpy(data, input, sizeof(input)); + + constexpr uint32_t kVuNop = 0x0000003Fu; + constexpr uint32_t kVuEndNop = 0x4000003Fu; + writeVuInstructionPair(code, 0u, makeVuLq(0xFu, 1u, 0u, 0), kVuNop); + writeVuInstructionPair(code, 8u, 0u, makeVuAdd(0xFu, 2u, 1u, 1u)); + writeVuInstructionPair(code, 16u, makeVuSq(0xFu, 2u, 0u, 1), kVuEndNop); + + R5900Context ctx; + runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + + float output[4]{}; + std::memcpy(output, data + 16u, sizeof(output)); + t.Equals(output[0], 2.0f, "VU0 output x should be doubled"); + t.Equals(output[1], 4.0f, "VU0 output y should be doubled"); + t.Equals(output[2], 6.0f, "VU0 output z should be doubled"); + t.Equals(output[3], 8.0f, "VU0 output w should be doubled"); + + alignas(16) float vf2[4]{}; + _mm_storeu_ps(vf2, ctx.vu0_vf[2]); + t.Equals(vf2[0], 2.0f, "VU0 VF2.x should copy back to CPU context"); + t.Equals(static_cast(ctx.vi[0]), 0u, "VU0 VI0 should remain zero"); }); tc.Run("GS sprite draw applies XYOFFSET and fully-outside scissor should not render", [](TestCase &t) diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index 0dd9c6a..1138499 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -1,6 +1,7 @@ #include "MiniTest.h" #include "ps2_runtime.h" #include "ps2_syscalls.h" +#include "Stubs/DMA.h" #include #include @@ -47,6 +48,9 @@ namespace std::atomic g_vblankStartHits{0u}; std::atomic g_vblankEndHits{0u}; std::atomic g_lastIntcArg{0u}; + std::atomic g_dmacSendHits{0u}; + std::atomic g_dmacSendLastCause{0u}; + std::atomic g_dmacSendLastChcr{0u}; void setRegU32(R5900Context &ctx, int reg, uint32_t value) { @@ -68,6 +72,25 @@ namespace std::memcpy(rdram + addr, &value, sizeof(value)); } + void writeGuestU64(uint8_t *rdram, uint32_t addr, uint64_t value) + { + std::memcpy(rdram + addr, &value, sizeof(value)); + } + + uint64_t makeDmaTag(uint16_t qwc, uint8_t id, uint32_t addr, bool irq = false) + { + return static_cast(qwc) | + (static_cast(id & 0x7u) << 28) | + (irq ? (1ull << 31) : 0ull) | + (static_cast(addr & 0x7FFFFFFFu) << 32); + } + + void writeDmaTag(uint8_t *rdram, uint32_t tagAddr, uint64_t tagLo) + { + std::memset(rdram + tagAddr, 0, 16); + std::memcpy(rdram + tagAddr, &tagLo, sizeof(tagLo)); + } + uint32_t readGuestU32(const uint8_t *rdram, uint32_t addr) { uint32_t value = 0; @@ -123,6 +146,36 @@ namespace ctx->pc = 0u; } + + void testDmacSendHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + (void)rdram; + + const uint32_t cause = getRegU32(ctx, 4); + g_dmacSendHits.fetch_add(1u, std::memory_order_relaxed); + g_dmacSendLastCause.store(cause, std::memory_order_relaxed); + + uint32_t channelBase = 0u; + if (cause == 0u) + { + channelBase = 0x10008000u; + } + else if (cause == 1u) + { + channelBase = 0x10009000u; + } + else if (cause == 2u) + { + channelBase = 0x1000A000u; + } + + if (runtime && channelBase != 0u) + { + g_dmacSendLastChcr.store(runtime->memory().readIORegister(channelBase + 0x00u), std::memory_order_relaxed); + } + + ctx->pc = 0u; + } } void register_ps2_runtime_interrupt_tests() @@ -167,6 +220,39 @@ void register_ps2_runtime_interrupt_tests() cleanupRuntime(env); }); + tc.Run("VSync worker updates GS CSR FIELD bit for MMIO polling loops", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed"); + + constexpr uint32_t kFlagAddr = 0x1080u; + constexpr uint32_t kTickAddr = 0x1090u; + constexpr uint64_t kGsCsrFieldMask = 0x2000ull; + + env.runtime.memory().gs().csr = 0x3ull; + + R5900Context ctx{}; + setRegU32(ctx, 4, kFlagAddr); + setRegU32(ctx, 5, kTickAddr); + t.IsTrue(callSyscall(0x73u, env.rdram.data(), &ctx, &env.runtime), "SetVSyncFlag syscall should dispatch"); + + const uint64_t initialField = env.runtime.memory().gs().csr & kGsCsrFieldMask; + const bool firstFieldFlip = waitUntil([&]() { + return (env.runtime.memory().gs().csr & kGsCsrFieldMask) != initialField; + }, std::chrono::milliseconds(300)); + t.IsTrue(firstFieldFlip, "VSync worker should toggle GS CSR FIELD for direct CSR polling"); + t.Equals(env.runtime.memory().gs().csr & 0x3ull, 0x3ull, "VSync FIELD update should preserve CSR status bits"); + + const uint64_t fieldAfterFirstFlip = env.runtime.memory().gs().csr & kGsCsrFieldMask; + const bool secondFieldFlip = waitUntil([&]() { + return (env.runtime.memory().gs().csr & kGsCsrFieldMask) != fieldAfterFirstFlip; + }, std::chrono::milliseconds(300)); + t.IsTrue(secondFieldFlip, "VSync worker should keep alternating GS CSR FIELD"); + + cleanupRuntime(env); + }); + tc.Run("INTC VBLANK handlers respect EnableIntc and DisableIntc masks", [](TestCase &t) { notifyRuntimeStop(); @@ -251,6 +337,102 @@ void register_ps2_runtime_interrupt_tests() cleanupRuntime(env); }); + tc.Run("sceDmaSend dispatches completed VIF1 DMAC handler with latched END tag", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed"); + + constexpr uint32_t kHandlerAddr = 0x00ABD100u; + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag0 = 0x00028000u; + constexpr uint32_t kTag1 = kTag0 + 0x20u; + + uint8_t *rdram = env.runtime.memory().getRDRAM(); + writeDmaTag(rdram, kTag0, makeDmaTag(1u, 1u, 0u, false)); // CNT + writeGuestU64(rdram, kTag0 + 0x10u, 0u); + writeGuestU64(rdram, kTag0 + 0x18u, 0u); + writeDmaTag(rdram, kTag1, makeDmaTag(0u, 7u, 0u, false)); // END + + g_dmacSendHits.store(0u, std::memory_order_relaxed); + g_dmacSendLastCause.store(0u, std::memory_order_relaxed); + g_dmacSendLastChcr.store(0u, std::memory_order_relaxed); + env.runtime.registerFunction(kHandlerAddr, &testDmacSendHandler); + + R5900Context addCtx{}; + setRegU32(addCtx, 4, 1u); + setRegU32(addCtx, 5, kHandlerAddr); + setRegU32(addCtx, 6, 0u); + setRegU32(addCtx, 7, 0u); + ps2_syscalls::AddDmacHandler(rdram, &addCtx, &env.runtime); + t.IsTrue(getRegS32(addCtx, 2) > 0, "AddDmacHandler should register VIF1 handler"); + + R5900Context enableCtx{}; + setRegU32(enableCtx, 4, 1u); + ps2_syscalls::EnableDmac(rdram, &enableCtx, &env.runtime); + t.Equals(getRegS32(enableCtx, 2), KE_OK, "EnableDmac should enable VIF1 cause"); + + R5900Context sendCtx{}; + setRegU32(sendCtx, 4, kVif1Ch); + setRegU32(sendCtx, 5, kTag0); + ps2_stubs::sceDmaSend(rdram, &sendCtx, &env.runtime); + + t.Equals(getRegS32(sendCtx, 2), 0, "sceDmaSend should succeed"); + t.Equals(g_dmacSendHits.load(std::memory_order_relaxed), 1u, "sceDmaSend should dispatch the VIF1 DMAC handler"); + t.Equals(g_dmacSendLastCause.load(std::memory_order_relaxed), 1u, "DMAC handler should observe VIF1 cause"); + t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x100u, 0u, "handler should see VIF1 STR cleared"); + t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x70000000u, 0x70000000u, "handler should see the latched END tag id"); + + cleanupRuntime(env); + }); + + tc.Run("MMIO VIF1 chain completion dispatches DMAC handler after CHCR store", [](TestCase &t) + { + notifyRuntimeStop(); + TestEnv env; + t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed"); + + constexpr uint32_t kHandlerAddr = 0x00ABD180u; + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag0 = 0x00028200u; + constexpr uint32_t kTag1 = kTag0 + 0x20u; + + uint8_t *rdram = env.runtime.memory().getRDRAM(); + writeDmaTag(rdram, kTag0, makeDmaTag(1u, 1u, 0u, false)); // CNT + writeGuestU64(rdram, kTag0 + 0x10u, 0u); + writeGuestU64(rdram, kTag0 + 0x18u, 0u); + writeDmaTag(rdram, kTag1, makeDmaTag(0u, 7u, 0u, false)); // END + + g_dmacSendHits.store(0u, std::memory_order_relaxed); + g_dmacSendLastCause.store(0u, std::memory_order_relaxed); + g_dmacSendLastChcr.store(0u, std::memory_order_relaxed); + env.runtime.registerFunction(kHandlerAddr, &testDmacSendHandler); + + R5900Context addCtx{}; + setRegU32(addCtx, 4, 1u); + setRegU32(addCtx, 5, kHandlerAddr); + setRegU32(addCtx, 6, 0u); + setRegU32(addCtx, 7, 0u); + ps2_syscalls::AddDmacHandler(rdram, &addCtx, &env.runtime); + t.IsTrue(getRegS32(addCtx, 2) > 0, "AddDmacHandler should register VIF1 handler"); + + R5900Context enableCtx{}; + setRegU32(enableCtx, 4, 1u); + ps2_syscalls::EnableDmac(rdram, &enableCtx, &env.runtime); + t.Equals(getRegS32(enableCtx, 2), KE_OK, "EnableDmac should enable VIF1 cause"); + + R5900Context storeCtx{}; + env.runtime.Store32(rdram, &storeCtx, kVif1Ch + 0x30u, kTag0); + env.runtime.Store32(rdram, &storeCtx, kVif1Ch + 0x00u, 0x185u); + + t.Equals(g_dmacSendHits.load(std::memory_order_relaxed), 1u, "CHCR store should dispatch the VIF1 DMAC handler"); + t.Equals(g_dmacSendLastCause.load(std::memory_order_relaxed), 1u, "DMAC handler should observe VIF1 cause"); + t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x100u, 0u, "handler should see VIF1 STR cleared"); + t.Equals(g_dmacSendLastChcr.load(std::memory_order_relaxed) & 0x70000000u, 0x70000000u, "handler should see the latched END tag id"); + + cleanupRuntime(env); + }); + tc.Run("negative interrupt-safe EE syscall ids dispatch", [](TestCase &t) { notifyRuntimeStop(); diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 6ae31a2..0b75c06 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -43,13 +44,16 @@ namespace constexpr int KE_RELEASE_WAIT = -418; constexpr uint32_t K_SEMA_WAIT_READY_ADDR = 0x1900u; + constexpr int THS_WAIT = 0x04; constexpr int THS_SUSPEND = 0x08; constexpr int THS_WAITSUSPEND = 0x0C; constexpr int THS_DORMANT = 0x10; + constexpr uint32_t TSW_SEMA = 2u; constexpr uint32_t TSW_EVENT = 3u; constexpr uint32_t K_EVENT_WAIT_READY_ADDR = 0x1800u; constexpr uint32_t K_EVENT_WAIT_GATE_ADDR = 0x1804u; + constexpr uint32_t K_TERMINATE_SEMA_WAIT_READY_ADDR = 0x1810u; struct EeThreadStatus { @@ -212,6 +216,18 @@ namespace ctx->pc = 0u; } + void waitSemaUntilTerminatedHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + if (!rdram || !ctx) + { + return; + } + + writeGuestU32(rdram, K_TERMINATE_SEMA_WAIT_READY_ADDR, 1u); + WaitSema(rdram, ctx, runtime); + ctx->pc = 0u; + } + void alarmNoopHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) { ctx->pc = 0u; @@ -871,7 +887,106 @@ void register_ps2_runtime_kernel_tests() t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteThread should clean up the waiter thread"); }); - tc.Run("setup heap configures limits and EndOfHeap reports the limit", [](TestCase &t) + tc.Run("TerminateThread unwinds semaphore wait as a normal thread exit", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kWaitThreadEntry = 0x00261000u; + const uint32_t semaParam[6] = { + 0u, + 1u, + 0u, + 0u, + 0u, + 0u + }; + writeGuestWords(env.rdram.data(), K_PARAM_ADDR, semaParam, std::size(semaParam)); + + R5900Context createSemaCtx{}; + setRegU32(createSemaCtx, 4, K_PARAM_ADDR); + CreateSema(env.rdram.data(), &createSemaCtx, &env.runtime); + const int32_t sid = getRegS32(createSemaCtx, 2); + t.IsTrue(sid > 0, "CreateSema should create a zero-count semaphore"); + + env.runtime.registerFunction(kWaitThreadEntry, &waitSemaUntilTerminatedHandler); + + const uint32_t threadParam[7] = { + 0u, + kWaitThreadEntry, + 0x00312000u, + 0x00000800u, + 0x00120000u, + 6u, + 0u + }; + + writeGuestU32(env.rdram.data(), K_TERMINATE_SEMA_WAIT_READY_ADDR, 0u); + writeGuestWords(env.rdram.data(), K_PARAM_ADDR, threadParam, std::size(threadParam)); + setRegU32(env.ctx, 4, K_PARAM_ADDR); + CreateThread(env.rdram.data(), &env.ctx, &env.runtime); + const int32_t tid = getRegS32(env.ctx, 2); + t.IsTrue(tid >= 2, "CreateThread should return a valid semaphore waiter thread id"); + + std::ostringstream capturedErr; + std::streambuf *oldErr = std::cerr.rdbuf(capturedErr.rdbuf()); + + setRegU32(env.ctx, 4, static_cast(tid)); + setRegU32(env.ctx, 5, static_cast(sid)); + StartThread(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "StartThread should launch the semaphore waiter"); + + const bool waiting = waitUntil([&]() + { + if (readGuestU32(env.rdram.data(), K_TERMINATE_SEMA_WAIT_READY_ADDR) != 1u) + { + return false; + } + + R5900Context statusCtx{}; + setRegU32(statusCtx, 4, static_cast(tid)); + setRegU32(statusCtx, 5, K_STATUS_ADDR); + ReferThreadStatus(env.rdram.data(), &statusCtx, &env.runtime); + if (getRegS32(statusCtx, 2) != KE_OK) + { + return false; + } + + EeThreadStatus status{}; + std::memcpy(&status, env.rdram.data() + K_STATUS_ADDR, sizeof(status)); + return status.status == THS_WAIT && status.waitType == TSW_SEMA; + }, std::chrono::milliseconds(200)); + t.IsTrue(waiting, "worker should block inside WaitSema before termination"); + + R5900Context terminateCtx{}; + setRegU32(terminateCtx, 4, static_cast(tid)); + TerminateThread(env.rdram.data(), &terminateCtx, &env.runtime); + t.Equals(getRegS32(terminateCtx, 2), KE_OK, "TerminateThread should join the semaphore waiter"); + + std::cerr.rdbuf(oldErr); + const std::string errText = capturedErr.str(); + t.IsTrue(errText.find("PS2 Thread Exit") == std::string::npos, + "thread-exit exceptions from Sync.cpp should be caught as normal exits"); + + R5900Context dormantCtx{}; + setRegU32(dormantCtx, 4, static_cast(tid)); + setRegU32(dormantCtx, 5, K_STATUS_ADDR); + ReferThreadStatus(env.rdram.data(), &dormantCtx, &env.runtime); + t.Equals(getRegS32(dormantCtx, 2), KE_OK, "terminated waiter should still have readable status"); + + EeThreadStatus dormantStatus{}; + std::memcpy(&dormantStatus, env.rdram.data() + K_STATUS_ADDR, sizeof(dormantStatus)); + t.Equals(dormantStatus.status, THS_DORMANT, "terminated waiter should become dormant"); + + setRegU32(env.ctx, 4, static_cast(tid)); + DeleteThread(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteThread should clean up the terminated waiter"); + + setRegU32(env.ctx, 4, static_cast(sid)); + DeleteSema(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), sid, "DeleteSema should return sid while cleaning up the waiter semaphore"); + }); + + tc.Run("setup heap and allocator primitives track end-of-heap", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 7a573b7..391205b 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -439,6 +439,132 @@ void register_ps2_sif_dma_tests() "SJX DMA should copy the chunk payload into the emulated SJRMT ring"); }); + tc.Run("sceSifSetDma recognizes SJX DTX payloads from rotated EE work buffers", [](TestCase &t) + { + TestEnv env; + setRecvxDtxCompatLayout(); + + constexpr uint32_t kClientAddr = 0x00031000u; + constexpr uint32_t kDtxSid = 0x7D000000u; + constexpr uint32_t kRecvAddr = 0x00031100u; + constexpr uint32_t kSendAddr = 0x00031200u; + constexpr uint32_t kDescAddr = 0x00031300u; + constexpr uint32_t kRegisteredEeWorkAddr = 0x00031400u; + constexpr uint32_t kRegisteredIopWorkAddr = 0x00031800u; + constexpr uint32_t kAltEeWorkAddr = 0x00031C00u; + constexpr uint32_t kAltIopWorkAddr = 0x00032000u; + constexpr uint32_t kRingAddr = 0x00032400u; + constexpr uint32_t kChunkDataAddr = 0x00032500u; + constexpr uint32_t kRegisteredWorkLen = 0x100u; + constexpr uint32_t kAltWorkLen = 0x180u; + constexpr uint32_t kChunkLen = 12u; + + ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); + + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, kDtxSid); + setRegU32(env.ctx, 6, 0u); + ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid"); + + writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); + writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr); + writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredWorkLen); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 0x422u); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, kSendAddr); + setRegU32(env.ctx, 8, 12u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 4u); + setRegU32(env.ctx, 11, 0u); + ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr); + t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle"); + + writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); + writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle); + writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u); + writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0x87654321u); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 0x400u); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, kSendAddr); + setRegU32(env.ctx, 8, 16u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 4u); + setRegU32(env.ctx, 11, 0u); + ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr); + t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle"); + + writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); + writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRegisteredEeWorkAddr); + writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredIopWorkAddr); + writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kRegisteredWorkLen); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 2u); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, kSendAddr); + setRegU32(env.ctx, 8, 16u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 4u); + setRegU32(env.ctx, 11, 0u); + ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed"); + + std::memset(env.rdram.data() + kRegisteredEeWorkAddr, 0, kRegisteredWorkLen); + std::memset(env.rdram.data() + kRegisteredIopWorkAddr, 0, kRegisteredWorkLen); + std::memset(env.rdram.data() + kAltEeWorkAddr, 0, kAltWorkLen); + std::memset(env.rdram.data() + kAltIopWorkAddr, 0, kAltWorkLen); + std::memset(env.rdram.data() + kRingAddr, 0, kRegisteredWorkLen); + for (uint32_t i = 0; i < kChunkLen; ++i) + { + env.rdram[kChunkDataAddr + i] = static_cast(0xC0u + i); + } + + writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x00u, 1u); + env.rdram[kAltEeWorkAddr + 0x10u] = 0u; + env.rdram[kAltEeWorkAddr + 0x11u] = 1u; + std::memcpy(env.rdram.data() + kAltEeWorkAddr + 0x12u, "\0\0", 2u); + writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x14u, sjxHandle); + writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x18u, kChunkDataAddr); + writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x1Cu, kChunkLen); + writeGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t), 9u); + + const Ps2SifDmaTransfer desc{ + kAltEeWorkAddr, + kAltIopWorkAddr, + static_cast(kAltWorkLen), + 0}; + std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); + + setRegU32(env.ctx, 4, kDescAddr); + setRegU32(env.ctx, 5, 1u); + ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); + t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the rotated SJX transport"); + t.Equals(env.rdram[kAltEeWorkAddr + 0x11u], static_cast(0u), + "rotated SJX DMA ack should rewrite the response line to room"); + t.Equals(readGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t)), 10u, + "rotated SJX DMA ack should advance the alternate EE footer ticket"); + + writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle); + writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 0x429u); + setRegU32(env.ctx, 6, 0u); + setRegU32(env.ctx, 7, kSendAddr); + setRegU32(env.ctx, 8, 8u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 4u); + setRegU32(env.ctx, 11, 0u); + ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kChunkLen, + "rotated SJX DMA should make SJRMT report available data"); + t.IsTrue(std::memcmp(env.rdram.data() + kRingAddr, env.rdram.data() + kChunkDataAddr, kChunkLen) == 0, + "rotated SJX DMA should copy the chunk payload into the emulated SJRMT ring"); + }); + tc.Run("sceSifSetDma lets active PS2RNA playback drain emulated SJRMT data", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index 3aac013..bebafca 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -2,10 +2,15 @@ #include "ps2_runtime.h" #include "ps2_syscalls.h" #include "ps2_stubs.h" +#include "runtime/ps2_iop.h" #include +#include #include #include +#include +#include +#include #include using namespace ps2_syscalls; @@ -109,6 +114,16 @@ namespace ps2_syscalls::setDtxCompatLayout(layout); } + std::atomic g_lotrSoundCallbackHits{0u}; + + void lotrSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + (void)rdram; + (void)runtime; + ++g_lotrSoundCallbackHits; + ctx->pc = ::getRegU32(ctx, 31); + } + void setRegU32(R5900Context &ctx, int reg, uint32_t value) { ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); @@ -129,6 +144,31 @@ namespace std::memcpy(rdram + addr, &value, sizeof(value)); } + struct ScopedTempDir + { + std::filesystem::path path; + + explicit ScopedTempDir(const std::string &name) + { + path = std::filesystem::temp_directory_path() / + ("ps2recomp_" + name + "_" + std::to_string(reinterpret_cast(this))); + std::filesystem::remove_all(path); + std::filesystem::create_directories(path); + } + + ~ScopedTempDir() + { + std::error_code ec; + std::filesystem::remove_all(path, ec); + } + }; + + void writeFile(const std::filesystem::path &path, const std::vector &data) + { + std::ofstream out(path, std::ios::binary); + out.write(reinterpret_cast(data.data()), static_cast(data.size())); + } + template void writeGuestStruct(uint8_t *rdram, uint32_t addr, const T &value) { @@ -240,6 +280,263 @@ void register_ps2_sif_rpc_tests() t.Equals(getRegS32(env.ctx, 2), 0, "SifCheckStatRpc should report not busy after synchronous completion"); }); + tc.Run("Fatal Frame SDRDRV RPC initializes header and loads body archives", [](TestCase &t) + { + TestEnv env; + ScopedTempDir temp("fatal_frame_sdrdrv"); + + std::vector header(64u, 0); + const char headerPayload[] = "img-header"; + std::memcpy(header.data(), headerPayload, sizeof(headerPayload) - 1u); + writeFile(temp.path / "img_hd.bin", header); + + constexpr uint32_t kSectorSize = 2048u; + std::vector body(kSectorSize * 2u, 0); + const char bodyPayload[] = "archive-data"; + std::memcpy(body.data() + kSectorSize, bodyPayload, sizeof(bodyPayload) - 1u); + writeFile(temp.path / "img_bd.bin", body); + + const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths(); + PS2Runtime::IoPaths ioPaths; + ioPaths.elfDirectory = temp.path; + ioPaths.hostRoot = temp.path; + ioPaths.cdRoot = temp.path; + ioPaths.mcRoot = temp.path / "mc0"; + PS2Runtime::setIoPaths(ioPaths); + + constexpr uint32_t kSendAddr = 0x00030000u; + constexpr uint32_t kRecvAddr = 0x00031000u; + constexpr uint32_t kDstAddr = 0x00032000u; + constexpr uint32_t kImgHeaderAddr = 0x012F0000u; + constexpr uint32_t kLoadId = 3u; + + std::array commands{}; + commands[0] = 0x0Eu; + commands[2] = 1u; // lbn + commands[3] = sizeof(bodyPayload) - 1u; + commands[4] = kDstAddr; + commands[6] = kLoadId; + std::memcpy(env.rdram.data() + kSendAddr, commands.data(), commands.size() * sizeof(uint32_t)); + std::memset(env.rdram.data() + kRecvAddr, 0xCC, 0x180u); + + env.runtime.iop().init(env.rdram.data()); + uint32_t resultPtr = 0u; + bool signalNowait = true; + const bool initHandled = env.runtime.iop().handleRPC(&env.runtime, + IOP_SID_FATAL_FRAME_SDRDRV, + 0u, + 0u, + 0u, + kRecvAddr, + 0x180u, + resultPtr, + signalNowait); + t.IsTrue(initHandled, "Fatal Frame SDRDRV init RPC should be handled"); + t.Equals(std::memcmp(env.rdram.data() + kImgHeaderAddr, "img-header", 10), 0, + "SDRDRV init RPC should load img_hd.bin into the arrangement table"); + + signalNowait = true; + const bool handled = env.runtime.iop().handleRPC(&env.runtime, + IOP_SID_FATAL_FRAME_SDRDRV, + 1u, + kSendAddr, + static_cast(commands.size() * sizeof(uint32_t)), + kRecvAddr, + 0x180u, + resultPtr, + signalNowait); + + PS2Runtime::setIoPaths(oldPaths); + + t.IsTrue(handled, "Fatal Frame SDRDRV SID should be handled"); + t.Equals(resultPtr, kRecvAddr, "SDRDRV RPC should return recv buffer"); + t.IsFalse(signalNowait, "SDRDRV RPC should not request special nowait signaling"); + t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "archive-data", 12), 0, + "SDRDRV load command should copy bytes from img_bd.bin by LBN"); + t.Equals(env.rdram[kRecvAddr + 0x6Cu + (kLoadId * 8u)], static_cast(0), + "SDRDRV load status should report completed"); + }); + + tc.Run("LotR ClFile RPC opens reads and reports EOF", [](TestCase &t) + { + TestEnv env; + ScopedTempDir temp("lotr_clfile_rpc"); + + const std::vector payload = {'a', 'b', 'c'}; + const std::vector loadPayload = {'x', 'y', 'z', '!'}; + writeFile(temp.path / "boot.cfg", payload); + writeFile(temp.path / "load.bin", loadPayload); + + const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths(); + PS2Runtime::IoPaths ioPaths; + ioPaths.elfDirectory = temp.path; + ioPaths.hostRoot = temp.path; + ioPaths.cdRoot = temp.path; + ioPaths.mcRoot = temp.path / "mc0"; + PS2Runtime::setIoPaths(ioPaths); + + constexpr uint32_t kSendAddr = 0x00036000u; + constexpr uint32_t kRecvAddr = 0x00037000u; + constexpr uint32_t kDstAddr = 0x00038000u; + + env.runtime.iop().init(env.rdram.data()); + + auto callClFileRpc = [&](uint32_t rpcNum, uint32_t sendSize) { + uint32_t resultPtr = 0u; + bool signalNowait = true; + const bool handled = env.runtime.iop().handleRPC(&env.runtime, + IOP_SID_LOTR_CLFILE, + rpcNum, + kSendAddr, + sendSize, + kRecvAddr, + 0x40u, + resultPtr, + signalNowait); + t.IsTrue(handled, "LotR ClFile SID should be handled"); + t.Equals(resultPtr, kRecvAddr, "ClFile RPC should return recv buffer"); + t.IsFalse(signalNowait, "ClFile RPC should not request special nowait signaling"); + }; + + std::memset(env.rdram.data() + kSendAddr, 0, 0x100u); + std::memcpy(env.rdram.data() + kSendAddr, "boot.cfg", 9u); + std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u); + callClFileRpc(0x08u, 0x100u); + + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0u, + "open should report success status"); + const uint32_t handle = readGuestStruct(env.rdram.data(), kRecvAddr + 4u); + t.IsTrue(handle != 0u, "open should return a non-zero remote file handle"); + + std::memset(env.rdram.data() + kSendAddr, 0, 0x100u); + std::memcpy(env.rdram.data() + kSendAddr, "missing.cfg", 12u); + std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u); + callClFileRpc(0x08u, 0x100u); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 0u, + "missing file open should not manufacture a handle from path bytes"); + + constexpr uint32_t kLoadDstAddr = 0x00039000u; + std::memset(env.rdram.data() + kSendAddr, 0, 0x110u); + std::memcpy(env.rdram.data() + kSendAddr, "load.bin", 9u); + writeGuestU32(env.rdram.data(), kSendAddr + 0x100u, static_cast(loadPayload.size())); + writeGuestU32(env.rdram.data(), kSendAddr + 0x104u, kLoadDstAddr); + std::memset(env.rdram.data() + kLoadDstAddr, 0xCC, 0x20u); + callClFileRpc(0x01u, 0x110u); + + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 5u, + "direct load should report a queued load result"); + const uint32_t loadHandle = readGuestStruct(env.rdram.data(), kRecvAddr + 4u); + t.IsTrue(loadHandle >= 3u, "direct load should return a status handle usable by getStatus"); + t.Equals(std::memcmp(env.rdram.data() + kLoadDstAddr, loadPayload.data(), loadPayload.size()), 0, + "direct load should copy file bytes into the requested guest destination"); + + writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); + callClFileRpc(0x05u, 0u); + writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); + callClFileRpc(0x03u, sizeof(uint32_t)); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 7u, + "direct load getStatus should report completed"); + + writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); + callClFileRpc(0x06u, sizeof(uint32_t)); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), + static_cast(loadPayload.size()), + "direct load getSize should report the loaded host file size"); + + writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); + callClFileRpc(0x09u, sizeof(uint32_t)); + + if (handle != 0u) + { + std::array readPacket = {handle, 2u, kDstAddr, 0u}; + writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); + std::memset(env.rdram.data() + kDstAddr, 0, 8u); + callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 2u, + "first read should report actual byte count"); + t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "ab", 2), 0, + "first read should copy file bytes into guest destination"); + + writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); + std::memset(env.rdram.data() + kDstAddr, 0, 8u); + callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 1u, + "short read should report remaining byte count"); + t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "c", 1), 0, + "short read should copy remaining file byte"); + + writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); + std::memset(env.rdram.data() + kDstAddr, 0xCC, 8u); + callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 0u, + "EOF read should report zero bytes"); + + writeGuestU32(env.rdram.data(), kSendAddr, handle); + callClFileRpc(0x09u, sizeof(uint32_t)); + } + + PS2Runtime::setIoPaths(oldPaths); + }); + + tc.Run("LotR sound RPC completes HLE callback without invoking guest loop", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kClientAddr = 0x00039000u; + constexpr uint32_t kSemaParamAddr = 0x00039100u; + constexpr uint32_t kSendAddr = 0x0003A000u; + constexpr uint32_t kRecvAddr = 0x0003C000u; + constexpr uint32_t kEndFunc = 0x001FFD70u; + + PS2SoundDriverCompatLayout layout{}; + layout.completionCallbacks = {kEndFunc, 0u, 0u, 0u}; + ps2_syscalls::setSoundDriverCompatLayout(layout); + + env.runtime.registerFunction(kEndFunc, lotrSoundEndCallbackShouldNotRun); + g_lotrSoundCallbackHits = 0u; + + SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); + + const uint32_t semaParam[6] = {0u, 1u, 0u, 0u, 0u, 0u}; + std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam)); + setRegU32(env.ctx, 4, kSemaParamAddr); + CreateSema(env.rdram.data(), &env.ctx, &env.runtime); + const int32_t semaId = getRegS32(env.ctx, 2); + t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id"); + + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, IOP_SID_LOTR_SOUND); + setRegU32(env.ctx, 6, 0u); + SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for LotR sound SID"); + + writeGuestU32(env.rdram.data(), kSendAddr, 3u); + std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x2000u); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, 0u); + setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); + setRegU32(env.ctx, 7, kSendAddr); + setRegU32(env.ctx, 8, 0x2000u); + setRegU32(env.ctx, 9, kRecvAddr); + setRegU32(env.ctx, 10, 0x2000u); + setRegU32(env.ctx, 11, kEndFunc); + setRegU32(env.ctx, 29, K_STACK_ADDR); + writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); + SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); + + t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for LotR sound RPC"); + t.Equals(g_lotrSoundCallbackHits.load(), 0u, + "HLE-completed LotR sound callback should not invoke the guest callback"); + t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0u, + "LotR sound response should report no active stream records"); + t.IsTrue(readGuestStruct(env.rdram.data(), kRecvAddr + 4u) != 0u, + "LotR sound response should advance the IOP update counter"); + + setRegU32(env.ctx, 4, static_cast(semaId)); + PollSema(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), semaId, "LotR sound callback completion should signal the sema"); + }); + tc.Run("bind before register creates placeholder then remaps", [](TestCase &t) { TestEnv env;