diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4334394..eb363c8b 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 f55bfb20..abe5bf5f 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 00000000..0c5fbf94 --- /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 69c37102..857102a2 100644 --- a/ps2xRecomp/CMakeLists.txt +++ b/ps2xRecomp/CMakeLists.txt @@ -12,7 +12,7 @@ include(FetchContent) FetchContent_Declare( elfio GIT_REPOSITORY https://github.com/serge1/ELFIO.git - GIT_TAG 7d30a22fc5aac06adfe7887ae57f3701b6b5f913 + GIT_TAG Release_3.12 GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(elfio) @@ -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/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index ddaf5fa5..0e324391 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -161,6 +161,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 @@ -225,12 +347,14 @@ 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 ) +ps2x_stage_ffmpeg_runtime_dlls(ps2EntryRunner) + if(PS2X_IS_VITA) set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d PARENTAL_LEVEL=1") @@ -265,11 +389,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 00000000..4a452752 --- /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_log.h b/ps2xRuntime/include/ps2_log.h index ef32c8f7..f44c1c6c 100644 --- a/ps2xRuntime/include/ps2_log.h +++ b/ps2xRuntime/include/ps2_log.h @@ -11,7 +11,7 @@ #define PS2_AGRESSIVE_LOGS_ENABLED 0 #endif -#if defined(_DEBUG) +#if defined(PS2_RUNTIME_LOGS) || defined(AGRESSIVE_LOGS) #define RUNTIME_LOG(x) do { std::cout << x; } while (0) #else #define RUNTIME_LOG(x) do {} while(0) diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index e40d96b5..37fd528d 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -639,11 +639,13 @@ class PS2Runtime std::unordered_map m_functionTable; std::atomic m_stopRequested{false}; +public: // TODO remove this later std::atomic m_debugPc{0}; std::atomic m_debugRa{0}; std::atomic m_debugSp{0}; std::atomic m_debugGp{0}; +private: struct LoadedModule { diff --git a/ps2xRuntime/include/ps2_stubs.h b/ps2xRuntime/include/ps2_stubs.h index a99becab..4492ce1f 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 1549bbee..dc9464f9 100644 --- a/ps2xRuntime/include/runtime/ps2_gs_gpu.h +++ b/ps2xRuntime/include/runtime/ps2_gs_gpu.h @@ -236,6 +236,7 @@ class GS } bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const; void latchHostPresentationFrame(); + bool tryLatchHostPresentationFrame(); bool copyLatchedHostPresentationFrame(std::vector &outPixels, uint32_t &outWidth, uint32_t &outHeight, @@ -253,6 +254,7 @@ class GS void snapshotVRAM(); void writeRegisterPacked(uint8_t regDesc, uint64_t lo, uint64_t hi); void vertexKick(bool drawing); + void latchHostPresentationFrameUnlocked(); void processImageData(const uint8_t *data, uint32_t sizeBytes); void performLocalToLocalTransfer(); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/Audio.cpp index 19f55b2c..4b9001f2 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,46 @@ namespace ps2_stubs if (cmd == kLibSdCmdBlockTrans || cmd == kLibSdCmdBlockTransAlt) { - if (arg4 != 0u) + if (arg4 != 0u && arg5 != 0u) { g_audio_stub_state.currentBlockBase = arg4 & kAudioPositionMask; + 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 (arg5 != 0u) + else { - g_audio_stub_state.currentBlockSize = arg5; + g_audio_stub_state.blockTransferActive = false; } - if (arg6 != 0u) + 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) { - g_audio_stub_state.currentPauseBase = arg6 & kAudioPositionMask; + 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 +121,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 286886f3..318ff5a1 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -1,8 +1,14 @@ #include "Common.h" #include "CD.h" +#include "MPEG.h" namespace ps2_stubs { + namespace + { + uint32_t g_cdStReadTraceCount = 0u; + } + void sceCdRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t a0 = getRegU32(ctx, 4); // usually lbn @@ -434,6 +440,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 +455,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 +486,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 +552,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 +580,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 +611,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/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 2d1b2aaa..a5470c88 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -1618,6 +1618,14 @@ namespace ps2_stubs void sceGsSyncV(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + static uint32_t s_syncVLogCount = 0u; + if (s_syncVLogCount < 32u) + { + std::cerr << "[sceGsSyncV:enter] pc=0x" << std::hex << ctx->pc + << " ra=0x" << getRegU32(ctx, 31) << std::dec << std::endl; + ++s_syncVLogCount; + } + const uint64_t tick = ps2_syscalls::WaitForNextVSyncTick(rdram, runtime); if (g_gparam.interlace != 0u) { diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index 0b746cdb..e779fb94 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] diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index d06f5a6e..fee4f8f6 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -1,41 +1,510 @@ #include "Common.h" #include "MPEG.h" +extern "C" +{ +#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()); + } + + 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) + { + 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; + } + + 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; + 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 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 +512,1068 @@ 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) + { + 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) + { + 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) + void resetMpegStubState() { std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.compat = layout; + resetMpegStubStateUnlocked(); + g_mpeg_cv.notify_all(); } - void clearMpegCompatLayout() + void notifyMpegCdStreamStart() { std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.compat = {}; + ++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); + } + 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 resetMpegStubState() + void notifyMpegCdStreamEof() { std::lock_guard lock(g_mpeg_stub_mutex); - resetMpegStubStateUnlocked(); + 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) + { + 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 +1592,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 +1668,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 +1744,244 @@ 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) + { + 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) + { + 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) + { + 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) + { + 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; + } + + 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.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; + haveFrame = true; + if (g_mpeg_stub_state.pictureTraceCount < 32u) + { + 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 + { + 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 +1995,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 (!runtime && frameCount == 0u) + { + writeBlankMpegFrame(rdram, imageAddr, width, height); } - setReturnU32(ctx, 0u); + setReturnS32(ctx, 0); } void sceMpegGetPictureRAW8(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -341,34 +2023,49 @@ 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; + 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 +2074,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 +2106,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 a77dacc5..bd9a8d5a 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/Syscalls/Helpers/Runtime.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h index 76cb5583..17a78fea 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) { diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h index fac2c0e8..5f94b6de 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h @@ -22,6 +22,7 @@ struct ThreadInfo int wakeupCount = 0; int currentPriority = 0; int suspendCount = 0; + std::atomic currentPc{0}; std::mutex m; std::condition_variable cv; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp index e024063c..6a8ec67f 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 @@ -576,6 +603,8 @@ namespace ps2_syscalls return; } + int waitType = TSW_NONE; + int waitId = 0; { std::lock_guard lock(info->m); if (info->status == THS_DORMANT) @@ -583,10 +612,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) { @@ -1068,23 +1100,7 @@ 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); } diff --git a/ps2xRuntime/src/lib/game_overrides.cpp b/ps2xRuntime/src/lib/game_overrides.cpp index bc557902..15ebe4dc 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,6 @@ namespace ps2_syscalls::setDtxCompatLayout(layout); } - void applyRecvxMpegCompat(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); - } - 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); } diff --git a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp index b02ecae0..442df53d 100644 --- a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp @@ -649,7 +649,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; @@ -672,7 +677,7 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, const uint32_t srcOff = GSPSMCT32::addrPSMCT32(basePtr, fbwBlocks, srcX, srcY); if (srcOff + srcPixelBytes > m_vramSize) { - return false; + return failCopy(); } dstRow[x * 4u + 0u] = m_vram[srcOff + 0u]; @@ -696,7 +701,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]; @@ -724,7 +729,7 @@ bool GS::copyFrameToHostRgbaUnlocked(const GSFrameReg &frame, const uint32_t srcOff = addrPSMCT16Family(basePtr, fbwBlocks, frame.psm, srcX, srcY); if (srcOff + sizeof(uint16_t) > m_vramSize) { - return false; + return failCopy(); } uint16_t pixel = 0u; @@ -752,7 +757,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; @@ -769,13 +774,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(); @@ -1075,7 +1096,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; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 98348406..76c48f04 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -429,22 +429,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; + } + + if (!latchedThisCall && s_hasUploadedFrame) + { + outWidth = (s_lastWidth != 0u) ? s_lastWidth : FB_WIDTH; + outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT; + return; } - std::vector scratch; + 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 +473,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; } @@ -494,7 +514,7 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint continue; } - const uint32_t pixel = sampleHostFramePixel(scratch, width, height, probe.x, probe.y); + const uint32_t pixel = sampleHostFramePixel(s_scratch, width, height, probe.x, probe.y); std::cout << " host[" << probe.x << "," << probe.y << "]=0x" << std::hex << pixel << std::dec; } @@ -509,8 +529,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 +541,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() diff --git a/ps2xStudio/CMakeLists.txt b/ps2xStudio/CMakeLists.txt index 7d739a27..b1994306 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 a429123f..8f65a722 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/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index 7941743a..4c8d9021 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" @@ -217,6 +218,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() @@ -506,10 +538,192 @@ 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("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 +760,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, diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 7dbe6725..8bf93c83 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -26,13 +27,16 @@ namespace constexpr int KE_SEMA_ZERO = -419; constexpr int KE_SEMA_OVF = -420; + 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_SEMA_WAIT_READY_ADDR = 0x1810u; struct EeThreadStatus { @@ -195,6 +199,18 @@ namespace ctx->pc = 0u; } + void waitSemaUntilTerminatedHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + if (!rdram || !ctx) + { + return; + } + + writeGuestU32(rdram, K_SEMA_WAIT_READY_ADDR, 1u); + WaitSema(rdram, ctx, runtime); + ctx->pc = 0u; + } + void alarmNoopHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) { ctx->pc = 0u; @@ -558,6 +574,105 @@ void register_ps2_runtime_kernel_tests() t.Equals(getRegS32(env.ctx, 2), KE_OK, "DeleteThread should clean up the waiter thread"); }); + 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_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_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), KE_OK, "DeleteSema should clean up the waiter semaphore"); + }); + tc.Run("setup heap and allocator primitives track end-of-heap", [](TestCase &t) { TestEnv env;