Feature/random fixes platform support (#179)

* feat: implement memory card IOP

* feat: IOP trace

* feat: move IOP logic to ps2xIOP
refactor: small refactor on audio api on runtime

* feat: android support
feat: prevent race on GS
feat: a bit cleanup and reimplement on memory card

* feat: finish guest thread
feat: android build support
feat: vita build support with suspicious setup scripts

* feat: remove idea from track
This commit is contained in:
Ranieri
2026-07-22 02:37:53 -03:00
committed by GitHub
parent 1176609890
commit f3687c5ae6
40 changed files with 1133 additions and 146 deletions
+6 -1
View File
@@ -17,4 +17,9 @@ ps2xRuntime/include/ps2_recompiled_functions.h
ps2xRuntime/include/ps2_recompiled_stubs.h
ps2xRuntime/src/runner/ps2_recompiled_functions.cpp
ps2xRuntime/src/runner/register_functions.cpp
ps2xRuntime/output
ps2xRuntime/output
ps2xRuntime/src/runner
android/app/.cxx/
android/local.properties
android/.gradle/
.idea/
+8
View File
@@ -19,6 +19,14 @@ option(PS2X_BUILD_ANALYZER "Build ps2xAnalyzer" ON)
option(PS2X_BUILD_TEST "Build ps2xTest" ON)
option(PS2X_BUILD_STUDIO "Build ps2xStudio" ON)
if(ANDROID)
message(STATUS "Android target detected, building runtime only")
set(PS2X_BUILD_RECOMP OFF)
set(PS2X_BUILD_ANALYZER OFF)
set(PS2X_BUILD_TEST OFF)
set(PS2X_BUILD_STUDIO OFF)
endif()
set(PS2X_IS_ARM_TARGET OFF)
set(PS2X_IS_AARCH64_TARGET OFF)
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64")
+36
View File
@@ -0,0 +1,36 @@
# Android runner
## Requirements
- Android Studio (recommended) or a local Gradle 8.7+ / JDK 17 install
- Android SDK 34 + NDK (installed automatically by Android Studio on first sync)
- CMake 3.22.1 from the SDK (Android Studio installs it on demand)
## Building
Option A — Android Studio: open the `android/` folder and run the `app` configuration.
Option B — command line (no wrapper is committed; generate it once):
```sh
cd android
gradle wrapper --gradle-version 8.9
./gradlew assembleRelease
```
APK output: `android/app/build/outputs/apk/release/app-release.apk`.
## Running a game
Since there is no argv on Android, the guest ELF path comes from
`PS2X_DEFAULT_BOOT_ELF`, set via the `ps2xBootElf` Gradle property
(`android/gradle.properties`, or `-Pps2xBootElf=...` on the command line).
```sh
adb install app/build/outputs/apk/release/app-release.apk
adb shell mkdir -p /storage/emulated/0/Android/data/com.ps2x.runner/files
adb push "path/to/game/." /storage/emulated/0/Android/data/com.ps2x.runner/files/
```
Logs: raylib output goes to logcat (`adb logcat -s raylib`); runtime `std::cout`/`cerr`
output is not redirected to logcat yet.
+59
View File
@@ -0,0 +1,59 @@
plugins {
id 'com.android.application'
}
// Override with: gradlew assembleRelease -Pps2xBootElf=/absolute/path/on/device.elf
def ps2xBootElf = project.findProperty('ps2xBootElf') ?: '/storage/emulated/0/Android/data/com.ps2x.runner/files/game.elf'
android {
namespace 'com.ps2x.runner'
compileSdk 34
ndkVersion '28.2.13676358'
defaultConfig {
applicationId 'com.ps2x.runner'
minSdk 28
targetSdk 34
versionCode 1
versionName '0.1.0'
externalNativeBuild {
cmake {
arguments '-DPS2X_BUILD_RECOMP=OFF',
'-DPS2X_BUILD_ANALYZER=OFF',
'-DPS2X_BUILD_TEST=OFF',
'-DPS2X_BUILD_STUDIO=OFF',
'-DPS2X_ENABLE_SCCACHE=OFF',
'-DPS2X_RUNNER_UNITY_BUILD_BATCH_SIZE=32',
'-DANDROID_CPP_FEATURES=rtti exceptions',
"-DPS2X_DEFAULT_BOOT_ELF=${ps2xBootElf}"
targets 'ps2EntryRunner'
}
}
ndk {
abiFilters 'arm64-v8a', 'x86_64'
}
}
externalNativeBuild {
cmake {
path '../../CMakeLists.txt'
version '3.22.1'
}
}
buildTypes {
debug {
externalNativeBuild {
cmake {
arguments '-DCMAKE_BUILD_TYPE=RelWithDebInfo'
}
}
}
release {
minifyEnabled false
signingConfig signingConfigs.debug
}
}
}
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="PS2 Recomp"
android:hasCode="false"
android:isGame="true">
<activity
android:name="android.app.NativeActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:screenOrientation="landscape"
android:exported="true">
<!-- Runner shared library built by ps2xRuntime/CMakeLists.txt -->
<meta-data
android:name="android.app.lib_name"
android:value="ps2EntryRunner" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
+3
View File
@@ -0,0 +1,3 @@
plugins {
id 'com.android.application' version '8.6.1' apply false
}
+4
View File
@@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx4g
android.useAndroidX=true
# ps2xBootElf=/storage/emulated/0/Android/data/com.ps2x.runner/files/SLUS_201.84
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+17
View File
@@ -0,0 +1,17 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = 'PS2Recomp'
include ':app'
+1 -1
View File
@@ -49,7 +49,7 @@ namespace ps2x::iop
struct MemoryCardRequest
{
MemoryCardOperation operation = MemoryCardOperation::Init;
// The fifth argument is carried at sp + 16 by the EE ABI
// The fifth argument is carried in $t0 by the EE n32 ABI
std::array<uint32_t, 5> arguments{};
};
+111 -11
View File
@@ -6,7 +6,8 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(PS2X_ENABLE_RUNNER_UNITY_BUILD "Build ps2EntryRunner with CMake unity build" ON)
set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 8 CACHE STRING "Unity build batch size for ps2EntryRunner")
set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 32 CACHE STRING "Unity build batch size for ps2EntryRunner")
option(PS2X_ENABLE_RUNNER_PCH "Precompile the heavy runtime headers for ps2EntryRunner" ON)
option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON)
option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF)
@@ -39,6 +40,12 @@ if((CMAKE_C_COMPILER MATCHES "arm-vita-eabi") OR
set(PS2X_IS_VITA ON)
endif()
set(PS2X_IS_ANDROID OFF)
if(ANDROID)
set(PS2X_IS_ANDROID ON)
endif()
option(PS2X_VITA_CREATE_PACKAGE "Create Vita eboot/vpk targets" ON)
set(PS2X_VITA_APP_NAME "PS2 Retro X" CACHE STRING "Display name for the Vita bubble")
set(PS2X_VITA_TITLEID "RANJ00001" CACHE STRING "9-character Vita title id")
@@ -63,6 +70,10 @@ else()
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_GAMES OFF CACHE BOOL "" FORCE)
if(PS2X_IS_ANDROID)
set(PLATFORM "Android" CACHE STRING "" FORCE)
endif()
FetchContent_Declare(
raylib
GIT_REPOSITORY "https://github.com/raysan5/raylib.git"
@@ -71,6 +82,7 @@ else()
)
FetchContent_MakeAvailable(raylib)
if(NOT PS2X_IS_ANDROID)
FetchContent_Declare(
imgui
GIT_REPOSITORY https://github.com/ocornut/imgui.git
@@ -113,6 +125,7 @@ else()
)
target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}")
target_link_libraries(rlImGui PUBLIC raylib imgui)
endif() # NOT PS2X_IS_ANDROID
endif()
add_library(ps2_host_backend INTERFACE)
@@ -156,6 +169,8 @@ if(PS2X_IS_VITA)
libIMGEGL_stub_weak
libgpu_es4_ext_stub_weak
libGLESv2_stub_weak
libGL_stub_weak
libGLESv1_CM_stub_weak
)
set(PS2X_VITA_INCLUDE_DIRS
@@ -186,19 +201,21 @@ if(PS2X_IS_VITA)
function(ps2x_find_vita_library out_var library_name)
string(MAKE_C_IDENTIFIER "${library_name}" library_id)
find_library(${out_var}
set(cache_var "PS2X_VITA_LIB_${library_id}")
find_library(${cache_var}
NAMES "${library_name}" "${library_name}.a" "lib${library_name}.a"
PATHS "${PS2X_VITA_LIBRARY_DIR}"
NO_DEFAULT_PATH
)
if(NOT ${out_var})
if(NOT ${cache_var})
message(FATAL_ERROR
"Could not find Vita library '${library_name}' under ${PS2X_VITA_LIBRARY_DIR}. "
"Install Quenom/raylib-5.5-vita and its SDL2/PVR dependencies into VitaSDK first.")
endif()
set(${out_var} "${${out_var}}" PARENT_SCOPE)
set(${out_var} "${${cache_var}}" PARENT_SCOPE)
endfunction()
set(PS2X_VITA_RESOLVED_LIBRARIES "")
@@ -218,9 +235,24 @@ if(PS2X_IS_VITA)
)
else()
target_link_libraries(ps2_host_backend INTERFACE raylib)
if(PS2X_IS_ANDROID)
target_link_libraries(ps2_host_backend INTERFACE log android)
endif()
endif()
if(WIN32)
if(PS2X_IS_ANDROID)
set(PS2X_ENABLE_FFMPEG_DEFAULT OFF)
else()
set(PS2X_ENABLE_FFMPEG_DEFAULT ON)
endif()
option(PS2X_ENABLE_FFMPEG "Enable FFmpeg-backed MPEG video decoding" ${PS2X_ENABLE_FFMPEG_DEFAULT})
if(NOT PS2X_ENABLE_FFMPEG)
message(STATUS "FFmpeg disabled; MPEG video decode falls back to stub frames")
add_library(ffmpeg INTERFACE)
elseif(WIN32)
include(ExternalProject)
set(FFMPEG_PREBUILT_TAG "n7.1-241205")
@@ -390,6 +422,10 @@ if(PS2X_STRICT_RETURN_DIAGNOSTICS)
)
endif()
target_compile_definitions(ps2_runtime PRIVATE
PS2X_HAS_FFMPEG=$<BOOL:${PS2X_ENABLE_FFMPEG}>
)
file(GLOB_RECURSE KERNEL_SRC_FILES CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/lib/Kernel/*.cpp"
)
@@ -411,9 +447,17 @@ elseif(EXISTS "${ROOT_MAIN_CPP}")
list(APPEND RUNNER_SRC_FILES "${ROOT_MAIN_CPP}")
endif()
add_executable(ps2EntryRunner
${RUNNER_SRC_FILES}
)
if(PS2X_IS_ANDROID)
add_library(ps2EntryRunner SHARED
${RUNNER_SRC_FILES}
)
target_link_options(ps2EntryRunner PRIVATE "-Wl,--undefined=ANativeActivity_onCreate")
else()
add_executable(ps2EntryRunner
${RUNNER_SRC_FILES}
)
endif()
if(PS2X_ENABLE_RUNNER_UNITY_BUILD)
set_target_properties(ps2EntryRunner PROPERTIES
@@ -422,6 +466,28 @@ if(PS2X_ENABLE_RUNNER_UNITY_BUILD)
)
endif()
if(PS2X_ENABLE_RUNNER_PCH)
set(PS2X_RUNNER_PCH_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_runtime_macros.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_runtime.h"
)
foreach(PS2X_RUNNER_GENERATED_HEADER IN ITEMS
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_recompiled_functions.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_recompiled_stubs.h")
if(EXISTS "${PS2X_RUNNER_GENERATED_HEADER}")
list(APPEND PS2X_RUNNER_PCH_HEADERS "${PS2X_RUNNER_GENERATED_HEADER}")
endif()
endforeach()
list(APPEND PS2X_RUNNER_PCH_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_syscalls.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/ps2_stubs.h"
)
target_precompile_headers(ps2EntryRunner PRIVATE ${PS2X_RUNNER_PCH_HEADERS})
endif()
if(PS2X_DEFAULT_BOOT_ELF_DEFINE)
target_compile_definitions(ps2EntryRunner PRIVATE
PS2X_DEFAULT_BOOT_ELF="${PS2X_DEFAULT_BOOT_ELF_DEFINE}"
@@ -443,7 +509,7 @@ target_link_libraries(ps2EntryRunner
ps2_runtime
)
if(NOT PS2X_IS_VITA)
if(NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID)
target_sources(ps2EntryRunner PRIVATE
src/lib/ps2_debug_panel.cpp
)
@@ -454,13 +520,33 @@ endif()
ps2x_stage_ffmpeg_runtime_dlls(ps2EntryRunner)
if(PS2X_IS_VITA)
set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d PARENTAL_LEVEL=1")
set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d PARENTAL_LEVEL=1")
set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d ATTRIBUTE2=12")
if(PS2X_VITA_CREATE_PACKAGE)
set(PS2X_VITA_VPK_FILES "")
foreach(PS2X_VITA_MODULE IN ITEMS
libgpu_es4_ext.suprx
libIMGEGL.suprx
libGLESv2.suprx
libpvrPSP2_WSEGL.suprx
libGL.suprx)
set(PS2X_VITA_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/vita/module/${PS2X_VITA_MODULE}")
if(EXISTS "${PS2X_VITA_MODULE_PATH}")
list(APPEND PS2X_VITA_VPK_FILES FILE "${PS2X_VITA_MODULE_PATH}" "module/${PS2X_VITA_MODULE}")
else()
message(WARNING
"Missing ${PS2X_VITA_MODULE_PATH}; the vpk will rely on ur0:data/external/ for it")
endif()
endforeach()
vita_create_self(eboot.bin ps2EntryRunner UNSAFE)
vita_create_vpk(ps2EntryRunner.vpk ${PS2X_VITA_TITLEID} eboot.bin
VERSION ${PS2X_VITA_VERSION}
NAME ${PS2X_VITA_APP_NAME}
${PS2X_VITA_VPK_FILES}
)
endif()
endif()
@@ -488,13 +574,27 @@ if(MSVC)
target_link_options(ps2EntryRunner PRIVATE "/FORCE:MULTIPLE")
endif()
# Vita: Work around for GCC 10's LTO plugin
if(PS2X_IS_VITA)
target_link_options(ps2EntryRunner PRIVATE "-Wl,--allow-multiple-definition")
target_sources(ps2EntryRunner PRIVATE src/lib/ps2_vita_runtime.cpp)
set_source_files_properties(src/lib/ps2_vita_runtime.cpp PROPERTIES
SKIP_UNITY_BUILD_INCLUSION TRUE
COMPILE_OPTIONS "-fno-lto"
)
endif()
if(PS2X_IS_ANDROID)
target_sources(ps2EntryRunner PRIVATE src/lib/ps2_android_runtime.cpp)
endif()
install(TARGETS ps2_runtime ps2EntryRunner
RUNTIME DESTINATION bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
if(WIN32)
if(WIN32 AND PS2X_ENABLE_FFMPEG)
install(DIRECTORY "${FFMPEG_BIN_DIR}/"
DESTINATION bin
FILES_MATCHING
+30
View File
@@ -348,6 +348,19 @@ public:
uint32_t m_depth = 0u;
};
class DeferredGuestYieldScope
{
public:
explicit DeferredGuestYieldScope(bool &pendingOut) noexcept;
~DeferredGuestYieldScope();
DeferredGuestYieldScope(const DeferredGuestYieldScope &) = delete;
DeferredGuestYieldScope &operator=(const DeferredGuestYieldScope &) = delete;
private:
bool &m_pendingOut;
};
bool replaceFunction(uint32_t address, RecompiledFunction func);
// TODO remove this later need to update all tests
bool registerFunction(uint32_t address, RecompiledFunction func);
@@ -399,17 +412,33 @@ public:
uint32_t guestHeapEnd() const;
uint32_t guestHeapLimit() const;
uint32_t reserveAsyncCallbackStack(uint32_t size, uint32_t alignment = 16u);
void dispatchLoop(uint8_t *rdram, R5900Context *ctx);
void drainCompletedDmacHandlers(uint8_t *rdram);
bool shouldPreemptGuestExecution();
void yieldGuestExecutionAfterWake();
void waitForGuestExecutionHandoff();
void waitForGuestExecutionHandoff(uint64_t baselineEpoch);
uint64_t guestExecutionHandoffEpochSnapshot() const
{
return m_guestExecutionHandoffEpoch.load(std::memory_order_acquire);
}
void requestStop();
bool isStopRequested() const;
uint32_t guestExecutionWaiterCountForTesting() const
{
return m_guestExecutionWaiters.load(std::memory_order_acquire);
}
uint64_t guestExecutionHandoffTimeouts() const
{
return m_guestExecutionHandoffTimeouts.load(std::memory_order_relaxed);
}
uint8_t Load8(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr);
uint16_t Load16(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr);
uint32_t Load32(uint8_t *rdram, R5900Context *ctx, uint32_t vaddr);
@@ -506,6 +535,7 @@ private:
mutable std::mutex m_guestExecutionHandoffMutex;
mutable std::condition_variable m_guestExecutionHandoffCv;
std::atomic<uint64_t> m_guestExecutionHandoffEpoch{0u};
std::atomic<uint64_t> m_guestExecutionHandoffTimeouts{0u};
mutable std::mutex m_guestHeapMutex;
mutable std::mutex m_asyncCallbackStackMutex;
std::vector<GuestHeapBlock> m_guestHeapBlocks;
+21 -18
View File
@@ -11,7 +11,7 @@
namespace GSMem
{
constexpr usz MEMORY_SIZE = 4_mb;
constexpr usz PAGE_SIZE = 8_kb;
constexpr usz GS_PAGE_SIZE = 8_kb;
// these are all the same regardless of storage mode
constexpr usz BLOCKS_PER_PAGE = 32;
@@ -38,19 +38,32 @@ namespace GSMem
Max = 0x3F // 6 bits of values
};
inline constexpr PixelStorageMode C32 = PixelStorageMode::C32;
inline constexpr PixelStorageMode C24 = PixelStorageMode::C24;
inline constexpr PixelStorageMode C16 = PixelStorageMode::C16;
inline constexpr PixelStorageMode C16S = PixelStorageMode::C16S;
inline constexpr PixelStorageMode P8 = PixelStorageMode::P8;
inline constexpr PixelStorageMode P4 = PixelStorageMode::P4;
inline constexpr PixelStorageMode P8H = PixelStorageMode::P8H;
inline constexpr PixelStorageMode P4HL = PixelStorageMode::P4HL;
inline constexpr PixelStorageMode P4HH = PixelStorageMode::P4HH;
inline constexpr PixelStorageMode Z32 = PixelStorageMode::Z32;
inline constexpr PixelStorageMode Z24 = PixelStorageMode::Z24;
inline constexpr PixelStorageMode Z16 = PixelStorageMode::Z16;
inline constexpr PixelStorageMode Z16S = PixelStorageMode::Z16S;
struct Extent2D
{
u32 x{ 0 };
u32 y{ 0 };
};
template<typename T, Extent2D Extent>
using LookupTable = std::array<std::array<T, Extent.x>, Extent.y>;
template<typename T, usz Width, usz Height>
using LookupTable = std::array<std::array<T, Width>, Height>;
constexpr bool IsValidPsm(PixelStorageMode psm)
{
using enum PixelStorageMode;
switch (psm)
{
case C32:
@@ -75,8 +88,6 @@ namespace GSMem
// Bits per pixel in unpacked format (GS memory)
constexpr usz UnpackedBitWidth(PixelStorageMode psm)
{
using enum PixelStorageMode;
switch (psm)
{
case C32:
@@ -104,8 +115,6 @@ namespace GSMem
// bits per pixel (packed width)
constexpr usz BitsPerPixel(PixelStorageMode psm)
{
using enum PixelStorageMode;
switch (psm)
{
case C32:
@@ -135,8 +144,6 @@ namespace GSMem
constexpr bool IsDepth(PixelStorageMode psm)
{
using enum PixelStorageMode;
switch (psm)
{
case Z16:
@@ -158,8 +165,6 @@ namespace GSMem
constexpr bool IsPaletted(PixelStorageMode psm)
{
using enum PixelStorageMode;
switch (psm)
{
case P8:
@@ -209,8 +214,6 @@ namespace GSMem
template<PixelStorageMode psm>
struct PixelStorageTraits
{
using enum PixelStorageMode;
// page extent, in units of pixels
static constexpr Extent2D PageExtent();
@@ -229,9 +232,9 @@ namespace GSMem
// pixel count in a page
static constexpr usz PixelsPerPage();
using BlockLookupTableT = LookupTable<u8, BlockExtent()>;
using ColumnLookupTableT = LookupTable<u16, ColumnExtent()>;
using PageLookupTableT = std::array<LookupTable<u16, PageExtent()>, BlocksPerPage()>;
using BlockLookupTableT = LookupTable<u8, BlockExtent().x, BlockExtent().y>;
using ColumnLookupTableT = LookupTable<u16, ColumnExtent().x, ColumnExtent().y>;
using PageLookupTableT = std::array<LookupTable<u16, PageExtent().x, PageExtent().y>, BlocksPerPage()>;
// calculates the column offset
static constexpr usz ColumnId(const ColumnLookupTableT& table, u32 x, u32 y);
+33 -20
View File
@@ -706,30 +706,42 @@ namespace ps2_stubs
}
uint32_t steps = 0u;
while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < 1024u)
bool reschedulePending = false;
uint64_t handoffBaseline = 0u;
{
if (!runtime->hasFunction(callbackCtx.pc))
{
if (g_gs_sync_v_callback_bad_pc_logs < 16u)
{
std::cerr << "[sceGsSyncVCallback:bad-pc] pc=0x" << std::hex << callbackCtx.pc
<< " ra=0x" << getRegU32(&callbackCtx, 31)
<< " sp=0x" << getRegU32(&callbackCtx, 29)
<< " gp=0x" << getRegU32(&callbackCtx, 28)
<< std::dec << std::endl;
++g_gs_sync_v_callback_bad_pc_logs;
}
callbackCtx.pc = 0u;
break;
}
PS2Runtime::GuestExecutionScope guestExecution(runtime);
PS2Runtime::DeferredGuestYieldScope deferYield(reschedulePending);
auto step = runtime->lookupFunction(callbackCtx.pc);
if (!step)
while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < 1024u)
{
break;
if (!runtime->hasFunction(callbackCtx.pc))
{
if (g_gs_sync_v_callback_bad_pc_logs < 16u)
{
std::cerr << "[sceGsSyncVCallback:bad-pc] pc=0x" << std::hex << callbackCtx.pc
<< " ra=0x" << getRegU32(&callbackCtx, 31)
<< " sp=0x" << getRegU32(&callbackCtx, 29)
<< " gp=0x" << getRegU32(&callbackCtx, 28)
<< std::dec << std::endl;
++g_gs_sync_v_callback_bad_pc_logs;
}
callbackCtx.pc = 0u;
break;
}
auto step = runtime->lookupFunction(callbackCtx.pc);
if (!step)
{
break;
}
++steps;
step(rdram, &callbackCtx, runtime);
}
++steps;
step(rdram, &callbackCtx, runtime);
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
if (reschedulePending && !runtime->isStopRequested())
{
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
if (shouldLogDispatch)
@@ -909,6 +921,7 @@ namespace ps2_stubs
mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0);
mem.processPendingTransfers();
ps2TraceGuestRangeWrite(rdram, dstAddr, totalImageBytes, "sceGsExecStoreImage", ctx);
runtime->gs().consumeLocalToHostBytes(dst, totalImageBytes);
runtime->guestFree(pktAddr);
+59 -19
View File
@@ -1,6 +1,11 @@
#include "Common.h"
#include "MPEG.h"
#if !defined(PS2X_HAS_FFMPEG)
#define PS2X_HAS_FFMPEG 1
#endif
#if PS2X_HAS_FFMPEG
extern "C"
{
#include <libavcodec/avcodec.h>
@@ -8,6 +13,7 @@ extern "C"
#include <libavutil/log.h>
#include <libswscale/swscale.h>
}
#endif
#include <chrono>
#include <condition_variable>
@@ -27,6 +33,7 @@ namespace ps2_stubs
std::vector<uint8_t> rgba;
};
#if PS2X_HAS_FFMPEG
std::string ffmpegErrorString(int err)
{
std::array<char, AV_ERROR_MAX_STRING_SIZE> buffer{};
@@ -419,6 +426,30 @@ namespace ps2_stubs
bool m_drained = false;
bool m_seenKeyframe = false;
};
#else
// TODO
class MpegFfmpegDecoder
{
public:
bool feed(const uint8_t *, size_t, std::deque<MpegDecodedFrame> &)
{
static bool s_warnedNoFfmpeg = false;
if (!s_warnedNoFfmpeg)
{
std::cerr << "[MPEG] runtime built without FFmpeg; MPEG video decode is disabled." << std::endl;
s_warnedNoFfmpeg = true;
}
return false;
}
bool flush(std::deque<MpegDecodedFrame> &)
{
return true;
}
void reset() {}
};
#endif
struct MpegRegisteredCallback
{
@@ -1316,33 +1347,42 @@ namespace ps2_stubs
callbackCtx.pc = callback.func;
uint32_t steps = 0u;
while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < kMpegCallbackMaxSteps)
bool reschedulePending = false;
uint64_t handoffBaseline = 0u;
{
if (!runtime->hasFunction(callbackCtx.pc))
PS2Runtime::GuestExecutionScope guestExecution(runtime);
PS2Runtime::DeferredGuestYieldScope deferYield(reschedulePending);
while (callbackCtx.pc != 0u && !runtime->isStopRequested() && steps < kMpegCallbackMaxSteps)
{
static uint32_t badPcLogCount = 0u;
if (badPcLogCount < 16u)
if (!runtime->hasFunction(callbackCtx.pc))
{
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;
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;
}
break;
}
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(callbackCtx.pc);
if (!step)
{
break;
}
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(callbackCtx.pc);
if (!step)
{
break;
}
{
PS2Runtime::GuestExecutionScope guestExecution(runtime);
step(rdram, &callbackCtx, runtime);
++steps;
}
++steps;
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
if (reschedulePending && !runtime->isStopRequested())
{
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
if (steps >= kMpegCallbackMaxSteps)
+54 -17
View File
@@ -85,14 +85,27 @@ namespace ps2_stubs
std::mutex g_mcStateMutex;
int32_t g_mcNextFd = 1;
int32_t g_mcLastCmd = 0;
bool g_mcCommandPending = false;
int32_t g_mcLastResult = 0;
std::unordered_map<int32_t, McOpenFile> g_mcFiles;
std::array<McPortState, 2> g_mcPorts{};
int32_t g_cvMcFileCursor = 0;
constexpr int32_t kCvMcFreeCapacityBytes = 0x01000000;
constexpr int32_t kCvMcSaveCapacityBytes = 0x00080000;
constexpr int32_t kCvMcConfigCapacityBytes = 0x00008000;
constexpr int32_t kCvMcIconCapacityBytes = 0x00004000;
constexpr int32_t kCvMcSaveFileBytes = 0x838;
constexpr int32_t kCvMcConfigFileBytes = 0x34;
constexpr int32_t kCvMcIconInfoBytes = 0x3C4;
constexpr int32_t kCvMcIconFileBytes = 0xB3F8;
constexpr int32_t cvMcKilobytes(int32_t bytes)
{
return (bytes + 1023) / 1024;
}
constexpr int32_t kCvMcRequiredFreeKb =
cvMcKilobytes(kCvMcSaveFileBytes) * 15 +
cvMcKilobytes(kCvMcConfigFileBytes) +
cvMcKilobytes(kCvMcIconInfoBytes) +
cvMcKilobytes(kCvMcIconFileBytes) + 11;
bool isValidMcPortSlot(int32_t port, int32_t slot)
{
@@ -351,6 +364,7 @@ namespace ps2_stubs
{
g_mcLastCmd = cmd;
g_mcLastResult = result;
g_mcCommandPending = true;
}
void closeMcFilesLocked()
@@ -464,7 +478,6 @@ namespace ps2_stubs
}
}
MemoryCardDebugSnapshot getMemoryCardDebugSnapshot()
{
MemoryCardDebugSnapshot snapshot{};
@@ -492,9 +505,7 @@ namespace ps2_stubs
snapshot.openFiles.push_back(std::move(row));
}
std::sort(snapshot.openFiles.begin(), snapshot.openFiles.end(), [](const MemoryCardDebugOpenFile &a, const MemoryCardDebugOpenFile &b)
{
return a.fd < b.fd;
});
{ return a.fd < b.fd; });
return snapshot;
}
@@ -544,6 +555,8 @@ namespace ps2_stubs
setMcCommandResultLocked(kMcCmdChdir, result);
}
RUNTIME_LOG("[MC] Chdir port=" << port << " '" << requestedDir
<< "' -> result=" << result << " cwd='" << currentDir << "'");
writeMcCString(rdram, currentDirAddr, currentDir);
setReturnS32(ctx, 0);
}
@@ -624,6 +637,7 @@ namespace ps2_stubs
g_mcNextFd = 1;
g_mcLastCmd = 0;
g_mcLastResult = 0;
g_mcCommandPending = false;
for (McPortState &state : g_mcPorts)
{
state.currentDir = "/";
@@ -685,8 +699,8 @@ namespace ps2_stubs
const int32_t port = static_cast<int32_t>(getRegU32(ctx, 4));
const int32_t slot = static_cast<int32_t>(getRegU32(ctx, 5));
const std::string rawPath = readPs2CStringBounded(rdram, getRegU32(ctx, 6), kMcMaxPathLen);
const int32_t maxEntries = static_cast<int32_t>(readStackU32(rdram, ctx, 16));
const uint32_t tableAddr = readStackU32(rdram, ctx, 20);
const int32_t maxEntries = static_cast<int32_t>(getRegU32(ctx, 8));
const uint32_t tableAddr = getRegU32(ctx, 9);
std::vector<SceMcTblGetDir> entries;
int32_t result = kMcResultNoEntry;
@@ -828,12 +842,18 @@ namespace ps2_stubs
setMcCommandResultLocked(kMcCmdGetDir, result);
}
RUNTIME_LOG("[MC] GetDir port=" << port << " '" << rawPath
<< "' maxent=" << maxEntries << " -> result=" << result);
setReturnS32(ctx, 0);
}
void sceMcGetEntSpace(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, 1024);
{
std::lock_guard<std::mutex> lock(g_mcStateMutex);
setMcCommandResultLocked(kMcCmdGetEntSpace, 1024);
}
setReturnS32(ctx, 0);
}
void sceMcGetInfo(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -842,7 +862,7 @@ namespace ps2_stubs
const int32_t slot = static_cast<int32_t>(getRegU32(ctx, 5));
const uint32_t typePtr = getRegU32(ctx, 6);
const uint32_t freePtr = getRegU32(ctx, 7);
const uint32_t formatPtr = readStackU32(rdram, ctx, 16);
const uint32_t formatPtr = getRegU32(ctx, 8);
int32_t cardType = 0;
int32_t freeBlocks = 0;
@@ -885,6 +905,9 @@ namespace ps2_stubs
}
}
RUNTIME_LOG("[MC] GetInfo port=" << port << " type=" << cardType
<< " free=" << freeBlocks << " format=" << format
<< " result=" << result);
setReturnS32(ctx, 0);
}
@@ -901,6 +924,7 @@ namespace ps2_stubs
g_mcNextFd = 1;
g_mcLastCmd = 0;
g_mcLastResult = 0;
g_mcCommandPending = false;
for (McPortState &state : g_mcPorts)
{
state.currentDir = "/";
@@ -1172,12 +1196,25 @@ namespace ps2_stubs
const uint32_t resultPtr = getRegU32(ctx, 6);
int32_t cmd = 0;
int32_t result = 0;
bool hadPending = false;
{
std::lock_guard<std::mutex> lock(g_mcStateMutex);
hadPending = g_mcCommandPending;
g_mcCommandPending = false;
cmd = g_mcLastCmd;
result = g_mcLastResult;
}
// libmc semantics: -1 means no async operation was executing; games rely
// on it to tell idle polling apart from command completion.
if (!hadPending)
{
setReturnS32(ctx, -1);
return;
}
RUNTIME_LOG("[MC] Sync cmd=" << cmd << " result=" << result);
if (cmdPtr != 0u)
{
if (uint8_t *out = getMemPtr(rdram, cmdPtr))
@@ -1366,7 +1403,7 @@ namespace ps2_stubs
void mcGetConfigCapacitySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, kCvMcConfigCapacityBytes);
setReturnS32(ctx, kCvMcConfigFileBytes);
}
void mcGetFileSelectWindowCursol(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -1376,17 +1413,17 @@ namespace ps2_stubs
void mcGetFreeCapacitySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, kCvMcFreeCapacityBytes);
setReturnS32(ctx, kCvMcRequiredFreeKb);
}
void mcGetIconCapacitySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, kCvMcIconCapacityBytes);
setReturnS32(ctx, kCvMcIconInfoBytes);
}
void mcGetIconFileCapacitySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, kCvMcIconCapacityBytes);
setReturnS32(ctx, kCvMcIconFileBytes);
}
void mcGetPortSelectDirInfo(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
@@ -1396,7 +1433,7 @@ namespace ps2_stubs
void mcGetSaveFileCapacitySize(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
setReturnS32(ctx, kCvMcSaveCapacityBytes);
setReturnS32(ctx, kCvMcSaveFileBytes);
}
void mcGetStringEnd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
+2
View File
@@ -618,6 +618,7 @@ namespace ps2_stubs
portState->transientState = 0u;
if (dmaStr)
{
ps2TraceGuestRangeWrite(rdram, dmaAddr, 32u, "scePadPortOpen", ctx);
std::memset(dmaStr, 0, 32);
}
setReturnS32(ctx, 1);
@@ -635,6 +636,7 @@ namespace ps2_stubs
return;
}
ps2TraceGuestRangeWrite(rdram, dataAddr, 32u, "scePadRead", ctx);
if (!readPadPortData(port, slot, runtime, data, dataAddr))
{
setReturnS32(ctx, 0);
+2
View File
@@ -256,6 +256,8 @@ namespace ps2_stubs
return true;
}
ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr);
const uint64_t srcBegin = srcAddr;
const uint64_t srcEnd = srcBegin + static_cast<uint64_t>(sizeBytes);
const uint64_t dstBegin = dstAddr;
@@ -196,6 +196,10 @@ namespace ps2_syscalls
std::lock_guard<std::mutex> lock(g_sys_fd_mutex);
bytesRead = fread(hostBuf, 1, size, fp);
}
if (bytesRead > 0)
{
ps2TraceGuestRangeWrite(rdram, bufAddr, static_cast<uint32_t>(bytesRead), "fioRead", ctx);
}
if (bytesRead < size && ferror(fp))
{
@@ -258,6 +258,8 @@ static void rpcCopyToRdram(uint8_t *rdram, uint32_t dst, uint32_t src, size_t si
if (!rdram || size == 0)
return;
ps2TraceGuestRangeWrite(rdram, dst, static_cast<uint32_t>(size), "rpcCopyToRdram", nullptr);
constexpr size_t kMaxRpcTransferBytes = 1u * 1024u * 1024u;
const size_t clampedSize = std::min(size, kMaxRpcTransferBytes);
if (clampedSize != size)
@@ -292,6 +294,8 @@ static void rpcZeroRdram(uint8_t *rdram, uint32_t dst, size_t size)
if (!rdram || size == 0)
return;
ps2TraceGuestRangeWrite(rdram, dst, static_cast<uint32_t>(size), "rpcZeroRdram", nullptr);
constexpr size_t kMaxRpcTransferBytes = 1u * 1024u * 1024u;
const size_t clampedSize = std::min(size, kMaxRpcTransferBytes);
if (clampedSize != size)
@@ -2,6 +2,7 @@
#include <cstddef>
#include <cstdint>
#include <thread>
inline std::unordered_map<int, FILE *> g_fileDescriptors;
inline int g_nextFd = 3; // Start after stdin, stdout, stderr
@@ -11,6 +11,7 @@ namespace ps2_syscalls
constexpr uint32_t kIntcVblankEnd = 3u;
constexpr auto kVblankPeriod = std::chrono::microseconds(16667);
constexpr int kMaxCatchupTicks = 4;
constexpr uint32_t kMaxIrqHandlerSteps = 4096u;
std::mutex g_irq_handler_mutex;
std::mutex g_irq_worker_mutex;
@@ -168,16 +169,37 @@ namespace ps2_syscalls
SET_GPR_U32(&irqCtx, 7, 0u);
irqCtx.pc = info.handler;
while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested())
bool reschedulePending = false;
uint64_t handoffBaseline = 0u;
uint32_t steps = 0u;
{
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc);
if (!step)
PS2Runtime::GuestExecutionScope guestExecution(runtime);
PS2Runtime::DeferredGuestYieldScope deferYield(reschedulePending);
while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested() && steps < kMaxIrqHandlerSteps)
{
break;
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc);
if (!step)
{
break;
}
step(rdram, &irqCtx, runtime);
++steps;
}
// Interrupt handlers must be able to preempt a guest thread that is
// spinning on interrupt-produced state, such as a vblank counter.
step(rdram, &irqCtx, runtime);
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
if (steps >= kMaxIrqHandlerSteps)
{
static uint32_t s_stepLimitLogCount = 0u;
if (s_stepLimitLogCount < 16u)
{
std::cerr << "[INTC:step-limit] handler=0x" << std::hex << info.handler << " pc=0x" << irqCtx.pc << std::dec << std::endl;
++s_stepLimitLogCount;
}
}
if (reschedulePending && !runtime->isStopRequested())
{
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
}
catch (const ThreadExitException &)
@@ -252,14 +274,39 @@ namespace ps2_syscalls
SET_GPR_U32(&irqCtx, 7, 0u);
irqCtx.pc = info.handler;
while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested())
bool reschedulePending = false;
uint64_t handoffBaseline = 0u;
uint32_t steps = 0u;
{
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc);
if (!step)
PS2Runtime::GuestExecutionScope guestExecution(runtime);
PS2Runtime::DeferredGuestYieldScope deferYield(reschedulePending);
while (irqCtx.pc != 0u && runtime && !runtime->isStopRequested() &&
steps < kMaxIrqHandlerSteps)
{
break;
PS2Runtime::RecompiledFunction step = runtime->lookupFunction(irqCtx.pc);
if (!step)
{
break;
}
step(rdram, &irqCtx, runtime);
++steps;
}
step(rdram, &irqCtx, runtime);
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
if (steps >= kMaxIrqHandlerSteps)
{
static uint32_t s_stepLimitLogCount = 0u;
if (s_stepLimitLogCount < 16u)
{
std::cerr << "[DMAC:step-limit] handler=0x" << std::hex << info.handler
<< " pc=0x" << irqCtx.pc << std::dec << std::endl;
++s_stepLimitLogCount;
}
}
if (reschedulePending && !runtime->isStopRequested())
{
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
}
catch (const ThreadExitException &)
@@ -304,6 +351,7 @@ namespace ps2_syscalls
{
std::lock_guard<std::mutex> lock(g_vsync_flag_mutex);
reg = g_vsync_registration;
g_vsync_registration = {};
tickValue = ++g_vsync_tick_counter;
}
@@ -353,9 +401,20 @@ namespace ps2_syscalls
for (int i = 0; i < ticksToProcess; ++i)
{
const uint64_t tickValue = signalVSyncFlag(rdram, runtime);
ps2_stubs::dispatchGsSyncVCallback(rdram, runtime, tickValue);
dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankStart);
bool reschedulePending = false;
uint64_t handoffBaseline = 0u;
{
PS2Runtime::GuestExecutionScope guestExecution(runtime);
PS2Runtime::DeferredGuestYieldScope deferYield(reschedulePending);
const uint64_t tickValue = signalVSyncFlag(rdram, runtime);
ps2_stubs::dispatchGsSyncVCallback(rdram, runtime, tickValue);
dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankStart);
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
if (reschedulePending && !runtime->isStopRequested())
{
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
std::this_thread::sleep_for(std::chrono::microseconds(500));
dispatchIntcHandlersForCause(rdram, runtime, kIntcVblankEnd);
}
@@ -1118,6 +1118,7 @@ namespace ps2_syscalls
const uint8_t *srcPtr = getConstMemPtr(rdram, src);
if (destPtr && srcPtr)
{
ps2TraceGuestRangeWrite(rdram, dest, size, "syscallCopy", ctx);
std::memcpy(destPtr, srcPtr, size);
}
}
@@ -460,10 +460,13 @@ namespace ps2_syscalls
<< std::hex << pc << std::dec << std::endl;
throw ThreadExitException();
}
uint64_t handoffBaseline = 0u;
{
PS2Runtime::GuestExecutionScope guestExecution(runtime);
step(rdram, threadCtx, runtime);
handoffBaseline = runtime->guestExecutionHandoffEpochSnapshot();
}
runtime->waitForGuestExecutionHandoff(handoffBaseline);
}
}
catch (const ThreadExitException &)
@@ -0,0 +1,3 @@
// TODO: on-screen touch joystick / button overlay.
#if defined(__ANDROID__)
#endif // __ANDROID__
-2
View File
@@ -4,8 +4,6 @@
namespace GSMem
{
using enum PixelStorageMode;
using C32Traits = PixelStorageTraits<C32>;
using Z32Traits = PixelStorageTraits<Z32>;
using C16Traits = PixelStorageTraits<C16>;
+6 -19
View File
@@ -159,6 +159,8 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t
}
if (size != 0)
{
uint8_t *const rdram = m_activeRdram ? m_activeRdram : m_runtime.memory().getRDRAM();
ps2TraceGuestRangeWrite(rdram, address, static_cast<uint32_t>(size), "IopHost::writeGuest", nullptr);
std::memcpy(destination, source, size);
}
return true;
@@ -173,6 +175,8 @@ bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size)
}
if (size != 0)
{
uint8_t *const rdram = m_activeRdram ? m_activeRdram : m_runtime.memory().getRDRAM();
ps2TraceGuestRangeWrite(rdram, address, static_cast<uint32_t>(size), "IopHost::zeroGuest", nullptr);
std::memset(destination, 0, size);
}
return true;
@@ -432,29 +436,12 @@ int32_t PS2IopHostAdapter::memoryCard(const ps2x::iop::MemoryCardRequest &reques
setRegU32(&context, static_cast<int>(4 + i), request.arguments[i]);
}
uint32_t stackAddress = 0u;
if (request.arguments[4] != 0u)
{
stackAddress = allocateGuest(32u, 16u);
if (stackAddress == 0u ||
!writeGuest(stackAddress + 16u, &request.arguments[4], sizeof(uint32_t)))
{
if (stackAddress != 0u)
{
freeGuest(stackAddress);
}
return -1;
}
setRegU32(&context, 29, stackAddress);
}
// EE n32 ABI: the fifth argument travels in $t0, matching the sceMc* stubs.
setRegU32(&context, 8, request.arguments[4]);
handler(m_activeRdram ? m_activeRdram : m_runtime.memory().getRDRAM(),
&context,
&m_runtime);
if (stackAddress != 0u)
{
freeGuest(stackAddress);
}
return ps2_stubs::getMemoryCardDebugSnapshot().lastResult;
}
+84 -3
View File
@@ -108,6 +108,8 @@ namespace
thread_local DispatchHistory g_dispatchHistory;
thread_local std::unordered_map<PS2Runtime *, uint32_t> g_guestExecutionDepths;
thread_local uint32_t g_deferredGuestYieldDepth = 0u;
thread_local bool g_deferredGuestYieldPending = false;
bool computeFileCrc32(const std::string &path, uint32_t &crcOut)
{
@@ -1962,11 +1964,15 @@ void PS2Runtime::dispatchLoop(uint8_t *rdram, R5900Context *ctx)
const uint32_t dispatchedPc = pc;
const uint32_t dispatchedRa = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
uint64_t handoffBaseline = 0u;
{
GuestExecutionScope guestExecution(this);
fn(rdram, ctx, this);
handoffBaseline = guestExecutionHandoffEpochSnapshot();
}
waitForGuestExecutionHandoff(handoffBaseline);
if (ctx->pc == 0u)
{
const uint32_t ra = static_cast<uint32_t>(_mm_extract_epi32(ctx->r[31], 0));
@@ -1991,10 +1997,19 @@ void PS2Runtime::dispatchLoop(uint8_t *rdram, R5900Context *ctx)
void PS2Runtime::enterGuestExecution()
{
uint32_t &depth = g_guestExecutionDepths[this];
if (depth != 0u)
{
m_guestExecutionMutex.lock();
++depth;
return;
}
m_guestExecutionWaiters.fetch_add(1u, std::memory_order_acq_rel);
m_guestExecutionMutex.lock();
m_guestExecutionWaiters.fetch_sub(1u, std::memory_order_acq_rel);
++g_guestExecutionDepths[this];
depth = 1u;
markGuestExecutionAcquired();
}
@@ -2039,13 +2054,22 @@ void PS2Runtime::reacquireGuestExecution(uint32_t depth)
}
uint32_t &heldDepth = g_guestExecutionDepths[this];
for (uint32_t i = 0; i < depth; ++i)
uint32_t remaining = depth;
if (heldDepth == 0u)
{
m_guestExecutionWaiters.fetch_add(1u, std::memory_order_acq_rel);
m_guestExecutionMutex.lock();
m_guestExecutionWaiters.fetch_sub(1u, std::memory_order_acq_rel);
++heldDepth;
heldDepth = 1u;
markGuestExecutionAcquired();
--remaining;
}
for (uint32_t i = 0; i < remaining; ++i)
{
m_guestExecutionMutex.lock();
++heldDepth;
}
}
@@ -2058,8 +2082,65 @@ void PS2Runtime::markGuestExecutionAcquired()
m_guestExecutionHandoffCv.notify_all();
}
void PS2Runtime::waitForGuestExecutionHandoff()
{
waitForGuestExecutionHandoff(guestExecutionHandoffEpochSnapshot());
}
void PS2Runtime::waitForGuestExecutionHandoff(uint64_t baselineEpoch)
{
// Lock-free fast path
if (m_guestExecutionWaiters.load(std::memory_order_acquire) == 0u)
{
return;
}
std::unique_lock<std::mutex> lock(m_guestExecutionHandoffMutex);
if (m_guestExecutionWaiters.load(std::memory_order_acquire) == 0u)
{
return;
}
const bool handedOff = m_guestExecutionHandoffCv.wait_for(
lock,
std::chrono::milliseconds(2),
[&]()
{
return m_guestExecutionWaiters.load(std::memory_order_acquire) == 0u ||
m_guestExecutionHandoffEpoch.load(std::memory_order_relaxed) != baselineEpoch ||
isStopRequested();
});
if (!handedOff)
{
m_guestExecutionHandoffTimeouts.fetch_add(1u, std::memory_order_relaxed);
}
}
PS2Runtime::DeferredGuestYieldScope::DeferredGuestYieldScope(bool &pendingOut) noexcept
: m_pendingOut(pendingOut)
{
++g_deferredGuestYieldDepth;
}
PS2Runtime::DeferredGuestYieldScope::~DeferredGuestYieldScope()
{
if (--g_deferredGuestYieldDepth == 0u && g_deferredGuestYieldPending)
{
g_deferredGuestYieldPending = false;
m_pendingOut = true;
}
}
void PS2Runtime::yieldGuestExecutionAfterWake()
{
if (g_deferredGuestYieldDepth != 0u)
{
g_deferredGuestYieldPending = true;
return;
}
auto it = g_guestExecutionDepths.find(this);
if (it == g_guestExecutionDepths.end() || it->second == 0u)
{
+64
View File
@@ -0,0 +1,64 @@
#if defined(PLATFORM_VITA)
#include <cstdarg>
#include <cstdio>
// Heap sizes must live in a native object as well(bc of LTO).
// the newlib malloc/new arena (PS2 guest RAM 32MB + GS + buffers live here).
extern "C" unsigned int _newlib_heap_size_user = 96u * 1024u * 1024u;
extern "C" unsigned int sceLibcHeapSize = 8u * 1024u * 1024u;
namespace
{
void ttyPuts(const char *text)
{
if (text)
{
std::fputs(text, stdout); // Vita TTY (capturable via PrincessLog etc.)
}
}
}
// Overrides raylib's debugnet transport with TTY output.
extern "C"
{
int debugNetInit(const char *, int, int)
{
return 0;
}
void debugNetFinish(void)
{
}
int debugNetUDPSend(const char *text)
{
ttyPuts(text);
return 0;
}
int debugNetUDPPrintf(const char *fmt, ...)
{
char line[512];
va_list args;
va_start(args, fmt);
std::vsnprintf(line, sizeof(line), fmt, args);
va_end(args);
ttyPuts(line);
return 0;
}
int debugNetPrintf(int level, const char *fmt, ...)
{
(void)level;
char line[512];
va_list args;
va_start(args, fmt);
std::vsnprintf(line, sizeof(line), fmt, args);
va_end(args);
ttyPuts(line);
return 0;
}
}
#endif // PLATFORM_VITA
+47
View File
@@ -15,8 +15,52 @@
#include <algorithm>
#include <cstdlib>
#if defined(__ANDROID__)
#include <android/log.h>
#include <unistd.h>
#include <thread>
#include <cstdio>
#include <cstring>
#endif
namespace
{
#if defined(__ANDROID__)
void redirectStdioToLogcat()
{
static int pipeFds[2];
if (pipe(pipeFds) != 0)
{
return;
}
setvbuf(stdout, nullptr, _IOLBF, 0);
setvbuf(stderr, nullptr, _IONBF, 0);
dup2(pipeFds[1], STDOUT_FILENO);
dup2(pipeFds[1], STDERR_FILENO);
std::thread([]()
{
FILE *reader = fdopen(pipeFds[0], "r");
if (!reader)
{
return;
}
char line[1024];
while (fgets(line, sizeof(line), reader))
{
size_t len = std::strlen(line);
if (len > 0 && line[len - 1] == '\n')
{
line[len - 1] = '\0';
}
__android_log_write(ANDROID_LOG_INFO, "ps2x", line);
}
})
.detach();
}
#endif
void setupTerminateLogger() // to help on release build crashs
{
std::set_terminate([]()
@@ -91,6 +135,9 @@ namespace
int main(int argc, char *argv[])
{
#if defined(__ANDROID__)
redirectStdioToLogcat();
#endif
setupTerminateLogger();
try
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+135 -1
View File
@@ -238,7 +238,7 @@ namespace
constexpr uint32_t kAsyncCounterAddr = 0x2400u;
void testWaitForAsyncCounter(uint8_t *rdram, R5900Context *ctx, PS2Runtime *)
void testWaitForAsyncCounter(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime)
{
if (!rdram || !ctx)
{
@@ -251,6 +251,10 @@ namespace
std::memcpy(&counter, rdram + kAsyncCounterAddr, sizeof(counter));
if (counter == 0u)
{
if (runtime != nullptr)
{
runtime->yieldGuestExecutionAfterWake();
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
} while (counter == 0u);
@@ -437,6 +441,136 @@ void register_ps2_runtime_expansion_tests()
t.IsTrue(peerRanAfterHandoff, "wake handoff should let the peer acquire guest execution before returning");
});
tc.Run("recursive guest execution acquisition does not advance the handoff epoch", [](TestCase &t)
{
PS2Runtime runtime;
const uint64_t initial = runtime.guestExecutionHandoffEpochSnapshot();
PS2Runtime::GuestExecutionScope outer(&runtime);
const uint64_t afterOuter = runtime.guestExecutionHandoffEpochSnapshot();
t.Equals(afterOuter, initial + 1u, "outer acquisition should advance the epoch exactly once");
{
PS2Runtime::GuestExecutionScope inner(&runtime);
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
"recursive acquisition must not advance the epoch");
PS2Runtime::GuestExecutionScope innermost(&runtime);
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
"deeper recursive acquisitions must not advance the epoch either");
}
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), afterOuter,
"releasing recursive acquisitions must not advance the epoch");
});
tc.Run("reacquiring a depth-4 release advances the handoff epoch exactly once", [](TestCase &t)
{
PS2Runtime runtime;
PS2Runtime::GuestExecutionScope s1(&runtime);
PS2Runtime::GuestExecutionScope s2(&runtime);
PS2Runtime::GuestExecutionScope s3(&runtime);
PS2Runtime::GuestExecutionScope s4(&runtime);
const uint64_t before = runtime.guestExecutionHandoffEpochSnapshot();
{
PS2Runtime::GuestExecutionReleaseScope release(&runtime);
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), before,
"releasing guest execution must not advance the epoch");
}
t.Equals(runtime.guestExecutionHandoffEpochSnapshot(), before + 1u,
"reacquiring a depth-4 release should advance the epoch exactly once");
});
tc.Run("handoff completed before the wait does not count as a timeout", [](TestCase &t)
{
PS2Runtime runtime;
const uint64_t timeoutsBefore = runtime.guestExecutionHandoffTimeouts();
std::atomic<bool> holderAcquired{false};
std::atomic<bool> releaseHolder{false};
uint64_t baseline = 0u;
std::thread holder;
{
PS2Runtime::GuestExecutionScope mainScope(&runtime);
holder = std::thread([&]()
{
PS2Runtime::GuestExecutionScope scope(&runtime);
holderAcquired.store(true, std::memory_order_release);
while (!releaseHolder.load(std::memory_order_acquire))
{
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
});
const bool holderContending = waitUntil([&]()
{
return runtime.guestExecutionWaiterCountForTesting() > 0u;
}, std::chrono::milliseconds(250));
t.IsTrue(holderContending, "holder thread should be queued before the release");
// Token captured BEFORE releasing guest execution, like the dispatchers do
baseline = runtime.guestExecutionHandoffEpochSnapshot();
}
const bool acquired = waitUntil([&]()
{
return holderAcquired.load(std::memory_order_acquire);
}, std::chrono::milliseconds(250));
t.IsTrue(acquired, "holder should acquire guest execution after the release");
// Second waiter keeps waiters > 0 so the wait below cannot take the
// no-waiters fast path: it must recognize the epoch advance instead.
std::thread secondWaiter([&]()
{
PS2Runtime::GuestExecutionScope scope(&runtime);
});
const bool secondContending = waitUntil([&]()
{
return runtime.guestExecutionWaiterCountForTesting() > 0u;
}, std::chrono::milliseconds(250));
t.IsTrue(secondContending, "second waiter should be queued while the holder owns guest execution");
runtime.waitForGuestExecutionHandoff(baseline);
t.Equals(runtime.guestExecutionHandoffTimeouts(), timeoutsBefore,
"a handoff that completed before the wait must not count as a timeout");
releaseHolder.store(true, std::memory_order_release);
if (holder.joinable())
{
holder.join();
}
if (secondWaiter.joinable())
{
secondWaiter.join();
}
});
tc.Run("nested DeferredGuestYieldScope delivers pending only to the outermost scope", [](TestCase &t)
{
PS2Runtime runtime;
bool outerPending = false;
bool innerPending = false;
{
PS2Runtime::DeferredGuestYieldScope outer(outerPending);
{
PS2Runtime::DeferredGuestYieldScope inner(innerPending);
runtime.yieldGuestExecutionAfterWake(); // must defer instead of yielding
}
t.IsFalse(innerPending, "inner scope must not consume the deferred yield");
t.IsFalse(outerPending, "pending must only be delivered when the outermost scope closes");
}
t.IsTrue(outerPending, "outermost scope should deliver the deferred yield");
t.IsFalse(innerPending, "inner scope must stay untouched");
});
tc.Run("guest preemption policy requests a dispatcher handoff when another guest thread contends", [](TestCase &t)
{
PS2Runtime runtime;
+15 -4
View File
@@ -183,7 +183,7 @@ void register_ps2_runtime_interrupt_tests()
{
MiniTest::Case("PS2RuntimeInterrupt", [](TestCase &tc)
{
tc.Run("SetVSyncFlag updates guest flag and monotonic tick", [](TestCase &t)
tc.Run("SetVSyncFlag arms a one-shot vblank notification", [](TestCase &t)
{
notifyRuntimeStop();
TestEnv env;
@@ -211,12 +211,23 @@ void register_ps2_runtime_interrupt_tests()
const uint64_t firstTick = readGuestU64(env.rdram.data(), kTickAddr);
t.IsTrue(firstTick > 0u, "First observed VSync tick should be positive");
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 1u, "VSync worker should set flag to one");
const bool tickRewritten = waitUntil([&]() {
return readGuestU64(env.rdram.data(), kTickAddr) != firstTick;
}, std::chrono::milliseconds(100));
t.IsTrue(!tickRewritten, "consumed registration should not be written again");
const bool secondTickSeen = waitUntil([&]() {
// Re-arming registers a fresh one-shot notification.
writeGuestU32(env.rdram.data(), kFlagAddr, 0u);
R5900Context rearmCtx{};
setRegU32(rearmCtx, 4, kFlagAddr);
setRegU32(rearmCtx, 5, kTickAddr);
t.IsTrue(callSyscall(0x73u, env.rdram.data(), &rearmCtx, &env.runtime), "SetVSyncFlag re-arm should dispatch");
const bool rearmedTickSeen = waitUntil([&]() {
return readGuestU64(env.rdram.data(), kTickAddr) > firstTick;
}, std::chrono::milliseconds(300));
t.IsTrue(secondTickSeen, "VSync tick should continue to advance");
t.IsTrue(readGuestU64(env.rdram.data(), kTickAddr) > firstTick, "tick should be monotonic");
t.IsTrue(rearmedTickSeen, "re-armed registration should observe a later tick");
t.Equals(readGuestU32(env.rdram.data(), kFlagAddr), 1u, "re-armed registration should set flag to one");
cleanupRuntime(env);
});
+15 -15
View File
@@ -89,12 +89,6 @@ namespace
std::memset(&ctx, 0, sizeof(ctx));
}
void writeStackArg(std::vector<uint8_t> &rdram, R5900Context &ctx, uint32_t slotIndex, uint32_t value)
{
const uint32_t sp = ::getRegU32(&ctx, 29);
writeGuestU32(rdram.data(), sp + 16u + slotIndex * sizeof(uint32_t), value);
}
int32_t syncMc(std::vector<uint8_t> &rdram, int32_t *cmdOut = nullptr)
{
R5900Context syncCtx{};
@@ -446,9 +440,9 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 5, 0u);
setRegU32(test.ctx, 6, patternAddr);
setRegU32(test.ctx, 7, 0u);
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
writeStackArg(test.rdram, test.ctx, 0u, 8u);
writeStackArg(test.rdram, test.ctx, 1u, GUEST_MC_TABLE_ADDR);
// EE n32 ABI: arguments 5 and 6 travel in $t0/$t1
setRegU32(test.ctx, 8, 8u);
setRegU32(test.ctx, 9, GUEST_MC_TABLE_ADDR);
ps2_stubs::sceMcGetDir(test.rdram.data(), &test.ctx, nullptr);
@@ -480,8 +474,8 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 5, 0u);
setRegU32(test.ctx, 6, typeAddr);
setRegU32(test.ctx, 7, freeAddr);
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
writeStackArg(test.rdram, test.ctx, 0u, formatAddr);
// EE n32 ABI: the fifth argument travels in $t0
setRegU32(test.ctx, 8, formatAddr);
ps2_stubs::sceMcGetInfo(test.rdram.data(), &test.ctx, nullptr);
int32_t cmd = 0;
@@ -501,8 +495,7 @@ void register_ps2_runtime_io_tests()
setRegU32(test.ctx, 5, 0u);
setRegU32(test.ctx, 6, typeAddr);
setRegU32(test.ctx, 7, freeAddr);
setRegU32(test.ctx, 29, GUEST_STACK_AREA_START);
writeStackArg(test.rdram, test.ctx, 0u, formatAddr);
setRegU32(test.ctx, 8, formatAddr);
ps2_stubs::sceMcGetInfo(test.rdram.data(), &test.ctx, nullptr);
t.Equals(syncMc(test.rdram, &cmd), -2, "unformatted cards should report sceMcResNoFormat through sceMcSync");
@@ -533,8 +526,15 @@ void register_ps2_runtime_io_tests()
ps2_stubs::sceMcEnd(test.rdram.data(), &test.ctx, nullptr);
t.Equals(getRegS32(&test.ctx, 2), 0, "sceMcEnd should succeed");
t.Equals(syncMc(test.rdram, &cmd), 0, "sceMcSync should report cleared result after sceMcEnd");
t.Equals(cmd, 0, "sceMcEnd should clear the last active libmc command");
// libmc semantics: with no async command pending, sceMcSync returns -1
// and leaves the cmd/result out-parameters untouched.
R5900Context syncCtx{};
setRegU32(syncCtx, 4, 0u);
setRegU32(syncCtx, 5, GUEST_MC_SYNC_CMD_ADDR);
setRegU32(syncCtx, 6, GUEST_MC_SYNC_RESULT_ADDR);
ps2_stubs::sceMcSync(test.rdram.data(), &syncCtx, nullptr);
t.Equals(getRegS32(&syncCtx, 2), -1,
"sceMcSync after sceMcEnd should report that no command is active");
});
tc.Run("sceIoctl cmd1 updates wait flag state", [](TestCase &t)
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
# Builds the PS2 runtime (ps2xIOP + ps2xRuntime + eboot.bin/vpk) for PS Vita.
#
# Usage:
# ./vita/build.sh [boot_elf_path_on_vita]
#
# Environment overrides:
# PS2X_BOOT_ELF guest ELF path baked into the runner (default: ux0:data/ps2x/game.elf)
# BUILD_DIR build directory (default: <repo>/outvita)
# CLEAN=1 wipe the build directory before configuring
#
# Prerequisites (run ./vita/setup.sh once to satisfy all of these):
# - VitaSDK installed with $VITASDK exported (https://vitasdk.org)
# - The PVR GL stack: PVR_PSP2 + gl4es4vita + SDL2(PVR) + raylib 5.5
# - ffmpeg vita libs available via vdpm (libavcodec etc.)
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/outvita}"
BOOT_ELF="${1:-${PS2X_BOOT_ELF:-ux0:data/ps2x/game.elf}}"
fail() {
echo "error: $*" >&2
exit 1
}
# --- checks -----------------------------------------------------------
[ -n "${VITASDK:-}" ] || fail "VITASDK is not set. Install VitaSDK and 'export VITASDK=/usr/local/vitasdk'."
[ -f "$VITASDK/share/vita.toolchain.cmake" ] || fail "missing $VITASDK/share/vita.toolchain.cmake"
[ -f "$VITASDK/share/vita.cmake" ] || fail "missing $VITASDK/share/vita.cmake"
command -v cmake >/dev/null || fail "cmake not found (sudo apt install cmake)"
command -v arm-vita-eabi-gcc >/dev/null || fail "arm-vita-eabi-gcc not in PATH (add \$VITASDK/bin to PATH)"
[ -f "$VITASDK/arm-vita-eabi/include/SDL2/SDL.h" ] ||
fail "SDL2 headers missing in VitaSDK. Run ./vita/setup.sh first."
[ -f "$VITASDK/arm-vita-eabi/include/gpu_es4/psp2_pvr_hint.h" ] ||
fail "PVR headers missing (SDL2 would build without GL). Run ./vita/setup.sh first."
if command -v arm-vita-eabi-pkg-config >/dev/null &&
! arm-vita-eabi-pkg-config --exists libavcodec 2>/dev/null; then
echo "warning: libavcodec not found via arm-vita-eabi-pkg-config; if configure fails, run: vdpm ffmpeg" >&2
fi
if [ ! -f "$REPO_ROOT/ps2xRuntime/src/runner/register_functions.cpp" ]; then
echo "warning: ps2xRuntime/src/runner/ has no recompiled game code; the runner will link without a game." >&2
fi
case "$REPO_ROOT" in
/mnt/*)
echo "warning: building under /mnt/ (Windows filesystem) is slow in WSL; consider cloning into the Linux filesystem." >&2
;;
esac
# --- configure ---------------------------------------------------------------
if [ "${CLEAN:-0}" = "1" ]; then
rm -rf "$BUILD_DIR"
fi
# A cache configured on another machine/OS (e.g. the Windows-side outvita) is unusable
if [ -f "$BUILD_DIR/CMakeCache.txt" ] &&
! grep -q "CMAKE_HOME_DIRECTORY:INTERNAL=$REPO_ROOT\$" "$BUILD_DIR/CMakeCache.txt"; then
echo "stale CMake cache from another environment detected; recreating $BUILD_DIR"
rm -rf "$BUILD_DIR"
fi
GENERATOR_ARGS=()
if command -v ninja >/dev/null; then
GENERATOR_ARGS=(-G Ninja)
fi
echo "==> configuring (boot ELF: $BOOT_ELF)"
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" "${GENERATOR_ARGS[@]}" \
-DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" \
-DCMAKE_BUILD_TYPE=Release \
-DPS2X_BUILD_RECOMP=OFF \
-DPS2X_BUILD_ANALYZER=OFF \
-DPS2X_BUILD_TEST=OFF \
-DPS2X_BUILD_STUDIO=OFF \
-DPS2X_ENABLE_SCCACHE=OFF \
-DPS2X_DEFAULT_BOOT_ELF="$BOOT_ELF"
# --- build -------------------------------------------------------------------
echo "==> building runtime"
cmake --build "$BUILD_DIR" -j"$(nproc)"
# --- report ------------------------------------------------------------------
VPK="$(find "$BUILD_DIR" -name '*.vpk' -print -quit)"
if [ -n "$VPK" ]; then
echo ""
echo "==> done: $VPK"
echo " install it with VitaShell, then push the game files to ux0:data/... (keep the ELF's real name)"
else
echo ""
echo "==> build finished, but no .vpk was produced (PS2X_VITA_CREATE_PACKAGE may be OFF)"
fi
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Setup of the PS Vita GL toolchain that the PS2 runtime needs.
#
# Installs, into $VITASDK, the full PowerVR OpenGL ES stack the runtime's raylib
# backend depends on, plus stages the runtime .suprx modules into the repo so the
# vpk can bundle them. Run this ONCE per machine (idempotent; safe to re-run).
#
# Prerequisites: VitaSDK installed with $VITASDK exported, plus curl/unzip/cmake.
# Usage: ./vita/setup.sh
#
# Stack::
# PVR_PSP2 v3.9 : the PowerVR SGX driver modules + EGL/GLES headers/stubs
# gl4es4vita 1.1.4: desktop-GL-over-GLES translation (libGL.suprx) + stubs
# SDL2 + PVR : SDL2 2.32.2 rebuilt with -DVIDEO_VITA_PVR=ON (GL support)
# raylib 5.5 : Quenom's SDL2-based Vita port (raylib itself)
set -euo pipefail
fail() { echo "error: $*" >&2; exit 1; }
[ -n "${VITASDK:-}" ] || fail "VITASDK is not set. 'export VITASDK=/usr/local/vitasdk' first."
[ -f "$VITASDK/share/vita.toolchain.cmake" ] || fail "missing $VITASDK/share/vita.toolchain.cmake"
command -v curl >/dev/null || fail "curl not found"
command -v unzip >/dev/null || fail "unzip not found"
command -v cmake >/dev/null || fail "cmake not found"
export PATH="$VITASDK/bin:$PATH"
PREFIX="$VITASDK/arm-vita-eabi"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MODULE_DEST="$REPO_ROOT/ps2xRuntime/vita/module"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
mkdir -p "$MODULE_DEST"
PVR_VER=3.9
GL4ES_VER=1.1.4
SDL_VER=2.32.2
echo "==> [1/4] PVR_PSP2 v$PVR_VER (driver modules, stubs, headers)"
cd "$WORK"
curl -sfL "https://github.com/GrapheneCt/PVR_PSP2/archive/refs/tags/v${PVR_VER}.tar.gz" -o pvr_src.tar.gz
curl -sfL "https://github.com/GrapheneCt/PVR_PSP2/releases/download/v${PVR_VER}/vitasdk_stubs.zip" -o pvr_stubs.zip
curl -sfL "https://github.com/GrapheneCt/PVR_PSP2/releases/download/v${PVR_VER}/PSVita_Release.zip" -o pvr_mods.zip
tar xzf pvr_src.tar.gz
mkdir -p pvr_stubs pvr_mods
unzip -oq pvr_stubs.zip -d pvr_stubs
unzip -oq pvr_mods.zip -d pvr_mods
# stub archives (the zip nests each stub inside a dir named *.a — take the files)
find pvr_stubs -type f -name '*.a' -exec install -D -t "$PREFIX/lib/" {} +
# headers
for d in EGL GLES GLES2 KHR; do
install -d "$PREFIX/include/$d"
install -D -t "$PREFIX/include/$d/" "PVR_PSP2-${PVR_VER}/include/$d/"*.h
done
install -D -t "$PREFIX/include/gpu_es4/" "PVR_PSP2-${PVR_VER}/include/gpu_es4/psp2_pvr_hint.h"
# runtime modules -> repo (bundled into the vpk at app0:module/)
find pvr_mods -name '*.suprx' -exec cp -v {} "$MODULE_DEST/" \;
echo "==> [2/4] gl4es4vita v$GL4ES_VER (libGL.suprx + gl4es stubs/headers)"
cd "$WORK"
GL4ES_BASE="https://github.com/SonicMastr/gl4es4vita/releases/download/v${GL4ES_VER}-vita"
curl -sfL "$GL4ES_BASE/include.zip" -o gl_inc.zip
curl -sfL "$GL4ES_BASE/vitasdk_stubs.zip" -o gl_stubs.zip
curl -sfL "$GL4ES_BASE/PSVita_Release.zip" -o gl_mods.zip
mkdir -p gl_inc gl_stubs gl_mods
unzip -oq gl_inc.zip -d gl_inc
unzip -oq gl_stubs.zip -d gl_stubs
unzip -oq gl_mods.zip -d gl_mods
find gl_inc -name '*.h' | while read -r h; do install -D "$h" "$PREFIX/include/${h#gl_inc/}"; done
find gl_stubs -type f -name '*.a' -exec install -D -t "$PREFIX/lib/" {} +
find gl_mods -name '*.suprx' -exec cp -v {} "$MODULE_DEST/" \;
echo "==> [3/4] SDL2 $SDL_VER with -DVIDEO_VITA_PVR=ON"
cd "$WORK"
curl -sfL "https://github.com/libsdl-org/SDL/releases/download/release-${SDL_VER}/SDL2-${SDL_VER}.tar.gz" -o sdl.tar.gz
tar xzf sdl.tar.gz
[ -f "$PREFIX/lib/libSDL2.a.bakgxm" ] || cp "$PREFIX/lib/libSDL2.a" "$PREFIX/lib/libSDL2.a.bakgxm" 2>/dev/null || true
mkdir -p "SDL2-${SDL_VER}/build"
cd "SDL2-${SDL_VER}/build"
cmake .. -DCMAKE_TOOLCHAIN_FILE="$VITASDK/share/vita.toolchain.cmake" \
-DCMAKE_INSTALL_PREFIX="$PREFIX" -DVIDEO_VITA_PVR=ON >/dev/null
make -j"$(nproc)" >/dev/null
make install >/dev/null
echo "==> [4/4] raylib 5.5 (Quenom SDL2 Vita port)"
cd "$WORK"
[ -f "$PREFIX/lib/libraylib.a.bak42" ] || cp "$PREFIX/lib/libraylib.a" "$PREFIX/lib/libraylib.a.bak42" 2>/dev/null || true
git clone --depth 1 https://github.com/Quenom/raylib-5.5-vita raylib
make -C raylib/src PLATFORM=PLATFORM_VITA -j"$(nproc)" >/dev/null
make -C raylib/src install >/dev/null
echo ""
echo "==> setup complete. Modules staged in ps2xRuntime/vita/module/:"
ls "$MODULE_DEST"
echo ""
echo "Now build the vpk with: ./vita/build.sh <boot-elf-path-on-vita>"