mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e27a658b32 | |||
| 75d729ce40 | |||
| 14b1e5cb39 | |||
| d9ea4fb63d | |||
| d74a3ce139 | |||
| 8d7e8a5a46 | |||
| f4309cd18c | |||
| f49ca4edbc | |||
| 61300792a0 | |||
| d87bff4256 | |||
| 6970f3a2c4 | |||
| f3687c5ae6 | |||
| 1176609890 | |||
| ee149581aa | |||
| 905b4edfa8 | |||
| 81f2a7f5e1 | |||
| c4355c8cda | |||
| d50ed4e300 | |||
| cf4a90c4ab | |||
| ecc86f4b5d | |||
| 52edf07657 | |||
| 61621b8313 | |||
| 5196a6672a | |||
| 3a2e0d69fd | |||
| db3d44fbde | |||
| 8c8a97af65 | |||
| a111059a70 | |||
| 9bf9c08d9d | |||
| a8828f5c77 | |||
| 91592e3faa | |||
| 790aaf4cda | |||
| 7562ec14c9 | |||
| ed8b3ebee1 | |||
| 93e221feaa | |||
| 553a9027d8 | |||
| cad1ca0bb5 | |||
| 7ca6a866c9 | |||
| 9e4fd794c1 | |||
| 493522a769 | |||
| da10073cb9 | |||
| ccd17e5244 | |||
| 765cbcca0f | |||
| b3be2650b0 |
@@ -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: |
|
||||
|
||||
+6
-1
@@ -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/
|
||||
+61
-11
@@ -1,14 +1,48 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project("PS2 Retro X")
|
||||
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VITASDK})
|
||||
set(PS2X_VITA_TOOLCHAIN_FILE "$ENV{VITASDK}/share/vita.toolchain.cmake")
|
||||
if(EXISTS "${PS2X_VITA_TOOLCHAIN_FILE}")
|
||||
set(CMAKE_TOOLCHAIN_FILE "${PS2X_VITA_TOOLCHAIN_FILE}" CACHE PATH
|
||||
"Toolchain file used for cross-compiling" FORCE)
|
||||
message(STATUS "Using VitaSDK toolchain from VITASDK: ${CMAKE_TOOLCHAIN_FILE}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
project(PS2RetroX)
|
||||
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
option(PS2X_RUNTIME OFF)
|
||||
option(PS2X_BUILD_RECOMP "Build ps2xRecomp" ON)
|
||||
option(PS2X_BUILD_RUNTIME "Build ps2xRuntime" ON)
|
||||
option(PS2X_BUILD_ANALYZER "Build ps2xAnalyzer" ON)
|
||||
option(PS2X_BUILD_TEST "Build ps2xTest" ON)
|
||||
option(PS2X_BUILD_STUDIO "Build ps2xStudio" ON)
|
||||
|
||||
# ARM64 support using sse2neon
|
||||
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")
|
||||
message(STATUS "ARM64 detected, fetching sse2neon")
|
||||
set(PS2X_IS_ARM_TARGET ON)
|
||||
set(PS2X_IS_AARCH64_TARGET ON)
|
||||
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^arm|^ARM")
|
||||
set(PS2X_IS_ARM_TARGET ON)
|
||||
endif()
|
||||
|
||||
if((CMAKE_C_COMPILER MATCHES "arm-vita-eabi") OR
|
||||
(CMAKE_CXX_COMPILER MATCHES "arm-vita-eabi"))
|
||||
set(PS2X_IS_ARM_TARGET ON)
|
||||
endif()
|
||||
|
||||
if(PS2X_IS_ARM_TARGET)
|
||||
message(STATUS "ARM target detected, fetching sse2neon")
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
@@ -22,13 +56,13 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64")
|
||||
include_directories(${sse2neon_SOURCE_DIR})
|
||||
add_compile_definitions(USE_SSE2NEON)
|
||||
|
||||
if(APPLE)
|
||||
if(PS2X_IS_AARCH64_TARGET AND APPLE)
|
||||
# macOS ARM64 already uses optimal defaults
|
||||
message(STATUS "macOS ARM64 - using default compiler flags")
|
||||
elseif(MSVC)
|
||||
elseif(PS2X_IS_AARCH64_TARGET AND MSVC)
|
||||
# Windows ARM64 - MSVC already uses optimal defaults
|
||||
message(STATUS "Windows ARM64 (MSVC) - using default compiler flags")
|
||||
else()
|
||||
elseif(PS2X_IS_AARCH64_TARGET)
|
||||
# Linux ARM64 - add NEON flags
|
||||
message(STATUS "Non-Apple ARM64 - adding NEON compiler flags")
|
||||
add_compile_options(-march=armv8-a+fp+simd)
|
||||
@@ -39,13 +73,29 @@ if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64")
|
||||
if(COMPILER_SUPPORTS_CRYPTO_CRC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv8-a+fp+simd+crypto+crc")
|
||||
message(STATUS "Crypto and CRC extensions enabled")
|
||||
else()
|
||||
add_compile_options(-march=armv8-a+fp+simd)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_subdirectory("ps2xRecomp")
|
||||
if(PS2X_BUILD_RECOMP)
|
||||
add_subdirectory("ps2xRecomp")
|
||||
endif()
|
||||
|
||||
add_subdirectory("ps2xRuntime")
|
||||
if(PS2X_BUILD_RUNTIME)
|
||||
add_subdirectory("ps2xIOP")
|
||||
add_subdirectory("ps2xRuntime")
|
||||
endif()
|
||||
|
||||
add_subdirectory("ps2xAnalyzer")
|
||||
add_subdirectory("ps2xTest")
|
||||
if(PS2X_BUILD_ANALYZER)
|
||||
add_subdirectory("ps2xAnalyzer")
|
||||
endif()
|
||||
|
||||
if(PS2X_BUILD_TEST)
|
||||
add_subdirectory("ps2xTest")
|
||||
endif()
|
||||
|
||||
if(PS2X_BUILD_STUDIO)
|
||||
add_subdirectory("ps2xStudio")
|
||||
endif()
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ This project statically recompiles PS2 ELF binaries into C++ and provides a runt
|
||||
* `ps2xAnalyzer`: scans ELF/functions and writes TOML config (`stubs`, `skip`, instruction patches).
|
||||
* `ps2xRecomp`: reads TOML + ELF, decodes R5900 instructions, and generates C++ output.
|
||||
* `ps2xRuntime`: hosts memory, function registration, syscall dispatch, and hardware stubs.
|
||||
* `ps2xIOP`: R3000A IRX execution, a virtual IOP kernel, and generic HLE fallbacks.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -61,29 +62,37 @@ cmake --build out/build --config Debug
|
||||
|
||||
### Usage
|
||||
|
||||
1. Analyze ELF and generate config:
|
||||
Preferred workflow for retail or stripped games:
|
||||
|
||||
```bash
|
||||
./ps2_analyzer your_game.elf config.toml
|
||||
```
|
||||
*For better results on retail games, see the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-recommended-for-complex-games).*
|
||||
|
||||
2. Recompile using generated TOML:
|
||||
1. Open the ELF in Ghidra.
|
||||
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
3. Use the exported TOML and CSV map.
|
||||
4. Recompile with the exported TOML:
|
||||
|
||||
```bash
|
||||
./ps2_recomp config.toml
|
||||
```
|
||||
|
||||
3. Build generated output and link with `ps2xRuntime`.
|
||||
Fallback workflow for quick local experiments or ELFs with debug symbol :
|
||||
|
||||
```bash
|
||||
./ps2_analyzer your_game.elf config.toml
|
||||
```
|
||||
|
||||
See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for ghdira instructions.
|
||||
|
||||
Then build generated output and link with `ps2xRuntime`.
|
||||
|
||||
### Configuration
|
||||
|
||||
Main fields in `config.toml`:
|
||||
|
||||
* `general.input`: source ELF path.
|
||||
* `general.ghidra_output`: optional function map CSV.
|
||||
* `general.ghidra_output`: recommended function map CSV exported from Ghidra.
|
||||
* `general.output`: generated C++ output folder.
|
||||
* `general.single_file_output`: one combined cpp or one file per function.
|
||||
* `general.low_memory_mode`: reduce peak output-generation memory by avoiding retained disassembly strings and forcing serial output generation. Generated instruction comments are still emitted; disassembly text is produced while writing each output file instead of being kept in memory.
|
||||
* `general.output_worker_threads`: number of output-generation workers (clamped to nproc * 2). A positive value uses exactly that many workers. `0` uses `nproc - 1` when at least two hardware threads are available, otherwise serial output generation. `1` forces serial output generation.
|
||||
* `general.patch_syscalls`: apply configured patches to `SYSCALL` instructions (`false` recommended).
|
||||
* `general.patch_cop0`: apply configured patches to COP0 instructions.
|
||||
* `general.patch_cache`: apply configured patches to CACHE instructions.
|
||||
@@ -96,7 +105,7 @@ Address binding for stripped ELFs:
|
||||
* Use `handler@0xADDRESS` inside `general.stubs` to map a stripped function start directly to a runtime handler.
|
||||
* Example: `sceCdRead@0x00123456` binds function start `0x00123456` to `ps2_stubs::sceCdRead(...)`.
|
||||
* Generic temporary handlers are available: `ret0@0xADDR`, `ret1@0xADDR`, `reta0@0xADDR`.
|
||||
* Before manual binding, try plain recompilation first: if ELF relocation symbols are present for calls, runtime handler routing can be inferred automatically.
|
||||
* Before manual binding, prefer recompilation from a Ghidra-exported TOML/CSV first. The extra boundaries and synthetic entry points are usually more important than manual early triage.
|
||||
* The address must be the function start in that exact ELF build.
|
||||
* Addresses are not portable across different games/regions/builds.
|
||||
* The handler name must exist in runtime call lists (`PS2_SYSCALL_LIST` or `PS2_STUB_LIST`).
|
||||
@@ -104,31 +113,12 @@ Address binding for stripped ELFs:
|
||||
Example:
|
||||
|
||||
```toml
|
||||
[general]
|
||||
input = "path/to/game.elf"
|
||||
ghidra_output = ""
|
||||
output = "output/"
|
||||
|
||||
single_file_output = true
|
||||
patch_syscalls = false
|
||||
patch_cop0 = true
|
||||
patch_cache = true
|
||||
|
||||
stubs = ["printf", "malloc", "free"]
|
||||
|
||||
# stripped function binding by address:
|
||||
# stubs = ["sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
|
||||
stubs = ["sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
|
||||
# temporary return handlers:
|
||||
# stubs = ["ret0@0x001D9410", "ret1@0x001D5BC8", "reta0@0x0024B7C0"]
|
||||
stubs = ["ret0@0x001D9410", "ret1@0x001D5BC8", "reta0@0x0024B7C0"]
|
||||
# mixed example:
|
||||
# stubs = ["printf", "sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
|
||||
|
||||
skip = ["abort", "exit"]
|
||||
|
||||
[patches]
|
||||
instructions = [
|
||||
{ address = "0x100004", value = "0x00000000" }
|
||||
]
|
||||
stubs = ["printf", "sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
|
||||
```
|
||||
|
||||
### Runtime
|
||||
@@ -141,13 +131,16 @@ To execute the recompiled code.
|
||||
* Some syscall dispatcher with common kernel IDs.
|
||||
* Basic GS/VU/file/system stubs.
|
||||
* Foundation to expand and port your game.
|
||||
* `ps2xIOP` execution of original IRX modules with generic HLE fallbacks.
|
||||
|
||||
See [IOP emulation](ps2xIOP/README.md) for module execution and the service boundary.
|
||||
|
||||
### Game Override Hooks
|
||||
|
||||
Game overrides are runtime-side, build-scoped patch modules.
|
||||
|
||||
A game override is C++ code that runs during `loadELF` and can replace function bindings by address for one specific game build. This is separate from recompilation output and separate from global runtime stubs/syscalls.
|
||||
|
||||
|
||||
A game override is C++ code that runs during `loadELF` and can replace EE function bindings by address for one specific game build. IOP RPC/DMA behavior is handled by the `ps2xIOP` emulator and its runtime transport. This is separate from recompilation output and separate from global runtime stubs/syscalls.
|
||||
|
||||
API:
|
||||
|
||||
* Header: `ps2xRuntime/include/game_overrides.h`
|
||||
@@ -169,9 +162,8 @@ Use Game Override modules when:
|
||||
6. Re-test from cold boot after each batch.
|
||||
|
||||
### Limitations
|
||||
|
||||
* Graphics Synthesizer and other hardware components need external implementation
|
||||
* VU1 microcode is not complete.
|
||||
|
||||
* Performance is very bad for VU and GS
|
||||
* Hardware emulation is partial and many paths are stubbed.
|
||||
|
||||
### Acknowledgments
|
||||
@@ -180,3 +172,4 @@ Use Game Override modules when:
|
||||
* Uses ELFIO for ELF parsing
|
||||
* Uses toml11 for TOML parsing
|
||||
* Uses fmt for string formatting
|
||||
* Reference for runtime PCSX2
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.6.1' apply false
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx4g
|
||||
android.useAndroidX=true
|
||||
|
||||
# ps2xBootElf=/storage/emulated/0/Android/data/com.ps2x.runner/files/SLUS_201.84
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = 'PS2Recomp'
|
||||
include ':app'
|
||||
@@ -4,8 +4,23 @@ project(PS2Analyzer VERSION 0.1.0 LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
FetchContent_Declare(
|
||||
nlohmann_json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(nlohmann_json)
|
||||
|
||||
set(PS2ANALYZER_LIB_SOURCES
|
||||
src/analysis_passes.cpp
|
||||
src/elf_analysis_context.cpp
|
||||
src/elf_analyzer.cpp
|
||||
src/function_classifier.cpp
|
||||
src/sce_symbol_scanner.cpp
|
||||
src/toml_generator.cpp
|
||||
)
|
||||
|
||||
add_library(ps2_analyzer_lib STATIC ${PS2ANALYZER_LIB_SOURCES})
|
||||
@@ -13,10 +28,12 @@ add_library(ps2_analyzer_lib STATIC ${PS2ANALYZER_LIB_SOURCES})
|
||||
target_include_directories(ps2_analyzer_lib PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/ps2xRecomp/include
|
||||
${CMAKE_SOURCE_DIR}/ps2xRuntime/include
|
||||
)
|
||||
|
||||
target_link_libraries(ps2_analyzer_lib PUBLIC
|
||||
ps2_recomp_lib
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
add_executable(ps2_analyzer
|
||||
@@ -32,3 +49,10 @@ install(TARGETS ps2_analyzer ps2_analyzer_lib
|
||||
LIBRARY DESTINATION lib
|
||||
ARCHIVE DESTINATION lib
|
||||
)
|
||||
|
||||
include("${CMAKE_SOURCE_DIR}/ps2xRuntime/cmake/ReleaseMode.cmake")
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||
EnableFastReleaseMode(ps2_analyzer_lib)
|
||||
EnableFastReleaseMode(ps2_analyzer)
|
||||
endif()
|
||||
|
||||
+39
-17
@@ -9,55 +9,77 @@ The analyzer supports three distinct paths for discovering code within a PS2 bin
|
||||
### 1. DWARF Debug Information
|
||||
If the ELF was compiled with debug symbols (`-g`), the analyzer uses `libdwarf` to extract perfect function names and exact start/end addresses. This is common in homebrew or early development builds.
|
||||
|
||||
### 2. Native Heuristic Scanner (Retail/Stripped)
|
||||
### 2. Native Heuristic Scanner (Test only)
|
||||
For commercial games where symbols are stripped, the analyzer uses a "JAL Scanner":
|
||||
* It scans executable sections for `JAL` (Jump and Link) instructions.
|
||||
* It infers function start points based on jump targets.
|
||||
* It generates names like `sub_XXXXXXXX`.
|
||||
|
||||
### 3. Ghidra Integration (For Complex Games)
|
||||
For the highest accuracy in stripped games, you can use Ghidra's superior analysis engine:
|
||||
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.py` or `.java`.
|
||||
Use this path only as a quick fallback when you do not yet have a Ghidra project. It is not the preferred workflow for retail games.
|
||||
|
||||
### 3. SCE SDK Symbol Database (For SDK Function Names)
|
||||
For stripped retail games, the analyzer can identify SCE/PS2SDK library functions from a
|
||||
`sce-symbol-scanner` compatible database. A snapshot of the database is embedded in the
|
||||
analyzer. Pass the directory that contains `symbols.json` and `tree.json`, or point
|
||||
`PS2RECOMP_SCE_SYMBOL_DB` at that directory, only when you want to override the embedded
|
||||
snapshot.
|
||||
|
||||
This path is meant to recover names such as CD/DVD, pad, DMA, GS, kernel, and libc SDK
|
||||
functions so they can be classified before the expensive analysis passes run.
|
||||
|
||||
The current database was built from PS2 games with debug information, primarily the
|
||||
Japanese set, and depends on samples that retained relocations. Treat the result as a
|
||||
high-confidence hint rather than a complete SDK catalog: it can miss SDK variants that
|
||||
were not present in the sampled games, and ambiguous matches are intentionally ignored.
|
||||
|
||||
### 4. Ghidra Integration
|
||||
1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
2. Run it in Ghidra to export a CSV map of all functions.
|
||||
3. Add the CSV path to your TOML: `ghidra_output = "path/to/map.csv"`.
|
||||
4. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
|
||||
3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`.
|
||||
4. Run the recompiler with that exported TOML.
|
||||
5. The recompiler will prioritize Ghidra's boundaries over its own heuristics.
|
||||
|
||||
## Key Features
|
||||
|
||||
* Analyzes PS2 ELF binaries to extract symbols, functions, and structure
|
||||
* Identifies common library functions that should be stubbed
|
||||
* Flags system functions that should be skipped during recompilation
|
||||
* Reports risky instruction patterns for manual review without auto-skipping functions
|
||||
* Detects potential instruction patterns that may need patching
|
||||
* Generates a ready-to-use TOML configuration file for PS2Recomp
|
||||
|
||||
## Using the Analyzer
|
||||
```bash
|
||||
ps2_analyzer <input_elf> <output_toml>
|
||||
ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]
|
||||
```
|
||||
|
||||
### Parameters:
|
||||
|
||||
* `input_elf`: Path to the PS2 ELF file.
|
||||
* `output_toml`: Path where the generated TOML configuration will be saved.
|
||||
* `sce_symbol_db_dir`: Optional override path to a directory containing `symbols.json` and `tree.json`.
|
||||
|
||||
## Example Workflow
|
||||
1. Run the analyzer on your game:
|
||||
`ps2_analyzer game.elf config.toml`
|
||||
2. (Optional) Open `game.elf` in Ghidra, run the export script, and update `config.toml` with the CSV path.
|
||||
3. Run the recompiler:
|
||||
`ps2recomp config.toml`
|
||||
1. Open `game.elf` in Ghidra.
|
||||
2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`.
|
||||
3. Use the exported TOML and CSV.
|
||||
4. Run the recompiler: `ps2recomp config.toml`
|
||||
|
||||
Fallback:
|
||||
1. Run `ps2_analyzer game.elf config.toml`.
|
||||
2. Use that TOML only for quick bring-up or symbol-rich builds.
|
||||
|
||||
## Generated Configuration
|
||||
The tool creates a TOML file with the following sections:
|
||||
* `[general]`: Paths to ELF and Ghidra maps.
|
||||
* `stubs`: List of library functions to be replaced by C++ stubs.
|
||||
* `skip`: List of functions to be ignored (entry points, initialization).
|
||||
* `stubs`: Runtime-known functions to be replaced by C++ stubs or syscall handlers.
|
||||
* `untracked_stubs`: Detected library-like functions without runtime handlers. This is informational only and is ignored by the recompiler.
|
||||
* `entry_points`: Guest functions without runtime handlers that may be referenced by address.
|
||||
* `skip`: Legacy compatibility field. The analyzer no longer auto-populates it.
|
||||
* `[patches]`: Individual instructions that need to be replaced (SYSCALLs, COP0, etc.).
|
||||
|
||||
## Limitations
|
||||
|
||||
* Heuristics may not catch all special cases in highly optimized code.
|
||||
* Self-modifying code is flagged but requires manual review.
|
||||
* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries.
|
||||
|
||||
For more details on the recompilation process, see the [Main README](../README.md).
|
||||
For more details on the recompilation process, see the [Main README](../README.md).
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef PS2RECOMP_ANALYSIS_PASSES_H
|
||||
#define PS2RECOMP_ANALYSIS_PASSES_H
|
||||
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
class AnalysisPasses
|
||||
{
|
||||
public:
|
||||
static bool hasHardwareIOSignal(const std::vector<Instruction> &instructions);
|
||||
static bool hasLargeComplexMMISignal(const std::vector<Instruction> &instructions,
|
||||
size_t largeInstructionThreshold = 500);
|
||||
static bool hasSelfModifyingSignal(const std::vector<Instruction> &instructions,
|
||||
const std::vector<Section> §ions);
|
||||
static std::vector<JumpTable> detectJumpTables(
|
||||
const std::vector<Instruction> &instructions,
|
||||
const std::vector<Section> §ions,
|
||||
const std::function<bool(uint32_t, uint32_t &)> &readWord);
|
||||
static std::unordered_set<std::string> findRecursiveFunctions(
|
||||
const std::unordered_map<std::string, std::vector<std::string>> &callGraph);
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_ANALYSIS_PASSES_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
|
||||
#define PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
|
||||
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct ElfAnalysisContext
|
||||
{
|
||||
std::vector<Function> functions;
|
||||
std::vector<Symbol> symbols;
|
||||
std::vector<Section> sections;
|
||||
std::vector<Relocation> relocations;
|
||||
|
||||
std::unordered_map<uint32_t, size_t> functionIndexByStart;
|
||||
mutable std::unordered_map<uint32_t, std::vector<Instruction>> instructionCache;
|
||||
|
||||
void clear();
|
||||
void buildFunctionIndex();
|
||||
void clearInstructionCache();
|
||||
Function *findFunction(uint32_t start);
|
||||
const Function *findFunction(uint32_t start) const;
|
||||
Function *findFunctionContaining(uint32_t address);
|
||||
const Function *findFunctionContaining(uint32_t address) const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_ELF_ANALYSIS_CONTEXT_H
|
||||
@@ -1,6 +1,9 @@
|
||||
#ifndef PS2RECOMP_ELF_ANALYZER_H
|
||||
#define PS2RECOMP_ELF_ANALYZER_H
|
||||
|
||||
#include "ps2recomp/elf_analysis_context.h"
|
||||
#include "ps2recomp/function_classifier.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
@@ -9,6 +12,7 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
@@ -31,19 +35,29 @@ namespace ps2recomp
|
||||
explicit ElfAnalyzer(const std::string &elfPath);
|
||||
~ElfAnalyzer();
|
||||
|
||||
public:
|
||||
void setSceSymbolDatabasePath(const std::string &databasePath);
|
||||
bool analyze();
|
||||
bool generateToml(const std::string &outputPath);
|
||||
bool importGhidraMap(const std::string &csvPath);
|
||||
|
||||
public:
|
||||
const std::vector<Function> &getFunctions() const;
|
||||
|
||||
public:
|
||||
bool isLibrarySymbolNameForHeuristics(const std::string &name) const;
|
||||
static bool isReliableSymbolNameForHeuristics(const std::string &name);
|
||||
static bool isSystemSymbolNameForHeuristics(const std::string &name);
|
||||
static bool shouldAutoSkipNameForHeuristics(const std::string &name);
|
||||
static bool shouldSkipSystemSymbolForHeuristics(const std::string &name, const std::unordered_set<std::string> &forcedRecompileNames);
|
||||
|
||||
public:
|
||||
static int findEntryFunctionIndexForHeuristics(const std::vector<Function> &functions, uint32_t entryAddress);
|
||||
static int findFallbackEntryFunctionIndexForHeuristics(const std::vector<Function> &functions);
|
||||
|
||||
public:
|
||||
static bool hasHardwareIOSignalForHeuristics(const std::vector<Instruction> &instructions);
|
||||
static bool hasLargeComplexMMISignalForHeuristics(const std::vector<Instruction> &instructions, size_t largeInstructionThreshold = 500);
|
||||
static bool hasSelfModifyingSignalForHeuristics(const std::vector<Instruction> &instructions, const std::vector<Section> §ions);
|
||||
static bool shouldSkipForPatchDensityForHeuristics(const std::string &functionName, uint32_t functionSizeBytes, size_t patchCount, bool isLibraryFunction);
|
||||
|
||||
public:
|
||||
static std::vector<JumpTable> detectJumpTablesForHeuristics(const std::vector<Instruction> &instructions, const std::vector<Section> §ions, const std::function<bool(uint32_t, uint32_t &)> &readWord);
|
||||
static std::unordered_set<std::string> findRecursiveFunctionsForHeuristics(const std::unordered_map<std::string, std::vector<std::string>> &callGraph);
|
||||
|
||||
@@ -52,27 +66,43 @@ namespace ps2recomp
|
||||
std::unique_ptr<ElfParser> m_elfParser;
|
||||
std::unique_ptr<R5900Decoder> m_decoder;
|
||||
|
||||
std::vector<Function> m_functions;
|
||||
std::vector<Symbol> m_symbols;
|
||||
std::vector<Section> m_sections;
|
||||
std::vector<Relocation> m_relocations;
|
||||
ElfAnalysisContext m_context;
|
||||
|
||||
std::unordered_set<std::string> m_libFunctions;
|
||||
std::unordered_set<std::string> m_skipFunctions;
|
||||
std::unordered_set<std::string> m_untrackedStubFunctions;
|
||||
std::unordered_set<uint32_t> m_forceRecompileStarts;
|
||||
std::unordered_set<std::string> m_knownLibNames;
|
||||
std::unordered_set<std::string> m_sceSdkFunctionNames;
|
||||
|
||||
FunctionClassifier m_classifier;
|
||||
|
||||
std::unordered_map<std::string, std::set<std::string>> m_functionDataUsage;
|
||||
std::unordered_map<uint32_t, std::string> m_commonDataAccess;
|
||||
|
||||
std::map<uint32_t, uint32_t> m_patches;
|
||||
std::map<uint32_t, std::string> m_patchReasons;
|
||||
|
||||
std::unordered_map<uint32_t, CFG> m_functionCFGs;
|
||||
std::vector<JumpTable> m_jumpTables;
|
||||
std::unordered_map<uint32_t, std::vector<FunctionCall>> m_functionCalls;
|
||||
std::map<uint32_t, std::string> m_performanceCriticalReasons;
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> m_mmioByInstructionAddress;
|
||||
|
||||
void initializeLibraryFunctions();
|
||||
std::string m_sceSymbolDatabasePath;
|
||||
|
||||
bool loadElf();
|
||||
void buildFunctionIndex();
|
||||
void decodeAllFunctionsOnce();
|
||||
void classifyFunctions();
|
||||
void runDataUsagePass();
|
||||
void runPatchDetectionPass();
|
||||
void runControlFlowPass();
|
||||
void runJumpTablePass();
|
||||
void runPerformancePass();
|
||||
void runSignaturePass() const;
|
||||
void printAnalysisSummary() const;
|
||||
|
||||
void discoverSceSdkSymbols();
|
||||
void analyzeEntryPoint();
|
||||
void analyzeLibraryFunctions();
|
||||
void analyzeDataUsage();
|
||||
@@ -94,7 +124,7 @@ namespace ps2recomp
|
||||
|
||||
void analyzeControlFlow();
|
||||
void detectJumpTables();
|
||||
void analyzePerformanceCriticalPaths() const;
|
||||
void analyzePerformanceCriticalPaths();
|
||||
void identifyRecursiveFunctions();
|
||||
void analyzeRegisterUsage() const;
|
||||
void analyzeFunctionSignatures() const;
|
||||
@@ -105,16 +135,15 @@ namespace ps2recomp
|
||||
bool identifyStringOperationPattern(const Function &func) const;
|
||||
bool identifyMathPattern(const Function &func) const;
|
||||
|
||||
bool isSystemFunction(const std::string &name) const;
|
||||
bool isLibraryFunction(const std::string &name) const;
|
||||
void clearDecodedInstructionCache();
|
||||
const std::vector<Instruction> &getDecodedInstructions(const Function &function) const;
|
||||
std::vector<Instruction> decodeFunction(const Function &function) const;
|
||||
CFG buildCFG(const Function &function) const;
|
||||
std::string formatAddress(uint32_t address) const;
|
||||
std::string escapeBackslashes(const std::string &path);
|
||||
bool hasMMIInstructions(const Function &function) const;
|
||||
bool hasVUInstructions(const Function &function) const;
|
||||
bool shouldAutoSkipByHeuristic(const Function &function) const;
|
||||
bool identifyFunctionType(const Function &function);
|
||||
void identifyFunctionType(const Function &function) const;
|
||||
void categorizeFunction(Function &function);
|
||||
uint32_t getSuccessor(const Instruction &inst, uint32_t currentAddr);
|
||||
bool isSelfModifyingCode(const Function &function) const;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef PS2RECOMP_FUNCTION_CLASSIFIER_H
|
||||
#define PS2RECOMP_FUNCTION_CLASSIFIER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
class FunctionClassifier
|
||||
{
|
||||
public:
|
||||
FunctionClassifier();
|
||||
|
||||
void setSceSdkFunctionNames(const std::unordered_set<std::string> *names);
|
||||
bool isLibraryFunction(const std::string &name) const;
|
||||
static bool hasRuntimeHandler(const std::string &name);
|
||||
|
||||
static bool isReliableSymbolName(const std::string &name);
|
||||
static bool hasPs2ApiPrefix(const std::string &name);
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> m_knownLibNames;
|
||||
const std::unordered_set<std::string> *m_sceSdkFunctionNames = nullptr;
|
||||
|
||||
void initializeKnownLibraryFunctions();
|
||||
static bool matchesKernelRuntimeName(const std::string &name);
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_FUNCTION_CLASSIFIER_H
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
#ifndef PS2RECOMP_SCE_SYMBOL_SCANNER_H
|
||||
#define PS2RECOMP_SCE_SYMBOL_SCANNER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Section;
|
||||
|
||||
struct SceSymbolMatch
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t size = 0;
|
||||
std::string name;
|
||||
std::string library;
|
||||
std::string hash;
|
||||
uint32_t variantHash = 0;
|
||||
};
|
||||
|
||||
class SceSymbolScanner
|
||||
{
|
||||
public:
|
||||
SceSymbolScanner();
|
||||
~SceSymbolScanner();
|
||||
|
||||
bool loadDatabase(const std::string &databasePath);
|
||||
std::vector<SceSymbolMatch> scan(const std::vector<Section> §ions) const;
|
||||
const std::string &lastError() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_SCE_SYMBOL_SCANNER_H
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef PS2RECOMP_TOML_GENERATOR_H
|
||||
#define PS2RECOMP_TOML_GENERATOR_H
|
||||
|
||||
#include "ps2recomp/elf_analysis_context.h"
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct TomlGeneratorInput
|
||||
{
|
||||
const std::string &elfPath;
|
||||
const ElfAnalysisContext &context;
|
||||
const std::unordered_set<std::string> &libFunctions;
|
||||
const std::unordered_set<std::string> &untrackedStubFunctions;
|
||||
const std::unordered_map<uint32_t, uint32_t> &mmioByInstructionAddress;
|
||||
const std::vector<JumpTable> &jumpTables;
|
||||
const std::map<uint32_t, uint32_t> &patches;
|
||||
const std::map<uint32_t, std::string> &patchReasons;
|
||||
const std::map<uint32_t, std::string> &performanceCriticalReasons;
|
||||
};
|
||||
|
||||
class TomlGenerator
|
||||
{
|
||||
public:
|
||||
static bool generate(const TomlGeneratorInput &input, const std::string &outputPath);
|
||||
|
||||
private:
|
||||
static std::string escapeBackslashes(const std::string &path);
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_TOML_GENERATOR_H
|
||||
@@ -0,0 +1,446 @@
|
||||
#include "ps2recomp/analysis_passes.h"
|
||||
|
||||
#include "ps2recomp/instructions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
bool AnalysisPasses::hasHardwareIOSignal(const std::vector<Instruction> &instructions)
|
||||
{
|
||||
for (const auto &inst : instructions)
|
||||
{
|
||||
if (inst.opcode == OPCODE_LUI)
|
||||
{
|
||||
const uint32_t upperAddr = inst.immediate << 16;
|
||||
if ((upperAddr >= 0x10000000 && upperAddr < 0x14000000) || // I/O area
|
||||
(upperAddr >= 0x1F800000 && upperAddr < 0x1F900000)) // Scratchpad RAM
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AnalysisPasses::hasLargeComplexMMISignal(const std::vector<Instruction> &instructions,
|
||||
size_t largeInstructionThreshold)
|
||||
{
|
||||
if (instructions.size() <= largeInstructionThreshold)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto &inst : instructions)
|
||||
{
|
||||
if (inst.isMMI &&
|
||||
inst.opcode == OPCODE_MMI &&
|
||||
(inst.function == MMI_MMI0 || inst.function == MMI_MMI1 ||
|
||||
inst.function == MMI_MMI2 || inst.function == MMI_MMI3))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AnalysisPasses::hasSelfModifyingSignal(const std::vector<Instruction> &instructions,
|
||||
const std::vector<Section> §ions)
|
||||
{
|
||||
for (size_t i = 0; i < instructions.size(); i++)
|
||||
{
|
||||
const auto &inst = instructions[i];
|
||||
if (!(inst.opcode == OPCODE_SW || inst.opcode == OPCODE_SH ||
|
||||
inst.opcode == OPCODE_SB || inst.opcode == OPCODE_SQ))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t baseAddr = 0;
|
||||
for (int j = static_cast<int>(i) - 1; j >= 0 && j >= static_cast<int>(i) - 5; j--)
|
||||
{
|
||||
const auto &prevInst = instructions[static_cast<size_t>(j)];
|
||||
if (prevInst.opcode == OPCODE_LUI && prevInst.rt == inst.rs)
|
||||
{
|
||||
baseAddr = prevInst.immediate << 16;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseAddr == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t targetAddr = baseAddr + static_cast<int16_t>(inst.immediate);
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (section.isCode &&
|
||||
targetAddr >= section.address &&
|
||||
targetAddr < section.address + section.size)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<JumpTable> AnalysisPasses::detectJumpTables(
|
||||
const std::vector<Instruction> &instructions,
|
||||
const std::vector<Section> §ions,
|
||||
const std::function<bool(uint32_t, uint32_t &)> &readWord)
|
||||
{
|
||||
std::vector<JumpTable> jumpTables;
|
||||
|
||||
auto addSignedImm16 = [](uint32_t hiPart, uint16_t imm16) -> uint32_t
|
||||
{
|
||||
return hiPart + static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(imm16)));
|
||||
};
|
||||
|
||||
auto orUnsignedImm16 = [](uint32_t hiPart, uint16_t imm16) -> uint32_t
|
||||
{
|
||||
return hiPart | static_cast<uint32_t>(imm16);
|
||||
};
|
||||
|
||||
auto looksLikeCodeTarget = [§ions](uint32_t addr) -> bool
|
||||
{
|
||||
if (addr == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sections.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const auto §ion : sections)
|
||||
{
|
||||
if (!section.isCode)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t sectionEnd = section.address + section.size;
|
||||
if (addr >= section.address && addr < sectionEnd)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
auto readJumpEntryCandidate = [&](uint32_t entryAddr, bool isLoadDouble, uint32_t &outTarget) -> bool
|
||||
{
|
||||
outTarget = 0;
|
||||
|
||||
uint32_t w0 = 0;
|
||||
if (!readWord(entryAddr, w0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLoadDouble)
|
||||
{
|
||||
outTarget = w0;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t w1 = 0;
|
||||
if (!readWord(entryAddr + 4u, w1))
|
||||
{
|
||||
outTarget = w0;
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool w0Looks = looksLikeCodeTarget(w0);
|
||||
const bool w1Looks = looksLikeCodeTarget(w1);
|
||||
|
||||
if (w0Looks && !w1Looks)
|
||||
{
|
||||
outTarget = w0;
|
||||
return true;
|
||||
}
|
||||
if (w1Looks && !w0Looks)
|
||||
{
|
||||
outTarget = w1;
|
||||
return true;
|
||||
}
|
||||
outTarget = w0;
|
||||
return true;
|
||||
};
|
||||
|
||||
auto tryBuildTable = [&](uint32_t baseAddr, uint32_t baseReg, uint32_t numEntries, uint32_t strideBytes, bool isLoadDouble) -> std::optional<JumpTable>
|
||||
{
|
||||
JumpTable jumpTable;
|
||||
jumpTable.address = baseAddr;
|
||||
jumpTable.baseRegister = baseReg;
|
||||
|
||||
uint32_t validCodeTargets = 0;
|
||||
uint32_t totalRead = 0;
|
||||
|
||||
for (uint32_t e = 0; e < numEntries; e++)
|
||||
{
|
||||
const uint32_t entryAddr = baseAddr + (e * strideBytes);
|
||||
|
||||
uint32_t targetAddr = 0;
|
||||
if (!readJumpEntryCandidate(entryAddr, isLoadDouble, targetAddr))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalRead++;
|
||||
|
||||
if (looksLikeCodeTarget(targetAddr))
|
||||
{
|
||||
validCodeTargets++;
|
||||
}
|
||||
|
||||
JumpTableEntry entry;
|
||||
entry.index = e;
|
||||
entry.target = targetAddr;
|
||||
jumpTable.entries.push_back(entry);
|
||||
}
|
||||
|
||||
if (jumpTable.entries.empty())
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (sections.empty())
|
||||
{
|
||||
ok = (totalRead >= 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
ok = (validCodeTargets >= 2) &&
|
||||
(totalRead >= 2) &&
|
||||
(validCodeTargets * 2 >= totalRead);
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return jumpTable;
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < instructions.size(); i++)
|
||||
{
|
||||
const auto &inst = instructions[i];
|
||||
|
||||
if (inst.opcode != OPCODE_SLTIU || i + 2 >= instructions.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &nextInst = instructions[i + 1];
|
||||
if (nextInst.opcode != OPCODE_BNE && nextInst.opcode != OPCODE_BEQ)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (size_t j = i + 2; j < std::min(i + 10, instructions.size()); j++)
|
||||
{
|
||||
const auto &loadInst = instructions[j];
|
||||
const bool isLoadWord = (loadInst.opcode == OPCODE_LW);
|
||||
const bool isLoadDouble = (loadInst.opcode == OPCODE_LD);
|
||||
|
||||
if ((!isLoadWord && !isLoadDouble) || j + 1 >= instructions.size())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto &jumpInst = instructions[j + 1];
|
||||
if (jumpInst.opcode != OPCODE_SPECIAL ||
|
||||
jumpInst.function != SPECIAL_JR ||
|
||||
jumpInst.rs != loadInst.rt)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t numEntries = inst.immediate;
|
||||
if (numEntries == 0 || numEntries >= 1000)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
uint32_t baseAddr = 0;
|
||||
for (int k = static_cast<int>(j) - 1; k >= static_cast<int>(i); k--)
|
||||
{
|
||||
const auto &addrInst = instructions[static_cast<size_t>(k)];
|
||||
if (addrInst.opcode != OPCODE_LUI)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t hiPart = (addrInst.immediate << 16);
|
||||
|
||||
if (static_cast<size_t>(k + 1) < instructions.size())
|
||||
{
|
||||
const auto &offsetInst = instructions[static_cast<size_t>(k + 1)];
|
||||
const bool isAddiuOrOri = (offsetInst.opcode == OPCODE_ADDIU || offsetInst.opcode == OPCODE_ORI);
|
||||
|
||||
if (isAddiuOrOri &&
|
||||
offsetInst.rs == addrInst.rt &&
|
||||
offsetInst.rt == loadInst.rs)
|
||||
{
|
||||
if (offsetInst.opcode == OPCODE_ADDIU)
|
||||
{
|
||||
baseAddr = addSignedImm16(hiPart, offsetInst.immediate);
|
||||
}
|
||||
else
|
||||
{
|
||||
baseAddr = orUnsignedImm16(hiPart, offsetInst.immediate);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (addrInst.rt == loadInst.rs)
|
||||
{
|
||||
baseAddr = addSignedImm16(hiPart, loadInst.immediate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseAddr == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t preferredStride = isLoadDouble ? 8u : 4u;
|
||||
|
||||
std::optional<JumpTable> table = tryBuildTable(baseAddr, loadInst.rs, numEntries, preferredStride, isLoadDouble);
|
||||
if (!table && isLoadDouble)
|
||||
{
|
||||
table = tryBuildTable(baseAddr, loadInst.rs, numEntries, 4u, isLoadDouble);
|
||||
}
|
||||
|
||||
if (table)
|
||||
{
|
||||
jumpTables.push_back(std::move(*table));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return jumpTables;
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> AnalysisPasses::findRecursiveFunctions(
|
||||
const std::unordered_map<std::string, std::vector<std::string>> &callGraph)
|
||||
{
|
||||
std::unordered_set<std::string> nodes;
|
||||
for (const auto &[caller, callees] : callGraph)
|
||||
{
|
||||
nodes.insert(caller);
|
||||
for (const auto &callee : callees)
|
||||
{
|
||||
nodes.insert(callee);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, int> index;
|
||||
std::unordered_map<std::string, int> lowlink;
|
||||
std::unordered_set<std::string> onStack;
|
||||
std::vector<std::string> stack;
|
||||
|
||||
index.reserve(nodes.size());
|
||||
lowlink.reserve(nodes.size());
|
||||
onStack.reserve(nodes.size());
|
||||
stack.reserve(nodes.size());
|
||||
|
||||
int currentIndex = 0;
|
||||
std::vector<std::vector<std::string>> sccs;
|
||||
sccs.reserve(nodes.size());
|
||||
|
||||
std::function<void(const std::string &)> strongconnect;
|
||||
strongconnect = [&](const std::string &v)
|
||||
{
|
||||
index[v] = currentIndex;
|
||||
lowlink[v] = currentIndex;
|
||||
currentIndex++;
|
||||
|
||||
stack.push_back(v);
|
||||
onStack.insert(v);
|
||||
|
||||
auto it = callGraph.find(v);
|
||||
if (it != callGraph.end())
|
||||
{
|
||||
for (const auto &w : it->second)
|
||||
{
|
||||
if (!index.contains(w))
|
||||
{
|
||||
strongconnect(w);
|
||||
lowlink[v] = std::min(lowlink[v], lowlink[w]);
|
||||
}
|
||||
else if (onStack.contains(w))
|
||||
{
|
||||
lowlink[v] = std::min(lowlink[v], index[w]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lowlink[v] == index[v])
|
||||
{
|
||||
std::vector<std::string> scc;
|
||||
while (!stack.empty())
|
||||
{
|
||||
std::string w = stack.back();
|
||||
stack.pop_back();
|
||||
onStack.erase(w);
|
||||
scc.push_back(w);
|
||||
if (w == v)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sccs.push_back(std::move(scc));
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto &name : nodes)
|
||||
{
|
||||
if (!index.contains(name))
|
||||
{
|
||||
strongconnect(name);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> recursive;
|
||||
for (const auto &scc : sccs)
|
||||
{
|
||||
if (scc.size() > 1)
|
||||
{
|
||||
recursive.insert(scc.begin(), scc.end());
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string &name = scc[0];
|
||||
auto it = callGraph.find(name);
|
||||
if (it == callGraph.end())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::find(it->second.begin(), it->second.end(), name) != it->second.end())
|
||||
{
|
||||
recursive.insert(name);
|
||||
}
|
||||
}
|
||||
|
||||
return recursive;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,11 @@ void printUsage()
|
||||
{
|
||||
std::cout << "PS2 ELF Analyzer\n";
|
||||
std::cout << "A tool to analyze PS2 ELF files and generate TOML configuration for PS2Recomp\n\n";
|
||||
std::cout << "Usage: ps2_analyzer <input_elf> <output_toml>\n";
|
||||
std::cout << "Usage: ps2_analyzer <input_elf> <output_toml> [sce_symbol_db_dir]\n";
|
||||
std::cout << " input_elf Path to the PS2 ELF file\n";
|
||||
std::cout << " output_toml Path to output TOML configuration file\n";
|
||||
std::cout << " sce_symbol_db_dir Optional override directory containing symbols.json and tree.json\n";
|
||||
std::cout << " If omitted, the embedded SCE symbol database is used\n";
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
@@ -21,15 +23,28 @@ int main(int argc, char *argv[])
|
||||
|
||||
std::string elfPath = argv[1];
|
||||
std::string tomlPath = argv[2];
|
||||
std::string sceSymbolDbPath = argc >= 4 ? argv[3] : "";
|
||||
|
||||
std::cout << "PS2 ELF Analyzer\n";
|
||||
std::cout << "----------------\n";
|
||||
std::cout << "Input ELF: " << elfPath << "\n";
|
||||
std::cout << "Output TOML: " << tomlPath << "\n\n";
|
||||
if (!sceSymbolDbPath.empty())
|
||||
{
|
||||
std::cout << "SCE symbol DB: " << sceSymbolDbPath << "\n\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cout << "SCE symbol DB: embedded\n\n";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ps2recomp::ElfAnalyzer analyzer(elfPath);
|
||||
if (!sceSymbolDbPath.empty())
|
||||
{
|
||||
analyzer.setSceSymbolDatabasePath(sceSymbolDbPath);
|
||||
}
|
||||
|
||||
if (!analyzer.analyze())
|
||||
{
|
||||
@@ -55,4 +70,4 @@ int main(int argc, char *argv[])
|
||||
std::cerr << "Error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "ps2recomp/elf_analysis_context.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
void ElfAnalysisContext::clear()
|
||||
{
|
||||
functions.clear();
|
||||
symbols.clear();
|
||||
sections.clear();
|
||||
relocations.clear();
|
||||
functionIndexByStart.clear();
|
||||
instructionCache.clear();
|
||||
}
|
||||
|
||||
void ElfAnalysisContext::buildFunctionIndex()
|
||||
{
|
||||
functionIndexByStart.clear();
|
||||
functionIndexByStart.reserve(functions.size());
|
||||
for (size_t index = 0; index < functions.size(); ++index)
|
||||
{
|
||||
functionIndexByStart[functions[index].start] = index;
|
||||
}
|
||||
}
|
||||
|
||||
void ElfAnalysisContext::clearInstructionCache()
|
||||
{
|
||||
instructionCache.clear();
|
||||
for (auto &func : functions)
|
||||
{
|
||||
func.instructions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Function *ElfAnalysisContext::findFunction(uint32_t start)
|
||||
{
|
||||
const auto it = functionIndexByStart.find(start);
|
||||
if (it == functionIndexByStart.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &functions[it->second];
|
||||
}
|
||||
|
||||
const Function *ElfAnalysisContext::findFunction(uint32_t start) const
|
||||
{
|
||||
const auto it = functionIndexByStart.find(start);
|
||||
if (it == functionIndexByStart.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &functions[it->second];
|
||||
}
|
||||
|
||||
Function *ElfAnalysisContext::findFunctionContaining(uint32_t address)
|
||||
{
|
||||
auto it = std::find_if(functions.begin(), functions.end(),
|
||||
[address](const Function &function)
|
||||
{
|
||||
return function.start <= address && address < function.end;
|
||||
});
|
||||
if (it == functions.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &(*it);
|
||||
}
|
||||
|
||||
const Function *ElfAnalysisContext::findFunctionContaining(uint32_t address) const
|
||||
{
|
||||
auto it = std::find_if(functions.begin(), functions.end(),
|
||||
[address](const Function &function)
|
||||
{
|
||||
return function.start <= address && address < function.end;
|
||||
});
|
||||
if (it == functions.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &(*it);
|
||||
}
|
||||
}
|
||||
+482
-1166
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,218 @@
|
||||
#include "ps2recomp/function_classifier.h"
|
||||
|
||||
#include "ps2_runtime_calls.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <regex>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
FunctionClassifier::FunctionClassifier()
|
||||
{
|
||||
initializeKnownLibraryFunctions();
|
||||
}
|
||||
|
||||
void FunctionClassifier::setSceSdkFunctionNames(const std::unordered_set<std::string> *names)
|
||||
{
|
||||
m_sceSdkFunctionNames = names;
|
||||
}
|
||||
|
||||
bool FunctionClassifier::hasRuntimeHandler(const std::string &name)
|
||||
{
|
||||
return !ps2_runtime_calls::resolveSyscallName(name).empty() ||
|
||||
!ps2_runtime_calls::resolveStubName(name).empty();
|
||||
}
|
||||
|
||||
void FunctionClassifier::initializeKnownLibraryFunctions()
|
||||
{
|
||||
const std::vector<std::string> stdLibFuncs = {
|
||||
"printf", "sprintf", "snprintf", "fprintf", "vprintf", "vfprintf", "vsprintf", "vsnprintf",
|
||||
"puts", "putchar", "getchar", "gets", "fgets", "fputs", "scanf", "fscanf", "sscanf",
|
||||
"sprint", "sbprintf", "__mcmp", "__sbprintf", "__sprint", "__sprint_r",
|
||||
"malloc", "free", "calloc", "realloc", "aligned_alloc", "posix_memalign",
|
||||
"memcpy", "memset", "memmove", "memcmp", "memchr", "bcopy", "bzero",
|
||||
"strcpy", "strncpy", "strcat", "strncat", "strcmp", "strncmp", "strlen", "strstr",
|
||||
"strchr", "strrchr", "strdup", "strtok", "strtok_r", "strerror",
|
||||
"fopen", "fclose", "fread", "fwrite", "fseek", "ftell", "rewind", "fflush",
|
||||
"fgetc", "fgets", "feof", "ferror", "clearerr", "fileno", "tmpfile", "remove", "rename",
|
||||
"open", "close", "read", "write", "lseek", "stat", "fstat",
|
||||
"atoi", "atol", "atoll", "atof", "strtol", "strtoul", "strtoll", "strtoull", "strtod", "strtof",
|
||||
"rand", "srand", "random", "srandom", "drand48", "sqrt", "pow", "exp", "log", "log10",
|
||||
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh",
|
||||
"floor", "ceil", "fabs", "fmod", "frexp", "ldexp", "modf",
|
||||
"time", "ctime", "clock", "difftime", "mktime", "localtime", "gmtime", "asctime", "strftime",
|
||||
"gettimeofday", "nanosleep", "usleep",
|
||||
"abort", "exit", "_exit", "atexit", "system", "getpid", "fork", "waitpid",
|
||||
"qsort", "bsearch", "abs", "div", "labs", "ldiv", "llabs", "lldiv",
|
||||
"isalnum", "isalpha", "isdigit", "islower", "isupper", "isspace", "tolower", "toupper",
|
||||
"setjmp", "longjmp", "getenv", "setenv", "unsetenv",
|
||||
"perror", "fputc", "getc", "ungetc", "freopen", "setvbuf", "setbuf",
|
||||
"strnlen", "strspn", "strcspn", "strcasecmp", "strncasecmp"};
|
||||
|
||||
m_knownLibNames.insert(stdLibFuncs.begin(), stdLibFuncs.end());
|
||||
}
|
||||
|
||||
bool FunctionClassifier::hasPs2ApiPrefix(const std::string &name)
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<std::string> libraryPrefixes = {
|
||||
"sce", "Sce", "SCE",
|
||||
"sif", "Sif", "SIF",
|
||||
"gs", "Gs", "GS",
|
||||
"dma", "Dma", "DMA",
|
||||
"iop", "Iop", "IOP",
|
||||
"vif", "Vif", "VIF",
|
||||
"spu", "Spu", "SPU",
|
||||
"mc", "Mc", "MC",
|
||||
"libc", "Libc", "LIBC"};
|
||||
|
||||
std::string base = name;
|
||||
if (base[0] == '_' && base.size() > 1)
|
||||
{
|
||||
base = base.substr(1);
|
||||
}
|
||||
|
||||
auto hasSdkPrefixShape = [](const std::string &value, const std::string &prefix) -> bool
|
||||
{
|
||||
if (value.rfind(prefix, 0) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.size() == prefix.size())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return !std::islower(static_cast<unsigned char>(value[prefix.size()]));
|
||||
};
|
||||
|
||||
for (const auto &prefix : libraryPrefixes)
|
||||
{
|
||||
if (hasSdkPrefixShape(base, prefix))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FunctionClassifier::matchesKernelRuntimeName(const std::string &name)
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
static const std::regex kernelRuntimePattern(
|
||||
"^(?:(?:Create|Delete|Start|ExitDelete|Exit|Terminate|Suspend|Resume|Sleep|Wakeup|CancelWakeup|Change|Rotate|Release|Setup|Register|Query|Get|Set|Refer|Poll|Wait|Signal|Enable|Disable|Flush|Reset|Add|Init)(?:Thread|Sema|EventFlag|Alarm|Intc|IntcHandler2|Dmac|DmacHandler2|OsdConfigParam|MemorySize|VSyncFlag|Heap|TLS|Status|Cache|Syscall|TLB|TLBEntry|GsCrt)|EndOfHeap|GsGetIMR|GsPutIMR|Deci2Call|Sif[A-Za-z0-9_]+|i(?:SignalSema|PollSema|ReferSemaStatus|SetEventFlag|ClearEventFlag|PollEventFlag|ReferEventFlagStatus|WakeupThread|CancelWakeupThread|ReleaseWaitThread|SetAlarm|CancelAlarm|FlushCache|sceSifSetDma|sceSifSetDChain))$");
|
||||
return std::regex_match(name, kernelRuntimePattern);
|
||||
}
|
||||
|
||||
bool FunctionClassifier::isReliableSymbolName(const std::string &name)
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
auto startsWith = [&](const char *prefix) -> bool
|
||||
{
|
||||
return name.rfind(prefix, 0) == 0;
|
||||
};
|
||||
|
||||
if (startsWith("sub_") || startsWith("FUN_") || startsWith("func_") ||
|
||||
startsWith("entry_") || startsWith("function_") || startsWith("LAB_"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasAlpha = false;
|
||||
bool allHexOrPrefix = true;
|
||||
for (char c : name)
|
||||
{
|
||||
if (std::isalpha(static_cast<unsigned char>(c)))
|
||||
{
|
||||
hasAlpha = true;
|
||||
}
|
||||
|
||||
if (!(std::isxdigit(static_cast<unsigned char>(c)) || c == 'x' || c == 'X' || c == '_'))
|
||||
{
|
||||
allHexOrPrefix = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAlpha)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((startsWith("0x") || startsWith("0X")) && allHexOrPrefix)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FunctionClassifier::isLibraryFunction(const std::string &name) const
|
||||
{
|
||||
if (name.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isReliableSymbolName(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasRuntimeHandler(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string normalizedName = name;
|
||||
if (normalizedName[0] == '_' && normalizedName.size() > 1)
|
||||
{
|
||||
normalizedName = normalizedName.substr(1);
|
||||
}
|
||||
|
||||
if (hasRuntimeHandler(normalizedName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_sceSdkFunctionNames != nullptr &&
|
||||
(m_sceSdkFunctionNames->contains(name) ||
|
||||
m_sceSdkFunctionNames->contains(normalizedName)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (matchesKernelRuntimeName(normalizedName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_knownLibNames.contains(name) ||
|
||||
m_knownLibNames.contains(normalizedName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasPs2ApiPrefix(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
static const std::regex cLibPattern("^_*(mem|str|time|f?printf|f?scanf|malloc|free|calloc|realloc|atoi|itoa|rand|srand|abort|exit|atexit|getenv|system|bsearch|qsort|abs|labs|div|ldiv|mblen|mbtowc|wctomb|mbstowcs|wcstombs).*");
|
||||
return std::regex_match(normalizedName, cLibPattern);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,793 @@
|
||||
#include "ps2recomp/sce_symbol_scanner.h"
|
||||
#include "ps2recomp/sce_symbol_database_data.h"
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
namespace
|
||||
{
|
||||
enum class RelocationType
|
||||
{
|
||||
None,
|
||||
Mips26,
|
||||
MipsLo16,
|
||||
MipsHi16,
|
||||
Mips32,
|
||||
MipsGpRel16,
|
||||
MipsLiteral,
|
||||
};
|
||||
|
||||
struct MatchSymbolKey
|
||||
{
|
||||
std::string library;
|
||||
std::string name;
|
||||
std::string hash;
|
||||
uint32_t variantHash = 0;
|
||||
};
|
||||
|
||||
struct RelocationRecord
|
||||
{
|
||||
uint32_t offset = 0;
|
||||
RelocationType type = RelocationType::None;
|
||||
};
|
||||
|
||||
struct SymbolRecord
|
||||
{
|
||||
std::string library;
|
||||
std::string name;
|
||||
std::string hashText;
|
||||
std::array<uint8_t, 20> hash = {};
|
||||
uint32_t variantHash = 0;
|
||||
uint32_t size = 0;
|
||||
bool isFunction = false;
|
||||
std::vector<RelocationRecord> relocations;
|
||||
|
||||
size_t staticBitCount() const
|
||||
{
|
||||
size_t relocatedStaticBits = 0;
|
||||
for (const auto &relocation : relocations)
|
||||
{
|
||||
switch (relocation.type)
|
||||
{
|
||||
case RelocationType::None:
|
||||
relocatedStaticBits += 32;
|
||||
break;
|
||||
case RelocationType::Mips26:
|
||||
relocatedStaticBits += 6;
|
||||
break;
|
||||
case RelocationType::MipsLo16:
|
||||
case RelocationType::MipsHi16:
|
||||
case RelocationType::MipsGpRel16:
|
||||
case RelocationType::MipsLiteral:
|
||||
relocatedStaticBits += 16;
|
||||
break;
|
||||
case RelocationType::Mips32:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t totalBits = static_cast<size_t>(size) * 8;
|
||||
if (relocatedStaticBits >= totalBits)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return totalBits - relocatedStaticBits;
|
||||
}
|
||||
};
|
||||
|
||||
struct MatchNode;
|
||||
|
||||
struct MatchEdge
|
||||
{
|
||||
uint32_t value = 0;
|
||||
RelocationType relocationType = RelocationType::None;
|
||||
std::unique_ptr<MatchNode> child;
|
||||
};
|
||||
|
||||
struct MatchNode
|
||||
{
|
||||
uint32_t offset = 0;
|
||||
std::vector<MatchEdge> next;
|
||||
std::vector<MatchSymbolKey> symbols;
|
||||
};
|
||||
|
||||
struct Candidate
|
||||
{
|
||||
const SymbolRecord *symbol = nullptr;
|
||||
uint32_t address = 0;
|
||||
uint32_t actualSize = 0;
|
||||
};
|
||||
|
||||
static std::string toUpperAscii(std::string value)
|
||||
{
|
||||
for (char &ch : value)
|
||||
{
|
||||
ch = static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static RelocationType parseRelocationType(const std::string &value)
|
||||
{
|
||||
const std::string upper = toUpperAscii(value);
|
||||
if (upper == "NONE")
|
||||
{
|
||||
return RelocationType::None;
|
||||
}
|
||||
if (upper == "MIPS_26" || upper == "MIPS26")
|
||||
{
|
||||
return RelocationType::Mips26;
|
||||
}
|
||||
if (upper == "LO16" || upper == "MIPS_LO16" || upper == "MIPSLO16")
|
||||
{
|
||||
return RelocationType::MipsLo16;
|
||||
}
|
||||
if (upper == "HI16" || upper == "MIPS_HI16" || upper == "MIPSHI16")
|
||||
{
|
||||
return RelocationType::MipsHi16;
|
||||
}
|
||||
if (upper == "MIPS_32" || upper == "MIPS32")
|
||||
{
|
||||
return RelocationType::Mips32;
|
||||
}
|
||||
if (upper == "MIPS_GPREL16" || upper == "MIPSGPREL16")
|
||||
{
|
||||
return RelocationType::MipsGpRel16;
|
||||
}
|
||||
if (upper == "MIPS_LITERAL" || upper == "MIPSLITERAL")
|
||||
{
|
||||
return RelocationType::MipsLiteral;
|
||||
}
|
||||
return RelocationType::None;
|
||||
}
|
||||
|
||||
static uint32_t relocationMask(RelocationType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RelocationType::None:
|
||||
return 0xFFFFFFFFu;
|
||||
case RelocationType::Mips26:
|
||||
return 0xFC000000u;
|
||||
case RelocationType::MipsLo16:
|
||||
case RelocationType::MipsHi16:
|
||||
case RelocationType::MipsGpRel16:
|
||||
case RelocationType::MipsLiteral:
|
||||
return 0xFFFF0000u;
|
||||
case RelocationType::Mips32:
|
||||
return 0u;
|
||||
}
|
||||
return 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
static uint32_t readLe32(const uint8_t *data)
|
||||
{
|
||||
return static_cast<uint32_t>(data[0]) |
|
||||
(static_cast<uint32_t>(data[1]) << 8) |
|
||||
(static_cast<uint32_t>(data[2]) << 16) |
|
||||
(static_cast<uint32_t>(data[3]) << 24);
|
||||
}
|
||||
|
||||
static void writeLe32(uint8_t *data, uint32_t value)
|
||||
{
|
||||
data[0] = static_cast<uint8_t>(value & 0xFFu);
|
||||
data[1] = static_cast<uint8_t>((value >> 8) & 0xFFu);
|
||||
data[2] = static_cast<uint8_t>((value >> 16) & 0xFFu);
|
||||
data[3] = static_cast<uint8_t>((value >> 24) & 0xFFu);
|
||||
}
|
||||
|
||||
static uint32_t disabledRelocationValue(RelocationType type, uint32_t value)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RelocationType::None:
|
||||
return value;
|
||||
case RelocationType::Mips26:
|
||||
return value & 0xFC000000u;
|
||||
case RelocationType::MipsLo16:
|
||||
case RelocationType::MipsHi16:
|
||||
case RelocationType::MipsGpRel16:
|
||||
case RelocationType::MipsLiteral:
|
||||
return value & 0xFFFF0000u;
|
||||
case RelocationType::Mips32:
|
||||
return 0u;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string toHex8(uint32_t value)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << std::hex;
|
||||
stream.width(8);
|
||||
stream.fill('0');
|
||||
stream << value;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
static std::string makeSymbolKey(const std::string &library,
|
||||
const std::string &name,
|
||||
const std::string &hash,
|
||||
uint32_t variantHash)
|
||||
{
|
||||
return library + '\n' + name + '\n' + hash + '\n' + toHex8(variantHash);
|
||||
}
|
||||
|
||||
static std::string makeSymbolKey(const SymbolRecord &symbol)
|
||||
{
|
||||
return makeSymbolKey(symbol.library, symbol.name, symbol.hashText, symbol.variantHash);
|
||||
}
|
||||
|
||||
static uint8_t hexNibble(char ch)
|
||||
{
|
||||
if (ch >= '0' && ch <= '9')
|
||||
{
|
||||
return static_cast<uint8_t>(ch - '0');
|
||||
}
|
||||
if (ch >= 'a' && ch <= 'f')
|
||||
{
|
||||
return static_cast<uint8_t>(10 + ch - 'a');
|
||||
}
|
||||
if (ch >= 'A' && ch <= 'F')
|
||||
{
|
||||
return static_cast<uint8_t>(10 + ch - 'A');
|
||||
}
|
||||
throw std::runtime_error("invalid hex digit");
|
||||
}
|
||||
|
||||
static std::array<uint8_t, 20> parseSha1(const std::string &hex)
|
||||
{
|
||||
if (hex.size() != 40)
|
||||
{
|
||||
throw std::runtime_error("invalid SHA-1 length");
|
||||
}
|
||||
|
||||
std::array<uint8_t, 20> bytes = {};
|
||||
for (size_t i = 0; i < bytes.size(); ++i)
|
||||
{
|
||||
bytes[i] = static_cast<uint8_t>((hexNibble(hex[i * 2]) << 4) |
|
||||
hexNibble(hex[i * 2 + 1]));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static uint32_t rotateLeft(uint32_t value, uint32_t bits)
|
||||
{
|
||||
return (value << bits) | (value >> (32 - bits));
|
||||
}
|
||||
|
||||
static std::array<uint8_t, 20> sha1(const std::vector<uint8_t> &data)
|
||||
{
|
||||
std::vector<uint8_t> message = data;
|
||||
const uint64_t bitLength = static_cast<uint64_t>(message.size()) * 8u;
|
||||
|
||||
message.push_back(0x80u);
|
||||
while ((message.size() % 64) != 56)
|
||||
{
|
||||
message.push_back(0u);
|
||||
}
|
||||
|
||||
for (int shift = 56; shift >= 0; shift -= 8)
|
||||
{
|
||||
message.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xFFu));
|
||||
}
|
||||
|
||||
uint32_t h0 = 0x67452301u;
|
||||
uint32_t h1 = 0xEFCDAB89u;
|
||||
uint32_t h2 = 0x98BADCFEu;
|
||||
uint32_t h3 = 0x10325476u;
|
||||
uint32_t h4 = 0xC3D2E1F0u;
|
||||
|
||||
for (size_t chunk = 0; chunk < message.size(); chunk += 64)
|
||||
{
|
||||
std::array<uint32_t, 80> w = {};
|
||||
for (size_t i = 0; i < 16; ++i)
|
||||
{
|
||||
const size_t base = chunk + i * 4;
|
||||
w[i] = (static_cast<uint32_t>(message[base]) << 24) |
|
||||
(static_cast<uint32_t>(message[base + 1]) << 16) |
|
||||
(static_cast<uint32_t>(message[base + 2]) << 8) |
|
||||
static_cast<uint32_t>(message[base + 3]);
|
||||
}
|
||||
for (size_t i = 16; i < 80; ++i)
|
||||
{
|
||||
w[i] = rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
||||
}
|
||||
|
||||
uint32_t a = h0;
|
||||
uint32_t b = h1;
|
||||
uint32_t c = h2;
|
||||
uint32_t d = h3;
|
||||
uint32_t e = h4;
|
||||
|
||||
for (size_t i = 0; i < 80; ++i)
|
||||
{
|
||||
uint32_t f = 0;
|
||||
uint32_t k = 0;
|
||||
if (i < 20)
|
||||
{
|
||||
f = (b & c) | ((~b) & d);
|
||||
k = 0x5A827999u;
|
||||
}
|
||||
else if (i < 40)
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0x6ED9EBA1u;
|
||||
}
|
||||
else if (i < 60)
|
||||
{
|
||||
f = (b & c) | (b & d) | (c & d);
|
||||
k = 0x8F1BBCDCu;
|
||||
}
|
||||
else
|
||||
{
|
||||
f = b ^ c ^ d;
|
||||
k = 0xCA62C1D6u;
|
||||
}
|
||||
|
||||
const uint32_t temp = rotateLeft(a, 5) + f + e + k + w[i];
|
||||
e = d;
|
||||
d = c;
|
||||
c = rotateLeft(b, 30);
|
||||
b = a;
|
||||
a = temp;
|
||||
}
|
||||
|
||||
h0 += a;
|
||||
h1 += b;
|
||||
h2 += c;
|
||||
h3 += d;
|
||||
h4 += e;
|
||||
}
|
||||
|
||||
const std::array<uint32_t, 5> words = {h0, h1, h2, h3, h4};
|
||||
std::array<uint8_t, 20> digest = {};
|
||||
for (size_t i = 0; i < words.size(); ++i)
|
||||
{
|
||||
digest[i * 4] = static_cast<uint8_t>((words[i] >> 24) & 0xFFu);
|
||||
digest[i * 4 + 1] = static_cast<uint8_t>((words[i] >> 16) & 0xFFu);
|
||||
digest[i * 4 + 2] = static_cast<uint8_t>((words[i] >> 8) & 0xFFu);
|
||||
digest[i * 4 + 3] = static_cast<uint8_t>(words[i] & 0xFFu);
|
||||
}
|
||||
return digest;
|
||||
}
|
||||
|
||||
static fs::path resolveDatabasePath(const fs::path &inputPath)
|
||||
{
|
||||
if (fs::exists(inputPath / "symbols.json") && fs::exists(inputPath / "tree.json"))
|
||||
{
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
const fs::path resourcePath = inputPath / "symboldb" / "app" / "src" / "main" / "resources";
|
||||
if (fs::exists(resourcePath / "symbols.json") && fs::exists(resourcePath / "tree.json"))
|
||||
{
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
return inputPath;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
static std::string joinJsonChunks(const std::string_view (&chunks)[N])
|
||||
{
|
||||
size_t size = 0;
|
||||
for (std::string_view chunk : chunks)
|
||||
{
|
||||
size += chunk.size();
|
||||
}
|
||||
|
||||
std::string joined;
|
||||
joined.reserve(size);
|
||||
for (std::string_view chunk : chunks)
|
||||
{
|
||||
joined.append(chunk.data(), chunk.size());
|
||||
}
|
||||
return joined;
|
||||
}
|
||||
}
|
||||
|
||||
class SceSymbolScanner::Impl
|
||||
{
|
||||
public:
|
||||
bool loadDatabase(const std::string &databasePath)
|
||||
{
|
||||
m_lastError.clear();
|
||||
m_symbols.clear();
|
||||
m_root.reset();
|
||||
|
||||
try
|
||||
{
|
||||
if (databasePath.empty())
|
||||
{
|
||||
loadEmbeddedSymbols();
|
||||
loadEmbeddedTree();
|
||||
}
|
||||
else
|
||||
{
|
||||
const fs::path resolvedPath = resolveDatabasePath(databasePath);
|
||||
loadSymbols(resolvedPath / "symbols.json");
|
||||
loadTree(resolvedPath / "tree.json");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
m_lastError = e.what();
|
||||
m_symbols.clear();
|
||||
m_root.reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<SceSymbolMatch> scan(const std::vector<Section> §ions) const
|
||||
{
|
||||
std::unordered_map<uint32_t, std::map<std::string, Candidate>> candidatesByAddress;
|
||||
|
||||
if (!m_root)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
for (const Section §ion : sections)
|
||||
{
|
||||
if (!section.isCode || section.data == nullptr || section.size < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4)
|
||||
{
|
||||
const std::vector<const SymbolRecord *> symbols = findCandidateSymbols(section, offset);
|
||||
if (symbols.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const SymbolRecord *symbol : symbols)
|
||||
{
|
||||
if (symbol == nullptr || !symbol->isFunction || symbol->size == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (offset > section.size || symbol->size > section.size - offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!matchesSymbol(section, offset, *symbol))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t actualSize = symbol->size;
|
||||
while (actualSize <= section.size - offset - 4 &&
|
||||
readLe32(section.data + offset + actualSize) == 0)
|
||||
{
|
||||
actualSize += 4;
|
||||
}
|
||||
|
||||
Candidate candidate;
|
||||
candidate.symbol = symbol;
|
||||
candidate.address = section.address + offset;
|
||||
candidate.actualSize = actualSize;
|
||||
candidatesByAddress[candidate.address][makeSymbolKey(*symbol)] = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolveCandidates(candidatesByAddress);
|
||||
}
|
||||
|
||||
const std::string &lastError() const
|
||||
{
|
||||
return m_lastError;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, SymbolRecord> m_symbols;
|
||||
std::unique_ptr<MatchNode> m_root;
|
||||
std::string m_lastError;
|
||||
|
||||
void loadEmbeddedSymbols()
|
||||
{
|
||||
const std::string jsonText = joinJsonChunks(sce_symbol_database::kSymbolsJsonChunks);
|
||||
loadSymbolsJson(nlohmann::json::parse(jsonText));
|
||||
}
|
||||
|
||||
void loadEmbeddedTree()
|
||||
{
|
||||
const std::string jsonText = joinJsonChunks(sce_symbol_database::kTreeJsonChunks);
|
||||
loadTreeJson(nlohmann::json::parse(jsonText));
|
||||
}
|
||||
|
||||
void loadSymbols(const fs::path &path)
|
||||
{
|
||||
std::ifstream file(path);
|
||||
if (!file)
|
||||
{
|
||||
throw std::runtime_error("unable to open " + path.string());
|
||||
}
|
||||
|
||||
const nlohmann::json root = nlohmann::json::parse(file);
|
||||
loadSymbolsJson(root);
|
||||
}
|
||||
|
||||
void loadSymbolsJson(const nlohmann::json &root)
|
||||
{
|
||||
for (auto libraryIt = root.begin(); libraryIt != root.end(); ++libraryIt)
|
||||
{
|
||||
const std::string library = libraryIt.key();
|
||||
for (auto nameIt = libraryIt.value().begin(); nameIt != libraryIt.value().end(); ++nameIt)
|
||||
{
|
||||
const std::string name = nameIt.key();
|
||||
for (auto hashIt = nameIt.value().begin(); hashIt != nameIt.value().end(); ++hashIt)
|
||||
{
|
||||
const std::string hash = hashIt.key();
|
||||
for (auto variantIt = hashIt.value().begin(); variantIt != hashIt.value().end(); ++variantIt)
|
||||
{
|
||||
SymbolRecord symbol;
|
||||
symbol.library = library;
|
||||
symbol.name = name;
|
||||
symbol.hashText = hash;
|
||||
symbol.hash = parseSha1(hash);
|
||||
symbol.variantHash = static_cast<uint32_t>(std::stoul(variantIt.key(), nullptr, 16));
|
||||
|
||||
const nlohmann::json &jsonSymbol = variantIt.value();
|
||||
symbol.size = jsonSymbol.value("size", 0u);
|
||||
|
||||
const std::string type = toUpperAscii(jsonSymbol.value("type", std::string()));
|
||||
symbol.isFunction = (type == "FUNCTION" || type == "FUNC");
|
||||
|
||||
const nlohmann::json relocations =
|
||||
jsonSymbol.value("relocations", nlohmann::json::object());
|
||||
for (auto relocationIt = relocations.begin(); relocationIt != relocations.end(); ++relocationIt)
|
||||
{
|
||||
RelocationRecord relocation;
|
||||
relocation.offset = static_cast<uint32_t>(std::stoul(relocationIt.key(), nullptr, 0));
|
||||
relocation.type = parseRelocationType(relocationIt.value().value("type", std::string("none")));
|
||||
symbol.relocations.push_back(relocation);
|
||||
}
|
||||
|
||||
m_symbols[makeSymbolKey(symbol)] = std::move(symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadTree(const fs::path &path)
|
||||
{
|
||||
std::ifstream file(path);
|
||||
if (!file)
|
||||
{
|
||||
throw std::runtime_error("unable to open " + path.string());
|
||||
}
|
||||
|
||||
const nlohmann::json root = nlohmann::json::parse(file);
|
||||
loadTreeJson(root);
|
||||
}
|
||||
|
||||
void loadTreeJson(const nlohmann::json &root)
|
||||
{
|
||||
m_root = parseNode(root);
|
||||
}
|
||||
|
||||
std::unique_ptr<MatchNode> parseNode(const nlohmann::json &jsonNode) const
|
||||
{
|
||||
auto node = std::make_unique<MatchNode>();
|
||||
node->offset = jsonNode.value("offset", 0u);
|
||||
|
||||
if (jsonNode.contains("symbols"))
|
||||
{
|
||||
for (const nlohmann::json &jsonSymbol : jsonNode["symbols"])
|
||||
{
|
||||
MatchSymbolKey symbol;
|
||||
symbol.library = jsonSymbol.value("library", std::string());
|
||||
symbol.name = jsonSymbol.value("name", std::string());
|
||||
symbol.hash = jsonSymbol.value("hash", std::string());
|
||||
symbol.variantHash = jsonSymbol.value("variant", 0u);
|
||||
node->symbols.push_back(std::move(symbol));
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonNode.contains("next"))
|
||||
{
|
||||
for (const nlohmann::json &jsonEdge : jsonNode["next"])
|
||||
{
|
||||
MatchEdge edge;
|
||||
const nlohmann::json &match = jsonEdge["match"];
|
||||
edge.value = match.value("value", 0u);
|
||||
if (match.contains("relocation") && match["relocation"].contains("type"))
|
||||
{
|
||||
edge.relocationType = parseRelocationType(match["relocation"].value("type", std::string("none")));
|
||||
}
|
||||
edge.child = parseNode(jsonEdge["child"]);
|
||||
node->next.push_back(std::move(edge));
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
const SymbolRecord *findSymbol(const MatchSymbolKey &key) const
|
||||
{
|
||||
const auto it = m_symbols.find(makeSymbolKey(key.library, key.name, key.hash, key.variantHash));
|
||||
if (it == m_symbols.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
std::vector<const SymbolRecord *> findCandidateSymbols(const Section §ion, uint32_t offset) const
|
||||
{
|
||||
std::vector<const SymbolRecord *> symbols;
|
||||
std::vector<const MatchNode *> stack;
|
||||
stack.push_back(m_root.get());
|
||||
|
||||
while (!stack.empty())
|
||||
{
|
||||
const MatchNode *node = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
if (node == nullptr || node->offset > section.size || offset > section.size - node->offset)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (section.size - offset - node->offset < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t value = readLe32(section.data + offset + node->offset);
|
||||
for (const MatchEdge &edge : node->next)
|
||||
{
|
||||
const uint32_t mask = relocationMask(edge.relocationType);
|
||||
if ((value & mask) != (edge.value & mask))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const MatchSymbolKey &key : edge.child->symbols)
|
||||
{
|
||||
if (const SymbolRecord *symbol = findSymbol(key))
|
||||
{
|
||||
symbols.push_back(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
if (!edge.child->next.empty())
|
||||
{
|
||||
stack.push_back(edge.child.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
bool matchesSymbol(const Section §ion, uint32_t offset, const SymbolRecord &symbol) const
|
||||
{
|
||||
std::vector<uint8_t> bytes(section.data + offset, section.data + offset + symbol.size);
|
||||
for (const RelocationRecord &relocation : symbol.relocations)
|
||||
{
|
||||
if (relocation.offset > bytes.size() || bytes.size() - relocation.offset < 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t value = readLe32(bytes.data() + relocation.offset);
|
||||
writeLe32(bytes.data() + relocation.offset,
|
||||
disabledRelocationValue(relocation.type, value));
|
||||
}
|
||||
|
||||
return sha1(bytes) == symbol.hash;
|
||||
}
|
||||
|
||||
std::vector<SceSymbolMatch> resolveCandidates(
|
||||
const std::unordered_map<uint32_t, std::map<std::string, Candidate>> &candidatesByAddress) const
|
||||
{
|
||||
std::vector<SceSymbolMatch> matches;
|
||||
matches.reserve(candidatesByAddress.size());
|
||||
|
||||
for (const auto &[address, candidatesByKey] : candidatesByAddress)
|
||||
{
|
||||
std::vector<const Candidate *> viable;
|
||||
viable.reserve(candidatesByKey.size());
|
||||
for (const auto &[_, candidate] : candidatesByKey)
|
||||
{
|
||||
if (candidate.symbol != nullptr && candidate.symbol->staticBitCount() >= 256)
|
||||
{
|
||||
viable.push_back(&candidate);
|
||||
}
|
||||
}
|
||||
|
||||
if (viable.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// The upstream scanner also uses dependency and adjacent-library context.
|
||||
// This analyzer integration keeps only unambiguous direct hash matches for now.
|
||||
std::set<std::string> identities;
|
||||
for (const Candidate *candidate : viable)
|
||||
{
|
||||
identities.insert(candidate->symbol->library + '\n' + candidate->symbol->name);
|
||||
}
|
||||
if (identities.size() != 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const Candidate *best = *std::max_element(
|
||||
viable.begin(),
|
||||
viable.end(),
|
||||
[](const Candidate *lhs, const Candidate *rhs)
|
||||
{
|
||||
if (lhs->actualSize != rhs->actualSize)
|
||||
{
|
||||
return lhs->actualSize < rhs->actualSize;
|
||||
}
|
||||
return lhs->symbol->staticBitCount() < rhs->symbol->staticBitCount();
|
||||
});
|
||||
|
||||
SceSymbolMatch match;
|
||||
match.address = address;
|
||||
match.size = best->actualSize;
|
||||
match.name = best->symbol->name;
|
||||
match.library = best->symbol->library;
|
||||
match.hash = best->symbol->hashText;
|
||||
match.variantHash = best->symbol->variantHash;
|
||||
matches.push_back(std::move(match));
|
||||
}
|
||||
|
||||
std::sort(matches.begin(), matches.end(),
|
||||
[](const SceSymbolMatch &a, const SceSymbolMatch &b)
|
||||
{
|
||||
return a.address < b.address;
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
};
|
||||
|
||||
SceSymbolScanner::SceSymbolScanner()
|
||||
: m_impl(std::make_unique<Impl>())
|
||||
{
|
||||
}
|
||||
|
||||
SceSymbolScanner::~SceSymbolScanner() = default;
|
||||
|
||||
bool SceSymbolScanner::loadDatabase(const std::string &databasePath)
|
||||
{
|
||||
return m_impl->loadDatabase(databasePath);
|
||||
}
|
||||
|
||||
std::vector<SceSymbolMatch> SceSymbolScanner::scan(const std::vector<Section> §ions) const
|
||||
{
|
||||
return m_impl->scan(sections);
|
||||
}
|
||||
|
||||
const std::string &SceSymbolScanner::lastError() const
|
||||
{
|
||||
return m_impl->lastError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#include "ps2recomp/toml_generator.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
bool TomlGenerator::generate(const TomlGeneratorInput &input, const std::string &outputPath)
|
||||
{
|
||||
std::ofstream file(outputPath);
|
||||
if (!file)
|
||||
{
|
||||
std::cerr << "Failed to open output file: " << outputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path elfPathObj(input.elfPath);
|
||||
std::string elfFileName = elfPathObj.filename().string();
|
||||
|
||||
fs::path outputPathObj(outputPath);
|
||||
fs::path outputDir = outputPathObj.parent_path();
|
||||
if (outputDir.empty())
|
||||
{
|
||||
outputDir = ".";
|
||||
}
|
||||
|
||||
const fs::path generatedOutputDir = outputDir / "output";
|
||||
std::string outputDirStr = generatedOutputDir.generic_string() + "/";
|
||||
|
||||
if (!fs::exists(generatedOutputDir))
|
||||
{
|
||||
fs::create_directories(generatedOutputDir);
|
||||
}
|
||||
|
||||
file << "# PS2Recomp configuration for: " << elfFileName << "\n";
|
||||
file << "# Generated by ElfAnalyzer\n\n";
|
||||
|
||||
file << "[general]\n";
|
||||
file << "# Path to input ELF file\n";
|
||||
file << "input = \"" << escapeBackslashes(input.elfPath) << "\"\n\n";
|
||||
|
||||
file << "# Path to Ghidra exported function map (optional CSV)\n";
|
||||
file << "ghidra_output = \"\"\n\n";
|
||||
|
||||
file << "# Path to output directory\n";
|
||||
file << "output = \"" << escapeBackslashes(outputDirStr) << "\"\n\n";
|
||||
|
||||
file << "# Single file output mode (recommended for large games)\n";
|
||||
file << "single_file_output = false\n\n";
|
||||
|
||||
file << "# Patch policy (instruction-driven handling is preferred for syscalls)\n";
|
||||
file << "patch_syscalls = false\n";
|
||||
file << "patch_cop0 = true\n";
|
||||
file << "patch_cache = true\n\n";
|
||||
|
||||
std::unordered_map<std::string, size_t> functionNameCounts;
|
||||
functionNameCounts.reserve(input.context.functions.size());
|
||||
for (const auto &func : input.context.functions)
|
||||
{
|
||||
if (!func.name.empty())
|
||||
{
|
||||
functionNameCounts[func.name]++;
|
||||
}
|
||||
}
|
||||
|
||||
auto makeSelector = [&](const std::string &name, uint32_t start) -> std::string
|
||||
{
|
||||
std::stringstream selector;
|
||||
selector << name << "@0x"
|
||||
<< std::hex << std::uppercase << std::setw(8) << std::setfill('0')
|
||||
<< start;
|
||||
return selector.str();
|
||||
};
|
||||
|
||||
auto collectFunctionSelectors =
|
||||
[&](const std::unordered_set<std::string> &nameSet) -> std::vector<std::string>
|
||||
{
|
||||
std::vector<const Function *> orderedFunctions;
|
||||
orderedFunctions.reserve(input.context.functions.size());
|
||||
for (const auto &func : input.context.functions)
|
||||
{
|
||||
orderedFunctions.push_back(&func);
|
||||
}
|
||||
|
||||
std::sort(orderedFunctions.begin(), orderedFunctions.end(),
|
||||
[](const Function *a, const Function *b)
|
||||
{ return a->start < b->start; });
|
||||
|
||||
std::vector<std::string> entries;
|
||||
std::unordered_set<std::string> seenEntries;
|
||||
std::unordered_set<std::string> coveredNames;
|
||||
|
||||
for (const Function *func : orderedFunctions)
|
||||
{
|
||||
if (!nameSet.contains(func->name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
coveredNames.insert(func->name);
|
||||
const std::string entry = makeSelector(func->name, func->start);
|
||||
if (seenEntries.insert(entry).second)
|
||||
{
|
||||
entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> leftovers;
|
||||
leftovers.reserve(nameSet.size());
|
||||
for (const auto &name : nameSet)
|
||||
{
|
||||
if (!coveredNames.contains(name) && seenEntries.insert(name).second)
|
||||
{
|
||||
leftovers.push_back(name);
|
||||
}
|
||||
}
|
||||
std::sort(leftovers.begin(), leftovers.end());
|
||||
entries.insert(entries.end(), leftovers.begin(), leftovers.end());
|
||||
|
||||
return entries;
|
||||
};
|
||||
|
||||
const std::vector<std::string> stubEntries = collectFunctionSelectors(input.libFunctions);
|
||||
const std::vector<std::string> untrackedStubEntries = collectFunctionSelectors(input.untrackedStubFunctions);
|
||||
|
||||
file << "# Functions to stub (only names with runtime syscall/stub handlers)\n";
|
||||
file << "stubs = [\n";
|
||||
for (const auto &func : stubEntries)
|
||||
{
|
||||
file << " \"" << func << "\",\n";
|
||||
}
|
||||
file << "]\n\n";
|
||||
|
||||
file << "# Guest functions without runtime handlers that may be referenced by address.\n";
|
||||
file << "# PS2Recomp keeps their guest implementation and exposes exact callable entries.\n";
|
||||
file << "entry_points = [\n";
|
||||
for (const auto &func : untrackedStubEntries)
|
||||
{
|
||||
file << " \"" << func << "\",\n";
|
||||
}
|
||||
file << "]\n\n";
|
||||
|
||||
file << "# Legacy compatibility field. The analyzer no longer auto-populates skip entries.\n";
|
||||
file << "skip = []\n\n";
|
||||
|
||||
if (!input.mmioByInstructionAddress.empty())
|
||||
{
|
||||
file << "# Detected MMIO accesses\n";
|
||||
file << "[mmio]\n";
|
||||
for (const auto &[instAddr, mmioAddr] : input.mmioByInstructionAddress)
|
||||
{
|
||||
file << "\"0x" << std::hex << instAddr << "\" = \"0x" << mmioAddr << "\"\n"
|
||||
<< std::dec;
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
|
||||
if (!input.jumpTables.empty())
|
||||
{
|
||||
file << "# Jump tables detected in the program\n";
|
||||
file << "[jump_tables]\n";
|
||||
|
||||
for (const auto &jt : input.jumpTables)
|
||||
{
|
||||
file << "[[jump_tables.table]]\n";
|
||||
file << "address = \"0x" << std::hex << jt.address << "\"\n"
|
||||
<< std::dec;
|
||||
file << "entries = [\n";
|
||||
|
||||
for (const auto &[index, target] : jt.entries)
|
||||
{
|
||||
file << " { index = " << index << ", target = \"0x"
|
||||
<< std::hex << target << "\" },\n"
|
||||
<< std::dec;
|
||||
}
|
||||
|
||||
file << "]\n\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.patches.empty())
|
||||
{
|
||||
file << "# Patches to apply during recompilation\n";
|
||||
file << "[patches]\n";
|
||||
file << "# Individual instruction patches\n";
|
||||
file << "instructions = [\n";
|
||||
for (const auto &[address, value] : input.patches)
|
||||
{
|
||||
auto reasonIt = input.patchReasons.find(address);
|
||||
const std::string reason = reasonIt == input.patchReasons.end() ? "" : reasonIt->second;
|
||||
file << " { address = \"0x" << std::hex << address << "\", value = \"0x"
|
||||
<< std::hex << value << "\" }, # " << reason << "\n";
|
||||
}
|
||||
file << "]\n\n";
|
||||
}
|
||||
|
||||
file << "# Performance critical functions (may need manual optimization)\n";
|
||||
file << "[performance]\n";
|
||||
file << "critical = [\n";
|
||||
for (const auto &func : input.context.functions)
|
||||
{
|
||||
auto reasonIt = input.performanceCriticalReasons.find(func.start);
|
||||
if (reasonIt != input.performanceCriticalReasons.end())
|
||||
{
|
||||
file << " \"" << func.name << "\", # " << reasonIt->second << "\n";
|
||||
}
|
||||
}
|
||||
file << "]\n\n";
|
||||
|
||||
std::cout << "Generated TOML configuration: " << outputPath << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string TomlGenerator::escapeBackslashes(const std::string &path)
|
||||
{
|
||||
std::string result;
|
||||
for (char ch : path)
|
||||
{
|
||||
if (ch == '\\')
|
||||
{
|
||||
result.append("\\\\");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.push_back(ch);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(ps2xIOP LANGUAGES CXX)
|
||||
|
||||
option(PS2X_IOP_BUILD_TESTS "Build ps2xIOP emulator smoke tests" OFF)
|
||||
|
||||
add_library(ps2_iop STATIC
|
||||
src/ps2_path.cpp
|
||||
src/iop_module_manager.cpp
|
||||
src/iop_subsystem.cpp
|
||||
src/emulator/iop_emulator.cpp
|
||||
src/emulator/core/iop_cpu.cpp
|
||||
src/emulator/core/iop_kernel.cpp
|
||||
src/emulator/core/iop_memory.cpp
|
||||
src/emulator/services/iop_module_loader.cpp
|
||||
src/emulator/services/iop_rpc.cpp
|
||||
src/emulator/imports/iop_cdvd.cpp
|
||||
src/emulator/imports/iop_heaplib.cpp
|
||||
src/emulator/imports/iop_imports.cpp
|
||||
src/emulator/imports/iop_intrman.cpp
|
||||
src/emulator/imports/iop_ioman.cpp
|
||||
src/emulator/imports/iop_loadcore.cpp
|
||||
src/emulator/imports/iop_stdio.cpp
|
||||
src/emulator/imports/iop_sysclib.cpp
|
||||
src/emulator/imports/iop_sysmem.cpp
|
||||
src/emulator/imports/iop_timrman.cpp
|
||||
src/emulator/imports/iop_vblank.cpp
|
||||
src/modules/dbcman.cpp
|
||||
src/modules/libsd.cpp
|
||||
src/modules/mcserv.cpp
|
||||
)
|
||||
|
||||
target_compile_features(ps2_iop PUBLIC cxx_std_20)
|
||||
|
||||
target_include_directories(ps2_iop
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
$<INSTALL_INTERFACE:include>
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
add_library(ps2x::iop ALIAS ps2_iop)
|
||||
|
||||
if(PS2X_IOP_BUILD_TESTS)
|
||||
enable_testing()
|
||||
add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp)
|
||||
target_link_libraries(ps2_iop_emulator_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_emulator_tests COMMAND ps2_iop_emulator_tests)
|
||||
|
||||
add_executable(ps2_iop_import_tests tests/iop_import_tests.cpp)
|
||||
target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests)
|
||||
|
||||
add_executable(ps2_iop_compatibility_tests tests/iop_compatibility_tests.cpp)
|
||||
target_link_libraries(ps2_iop_compatibility_tests PRIVATE ps2_iop)
|
||||
add_test(NAME ps2_iop_compatibility_tests COMMAND ps2_iop_compatibility_tests)
|
||||
|
||||
add_executable(ps2_iop_import_version_tests tests/iop_import_version_tests.cpp)
|
||||
target_link_libraries(ps2_iop_import_version_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests)
|
||||
|
||||
add_executable(ps2_iop_execution_tests tests/iop_execution_tests.cpp)
|
||||
target_link_libraries(ps2_iop_execution_tests PRIVATE ps2_iop)
|
||||
target_include_directories(ps2_iop_execution_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||
add_test(NAME ps2_iop_execution_tests COMMAND ps2_iop_execution_tests)
|
||||
|
||||
endif()
|
||||
|
||||
install(TARGETS ps2_iop
|
||||
ARCHIVE DESTINATION lib
|
||||
LIBRARY DESTINATION lib
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
# ps2xIOP
|
||||
|
||||
`ps2xIOP` runs original IRX modules on an R3000A interpreter, with a virtual
|
||||
IOP kernel providing imports without a PS2 BIOS. The C++20 static library
|
||||
`ps2_iop` / `ps2x::iop` is linked into `ps2xRuntime`.
|
||||
|
||||
## Execution policy
|
||||
|
||||
Game-specific IOP code executes from IRX modules. There is no game-profile
|
||||
selection or native profile-plugin loader. A physical IRX RPC server is
|
||||
authoritative for its SID.
|
||||
|
||||
Generic HLE services remain available when no loaded IRX provides an endpoint:
|
||||
|
||||
| Service | SID | Activation |
|
||||
| --- | --- | --- |
|
||||
| MCSERV | `0x80000400`, `0x80000480` | Recognized module load |
|
||||
| LIBSD | `0x80000701` | Recognized module load |
|
||||
| DBCMAN | `0x80001300` | Recognized module load |
|
||||
|
||||
These services are dormant before module load and after reset or the final
|
||||
module stop. Unknown modules fail to load; unknown RPC SIDs remain unhandled.
|
||||
Games previously using TSNDDRV, CRI DTX, CLFILE, SOUND or SDRDRV profiles now
|
||||
require their IRX modules and support for the imports and hardware they use.
|
||||
|
||||
## Lifecycle and transport
|
||||
|
||||
- `reset()` clears loaded modules, HLE service state and emulator state.
|
||||
- `loadModule(...)` / `loadModuleBuffer(...)` load and start an IRX.
|
||||
- `stopModule(...)` releases a module and its owned state.
|
||||
- `runEeCycles(...)` advances the IOP from EE cycle accounting.
|
||||
- `selectRpcAbi(...)`, `handleRpc(...)` and `onSifTransfer(...)` connect SIF transport.
|
||||
|
||||
IOP RAM is separate from EE RAM. The transport copies data through the IOP
|
||||
memory accessors; SIF notifications do not mirror bytes into equal-numbered EE
|
||||
addresses. `RpcResult` describes completion and dispatch actions for the runtime.
|
||||
|
||||
Link with `target_link_libraries(my_runtime PRIVATE ps2x::iop)`. The public API
|
||||
is [iop_subsystem.h](include/ps2x/iop/iop_subsystem.h); `PS2Runtime` owns its
|
||||
subsystem and host adapter.
|
||||
|
||||
## Diagnostics and tests
|
||||
|
||||
`debugSnapshot()` exposes emulator cycle/instruction counts, loaded module,
|
||||
thread and RPC-server counts, generic service metrics and load diagnostics.
|
||||
The runtime debugger renders these in the **IOP/SIF** tab.
|
||||
|
||||
Build standalone tests with:
|
||||
|
||||
```sh
|
||||
cmake -S ps2xIOP -B out/build/iop-tests -DPS2X_IOP_BUILD_TESTS=ON
|
||||
cmake --build out/build/iop-tests
|
||||
ctest --test-dir out/build/iop-tests --output-on-failure
|
||||
```
|
||||
|
||||
The suites cover IRX execution, RPC, imports, version resolution and generic
|
||||
HLE compatibility. `ps2x_tests` also covers runtime SIF RPC/DMA integration.
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
enum class HostPathKind : uint32_t
|
||||
{
|
||||
ElfDirectory = 0,
|
||||
CdRoot = 1,
|
||||
CdImage = 2,
|
||||
HostRoot = 3,
|
||||
MemoryCardRoot = 4,
|
||||
};
|
||||
|
||||
enum class LogLevel : uint32_t
|
||||
{
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warning = 2,
|
||||
Error = 3,
|
||||
};
|
||||
|
||||
enum class MemoryCardOperation : uint32_t
|
||||
{
|
||||
Init,
|
||||
GetInfo,
|
||||
Open,
|
||||
Close,
|
||||
Seek,
|
||||
Read,
|
||||
Write,
|
||||
Flush,
|
||||
Chdir,
|
||||
GetDir,
|
||||
SetFileInfo,
|
||||
Delete,
|
||||
Format,
|
||||
Unformat,
|
||||
Mkdir,
|
||||
};
|
||||
|
||||
struct MemoryCardRequest
|
||||
{
|
||||
MemoryCardOperation operation = MemoryCardOperation::Init;
|
||||
// The fifth argument is carried in $t0 by the EE n32 ABI
|
||||
std::array<uint32_t, 5> arguments{};
|
||||
};
|
||||
|
||||
class IopHost
|
||||
{
|
||||
public:
|
||||
virtual ~IopHost() = default;
|
||||
|
||||
virtual bool readGuest(uint32_t address, void *destination, size_t size) const = 0;
|
||||
virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0;
|
||||
virtual bool zeroGuest(uint32_t address, size_t size) = 0;
|
||||
virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0;
|
||||
|
||||
// IOP RAM is a distinct address space from the EE guest. TODO remove this later
|
||||
virtual bool readIopMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
(void)address;
|
||||
(void)destination;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool writeIopMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
(void)address;
|
||||
(void)source;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool zeroIopMemory(uint32_t address, size_t size)
|
||||
{
|
||||
(void)address;
|
||||
(void)size;
|
||||
return false;
|
||||
}
|
||||
virtual bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const
|
||||
{
|
||||
(void)address;
|
||||
normalized = 0u;
|
||||
return false;
|
||||
}
|
||||
virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0;
|
||||
virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0;
|
||||
virtual void freeGuest(uint32_t address) = 0;
|
||||
|
||||
virtual void audioCommand(uint32_t sid, uint32_t function, GuestBuffer send, GuestBuffer receive) = 0;
|
||||
|
||||
virtual std::string hostPath(HostPathKind kind) const = 0;
|
||||
virtual std::string translateGuestPath(std::string_view path) const = 0;
|
||||
virtual uint64_t openHostFile(std::string_view path) = 0;
|
||||
virtual bool hostFileSize(uint64_t handle, uint64_t &size) const = 0;
|
||||
virtual bool readHostFile(uint64_t handle,
|
||||
uint64_t offset,
|
||||
void *destination,
|
||||
size_t size,
|
||||
size_t &bytesRead) = 0;
|
||||
virtual void closeHostFile(uint64_t handle) = 0;
|
||||
|
||||
virtual int32_t memoryCard(const MemoryCardRequest &request) = 0;
|
||||
|
||||
virtual bool hasGuestFunction(uint32_t address) const = 0;
|
||||
virtual bool invokeGuestFunction(uint64_t callToken,
|
||||
uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t *resultAddress) = 0;
|
||||
|
||||
// Deliver an IOP -> EE SIF command packet. The default keeps hosts
|
||||
// which do not emulate the EE command dispatcher source-compatible.
|
||||
virtual bool sendSifCommand(uint32_t commandId,
|
||||
const void *packet,
|
||||
size_t packetSize)
|
||||
{
|
||||
(void)commandId;
|
||||
(void)packet;
|
||||
(void)packetSize;
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void log(LogLevel level, std::string_view message) = 0;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopSubsystem
|
||||
{
|
||||
public:
|
||||
explicit IopSubsystem(IopHost &host);
|
||||
~IopSubsystem();
|
||||
|
||||
IopSubsystem(const IopSubsystem &) = delete;
|
||||
IopSubsystem &operator=(const IopSubsystem &) = delete;
|
||||
IopSubsystem(IopSubsystem &&) noexcept;
|
||||
IopSubsystem &operator=(IopSubsystem &&) noexcept;
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result = nullptr);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
|
||||
[[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const;
|
||||
[[nodiscard]] bool canBindRpc(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
// Physical IOP RAM access shared by the emulator, SIF DMA, and HLE services. Addresses are IOP addresses.
|
||||
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
|
||||
[[nodiscard]] bool freeMemory(uint32_t address);
|
||||
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
|
||||
|
||||
[[nodiscard]] DebugSnapshot debugSnapshot() const;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
struct GuestBuffer
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct GameIdentity
|
||||
{
|
||||
std::string elfName;
|
||||
uint32_t entryPoint = 0;
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct GameMatcher
|
||||
{
|
||||
std::string elfName;
|
||||
uint32_t entryPoint = 0;
|
||||
uint32_t crc32 = 0;
|
||||
};
|
||||
|
||||
struct ModuleLoadResult
|
||||
{
|
||||
bool handled = false;
|
||||
int32_t moduleId = -1;
|
||||
int32_t startResult = -1;
|
||||
};
|
||||
|
||||
enum class RpcAbi : uint32_t
|
||||
{
|
||||
RuntimeDefault = 0,
|
||||
Registers = 1,
|
||||
Stack = 2,
|
||||
};
|
||||
|
||||
struct RpcCallCandidate
|
||||
{
|
||||
uint32_t sendSize = 0;
|
||||
uint32_t receiveAddress = 0;
|
||||
uint32_t receiveSize = 0;
|
||||
uint32_t endFunction = 0;
|
||||
uint32_t endParameter = 0;
|
||||
bool plausible = false;
|
||||
};
|
||||
|
||||
struct RpcAbiRequest
|
||||
{
|
||||
uint32_t boundSid = 0;
|
||||
uint32_t function = 0;
|
||||
RpcCallCandidate registers;
|
||||
RpcCallCandidate stack;
|
||||
};
|
||||
|
||||
struct RpcRequest
|
||||
{
|
||||
uint64_t callToken = 0;
|
||||
uint32_t clientAddress = 0;
|
||||
uint32_t serverAddress = 0;
|
||||
uint32_t serverFunction = 0;
|
||||
uint32_t serverBuffer = 0;
|
||||
uint32_t sid = 0;
|
||||
uint32_t function = 0;
|
||||
uint32_t mode = 0;
|
||||
GuestBuffer send;
|
||||
GuestBuffer receive;
|
||||
uint32_t endFunction = 0;
|
||||
uint32_t endParameter = 0;
|
||||
};
|
||||
|
||||
enum class IopHandleKind : uint32_t
|
||||
{
|
||||
RpcServer = 0,
|
||||
RpcPacket = 1,
|
||||
};
|
||||
|
||||
enum class CallbackPolicy : uint32_t
|
||||
{
|
||||
RuntimeDefault = 0,
|
||||
Suppress = 1,
|
||||
};
|
||||
|
||||
enum class ServerDispatchPolicy : uint32_t
|
||||
{
|
||||
RuntimeDefault = 0,
|
||||
Suppress = 1,
|
||||
};
|
||||
|
||||
struct RpcResult
|
||||
{
|
||||
bool handled = false;
|
||||
uint32_t resultAddress = 0;
|
||||
bool signalNowaitCompletion = false;
|
||||
bool signalCompletion = false;
|
||||
CallbackPolicy callbackPolicy = CallbackPolicy::RuntimeDefault;
|
||||
ServerDispatchPolicy serverDispatchPolicy = ServerDispatchPolicy::RuntimeDefault;
|
||||
uint32_t guestFunction = 0;
|
||||
uint32_t guestArguments[4]{};
|
||||
uint32_t guestDefaultResultAddress = 0;
|
||||
};
|
||||
|
||||
enum class SifTransferKind : uint32_t
|
||||
{
|
||||
SetDma = 0,
|
||||
GetOtherData = 1,
|
||||
};
|
||||
|
||||
enum class SifTransferPhase : uint32_t
|
||||
{
|
||||
BeforeCopy = 0,
|
||||
AfterCopy = 1,
|
||||
};
|
||||
|
||||
struct SifTransfer
|
||||
{
|
||||
SifTransferKind kind = SifTransferKind::SetDma;
|
||||
SifTransferPhase phase = SifTransferPhase::AfterCopy;
|
||||
uint32_t sourceAddress = 0;
|
||||
uint32_t destinationAddress = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct DebugMetric
|
||||
{
|
||||
std::string name;
|
||||
uint64_t value = 0;
|
||||
bool hexadecimal = false;
|
||||
};
|
||||
|
||||
struct DebugService
|
||||
{
|
||||
std::string name;
|
||||
std::vector<uint32_t> sids;
|
||||
bool active = true;
|
||||
std::vector<DebugMetric> metrics;
|
||||
};
|
||||
|
||||
struct DebugSnapshot
|
||||
{
|
||||
uint64_t emulatorCycles = 0;
|
||||
uint64_t emulatorInstructions = 0;
|
||||
uint32_t emulatorLoadedModules = 0;
|
||||
uint32_t emulatorThreads = 0;
|
||||
uint32_t emulatorRpcServers = 0;
|
||||
std::vector<DebugService> services;
|
||||
std::vector<std::string> diagnostics;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
enum class Ps2PathDevice
|
||||
{
|
||||
Invalid,
|
||||
Host,
|
||||
Cdrom,
|
||||
MemoryCard0,
|
||||
Rom0,
|
||||
NativeHost,
|
||||
};
|
||||
|
||||
struct ParsedPs2Path
|
||||
{
|
||||
Ps2PathDevice device = Ps2PathDevice::Invalid;
|
||||
std::string deviceName;
|
||||
std::string path;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept
|
||||
{
|
||||
return device != Ps2PathDevice::Invalid;
|
||||
}
|
||||
};
|
||||
|
||||
[[nodiscard]] ParsedPs2Path parsePs2Path(std::string_view path);
|
||||
|
||||
// Returns a lower-case module/file leaf without an optional .irx suffix.
|
||||
[[nodiscard]] std::string ps2PathLeafKey(const ParsedPs2Path &path);
|
||||
[[nodiscard]] std::string ps2PathLeafKey(std::string_view path);
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
#include "iop_cpu.h"
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopCpuCore::IopCpuCore(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopCpuCore::writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
cpu.gpr[reg] = value;
|
||||
writtenReg = reg;
|
||||
}
|
||||
|
||||
void IopCpuCore::scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue)
|
||||
{
|
||||
if (reg == 0u)
|
||||
return;
|
||||
scheduled = true;
|
||||
scheduledReg = reg;
|
||||
scheduledValue = value;
|
||||
}
|
||||
|
||||
void IopCpuCore::raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress) const
|
||||
{
|
||||
uint32_t cause = cpu.cop0[13] & ~0x7Cu;
|
||||
cause |= (code & 0x1Fu) << 2u;
|
||||
if (delaySlot)
|
||||
{
|
||||
cause |= 0x80000000u;
|
||||
cpu.cop0[14] = faultPc - 4u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cause &= ~0x80000000u;
|
||||
cpu.cop0[14] = faultPc;
|
||||
}
|
||||
cpu.cop0[13] = cause;
|
||||
if (badAddress)
|
||||
cpu.cop0[8] = *badAddress;
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x3Fu) | ((status << 2u) & 0x3Fu);
|
||||
cpu.pc = (status & (1u << 22u)) ? 0xBFC00180u : 0x80000080u;
|
||||
cpu.branchPending = false;
|
||||
cpu.pendingLoad = false;
|
||||
cpu.exception = true;
|
||||
}
|
||||
|
||||
bool IopCpuCore::executeInstruction(IopCpuState &cpu)
|
||||
{
|
||||
return executeInstruction(cpu, m_memory.read32(cpu.pc));
|
||||
}
|
||||
|
||||
bool IopCpuCore::executeInstruction(IopCpuState &cpu, uint32_t instruction)
|
||||
{
|
||||
const uint32_t pc = cpu.pc;
|
||||
const bool wasDelaySlot = cpu.branchPending;
|
||||
const uint32_t priorBranchTarget = cpu.branchTarget;
|
||||
|
||||
cpu.branchPending = false;
|
||||
cpu.exception = false;
|
||||
cpu.yielded = false;
|
||||
|
||||
const uint32_t opcode = instruction >> 26u;
|
||||
const uint32_t rs = (instruction >> 21u) & 31u;
|
||||
const uint32_t rt = (instruction >> 16u) & 31u;
|
||||
const uint32_t rd = (instruction >> 11u) & 31u;
|
||||
const uint32_t sa = (instruction >> 6u) & 31u;
|
||||
const uint32_t funct = instruction & 63u;
|
||||
const uint32_t imm = instruction & 0xFFFFu;
|
||||
const int32_t simm = static_cast<int16_t>(imm);
|
||||
const uint32_t nextPc = pc + 4u;
|
||||
|
||||
uint32_t writtenReg = 0u;
|
||||
bool scheduledLoad = false;
|
||||
uint32_t scheduledReg = 0u;
|
||||
uint32_t scheduledValue = 0u;
|
||||
bool newBranch = false;
|
||||
uint32_t newBranchTarget = 0u;
|
||||
|
||||
auto branch = [&](bool condition)
|
||||
{
|
||||
if (condition)
|
||||
{
|
||||
newBranch = true;
|
||||
newBranchTarget = nextPc + (static_cast<uint32_t>(simm) << 2u);
|
||||
}
|
||||
};
|
||||
auto write = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
writeRegister(cpu, reg, value, writtenReg);
|
||||
};
|
||||
auto load = [&](uint32_t reg, uint32_t value)
|
||||
{
|
||||
scheduleLoad(reg, value, scheduledLoad, scheduledReg, scheduledValue);
|
||||
};
|
||||
auto overflowAdd = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) + rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
auto overflowSub = [&](int32_t lhs, int32_t rhs, uint32_t reg)
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(lhs) - rhs;
|
||||
if (result > std::numeric_limits<int32_t>::max() || result < std::numeric_limits<int32_t>::min())
|
||||
raiseException(cpu, 12u, pc, wasDelaySlot);
|
||||
else
|
||||
write(reg, static_cast<uint32_t>(static_cast<int32_t>(result)));
|
||||
};
|
||||
|
||||
// TODO kill this magic number and make it a constant somewhere
|
||||
switch (opcode)
|
||||
{
|
||||
case 0x00:
|
||||
switch (funct)
|
||||
{
|
||||
case 0x00:
|
||||
write(rd, cpu.gpr[rt] << sa);
|
||||
break;
|
||||
case 0x02:
|
||||
write(rd, cpu.gpr[rt] >> sa);
|
||||
break;
|
||||
case 0x03:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> sa));
|
||||
break;
|
||||
case 0x04:
|
||||
write(rd, cpu.gpr[rt] << (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x06:
|
||||
write(rd, cpu.gpr[rt] >> (cpu.gpr[rs] & 31u));
|
||||
break;
|
||||
case 0x07:
|
||||
write(rd, static_cast<uint32_t>(static_cast<int32_t>(cpu.gpr[rt]) >> (cpu.gpr[rs] & 31u)));
|
||||
break;
|
||||
case 0x08:
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x09:
|
||||
write(rd ? rd : 31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x0C:
|
||||
raiseException(cpu, 8u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x0D:
|
||||
raiseException(cpu, 9u, pc, wasDelaySlot);
|
||||
break;
|
||||
case 0x10:
|
||||
write(rd, cpu.hi);
|
||||
break;
|
||||
case 0x11:
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x12:
|
||||
write(rd, cpu.lo);
|
||||
break;
|
||||
case 0x13:
|
||||
cpu.lo = cpu.gpr[rs];
|
||||
break;
|
||||
case 0x18:
|
||||
{
|
||||
const int64_t result = static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rs])) * static_cast<int64_t>(static_cast<int32_t>(cpu.gpr[rt]));
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(static_cast<uint64_t>(result) >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x19:
|
||||
{
|
||||
const uint64_t result = static_cast<uint64_t>(cpu.gpr[rs]) * cpu.gpr[rt];
|
||||
cpu.lo = static_cast<uint32_t>(result);
|
||||
cpu.hi = static_cast<uint32_t>(result >> 32u);
|
||||
break;
|
||||
}
|
||||
case 0x1A:
|
||||
{
|
||||
const int32_t lhs = static_cast<int32_t>(cpu.gpr[rs]);
|
||||
const int32_t rhs = static_cast<int32_t>(cpu.gpr[rt]);
|
||||
if (rhs == 0)
|
||||
{
|
||||
cpu.lo = lhs >= 0 ? 0xFFFFFFFFu : 1u;
|
||||
cpu.hi = static_cast<uint32_t>(lhs);
|
||||
}
|
||||
else if (lhs == std::numeric_limits<int32_t>::min() && rhs == -1)
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs);
|
||||
cpu.hi = 0u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = static_cast<uint32_t>(lhs / rhs);
|
||||
cpu.hi = static_cast<uint32_t>(lhs % rhs);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x1B:
|
||||
if (cpu.gpr[rt] == 0u)
|
||||
{
|
||||
cpu.lo = 0xFFFFFFFFu;
|
||||
cpu.hi = cpu.gpr[rs];
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.lo = cpu.gpr[rs] / cpu.gpr[rt];
|
||||
cpu.hi = cpu.gpr[rs] % cpu.gpr[rt];
|
||||
}
|
||||
break;
|
||||
case 0x20:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x21:
|
||||
write(rd, cpu.gpr[rs] + cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x22:
|
||||
overflowSub(static_cast<int32_t>(cpu.gpr[rs]), static_cast<int32_t>(cpu.gpr[rt]), rd);
|
||||
break;
|
||||
case 0x23:
|
||||
write(rd, cpu.gpr[rs] - cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x24:
|
||||
write(rd, cpu.gpr[rs] & cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x25:
|
||||
write(rd, cpu.gpr[rs] | cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x26:
|
||||
write(rd, cpu.gpr[rs] ^ cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x27:
|
||||
write(rd, ~(cpu.gpr[rs] | cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x2A:
|
||||
write(rd, static_cast<int32_t>(cpu.gpr[rs]) < static_cast<int32_t>(cpu.gpr[rt]) ? 1u : 0u);
|
||||
break;
|
||||
case 0x2B:
|
||||
write(rd, cpu.gpr[rs] < cpu.gpr[rt] ? 1u : 0u);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x01:
|
||||
switch (rt)
|
||||
{
|
||||
case 0x00:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x01:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
case 0x10:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) < 0);
|
||||
break;
|
||||
case 0x11:
|
||||
write(31u, pc + 8u);
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) >= 0);
|
||||
break;
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x02:
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x03:
|
||||
write(31u, pc + 8u);
|
||||
newBranch = true;
|
||||
newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u);
|
||||
break;
|
||||
case 0x04:
|
||||
branch(cpu.gpr[rs] == cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x05:
|
||||
branch(cpu.gpr[rs] != cpu.gpr[rt]);
|
||||
break;
|
||||
case 0x06:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) <= 0);
|
||||
break;
|
||||
case 0x07:
|
||||
branch(static_cast<int32_t>(cpu.gpr[rs]) > 0);
|
||||
break;
|
||||
case 0x08:
|
||||
overflowAdd(static_cast<int32_t>(cpu.gpr[rs]), simm, rt);
|
||||
break;
|
||||
case 0x09:
|
||||
write(rt, cpu.gpr[rs] + static_cast<uint32_t>(simm));
|
||||
break;
|
||||
case 0x0A:
|
||||
write(rt, static_cast<int32_t>(cpu.gpr[rs]) < simm ? 1u : 0u);
|
||||
break;
|
||||
case 0x0B:
|
||||
write(rt, cpu.gpr[rs] < static_cast<uint32_t>(simm) ? 1u : 0u);
|
||||
break;
|
||||
case 0x0C:
|
||||
write(rt, cpu.gpr[rs] & imm);
|
||||
break;
|
||||
case 0x0D:
|
||||
write(rt, cpu.gpr[rs] | imm);
|
||||
break;
|
||||
case 0x0E:
|
||||
write(rt, cpu.gpr[rs] ^ imm);
|
||||
break;
|
||||
case 0x0F:
|
||||
write(rt, imm << 16u);
|
||||
break;
|
||||
case 0x10:
|
||||
{
|
||||
const uint32_t copRs = rs;
|
||||
if (copRs == 0x00)
|
||||
load(rt, cpu.cop0[rd]);
|
||||
else if (copRs == 0x04)
|
||||
cpu.cop0[rd] = cpu.gpr[rt];
|
||||
else if (copRs == 0x10 && funct == 0x10)
|
||||
{
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
cpu.cop0[12] = (status & ~0x0Fu) | ((status >> 2u) & 0x0Fu);
|
||||
}
|
||||
else
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
case 0x20:
|
||||
case 0x24:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint8_t value = m_memory.read8(address);
|
||||
load(rt, opcode == 0x20
|
||||
? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int8_t>(value)))
|
||||
: value);
|
||||
break;
|
||||
}
|
||||
case 0x21:
|
||||
case 0x25:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
const uint16_t value = m_memory.read16(address);
|
||||
load(rt, opcode == 0x21 ? static_cast<uint32_t>(static_cast<int32_t>(static_cast<int16_t>(value))) : value);
|
||||
break;
|
||||
}
|
||||
case 0x22:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00FFFFFFu, 0x0000FFFFu, 0x000000FFu, 0x00000000u};
|
||||
static constexpr uint32_t shifts[4] = {24u, 16u, 8u, 0u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory << shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x23:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 4u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
load(rt, m_memory.read32(address));
|
||||
break;
|
||||
}
|
||||
case 0x26:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t memory = m_memory.read32(address & ~3u);
|
||||
const uint32_t old = cpu.gpr[rt];
|
||||
static constexpr uint32_t masks[4] = {0x00000000u, 0xFF000000u, 0xFFFF0000u, 0xFFFFFF00u};
|
||||
static constexpr uint32_t shifts[4] = {0u, 8u, 16u, 24u};
|
||||
load(rt, (old & masks[address & 3u]) | (memory >> shifts[address & 3u]));
|
||||
break;
|
||||
}
|
||||
case 0x28:
|
||||
m_memory.write8(cpu.gpr[rs] + static_cast<uint32_t>(simm), static_cast<uint8_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
case 0x29:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 1u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write16(address, static_cast<uint16_t>(cpu.gpr[rt]));
|
||||
break;
|
||||
}
|
||||
case 0x2A:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = (old & 0xFFFFFF00u) | (value >> 24u);
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0xFFFF0000u) | (value >> 16u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0xFF000000u) | (value >> 8u);
|
||||
break;
|
||||
case 3u:
|
||||
result = value;
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
case 0x2B:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
if (address & 3u)
|
||||
{
|
||||
raiseException(cpu, 5u, pc, wasDelaySlot, address);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(address, cpu.gpr[rt]);
|
||||
break;
|
||||
}
|
||||
case 0x2E:
|
||||
{
|
||||
const uint32_t address = cpu.gpr[rs] + static_cast<uint32_t>(simm);
|
||||
const uint32_t aligned = address & ~3u;
|
||||
const uint32_t old = m_memory.read32(aligned);
|
||||
const uint32_t value = cpu.gpr[rt];
|
||||
uint32_t result = old;
|
||||
switch (address & 3u)
|
||||
{
|
||||
case 0u:
|
||||
result = value;
|
||||
break;
|
||||
case 1u:
|
||||
result = (old & 0x000000FFu) | (value << 8u);
|
||||
break;
|
||||
case 2u:
|
||||
result = (old & 0x0000FFFFu) | (value << 16u);
|
||||
break;
|
||||
case 3u:
|
||||
result = (old & 0x00FFFFFFu) | (value << 24u);
|
||||
break;
|
||||
}
|
||||
m_memory.write32(aligned, result);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
raiseException(cpu, 10u, pc, wasDelaySlot);
|
||||
break;
|
||||
}
|
||||
|
||||
cpu.gpr[0] = 0u;
|
||||
if (cpu.exception)
|
||||
return !cpu.stopped;
|
||||
|
||||
if (cpu.pendingLoad)
|
||||
{
|
||||
if (cpu.pendingLoadReg != 0u && cpu.pendingLoadReg != writtenReg)
|
||||
cpu.gpr[cpu.pendingLoadReg] = cpu.pendingLoadValue;
|
||||
cpu.pendingLoad = false;
|
||||
}
|
||||
if (scheduledLoad)
|
||||
{
|
||||
cpu.pendingLoad = true;
|
||||
cpu.pendingLoadReg = scheduledReg;
|
||||
cpu.pendingLoadValue = scheduledValue;
|
||||
}
|
||||
cpu.gpr[0] = 0u;
|
||||
|
||||
if (wasDelaySlot)
|
||||
{
|
||||
cpu.pc = priorBranchTarget;
|
||||
cpu.branchPending = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.pc = nextPc;
|
||||
cpu.branchPending = newBranch;
|
||||
cpu.branchTarget = newBranchTarget;
|
||||
}
|
||||
return !cpu.stopped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopCpuState
|
||||
{
|
||||
std::array<uint32_t, 32> gpr{};
|
||||
uint32_t hi = 0;
|
||||
uint32_t lo = 0;
|
||||
uint32_t pc = 0;
|
||||
std::array<uint32_t, 32> cop0{};
|
||||
uint32_t pendingLoadReg = 0;
|
||||
uint32_t pendingLoadValue = 0;
|
||||
bool pendingLoad = false;
|
||||
bool branchPending = false;
|
||||
uint32_t branchTarget = 0;
|
||||
bool stopped = false;
|
||||
bool yielded = false;
|
||||
bool exception = false;
|
||||
};
|
||||
|
||||
class IopCpuCore
|
||||
{
|
||||
public:
|
||||
explicit IopCpuCore(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool executeInstruction(IopCpuState &cpu);
|
||||
[[nodiscard]] bool executeInstruction(IopCpuState &cpu, uint32_t instruction);
|
||||
void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional<uint32_t> badAddress = std::nullopt) const;
|
||||
|
||||
private:
|
||||
static void writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg);
|
||||
static void scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue);
|
||||
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,787 @@
|
||||
#include "iop_kernel.h"
|
||||
|
||||
#include "iop_memory.h"
|
||||
#include "../iop_emulator_const.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
IopKernel::IopKernel(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopKernel::reset()
|
||||
{
|
||||
m_threads.clear();
|
||||
m_semaphores.clear();
|
||||
m_eventFlags.clear();
|
||||
m_nextThreadId = 1;
|
||||
m_nextSemaphoreId = 1;
|
||||
m_nextEventFlagId = 1;
|
||||
m_currentThread = nullptr;
|
||||
m_nextReady = nullptr;
|
||||
m_nextWakeCycle = UINT64_MAX;
|
||||
m_scheduleDirty = true;
|
||||
m_hasDeadThreads = false;
|
||||
}
|
||||
|
||||
void IopKernel::setThreadState(IopThread &thread, IopThreadState state)
|
||||
{
|
||||
if (thread.state == state)
|
||||
return;
|
||||
thread.state = state;
|
||||
m_scheduleDirty = true;
|
||||
if (state == IopThreadState::Dead)
|
||||
m_hasDeadThreads = true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // CreateThread
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
IopThread thread;
|
||||
thread.id = static_cast<int>(m_nextThreadId++);
|
||||
thread.attr = m_memory.read32(descriptor + 0u);
|
||||
thread.option = m_memory.read32(descriptor + 4u);
|
||||
thread.entry = m_memory.read32(descriptor + 8u);
|
||||
thread.stackSize = std::max<uint32_t>(m_memory.read32(descriptor + 12u), 0x100u);
|
||||
thread.priority = std::clamp<uint32_t>(m_memory.read32(descriptor + 16u), 1u, 126u);
|
||||
thread.initialPriority = thread.priority;
|
||||
thread.stackBase = m_memory.allocate(thread.stackSize + kStackGuardBytes, 16u);
|
||||
if (thread.stackBase == 0u)
|
||||
{
|
||||
setV0(-400);
|
||||
return true;
|
||||
}
|
||||
const int id = thread.id;
|
||||
m_threads.emplace(id, std::move(thread));
|
||||
m_scheduleDirty = true;
|
||||
setV0(id);
|
||||
return true;
|
||||
}
|
||||
case 5: // DeleteThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (&it->second == m_currentThread)
|
||||
{
|
||||
setThreadState(it->second, IopThreadState::Dead);
|
||||
cpu.stopped = cpu.yielded = true;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
m_scheduleDirty = true;
|
||||
if (it->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(it->second.stackBase);
|
||||
m_threads.erase(it);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6: // StartThread
|
||||
case 7: // StartThreadArgs
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
IopThread &thread = it->second;
|
||||
thread.cpu = {};
|
||||
thread.cpu.pc = thread.entry;
|
||||
thread.cpu.gpr[4] = cpu.gpr[5];
|
||||
thread.cpu.gpr[5] = ordinal == 7 ? cpu.gpr[6] : 0u;
|
||||
thread.cpu.gpr[28] = cpu.gpr[28];
|
||||
thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u;
|
||||
thread.cpu.gpr[31] = kThreadReturnSentinel;
|
||||
setThreadState(thread, IopThreadState::Ready);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8: // ExitThread
|
||||
case 9: // ExitDeleteThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
setThreadState(*m_currentThread, ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant);
|
||||
cpu.stopped = true;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10:
|
||||
case 11: // TerminateThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
setThreadState(it->second, IopThreadState::Dormant);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 12:
|
||||
case 13:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 14:
|
||||
case 15:
|
||||
{
|
||||
int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
it->second.priority = std::clamp<uint32_t>(cpu.gpr[5], 1u, 126u);
|
||||
m_scheduleDirty = true;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 16:
|
||||
case 17:
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
return true;
|
||||
case 18:
|
||||
case 19:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep ||
|
||||
it->second.state == IopThreadState::Delay ||
|
||||
it->second.state == IopThreadState::Semaphore ||
|
||||
it->second.state == IopThreadState::EventFlag)
|
||||
{
|
||||
setThreadState(it->second, IopThreadState::Ready);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
setV0(m_currentThread != nullptr ? m_currentThread->id : 0);
|
||||
return true;
|
||||
case 21:
|
||||
setV0(0x1000);
|
||||
return true;
|
||||
case 22:
|
||||
case 23:
|
||||
setV0(referThreadStatus(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]) ? 0 : -1);
|
||||
return true;
|
||||
case 24: // SleepThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
if (m_currentThread->wakeupCount > 0)
|
||||
--m_currentThread->wakeupCount;
|
||||
else
|
||||
sleepCurrent(cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 25:
|
||||
case 26:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Sleep)
|
||||
setThreadState(it->second, IopThreadState::Ready);
|
||||
else
|
||||
++it->second.wakeupCount;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
case 28:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const int old = it->second.wakeupCount;
|
||||
it->second.wakeupCount = 0;
|
||||
setV0(old);
|
||||
return true;
|
||||
}
|
||||
case 29:
|
||||
case 30: // SuspendThread / iSuspendThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
setThreadState(it->second, IopThreadState::Suspended);
|
||||
if (m_currentThread == &it->second)
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 31:
|
||||
case 32: // ResumeThread / iResumeThread
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.state == IopThreadState::Suspended)
|
||||
setThreadState(it->second, IopThreadState::Ready);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // DelayThread
|
||||
if (m_currentThread != nullptr)
|
||||
{
|
||||
const uint64_t delayCycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz + 999'999ull) / 1'000'000ull;
|
||||
delayCurrentUntil(currentCycle + std::max<uint64_t>(delayCycles, 1u), cpu);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 34: // GetSystemTime
|
||||
m_memory.write32(cpu.gpr[4], static_cast<uint32_t>(currentCycle));
|
||||
m_memory.write32(cpu.gpr[4] + 4u, static_cast<uint32_t>(currentCycle >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
case 35:
|
||||
case 36:
|
||||
case 37:
|
||||
case 38:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 39: // USec2SysClock
|
||||
{
|
||||
const uint64_t cycles = (static_cast<uint64_t>(cpu.gpr[4]) * kIopClockHz) / 1'000'000ull;
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(cycles));
|
||||
m_memory.write32(cpu.gpr[5] + 4u, static_cast<uint32_t>(cycles >> 32u));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 40:
|
||||
{
|
||||
const uint64_t cycles = static_cast<uint64_t>(m_memory.read32(cpu.gpr[4])) | (static_cast<uint64_t>(m_memory.read32(cpu.gpr[4] + 4u)) << 32u);
|
||||
const uint64_t usec = (cycles * 1'000'000ull) / kIopClockHz;
|
||||
if (cpu.gpr[5] != 0u)
|
||||
m_memory.write32(cpu.gpr[5], static_cast<uint32_t>(usec / 1'000'000ull));
|
||||
if (cpu.gpr[6] != 0u)
|
||||
m_memory.write32(cpu.gpr[6], static_cast<uint32_t>(usec % 1'000'000ull));
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 41:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::referThreadStatus(int id, uint32_t outputAddress)
|
||||
{
|
||||
if (id == 0 && m_currentThread != nullptr)
|
||||
id = m_currentThread->id;
|
||||
const auto it = m_threads.find(id);
|
||||
if (it == m_threads.end() || outputAddress == 0u)
|
||||
return false;
|
||||
|
||||
const IopThread &thread = it->second;
|
||||
uint32_t status = 0x10u;
|
||||
switch (thread.state)
|
||||
{
|
||||
case IopThreadState::Running:
|
||||
status = 0x01u;
|
||||
break;
|
||||
case IopThreadState::Ready:
|
||||
status = 0x02u;
|
||||
break;
|
||||
case IopThreadState::Sleep:
|
||||
case IopThreadState::Delay:
|
||||
case IopThreadState::Semaphore:
|
||||
case IopThreadState::EventFlag:
|
||||
status = 0x04u;
|
||||
break;
|
||||
case IopThreadState::Suspended:
|
||||
status = 0x08u;
|
||||
break;
|
||||
default:
|
||||
status = 0x10u;
|
||||
break;
|
||||
}
|
||||
|
||||
m_memory.write32(outputAddress + 0u, thread.attr);
|
||||
m_memory.write32(outputAddress + 4u, thread.option);
|
||||
m_memory.write32(outputAddress + 8u, status);
|
||||
m_memory.write32(outputAddress + 12u, thread.entry);
|
||||
m_memory.write32(outputAddress + 16u, thread.stackBase);
|
||||
m_memory.write32(outputAddress + 20u, thread.stackSize);
|
||||
m_memory.write32(outputAddress + 24u, thread.cpu.gpr[28]);
|
||||
m_memory.write32(outputAddress + 28u, thread.initialPriority);
|
||||
m_memory.write32(outputAddress + 32u, thread.priority);
|
||||
m_memory.write32(outputAddress + 36u, thread.state == IopThreadState::Sleep ? 1u : thread.state == IopThreadState::Delay ? 2u
|
||||
: thread.state == IopThreadState::Semaphore ? 3u
|
||||
: thread.state == IopThreadState::EventFlag ? 4u
|
||||
: 0u);
|
||||
m_memory.write32(outputAddress + 40u, static_cast<uint32_t>(thread.waitId));
|
||||
m_memory.write32(outputAddress + 44u, static_cast<uint32_t>(thread.wakeupCount));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
Semaphore semaphore;
|
||||
semaphore.id = static_cast<int>(m_nextSemaphoreId++);
|
||||
semaphore.attr = m_memory.read32(descriptor + 0u);
|
||||
semaphore.option = m_memory.read32(descriptor + 4u);
|
||||
semaphore.current = static_cast<int>(m_memory.read32(descriptor + 8u));
|
||||
semaphore.maximum = std::max(1, static_cast<int>(m_memory.read32(descriptor + 12u)));
|
||||
m_semaphores.emplace(semaphore.id, semaphore);
|
||||
setV0(semaphore.id);
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
setV0(m_semaphores.erase(static_cast<int>(cpu.gpr[4])) != 0u ? 0 : -1);
|
||||
return true;
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current < it->second.maximum)
|
||||
++it->second.current;
|
||||
wakeOneSemaphore(id);
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (it->second.current > 0)
|
||||
{
|
||||
--it->second.current;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 9)
|
||||
setV0(-419);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
setThreadState(*m_currentThread, IopThreadState::Semaphore);
|
||||
m_currentThread->waitId = id;
|
||||
cpu.yielded = true;
|
||||
setV0(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 11:
|
||||
case 12:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
const auto it = m_semaphores.find(id);
|
||||
if (it == m_semaphores.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t outputAddress = cpu.gpr[5];
|
||||
if (outputAddress != 0u)
|
||||
{
|
||||
m_memory.write32(outputAddress + 0u, it->second.attr);
|
||||
m_memory.write32(outputAddress + 4u, it->second.option);
|
||||
m_memory.write32(outputAddress + 8u, 0u);
|
||||
m_memory.write32(outputAddress + 12u, static_cast<uint32_t>(it->second.maximum));
|
||||
m_memory.write32(outputAddress + 16u, static_cast<uint32_t>(it->second.current));
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Semaphore && thread.waitId == id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(outputAddress + 20u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::wakeOneSemaphore(int id)
|
||||
{
|
||||
IopThread *best = nullptr;
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::Semaphore || thread.waitId != id)
|
||||
continue;
|
||||
if (best == nullptr || thread.priority < best->priority)
|
||||
best = &thread;
|
||||
}
|
||||
|
||||
const auto semaphore = m_semaphores.find(id);
|
||||
if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0)
|
||||
{
|
||||
--semaphore->second.current;
|
||||
setThreadState(*best, IopThreadState::Ready);
|
||||
best->waitId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopKernel::eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode)
|
||||
{
|
||||
if (bits == 0u)
|
||||
return false;
|
||||
return (mode & 1u) != 0u ? (event.bits & bits) != 0u : (event.bits & bits) == bits;
|
||||
}
|
||||
|
||||
void IopKernel::wakeEventWaiters(EventFlag &event)
|
||||
{
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state != IopThreadState::EventFlag || thread.waitId != event.id)
|
||||
continue;
|
||||
if (!eventSatisfied(event, thread.waitBits, thread.waitMode))
|
||||
continue;
|
||||
|
||||
if (thread.waitResultAddress != 0u)
|
||||
m_memory.write32(thread.waitResultAddress, event.bits);
|
||||
thread.cpu.gpr[2] = 0u;
|
||||
if ((thread.waitMode & 0x10u) != 0u)
|
||||
event.bits = 0u;
|
||||
setThreadState(thread, IopThreadState::Ready);
|
||||
thread.waitId = 0;
|
||||
thread.waitBits = 0;
|
||||
thread.waitMode = 0;
|
||||
thread.waitResultAddress = 0;
|
||||
if (event.bits == 0u)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int IopKernel::createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits)
|
||||
{
|
||||
EventFlag event;
|
||||
event.id = static_cast<int>(m_nextEventFlagId++);
|
||||
event.attr = attr;
|
||||
event.option = option;
|
||||
event.bits = bits;
|
||||
const int id = event.id;
|
||||
m_eventFlags.emplace(id, event);
|
||||
return id;
|
||||
}
|
||||
|
||||
bool IopKernel::setInternalEventFlag(int id, uint32_t bits)
|
||||
{
|
||||
const auto event = m_eventFlags.find(id);
|
||||
if (event == m_eventFlags.end())
|
||||
return false;
|
||||
event->second.bits |= bits;
|
||||
wakeEventWaiters(event->second);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](int32_t value)
|
||||
{
|
||||
cpu.gpr[2] = static_cast<uint32_t>(value);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4:
|
||||
{
|
||||
const uint32_t descriptor = cpu.gpr[4];
|
||||
setV0(createInternalEventFlag(m_memory.read32(descriptor + 0u),
|
||||
m_memory.read32(descriptor + 4u),
|
||||
m_memory.read32(descriptor + 8u)));
|
||||
return true;
|
||||
}
|
||||
case 5:
|
||||
{
|
||||
const int id = static_cast<int>(cpu.gpr[4]);
|
||||
if (m_eventFlags.erase(id) == 0u)
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
for (auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == id)
|
||||
{
|
||||
setThreadState(thread, IopThreadState::Ready);
|
||||
thread.waitId = 0;
|
||||
thread.cpu.gpr[2] = static_cast<uint32_t>(-1);
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 6:
|
||||
case 7:
|
||||
{
|
||||
if (!setInternalEventFlag(static_cast<int>(cpu.gpr[4]), cpu.gpr[5]))
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 8:
|
||||
case 9:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
// IOP ClearEventFlag applies a mask: callers pass ~bitsToClear.
|
||||
event->second.bits &= cpu.gpr[5];
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 10: // WaitEventFlag
|
||||
case 11: // PollEventFlag
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
const uint32_t bits = cpu.gpr[5];
|
||||
const uint32_t mode = cpu.gpr[6];
|
||||
if (eventSatisfied(event->second, bits, mode))
|
||||
{
|
||||
if (cpu.gpr[7] != 0u)
|
||||
m_memory.write32(cpu.gpr[7], event->second.bits);
|
||||
if ((mode & 0x10u) != 0u)
|
||||
event->second.bits = 0u;
|
||||
setV0(0);
|
||||
}
|
||||
else if (ordinal == 11)
|
||||
setV0(-418);
|
||||
else if (m_currentThread != nullptr)
|
||||
{
|
||||
setThreadState(*m_currentThread, IopThreadState::EventFlag);
|
||||
m_currentThread->waitId = event->second.id;
|
||||
m_currentThread->waitBits = bits;
|
||||
m_currentThread->waitMode = mode;
|
||||
m_currentThread->waitResultAddress = cpu.gpr[7];
|
||||
setV0(0);
|
||||
cpu.yielded = true;
|
||||
}
|
||||
else
|
||||
setV0(-418);
|
||||
return true;
|
||||
}
|
||||
case 13:
|
||||
case 14:
|
||||
{
|
||||
const auto event = m_eventFlags.find(static_cast<int>(cpu.gpr[4]));
|
||||
if (event == m_eventFlags.end())
|
||||
{
|
||||
setV0(-1);
|
||||
return true;
|
||||
}
|
||||
if (cpu.gpr[5] != 0u)
|
||||
{
|
||||
uint32_t waiters = 0u;
|
||||
for (const auto &[threadId, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::EventFlag && thread.waitId == event->second.id)
|
||||
++waiters;
|
||||
}
|
||||
m_memory.write32(cpu.gpr[5] + 0u, event->second.attr);
|
||||
m_memory.write32(cpu.gpr[5] + 4u, event->second.option);
|
||||
m_memory.write32(cpu.gpr[5] + 8u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 12u, event->second.bits);
|
||||
m_memory.write32(cpu.gpr[5] + 16u, waiters);
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::sleepCurrent(IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
setThreadState(*m_currentThread, IopThreadState::Sleep);
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
void IopKernel::delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu)
|
||||
{
|
||||
if (m_currentThread == nullptr)
|
||||
return;
|
||||
m_currentThread->wakeCycle = wakeCycle;
|
||||
setThreadState(*m_currentThread, IopThreadState::Delay);
|
||||
m_scheduleDirty = true;
|
||||
cpu.yielded = true;
|
||||
}
|
||||
|
||||
IopThread *IopKernel::beginNextReady(uint64_t currentCycle)
|
||||
{
|
||||
if (m_scheduleDirty || currentCycle >= m_nextWakeCycle)
|
||||
{
|
||||
m_nextReady = nullptr;
|
||||
m_nextWakeCycle = UINT64_MAX;
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay)
|
||||
{
|
||||
if (thread.wakeCycle <= currentCycle)
|
||||
thread.state = IopThreadState::Ready;
|
||||
else
|
||||
m_nextWakeCycle = std::min(m_nextWakeCycle, thread.wakeCycle);
|
||||
}
|
||||
if (thread.state == IopThreadState::Ready && (m_nextReady == nullptr || thread.priority < m_nextReady->priority ||
|
||||
(thread.priority == m_nextReady->priority && thread.id < m_nextReady->id)))
|
||||
m_nextReady = &thread;
|
||||
}
|
||||
m_scheduleDirty = false;
|
||||
}
|
||||
IopThread *next = m_nextReady;
|
||||
if (next == nullptr)
|
||||
return nullptr;
|
||||
|
||||
m_currentThread = next;
|
||||
next->state = IopThreadState::Running;
|
||||
next->cpu.stopped = false;
|
||||
next->cpu.yielded = false;
|
||||
return next;
|
||||
}
|
||||
|
||||
uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const
|
||||
{
|
||||
if (!m_scheduleDirty)
|
||||
return std::min(fallback, m_nextWakeCycle);
|
||||
uint64_t nextWake = fallback;
|
||||
for (const auto &[id, thread] : m_threads)
|
||||
{
|
||||
if (thread.state == IopThreadState::Delay)
|
||||
nextWake = std::min(nextWake, thread.wakeCycle);
|
||||
}
|
||||
return nextWake;
|
||||
}
|
||||
|
||||
void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel)
|
||||
{
|
||||
if (thread.state != IopThreadState::Dead && (thread.cpu.pc == returnSentinel || thread.cpu.stopped))
|
||||
setThreadState(thread, IopThreadState::Dormant);
|
||||
else if (thread.state == IopThreadState::Running)
|
||||
thread.state = IopThreadState::Ready;
|
||||
m_currentThread = nullptr;
|
||||
cleanupDeadThreads();
|
||||
}
|
||||
|
||||
void IopKernel::cleanupDeadThreads()
|
||||
{
|
||||
if (!m_hasDeadThreads)
|
||||
return;
|
||||
m_hasDeadThreads = false;
|
||||
for (auto thread = m_threads.begin(); thread != m_threads.end();)
|
||||
{
|
||||
if (thread->second.state != IopThreadState::Dead)
|
||||
{
|
||||
++thread;
|
||||
continue;
|
||||
}
|
||||
if (&thread->second == m_currentThread)
|
||||
{
|
||||
thread->second.cpu.stopped = thread->second.cpu.yielded = true;
|
||||
m_hasDeadThreads = true;
|
||||
++thread;
|
||||
continue;
|
||||
}
|
||||
m_scheduleDirty = true;
|
||||
if (thread->second.stackBase != 0u)
|
||||
(void)m_memory.freeAllocation(thread->second.stackBase);
|
||||
thread = m_threads.erase(thread);
|
||||
}
|
||||
}
|
||||
|
||||
void IopKernel::terminateThreadsInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto &[id, thread] : m_threads)
|
||||
{
|
||||
const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc);
|
||||
if (pc >= base && pc < base + size)
|
||||
setThreadState(thread, IopThreadState::Dead);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_cpu.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopThreadState : uint8_t
|
||||
{
|
||||
Dormant,
|
||||
Ready,
|
||||
Running,
|
||||
Sleep,
|
||||
Delay,
|
||||
Semaphore,
|
||||
EventFlag,
|
||||
Suspended,
|
||||
Dead,
|
||||
};
|
||||
|
||||
struct IopThread
|
||||
{
|
||||
int id = 0;
|
||||
IopThreadState state = IopThreadState::Dormant;
|
||||
IopCpuState cpu;
|
||||
uint32_t entry = 0;
|
||||
uint32_t stackBase = 0;
|
||||
uint32_t stackSize = 0;
|
||||
uint32_t priority = 0x40;
|
||||
uint32_t initialPriority = 0x40;
|
||||
uint32_t option = 0;
|
||||
uint32_t attr = 0;
|
||||
uint64_t wakeCycle = 0;
|
||||
int waitId = 0;
|
||||
uint32_t waitBits = 0;
|
||||
uint32_t waitMode = 0;
|
||||
uint32_t waitResultAddress = 0;
|
||||
int wakeupCount = 0;
|
||||
};
|
||||
|
||||
class IopKernel
|
||||
{
|
||||
public:
|
||||
explicit IopKernel(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] bool dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
[[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
[[nodiscard]] int createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits);
|
||||
[[nodiscard]] bool setInternalEventFlag(int id, uint32_t bits);
|
||||
|
||||
void sleepCurrent(IopCpuState &cpu);
|
||||
void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu);
|
||||
|
||||
[[nodiscard]] IopThread *beginNextReady(uint64_t currentCycle);
|
||||
[[nodiscard]] uint64_t nextWakeCycle(uint64_t fallback) const;
|
||||
void endTimeslice(IopThread &thread, uint32_t returnSentinel);
|
||||
void cleanupDeadThreads();
|
||||
void terminateThreadsInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] size_t threadCount() const noexcept { return m_threads.size(); }
|
||||
|
||||
private:
|
||||
struct Semaphore
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
int current = 0;
|
||||
int maximum = 1;
|
||||
};
|
||||
|
||||
struct EventFlag
|
||||
{
|
||||
int id = 0;
|
||||
uint32_t bits = 0;
|
||||
uint32_t attr = 0;
|
||||
uint32_t option = 0;
|
||||
};
|
||||
|
||||
[[nodiscard]] bool referThreadStatus(int id, uint32_t outputAddress);
|
||||
void wakeOneSemaphore(int id);
|
||||
[[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode);
|
||||
void wakeEventWaiters(EventFlag &event);
|
||||
void setThreadState(IopThread &thread, IopThreadState state);
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<int, IopThread> m_threads;
|
||||
std::unordered_map<int, Semaphore> m_semaphores;
|
||||
std::unordered_map<int, EventFlag> m_eventFlags;
|
||||
uint32_t m_nextThreadId = 1;
|
||||
uint32_t m_nextSemaphoreId = 1;
|
||||
uint32_t m_nextEventFlagId = 1;
|
||||
IopThread *m_currentThread = nullptr;
|
||||
IopThread *m_nextReady = nullptr;
|
||||
uint64_t m_nextWakeCycle = UINT64_MAX;
|
||||
bool m_scheduleDirty = true;
|
||||
bool m_hasDeadThreads = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
#include "iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kDmaSpu0Chcr = 0x1F8010C8u;
|
||||
constexpr uint32_t kDmaSpu1Chcr = 0x1F801508u;
|
||||
constexpr uint32_t kDmaStart = 1u << 24u;
|
||||
constexpr int kDmaSpu0Irq = 0x24;
|
||||
constexpr int kDmaSpu1Irq = 0x28;
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
return (value + alignment - 1u) & ~(alignment - 1u);
|
||||
}
|
||||
}
|
||||
|
||||
IopMemory::IopMemory()
|
||||
: m_ram(RamSize), m_owned(RamSize), m_scratch(ScratchSize)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void IopMemory::reset()
|
||||
{
|
||||
std::fill(m_ram.begin(), m_ram.end(), uint8_t{0});
|
||||
std::fill(m_owned.begin(), m_owned.end(), uint8_t{0});
|
||||
std::fill(m_scratch.begin(), m_scratch.end(), uint8_t{0});
|
||||
m_hardware.clear();
|
||||
m_allocations.clear();
|
||||
m_heapCursor = HeapBase;
|
||||
m_interruptStatus = 0;
|
||||
m_interruptMask = 0;
|
||||
m_interruptControl = 1;
|
||||
m_dmaStart.reset();
|
||||
}
|
||||
|
||||
uint32_t IopMemory::physicalAddress(uint32_t address) noexcept
|
||||
{
|
||||
return address & 0x1FFFFFFFu;
|
||||
}
|
||||
|
||||
uint8_t IopMemory::read8(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
return m_ram[phys];
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
return m_scratch[phys - ScratchBase];
|
||||
const uint32_t value = readHardware32(phys & ~3u);
|
||||
return static_cast<uint8_t>(value >> ((phys & 3u) * 8u));
|
||||
}
|
||||
|
||||
uint16_t IopMemory::read16(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
uint16_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
return static_cast<uint16_t>(read8(address) | (static_cast<uint16_t>(read8(address + 1u)) << 8u));
|
||||
}
|
||||
|
||||
uint32_t IopMemory::read32(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_ram.data() + phys, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
uint32_t value;
|
||||
std::memcpy(&value, m_scratch.data() + (phys - ScratchBase), sizeof(value));
|
||||
return value;
|
||||
}
|
||||
if ((phys & 3u) == 0u && isHardwareAddress(phys))
|
||||
return readHardware32(phys);
|
||||
|
||||
return static_cast<uint32_t>(read8(address)) |
|
||||
(static_cast<uint32_t>(read8(address + 1u)) << 8u) |
|
||||
(static_cast<uint32_t>(read8(address + 2u)) << 16u) |
|
||||
(static_cast<uint32_t>(read8(address + 3u)) << 24u);
|
||||
}
|
||||
|
||||
void IopMemory::write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys < RamSize)
|
||||
{
|
||||
m_ram[phys] = value;
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if (phys >= ScratchBase && phys < ScratchBase + ScratchSize)
|
||||
{
|
||||
m_scratch[phys - ScratchBase] = value;
|
||||
return;
|
||||
}
|
||||
const uint32_t aligned = phys & ~3u;
|
||||
uint32_t current = readHardware32(aligned);
|
||||
const uint32_t shift = (phys & 3u) * 8u;
|
||||
current = (current & ~(0xFFu << shift)) | (static_cast<uint32_t>(value) << shift);
|
||||
writeHardware32(aligned, current);
|
||||
}
|
||||
|
||||
void IopMemory::write16(uint32_t address, uint16_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys + 1u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
}
|
||||
|
||||
void IopMemory::write32(uint32_t address, uint32_t value)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((phys & 3u) == 0u && phys + 3u < RamSize)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, &value, sizeof(value));
|
||||
markOwned(phys, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize)
|
||||
{
|
||||
std::memcpy(m_scratch.data() + (phys - ScratchBase), &value, sizeof(value));
|
||||
return;
|
||||
}
|
||||
if ((phys & 3u) == 0u)
|
||||
{
|
||||
writeHardware32(phys, value);
|
||||
return;
|
||||
}
|
||||
write8(address, static_cast<uint8_t>(value));
|
||||
write8(address + 1u, static_cast<uint8_t>(value >> 8u));
|
||||
write8(address + 2u, static_cast<uint8_t>(value >> 16u));
|
||||
write8(address + 3u, static_cast<uint8_t>(value >> 24u));
|
||||
}
|
||||
|
||||
bool IopMemory::readRam(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!destination && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, m_ram.data() + phys, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::writeRam(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if ((!source && size != 0u) || phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memcpy(m_ram.data() + phys, source, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::zeroRam(uint32_t address, size_t size)
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
{
|
||||
std::memset(m_ram.data() + phys, 0, size);
|
||||
markOwned(phys, size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopMemory::ownsRamRange(uint32_t address, size_t size) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
if (phys > RamSize || size > RamSize - phys)
|
||||
return false;
|
||||
return std::all_of(m_owned.begin() + phys, m_owned.begin() + phys + size,
|
||||
[](uint8_t value)
|
||||
{ return value != 0u; });
|
||||
}
|
||||
|
||||
void IopMemory::markOwned(uint32_t address, size_t size)
|
||||
{
|
||||
if (address > RamSize || size > RamSize - address)
|
||||
return;
|
||||
std::fill(m_owned.begin() + address, m_owned.begin() + address + size, uint8_t{1});
|
||||
}
|
||||
|
||||
bool IopMemory::isHardwareAddress(uint32_t address) const
|
||||
{
|
||||
const uint32_t phys = physicalAddress(address);
|
||||
return (phys >= HardwareBase && phys < HardwareEnd) ||
|
||||
(phys >= Spu2Base && phys < Spu2End) ||
|
||||
(phys >= SifBase && phys < SifEnd);
|
||||
}
|
||||
|
||||
uint32_t IopMemory::readHardware32(uint32_t address) const
|
||||
{
|
||||
const auto value = m_hardware.find(address);
|
||||
if (value != m_hardware.end())
|
||||
return value->second;
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
return m_interruptStatus;
|
||||
case 0x1F801074u:
|
||||
return m_interruptMask;
|
||||
case 0x1F801078u:
|
||||
return m_interruptControl;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
void IopMemory::writeHardware32(uint32_t address, uint32_t value)
|
||||
{
|
||||
switch (address)
|
||||
{
|
||||
case 0x1F801070u:
|
||||
m_interruptStatus &= value;
|
||||
return;
|
||||
case 0x1F801074u:
|
||||
m_interruptMask = value;
|
||||
return;
|
||||
case 0x1F801078u:
|
||||
m_interruptControl = value & 1u;
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
m_hardware[address] = value;
|
||||
if ((address != kDmaSpu0Chcr && address != kDmaSpu1Chcr) || (value & kDmaStart) == 0u)
|
||||
return;
|
||||
|
||||
const bool secondCore = address == kDmaSpu1Chcr;
|
||||
m_hardware[address] = value & ~kDmaStart;
|
||||
|
||||
const uint32_t statusAddress = 0x1F900344u + (secondCore ? 0x400u : 0u);
|
||||
const uint32_t alignedStatus = statusAddress & ~3u;
|
||||
const uint32_t shift = (statusAddress & 2u) * 8u;
|
||||
uint32_t status = 0u;
|
||||
if (const auto current = m_hardware.find(alignedStatus); current != m_hardware.end())
|
||||
status = current->second;
|
||||
status |= 0x80u << shift;
|
||||
m_hardware[alignedStatus] = status;
|
||||
|
||||
const uint32_t blockControlAddress = address - sizeof(uint32_t);
|
||||
uint32_t blockControl = 0u;
|
||||
if (const auto current = m_hardware.find(blockControlAddress); current != m_hardware.end())
|
||||
blockControl = current->second;
|
||||
const uint32_t wordsPerBlock = std::max<uint32_t>(blockControl & 0xFFFFu, 1u);
|
||||
const uint32_t blockCount = std::max<uint32_t>(blockControl >> 16u, 1u);
|
||||
const uint64_t transferWords = static_cast<uint64_t>(wordsPerBlock) * blockCount;
|
||||
m_dmaStart = DmaStart{
|
||||
secondCore ? kDmaSpu1Irq : kDmaSpu0Irq,
|
||||
std::max<uint64_t>(transferWords * 2u, 64u),
|
||||
};
|
||||
}
|
||||
|
||||
std::optional<IopMemory::DmaStart> IopMemory::takeDmaStart() noexcept
|
||||
{
|
||||
std::optional<DmaStart> result = m_dmaStart;
|
||||
m_dmaStart.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::allocate(uint32_t size, uint32_t alignment, std::optional<uint32_t> fixed)
|
||||
{
|
||||
size = alignUp(std::max(size, 1u), 16u);
|
||||
alignment = std::max<uint32_t>(alignment, 4u);
|
||||
if (fixed)
|
||||
{
|
||||
const uint32_t address = *fixed;
|
||||
if (address < HeapBase || address + size > HeapLimit)
|
||||
return 0u;
|
||||
for (const auto &block : m_allocations)
|
||||
if (address < block.address + block.size && block.address < address + size)
|
||||
return 0u;
|
||||
m_allocations.push_back({address, size});
|
||||
markOwned(address, size);
|
||||
return address;
|
||||
}
|
||||
|
||||
uint32_t candidate = alignUp(m_heapCursor, alignment);
|
||||
for (;;)
|
||||
{
|
||||
bool overlap = false;
|
||||
for (const auto &block : m_allocations)
|
||||
{
|
||||
if (candidate < block.address + block.size && block.address < candidate + size)
|
||||
{
|
||||
candidate = alignUp(block.address + block.size, alignment);
|
||||
overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!overlap)
|
||||
break;
|
||||
}
|
||||
if (candidate > HeapLimit || size > HeapLimit - candidate)
|
||||
return 0u;
|
||||
m_allocations.push_back({candidate, size});
|
||||
markOwned(candidate, size);
|
||||
m_heapCursor = std::max(m_heapCursor, candidate + size);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
bool IopMemory::freeAllocation(uint32_t address)
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{ return candidate.address == address; });
|
||||
if (block == m_allocations.end())
|
||||
return false;
|
||||
std::fill(m_owned.begin() + block->address,
|
||||
m_owned.begin() + block->address + block->size,
|
||||
uint8_t{0});
|
||||
m_allocations.erase(block);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t IopMemory::maxFreeMemory() const
|
||||
{
|
||||
return m_heapCursor < HeapLimit ? HeapLimit - m_heapCursor : 0u;
|
||||
}
|
||||
|
||||
std::optional<IopMemory::Allocation> IopMemory::allocationContaining(uint32_t address) const
|
||||
{
|
||||
const auto block = std::find_if(m_allocations.begin(), m_allocations.end(),
|
||||
[&](const Allocation &candidate)
|
||||
{
|
||||
return address >= candidate.address &&
|
||||
address < candidate.address + candidate.size;
|
||||
});
|
||||
if (block == m_allocations.end())
|
||||
return std::nullopt;
|
||||
return *block;
|
||||
}
|
||||
|
||||
std::string IopMemory::readString(uint32_t address, size_t limit) const
|
||||
{
|
||||
std::string result;
|
||||
result.reserve(std::min<size_t>(limit, 64u));
|
||||
for (size_t i = 0; i < limit; ++i)
|
||||
{
|
||||
const char ch = static_cast<char>(read8(address + static_cast<uint32_t>(i)));
|
||||
if (ch == '\0')
|
||||
break;
|
||||
result.push_back(ch);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory
|
||||
{
|
||||
public:
|
||||
static constexpr uint32_t RamSize = 2u * 1024u * 1024u;
|
||||
static constexpr uint32_t ScratchBase = 0x1F800000u;
|
||||
static constexpr uint32_t ScratchSize = 0x400u;
|
||||
static constexpr uint32_t HardwareBase = 0x1F801000u;
|
||||
static constexpr uint32_t HardwareEnd = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2Base = 0x1F900000u;
|
||||
static constexpr uint32_t Spu2End = 0x1FA00000u;
|
||||
static constexpr uint32_t SifBase = 0x1D000000u;
|
||||
static constexpr uint32_t SifEnd = 0x1D001000u;
|
||||
static constexpr uint32_t HeapBase = 0x00120000u;
|
||||
static constexpr uint32_t HeapLimit = 0x001F0000u;
|
||||
|
||||
struct Allocation
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t size = 0;
|
||||
};
|
||||
|
||||
struct DmaStart
|
||||
{
|
||||
int irq = 0;
|
||||
uint64_t delayCycles = 0;
|
||||
};
|
||||
|
||||
IopMemory();
|
||||
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] uint8_t read8(uint32_t address) const;
|
||||
[[nodiscard]] uint16_t read16(uint32_t address) const;
|
||||
[[nodiscard]] uint32_t read32(uint32_t address) const;
|
||||
void write8(uint32_t address, uint8_t value);
|
||||
void write16(uint32_t address, uint16_t value);
|
||||
void write32(uint32_t address, uint32_t value);
|
||||
|
||||
[[nodiscard]] bool readRam(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeRam(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroRam(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool ownsRamRange(uint32_t address, size_t size) const;
|
||||
[[nodiscard]] bool isHardwareAddress(uint32_t address) const;
|
||||
[[nodiscard]] std::string readString(uint32_t address, size_t limit = 1024u) const;
|
||||
|
||||
[[nodiscard]] uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt);
|
||||
[[nodiscard]] bool freeAllocation(uint32_t address);
|
||||
[[nodiscard]] uint32_t maxFreeMemory() const;
|
||||
[[nodiscard]] std::optional<Allocation> allocationContaining(uint32_t address) const;
|
||||
|
||||
[[nodiscard]] uint32_t interruptStatus() const noexcept { return m_interruptStatus; }
|
||||
[[nodiscard]] uint32_t interruptMask() const noexcept { return m_interruptMask; }
|
||||
[[nodiscard]] uint32_t interruptControl() const noexcept { return m_interruptControl; }
|
||||
void setInterruptStatus(uint32_t value) noexcept { m_interruptStatus = value; }
|
||||
void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; }
|
||||
void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; }
|
||||
|
||||
[[nodiscard]] bool hasDmaStart() const noexcept { return m_dmaStart.has_value(); }
|
||||
[[nodiscard]] std::optional<DmaStart> takeDmaStart() noexcept;
|
||||
[[nodiscard]] std::span<const uint8_t> ram() const noexcept { return m_ram; }
|
||||
|
||||
[[nodiscard]] static uint32_t physicalAddress(uint32_t address) noexcept;
|
||||
|
||||
private:
|
||||
[[nodiscard]] uint32_t readHardware32(uint32_t address) const;
|
||||
void writeHardware32(uint32_t address, uint32_t value);
|
||||
void markOwned(uint32_t address, size_t size);
|
||||
|
||||
std::vector<uint8_t> m_ram;
|
||||
std::vector<uint8_t> m_owned;
|
||||
std::vector<uint8_t> m_scratch;
|
||||
std::unordered_map<uint32_t, uint32_t> m_hardware;
|
||||
std::vector<Allocation> m_allocations;
|
||||
uint32_t m_heapCursor = HeapBase;
|
||||
uint32_t m_interruptStatus = 0;
|
||||
uint32_t m_interruptMask = 0;
|
||||
uint32_t m_interruptControl = 1;
|
||||
std::optional<DmaStart> m_dmaStart;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
#include "iop_cdvd.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kSectorSize = 2048u;
|
||||
constexpr uint32_t kPrimaryVolumeDescriptorLsn = 16u;
|
||||
constexpr uint32_t kVolumeDescriptorTerminatorLsn = 17u;
|
||||
constexpr uint32_t kFirstDirectoryLsn = 20u;
|
||||
constexpr uint32_t kCdvdErrorNone = 0u;
|
||||
constexpr uint32_t kCdvdErrorRead = 0x30u;
|
||||
constexpr uint32_t kCdvdTypePs2Dvd = 0x14u;
|
||||
constexpr uint32_t kCdvdReadyComplete = 2u;
|
||||
constexpr uint32_t kCdvdStatusPause = 0x0Au;
|
||||
constexpr uint32_t kCdvdInitExit = 5u;
|
||||
constexpr uint32_t kCdvdCallbackRead = 1u;
|
||||
constexpr uint32_t kCdvdCallbackSeek = 4u;
|
||||
constexpr uint32_t kCdvdInterruptReadyBits = 0x29u;
|
||||
constexpr uint32_t kEventFlagMulti = 2u;
|
||||
constexpr uint32_t kCdvdStreamTimeout = 5000u;
|
||||
constexpr uint32_t kCdvdSyncTimeout = 15000u;
|
||||
constexpr uint32_t kCdvdmanVersion = 0x0226u;
|
||||
|
||||
uint32_t alignSectors(uint64_t bytes)
|
||||
{
|
||||
return static_cast<uint32_t>((bytes + kSectorSize - 1u) / kSectorSize);
|
||||
}
|
||||
|
||||
void writeLe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value);
|
||||
destination[1] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
|
||||
void writeBe16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
destination[0] = static_cast<uint8_t>(value >> 8u);
|
||||
destination[1] = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
void writeLe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> (i * 8u));
|
||||
}
|
||||
|
||||
void writeBe32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
destination[i] = static_cast<uint8_t>(value >> ((3u - i) * 8u));
|
||||
}
|
||||
|
||||
void writeBoth16(uint8_t *destination, uint16_t value)
|
||||
{
|
||||
writeLe16(destination, value);
|
||||
writeBe16(destination + 2u, value);
|
||||
}
|
||||
|
||||
void writeBoth32(uint8_t *destination, uint32_t value)
|
||||
{
|
||||
writeLe32(destination, value);
|
||||
writeBe32(destination + 4u, value);
|
||||
}
|
||||
|
||||
std::string isoName(const std::filesystem::path &path, bool directory)
|
||||
{
|
||||
std::string name = path.filename().string();
|
||||
for (char &character : name)
|
||||
{
|
||||
const unsigned char byte = static_cast<unsigned char>(character);
|
||||
character = byte < 0x80u ? static_cast<char>(std::toupper(byte)) : '_';
|
||||
}
|
||||
if (name.size() > 200u)
|
||||
name.resize(200u);
|
||||
if (!directory && name.find(';') == std::string::npos)
|
||||
name += ";1";
|
||||
return name;
|
||||
}
|
||||
|
||||
size_t directoryRecordSize(size_t identifierSize)
|
||||
{
|
||||
const size_t unpadded = 33u + identifierSize;
|
||||
return unpadded + (unpadded & 1u);
|
||||
}
|
||||
|
||||
uint32_t directoryBytesFor(const std::vector<size_t> &identifierSizes)
|
||||
{
|
||||
uint64_t cursor = 0u;
|
||||
for (const size_t identifierSize : identifierSizes)
|
||||
{
|
||||
const uint64_t recordSize = directoryRecordSize(identifierSize);
|
||||
const uint64_t sectorOffset = cursor % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
cursor += kSectorSize - sectorOffset;
|
||||
cursor += recordSize;
|
||||
}
|
||||
return static_cast<uint32_t>(std::max<uint64_t>(kSectorSize, alignSectors(cursor) * kSectorSize));
|
||||
}
|
||||
|
||||
std::string normalizedIsoComponent(std::string_view value)
|
||||
{
|
||||
std::string result(value);
|
||||
const size_t semicolon = result.rfind(';');
|
||||
if (semicolon != std::string::npos && semicolon + 1u < result.size() &&
|
||||
std::all_of(result.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u), result.end(),
|
||||
[](unsigned char ch)
|
||||
{ return std::isdigit(ch) != 0; }))
|
||||
{
|
||||
result.resize(semicolon);
|
||||
}
|
||||
std::transform(result.begin(), result.end(), result.begin(),
|
||||
[](unsigned char ch)
|
||||
{ return static_cast<char>(std::toupper(ch)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
size_t writeDirectoryRecord(uint8_t *destination,
|
||||
uint32_t lsn,
|
||||
uint32_t size,
|
||||
bool directory,
|
||||
const uint8_t *identifier,
|
||||
size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
std::memset(destination, 0, recordSize);
|
||||
destination[0] = static_cast<uint8_t>(recordSize);
|
||||
writeBoth32(destination + 2u, lsn);
|
||||
writeBoth32(destination + 10u, size);
|
||||
destination[25] = directory ? 2u : 0u;
|
||||
writeBoth16(destination + 28u, 1u);
|
||||
destination[32] = static_cast<uint8_t>(identifierSize);
|
||||
std::memcpy(destination + 33u, identifier, identifierSize);
|
||||
return recordSize;
|
||||
}
|
||||
}
|
||||
|
||||
class IopCdvd::Impl
|
||||
{
|
||||
public:
|
||||
struct Callback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
struct IsoNode
|
||||
{
|
||||
std::filesystem::path hostPath;
|
||||
std::string identifier;
|
||||
size_t parent = 0u;
|
||||
bool directory = false;
|
||||
uint32_t lsn = 0u;
|
||||
uint32_t size = 0u;
|
||||
uint32_t sectors = 0u;
|
||||
uint64_t handle = 0u;
|
||||
std::vector<size_t> children;
|
||||
};
|
||||
|
||||
Impl(IopHost &hostRef, IopMemory &memoryRef, IopKernel &kernelRef)
|
||||
: host(hostRef), memory(memoryRef), kernel(kernelRef)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl()
|
||||
{
|
||||
closeFiles();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
closeFiles();
|
||||
callback = {};
|
||||
initialized = false;
|
||||
mediaMode = 0u;
|
||||
currentLsn = 0u;
|
||||
lastError = kCdvdErrorNone;
|
||||
streamFlag = 0u;
|
||||
lastReadTimeout = 0u;
|
||||
interruptEventFlagId = 0;
|
||||
virtualIsoBuilt = false;
|
||||
virtualIsoValid = false;
|
||||
completionCallback.reset();
|
||||
nodes.clear();
|
||||
metadataSectors.clear();
|
||||
imageHandle = 0u;
|
||||
}
|
||||
|
||||
bool dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceCdInit
|
||||
initialized = a0 != kCdvdInitExit;
|
||||
if (initialized)
|
||||
{
|
||||
callback = {};
|
||||
completionCallback.reset();
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 5: // sceCdStandby
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 6: // sceCdRead
|
||||
if (readSectors(a0, a1, a2))
|
||||
{
|
||||
signalCommandComplete();
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackRead,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
}
|
||||
else
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 7: // sceCdSeek
|
||||
currentLsn = a0;
|
||||
lastError = kCdvdErrorNone;
|
||||
signalCommandComplete();
|
||||
if (callback.address != 0u)
|
||||
{
|
||||
completionCallback = CompletionCallback{
|
||||
callback.address,
|
||||
callback.gp,
|
||||
kCdvdCallbackSeek,
|
||||
};
|
||||
}
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
case 8: // sceCdGetError
|
||||
cpu.gpr[2] = lastError;
|
||||
return true;
|
||||
|
||||
case 10: // sceCdSearchFile
|
||||
cpu.gpr[2] = searchFile(a0, a1) ? 1u : 0u;
|
||||
return true;
|
||||
|
||||
case 11: // sceCdSync
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
|
||||
case 12: // sceCdGetDiskType
|
||||
cpu.gpr[2] = kCdvdTypePs2Dvd;
|
||||
return true;
|
||||
|
||||
case 13: // sceCdDiskReady
|
||||
cpu.gpr[2] = kCdvdReadyComplete;
|
||||
return true;
|
||||
|
||||
case 28: // sceCdStatus
|
||||
cpu.gpr[2] = kCdvdStatusPause;
|
||||
return true;
|
||||
|
||||
case 37: // sceCdCallback
|
||||
{
|
||||
const uint32_t previous = callback.address;
|
||||
callback = {a0, cpu.gpr[28]};
|
||||
cpu.gpr[2] = previous;
|
||||
return true;
|
||||
}
|
||||
|
||||
case 50: // sceCdSC
|
||||
{
|
||||
const int32_t code = static_cast<int32_t>(a0);
|
||||
switch (code)
|
||||
{
|
||||
case -23: // Translate a logical sector for a dual-layer disc.
|
||||
// The host image exposes one continuous LSN space, so no layer offset is required.
|
||||
cpu.gpr[2] = a1 != 0u ? memory.read32(a1) : 0u;
|
||||
return true;
|
||||
case -18:
|
||||
lastReadTimeout = a1 != 0u ? memory.read32(a1) : 0u;
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
case -17:
|
||||
cpu.gpr[2] = kCdvdStreamTimeout;
|
||||
return true;
|
||||
case -15:
|
||||
cpu.gpr[2] = kCdvdSyncTimeout;
|
||||
return true;
|
||||
case -11:
|
||||
cpu.gpr[2] = static_cast<uint32_t>(ensureInterruptEventFlag());
|
||||
return true;
|
||||
case -9:
|
||||
cpu.gpr[2] = kCdvdmanVersion;
|
||||
return true;
|
||||
case -2:
|
||||
lastError = a1 != 0u ? memory.read8(a1) : kCdvdErrorNone;
|
||||
cpu.gpr[2] = lastError;
|
||||
return true;
|
||||
case -1:
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
if (a1 != 0u)
|
||||
memory.write32(a1, lastError & 0xFFu);
|
||||
if (code != -1)
|
||||
streamFlag = static_cast<uint32_t>(code);
|
||||
cpu.gpr[2] = streamFlag;
|
||||
return true;
|
||||
default:
|
||||
// sceCdSC is intentionally extensible; unsupported controls are no-ops in cdvdman.
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
case 75: // sceCdMmode
|
||||
mediaMode = a0;
|
||||
cpu.gpr[2] = 1u;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<CompletionCallback> takeCompletionCallback() noexcept
|
||||
{
|
||||
std::optional<CompletionCallback> result = completionCallback;
|
||||
completionCallback.reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
int ensureInterruptEventFlag()
|
||||
{
|
||||
if (interruptEventFlagId == 0)
|
||||
{
|
||||
interruptEventFlagId = kernel.createInternalEventFlag(
|
||||
kEventFlagMulti, 0u, kCdvdInterruptReadyBits);
|
||||
}
|
||||
return interruptEventFlagId;
|
||||
}
|
||||
|
||||
void signalCommandComplete()
|
||||
{
|
||||
if (interruptEventFlagId != 0)
|
||||
(void)kernel.setInternalEventFlag(interruptEventFlagId, kCdvdInterruptReadyBits);
|
||||
}
|
||||
|
||||
void closeFiles()
|
||||
{
|
||||
if (imageHandle != 0u)
|
||||
host.closeHostFile(imageHandle);
|
||||
imageHandle = 0u;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.handle != 0u)
|
||||
host.closeHostFile(node.handle);
|
||||
node.handle = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool addDirectory(size_t parent, const std::filesystem::path &path)
|
||||
{
|
||||
std::error_code error;
|
||||
std::vector<std::filesystem::directory_entry> entries;
|
||||
for (
|
||||
std::filesystem::directory_iterator iterator(path, std::filesystem::directory_options::skip_permission_denied, error),
|
||||
end;
|
||||
!error && iterator != end;
|
||||
iterator.increment(error))
|
||||
{
|
||||
const std::filesystem::directory_entry &entry = *iterator;
|
||||
if (entry.is_symlink(error))
|
||||
{
|
||||
error.clear();
|
||||
continue;
|
||||
}
|
||||
error.clear();
|
||||
if (entry.is_directory(error) || entry.is_regular_file(error))
|
||||
entries.push_back(entry);
|
||||
error.clear();
|
||||
}
|
||||
|
||||
std::sort(entries.begin(), entries.end(),
|
||||
[](const auto &lhs, const auto &rhs)
|
||||
{
|
||||
return isoName(lhs.path(), lhs.is_directory()) < isoName(rhs.path(), rhs.is_directory());
|
||||
});
|
||||
|
||||
for (const auto &entry : entries)
|
||||
{
|
||||
error.clear();
|
||||
const bool directory = entry.is_directory(error);
|
||||
if (error)
|
||||
continue;
|
||||
IsoNode node;
|
||||
node.hostPath = entry.path();
|
||||
node.identifier = isoName(entry.path(), directory);
|
||||
node.parent = parent;
|
||||
node.directory = directory;
|
||||
if (!directory)
|
||||
{
|
||||
const uint64_t fileSize = entry.file_size(error);
|
||||
if (error)
|
||||
continue;
|
||||
node.size = static_cast<uint32_t>(std::min<uint64_t>(fileSize, std::numeric_limits<uint32_t>::max()));
|
||||
}
|
||||
const size_t index = nodes.size();
|
||||
nodes.push_back(std::move(node));
|
||||
nodes[parent].children.push_back(index);
|
||||
if (directory && !addDirectory(index, entry.path()))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool buildVirtualIso()
|
||||
{
|
||||
if (virtualIsoBuilt)
|
||||
return virtualIsoValid;
|
||||
virtualIsoBuilt = true;
|
||||
|
||||
const std::string rootValue = host.hostPath(HostPathKind::CdRoot);
|
||||
if (rootValue.empty())
|
||||
return false;
|
||||
const std::filesystem::path rootPath(rootValue);
|
||||
std::error_code error;
|
||||
if (!std::filesystem::is_directory(rootPath, error) || error)
|
||||
return false;
|
||||
|
||||
nodes.clear();
|
||||
IsoNode root;
|
||||
root.hostPath = rootPath;
|
||||
root.identifier.clear();
|
||||
root.parent = 0u;
|
||||
root.directory = true;
|
||||
nodes.push_back(std::move(root));
|
||||
if (!addDirectory(0u, rootPath))
|
||||
return false;
|
||||
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<size_t> identifierSizes = {1u, 1u};
|
||||
identifierSizes.reserve(node.children.size() + 2u);
|
||||
for (const size_t child : node.children)
|
||||
identifierSizes.push_back(nodes[child].identifier.size());
|
||||
node.size = directoryBytesFor(identifierSizes);
|
||||
node.sectors = node.size / kSectorSize;
|
||||
}
|
||||
|
||||
uint32_t cursor = kFirstDirectoryLsn;
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
cursor += node.sectors;
|
||||
}
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (node.directory)
|
||||
continue;
|
||||
node.lsn = cursor;
|
||||
node.sectors = alignSectors(node.size);
|
||||
cursor += node.sectors;
|
||||
}
|
||||
volumeSectors = std::max<uint32_t>(cursor, 32u);
|
||||
|
||||
std::array<uint8_t, kSectorSize> primary{};
|
||||
primary[0] = 1u;
|
||||
std::memcpy(primary.data() + 1u, "CD001", 5u);
|
||||
primary[6] = 1u;
|
||||
std::memset(primary.data() + 8u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 8u, "PS2XRECOMP", 10u);
|
||||
std::memset(primary.data() + 40u, ' ', 32u);
|
||||
std::memcpy(primary.data() + 40u, "PS2X VIRTUAL DISC", 17u);
|
||||
writeBoth32(primary.data() + 80u, volumeSectors);
|
||||
writeBoth16(primary.data() + 120u, 1u);
|
||||
writeBoth16(primary.data() + 124u, 1u);
|
||||
writeBoth16(primary.data() + 128u, static_cast<uint16_t>(kSectorSize));
|
||||
const uint8_t rootIdentifier = 0u;
|
||||
(void)writeDirectoryRecord(primary.data() + 156u, nodes[0].lsn, nodes[0].size, true, &rootIdentifier, 1u);
|
||||
primary[881] = 1u;
|
||||
metadataSectors[kPrimaryVolumeDescriptorLsn] = primary;
|
||||
|
||||
std::array<uint8_t, kSectorSize> terminator{};
|
||||
terminator[0] = 255u;
|
||||
std::memcpy(terminator.data() + 1u, "CD001", 5u);
|
||||
terminator[6] = 1u;
|
||||
metadataSectors[kVolumeDescriptorTerminatorLsn] = terminator;
|
||||
|
||||
for (size_t nodeIndex = 0u; nodeIndex < nodes.size(); ++nodeIndex)
|
||||
{
|
||||
const IsoNode &node = nodes[nodeIndex];
|
||||
if (!node.directory)
|
||||
continue;
|
||||
std::vector<uint8_t> bytes(node.size, 0u);
|
||||
size_t offset = 0u;
|
||||
const auto appendRecord = [&](const IsoNode &entry, const uint8_t *identifier, size_t identifierSize)
|
||||
{
|
||||
const size_t recordSize = directoryRecordSize(identifierSize);
|
||||
const size_t sectorOffset = offset % kSectorSize;
|
||||
if (sectorOffset + recordSize > kSectorSize)
|
||||
offset += kSectorSize - sectorOffset;
|
||||
offset += writeDirectoryRecord(bytes.data() + offset,
|
||||
entry.lsn,
|
||||
entry.size,
|
||||
entry.directory,
|
||||
identifier,
|
||||
identifierSize);
|
||||
};
|
||||
const uint8_t selfIdentifier = 0u;
|
||||
const uint8_t parentIdentifier = 1u;
|
||||
appendRecord(node, &selfIdentifier, 1u);
|
||||
appendRecord(nodes[node.parent], &parentIdentifier, 1u);
|
||||
for (const size_t childIndex : node.children)
|
||||
{
|
||||
const IsoNode &child = nodes[childIndex];
|
||||
appendRecord(child, reinterpret_cast<const uint8_t *>(child.identifier.data()), child.identifier.size());
|
||||
}
|
||||
for (uint32_t sector = 0u; sector < node.sectors; ++sector)
|
||||
{
|
||||
std::array<uint8_t, kSectorSize> contents{};
|
||||
std::memcpy(contents.data(), bytes.data() + sector * kSectorSize, kSectorSize);
|
||||
metadataSectors[node.lsn + sector] = contents;
|
||||
}
|
||||
}
|
||||
|
||||
virtualIsoValid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *findVirtualIsoNode(std::string_view guestPath)
|
||||
{
|
||||
if (!buildVirtualIso())
|
||||
return nullptr;
|
||||
|
||||
const ParsedPs2Path parsed = parsePs2Path(guestPath);
|
||||
if (!parsed || parsed.device != Ps2PathDevice::Cdrom)
|
||||
return nullptr;
|
||||
|
||||
size_t current = 0u;
|
||||
size_t begin = 0u;
|
||||
while (begin <= parsed.path.size())
|
||||
{
|
||||
const size_t end = parsed.path.find('/', begin);
|
||||
const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin;
|
||||
const std::string_view component(parsed.path.data() + begin, length);
|
||||
begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u;
|
||||
|
||||
if (component.empty() || component == ".")
|
||||
continue;
|
||||
if (component == "..")
|
||||
return nullptr;
|
||||
|
||||
const std::string wanted = normalizedIsoComponent(component);
|
||||
const auto child = std::find_if(nodes[current].children.begin(), nodes[current].children.end(),
|
||||
[&](size_t childIndex)
|
||||
{
|
||||
return normalizedIsoComponent(nodes[childIndex].identifier) == wanted;
|
||||
});
|
||||
if (child == nodes[current].children.end())
|
||||
return nullptr;
|
||||
current = *child;
|
||||
}
|
||||
return &nodes[current];
|
||||
}
|
||||
|
||||
bool searchFile(uint32_t resultAddress, uint32_t nameAddress)
|
||||
{
|
||||
if (resultAddress == 0u || nameAddress == 0u)
|
||||
return false;
|
||||
|
||||
const std::string guestPath = memory.readString(nameAddress, 1024u);
|
||||
IsoNode *node = findVirtualIsoNode(guestPath);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
// sceCdlFILE: lsn, size, name[16], date/flags[8].
|
||||
std::array<uint8_t, 32u> result{};
|
||||
writeLe32(result.data(), node->lsn);
|
||||
writeLe32(result.data() + 4u, node->size);
|
||||
const std::string leaf = normalizedIsoComponent(node->identifier);
|
||||
std::memcpy(result.data() + 8u, leaf.data(), std::min<size_t>(16u, leaf.size()));
|
||||
result[24u] = node->directory ? 2u : 0u;
|
||||
return memory.writeRam(resultAddress, result.data(), result.size());
|
||||
}
|
||||
|
||||
IsoNode *fileForSector(uint32_t lsn)
|
||||
{
|
||||
for (IsoNode &node : nodes)
|
||||
{
|
||||
if (!node.directory && lsn >= node.lsn && lsn < node.lsn + node.sectors)
|
||||
return &node;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool readVirtualSector(uint32_t lsn, uint8_t *destination)
|
||||
{
|
||||
const auto metadata = metadataSectors.find(lsn);
|
||||
if (metadata != metadataSectors.end())
|
||||
{
|
||||
std::memcpy(destination, metadata->second.data(), kSectorSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
IsoNode *node = fileForSector(lsn);
|
||||
if (!node)
|
||||
{
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
return lsn < volumeSectors;
|
||||
}
|
||||
if (node->handle == 0u)
|
||||
node->handle = host.openHostFile(node->hostPath.string());
|
||||
if (node->handle == 0u)
|
||||
return false;
|
||||
|
||||
std::memset(destination, 0, kSectorSize);
|
||||
const uint64_t offset = static_cast<uint64_t>(lsn - node->lsn) * kSectorSize;
|
||||
const size_t wanted = static_cast<size_t>(std::min<uint64_t>(kSectorSize, static_cast<uint64_t>(node->size) - offset));
|
||||
size_t bytesRead = 0u;
|
||||
return host.readHostFile(node->handle, offset, destination, wanted, bytesRead) && bytesRead == wanted;
|
||||
}
|
||||
|
||||
bool readSectors(uint32_t lsn, uint32_t sectors, uint32_t destination)
|
||||
{
|
||||
if (sectors == 0u)
|
||||
{
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
const uint64_t byteCount64 = static_cast<uint64_t>(sectors) * kSectorSize;
|
||||
if (byteCount64 > IopMemory::RamSize || !memory.ownsRamRange(destination, static_cast<size_t>(byteCount64)))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
const size_t byteCount = static_cast<size_t>(byteCount64);
|
||||
std::vector<uint8_t> bytes(byteCount, 0u);
|
||||
|
||||
bool read = false;
|
||||
const std::string imagePath = host.hostPath(HostPathKind::CdImage);
|
||||
if (!imagePath.empty())
|
||||
{
|
||||
if (imageHandle == 0u)
|
||||
imageHandle = host.openHostFile(imagePath);
|
||||
if (imageHandle != 0u)
|
||||
{
|
||||
size_t bytesRead = 0u;
|
||||
read = host.readHostFile(imageHandle,
|
||||
static_cast<uint64_t>(lsn) * kSectorSize,
|
||||
bytes.data(),
|
||||
byteCount,
|
||||
bytesRead) &&
|
||||
bytesRead == byteCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (!read && buildVirtualIso())
|
||||
{
|
||||
read = true;
|
||||
for (uint32_t sector = 0u; sector < sectors; ++sector)
|
||||
{
|
||||
if (!readVirtualSector(lsn + sector, bytes.data() + static_cast<size_t>(sector) * kSectorSize))
|
||||
{
|
||||
read = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!read || !memory.writeRam(destination, bytes.data(), bytes.size()))
|
||||
{
|
||||
lastError = kCdvdErrorRead;
|
||||
return false;
|
||||
}
|
||||
lastError = kCdvdErrorNone;
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
IopMemory &memory;
|
||||
IopKernel &kernel;
|
||||
Callback callback;
|
||||
std::optional<CompletionCallback> completionCallback;
|
||||
bool initialized = false;
|
||||
uint32_t mediaMode = 0u;
|
||||
uint32_t currentLsn = 0u;
|
||||
uint32_t lastError = kCdvdErrorNone;
|
||||
uint32_t streamFlag = 0u;
|
||||
uint32_t lastReadTimeout = 0u;
|
||||
int interruptEventFlagId = 0;
|
||||
uint64_t imageHandle = 0u;
|
||||
bool virtualIsoBuilt = false;
|
||||
bool virtualIsoValid = false;
|
||||
uint32_t volumeSectors = 0u;
|
||||
std::vector<IsoNode> nodes;
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, kSectorSize>> metadataSectors;
|
||||
};
|
||||
|
||||
IopCdvd::IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel)
|
||||
: m_impl(std::make_unique<Impl>(host, memory, kernel))
|
||||
{
|
||||
}
|
||||
|
||||
IopCdvd::~IopCdvd() = default;
|
||||
|
||||
void IopCdvd::reset() noexcept
|
||||
{
|
||||
m_impl->reset();
|
||||
}
|
||||
|
||||
bool IopCdvd::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
return m_impl->dispatchImport(ordinal, cpu);
|
||||
}
|
||||
|
||||
std::optional<IopCdvd::CompletionCallback> IopCdvd::takeCompletionCallback() noexcept
|
||||
{
|
||||
return m_impl->takeCompletionCallback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
class IopMemory;
|
||||
|
||||
class IopCdvd
|
||||
{
|
||||
public:
|
||||
struct CompletionCallback
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
uint32_t reason = 0u;
|
||||
};
|
||||
|
||||
IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel);
|
||||
~IopCdvd();
|
||||
|
||||
IopCdvd(const IopCdvd &) = delete;
|
||||
IopCdvd &operator=(const IopCdvd &) = delete;
|
||||
|
||||
void reset() noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] std::optional<CompletionCallback> takeCompletionCallback() noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "iop_heaplib.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopHeaplib::IopHeaplib(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopHeaplib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // CreateHeap
|
||||
setV0(m_memory.allocate(16u, 16u));
|
||||
return true;
|
||||
case 5: // DeleteHeap
|
||||
if (a0 != 0u)
|
||||
(void)m_memory.freeAllocation(a0);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6:
|
||||
setV0(m_memory.allocate(a1, 16u));
|
||||
return true;
|
||||
case 7:
|
||||
setV0(m_memory.freeAllocation(a1) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 8:
|
||||
setV0(m_memory.maxFreeMemory());
|
||||
return true;
|
||||
case 11:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 15:
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->size);
|
||||
else
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopHeaplib
|
||||
{
|
||||
public:
|
||||
explicit IopHeaplib(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "iop_imports.h"
|
||||
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kImportMagic = 0x41E00000u;
|
||||
constexpr uint32_t kExportMagic = 0x41C00000u;
|
||||
|
||||
bool equalsIgnoreCase(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(rhs[i])))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string trimLibraryName(const char *name)
|
||||
{
|
||||
size_t length = 0u;
|
||||
while (length < 8u && name[length] != '\0')
|
||||
++length;
|
||||
return std::string(name, length);
|
||||
}
|
||||
}
|
||||
|
||||
IopImportRegistry::IopImportRegistry(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopImportRegistry::reset()
|
||||
{
|
||||
m_libraries.clear();
|
||||
}
|
||||
|
||||
std::optional<IopImportCall> IopImportRegistry::decode(uint32_t pc) const
|
||||
{
|
||||
return decode(pc, m_memory.read32(pc));
|
||||
}
|
||||
|
||||
std::optional<IopImportCall> IopImportRegistry::decodeStub(uint32_t pc) const
|
||||
{
|
||||
const uint32_t delay = m_memory.read32(pc + 4u);
|
||||
if ((delay & 0xFFFF0000u) != 0x24000000u)
|
||||
return std::nullopt;
|
||||
|
||||
const uint32_t physicalPc = IopMemory::physicalAddress(pc);
|
||||
const uint32_t searchBegin = physicalPc > 0x10000u ? physicalPc - 0x10000u : 0u;
|
||||
for (uint32_t candidate = physicalPc & ~3u; candidate >= searchBegin + 20u; candidate -= 4u)
|
||||
{
|
||||
const uint32_t table = candidate - 20u;
|
||||
if (m_memory.read32(table) != kImportMagic)
|
||||
{
|
||||
if (candidate == searchBegin + 20u)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(table + 12u + i));
|
||||
const uint32_t stubs = table + 20u;
|
||||
if (physicalPc < stubs || ((physicalPc - stubs) & 7u) != 0u)
|
||||
continue;
|
||||
|
||||
bool valid = false;
|
||||
for (uint32_t stub = stubs;
|
||||
stub + 7u < IopMemory::RamSize && stub <= physicalPc;
|
||||
stub += 8u)
|
||||
{
|
||||
const uint32_t first = m_memory.read32(stub);
|
||||
const uint32_t second = m_memory.read32(stub + 4u);
|
||||
if (first == 0u && second == 0u)
|
||||
break;
|
||||
if (stub == physicalPc)
|
||||
{
|
||||
valid = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (valid)
|
||||
{
|
||||
return IopImportCall{
|
||||
trimLibraryName(name),
|
||||
static_cast<uint16_t>(delay & 0xFFFFu),
|
||||
m_memory.read16(table + 8u),
|
||||
};
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::registerExportTable(uint32_t address)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
if (physical + 20u > IopMemory::RamSize ||
|
||||
m_memory.read32(physical) != kExportMagic)
|
||||
return false;
|
||||
|
||||
char name[9]{};
|
||||
for (uint32_t i = 0; i < 8u; ++i)
|
||||
name[i] = static_cast<char>(m_memory.read8(physical + 12u + i));
|
||||
|
||||
ExportLibrary library;
|
||||
library.tableAddress = physical;
|
||||
library.version = m_memory.read16(physical + 8u);
|
||||
library.name = trimLibraryName(name);
|
||||
for (uint32_t cursor = physical + 20u; cursor + 3u < IopMemory::RamSize; cursor += 4u)
|
||||
{
|
||||
const uint32_t function = m_memory.read32(cursor);
|
||||
if (function == 0u)
|
||||
break;
|
||||
library.functions.push_back(function);
|
||||
if (library.functions.size() > 1024u)
|
||||
return false;
|
||||
}
|
||||
m_libraries[physical] = std::move(library);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IopImportRegistry::releaseExportTable(uint32_t address)
|
||||
{
|
||||
return m_libraries.erase(IopMemory::physicalAddress(address)) != 0u;
|
||||
}
|
||||
|
||||
const IopImportRegistry::ExportLibrary *IopImportRegistry::findLibrary(std::string_view name, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *selected = nullptr;
|
||||
for (const auto &[address, library] : m_libraries)
|
||||
{
|
||||
(void)address;
|
||||
if (!equalsIgnoreCase(library.name, name) ||
|
||||
(version && (library.version >> 8u) != (*version >> 8u)))
|
||||
continue;
|
||||
// LOADCORE links by major version; a newer minor supersedes older exports.
|
||||
if (!selected || library.version > selected->version)
|
||||
selected = &library;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::findTable(std::string_view library, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
return found ? found->tableAddress : 0u;
|
||||
}
|
||||
|
||||
uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version) const
|
||||
{
|
||||
const ExportLibrary *found = findLibrary(library, version);
|
||||
if (!found || ordinal >= found->functions.size())
|
||||
return 0u;
|
||||
return found->functions[ordinal];
|
||||
}
|
||||
|
||||
int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode)
|
||||
{
|
||||
constexpr int32_t kLibraryNotFound = -213;
|
||||
constexpr int32_t kIllegalLibrary = -214;
|
||||
|
||||
if (address == 0u)
|
||||
return kIllegalLibrary;
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
if (physical + 12u > IopMemory::RamSize)
|
||||
return kLibraryNotFound;
|
||||
|
||||
const bool registered = m_libraries.find(physical) != m_libraries.end();
|
||||
if (!registered && m_memory.read32(physical) != kExportMagic)
|
||||
return kLibraryNotFound;
|
||||
|
||||
const uint16_t oldMode = m_memory.read16(physical + 10u);
|
||||
m_memory.write16(physical + 10u, static_cast<uint16_t>((oldMode & ~6u) | (mode & 6u)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
void IopImportRegistry::eraseRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto library = m_libraries.begin(); library != m_libraries.end();)
|
||||
{
|
||||
if (library->first >= base && library->first < base + size)
|
||||
library = m_libraries.erase(library);
|
||||
else
|
||||
++library;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
struct IopImportCall
|
||||
{
|
||||
std::string library;
|
||||
uint16_t ordinal = 0;
|
||||
uint16_t version = 0;
|
||||
};
|
||||
|
||||
class IopImportRegistry
|
||||
{
|
||||
public:
|
||||
explicit IopImportRegistry(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc) const;
|
||||
[[nodiscard]] std::optional<IopImportCall> decode(uint32_t pc, uint32_t instruction) const
|
||||
{
|
||||
if (instruction != 0x03E00008u) // Import stubs begin with jr ra.
|
||||
return std::nullopt;
|
||||
return decodeStub(pc);
|
||||
}
|
||||
[[nodiscard]] bool registerExportTable(uint32_t address);
|
||||
[[nodiscard]] bool releaseExportTable(uint32_t address);
|
||||
[[nodiscard]] uint32_t findTable(std::string_view library, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal, std::optional<uint16_t> version = std::nullopt) const;
|
||||
[[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode);
|
||||
void eraseRange(uint32_t base, uint32_t size);
|
||||
|
||||
private:
|
||||
[[nodiscard]] std::optional<IopImportCall> decodeStub(uint32_t pc) const;
|
||||
struct ExportLibrary
|
||||
{
|
||||
uint32_t tableAddress = 0;
|
||||
uint16_t version = 0;
|
||||
std::string name;
|
||||
std::vector<uint32_t> functions;
|
||||
};
|
||||
|
||||
[[nodiscard]] const ExportLibrary *findLibrary(std::string_view name, std::optional<uint16_t> version) const;
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<uint32_t, ExportLibrary> m_libraries;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "iop_intrman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopIntrman::IopIntrman(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopIntrman::reset()
|
||||
{
|
||||
m_handlers.clear();
|
||||
m_enabled.clear();
|
||||
}
|
||||
|
||||
bool IopIntrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const uint32_t a3 = cpu.gpr[7];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 4: // RegisterIntrHandler
|
||||
m_handlers[static_cast<int>(a0)] = {a2, a3, cpu.gpr[28]};
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 5: // ReleaseIntrHandler
|
||||
m_handlers.erase(static_cast<int>(a0));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6: // EnableIntr
|
||||
m_enabled[static_cast<int>(a0)] = true;
|
||||
if (a0 < 32u)
|
||||
m_memory.setInterruptMask(m_memory.interruptMask() | (1u << a0));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 7: // DisableIntr
|
||||
if (a1 != 0u)
|
||||
m_memory.write32(a1, a0);
|
||||
if (a0 < 32u)
|
||||
m_memory.setInterruptMask(m_memory.interruptMask() & ~(1u << a0));
|
||||
m_enabled[static_cast<int>(a0)] = false;
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 8: // CpuDisableIntr
|
||||
m_memory.setInterruptControl(0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 9: // CpuEnableIntr
|
||||
m_memory.setInterruptControl(1u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 14:
|
||||
setV0(a0 != 0u
|
||||
? executor.executeGuestFunctionWithBudget(a0, a1, a2, a3, 0u, cpu.gpr[28], 100000u)
|
||||
: 0u);
|
||||
return true;
|
||||
case 15:
|
||||
case 16:
|
||||
case 23:
|
||||
case 24:
|
||||
case 25:
|
||||
case 28:
|
||||
case 30:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 17:
|
||||
if (a0 != 0u)
|
||||
m_memory.write32(a0, m_memory.interruptControl());
|
||||
m_memory.setInterruptControl(0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 18:
|
||||
m_memory.setInterruptControl(a0 != 0u ? 1u : 0u);
|
||||
setV0(0u);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopIntrman::dispatchInterrupt(int irq, IopGuestExecutor &executor) const
|
||||
{
|
||||
const auto enabled = m_enabled.find(irq);
|
||||
if (enabled == m_enabled.end() || !enabled->second)
|
||||
return false;
|
||||
const auto handler = m_handlers.find(irq);
|
||||
if (handler == m_handlers.end() || handler->second.function == 0u)
|
||||
return false;
|
||||
|
||||
(void)executor.executeGuestFunctionWithBudget(handler->second.function,
|
||||
handler->second.argument,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
handler->second.gp,
|
||||
100000u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
class IopMemory;
|
||||
|
||||
class IopIntrman
|
||||
{
|
||||
public:
|
||||
explicit IopIntrman(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
|
||||
[[nodiscard]] bool dispatchInterrupt(int irq, IopGuestExecutor &executor) const;
|
||||
|
||||
private:
|
||||
struct Handler
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t argument = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::map<int, Handler> m_handlers;
|
||||
std::map<int, bool> m_enabled;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "iop_ioman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopIoman::IopIoman(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
void IopIoman::reset()
|
||||
{
|
||||
m_devices.clear();
|
||||
}
|
||||
|
||||
bool IopIoman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor)
|
||||
{
|
||||
constexpr size_t kMaxDevices = 16u;
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 20: // AddDrv
|
||||
{
|
||||
if (a0 == 0u || m_devices.size() >= kMaxDevices)
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t nameAddress = m_memory.read32(a0);
|
||||
const uint32_t operations = m_memory.read32(a0 + 16u);
|
||||
const std::string name = m_memory.readString(nameAddress, 64u);
|
||||
if (nameAddress == 0u || operations == 0u || name.empty())
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
m_devices.push_back({a0, cpu.gpr[28], name});
|
||||
const uint32_t init = m_memory.read32(operations);
|
||||
if (init != 0u)
|
||||
{
|
||||
const int32_t result = static_cast<int32_t>(
|
||||
executor.executeGuestFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28]));
|
||||
if (result < 0)
|
||||
{
|
||||
m_devices.pop_back();
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 21: // DelDrv
|
||||
{
|
||||
const std::string name = m_memory.readString(a0, 64u);
|
||||
const auto device = std::find_if(
|
||||
m_devices.begin(), m_devices.end(),
|
||||
[&](const Device &candidate)
|
||||
{ return candidate.name == name; });
|
||||
if (device == m_devices.end())
|
||||
{
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t operations = m_memory.read32(device->address + 16u);
|
||||
const uint32_t deinit = operations != 0u ? m_memory.read32(operations + 4u) : 0u;
|
||||
if (deinit != 0u)
|
||||
(void)executor.executeGuestFunction(deinit, device->address, 0u, 0u, 0u, device->gp);
|
||||
m_devices.erase(device);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
class IopMemory;
|
||||
|
||||
class IopIoman
|
||||
{
|
||||
public:
|
||||
explicit IopIoman(IopMemory &memory) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor);
|
||||
|
||||
private:
|
||||
struct Device
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint32_t gp = 0u;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
IopMemory &m_memory;
|
||||
std::vector<Device> m_devices;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "iop_loadcore.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "iop_imports.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopLoadcore::IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept
|
||||
: m_memory(memory), m_imports(imports)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopLoadcore::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 8:
|
||||
case 9:
|
||||
case 12:
|
||||
case 13:
|
||||
case 14:
|
||||
case 15:
|
||||
case 16:
|
||||
case 17:
|
||||
case 20:
|
||||
case 21:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 6:
|
||||
case 10:
|
||||
setV0(m_imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 7:
|
||||
setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 11: // QueryLibraryEntryTable returns the function array, not the export header.
|
||||
{
|
||||
const uint32_t address = IopMemory::physicalAddress(a0);
|
||||
if (a0 == 0u || address > IopMemory::RamSize - 20u)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
const uint32_t table = m_imports.findTable(m_memory.readString(address + 12u, 8u), m_memory.read16(address + 8u));
|
||||
setV0(table != 0u ? table + 20u : 0u);
|
||||
return true;
|
||||
}
|
||||
case 27: // SetRebootTimeLibraryHandlingMode
|
||||
setV0(static_cast<uint32_t>(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5])));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopImportRegistry;
|
||||
class IopMemory;
|
||||
|
||||
class IopLoadcore
|
||||
{
|
||||
public:
|
||||
IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
IopImportRegistry &m_imports;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#include "iop_stdio.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopStdio::IopStdio(IopHost &host, IopMemory &memory) noexcept
|
||||
: m_host(host), m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopStdio::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
const auto logString = [&](std::string_view prefix, uint32_t address, uint32_t resultBias = 0u)
|
||||
{
|
||||
const std::string text = m_memory.readString(address, 2048u);
|
||||
m_host.log(LogLevel::Info, std::string(prefix) + text);
|
||||
setV0(static_cast<uint32_t>(text.size()) + resultBias);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // printf
|
||||
logString("[IOP printf] ", a0);
|
||||
return true;
|
||||
case 5: // getchar
|
||||
case 10:
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 6: // putchar
|
||||
m_host.log(LogLevel::Info, std::string("[IOP putchar] ") + static_cast<char>(a0 & 0xFFu));
|
||||
setV0(a0 & 0xFFu);
|
||||
return true;
|
||||
case 7: // puts
|
||||
logString("[IOP puts] ", a0, 1u);
|
||||
return true;
|
||||
case 8: // gets
|
||||
case 13:
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 9: // fdprintf
|
||||
logString("[IOP fdprintf] ", a1);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(a0 & 0xFFu);
|
||||
return true;
|
||||
case 12: // fdputs
|
||||
logString("[IOP fdputs] ", a0);
|
||||
return true;
|
||||
case 14: // vfdprintf
|
||||
logString("[IOP vfdprintf] ", a1);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopStdio
|
||||
{
|
||||
public:
|
||||
IopStdio(IopHost &host, IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
#include "iop_sysclib.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopSysclib::IopSysclib(IopMemory &memory) noexcept
|
||||
: m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopSysclib::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
const auto compare = [&](uint32_t lhs, uint32_t rhs, uint32_t count) -> int32_t
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(lhs + i);
|
||||
const uint8_t right = m_memory.read8(rhs + i);
|
||||
if (left != right)
|
||||
return static_cast<int32_t>(left) - static_cast<int32_t>(right);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const auto copy = [&](uint32_t destination, uint32_t source, uint32_t count)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
m_memory.write8(destination + i, m_memory.read8(source + i));
|
||||
};
|
||||
const auto appendString = [&](uint32_t destination, uint32_t source, std::optional<uint32_t> maxAppend = std::nullopt)
|
||||
{
|
||||
uint32_t destinationOffset = 0;
|
||||
while (m_memory.read8(destination + destinationOffset) != 0u && destinationOffset < (1u << 20))
|
||||
++destinationOffset;
|
||||
|
||||
uint32_t sourceOffset = 0;
|
||||
while (sourceOffset < (1u << 20) && (!maxAppend || sourceOffset < *maxAppend))
|
||||
{
|
||||
const uint8_t character = m_memory.read8(source + sourceOffset);
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, character);
|
||||
++sourceOffset;
|
||||
if (character == 0u)
|
||||
return;
|
||||
}
|
||||
m_memory.write8(destination + destinationOffset + sourceOffset, 0u);
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // setjmp - enough for callers which only test the initial return.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 5: // longjmp, TODO bc w can do it without the BIOS jmp_buf ABI.
|
||||
setV0(a1 == 0u ? 1u : a1);
|
||||
return true;
|
||||
case 6:
|
||||
setV0(static_cast<uint32_t>(std::toupper(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 7:
|
||||
setV0(static_cast<uint32_t>(std::tolower(static_cast<unsigned char>(a0))));
|
||||
return true;
|
||||
case 8:
|
||||
case 9: // ctype table is optional for most IRXs.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 10: // memchr
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
if (m_memory.read8(a0 + i) == static_cast<uint8_t>(a1))
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 11:
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 12:
|
||||
copy(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 13:
|
||||
{
|
||||
std::vector<uint8_t> temporary(a2);
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
temporary[i] = m_memory.read8(a1 + i);
|
||||
(void)m_memory.writeRam(a0, temporary.data(), temporary.size());
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 14:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write8(a0 + i, static_cast<uint8_t>(a1));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 15: // bcmp
|
||||
setV0(static_cast<uint32_t>(compare(a0, a1, a2)));
|
||||
return true;
|
||||
case 16: // bcopy(src,dst,n)
|
||||
copy(a1, a0, a2);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 17:
|
||||
for (uint32_t i = 0; i < a1; ++i)
|
||||
m_memory.write8(a0 + i, 0u);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 18: // prnt
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // sprintf: preserve useful literal formats even before full vararg formatting.
|
||||
case 42: // vsprintf fallback: copy format literal.
|
||||
{
|
||||
const std::string format = m_memory.readString(a1, 4096u);
|
||||
for (size_t i = 0; i <= format.size(); ++i)
|
||||
{
|
||||
m_memory.write8(a0 + static_cast<uint32_t>(i), i < format.size() ? static_cast<uint8_t>(format[i]) : 0u);
|
||||
}
|
||||
setV0(static_cast<uint32_t>(format.size()));
|
||||
return true;
|
||||
}
|
||||
case 20:
|
||||
appendString(a0, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 21: // strchr
|
||||
case 25: // index
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 22: // strcmp
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23: // strcpy
|
||||
{
|
||||
uint32_t i = 0;
|
||||
for (;; ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a1 + i);
|
||||
m_memory.write8(a0 + i, character);
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 24: // strcspn
|
||||
{
|
||||
const std::string reject = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || reject.find(character) != std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 26: // rindex
|
||||
case 32: // strrchr
|
||||
{
|
||||
const uint8_t needle = static_cast<uint8_t>(a1);
|
||||
uint32_t found = 0u;
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const uint8_t character = m_memory.read8(a0 + i);
|
||||
if (character == needle)
|
||||
found = a0 + i;
|
||||
if (character == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(found);
|
||||
return true;
|
||||
}
|
||||
case 27:
|
||||
setV0(static_cast<uint32_t>(m_memory.readString(a0, 1u << 20).size()));
|
||||
return true;
|
||||
case 28:
|
||||
appendString(a0, a1, a2);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 29: // strncmp
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t left = m_memory.read8(a0 + i);
|
||||
const uint8_t right = m_memory.read8(a1 + i);
|
||||
if (left != right)
|
||||
{
|
||||
setV0(static_cast<uint32_t>(static_cast<int32_t>(left) - static_cast<int32_t>(right)));
|
||||
return true;
|
||||
}
|
||||
if (left == 0u)
|
||||
break;
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
case 30: // strncpy
|
||||
{
|
||||
bool ended = false;
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
{
|
||||
const uint8_t character = ended ? 0u : m_memory.read8(a1 + i);
|
||||
if (character == 0u)
|
||||
ended = true;
|
||||
m_memory.write8(a0 + i, character);
|
||||
}
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
case 31: // strpbrk
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
for (uint32_t i = 0; i < (1u << 20); ++i)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + i));
|
||||
if (character == 0)
|
||||
break;
|
||||
if (accept.find(character) != std::string::npos)
|
||||
{
|
||||
setV0(a0 + i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 33: // strspn
|
||||
{
|
||||
const std::string accept = m_memory.readString(a1, 4096u);
|
||||
uint32_t count = 0;
|
||||
for (; count < (1u << 20); ++count)
|
||||
{
|
||||
const char character = static_cast<char>(m_memory.read8(a0 + count));
|
||||
if (character == 0 || accept.find(character) == std::string::npos)
|
||||
break;
|
||||
}
|
||||
setV0(count);
|
||||
return true;
|
||||
}
|
||||
case 34: // strstr
|
||||
{
|
||||
const std::string needle = m_memory.readString(a1, 4096u);
|
||||
if (needle.empty())
|
||||
{
|
||||
setV0(a0);
|
||||
return true;
|
||||
}
|
||||
const std::string haystack = m_memory.readString(a0, 1u << 20);
|
||||
const size_t position = haystack.find(needle);
|
||||
setV0(position == std::string::npos
|
||||
? 0u
|
||||
: a0 + static_cast<uint32_t>(position));
|
||||
return true;
|
||||
}
|
||||
case 35: // strtok state is intentionally not shared across modules yet.
|
||||
setV0(0);
|
||||
return true;
|
||||
case 36:
|
||||
case 38: // strtol / strtoul
|
||||
{
|
||||
const std::string value = m_memory.readString(a0, 4096u);
|
||||
char *end = nullptr;
|
||||
const int base = static_cast<int>(a2);
|
||||
const unsigned long parsed = ordinal == 36
|
||||
? static_cast<unsigned long>(std::strtol(value.c_str(), &end, base))
|
||||
: std::strtoul(value.c_str(), &end, base);
|
||||
if (a1 != 0u)
|
||||
{
|
||||
m_memory.write32(a1, a0 + static_cast<uint32_t>(end - value.c_str()));
|
||||
}
|
||||
setV0(static_cast<uint32_t>(parsed));
|
||||
return true;
|
||||
}
|
||||
case 37: // atob
|
||||
setV0(0);
|
||||
return true;
|
||||
case 40: // _wmemcopy, count is 32-bit words
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, m_memory.read32(a1 + i * 4u));
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 41:
|
||||
for (uint32_t i = 0; i < a2; ++i)
|
||||
m_memory.write32(a0 + i * 4u, a1);
|
||||
setV0(a0);
|
||||
return true;
|
||||
case 43:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopSysclib
|
||||
{
|
||||
public:
|
||||
explicit IopSysclib(IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "iop_sysmem.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopSysmem::IopSysmem(IopHost &host, IopMemory &memory) noexcept
|
||||
: m_host(host), m_memory(memory)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopSysmem::dispatchImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // AllocSysMemory
|
||||
{
|
||||
const uint32_t address = a0 == 2u
|
||||
? m_memory.allocate(a1, 16u, a2)
|
||||
: m_memory.allocate(a1, 16u);
|
||||
setV0(address);
|
||||
return true;
|
||||
}
|
||||
case 5: // FreeSysMemory
|
||||
setV0(m_memory.freeAllocation(a0) ? 0u : 0xFFFFFFFFu);
|
||||
return true;
|
||||
case 6: // QueryMemSize
|
||||
setV0(IopMemory::RamSize);
|
||||
return true;
|
||||
case 7: // QueryMaxFreeMemSize
|
||||
case 8: // QueryTotalFreeMemSize
|
||||
setV0(m_memory.maxFreeMemory());
|
||||
return true;
|
||||
case 9: // QueryBlockTopAddress
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->address);
|
||||
else
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 10: // QueryBlockSize
|
||||
if (const auto block = m_memory.allocationContaining(a0))
|
||||
setV0(block->size);
|
||||
else
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 14: // Kprintf
|
||||
{
|
||||
const std::string format = m_memory.readString(a0, 512u);
|
||||
m_host.log(LogLevel::Info, std::string("[IOP Kprintf] ") + format);
|
||||
setV0(static_cast<uint32_t>(format.size()));
|
||||
return true;
|
||||
}
|
||||
case 15:
|
||||
setV0(0u);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopMemory;
|
||||
|
||||
class IopSysmem
|
||||
{
|
||||
public:
|
||||
IopSysmem(IopHost &host, IopMemory &memory) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
|
||||
private:
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
#include "iop_timrman.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../services/iop_rpc.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <limits>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr int32_t kNoTimer = -150;
|
||||
constexpr int32_t kIllegalTimerId = -151;
|
||||
constexpr int32_t kIllegalSource = -152;
|
||||
constexpr int32_t kIllegalPrescale = -153;
|
||||
constexpr int32_t kTimerBusy = -154;
|
||||
constexpr int32_t kTimerNotConfigured = -155;
|
||||
constexpr int32_t kTimerNotRunning = -156;
|
||||
constexpr int32_t kIllegalMode = -405;
|
||||
constexpr uint64_t kIopClockHz = 36'864'000ull;
|
||||
constexpr uint64_t kPixelClockHz = 13'500'000ull;
|
||||
constexpr uint64_t kHlineClockHz = 15'734ull;
|
||||
|
||||
constexpr std::array<size_t, 6> kAllocationOrder{2u, 5u, 4u, 3u, 0u, 1u};
|
||||
constexpr std::array<uint32_t, 6> kAddresses{
|
||||
0xBF801100u,
|
||||
0xBF801110u,
|
||||
0xBF801120u,
|
||||
0xBF801480u,
|
||||
0xBF801490u,
|
||||
0xBF8014A0u,
|
||||
};
|
||||
constexpr std::array<uint8_t, 6> kSources{0x0Bu, 0x0Du, 0x01u, 0x05u, 0x01u, 0x01u};
|
||||
constexpr std::array<uint8_t, 6> kWidths{16u, 16u, 16u, 32u, 32u, 32u};
|
||||
constexpr std::array<uint16_t, 6> kMaxPrescales{1u, 1u, 8u, 1u, 256u, 256u};
|
||||
constexpr std::array<uint8_t, 6> kIrqs{4u, 5u, 6u, 14u, 15u, 16u};
|
||||
|
||||
uint32_t errorValue(int32_t error) noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(error);
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::reset() noexcept
|
||||
{
|
||||
for (size_t i = 0u; i < m_timers.size(); ++i)
|
||||
{
|
||||
m_timers[i] = {};
|
||||
m_timers[i].address = kAddresses[i];
|
||||
m_timers[i].sources = kSources[i];
|
||||
m_timers[i].width = kWidths[i];
|
||||
m_timers[i].maxPrescale = kMaxPrescales[i];
|
||||
m_timers[i].irq = kIrqs[i];
|
||||
}
|
||||
m_holdMode = 0u;
|
||||
m_servicing = false;
|
||||
}
|
||||
|
||||
uint32_t IopTimrman::timerId(size_t index) noexcept
|
||||
{
|
||||
return (static_cast<uint32_t>(index + 1u) << 28u) | (kAddresses[index] >> 4u);
|
||||
}
|
||||
|
||||
IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) noexcept
|
||||
{
|
||||
const uint32_t encoded = id >> 28u;
|
||||
if (encoded == 0u || encoded > m_timers.size())
|
||||
return nullptr;
|
||||
Timer &timer = m_timers[encoded - 1u];
|
||||
return timer.users != 0u && (id & 0x0FFFFFFFu) == (timer.address >> 4u)
|
||||
? &timer
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
const IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) const noexcept
|
||||
{
|
||||
return const_cast<IopTimrman *>(this)->timerFromId(id);
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::ticksToCycles(const Timer &timer, uint64_t ticks) noexcept
|
||||
{
|
||||
const uint64_t prescale = std::max<uint64_t>(timer.prescale, 1u);
|
||||
const uint64_t sourceHz = timer.source == 2u
|
||||
? kPixelClockHz
|
||||
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
|
||||
if (ticks == 0u)
|
||||
ticks = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
const unsigned long long scaled = ticks * prescale;
|
||||
if (sourceHz == kIopClockHz)
|
||||
return std::max<uint64_t>(scaled, 1u);
|
||||
const uint64_t whole = (scaled / sourceHz) * kIopClockHz;
|
||||
const uint64_t remainder = scaled % sourceHz;
|
||||
return std::max<uint64_t>(1u, whole + (remainder * kIopClockHz + sourceHz - 1u) / sourceHz);
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
if (!timer.running || currentCycle <= timer.counterBaseCycle)
|
||||
return 0u;
|
||||
const uint64_t elapsed = currentCycle - timer.counterBaseCycle;
|
||||
const uint64_t sourceHz = timer.source == 2u
|
||||
? kPixelClockHz
|
||||
: (timer.source == 4u ? kHlineClockHz : kIopClockHz);
|
||||
return (elapsed * sourceHz) / (kIopClockHz * std::max<uint64_t>(timer.prescale, 1u));
|
||||
}
|
||||
|
||||
uint32_t IopTimrman::counterValue(const Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
const uint64_t value = static_cast<uint64_t>(timer.counterBase) + elapsedTicks(timer, currentCycle);
|
||||
return timer.width == 16u ? static_cast<uint32_t>(value & 0xFFFFu) : static_cast<uint32_t>(value);
|
||||
}
|
||||
|
||||
void IopTimrman::schedule(Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
if (!timer.running)
|
||||
return;
|
||||
|
||||
const uint64_t current = counterValue(timer, currentCycle);
|
||||
timer.counterBase = static_cast<uint32_t>(current);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
|
||||
if (timer.compareCallback.function != 0u)
|
||||
{
|
||||
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
const uint64_t compare = timer.width == 16u ? (timer.compare & 0xFFFFu) : timer.compare;
|
||||
uint64_t delta = (compare + modulus - current) % modulus;
|
||||
if (delta == 0u)
|
||||
delta = modulus;
|
||||
timer.compareCycle = currentCycle + ticksToCycles(timer, delta);
|
||||
}
|
||||
|
||||
if (timer.overflowCallback.function != 0u)
|
||||
{
|
||||
const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u);
|
||||
uint64_t delta = modulus - current;
|
||||
if (delta == 0u)
|
||||
delta = modulus;
|
||||
timer.overflowCycle = currentCycle + ticksToCycles(timer, delta);
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::stop(Timer &timer, uint64_t currentCycle) noexcept
|
||||
{
|
||||
timer.counterBase = counterValue(timer, currentCycle);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
timer.running = false;
|
||||
timer.liveMode = 0u;
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
}
|
||||
|
||||
bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
const uint32_t a1 = cpu.gpr[5];
|
||||
const uint32_t a2 = cpu.gpr[6];
|
||||
const uint32_t a3 = cpu.gpr[7];
|
||||
const auto setV0 = [&](uint32_t value) { cpu.gpr[2] = value; };
|
||||
|
||||
switch (ordinal)
|
||||
{
|
||||
case 3: // GetTimersTable
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 4: // AllocHardTimer
|
||||
for (const size_t index : kAllocationOrder)
|
||||
{
|
||||
Timer &timer = m_timers[index];
|
||||
if (timer.users != 0u || (timer.sources & a0) == 0u || timer.width != a1 || timer.maxPrescale < a2)
|
||||
continue;
|
||||
timer.users = 1u;
|
||||
timer.source = a0;
|
||||
timer.prescale = std::max(a2, 1u);
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
setV0(timerId(index));
|
||||
return true;
|
||||
}
|
||||
setV0(errorValue(kNoTimer));
|
||||
return true;
|
||||
case 5: // ReferHardTimer
|
||||
for (size_t index = 0u; index < m_timers.size(); ++index)
|
||||
{
|
||||
Timer &timer = m_timers[index];
|
||||
if (timer.users == 0u || timer.liveMode == 0u || (timer.sources & a0) == 0u ||
|
||||
timer.width != a1 || (timer.liveMode & a3) != a2)
|
||||
continue;
|
||||
++timer.users;
|
||||
setV0(timerId(index));
|
||||
return true;
|
||||
}
|
||||
setV0(errorValue(kNoTimer));
|
||||
return true;
|
||||
case 6: // FreeHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (--timer->users == 0u)
|
||||
{
|
||||
const uint32_t address = timer->address;
|
||||
const uint8_t sources = timer->sources;
|
||||
const uint8_t width = timer->width;
|
||||
const uint16_t maxPrescale = timer->maxPrescale;
|
||||
const uint8_t irq = timer->irq;
|
||||
*timer = {};
|
||||
timer->address = address;
|
||||
timer->sources = sources;
|
||||
timer->width = width;
|
||||
timer->maxPrescale = maxPrescale;
|
||||
timer->irq = irq;
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 7: // SetTimerMode
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (a1 == 0u)
|
||||
stop(*timer, currentCycle);
|
||||
else
|
||||
{
|
||||
timer->liveMode = a1;
|
||||
timer->running = true;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
schedule(*timer, currentCycle);
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 8: // GetTimerStatus
|
||||
case 17: // GetTimerMode
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->liveMode : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 9: // SetTimerCounter
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
timer->counterBase = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 10: // GetTimerCounter
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? counterValue(*timer, currentCycle) : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 11: // SetTimerCompare
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 12: // GetTimerCompare
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->compare : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 13: // SetHoldMode
|
||||
m_holdMode = (m_holdMode & ~(0xFu << ((a0 & 7u) * 4u))) | ((a1 & 0xFu) << ((a0 & 7u) * 4u));
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 14: // GetHoldMode
|
||||
setV0((m_holdMode >> ((a0 & 7u) * 4u)) & 0xFu);
|
||||
return true;
|
||||
case 15: // GetHoldReg
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 16: // GetHardTimerIntrCode
|
||||
{
|
||||
const Timer *timer = timerFromId(a0);
|
||||
setV0(timer ? timer->irq : errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
case 18: // GetTimerReadFunc
|
||||
// Returning a host-side register reader as a guest function is not meaningful.
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 20: // SetTimerHandler
|
||||
case 21: // SetOverflowHandler
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerNotRunning));
|
||||
return true;
|
||||
}
|
||||
if (ordinal == 20u)
|
||||
{
|
||||
timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1;
|
||||
timer->compareCallback = {a2, a3, cpu.gpr[28]};
|
||||
}
|
||||
else
|
||||
{
|
||||
timer->overflowCallback = {a1, a2, cpu.gpr[28]};
|
||||
}
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 22: // SetupHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerBusy));
|
||||
return true;
|
||||
}
|
||||
if ((a2 != 0u && a2 != 1u && a2 != 3u && a2 != 5u && a2 != 7u))
|
||||
{
|
||||
setV0(errorValue(kIllegalMode));
|
||||
return true;
|
||||
}
|
||||
if ((timer->sources & a1) == 0u)
|
||||
{
|
||||
setV0(errorValue(kIllegalSource));
|
||||
return true;
|
||||
}
|
||||
if (a3 == 0u || a3 > timer->maxPrescale)
|
||||
{
|
||||
setV0(errorValue(kIllegalPrescale));
|
||||
return true;
|
||||
}
|
||||
timer->source = a1;
|
||||
timer->setupMode = a2;
|
||||
timer->prescale = a3;
|
||||
timer->configured = true;
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 23: // StartHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerBusy));
|
||||
return true;
|
||||
}
|
||||
if (!timer->configured)
|
||||
{
|
||||
setV0(errorValue(kTimerNotConfigured));
|
||||
return true;
|
||||
}
|
||||
timer->counterBase = 0u;
|
||||
timer->counterBaseCycle = currentCycle;
|
||||
timer->liveMode = 0x80000000u | timer->setupMode;
|
||||
timer->running = true;
|
||||
schedule(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
case 24: // StopHardTimer
|
||||
{
|
||||
Timer *timer = timerFromId(a0);
|
||||
if (!timer)
|
||||
{
|
||||
setV0(errorValue(kIllegalTimerId));
|
||||
return true;
|
||||
}
|
||||
if (!timer->running)
|
||||
{
|
||||
setV0(errorValue(kTimerNotRunning));
|
||||
return true;
|
||||
}
|
||||
stop(*timer, currentCycle);
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor)
|
||||
{
|
||||
if (m_servicing)
|
||||
return;
|
||||
m_servicing = true;
|
||||
struct ServiceGuard
|
||||
{
|
||||
bool &flag;
|
||||
~ServiceGuard() { flag = false; }
|
||||
} guard{m_servicing};
|
||||
|
||||
for (Timer &timer : m_timers)
|
||||
{
|
||||
if (!timer.running)
|
||||
continue;
|
||||
|
||||
const bool compareDue = timer.compareCycle <= currentCycle;
|
||||
const bool overflowDue = timer.overflowCycle <= currentCycle;
|
||||
if (!compareDue && !overflowDue)
|
||||
continue;
|
||||
|
||||
const Callback callback = compareDue ? timer.compareCallback : timer.overflowCallback;
|
||||
timer.compareCycle = UINT64_MAX;
|
||||
timer.overflowCycle = UINT64_MAX;
|
||||
const uint32_t result = callback.function != 0u
|
||||
? executor.executeGuestFunctionWithBudget(callback.function,
|
||||
callback.common,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
callback.gp,
|
||||
100000u)
|
||||
: 0u;
|
||||
if (!timer.running)
|
||||
continue;
|
||||
if (result == 0u)
|
||||
{
|
||||
stop(timer, currentCycle);
|
||||
continue;
|
||||
}
|
||||
if (compareDue)
|
||||
timer.compare = timer.width == 16u ? (result & 0xFFFFu) : result;
|
||||
timer.counterBase = 0u;
|
||||
timer.counterBaseCycle = currentCycle;
|
||||
schedule(timer, currentCycle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept
|
||||
{
|
||||
uint64_t next = fallback;
|
||||
for (const Timer &timer : m_timers)
|
||||
{
|
||||
next = std::min(next, timer.compareCycle);
|
||||
next = std::min(next, timer.overflowCycle);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopGuestExecutor;
|
||||
|
||||
class IopTimrman
|
||||
{
|
||||
public:
|
||||
void reset() noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
void serviceDue(uint64_t currentCycle, IopGuestExecutor &executor);
|
||||
[[nodiscard]] uint64_t nextEventCycle(uint64_t fallback) const noexcept;
|
||||
|
||||
private:
|
||||
struct Callback
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t common = 0u;
|
||||
uint32_t gp = 0u;
|
||||
};
|
||||
|
||||
struct Timer
|
||||
{
|
||||
uint32_t address = 0u;
|
||||
uint8_t sources = 0u;
|
||||
uint8_t width = 0u;
|
||||
uint16_t maxPrescale = 0u;
|
||||
uint8_t irq = 0u;
|
||||
uint8_t users = 0u;
|
||||
|
||||
uint32_t source = 1u;
|
||||
uint32_t prescale = 1u;
|
||||
uint32_t setupMode = 0u;
|
||||
uint32_t liveMode = 0u;
|
||||
uint32_t counterBase = 0u;
|
||||
uint32_t compare = 0u;
|
||||
uint64_t counterBaseCycle = 0u;
|
||||
uint64_t compareCycle = UINT64_MAX;
|
||||
uint64_t overflowCycle = UINT64_MAX;
|
||||
bool configured = false;
|
||||
bool running = false;
|
||||
|
||||
Callback compareCallback;
|
||||
Callback overflowCallback;
|
||||
};
|
||||
|
||||
[[nodiscard]] Timer *timerFromId(uint32_t timerId) noexcept;
|
||||
[[nodiscard]] const Timer *timerFromId(uint32_t timerId) const noexcept;
|
||||
[[nodiscard]] static uint32_t timerId(size_t index) noexcept;
|
||||
[[nodiscard]] static uint64_t ticksToCycles(const Timer &timer, uint64_t ticks) noexcept;
|
||||
[[nodiscard]] static uint64_t elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept;
|
||||
[[nodiscard]] static uint32_t counterValue(const Timer &timer, uint64_t currentCycle) noexcept;
|
||||
static void schedule(Timer &timer, uint64_t currentCycle) noexcept;
|
||||
static void stop(Timer &timer, uint64_t currentCycle) noexcept;
|
||||
|
||||
std::array<Timer, 6> m_timers{};
|
||||
uint32_t m_holdMode = 0u;
|
||||
bool m_servicing = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "iop_vblank.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../iop_emulator_const.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopVblank::IopVblank(IopKernel &kernel) noexcept
|
||||
: m_kernel(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
bool IopVblank::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle)
|
||||
{
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // WaitVblankStart
|
||||
case 5: // WaitVblankEnd
|
||||
case 6: // WaitVblank
|
||||
case 7: // WaitNonVblank
|
||||
{
|
||||
const bool waitForEnd = ordinal == 5u || ordinal == 7u;
|
||||
const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u;
|
||||
const uint64_t fieldStart = currentCycle - (currentCycle % kVblankPeriodCycles);
|
||||
uint64_t wakeCycle = fieldStart + phase;
|
||||
if (wakeCycle <= currentCycle)
|
||||
wakeCycle += kVblankPeriodCycles;
|
||||
m_kernel.delayCurrentUntil(wakeCycle, cpu);
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
}
|
||||
case 8: // RegisterVblankHandler
|
||||
case 9: // ReleaseVblankHandler
|
||||
// Callback delivery is not required by the scheduler wait ABI yet.
|
||||
cpu.gpr[2] = 0u;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
|
||||
class IopVblank
|
||||
{
|
||||
public:
|
||||
explicit IopVblank(IopKernel &kernel) noexcept;
|
||||
|
||||
[[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle);
|
||||
|
||||
private:
|
||||
IopKernel &m_kernel;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,840 @@
|
||||
#include "iop_emulator.h"
|
||||
#include "imports/iop_cdvd.h"
|
||||
#include "core/iop_cpu.h"
|
||||
#include "imports/iop_heaplib.h"
|
||||
#include "imports/iop_imports.h"
|
||||
#include "imports/iop_intrman.h"
|
||||
#include "imports/iop_ioman.h"
|
||||
#include "core/iop_kernel.h"
|
||||
#include "imports/iop_loadcore.h"
|
||||
#include "core/iop_memory.h"
|
||||
#include "services/iop_module_loader.h"
|
||||
#include "services/iop_rpc.h"
|
||||
#include "imports/iop_stdio.h"
|
||||
#include "imports/iop_sysclib.h"
|
||||
#include "imports/iop_sysmem.h"
|
||||
#include "imports/iop_timrman.h"
|
||||
#include "imports/iop_vblank.h"
|
||||
#include "iop_emulator_const.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kRamSize = IopMemory::RamSize;
|
||||
constexpr uint32_t kKernelHeapBase = IopMemory::HeapBase;
|
||||
constexpr uint32_t kKernelHeapLimit = IopMemory::HeapLimit;
|
||||
constexpr uint32_t kCallStackBase = kKernelHeapLimit;
|
||||
constexpr uint32_t kCallStackLimit = 0x001FFF00u;
|
||||
constexpr uint32_t kCallStackSize = 0x2000u;
|
||||
constexpr uint32_t kCallStackCapacity = (kCallStackLimit - kCallStackBase) / kCallStackSize;
|
||||
constexpr uint64_t kCdvdCompletionCycles = 128u;
|
||||
|
||||
uint32_t physicalAddress(uint32_t address)
|
||||
{
|
||||
return IopMemory::physicalAddress(address);
|
||||
}
|
||||
|
||||
int32_t sign16(uint32_t value)
|
||||
{
|
||||
return static_cast<int16_t>(value & 0xFFFFu);
|
||||
}
|
||||
|
||||
bool iequals(std::string_view lhs, std::string_view rhs)
|
||||
{
|
||||
if (lhs.size() != rhs.size())
|
||||
return false;
|
||||
for (size_t i = 0; i < lhs.size(); ++i)
|
||||
{
|
||||
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
|
||||
std::tolower(static_cast<unsigned char>(rhs[i])))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class IopEmulator::Impl final : public IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
using CpuState = IopCpuState;
|
||||
|
||||
struct Module
|
||||
{
|
||||
int id = 0;
|
||||
std::string path;
|
||||
std::string name;
|
||||
uint32_t base = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t gp = 0;
|
||||
bool resident = false;
|
||||
};
|
||||
|
||||
struct GuestCallback
|
||||
{
|
||||
uint32_t function = 0;
|
||||
uint32_t gp = 0;
|
||||
};
|
||||
|
||||
struct ScheduledGuestCallback
|
||||
{
|
||||
uint32_t function = 0u;
|
||||
uint32_t gp = 0u;
|
||||
uint32_t argument = 0u;
|
||||
};
|
||||
|
||||
explicit Impl(IopHost &hostRef)
|
||||
: host(hostRef),
|
||||
sysmem(host, memory),
|
||||
kernel(memory),
|
||||
cdvd(host, memory, kernel),
|
||||
vblank(kernel),
|
||||
rpc(host, memory, kernel),
|
||||
sysclib(memory),
|
||||
stdio(host, memory),
|
||||
heaplib(memory),
|
||||
intrman(memory),
|
||||
timrman(),
|
||||
ioman(memory),
|
||||
cpuCore(memory),
|
||||
imports(memory),
|
||||
loadcore(memory, imports)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
memory.reset();
|
||||
kernel.reset();
|
||||
modules.clear();
|
||||
imports.reset();
|
||||
rpc.reset();
|
||||
cdvd.reset();
|
||||
intrman.reset();
|
||||
timrman.reset();
|
||||
ioman.reset();
|
||||
pendingDmaInterrupts.clear();
|
||||
nextDmaInterruptCycle = UINT64_MAX;
|
||||
pendingGuestCallbacks.clear();
|
||||
nextModuleId = 1;
|
||||
moduleCursor = kModuleLoadBase;
|
||||
totalCycles = 0;
|
||||
totalInstructions = 0;
|
||||
eeCycleCarry = 0;
|
||||
activeCpu = nullptr;
|
||||
lastError.clear();
|
||||
servicingDmaInterrupts = false;
|
||||
servicingGuestCallbacks = false;
|
||||
callDepth = 0u;
|
||||
secrMcCommandHandler = {};
|
||||
secrMcDevIdHandler = {};
|
||||
checkKelfPathCallback = {};
|
||||
}
|
||||
|
||||
uint8_t read8(uint32_t address) const
|
||||
{
|
||||
return memory.read8(address);
|
||||
}
|
||||
|
||||
uint16_t read16(uint32_t address) const
|
||||
{
|
||||
return memory.read16(address);
|
||||
}
|
||||
|
||||
uint32_t read32(uint32_t address) const
|
||||
{
|
||||
return memory.read32(address);
|
||||
}
|
||||
|
||||
void write8(uint32_t address, uint8_t value)
|
||||
{
|
||||
memory.write8(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void write16(uint32_t address, uint16_t value)
|
||||
{
|
||||
memory.write16(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void write32(uint32_t address, uint32_t value)
|
||||
{
|
||||
memory.write32(address, value);
|
||||
schedulePendingDma();
|
||||
}
|
||||
|
||||
void schedulePendingDma()
|
||||
{
|
||||
if (const auto dma = memory.takeDmaStart())
|
||||
{
|
||||
pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles;
|
||||
nextDmaInterruptCycle = UINT64_MAX;
|
||||
for (const auto &[irq, cycle] : pendingDmaInterrupts)
|
||||
nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, cycle);
|
||||
}
|
||||
}
|
||||
|
||||
bool readRam(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return memory.readRam(address, destination, size);
|
||||
}
|
||||
|
||||
bool writeRam(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return memory.writeRam(address, source, size);
|
||||
}
|
||||
|
||||
bool zeroRam(uint32_t address, size_t size)
|
||||
{
|
||||
return memory.zeroRam(address, size);
|
||||
}
|
||||
|
||||
bool isHardwareAddress(uint32_t phys) const
|
||||
{
|
||||
return memory.isHardwareAddress(phys);
|
||||
}
|
||||
|
||||
uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional<uint32_t> fixed = std::nullopt)
|
||||
{
|
||||
return memory.allocate(size, alignment, fixed);
|
||||
}
|
||||
|
||||
bool freeAllocation(uint32_t address)
|
||||
{
|
||||
return memory.freeAllocation(address);
|
||||
}
|
||||
|
||||
void log(LogLevel level, std::string_view text)
|
||||
{
|
||||
host.log(level, text);
|
||||
}
|
||||
|
||||
bool checkInterrupt(CpuState &cpu)
|
||||
{
|
||||
const uint32_t status = cpu.cop0[12];
|
||||
if ((status & 1u) == 0u)
|
||||
return false;
|
||||
if ((status & 0x2u) != 0u)
|
||||
return false;
|
||||
const bool pending = memory.interruptControl() != 0u && (memory.interruptStatus() & memory.interruptMask()) != 0u;
|
||||
if (!pending)
|
||||
return false;
|
||||
cpu.cop0[13] |= 0x400u;
|
||||
cpuCore.raiseException(cpu, 0u, cpu.pc, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ImportDisposition
|
||||
{
|
||||
Handled,
|
||||
JumpToGuest,
|
||||
Missing,
|
||||
};
|
||||
|
||||
ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu)
|
||||
{
|
||||
const uint32_t a0 = cpu.gpr[4];
|
||||
auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
|
||||
if (iequals(call.library, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu))
|
||||
{
|
||||
if (const auto callback = cdvd.takeCompletionCallback())
|
||||
{
|
||||
pendingGuestCallbacks.emplace(
|
||||
totalCycles + kCdvdCompletionCycles,
|
||||
ScheduledGuestCallback{
|
||||
callback->address,
|
||||
callback->gp,
|
||||
callback->reason,
|
||||
});
|
||||
}
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
|
||||
if (iequals(call.library, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
if (iequals(call.library, "thbase") || iequals(call.library, "threadman"))
|
||||
{
|
||||
return kernel.dispatchThreadImport(call.ordinal, cpu, totalCycles)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "thsemap"))
|
||||
{
|
||||
return kernel.dispatchSemaphoreImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "thevent"))
|
||||
{
|
||||
return kernel.dispatchEventImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "sifcmd"))
|
||||
{
|
||||
return rpc.dispatchSifCmdImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "secrman"))
|
||||
{
|
||||
switch (call.ordinal)
|
||||
{
|
||||
case 4: // SecrSetMcCommandHandler
|
||||
secrMcCommandHandler = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
case 5: // SecrSetMcDevIDHandler
|
||||
secrMcDevIdHandler = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iequals(call.library, "modload") && call.ordinal == 13u)
|
||||
{
|
||||
checkKelfPathCallback = {a0, cpu.gpr[28]};
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
if (iequals(call.library, "ioman") && ioman.dispatchImport(call.ordinal, cpu, *this))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "sifman"))
|
||||
{
|
||||
return rpc.dispatchSifManImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "vblank") && vblank.dispatchImport(call.ordinal, cpu, totalCycles))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "timrman") && timrman.dispatchImport(call.ordinal, cpu, totalCycles))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "dmacman"))
|
||||
{
|
||||
setV0(0);
|
||||
return ImportDisposition::Handled;
|
||||
}
|
||||
if (iequals(call.library, "stdio") && stdio.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
if (iequals(call.library, "sysclib"))
|
||||
{
|
||||
return sysclib.dispatchImport(call.ordinal, cpu)
|
||||
? ImportDisposition::Handled
|
||||
: ImportDisposition::Missing;
|
||||
}
|
||||
if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu))
|
||||
return ImportDisposition::Handled;
|
||||
|
||||
const uint32_t target = imports.resolve(call.library, call.ordinal, call.version);
|
||||
if (target != 0u)
|
||||
{
|
||||
cpu.pc = target;
|
||||
cpu.branchPending = false;
|
||||
return ImportDisposition::JumpToGuest;
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << "[IOP] unhandled import " << call.library << ':' << call.ordinal
|
||||
<< " version=0x" << std::hex << call.version << " pc=0x" << cpu.pc;
|
||||
log(LogLevel::Warning, out.str());
|
||||
setV0(0);
|
||||
return ImportDisposition::Missing;
|
||||
}
|
||||
|
||||
bool step(CpuState &cpu)
|
||||
{
|
||||
if (cpu.stopped)
|
||||
return false;
|
||||
if (cpu.pc == kThreadReturnSentinel || cpu.pc == kCallReturnSentinel)
|
||||
{
|
||||
cpu.stopped = true;
|
||||
return false;
|
||||
}
|
||||
if (physicalAddress(cpu.pc) >= kRamSize)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc;
|
||||
log(LogLevel::Error, out.str());
|
||||
cpu.stopped = true;
|
||||
return false;
|
||||
}
|
||||
if (checkInterrupt(cpu))
|
||||
return true;
|
||||
|
||||
const uint32_t instruction = memory.read32(cpu.pc);
|
||||
if (const auto import = imports.decode(cpu.pc, instruction))
|
||||
{
|
||||
const ImportDisposition disposition = dispatchImport(*import, cpu);
|
||||
++totalInstructions;
|
||||
++totalCycles;
|
||||
if (disposition == ImportDisposition::JumpToGuest)
|
||||
return true;
|
||||
cpu.pc = cpu.gpr[31];
|
||||
cpu.branchPending = false;
|
||||
return !cpu.stopped;
|
||||
}
|
||||
|
||||
const bool running = cpuCore.executeInstruction(cpu, instruction);
|
||||
if (memory.hasDmaStart())
|
||||
schedulePendingDma();
|
||||
++totalInstructions;
|
||||
++totalCycles;
|
||||
return running;
|
||||
}
|
||||
|
||||
uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget)
|
||||
{
|
||||
CpuState *previous = activeCpu;
|
||||
activeCpu = &cpu;
|
||||
const uint64_t start = totalInstructions;
|
||||
while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget)
|
||||
{
|
||||
if (!step(cpu))
|
||||
break;
|
||||
if (!servicingDmaInterrupts && totalCycles >= nextDmaInterruptCycle)
|
||||
servicePendingDmaInterrupts();
|
||||
if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty())
|
||||
servicePendingGuestCallbacks();
|
||||
}
|
||||
activeCpu = previous;
|
||||
return static_cast<uint32_t>(totalInstructions - start);
|
||||
}
|
||||
|
||||
uint32_t callFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t budget = kMaxCallInstructions)
|
||||
{
|
||||
struct CallDepthGuard
|
||||
{
|
||||
uint32_t &depth;
|
||||
~CallDepthGuard() { --depth; }
|
||||
};
|
||||
|
||||
const uint32_t depth = callDepth++;
|
||||
const CallDepthGuard depthGuard{callDepth};
|
||||
CpuState cpu{};
|
||||
cpu.pc = address;
|
||||
cpu.gpr[4] = a0;
|
||||
cpu.gpr[5] = a1;
|
||||
cpu.gpr[6] = a2;
|
||||
cpu.gpr[7] = a3;
|
||||
cpu.gpr[28] = gp;
|
||||
if (depth < kCallStackCapacity)
|
||||
{
|
||||
const uint32_t stackTop = kCallStackLimit - depth * kCallStackSize;
|
||||
cpu.gpr[29] = stackTop - 32u;
|
||||
}
|
||||
else if (activeCpu && activeCpu->gpr[29] > kCallStackBase + kStackGuardBytes)
|
||||
{
|
||||
// Extremely deep re-entrancy borrows unused space below the
|
||||
// suspended caller's live frame. Stack growth remains away
|
||||
// from the caller, so its saved registers stay intact.
|
||||
cpu.gpr[29] = (activeCpu->gpr[29] - kStackGuardBytes) & ~15u;
|
||||
}
|
||||
else
|
||||
{
|
||||
cpu.gpr[29] = kCallStackBase - 32u;
|
||||
}
|
||||
cpu.gpr[31] = kCallReturnSentinel;
|
||||
runCpu(cpu, budget);
|
||||
return cpu.gpr[2];
|
||||
}
|
||||
|
||||
uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp) override
|
||||
{
|
||||
return callFunction(address, a0, a1, a2, a3, gp);
|
||||
}
|
||||
|
||||
uint32_t executeGuestFunctionWithBudget(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t instructionBudget) override
|
||||
{
|
||||
return callFunction(address, a0, a1, a2, a3, gp, instructionBudget);
|
||||
}
|
||||
|
||||
// Not that good to use exception handling for control flow but will do for now
|
||||
void servicePendingDmaInterrupts()
|
||||
{
|
||||
if (servicingDmaInterrupts || totalCycles < nextDmaInterruptCycle)
|
||||
return;
|
||||
|
||||
servicingDmaInterrupts = true;
|
||||
|
||||
std::vector<int> completed;
|
||||
nextDmaInterruptCycle = UINT64_MAX;
|
||||
for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();)
|
||||
{
|
||||
if (it->second > totalCycles)
|
||||
{
|
||||
nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, it->second);
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
completed.push_back(it->first);
|
||||
it = pendingDmaInterrupts.erase(it);
|
||||
}
|
||||
try
|
||||
{
|
||||
for (const int irq : completed)
|
||||
(void)intrman.dispatchInterrupt(irq, *this);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
servicingDmaInterrupts = false;
|
||||
throw;
|
||||
}
|
||||
servicingDmaInterrupts = false;
|
||||
}
|
||||
|
||||
void servicePendingGuestCallbacks()
|
||||
{
|
||||
if (servicingGuestCallbacks || pendingGuestCallbacks.empty())
|
||||
return;
|
||||
|
||||
std::vector<ScheduledGuestCallback> callbacks;
|
||||
for (auto it = pendingGuestCallbacks.begin(); it != pendingGuestCallbacks.end();)
|
||||
{
|
||||
if (it->first > totalCycles)
|
||||
break;
|
||||
callbacks.push_back(it->second);
|
||||
it = pendingGuestCallbacks.erase(it);
|
||||
}
|
||||
if (callbacks.empty())
|
||||
return;
|
||||
|
||||
servicingGuestCallbacks = true;
|
||||
try
|
||||
{
|
||||
for (const ScheduledGuestCallback &callback : callbacks)
|
||||
{
|
||||
if (callback.function != 0u)
|
||||
{
|
||||
(void)callFunction(callback.function,
|
||||
callback.argument,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
callback.gp,
|
||||
100000u);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
servicingGuestCallbacks = false;
|
||||
throw;
|
||||
}
|
||||
servicingGuestCallbacks = false;
|
||||
}
|
||||
|
||||
void runCycles(uint64_t cycles) noexcept
|
||||
{
|
||||
try
|
||||
{
|
||||
const uint64_t target = totalCycles + cycles;
|
||||
while (totalCycles < target)
|
||||
{
|
||||
servicePendingDmaInterrupts();
|
||||
servicePendingGuestCallbacks();
|
||||
timrman.serviceDue(totalCycles, *this);
|
||||
IopThread *next = kernel.beginNextReady(totalCycles);
|
||||
if (!next)
|
||||
{
|
||||
uint64_t nextWake = kernel.nextWakeCycle(target);
|
||||
nextWake = std::min(nextWake, nextDmaInterruptCycle);
|
||||
if (!pendingGuestCallbacks.empty())
|
||||
nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first);
|
||||
nextWake = timrman.nextEventCycle(nextWake);
|
||||
totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake));
|
||||
continue;
|
||||
}
|
||||
const uint64_t before = totalCycles;
|
||||
runCpu(next->cpu, static_cast<uint32_t>(std::min<uint64_t>(kDefaultSlice, target - totalCycles)));
|
||||
kernel.endTimeslice(*next, kThreadReturnSentinel);
|
||||
if (totalCycles == before)
|
||||
++totalCycles;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Runtime scheduling must never throw through EeScheduler::accountCycles().
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult loadImage(std::string path, std::span<const uint8_t> image, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
ModuleLoadResult result{true, -1, -1};
|
||||
const IopImageLoadResult loaded = IopModuleLoader::load(image, memory, moduleCursor);
|
||||
moduleCursor = loaded.nextModuleCursor;
|
||||
if (!loaded)
|
||||
{
|
||||
if (loaded.error == IopImageLoadError::InvalidElf)
|
||||
log(LogLevel::Error, "[IOP] rejected invalid/non-MIPS IRX ELF");
|
||||
else if (loaded.error == IopImageLoadError::ArenaExhausted)
|
||||
log(LogLevel::Error, "[IOP] module arena exhausted");
|
||||
return result;
|
||||
}
|
||||
if (!loaded.relocationsComplete)
|
||||
log(LogLevel::Warning, "[IOP] one or more IRX relocations were unsupported");
|
||||
|
||||
Module module;
|
||||
module.id = nextModuleId++;
|
||||
module.path = std::move(path);
|
||||
const size_t slash = module.path.find_last_of("/\\:");
|
||||
module.name = slash == std::string::npos ? module.path : module.path.substr(slash + 1u);
|
||||
module.base = loaded.base;
|
||||
module.size = loaded.size;
|
||||
module.entry = loaded.entry;
|
||||
module.gp = loaded.gp;
|
||||
|
||||
uint32_t args = 0u;
|
||||
if (arguments && argumentSize)
|
||||
{
|
||||
args = allocate(argumentSize + 1u, 16u);
|
||||
if (args)
|
||||
{
|
||||
writeRam(args, arguments, argumentSize);
|
||||
write8(args + argumentSize, 0u);
|
||||
}
|
||||
}
|
||||
const uint32_t startResult = callFunction(module.entry, argumentSize, args, 0u, 0u, module.gp);
|
||||
if (args)
|
||||
freeAllocation(args);
|
||||
module.resident = startResult == 0u || startResult == 2u;
|
||||
result.moduleId = module.id;
|
||||
result.startResult = static_cast<int32_t>(startResult);
|
||||
modules[module.id] = std::move(module);
|
||||
|
||||
std::ostringstream out;
|
||||
out << "[IOP] loaded IRX id=" << result.moduleId
|
||||
<< " entry=0x" << std::hex << modules[result.moduleId].entry
|
||||
<< " base=0x" << modules[result.moduleId].base
|
||||
<< " start=" << std::dec << result.startResult;
|
||||
log(LogLevel::Info, out.str());
|
||||
return result;
|
||||
}
|
||||
|
||||
ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
std::vector<uint8_t> image;
|
||||
if (!IopModuleLoader::readWholeHostFile(host, path, image))
|
||||
{
|
||||
log(LogLevel::Warning, std::string("[IOP] failed to open IRX '") + std::string(path) + "'");
|
||||
return {true, -1, -1};
|
||||
}
|
||||
return loadImage(std::string(path), image, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
std::vector<uint8_t> image;
|
||||
if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image))
|
||||
return {true, -1, -1};
|
||||
std::ostringstream tag;
|
||||
tag << "buffer@0x" << std::hex << guestAddress;
|
||||
return loadImage(tag.str(), image, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
auto it = modules.find(moduleId);
|
||||
if (it == modules.end())
|
||||
return false;
|
||||
// A removable IRX normally exposes a stop entry through module metadata. We do not guess it; terminate owned execution and release the image cleanly.
|
||||
kernel.terminateThreadsInRange(it->second.base, it->second.size);
|
||||
rpc.removeServersInRange(it->second.base, it->second.size);
|
||||
imports.eraseRange(it->second.base, it->second.size);
|
||||
modules.erase(it);
|
||||
kernel.cleanupDeadThreads();
|
||||
if (result)
|
||||
*result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
IopMemory memory;
|
||||
IopSysmem sysmem;
|
||||
IopKernel kernel;
|
||||
IopCdvd cdvd;
|
||||
IopVblank vblank;
|
||||
IopRpcBridge rpc;
|
||||
IopSysclib sysclib;
|
||||
IopStdio stdio;
|
||||
IopHeaplib heaplib;
|
||||
IopIntrman intrman;
|
||||
IopTimrman timrman;
|
||||
IopIoman ioman;
|
||||
IopCpuCore cpuCore;
|
||||
IopImportRegistry imports;
|
||||
IopLoadcore loadcore;
|
||||
std::map<int, Module> modules;
|
||||
std::map<int, uint64_t> pendingDmaInterrupts;
|
||||
uint64_t nextDmaInterruptCycle = UINT64_MAX;
|
||||
std::multimap<uint64_t, ScheduledGuestCallback> pendingGuestCallbacks;
|
||||
uint32_t nextModuleId = 1;
|
||||
uint32_t moduleCursor = kModuleLoadBase;
|
||||
uint64_t totalCycles = 0;
|
||||
uint64_t totalInstructions = 0;
|
||||
uint64_t eeCycleCarry = 0;
|
||||
CpuState *activeCpu = nullptr;
|
||||
std::string lastError;
|
||||
bool servicingDmaInterrupts = false;
|
||||
bool servicingGuestCallbacks = false;
|
||||
uint32_t callDepth = 0u;
|
||||
GuestCallback secrMcCommandHandler;
|
||||
GuestCallback secrMcDevIdHandler;
|
||||
GuestCallback checkKelfPathCallback;
|
||||
};
|
||||
|
||||
IopEmulator::IopEmulator(IopHost &host)
|
||||
: m_impl(std::make_unique<Impl>(host))
|
||||
{
|
||||
}
|
||||
|
||||
IopEmulator::~IopEmulator() = default;
|
||||
|
||||
void IopEmulator::reset()
|
||||
{
|
||||
m_impl->reset();
|
||||
}
|
||||
|
||||
ModuleLoadResult IopEmulator::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->loadModule(path, arguments, argumentSize);
|
||||
}
|
||||
|
||||
ModuleLoadResult IopEmulator::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool IopEmulator::stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
return m_impl->stopModule(moduleId, result);
|
||||
}
|
||||
|
||||
void IopEmulator::runEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
const uint64_t total = m_impl->eeCycleCarry + eeCycles;
|
||||
const uint64_t iopCycles = total / 8u;
|
||||
m_impl->eeCycleCarry = total % 8u;
|
||||
if (iopCycles)
|
||||
m_impl->runCycles(iopCycles);
|
||||
}
|
||||
|
||||
RpcResult IopEmulator::handleRpc(const RpcRequest &request)
|
||||
{
|
||||
return m_impl->rpc.handleRpc(request, *m_impl);
|
||||
}
|
||||
|
||||
bool IopEmulator::hasRpcServer(uint32_t sid) const noexcept
|
||||
{
|
||||
return m_impl->rpc.hasServer(sid);
|
||||
}
|
||||
|
||||
void IopEmulator::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
m_impl->rpc.onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::allocateMemory(uint32_t size, uint32_t alignment)
|
||||
{
|
||||
return m_impl->memory.allocate(size, alignment);
|
||||
}
|
||||
|
||||
bool IopEmulator::freeMemory(uint32_t address)
|
||||
{
|
||||
return m_impl->memory.freeAllocation(address);
|
||||
}
|
||||
|
||||
bool IopEmulator::readMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.readRam(address, destination, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::writeMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.writeRam(address, source, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::zeroMemory(uint32_t address, size_t size)
|
||||
{
|
||||
return isMemoryRange(address, size) &&
|
||||
m_impl->memory.zeroRam(address, size);
|
||||
}
|
||||
|
||||
bool IopEmulator::isMemoryRange(uint32_t address, size_t size) const
|
||||
{
|
||||
const bool physicalSegment = address < IopMemory::RamSize;
|
||||
const bool cachedSegment = address >= 0x80000000u && address < 0x80200000u;
|
||||
const bool uncachedSegment = address >= 0xA0000000u && address < 0xA0200000u;
|
||||
if (!physicalSegment && !cachedSegment && !uncachedSegment)
|
||||
return false;
|
||||
const uint32_t physical = IopMemory::physicalAddress(address);
|
||||
return physical <= IopMemory::RamSize && size <= IopMemory::RamSize - physical;
|
||||
}
|
||||
|
||||
uint64_t IopEmulator::cycles() const noexcept
|
||||
{
|
||||
return m_impl->totalCycles;
|
||||
}
|
||||
|
||||
uint64_t IopEmulator::instructions() const noexcept
|
||||
{
|
||||
return m_impl->totalInstructions;
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::loadedModuleCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->modules.size());
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::threadCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->kernel.threadCount());
|
||||
}
|
||||
|
||||
uint32_t IopEmulator::rpcServerCount() const noexcept
|
||||
{
|
||||
return static_cast<uint32_t>(m_impl->rpc.serverCount());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopEmulator
|
||||
{
|
||||
public:
|
||||
explicit IopEmulator(IopHost &host);
|
||||
~IopEmulator();
|
||||
|
||||
IopEmulator(const IopEmulator &) = delete;
|
||||
IopEmulator &operator=(const IopEmulator &) = delete;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize);
|
||||
[[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result);
|
||||
void runEeCycles(uint64_t eeCycles) noexcept;
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request);
|
||||
[[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept;
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
|
||||
[[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u);
|
||||
[[nodiscard]] bool freeMemory(uint32_t address);
|
||||
[[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const;
|
||||
[[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size);
|
||||
[[nodiscard]] bool zeroMemory(uint32_t address, size_t size);
|
||||
[[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const;
|
||||
|
||||
[[nodiscard]] uint64_t cycles() const noexcept;
|
||||
[[nodiscard]] uint64_t instructions() const noexcept;
|
||||
[[nodiscard]] uint32_t loadedModuleCount() const noexcept;
|
||||
[[nodiscard]] uint32_t threadCount() const noexcept;
|
||||
[[nodiscard]] uint32_t rpcServerCount() const noexcept;
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> m_impl;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
constexpr uint32_t kThreadReturnSentinel = 0x1FFFFF00u;
|
||||
constexpr uint32_t kCallReturnSentinel = 0x1FFFFF04u;
|
||||
constexpr uint64_t kIopClockHz = 36'864'000ull;
|
||||
// NTSC field cadence (approximately 59.94 Hz). VBlank imports are
|
||||
// scheduler waits, not no-op timing hints: returning immediately lets
|
||||
// high-priority IRX threads busy-loop and starve RPC server threads.
|
||||
constexpr uint64_t kVblankPeriodCycles = (kIopClockHz * 1001ull + 30'000ull) / 60'000ull;
|
||||
constexpr uint64_t kVblankEndPhaseCycles = kVblankPeriodCycles / 16ull;
|
||||
constexpr uint32_t kDefaultSlice = 256u;
|
||||
constexpr uint32_t kMaxCallInstructions = 2'000'000u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
constexpr uint32_t kStackGuardBytes = 64u;
|
||||
@@ -0,0 +1,561 @@
|
||||
#include "iop_module_loader.h"
|
||||
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kMaxImageSize = 64u * 1024u * 1024u;
|
||||
constexpr uint32_t kModuleLoadBase = 0x00010000u;
|
||||
|
||||
constexpr uint16_t ET_EXEC = 2;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC = 0xFF80u;
|
||||
constexpr uint16_t ET_SCE_IOPRELEXEC2 = 0xFF81u;
|
||||
constexpr uint16_t EM_MIPS = 8;
|
||||
constexpr uint32_t PT_LOAD = 1;
|
||||
constexpr uint32_t PT_SCE_IOPMOD = 0x70000080u;
|
||||
constexpr uint32_t PT_MIPS_REGINFO = 0x70000000u;
|
||||
constexpr uint32_t SHT_SYMTAB = 2;
|
||||
constexpr uint32_t SHT_MIPS_REGINFO = 0x70000006u;
|
||||
constexpr uint32_t SHT_RELA = 4;
|
||||
constexpr uint32_t SHT_NOBITS = 8;
|
||||
constexpr uint32_t SHT_REL = 9;
|
||||
constexpr uint32_t SHF_ALLOC = 0x2;
|
||||
constexpr uint32_t R_MIPS_NONE = 0;
|
||||
constexpr uint32_t R_MIPS_16 = 1;
|
||||
constexpr uint32_t R_MIPS_32 = 2;
|
||||
constexpr uint32_t R_MIPS_REL32 = 3;
|
||||
constexpr uint32_t R_MIPS_26 = 4;
|
||||
constexpr uint32_t R_MIPS_HI16 = 5;
|
||||
constexpr uint32_t R_MIPS_LO16 = 6;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct Elf32Ehdr
|
||||
{
|
||||
unsigned char ident[16];
|
||||
uint16_t type;
|
||||
uint16_t machine;
|
||||
uint32_t version;
|
||||
uint32_t entry;
|
||||
uint32_t phoff;
|
||||
uint32_t shoff;
|
||||
uint32_t flags;
|
||||
uint16_t ehsize;
|
||||
uint16_t phentsize;
|
||||
uint16_t phnum;
|
||||
uint16_t shentsize;
|
||||
uint16_t shnum;
|
||||
uint16_t shstrndx;
|
||||
};
|
||||
|
||||
struct Elf32Phdr
|
||||
{
|
||||
uint32_t type;
|
||||
uint32_t offset;
|
||||
uint32_t vaddr;
|
||||
uint32_t paddr;
|
||||
uint32_t filesz;
|
||||
uint32_t memsz;
|
||||
uint32_t flags;
|
||||
uint32_t align;
|
||||
};
|
||||
|
||||
struct Elf32Shdr
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t type;
|
||||
uint32_t flags;
|
||||
uint32_t addr;
|
||||
uint32_t offset;
|
||||
uint32_t size;
|
||||
uint32_t link;
|
||||
uint32_t info;
|
||||
uint32_t addralign;
|
||||
uint32_t entsize;
|
||||
};
|
||||
|
||||
struct Elf32Sym
|
||||
{
|
||||
uint32_t name;
|
||||
uint32_t value;
|
||||
uint32_t size;
|
||||
uint8_t info;
|
||||
uint8_t other;
|
||||
uint16_t shndx;
|
||||
};
|
||||
|
||||
struct Elf32Rel
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
};
|
||||
|
||||
struct Elf32Rela
|
||||
{
|
||||
uint32_t offset;
|
||||
uint32_t info;
|
||||
int32_t addend;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(Elf32Ehdr) == 52);
|
||||
static_assert(sizeof(Elf32Phdr) == 32);
|
||||
static_assert(sizeof(Elf32Shdr) == 40);
|
||||
static_assert(sizeof(Elf32Sym) == 16);
|
||||
|
||||
struct PendingHi16
|
||||
{
|
||||
uint32_t address = 0;
|
||||
uint32_t symbolValue = 0;
|
||||
uint32_t symbolIndex = 0;
|
||||
};
|
||||
|
||||
uint32_t alignUp(uint32_t value, uint32_t alignment)
|
||||
{
|
||||
if (alignment <= 1u)
|
||||
return value;
|
||||
const uint32_t mask = alignment - 1u;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
|
||||
bool checkedRange(size_t total, uint32_t offset, uint32_t size)
|
||||
{
|
||||
return offset <= total && size <= total - offset;
|
||||
}
|
||||
|
||||
bool validElfHeader(const Elf32Ehdr &header)
|
||||
{
|
||||
return header.ident[0] == 0x7Fu &&
|
||||
header.ident[1] == 'E' &&
|
||||
header.ident[2] == 'L' &&
|
||||
header.ident[3] == 'F' &&
|
||||
header.ident[4] == 1 &&
|
||||
header.ident[5] == 1 &&
|
||||
header.machine == EM_MIPS &&
|
||||
header.ehsize >= sizeof(Elf32Ehdr);
|
||||
}
|
||||
|
||||
bool applyRelocations(std::span<const uint8_t> image,
|
||||
const std::vector<Elf32Shdr> §ions,
|
||||
int64_t delta,
|
||||
uint32_t loadBase,
|
||||
bool isIopRelocatable,
|
||||
IopMemory &memory)
|
||||
{
|
||||
if (sections.empty())
|
||||
return true;
|
||||
|
||||
bool allSupported = true;
|
||||
std::vector<PendingHi16> hi16;
|
||||
for (size_t sectionIndex = 0; sectionIndex < sections.size(); ++sectionIndex)
|
||||
{
|
||||
const Elf32Shdr &relsec = sections[sectionIndex];
|
||||
if (relsec.type != SHT_REL && relsec.type != SHT_RELA)
|
||||
continue;
|
||||
if (relsec.info >= sections.size())
|
||||
continue;
|
||||
|
||||
const Elf32Shdr &targetSection = sections[relsec.info];
|
||||
const uint32_t targetBase = static_cast<uint32_t>(static_cast<int64_t>(targetSection.addr) + delta);
|
||||
|
||||
std::span<const Elf32Sym> symbols;
|
||||
std::vector<Elf32Sym> symbolStorage;
|
||||
if (relsec.link < sections.size())
|
||||
{
|
||||
const Elf32Shdr &symsec = sections[relsec.link];
|
||||
if (symsec.type == SHT_SYMTAB && symsec.entsize >= sizeof(Elf32Sym) && checkedRange(image.size(), symsec.offset, symsec.size))
|
||||
{
|
||||
const size_t count = symsec.size / symsec.entsize;
|
||||
symbolStorage.resize(count);
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
{
|
||||
std::memcpy(&symbolStorage[i], image.data() + symsec.offset + i * symsec.entsize, sizeof(Elf32Sym));
|
||||
}
|
||||
symbols = symbolStorage;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t entrySize = relsec.type == SHT_RELA
|
||||
? std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rela))
|
||||
: std::max<uint32_t>(relsec.entsize, sizeof(Elf32Rel));
|
||||
if (entrySize == 0u || !checkedRange(image.size(), relsec.offset, relsec.size))
|
||||
continue;
|
||||
|
||||
for (uint32_t offset = 0; offset + entrySize <= relsec.size; offset += entrySize)
|
||||
{
|
||||
uint32_t relocationOffset = 0u;
|
||||
uint32_t relocationInfo = 0u;
|
||||
int32_t explicitAddend = 0;
|
||||
if (relsec.type == SHT_RELA)
|
||||
{
|
||||
Elf32Rela relocation{};
|
||||
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
explicitAddend = relocation.addend;
|
||||
}
|
||||
else
|
||||
{
|
||||
Elf32Rel relocation{};
|
||||
std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation));
|
||||
relocationOffset = relocation.offset;
|
||||
relocationInfo = relocation.info;
|
||||
}
|
||||
|
||||
const uint32_t type = relocationInfo & 0xFFu;
|
||||
const uint32_t symbolIndex = relocationInfo >> 8u;
|
||||
uint32_t symbolValue = isIopRelocatable ? loadBase : 0u;
|
||||
if (symbolIndex < symbols.size())
|
||||
{
|
||||
const Elf32Sym &symbol = symbols[symbolIndex];
|
||||
if (!isIopRelocatable || symbolIndex != 0u)
|
||||
{
|
||||
symbolValue = symbol.value;
|
||||
if (symbol.shndx != 0u)
|
||||
{
|
||||
symbolValue = static_cast<uint32_t>(static_cast<int64_t>(symbolValue) + delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sony IOP relocatable executables use absolute image offsets
|
||||
// and symbol index zero. loadcore applies them as loadBase +
|
||||
// r_offset; normal ELF REL sections use a section-relative offset.
|
||||
const uint64_t place64 = isIopRelocatable
|
||||
? static_cast<uint64_t>(loadBase) + relocationOffset
|
||||
: static_cast<uint64_t>(targetBase) + relocationOffset;
|
||||
if (place64 > std::numeric_limits<uint32_t>::max())
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
const uint32_t place = static_cast<uint32_t>(place64);
|
||||
if (place + 3u >= IopMemory::RamSize)
|
||||
{
|
||||
allSupported = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint32_t word = memory.read32(place);
|
||||
const int32_t addend = relsec.type == SHT_RELA
|
||||
? explicitAddend
|
||||
: static_cast<int32_t>(word);
|
||||
switch (type)
|
||||
{
|
||||
case R_MIPS_NONE:
|
||||
break;
|
||||
case R_MIPS_32:
|
||||
case R_MIPS_REL32:
|
||||
memory.write32(place, static_cast<uint32_t>(static_cast<int64_t>(addend) + symbolValue));
|
||||
break;
|
||||
case R_MIPS_26:
|
||||
{
|
||||
const uint32_t target = ((word & 0x03FFFFFFu) << 2u) + symbolValue;
|
||||
memory.write32(place, (word & 0xFC000000u) | ((target >> 2u) & 0x03FFFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_HI16:
|
||||
hi16.push_back({place, symbolValue, symbolIndex});
|
||||
break;
|
||||
case R_MIPS_LO16:
|
||||
{
|
||||
const int32_t lo = static_cast<int16_t>(word & 0xFFFFu);
|
||||
for (auto pending = hi16.begin(); pending != hi16.end();)
|
||||
{
|
||||
if (pending->symbolIndex != symbolIndex)
|
||||
{
|
||||
++pending;
|
||||
continue;
|
||||
}
|
||||
const uint32_t hiWord = memory.read32(pending->address);
|
||||
const int32_t hi = static_cast<int16_t>(hiWord & 0xFFFFu) << 16u;
|
||||
const int64_t full = static_cast<int64_t>(hi) + lo + pending->symbolValue;
|
||||
const uint32_t relocatedHi = static_cast<uint32_t>((full + 0x8000) >> 16u) & 0xFFFFu;
|
||||
memory.write32(pending->address, (hiWord & 0xFFFF0000u) | relocatedHi);
|
||||
pending = hi16.erase(pending);
|
||||
}
|
||||
const int64_t full = static_cast<int64_t>(lo) + symbolValue;
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(full) & 0xFFFFu));
|
||||
break;
|
||||
}
|
||||
case R_MIPS_16:
|
||||
memory.write32(place, (word & 0xFFFF0000u) | (static_cast<uint32_t>(addend + symbolValue) & 0xFFFFu));
|
||||
break;
|
||||
default:
|
||||
allSupported = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSupported;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
const std::string translated = host.translateGuestPath(guestPath);
|
||||
const std::string_view path = translated.empty() ? guestPath : std::string_view(translated);
|
||||
const uint64_t handle = host.openHostFile(path);
|
||||
if (handle == 0u)
|
||||
return false;
|
||||
|
||||
uint64_t size = 0u;
|
||||
if (!host.hostFileSize(handle, size) || size == 0u || size > kMaxImageSize)
|
||||
{
|
||||
host.closeHostFile(handle);
|
||||
return false;
|
||||
}
|
||||
|
||||
bytes.resize(static_cast<size_t>(size));
|
||||
size_t bytesRead = 0u;
|
||||
const bool ok = host.readHostFile(handle, 0u, bytes.data(), bytes.size(), bytesRead) && bytesRead == bytes.size();
|
||||
host.closeHostFile(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool IopModuleLoader::readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes)
|
||||
{
|
||||
Elf32Ehdr header{};
|
||||
if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header))
|
||||
return false;
|
||||
|
||||
uint64_t required = sizeof(header);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.phoff) + static_cast<uint64_t>(header.phentsize) * header.phnum);
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(header.shoff) + static_cast<uint64_t>(header.shentsize) * header.shnum);
|
||||
|
||||
if (required > kMaxImageSize)
|
||||
return false; // Should we log an error here? TODO check later
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
if (!host.readGuest(guestAddress, bytes.data(), bytes.size()))
|
||||
return false;
|
||||
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
const size_t offset = static_cast<size_t>(header.shoff) + static_cast<size_t>(i) * header.shentsize;
|
||||
std::memcpy(§ion, bytes.data() + offset, sizeof(section));
|
||||
if (section.type != SHT_NOBITS)
|
||||
{
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(section.offset) + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr))
|
||||
{
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
const size_t offset = static_cast<size_t>(header.phoff) + static_cast<size_t>(i) * header.phentsize;
|
||||
std::memcpy(&program, bytes.data() + offset, sizeof(program));
|
||||
required = std::max<uint64_t>(required, static_cast<uint64_t>(program.offset) + program.filesz);
|
||||
}
|
||||
}
|
||||
if (required > kMaxImageSize)
|
||||
return false;
|
||||
|
||||
bytes.resize(static_cast<size_t>(required));
|
||||
return host.readGuest(guestAddress, bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
IopImageLoadResult IopModuleLoader::load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor)
|
||||
{
|
||||
IopImageLoadResult result;
|
||||
result.nextModuleCursor = moduleCursor;
|
||||
if (image.size() < sizeof(Elf32Ehdr))
|
||||
return result;
|
||||
|
||||
Elf32Ehdr header{};
|
||||
std::memcpy(&header, image.data(), sizeof(header));
|
||||
if (!validElfHeader(header))
|
||||
{
|
||||
result.error = IopImageLoadError::InvalidElf;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t minVaddr = std::numeric_limits<uint32_t>::max();
|
||||
uint32_t maxVaddr = 0u;
|
||||
bool hasLoad = false;
|
||||
std::vector<Elf32Phdr> programHeaders;
|
||||
if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr) && checkedRange(image.size(), header.phoff, static_cast<uint32_t>(header.phentsize) * header.phnum))
|
||||
{
|
||||
programHeaders.reserve(header.phnum);
|
||||
for (uint16_t i = 0; i < header.phnum; ++i)
|
||||
{
|
||||
Elf32Phdr program{};
|
||||
std::memcpy(&program, image.data() + header.phoff + static_cast<size_t>(i) * header.phentsize, sizeof(program));
|
||||
programHeaders.push_back(program);
|
||||
if (program.type == PT_LOAD && program.memsz != 0u)
|
||||
{
|
||||
hasLoad = true;
|
||||
minVaddr = std::min(minVaddr, program.vaddr);
|
||||
maxVaddr = std::max(maxVaddr, program.vaddr + program.memsz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Elf32Shdr> sectionHeaders;
|
||||
if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr) && checkedRange(image.size(), header.shoff, static_cast<uint32_t>(header.shentsize) * header.shnum))
|
||||
{
|
||||
sectionHeaders.reserve(header.shnum);
|
||||
for (uint16_t i = 0; i < header.shnum; ++i)
|
||||
{
|
||||
Elf32Shdr section{};
|
||||
std::memcpy(§ion, image.data() + header.shoff + static_cast<size_t>(i) * header.shentsize, sizeof(section));
|
||||
sectionHeaders.push_back(section);
|
||||
if (!hasLoad && (section.flags & SHF_ALLOC) != 0u && section.size != 0u)
|
||||
{
|
||||
minVaddr = std::min(minVaddr, section.addr);
|
||||
maxVaddr = std::max(maxVaddr, section.addr + section.size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (minVaddr == std::numeric_limits<uint32_t>::max())
|
||||
minVaddr = 0u;
|
||||
uint32_t span = maxVaddr > minVaddr ? maxVaddr - minVaddr : 0x1000u;
|
||||
span = alignUp(span, 0x100u);
|
||||
const bool relocate = header.type != ET_EXEC ||
|
||||
maxVaddr > IopMemory::RamSize ||
|
||||
(minVaddr < kModuleLoadBase && minVaddr != 0u);
|
||||
uint32_t base = 0u;
|
||||
int64_t delta = 0;
|
||||
if (relocate)
|
||||
{
|
||||
base = alignUp(moduleCursor, 0x100u);
|
||||
if (base + span >= IopMemory::HeapBase)
|
||||
{
|
||||
result.error = IopImageLoadError::ArenaExhausted;
|
||||
return result;
|
||||
}
|
||||
delta = static_cast<int64_t>(base) - minVaddr;
|
||||
result.nextModuleCursor = base + span;
|
||||
}
|
||||
else
|
||||
{
|
||||
base = minVaddr;
|
||||
}
|
||||
|
||||
if (hasLoad)
|
||||
{
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type != PT_LOAD || program.memsz == 0u)
|
||||
continue;
|
||||
if (!checkedRange(image.size(), program.offset, program.filesz) ||
|
||||
program.memsz < program.filesz)
|
||||
return result;
|
||||
const uint32_t destination = static_cast<uint32_t>(static_cast<int64_t>(program.vaddr) + delta);
|
||||
if (destination >= IopMemory::RamSize || program.memsz > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (!memory.writeRam(destination, image.data() + program.offset, program.filesz))
|
||||
return result;
|
||||
if (program.memsz > program.filesz && !memory.zeroRam(destination + program.filesz, program.memsz - program.filesz))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uint32_t sectionCursor = base;
|
||||
for (auto §ion : sectionHeaders)
|
||||
{
|
||||
if ((section.flags & SHF_ALLOC) == 0u || section.size == 0u)
|
||||
continue;
|
||||
uint32_t destination = 0u;
|
||||
if (section.addr != 0u)
|
||||
{
|
||||
destination = static_cast<uint32_t>(static_cast<int64_t>(section.addr) + delta);
|
||||
}
|
||||
else
|
||||
{
|
||||
sectionCursor = alignUp(sectionCursor, std::max<uint32_t>(section.addralign, 4u));
|
||||
destination = sectionCursor;
|
||||
section.addr = static_cast<uint32_t>(static_cast<int64_t>(destination) - delta);
|
||||
sectionCursor += section.size;
|
||||
}
|
||||
if (destination >= IopMemory::RamSize || section.size > IopMemory::RamSize - destination)
|
||||
return result;
|
||||
if (section.type == SHT_NOBITS)
|
||||
{
|
||||
if (!memory.zeroRam(destination, section.size))
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!checkedRange(image.size(), section.offset, section.size) ||
|
||||
!memory.writeRam(destination, image.data() + section.offset, section.size))
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool isIopRelocatable = header.type == ET_SCE_IOPRELEXEC || header.type == ET_SCE_IOPRELEXEC2;
|
||||
result.relocationsComplete = applyRelocations(image,
|
||||
sectionHeaders,
|
||||
delta,
|
||||
base,
|
||||
isIopRelocatable,
|
||||
memory);
|
||||
|
||||
result.base = base;
|
||||
result.size = span;
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(header.entry) + delta);
|
||||
result.gp = 0u;
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (program.type == PT_SCE_IOPMOD && program.filesz >= 12u && checkedRange(image.size(), program.offset, 12u))
|
||||
{
|
||||
uint32_t entry = 0u;
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&entry, image.data() + program.offset + 4u, sizeof(entry));
|
||||
std::memcpy(&gp, image.data() + program.offset + 8u, sizeof(gp));
|
||||
result.entry = static_cast<uint32_t>(static_cast<int64_t>(entry) + delta);
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const auto &program : programHeaders)
|
||||
{
|
||||
if (result.gp != 0u)
|
||||
break;
|
||||
if (program.type == PT_MIPS_REGINFO && program.filesz >= 24u && checkedRange(image.size(), program.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + program.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.gp == 0u)
|
||||
{
|
||||
for (const auto §ion : sectionHeaders)
|
||||
{
|
||||
if (section.type == SHT_MIPS_REGINFO && section.size >= 24u && checkedRange(image.size(), section.offset, 24u))
|
||||
{
|
||||
uint32_t gp = 0u;
|
||||
std::memcpy(&gp, image.data() + section.offset + 20u, sizeof(gp));
|
||||
result.gp = gp != 0u
|
||||
? static_cast<uint32_t>(static_cast<int64_t>(gp) + delta)
|
||||
: 0u;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.error = IopImageLoadError::None;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopMemory;
|
||||
|
||||
enum class IopImageLoadError : uint8_t
|
||||
{
|
||||
None,
|
||||
InvalidElf,
|
||||
ArenaExhausted,
|
||||
MalformedImage,
|
||||
};
|
||||
|
||||
struct IopImageLoadResult
|
||||
{
|
||||
IopImageLoadError error = IopImageLoadError::MalformedImage;
|
||||
uint32_t base = 0;
|
||||
uint32_t size = 0;
|
||||
uint32_t entry = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t nextModuleCursor = 0;
|
||||
bool relocationsComplete = true;
|
||||
|
||||
[[nodiscard]] explicit operator bool() const noexcept
|
||||
{
|
||||
return error == IopImageLoadError::None;
|
||||
}
|
||||
};
|
||||
|
||||
class IopModuleLoader
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] static bool readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector<uint8_t> &bytes);
|
||||
[[nodiscard]] static IopImageLoadResult load(std::span<const uint8_t> image, IopMemory &memory, uint32_t moduleCursor);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
#include "iop_rpc.h"
|
||||
|
||||
#include "../core/iop_cpu.h"
|
||||
#include "../core/iop_kernel.h"
|
||||
#include "../core/iop_memory.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopRpcBridge::IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept
|
||||
: m_host(host), m_memory(memory), m_kernel(kernel)
|
||||
{
|
||||
}
|
||||
|
||||
void IopRpcBridge::reset()
|
||||
{
|
||||
m_servers.clear();
|
||||
m_nextDmaId = 1u;
|
||||
m_sifInitialized = false;
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // sceSifDma2Init
|
||||
case 5: // sceSifInit
|
||||
m_sifInitialized = true;
|
||||
setV0(0u);
|
||||
return true;
|
||||
case 7: // sceSifSetDma
|
||||
{
|
||||
constexpr uint32_t kDescriptorSize = 16u;
|
||||
constexpr uint32_t kMaxDescriptors = 32u;
|
||||
const uint32_t descriptorAddress = cpu.gpr[4];
|
||||
const uint32_t descriptorCount = cpu.gpr[5];
|
||||
if (descriptorAddress == 0u || descriptorCount == 0u || descriptorCount > kMaxDescriptors)
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct PendingTransfer
|
||||
{
|
||||
uint32_t source = 0u;
|
||||
uint32_t destination = 0u;
|
||||
uint32_t size = 0u;
|
||||
};
|
||||
|
||||
std::array<uint32_t, kMaxDescriptors * 4u> descriptorWords{};
|
||||
const size_t descriptorBytes = static_cast<size_t>(descriptorCount) * kDescriptorSize;
|
||||
if (!m_memory.readRam(descriptorAddress, descriptorWords.data(), descriptorBytes))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<PendingTransfer, kMaxDescriptors> pending{};
|
||||
uint32_t pendingCount = 0u;
|
||||
uint32_t largestTransfer = 0u;
|
||||
for (uint32_t i = 0u; i < descriptorCount; ++i)
|
||||
{
|
||||
const uint32_t source = descriptorWords[i * 4u + 0u];
|
||||
const uint32_t destination = descriptorWords[i * 4u + 1u];
|
||||
const int32_t signedSize = static_cast<int32_t>(descriptorWords[i * 4u + 2u]);
|
||||
if (signedSize <= 0)
|
||||
continue;
|
||||
|
||||
const uint32_t size = static_cast<uint32_t>(signedSize);
|
||||
if (!m_memory.ownsRamRange(source, size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
pending[pendingCount++] = {source, destination, size};
|
||||
largestTransfer = std::max(largestTransfer, size);
|
||||
}
|
||||
|
||||
// IOP-side sceSifSetDma sends IOP RAM to the EE. Validate all EE
|
||||
// destinations before committing any write so a bad chain cannot
|
||||
// partially update guest memory, but maybe we could skip this check if we trust the EE-side SIF driver to validate the chain ?!
|
||||
// TODO check later
|
||||
std::vector<uint8_t> scratch(largestTransfer);
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_host.readGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0u; i < pendingCount; ++i)
|
||||
{
|
||||
const PendingTransfer &transfer = pending[i];
|
||||
if (!m_memory.readRam(transfer.source, scratch.data(), transfer.size) || !m_host.writeGuest(transfer.destination, scratch.data(), transfer.size))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t dmaId = m_nextDmaId++;
|
||||
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
|
||||
{
|
||||
m_nextDmaId = 1u;
|
||||
}
|
||||
setV0(dmaId);
|
||||
return true;
|
||||
}
|
||||
case 8: // sceSifDmaStat
|
||||
setV0(0xFFFFFFFFu);
|
||||
return true;
|
||||
case 29: // sceSifCheckInit
|
||||
setV0(m_sifInitialized ? 1u : 0u);
|
||||
return true;
|
||||
default:
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu)
|
||||
{
|
||||
const auto setV0 = [&](uint32_t value)
|
||||
{
|
||||
cpu.gpr[2] = value;
|
||||
};
|
||||
switch (ordinal)
|
||||
{
|
||||
case 4: // InitCmd
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
case 8:
|
||||
case 9:
|
||||
case 10:
|
||||
case 11:
|
||||
case 14: // InitRpc
|
||||
case 15:
|
||||
case 16:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 12: // sceSifSendCmd
|
||||
case 13: // isceSifSendCmd
|
||||
{
|
||||
constexpr uint32_t kHeaderSize = 16u;
|
||||
constexpr uint32_t kMaxPacketSize = 112u;
|
||||
const uint32_t commandId = cpu.gpr[4];
|
||||
const uint32_t packetAddress = cpu.gpr[5];
|
||||
const uint32_t packetSize = cpu.gpr[6];
|
||||
const uint32_t extraSource = cpu.gpr[7];
|
||||
const uint32_t stackPointer = cpu.gpr[29];
|
||||
const uint32_t extraDestination = m_memory.read32(stackPointer + 16u);
|
||||
const int32_t signedExtraSize = static_cast<int32_t>(m_memory.read32(stackPointer + 20u));
|
||||
|
||||
if (packetAddress == 0u || packetSize < kHeaderSize || packetSize > kMaxPacketSize ||
|
||||
!m_memory.ownsRamRange(packetAddress, packetSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kMaxPacketSize> packet{};
|
||||
if (!m_memory.readRam(packetAddress, packet.data(), packetSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t extraSize = 0u;
|
||||
if (signedExtraSize > 0)
|
||||
{
|
||||
extraSize = static_cast<uint32_t>(signedExtraSize);
|
||||
if (extraSource == 0u || extraDestination == 0u ||
|
||||
!m_memory.ownsRamRange(extraSource, extraSize) ||
|
||||
!m_host.writeGuest(extraDestination, m_memory.ram().data() + IopMemory::physicalAddress(extraSource), extraSize))
|
||||
{
|
||||
setV0(0u);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const uint32_t sizeWord = packetSize | (extraSize << 8u);
|
||||
std::memcpy(packet.data() + 0u, &sizeWord, sizeof(sizeWord));
|
||||
std::memcpy(packet.data() + 4u, &extraDestination, sizeof(extraDestination));
|
||||
std::memcpy(packet.data() + 8u, &commandId, sizeof(commandId));
|
||||
|
||||
if (!m_host.sendSifCommand(commandId, packet.data(), packetSize))
|
||||
{
|
||||
// A command without an EE handler is still a completed DMA on real hardware. Only malformed packets fail above.
|
||||
}
|
||||
|
||||
const uint32_t dmaId = m_nextDmaId++;
|
||||
if (m_nextDmaId == 0u || m_nextDmaId > static_cast<uint32_t>(std::numeric_limits<int32_t>::max()))
|
||||
m_nextDmaId = 1u;
|
||||
setV0(dmaId);
|
||||
return true;
|
||||
}
|
||||
case 17: // sceSifRegisterRpc
|
||||
{
|
||||
RpcServer server;
|
||||
server.serverData = cpu.gpr[4];
|
||||
server.sid = cpu.gpr[5];
|
||||
server.function = cpu.gpr[6];
|
||||
server.gp = cpu.gpr[28];
|
||||
server.buffer = cpu.gpr[7];
|
||||
const uint32_t stackPointer = cpu.gpr[29];
|
||||
server.callback = m_memory.read32(stackPointer + 16u);
|
||||
server.callbackBuffer = m_memory.read32(stackPointer + 20u);
|
||||
server.queue = m_memory.read32(stackPointer + 24u);
|
||||
m_servers[server.sid] = server;
|
||||
if (server.serverData != 0u)
|
||||
{
|
||||
m_memory.write32(server.serverData + 0x20u, server.sid);
|
||||
m_memory.write32(server.serverData + 0x28u, server.function);
|
||||
m_memory.write32(server.serverData + 0x2Cu, server.buffer);
|
||||
}
|
||||
setV0(server.serverData);
|
||||
return true;
|
||||
}
|
||||
case 18:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 19: // SetRpcQueue
|
||||
setV0(cpu.gpr[4]);
|
||||
return true;
|
||||
case 20:
|
||||
case 21:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 22: // RpcLoop
|
||||
m_kernel.sleepCurrent(cpu);
|
||||
setV0(0);
|
||||
return true;
|
||||
case 23:
|
||||
setV0(0);
|
||||
return true;
|
||||
case 24: // RemoveRpc
|
||||
{
|
||||
const uint32_t serverData = cpu.gpr[4];
|
||||
for (auto server = m_servers.begin(); server != m_servers.end(); ++server)
|
||||
{
|
||||
if (server->second.serverData == serverData)
|
||||
{
|
||||
m_servers.erase(server);
|
||||
break;
|
||||
}
|
||||
}
|
||||
setV0(0);
|
||||
return true;
|
||||
}
|
||||
case 25:
|
||||
case 26:
|
||||
case 27:
|
||||
case 28:
|
||||
case 29:
|
||||
setV0(0);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
RpcResult IopRpcBridge::handleRpc(const RpcRequest &request, IopGuestExecutor &executor)
|
||||
{
|
||||
RpcResult result{};
|
||||
const auto serverIt = m_servers.find(request.sid);
|
||||
if (serverIt == m_servers.end() || serverIt->second.function == 0u)
|
||||
return result;
|
||||
|
||||
RpcServer &server = serverIt->second;
|
||||
if (request.send.size != 0u && server.buffer != 0u)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.send.size, IopMemory::RamSize - std::min(server.buffer, IopMemory::RamSize));
|
||||
if (copySize != 0u)
|
||||
{
|
||||
std::vector<uint8_t> payload(copySize);
|
||||
if (m_host.readGuest(request.send.address, payload.data(), payload.size()))
|
||||
(void)m_memory.writeRam(server.buffer, payload.data(), payload.size());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t returnPointer = executor.executeGuestFunction(server.function,
|
||||
request.function,
|
||||
server.buffer,
|
||||
request.send.size,
|
||||
0u,
|
||||
server.gp);
|
||||
if (returnPointer == 0u)
|
||||
returnPointer = server.buffer;
|
||||
if (request.receive.address != 0u && request.receive.size != 0u && returnPointer != 0u)
|
||||
{
|
||||
const uint32_t physical = IopMemory::physicalAddress(returnPointer);
|
||||
if (physical < IopMemory::RamSize)
|
||||
{
|
||||
const uint32_t copySize = std::min<uint32_t>(request.receive.size, IopMemory::RamSize - physical);
|
||||
(void)m_host.writeGuest(request.receive.address, m_memory.ram().data() + physical, copySize);
|
||||
if (copySize < request.receive.size)
|
||||
(void)m_host.zeroGuest(request.receive.address + copySize, request.receive.size - copySize);
|
||||
}
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
result.serverDispatchPolicy = ServerDispatchPolicy::Suppress;
|
||||
result.signalNowaitCompletion = true;
|
||||
result.signalCompletion = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
void IopRpcBridge::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
// The EE SIF transport owns the actual directional memory movement.
|
||||
// Services still receive both phases through IopSubsystem, but mirroring
|
||||
// IOP bytes through an equal-numbered EE address would alias two distinct
|
||||
// PS2 address spaces and can overwrite live game data.
|
||||
(void)transfer;
|
||||
}
|
||||
|
||||
void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size)
|
||||
{
|
||||
for (auto server = m_servers.begin(); server != m_servers.end();)
|
||||
{
|
||||
const uint32_t function = IopMemory::physicalAddress(server->second.function);
|
||||
if (function >= base && function < base + size)
|
||||
server = m_servers.erase(server);
|
||||
else
|
||||
++server;
|
||||
}
|
||||
}
|
||||
|
||||
bool IopRpcBridge::hasServer(uint32_t sid) const noexcept
|
||||
{
|
||||
const auto server = m_servers.find(sid);
|
||||
return server != m_servers.end() && server->second.function != 0u;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopHost;
|
||||
}
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
struct IopCpuState;
|
||||
class IopKernel;
|
||||
class IopMemory;
|
||||
|
||||
class IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
virtual ~IopGuestExecutor() = default;
|
||||
|
||||
[[nodiscard]] virtual uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp) = 0;
|
||||
[[nodiscard]] virtual uint32_t executeGuestFunctionWithBudget(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t a1,
|
||||
uint32_t a2,
|
||||
uint32_t a3,
|
||||
uint32_t gp,
|
||||
uint32_t instructionBudget)
|
||||
{
|
||||
return executeGuestFunction(address, a0, a1, a2, a3, gp);
|
||||
}
|
||||
};
|
||||
|
||||
class IopRpcBridge
|
||||
{
|
||||
public:
|
||||
IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept;
|
||||
|
||||
void reset();
|
||||
[[nodiscard]] bool dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] bool dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu);
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request, IopGuestExecutor &executor);
|
||||
void onSifTransfer(const SifTransfer &transfer);
|
||||
void removeServersInRange(uint32_t base, uint32_t size);
|
||||
|
||||
[[nodiscard]] bool hasServer(uint32_t sid) const noexcept;
|
||||
[[nodiscard]] size_t serverCount() const noexcept { return m_servers.size(); }
|
||||
|
||||
private:
|
||||
struct RpcServer
|
||||
{
|
||||
uint32_t sid = 0;
|
||||
uint32_t serverData = 0;
|
||||
uint32_t function = 0;
|
||||
uint32_t gp = 0;
|
||||
uint32_t buffer = 0;
|
||||
uint32_t callback = 0;
|
||||
uint32_t callbackBuffer = 0;
|
||||
uint32_t queue = 0;
|
||||
};
|
||||
|
||||
IopHost &m_host;
|
||||
IopMemory &m_memory;
|
||||
IopKernel &m_kernel;
|
||||
std::unordered_map<uint32_t, RpcServer> m_servers;
|
||||
uint32_t m_nextDmaId = 1u;
|
||||
bool m_sifInitialized = false;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "iop_module_manager.h"
|
||||
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
IopModuleManager::IopModuleManager()
|
||||
{
|
||||
// ROM modules that the no-BIOS HLE environment can legitimately provide.
|
||||
// Entries with RPC services become routable only after load.
|
||||
constexpr std::string_view modules[] = {
|
||||
"sysmem",
|
||||
"loadcore",
|
||||
"intrman",
|
||||
"sifman",
|
||||
"sifcmd",
|
||||
"sifinit",
|
||||
"ioman",
|
||||
"iomanx",
|
||||
"modload",
|
||||
"stdio",
|
||||
"sysclib",
|
||||
"thbase",
|
||||
"thevent",
|
||||
"thsemap",
|
||||
"thmsgbx",
|
||||
"timrman",
|
||||
"vblank",
|
||||
"secrman",
|
||||
"sio2man",
|
||||
"xsio2man",
|
||||
"sio2d",
|
||||
"padman",
|
||||
"xpadman",
|
||||
"mcman",
|
||||
"xmcman",
|
||||
"mcserv",
|
||||
"libsd",
|
||||
"cdvdman",
|
||||
"cdvdfsv",
|
||||
"dev9",
|
||||
"usbd",
|
||||
"usbhdfsd",
|
||||
"udnl",
|
||||
"fileio",
|
||||
"poweroff",
|
||||
"netman",
|
||||
"ps2ip",
|
||||
"dbcman",
|
||||
"dbcm",
|
||||
};
|
||||
for (const std::string_view module : modules)
|
||||
m_builtinKeys.emplace(module);
|
||||
}
|
||||
|
||||
void IopModuleManager::reset()
|
||||
{
|
||||
m_records.clear();
|
||||
m_hleIdsByKey.clear();
|
||||
m_loadedKeyReferences.clear();
|
||||
m_nextHleId = 0x40000000;
|
||||
}
|
||||
|
||||
void IopModuleManager::setServiceModuleKeys(std::vector<std::string> keys)
|
||||
{
|
||||
m_serviceKeys.clear();
|
||||
for (std::string &key : keys)
|
||||
{
|
||||
const std::string normalized = ps2PathLeafKey(key);
|
||||
if (!normalized.empty())
|
||||
m_serviceKeys.emplace(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult IopModuleManager::loadHle(std::string_view path)
|
||||
{
|
||||
ModuleLoadResult result{true, -1, -1};
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
if (key.empty() || (!m_builtinKeys.contains(key) && !m_serviceKeys.contains(key)))
|
||||
return result;
|
||||
|
||||
const auto existing = m_hleIdsByKey.find(key);
|
||||
if (existing != m_hleIdsByKey.end())
|
||||
{
|
||||
Record &record = m_records[existing->second];
|
||||
++record.references;
|
||||
addLoadedKey(key);
|
||||
result.moduleId = existing->second;
|
||||
result.startResult = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (m_nextHleId <= 0)
|
||||
return result;
|
||||
const int32_t id = m_nextHleId++;
|
||||
m_records.emplace(id, Record{key, 1u, false});
|
||||
m_hleIdsByKey.emplace(key, id);
|
||||
addLoadedKey(key);
|
||||
result.moduleId = id;
|
||||
result.startResult = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void IopModuleManager::observePhysicalLoad(int32_t moduleId, std::string_view path)
|
||||
{
|
||||
if (moduleId <= 0)
|
||||
return;
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
if (key.empty())
|
||||
return;
|
||||
m_records[moduleId] = Record{key, 1u, true};
|
||||
addLoadedKey(key);
|
||||
}
|
||||
|
||||
bool IopModuleManager::stopHle(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
const auto found = m_records.find(moduleId);
|
||||
if (found == m_records.end() || found->second.physical)
|
||||
return false;
|
||||
|
||||
Record &record = found->second;
|
||||
removeLoadedKey(record.key);
|
||||
if (record.references > 1u)
|
||||
{
|
||||
--record.references;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_hleIdsByKey.erase(record.key);
|
||||
m_records.erase(found);
|
||||
}
|
||||
if (result)
|
||||
*result = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IopModuleManager::observePhysicalStop(int32_t moduleId)
|
||||
{
|
||||
const auto found = m_records.find(moduleId);
|
||||
if (found == m_records.end() || !found->second.physical)
|
||||
return;
|
||||
removeLoadedKey(found->second.key);
|
||||
m_records.erase(found);
|
||||
}
|
||||
|
||||
bool IopModuleManager::isLoaded(std::span<const std::string_view> aliases) const
|
||||
{
|
||||
if (aliases.empty())
|
||||
return true;
|
||||
return std::any_of(aliases.begin(), aliases.end(), [&](std::string_view alias)
|
||||
{
|
||||
const std::string key = ps2PathLeafKey(alias);
|
||||
const auto found = m_loadedKeyReferences.find(key);
|
||||
return found != m_loadedKeyReferences.end() && found->second != 0u; });
|
||||
}
|
||||
|
||||
bool IopModuleManager::recognizes(std::string_view path) const
|
||||
{
|
||||
const std::string key = ps2PathLeafKey(path);
|
||||
return m_builtinKeys.contains(key) || m_serviceKeys.contains(key);
|
||||
}
|
||||
|
||||
void IopModuleManager::addLoadedKey(std::string_view key)
|
||||
{
|
||||
++m_loadedKeyReferences[std::string(key)];
|
||||
}
|
||||
|
||||
void IopModuleManager::removeLoadedKey(std::string_view key)
|
||||
{
|
||||
const auto found = m_loadedKeyReferences.find(std::string(key));
|
||||
if (found == m_loadedKeyReferences.end())
|
||||
return;
|
||||
if (found->second > 1u)
|
||||
--found->second;
|
||||
else
|
||||
m_loadedKeyReferences.erase(found);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopModuleManager
|
||||
{
|
||||
public:
|
||||
IopModuleManager();
|
||||
|
||||
void reset();
|
||||
void setServiceModuleKeys(std::vector<std::string> keys);
|
||||
|
||||
[[nodiscard]] ModuleLoadResult loadHle(std::string_view path);
|
||||
void observePhysicalLoad(int32_t moduleId, std::string_view path);
|
||||
[[nodiscard]] bool stopHle(int32_t moduleId, int32_t *result);
|
||||
void observePhysicalStop(int32_t moduleId);
|
||||
|
||||
[[nodiscard]] bool isLoaded(std::span<const std::string_view> aliases) const;
|
||||
[[nodiscard]] bool recognizes(std::string_view path) const;
|
||||
|
||||
private:
|
||||
struct Record
|
||||
{
|
||||
std::string key;
|
||||
uint32_t references = 0u;
|
||||
bool physical = false;
|
||||
};
|
||||
|
||||
void addLoadedKey(std::string_view key);
|
||||
void removeLoadedKey(std::string_view key);
|
||||
|
||||
std::unordered_set<std::string> m_builtinKeys;
|
||||
std::unordered_set<std::string> m_serviceKeys;
|
||||
std::unordered_map<int32_t, Record> m_records;
|
||||
std::unordered_map<std::string, int32_t> m_hleIdsByKey;
|
||||
std::unordered_map<std::string, uint32_t> m_loadedKeyReferences;
|
||||
int32_t m_nextHleId = 0x40000000;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
#include "ps2x/iop/iop_types.h"
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
class IopService
|
||||
{
|
||||
public:
|
||||
virtual ~IopService() = default;
|
||||
|
||||
[[nodiscard]] virtual std::string_view name() const = 0;
|
||||
[[nodiscard]] virtual std::span<const uint32_t> sids() const = 0;
|
||||
// A service with aliases is dormant until one of these IOP modules is
|
||||
// actually loaded.
|
||||
[[nodiscard]] virtual std::span<const std::string_view> moduleAliases() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
virtual void reset() = 0;
|
||||
|
||||
[[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const
|
||||
{
|
||||
(void)request;
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
[[nodiscard]] virtual RpcResult handleRpc(const RpcRequest &request) = 0;
|
||||
|
||||
virtual void onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
(void)transfer;
|
||||
}
|
||||
|
||||
virtual void appendDebugMetrics(std::vector<DebugMetric> &metrics) const
|
||||
{
|
||||
(void)metrics;
|
||||
}
|
||||
};
|
||||
|
||||
using ServiceList = std::vector<std::unique_ptr<IopService>>;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include "iop_service.h"
|
||||
#include "iop_module_manager.h"
|
||||
#include "emulator/iop_emulator.h"
|
||||
#include "module_factories.h"
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
class IopSubsystem::Impl
|
||||
{
|
||||
public:
|
||||
explicit Impl(IopHost &hostRef)
|
||||
: host(hostRef),
|
||||
emulator(hostRef)
|
||||
{
|
||||
coreServices.emplace_back(detail::createMcservService(host));
|
||||
coreServices.emplace_back(detail::createDbcmanService(host));
|
||||
coreServices.emplace_back(detail::createLibSdService(host));
|
||||
refreshServiceModuleKeys();
|
||||
rebuildRoutes();
|
||||
}
|
||||
|
||||
bool serviceActive(const detail::IopService &service) const
|
||||
{
|
||||
return moduleManager.isLoaded(service.moduleAliases());
|
||||
}
|
||||
|
||||
void refreshServiceModuleKeys()
|
||||
{
|
||||
std::vector<std::string> keys;
|
||||
for (const auto &service : coreServices)
|
||||
{
|
||||
for (std::string_view alias : service->moduleAliases())
|
||||
keys.emplace_back(alias);
|
||||
}
|
||||
moduleManager.setServiceModuleKeys(std::move(keys));
|
||||
}
|
||||
|
||||
void rebuildRoutes()
|
||||
{
|
||||
routes.clear();
|
||||
lastError.clear();
|
||||
for (const auto &service : coreServices)
|
||||
{
|
||||
if (!serviceActive(*service))
|
||||
continue;
|
||||
for (const uint32_t sid : service->sids())
|
||||
{
|
||||
if (!routes.emplace(sid, service.get()).second)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "duplicate IOP SID 0x" << std::hex << sid << " in core services";
|
||||
lastError = out.str();
|
||||
routes.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void recordLoadOutcome(std::string_view path, bool hle)
|
||||
{
|
||||
constexpr size_t maxOutcomes = 32u;
|
||||
if (loadOutcomes.size() >= maxOutcomes || !loggedLoadPaths.emplace(path).second)
|
||||
return;
|
||||
std::string message = hle ? "[IOP:HLE] fallback module='" : "[IOP:load-failed] module='";
|
||||
message.append(path);
|
||||
message += hle ? "' physical IRX unavailable; using registered HLE provider"
|
||||
: "' no HLE provider accepted the module; physical IRX was not loaded";
|
||||
loadOutcomes.push_back(message);
|
||||
host.log(hle ? LogLevel::Info : LogLevel::Warning, message);
|
||||
}
|
||||
|
||||
IopHost &host;
|
||||
detail::ServiceList coreServices;
|
||||
std::unordered_map<uint32_t, detail::IopService *> routes;
|
||||
std::vector<std::string> loadOutcomes;
|
||||
std::unordered_set<std::string> loggedLoadPaths;
|
||||
std::string lastError;
|
||||
detail::IopModuleManager moduleManager;
|
||||
detail::IopEmulator emulator;
|
||||
};
|
||||
|
||||
IopSubsystem::IopSubsystem(IopHost &host)
|
||||
: m_impl(std::make_unique<Impl>(host))
|
||||
{
|
||||
}
|
||||
|
||||
IopSubsystem::~IopSubsystem() = default;
|
||||
IopSubsystem::IopSubsystem(IopSubsystem &&) noexcept = default;
|
||||
IopSubsystem &IopSubsystem::operator=(IopSubsystem &&) noexcept = default;
|
||||
|
||||
void IopSubsystem::reset()
|
||||
{
|
||||
m_impl->moduleManager.reset();
|
||||
m_impl->loadOutcomes.clear();
|
||||
m_impl->loggedLoadPaths.clear();
|
||||
for (auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service)
|
||||
{
|
||||
service->reset();
|
||||
}
|
||||
}
|
||||
m_impl->emulator.reset();
|
||||
m_impl->refreshServiceModuleKeys();
|
||||
m_impl->rebuildRoutes();
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
const ParsedPs2Path parsed = parsePs2Path(path);
|
||||
if (!parsed)
|
||||
return {true, -1, -1};
|
||||
|
||||
if (parsed.device != Ps2PathDevice::Rom0)
|
||||
{
|
||||
ModuleLoadResult physical = m_impl->emulator.loadModule(path, arguments, argumentSize);
|
||||
if (physical.moduleId > 0)
|
||||
{
|
||||
m_impl->moduleManager.observePhysicalLoad(physical.moduleId, path);
|
||||
m_impl->rebuildRoutes();
|
||||
return physical;
|
||||
}
|
||||
}
|
||||
|
||||
ModuleLoadResult hle = m_impl->moduleManager.loadHle(path);
|
||||
if (hle.moduleId > 0)
|
||||
{
|
||||
m_impl->rebuildRoutes();
|
||||
if (parsed.device != Ps2PathDevice::Rom0)
|
||||
m_impl->recordLoadOutcome(path, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_impl->recordLoadOutcome(path, false);
|
||||
}
|
||||
return hle;
|
||||
}
|
||||
|
||||
ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize)
|
||||
{
|
||||
return m_impl->emulator.loadModuleBuffer(guestAddress, arguments, argumentSize);
|
||||
}
|
||||
|
||||
bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result)
|
||||
{
|
||||
if (m_impl->moduleManager.stopHle(moduleId, result))
|
||||
{
|
||||
m_impl->rebuildRoutes();
|
||||
return true;
|
||||
}
|
||||
if (!m_impl->emulator.stopModule(moduleId, result))
|
||||
return false;
|
||||
m_impl->moduleManager.observePhysicalStop(moduleId);
|
||||
m_impl->rebuildRoutes();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept
|
||||
{
|
||||
m_impl->emulator.runEeCycles(eeCycles);
|
||||
}
|
||||
|
||||
RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const
|
||||
{
|
||||
for (const auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service && m_impl->serviceActive(*service))
|
||||
{
|
||||
const RpcAbi selected = service->selectRpcAbi(request);
|
||||
if (selected != RpcAbi::RuntimeDefault)
|
||||
{
|
||||
return selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
return RpcAbi::RuntimeDefault;
|
||||
}
|
||||
|
||||
bool IopSubsystem::canBindRpc(uint32_t sid) const noexcept
|
||||
{
|
||||
if (m_impl->routes.find(sid) != m_impl->routes.end())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return m_impl->emulator.hasRpcServer(sid);
|
||||
}
|
||||
|
||||
RpcResult IopSubsystem::handleRpc(const RpcRequest &request)
|
||||
{
|
||||
const auto route = m_impl->routes.find(request.sid);
|
||||
detail::IopService *hle = route != m_impl->routes.end() ? route->second : nullptr;
|
||||
|
||||
RpcResult emulated = m_impl->emulator.handleRpc(request);
|
||||
if (emulated.handled || !hle)
|
||||
{
|
||||
return emulated;
|
||||
}
|
||||
return hle->handleRpc(request);
|
||||
}
|
||||
|
||||
void IopSubsystem::onSifTransfer(const SifTransfer &transfer)
|
||||
{
|
||||
for (auto &service : m_impl->coreServices)
|
||||
{
|
||||
if (service && m_impl->serviceActive(*service))
|
||||
{
|
||||
service->onSifTransfer(transfer);
|
||||
}
|
||||
}
|
||||
m_impl->emulator.onSifTransfer(transfer);
|
||||
}
|
||||
|
||||
uint32_t IopSubsystem::allocateMemory(uint32_t size, uint32_t alignment)
|
||||
{
|
||||
return m_impl->emulator.allocateMemory(size, alignment);
|
||||
}
|
||||
|
||||
bool IopSubsystem::freeMemory(uint32_t address)
|
||||
{
|
||||
return m_impl->emulator.freeMemory(address);
|
||||
}
|
||||
|
||||
bool IopSubsystem::readMemory(uint32_t address, void *destination, size_t size) const
|
||||
{
|
||||
return m_impl->emulator.readMemory(address, destination, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::writeMemory(uint32_t address, const void *source, size_t size)
|
||||
{
|
||||
return m_impl->emulator.writeMemory(address, source, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::zeroMemory(uint32_t address, size_t size)
|
||||
{
|
||||
return m_impl->emulator.zeroMemory(address, size);
|
||||
}
|
||||
|
||||
bool IopSubsystem::isMemoryRange(uint32_t address, size_t size) const
|
||||
{
|
||||
return m_impl->emulator.isMemoryRange(address, size);
|
||||
}
|
||||
|
||||
DebugSnapshot IopSubsystem::debugSnapshot() const
|
||||
{
|
||||
DebugSnapshot snapshot;
|
||||
snapshot.emulatorCycles = m_impl->emulator.cycles();
|
||||
snapshot.emulatorInstructions = m_impl->emulator.instructions();
|
||||
snapshot.emulatorLoadedModules = m_impl->emulator.loadedModuleCount();
|
||||
snapshot.emulatorThreads = m_impl->emulator.threadCount();
|
||||
snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount();
|
||||
snapshot.diagnostics = m_impl->loadOutcomes;
|
||||
if (!m_impl->lastError.empty())
|
||||
{
|
||||
snapshot.diagnostics.push_back(m_impl->lastError);
|
||||
}
|
||||
|
||||
for (const auto &service : m_impl->coreServices)
|
||||
{
|
||||
DebugService row;
|
||||
row.name = service->name();
|
||||
row.sids.assign(service->sids().begin(), service->sids().end());
|
||||
row.active = m_impl->serviceActive(*service);
|
||||
service->appendDebugMetrics(row.metrics);
|
||||
snapshot.services.push_back(std::move(row));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "iop_service.h"
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
std::unique_ptr<IopService> createDbcmanService(IopHost &host);
|
||||
std::unique_ptr<IopService> createLibSdService(IopHost &host);
|
||||
std::unique_ptr<IopService> createMcservService(IopHost &host);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
#include "module_factories.h"
|
||||
#include "rpc_reply.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kDbcManSid = 0x80001300u;
|
||||
constexpr uint32_t kRpcCheckVersion = 0x80001363u;
|
||||
constexpr uint32_t kMaxUnknownRpcLogs = 32u;
|
||||
|
||||
constexpr std::array<uint16_t, 2> kSupportedVersions{0x0310u, 0x0320u};
|
||||
constexpr uint16_t kReportedVersion = kSupportedVersions.front();
|
||||
|
||||
class DbcmanService final : public IopService
|
||||
{
|
||||
public:
|
||||
explicit DbcmanService(IopHost &host)
|
||||
: m_host(host)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return "dbcman";
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return kSids;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
|
||||
{
|
||||
return kModuleAliases;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_unknownRpcLogCount = 0u;
|
||||
m_versionQueryCount = 0u;
|
||||
m_failedVersionReplies = 0u;
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult result;
|
||||
if (request.sid != kDbcManSid)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
if (request.receive.address == 0u || request.receive.size == 0u)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (request.function == kRpcCheckVersion)
|
||||
{
|
||||
const uint32_t version = kReportedVersion;
|
||||
const std::array<uint32_t, 4> reply{version, version, version, version};
|
||||
const bool written = writeRpcWords(m_host, request.receive, reply);
|
||||
bool firstQuery = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
firstQuery = m_versionQueryCount++ == 0u;
|
||||
if (!written)
|
||||
++m_failedVersionReplies;
|
||||
}
|
||||
if (firstQuery)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "[DBCMAN:HLE] check-version reply=0x" << std::hex << version;
|
||||
m_host.log(LogLevel::Info, message.str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool shouldLog = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_unknownRpcLogCount < kMaxUnknownRpcLogs)
|
||||
{
|
||||
++m_unknownRpcLogCount;
|
||||
shouldLog = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldLog)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "[DBCMAN:stub]"
|
||||
<< " sid=0x" << std::hex << request.sid
|
||||
<< " rpc=0x" << request.function
|
||||
<< " send=0x" << request.send.address
|
||||
<< " sendSize=0x" << request.send.size
|
||||
<< " recv=0x" << request.receive.address
|
||||
<< " recvSize=0x" << request.receive.size;
|
||||
m_host.log(LogLevel::Info, message.str());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"reported_version", kReportedVersion, true});
|
||||
metrics.push_back({"version_queries", m_versionQueryCount, false});
|
||||
metrics.push_back({"failed_version_replies", m_failedVersionReplies, false});
|
||||
metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
inline static constexpr std::array<uint32_t, 1> kSids{kDbcManSid};
|
||||
inline static constexpr std::array<std::string_view, 3> kModuleAliases{"dbcman", "dbcm", "dbcmserv"};
|
||||
|
||||
IopHost &m_host;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_unknownRpcLogCount = 0u;
|
||||
uint64_t m_versionQueryCount = 0u;
|
||||
uint64_t m_failedVersionReplies = 0u;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createDbcmanService(IopHost &host)
|
||||
{
|
||||
return std::make_unique<DbcmanService>(host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "module_factories.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kLibSdSid = 0x80000701u;
|
||||
|
||||
class LibSdService final : public IopService
|
||||
{
|
||||
public:
|
||||
explicit LibSdService(IopHost &host)
|
||||
: m_host(host)
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override
|
||||
{
|
||||
return "libsd";
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override
|
||||
{
|
||||
return kSids;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override
|
||||
{
|
||||
return kModuleAliases;
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
if (request.sid != kLibSdSid)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
m_host.audioCommand(request.sid,
|
||||
request.function,
|
||||
request.send,
|
||||
request.receive);
|
||||
|
||||
RpcResult result;
|
||||
result.handled = true;
|
||||
result.resultAddress = request.receive.address;
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
inline static constexpr std::array<uint32_t, 1> kSids{kLibSdSid};
|
||||
inline static constexpr std::array<std::string_view, 1> kModuleAliases{"libsd"};
|
||||
|
||||
IopHost &m_host;
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createLibSdService(IopHost &host)
|
||||
{
|
||||
return std::make_unique<LibSdService>(host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
#include "../iop_service.h"
|
||||
#include "../rpc_reply.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t kMcservSid = 0x80000400u;
|
||||
constexpr uint32_t kMcservDev9Sid = 0x80000480u;
|
||||
constexpr int32_t kSucceeded = 0;
|
||||
constexpr int32_t kDenied = -5;
|
||||
constexpr uint32_t kMcservVersion = 0x0205u;
|
||||
constexpr uint32_t kMcmanVersion = 0x0206u;
|
||||
constexpr uint32_t kCreateDirectory = 0x0040u;
|
||||
|
||||
enum class Operation
|
||||
{
|
||||
Init,
|
||||
GetInfo,
|
||||
Open,
|
||||
Close,
|
||||
Seek,
|
||||
Read,
|
||||
Write,
|
||||
Flush,
|
||||
Chdir,
|
||||
GetDir,
|
||||
SetInfo,
|
||||
Delete,
|
||||
Format,
|
||||
Unformat,
|
||||
GetEnt,
|
||||
ChangePriority,
|
||||
Unknown,
|
||||
};
|
||||
|
||||
enum class Flavor
|
||||
{
|
||||
OldMcserv,
|
||||
NewXmcserv,
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct DescriptorParameter
|
||||
{
|
||||
int32_t fd;
|
||||
int32_t port;
|
||||
int32_t slot;
|
||||
int32_t size;
|
||||
int32_t offset;
|
||||
int32_t origin;
|
||||
uint32_t buffer;
|
||||
uint32_t parameter;
|
||||
uint8_t data[16];
|
||||
};
|
||||
|
||||
struct NameParameter
|
||||
{
|
||||
int32_t port;
|
||||
int32_t slot;
|
||||
int32_t flags;
|
||||
int32_t maxEntries;
|
||||
uint32_t pointer;
|
||||
char name[1024];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static_assert(sizeof(DescriptorParameter) == 48u);
|
||||
static_assert(sizeof(NameParameter) == 1044u);
|
||||
|
||||
Operation decodeOperation(uint32_t function, Flavor &flavor)
|
||||
{
|
||||
flavor = Flavor::NewXmcserv;
|
||||
switch (function)
|
||||
{
|
||||
case 0xFEu:
|
||||
return Operation::Init;
|
||||
case 0x01u:
|
||||
return Operation::GetInfo;
|
||||
case 0x02u:
|
||||
return Operation::Open;
|
||||
case 0x03u:
|
||||
return Operation::Close;
|
||||
case 0x04u:
|
||||
return Operation::Seek;
|
||||
case 0x05u:
|
||||
return Operation::Read;
|
||||
case 0x06u:
|
||||
return Operation::Write;
|
||||
case 0x0Au:
|
||||
return Operation::Flush;
|
||||
case 0x0Cu:
|
||||
return Operation::Chdir;
|
||||
case 0x0Du:
|
||||
return Operation::GetDir;
|
||||
case 0x0Eu:
|
||||
return Operation::SetInfo;
|
||||
case 0x0Fu:
|
||||
return Operation::Delete;
|
||||
case 0x10u:
|
||||
return Operation::Format;
|
||||
case 0x11u:
|
||||
return Operation::Unformat;
|
||||
case 0x12u:
|
||||
return Operation::GetEnt;
|
||||
case 0x14u:
|
||||
return Operation::ChangePriority;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
flavor = Flavor::OldMcserv;
|
||||
switch (function)
|
||||
{
|
||||
case 0x70u:
|
||||
return Operation::Init;
|
||||
case 0x71u:
|
||||
return Operation::Open;
|
||||
case 0x72u:
|
||||
return Operation::Close;
|
||||
case 0x73u:
|
||||
return Operation::Read;
|
||||
case 0x74u:
|
||||
return Operation::Write;
|
||||
case 0x75u:
|
||||
return Operation::Seek;
|
||||
case 0x76u:
|
||||
return Operation::GetDir;
|
||||
case 0x77u:
|
||||
return Operation::Format;
|
||||
case 0x78u:
|
||||
return Operation::GetInfo;
|
||||
case 0x79u:
|
||||
return Operation::Delete;
|
||||
case 0x7Au:
|
||||
return Operation::Flush;
|
||||
case 0x7Bu:
|
||||
return Operation::Chdir;
|
||||
case 0x7Cu:
|
||||
return Operation::SetInfo;
|
||||
case 0x80u:
|
||||
return Operation::Unformat;
|
||||
default:
|
||||
return Operation::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
bool usesNameParameter(Operation operation)
|
||||
{
|
||||
return operation == Operation::Open || operation == Operation::Chdir ||
|
||||
operation == Operation::GetDir || operation == Operation::SetInfo ||
|
||||
operation == Operation::Delete || operation == Operation::GetEnt;
|
||||
}
|
||||
|
||||
class McservService final : public IopService
|
||||
{
|
||||
public:
|
||||
explicit McservService(IopHost &host) : m_host(host) {}
|
||||
|
||||
[[nodiscard]] std::string_view name() const override { return "MCSERV"; }
|
||||
[[nodiscard]] std::span<const uint32_t> sids() const override { return m_sids; }
|
||||
[[nodiscard]] std::span<const std::string_view> moduleAliases() const override { return m_moduleAliases; }
|
||||
|
||||
void reset() override
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_unknownRpcLogCount = 0u;
|
||||
}
|
||||
(void)call(MemoryCardOperation::Init);
|
||||
}
|
||||
|
||||
[[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override
|
||||
{
|
||||
RpcResult response{};
|
||||
response.handled = true;
|
||||
response.resultAddress = request.receive.address;
|
||||
|
||||
Flavor flavor = Flavor::NewXmcserv;
|
||||
const Operation operation = decodeOperation(request.function, flavor);
|
||||
if (operation == Operation::Init)
|
||||
{
|
||||
const int32_t result = call(MemoryCardOperation::Init);
|
||||
writeInitResult(request.receive, flavor, result);
|
||||
return response;
|
||||
}
|
||||
|
||||
int32_t result = kDenied;
|
||||
if (operation == Operation::Unknown)
|
||||
{
|
||||
logUnknown(request);
|
||||
writeResult(request.receive, result);
|
||||
return response;
|
||||
}
|
||||
|
||||
if (usesNameParameter(operation))
|
||||
{
|
||||
NameParameter parameter{};
|
||||
if (request.send.address != 0u &&
|
||||
request.send.size >= sizeof(parameter) &&
|
||||
m_host.readGuest(request.send.address, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
result = handleNameOperation(operation, request.send.address, parameter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DescriptorParameter parameter{};
|
||||
if (request.send.address != 0u &&
|
||||
request.send.size >= sizeof(parameter) &&
|
||||
m_host.readGuest(request.send.address, ¶meter, sizeof(parameter)))
|
||||
{
|
||||
if (operation == Operation::Write && parameter.origin > 0 &&
|
||||
parameter.origin <= static_cast<int32_t>(sizeof(parameter.data)))
|
||||
{
|
||||
const uint32_t inlineAddress = request.send.address + static_cast<uint32_t>(offsetof(DescriptorParameter, data));
|
||||
const int32_t prefix = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), inlineAddress, static_cast<uint32_t>(parameter.origin));
|
||||
if (prefix < 0)
|
||||
{
|
||||
result = prefix;
|
||||
}
|
||||
else
|
||||
{
|
||||
const int32_t body = call(MemoryCardOperation::Write, static_cast<uint32_t>(parameter.fd), parameter.buffer, static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
result = body < 0 ? body : prefix + body;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = handleDescriptorOperation(operation, flavor, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writeResult(request.receive, result);
|
||||
return response;
|
||||
}
|
||||
|
||||
void appendDebugMetrics(std::vector<DebugMetric> &metrics) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false});
|
||||
}
|
||||
|
||||
private:
|
||||
int32_t call(MemoryCardOperation operation,
|
||||
uint32_t a0 = 0u,
|
||||
uint32_t a1 = 0u,
|
||||
uint32_t a2 = 0u,
|
||||
uint32_t a3 = 0u,
|
||||
uint32_t stackArgument = 0u)
|
||||
{
|
||||
return m_host.memoryCard({operation, {a0, a1, a2, a3, stackArgument}});
|
||||
}
|
||||
|
||||
void writeResult(GuestBuffer receive, int32_t result)
|
||||
{
|
||||
const std::array<uint32_t, 1> values{static_cast<uint32_t>(result)};
|
||||
(void)writeRpcWords(m_host, receive, values);
|
||||
}
|
||||
|
||||
void writeInitResult(GuestBuffer receive, Flavor flavor, int32_t result)
|
||||
{
|
||||
const std::array<uint32_t, 3> values = {static_cast<uint32_t>(result), kMcservVersion, kMcmanVersion};
|
||||
const size_t count = flavor == Flavor::NewXmcserv ? values.size() : 1u;
|
||||
(void)writeRpcWords(m_host, receive, std::span<const uint32_t>(values.data(), count));
|
||||
}
|
||||
|
||||
int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter ¶meter)
|
||||
{
|
||||
const uint32_t nameAddress = sendAddress + static_cast<uint32_t>(offsetof(NameParameter, name));
|
||||
const uint32_t port = static_cast<uint32_t>(parameter.port);
|
||||
const uint32_t slot = static_cast<uint32_t>(parameter.slot);
|
||||
switch (operation)
|
||||
{
|
||||
case Operation::Open:
|
||||
if ((static_cast<uint32_t>(parameter.flags) & kCreateDirectory) != 0u)
|
||||
{
|
||||
return call(MemoryCardOperation::Mkdir, port, slot, nameAddress);
|
||||
}
|
||||
return call(MemoryCardOperation::Open, port, slot, nameAddress, static_cast<uint32_t>(parameter.flags));
|
||||
case Operation::Chdir:
|
||||
return call(MemoryCardOperation::Chdir, port, slot, nameAddress, parameter.pointer);
|
||||
case Operation::SetInfo:
|
||||
return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress);
|
||||
case Operation::Delete:
|
||||
return call(MemoryCardOperation::Delete, port, slot, nameAddress);
|
||||
case Operation::GetDir:
|
||||
// MCSERV normally DMA-writes entries. The existing HLE returns
|
||||
// zero entries instead of fabricating directory contents.
|
||||
return kSucceeded;
|
||||
case Operation::GetEnt:
|
||||
return 1024;
|
||||
default:
|
||||
return kDenied;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter ¶meter)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case Operation::GetInfo:
|
||||
{
|
||||
uint32_t typeAddress = 0u;
|
||||
uint32_t freeAddress = 0u;
|
||||
uint32_t formatAddress = 0u;
|
||||
if (parameter.parameter != 0u)
|
||||
{
|
||||
const uint32_t outputSize = flavor == Flavor::NewXmcserv ? 192u : 64u;
|
||||
(void)m_host.zeroGuest(parameter.parameter, outputSize);
|
||||
typeAddress = parameter.parameter;
|
||||
freeAddress = parameter.parameter + 4u;
|
||||
if (flavor == Flavor::NewXmcserv)
|
||||
{
|
||||
formatAddress = parameter.parameter + 144u;
|
||||
}
|
||||
}
|
||||
return call(MemoryCardOperation::GetInfo,
|
||||
static_cast<uint32_t>(parameter.port),
|
||||
static_cast<uint32_t>(parameter.slot),
|
||||
typeAddress,
|
||||
freeAddress,
|
||||
formatAddress);
|
||||
}
|
||||
case Operation::Close:
|
||||
return call(MemoryCardOperation::Close, static_cast<uint32_t>(parameter.fd));
|
||||
case Operation::Seek:
|
||||
return call(MemoryCardOperation::Seek,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
static_cast<uint32_t>(parameter.offset),
|
||||
static_cast<uint32_t>(parameter.origin));
|
||||
case Operation::Read:
|
||||
if (parameter.parameter != 0u)
|
||||
{
|
||||
(void)m_host.zeroGuest(parameter.parameter, flavor == Flavor::NewXmcserv ? 192u : 64u);
|
||||
}
|
||||
return call(MemoryCardOperation::Read,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
parameter.buffer,
|
||||
static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
case Operation::Write:
|
||||
return call(MemoryCardOperation::Write,
|
||||
static_cast<uint32_t>(parameter.fd),
|
||||
parameter.buffer,
|
||||
static_cast<uint32_t>(std::max(parameter.size, 0)));
|
||||
case Operation::Flush:
|
||||
return call(MemoryCardOperation::Flush, static_cast<uint32_t>(parameter.fd));
|
||||
case Operation::Format:
|
||||
return call(MemoryCardOperation::Format,
|
||||
static_cast<uint32_t>(parameter.port),
|
||||
static_cast<uint32_t>(parameter.slot));
|
||||
case Operation::Unformat:
|
||||
return call(MemoryCardOperation::Unformat,
|
||||
static_cast<uint32_t>(parameter.port),
|
||||
static_cast<uint32_t>(parameter.slot));
|
||||
case Operation::ChangePriority:
|
||||
return kSucceeded;
|
||||
default:
|
||||
return kDenied;
|
||||
}
|
||||
}
|
||||
|
||||
void logUnknown(const RpcRequest &request)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
if (m_unknownRpcLogCount >= 32u)
|
||||
{
|
||||
return;
|
||||
}
|
||||
++m_unknownRpcLogCount;
|
||||
}
|
||||
std::ostringstream message;
|
||||
message << "MCSERV unknown RPC sid=0x" << std::hex << request.sid
|
||||
<< " function=0x" << request.function
|
||||
<< " send=0x" << request.send.address
|
||||
<< " size=0x" << request.send.size;
|
||||
m_host.log(LogLevel::Warning, message.str());
|
||||
}
|
||||
|
||||
IopHost &m_host;
|
||||
mutable std::mutex m_mutex;
|
||||
uint32_t m_unknownRpcLogCount = 0u;
|
||||
const std::array<uint32_t, 2> m_sids = {kMcservSid, kMcservDev9Sid};
|
||||
const std::array<std::string_view, 2> m_moduleAliases = {"mcserv", "xmcserv"};
|
||||
};
|
||||
}
|
||||
|
||||
std::unique_ptr<IopService> createMcservService(IopHost &host)
|
||||
{
|
||||
return std::make_unique<McservService>(host);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "ps2x/iop/ps2_path.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace ps2x::iop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::string lowerAscii(std::string_view value)
|
||||
{
|
||||
std::string result(value);
|
||||
std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch)
|
||||
{ return static_cast<char>(std::tolower(ch)); });
|
||||
return result;
|
||||
}
|
||||
|
||||
void normalizeSuffix(std::string &suffix)
|
||||
{
|
||||
std::replace(suffix.begin(), suffix.end(), '\\', '/');
|
||||
while (!suffix.empty() && suffix.front() == '/')
|
||||
suffix.erase(suffix.begin());
|
||||
|
||||
const size_t semicolon = suffix.rfind(';');
|
||||
if (semicolon == std::string::npos || semicolon + 1u == suffix.size())
|
||||
return;
|
||||
|
||||
const bool numeric = std::all_of(suffix.begin() + static_cast<std::ptrdiff_t>(semicolon + 1u),
|
||||
suffix.end(),
|
||||
[](unsigned char ch)
|
||||
{ return std::isdigit(ch) != 0; });
|
||||
if (numeric)
|
||||
suffix.erase(semicolon);
|
||||
}
|
||||
}
|
||||
|
||||
ParsedPs2Path parsePs2Path(std::string_view value)
|
||||
{
|
||||
ParsedPs2Path result;
|
||||
if (value.empty())
|
||||
return result;
|
||||
|
||||
const std::string lower = lowerAscii(value);
|
||||
size_t prefixLength = 0u;
|
||||
if (lower.rfind("host0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Host;
|
||||
result.deviceName = "host0";
|
||||
prefixLength = 6u;
|
||||
}
|
||||
else if (lower.rfind("host:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Host;
|
||||
result.deviceName = "host";
|
||||
prefixLength = 5u;
|
||||
}
|
||||
else if (lower.rfind("cdrom0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom0";
|
||||
prefixLength = 7u;
|
||||
}
|
||||
else if (lower.rfind("cdrom:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom";
|
||||
prefixLength = 6u;
|
||||
}
|
||||
else if (lower.rfind("mc0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::MemoryCard0;
|
||||
result.deviceName = "mc0";
|
||||
prefixLength = 4u;
|
||||
}
|
||||
else if (lower.rfind("rom0:", 0u) == 0u)
|
||||
{
|
||||
result.device = Ps2PathDevice::Rom0;
|
||||
result.deviceName = "rom0";
|
||||
prefixLength = 5u;
|
||||
}
|
||||
else if (value.size() > 2u && std::isalpha(static_cast<unsigned char>(value[0])) &&
|
||||
value[1] == ':' && (value[2] == '/' || value[2] == '\\'))
|
||||
{
|
||||
result.device = Ps2PathDevice::NativeHost;
|
||||
result.deviceName = "native";
|
||||
}
|
||||
else if (value.find(':') != std::string_view::npos)
|
||||
{
|
||||
// TODO maybe log an error here, but don't fail the parse. This is a non-standard device name.
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.device = Ps2PathDevice::Cdrom;
|
||||
result.deviceName = "cdrom0";
|
||||
}
|
||||
|
||||
result.path.assign(value.substr(prefixLength));
|
||||
if (result.device != Ps2PathDevice::NativeHost)
|
||||
normalizeSuffix(result.path);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ps2PathLeafKey(const ParsedPs2Path &parsed)
|
||||
{
|
||||
if (!parsed)
|
||||
return {};
|
||||
std::string path = parsed.path;
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
const size_t slash = path.find_last_of('/');
|
||||
if (slash != std::string::npos)
|
||||
path.erase(0u, slash + 1u);
|
||||
path = lowerAscii(path);
|
||||
if (path.size() > 4u && path.ends_with(".irx"))
|
||||
path.resize(path.size() - 4u);
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string ps2PathLeafKey(std::string_view path)
|
||||
{
|
||||
return ps2PathLeafKey(parsePs2Path(path));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <span>
|
||||
|
||||
namespace ps2x::iop::detail
|
||||
{
|
||||
[[nodiscard]] inline bool writeRpcWords(IopHost &host, GuestBuffer receive, std::span<const uint32_t> words)
|
||||
{
|
||||
const size_t count = std::min<size_t>(receive.size / sizeof(uint32_t), words.size());
|
||||
const size_t bytes = count * sizeof(uint32_t);
|
||||
if (receive.address == 0u || bytes == 0u)
|
||||
return false;
|
||||
if (bytes - 1u > std::numeric_limits<uint32_t>::max() - receive.address)
|
||||
return false;
|
||||
return host.writeGuest(receive.address, words.data(), bytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
#pragma once
|
||||
|
||||
#include "ps2x/iop/iop_subsystem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace iop_test
|
||||
{
|
||||
using namespace ps2x::iop;
|
||||
|
||||
inline void require(bool condition, const char *message)
|
||||
{
|
||||
if (!condition)
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
class Host final : public IopHost
|
||||
{
|
||||
public:
|
||||
explicit Host(size_t bytes = 0x20000u) : guest(bytes, 0xCCu) {}
|
||||
|
||||
bool readGuest(uint32_t address, void *destination, size_t size) const override
|
||||
{
|
||||
if ((size != 0u && !destination) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(destination, guest.data() + address, size);
|
||||
++guestReads;
|
||||
return true;
|
||||
}
|
||||
bool writeGuest(uint32_t address, const void *source, size_t size) override
|
||||
{
|
||||
if ((size != 0u && !source) || address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
if (size != 0u)
|
||||
std::memcpy(guest.data() + address, source, size);
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool zeroGuest(uint32_t address, size_t size) override
|
||||
{
|
||||
if (address > guest.size() || size > guest.size() - address)
|
||||
return false;
|
||||
std::fill_n(guest.begin() + address, size, uint8_t{0});
|
||||
++guestWrites;
|
||||
return true;
|
||||
}
|
||||
bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override
|
||||
{
|
||||
normalized = address;
|
||||
return address < guest.size();
|
||||
}
|
||||
uint32_t allocateIopHandle(IopHandleKind) override { return nextHandle += 0x80u; }
|
||||
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
|
||||
void freeGuest(uint32_t) override {}
|
||||
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override { ++audioCalls; }
|
||||
std::string hostPath(HostPathKind) const override { return {}; }
|
||||
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
|
||||
uint64_t openHostFile(std::string_view) override { return file.empty() ? 0u : 1u; }
|
||||
bool hostFileSize(uint64_t handle, uint64_t &size) const override
|
||||
{
|
||||
size = file.size();
|
||||
return handle == 1u && !file.empty();
|
||||
}
|
||||
bool readHostFile(uint64_t handle, uint64_t offset, void *destination, size_t size,
|
||||
size_t &bytesRead) override
|
||||
{
|
||||
bytesRead = 0u;
|
||||
if (handle != 1u || offset > file.size())
|
||||
return false;
|
||||
bytesRead = std::min(size, file.size() - static_cast<size_t>(offset));
|
||||
if (bytesRead != 0u)
|
||||
std::memcpy(destination, file.data() + offset, bytesRead);
|
||||
return true;
|
||||
}
|
||||
void closeHostFile(uint64_t) override {}
|
||||
int32_t memoryCard(const MemoryCardRequest &request) override
|
||||
{
|
||||
cardCalls.push_back(request);
|
||||
return request.operation == MemoryCardOperation::Init ? initResult : 0;
|
||||
}
|
||||
bool hasGuestFunction(uint32_t) const override { return false; }
|
||||
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override
|
||||
{
|
||||
return false;
|
||||
}
|
||||
void log(LogLevel, std::string_view message) override { logs.emplace_back(message); }
|
||||
|
||||
uint32_t word(uint32_t address) const
|
||||
{
|
||||
uint32_t value = 0u;
|
||||
require(readGuest(address, &value, sizeof(value)), "test read outside guest RAM");
|
||||
return value;
|
||||
}
|
||||
void fill(uint32_t address, size_t size, uint8_t value = 0xCCu)
|
||||
{
|
||||
require(address <= guest.size() && size <= guest.size() - address, "test fill outside RAM");
|
||||
std::fill_n(guest.begin() + address, size, value);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> guest;
|
||||
std::vector<uint8_t> file;
|
||||
std::vector<std::string> logs;
|
||||
std::vector<MemoryCardRequest> cardCalls;
|
||||
mutable size_t guestReads = 0u;
|
||||
size_t guestWrites = 0u;
|
||||
size_t audioCalls = 0u;
|
||||
int32_t initResult = 0;
|
||||
uint32_t nextHandle = 0x1000u;
|
||||
};
|
||||
|
||||
inline RpcRequest request(uint32_t sid, uint32_t function, uint32_t size = 16u)
|
||||
{
|
||||
RpcRequest result{};
|
||||
result.sid = sid;
|
||||
result.function = function;
|
||||
result.receive = {0x800u, size};
|
||||
return result;
|
||||
}
|
||||
|
||||
inline uint64_t metric(const IopSubsystem &iop, std::string_view service, std::string_view name)
|
||||
{
|
||||
for (const auto &row : iop.debugSnapshot().services)
|
||||
if (row.name == service)
|
||||
for (const auto &entry : row.metrics)
|
||||
if (entry.name == name)
|
||||
return entry.value;
|
||||
throw std::runtime_error("missing debug metric");
|
||||
}
|
||||
|
||||
class Irx
|
||||
{
|
||||
public:
|
||||
explicit Irx(uint32_t base = 0x10000u, uint32_t imageBytes = 0x500u)
|
||||
: bytes(0x100u + imageBytes, 0u)
|
||||
{
|
||||
put32(0u, 0x464C457Fu);
|
||||
bytes[4] = bytes[5] = bytes[6] = 1u;
|
||||
put16(16u, 2u);
|
||||
put16(18u, 8u);
|
||||
put32(20u, 1u);
|
||||
put32(24u, base);
|
||||
put32(28u, 52u);
|
||||
put16(40u, 52u);
|
||||
put16(42u, 32u);
|
||||
put16(44u, 1u);
|
||||
put32(52u, 1u);
|
||||
put32(56u, 0x100u);
|
||||
put32(60u, base);
|
||||
put32(64u, base);
|
||||
put32(68u, imageBytes);
|
||||
put32(72u, imageBytes);
|
||||
put32(76u, 7u);
|
||||
put32(80u, 4u);
|
||||
}
|
||||
void words(uint32_t offset, std::initializer_list<uint32_t> values)
|
||||
{
|
||||
for (uint32_t value : values)
|
||||
{
|
||||
put32(0x100u + offset, value);
|
||||
offset += 4u;
|
||||
}
|
||||
}
|
||||
void install(Host &host, uint32_t address = 0x1000u) const
|
||||
{
|
||||
require(host.writeGuest(address, bytes.data(), bytes.size()), "synthetic IRX does not fit");
|
||||
}
|
||||
std::vector<uint8_t> bytes;
|
||||
|
||||
private:
|
||||
void put16(uint32_t offset, uint16_t value)
|
||||
{
|
||||
require(offset + 2u <= bytes.size(), "IRX builder overflow");
|
||||
bytes[offset] = static_cast<uint8_t>(value);
|
||||
bytes[offset + 1u] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
void put32(uint32_t offset, uint32_t value)
|
||||
{
|
||||
put16(offset, static_cast<uint16_t>(value));
|
||||
put16(offset + 2u, static_cast<uint16_t>(value >> 16u));
|
||||
}
|
||||
};
|
||||
|
||||
inline Irx rpcServer(uint32_t sid, uint32_t reply)
|
||||
{
|
||||
Irx image;
|
||||
image.words(0u, {
|
||||
0x27BDFFE0u, 0xAFBF001Cu, // save ra
|
||||
0x3C040001u, 0x34840200u,
|
||||
0x3C050000u | (sid >> 16u), 0x34A50000u | (sid & 0xFFFFu),
|
||||
0x3C060001u, 0x34C60300u,
|
||||
0x3C070001u, 0x34E70400u,
|
||||
0xAFA00010u, 0xAFA00014u, 0xAFA00018u,
|
||||
0x0C00401Du, 0u, // jal 0x10074: sceSifRegisterRpc
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u,
|
||||
});
|
||||
image.words(0x60u, {0x41E00000u, 0u, 0x0101u, 0x63666973u, 0x0000646Du,
|
||||
0x03E00008u, 0x24000011u, 0u, 0u});
|
||||
image.words(0x300u, {0x3C020001u, 0x34420400u, 0x03E00008u, 0u});
|
||||
image.words(0x400u, {reply, reply, reply, reply});
|
||||
return image;
|
||||
}
|
||||
|
||||
struct Test
|
||||
{
|
||||
const char *name;
|
||||
void (*function)();
|
||||
};
|
||||
|
||||
inline int run(std::span<const Test> tests)
|
||||
{
|
||||
size_t failures = 0u;
|
||||
for (const Test &test : tests)
|
||||
{
|
||||
try
|
||||
{
|
||||
test.function();
|
||||
std::cout << "PASS " << test.name << '\n';
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
++failures;
|
||||
std::cerr << "FAIL " << test.name << ": " << error.what() << '\n';
|
||||
}
|
||||
}
|
||||
std::cout << tests.size() - failures << '/' << tests.size() << " cases passed\n";
|
||||
return failures == 0u ? 0 : 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
constexpr uint32_t dbcSid = 0x80001300u;
|
||||
constexpr uint32_t dbcVersion = 0x80001363u;
|
||||
constexpr uint32_t mcSid = 0x80000400u;
|
||||
|
||||
void dbcDefault()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(!iop.canBindRpc(dbcSid), "unloaded DBCMAN must stay dormant");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "DBCMAN load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
require(iop.handleRpc(query).handled, "version RPC not handled");
|
||||
for (uint32_t i = 0u; i < 4u; ++i)
|
||||
require(host.word(0x800u + i * 4u) == 0x0310u, "DBCMAN target version changed");
|
||||
}
|
||||
|
||||
|
||||
|
||||
void dbcResetAndReconfigure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "unexpected DBCMAN version");
|
||||
iop.reset();
|
||||
require(!iop.canBindRpc(dbcSid), "reset retained a module route");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "query counter not reset");
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "reload failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "IOP reboot changed target version");
|
||||
}
|
||||
|
||||
|
||||
void dbcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
for (uint32_t size = 0u; size <= 24u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 40u);
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion, size)).handled, "RPC failed");
|
||||
const uint32_t written = std::min(size / 4u, 4u) * 4u;
|
||||
for (uint32_t offset = written; offset < 32u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "reply wrote past whole-word payload");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "reply underflow");
|
||||
}
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0u;
|
||||
const size_t writes = host.guestWrites;
|
||||
require(iop.handleRpc(query).handled && host.guestWrites == writes, "null reply was written");
|
||||
}
|
||||
|
||||
void dbcNoAddressWrap()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"overflowed reply corrupted low guest addresses");
|
||||
require(metric(iop, "dbcman", "failed_version_replies") == 1u, "invalid reply not recorded");
|
||||
}
|
||||
|
||||
void dbcNoRequestGuessing()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed");
|
||||
auto query = request(dbcSid, dbcVersion);
|
||||
const std::array<uint32_t, 4> randomArguments{0x0310u, 0x00010000u, 0u, 0xFFFFu};
|
||||
require(host.writeGuest(0x600u, randomArguments.data(), sizeof(randomArguments)), "write failed");
|
||||
query.send = {0x600u, sizeof(randomArguments)};
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0x800u) == 0x0310u, "send buffer was guessed to be a requested version");
|
||||
}
|
||||
|
||||
void dbcPhysicalServerWins()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "HLE load failed");
|
||||
auto image = rpcServer(dbcSid, 0xDEADBEEFu);
|
||||
image.install(host);
|
||||
auto physical = iop.loadModuleBuffer(0x1000u);
|
||||
require(physical.moduleId > 0 && physical.startResult == 0, "physical IRX failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "physical RPC not handled");
|
||||
require(host.word(0x800u) == 0xDEADBEEFu, "HLE overwrote physical server version");
|
||||
require(metric(iop, "dbcman", "version_queries") == 0u, "HLE ran after physical service");
|
||||
require(iop.stopModule(physical.moduleId), "physical stop failed");
|
||||
require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "HLE fallback not restored");
|
||||
require(host.word(0x800u) == 0x0310u, "wrong HLE version after physical stop");
|
||||
}
|
||||
|
||||
void mcNewInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:XMCSERV").moduleId > 0, "XMCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0xFEu, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0x0205u && host.word(0x808u) == 0x0206u,
|
||||
"new memory-card init layout changed");
|
||||
require(host.word(0x80Cu) == 0xCCCCCCCCu, "new init wrote beyond 12-byte response");
|
||||
}
|
||||
|
||||
void mcOldInit()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
require(iop.handleRpc(request(mcSid, 0x70u, 16u)).handled, "init RPC failed");
|
||||
require(host.word(0x800u) == 0u && host.word(0x804u) == 0xCCCCCCCCu,
|
||||
"old init leaked extended protocol versions");
|
||||
}
|
||||
|
||||
void mcInitFailure()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
host.initResult = -5;
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
{
|
||||
require(iop.handleRpc(request(mcSid, operation)).handled, "init RPC failed");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "init failure reported as success");
|
||||
}
|
||||
}
|
||||
|
||||
void mcReplyBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t operation : {0x70u, 0xFEu})
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, operation, size)).handled, "init RPC failed");
|
||||
const uint32_t words = operation == 0xFEu ? 3u : 1u;
|
||||
const uint32_t written = std::min(size / 4u, words) * 4u;
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "init clobbered response tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "init underflowed buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFEu);
|
||||
query.receive.address = 0xFFFFFFF8u;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu, "init overflowed guest address");
|
||||
}
|
||||
|
||||
void mcShortNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
auto query = request(mcSid, 0x02u, 4u);
|
||||
query.send = {0x1000u, 20u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
const size_t calls = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.cardCalls.size() == calls, "short packet read a filename beyond send.size");
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "short packet not rejected");
|
||||
}
|
||||
|
||||
void mcFullNamePacket()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
const std::array<uint32_t, 5> header{1u, 0u, 1u, 0u, 0u};
|
||||
host.fill(0x1000u, 1044u, 0u);
|
||||
require(host.writeGuest(0x1000u, header.data(), sizeof(header)), "packet header write failed");
|
||||
constexpr char name[] = "/save.dat";
|
||||
require(host.writeGuest(0x1014u, name, sizeof(name)), "packet filename write failed");
|
||||
for (uint32_t operation : {0x02u, 0x71u})
|
||||
{
|
||||
auto query = request(mcSid, operation, 4u);
|
||||
query.send = {0x1000u, 1044u};
|
||||
const size_t before = host.cardCalls.size();
|
||||
require(iop.handleRpc(query).handled, "open RPC failed");
|
||||
require(host.cardCalls.size() == before + 1u, "valid packet not dispatched");
|
||||
const auto &call = host.cardCalls.back();
|
||||
require(call.operation == MemoryCardOperation::Open &&
|
||||
call.arguments[0] == 1u && call.arguments[1] == 0u &&
|
||||
call.arguments[2] == 0x1014u && call.arguments[3] == 1u,
|
||||
"valid name packet decoded incorrectly");
|
||||
}
|
||||
}
|
||||
|
||||
void mcStatusBounds()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed");
|
||||
for (uint32_t size = 0u; size <= 20u; ++size)
|
||||
{
|
||||
host.fill(0x7FCu, 32u);
|
||||
require(iop.handleRpc(request(mcSid, 0xFFFFFFFFu, size)).handled, "RPC failed");
|
||||
const uint32_t written = size >= 4u ? 4u : 0u;
|
||||
if (written != 0u)
|
||||
require(static_cast<int32_t>(host.word(0x800u)) == -5, "missing error status");
|
||||
for (uint32_t offset = written; offset < 24u; ++offset)
|
||||
require(host.guest[0x800u + offset] == 0xCCu, "status clobbered receive tail");
|
||||
require(host.word(0x7FCu) == 0xCCCCCCCCu, "status underflowed receive buffer");
|
||||
}
|
||||
auto query = request(mcSid, 0xFFFFFFFFu, 16u);
|
||||
query.receive.address = 0xFFFFFFFCu;
|
||||
require(iop.handleRpc(query).handled, "RPC failed");
|
||||
require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu,
|
||||
"status reply wrapped and zeroed low guest memory");
|
||||
}
|
||||
|
||||
void moduleAliases()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *path : {"rom0:XSIO2MAN", "rom0:XPADMAN", "rom0:XMCMAN"})
|
||||
{
|
||||
auto result = iop.loadModule(path);
|
||||
require(result.moduleId > 0 && result.startResult == 0, "known extended module rejected");
|
||||
}
|
||||
require(!iop.canBindRpc(mcSid), "XMCMAN alone enabled a memory-card RPC server");
|
||||
const auto module = iop.loadModule("CDROM0:\\IOP\\xMcSeRv.IrX;1");
|
||||
require(module.moduleId > 0 && iop.canBindRpc(mcSid), "normalized XMCSERV alias not activated");
|
||||
require(iop.stopModule(module.moduleId) && !iop.canBindRpc(mcSid), "stopped alias remained active");
|
||||
}
|
||||
|
||||
void unknownModulesStayUnknown()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
for (const char *name : {"MC2_D.IRX", "DS2U_D.IRX", "CDVDSTM.IRX", "SDRDRV.IRX", "EZPCM.IRX", "ANYTHING_D.IRX"})
|
||||
{
|
||||
const auto result = iop.loadModule(std::string("host0:IOPModules/") + name);
|
||||
require(result.moduleId < 0 && result.startResult < 0, "unsupported module got a fake success");
|
||||
}
|
||||
require(!iop.canBindRpc(0x19740512u), "game-specific SDRDRV activated globally");
|
||||
}
|
||||
|
||||
void moduleLifetime()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
const auto a = iop.loadModule("rom0:DBCMAN");
|
||||
const auto b = iop.loadModule("rom0:dbcman.irx");
|
||||
const auto alias = iop.loadModule("rom0:DBCM");
|
||||
require(a.moduleId > 0 && a.moduleId == b.moduleId && alias.moduleId > 0, "module IDs unstable");
|
||||
require(iop.stopModule(a.moduleId) && iop.canBindRpc(dbcSid), "first release removed shared route");
|
||||
require(iop.stopModule(b.moduleId) && iop.canBindRpc(dbcSid), "remaining alias not honored");
|
||||
require(iop.stopModule(alias.moduleId) && !iop.canBindRpc(dbcSid), "last release retained route");
|
||||
}
|
||||
|
||||
void loaderDiagnostics()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("host0:LIBSD.IRX").moduleId > 0, "LIBSD fallback failed");
|
||||
require(iop.loadModule("host0:MISSING.IRX").moduleId < 0, "unknown load accepted");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("host0:MISSING.IRX");
|
||||
auto snapshot = iop.debugSnapshot();
|
||||
require(snapshot.diagnostics.size() == 2u, "final loader outcomes not deduplicated");
|
||||
require(snapshot.diagnostics[0].find("[IOP:HLE]") != std::string::npos, "no fallback diagnostic");
|
||||
require(snapshot.diagnostics[1].find("no HLE provider") != std::string::npos, "no final failure diagnostic");
|
||||
for (unsigned i = 0u; i < 100u; ++i)
|
||||
(void)iop.loadModule("rom0:missing" + std::to_string(i));
|
||||
require(iop.debugSnapshot().diagnostics.size() <= 32u, "unbounded module diagnostics");
|
||||
iop.reset();
|
||||
require(iop.debugSnapshot().diagnostics.empty(), "stale load outcomes survived reset");
|
||||
}
|
||||
|
||||
void libsdUnchanged()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
require(iop.loadModule("rom0:LIBSD").moduleId > 0, "LIBSD load failed");
|
||||
require(iop.handleRpc(request(0x80000701u, 0x8010u)).handled, "LIBSD RPC not handled");
|
||||
require(host.audioCalls == 1u, "DBCMAN option intercepted LIBSD RPC");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"DBCMAN default and dormant route", dbcDefault},
|
||||
{"DBCMAN reboot and reconfiguration", dbcResetAndReconfigure},
|
||||
{"DBCMAN bounded whole-word response", dbcReplyBounds},
|
||||
{"DBCMAN rejects wrapping reply addresses", dbcNoAddressWrap},
|
||||
{"DBCMAN does not infer version from arbitrary RPC payload", dbcNoRequestGuessing},
|
||||
{"Physical DBCMAN server wins over configured HLE", dbcPhysicalServerWins},
|
||||
{"XMCSERV init status and two version fields", mcNewInit},
|
||||
{"Old MCSERV init is status only", mcOldInit},
|
||||
{"MCSERV propagates initialization failure", mcInitFailure},
|
||||
{"MCSERV response bounds for both dialects", mcReplyBounds},
|
||||
{"MCSERV rejects truncated name packet", mcShortNamePacket},
|
||||
{"MCSERV accepts complete name packets in both dialects", mcFullNamePacket},
|
||||
{"MCSERV status replies preserve bounds and cannot wrap", mcStatusBounds},
|
||||
{"Extended module aliases and activation", moduleAliases},
|
||||
{"Unsupported debug and game IRX stay unsupported", unknownModulesStayUnknown},
|
||||
{"HLE repeated loads and alias lifetime", moduleLifetime},
|
||||
{"Loader outcomes are bounded and resettable", loaderDiagnostics},
|
||||
{"LIBSD audio dispatch is unchanged", libsdUnchanged},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
#include "emulator/iop_emulator.h"
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
// Real guest stores start SPU DMA; intrman invokes the guest handler below.
|
||||
struct DmaFixture
|
||||
{
|
||||
Host host;
|
||||
IopEmulator iop{host};
|
||||
Irx image{0x10000, 0x900};
|
||||
uint32_t offset = 0;
|
||||
std::array<uint32_t, 2> starts{};
|
||||
std::array<uint32_t, 2> delays{};
|
||||
std::array<uint64_t, 2> deadlines{};
|
||||
|
||||
DmaFixture()
|
||||
{
|
||||
emit({0x27bdfff0, 0xafbf000c}); // save ra
|
||||
for (unsigned irq : {0x24u, 0x28u})
|
||||
{
|
||||
emit({0x24040000 | irq, 0x24050000, 0x3c060001, 0x34c60300,
|
||||
0x24070000 | irq, 0x0c004185, 0}); // RegisterIntrHandler
|
||||
emit({0x24040000 | irq, 0x0c004187, 0}); // EnableIntr
|
||||
}
|
||||
image.words(0x600, {0x41e00000, 0, 0x0101, 0x72746e69, 0x006e616d,
|
||||
0x03e00008, 0x24000004, 0x03e00008, 0x24000006, 0, 0});
|
||||
image.words(0x300, {
|
||||
0x3c080001, 0x8d090800, 0, // t1 = completed count (load delay)
|
||||
0x00095080, 0x01485021, 0xad440810, // trace[count] = irq
|
||||
0x25290001, 0xad090800,
|
||||
0x8d090804, 0, 0x1120000c, 0, // optionally rearm DMA from the handler
|
||||
0xad000804, 0x3c091f80, 0x352910c0,
|
||||
0x240a0020, 0xad2a0004, 0x3c0a0100, 0xad2a0008,
|
||||
0x24090050, 0x2529ffff, 0x1520fffe, 0, // handler runs past the new deadline
|
||||
0x03e00008, 0,
|
||||
});
|
||||
}
|
||||
|
||||
void emit(std::initializer_list<uint32_t> words)
|
||||
{
|
||||
image.words(offset, words);
|
||||
offset += uint32_t(words.size()) * 4;
|
||||
}
|
||||
|
||||
void start(unsigned channel, uint32_t delay)
|
||||
{
|
||||
emit({0x3c081f80, channel ? 0x35081500u : 0x350810c0u,
|
||||
0x24090000 | (delay / 2), 0xad090004, 0x3c090100});
|
||||
starts[channel] = offset;
|
||||
delays[channel] = delay;
|
||||
emit({0xad090008});
|
||||
}
|
||||
|
||||
void load()
|
||||
{
|
||||
const std::initializer_list<uint32_t> epilogue{0x8fbf000c, 0x27bd0010, 0x00001021, 0x03e00008, 0};
|
||||
image.words(offset, epilogue);
|
||||
const uint32_t entryEnd = offset + uint32_t(epilogue.size()) * 4;
|
||||
image.install(host);
|
||||
const auto result = iop.loadModuleBuffer(0x1000, nullptr, 0);
|
||||
require(result.moduleId > 0 && result.startResult == 0, "DMA IRX initialization failed");
|
||||
for (unsigned channel = 0; channel < 2; ++channel)
|
||||
if (delays[channel])
|
||||
deadlines[channel] = iop.cycles() - (entryEnd - starts[channel]) / 4 + delays[channel];
|
||||
}
|
||||
|
||||
uint32_t word(uint32_t address)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
require(iop.readMemory(address, &value, sizeof(value)), "IOP trace read failed");
|
||||
return value;
|
||||
}
|
||||
|
||||
void advanceTo(uint64_t cycle)
|
||||
{
|
||||
require(cycle >= iop.cycles(), "test attempted to reverse time");
|
||||
iop.runEeCycles((cycle - iop.cycles()) * 8);
|
||||
}
|
||||
};
|
||||
|
||||
void dmaDeadlines()
|
||||
{
|
||||
DmaFixture f;
|
||||
f.start(0, 1000);
|
||||
f.start(1, 200);
|
||||
f.load();
|
||||
f.advanceTo(f.deadlines[1]);
|
||||
require(f.word(0x10800) == 0, "idle DMA dispatched before its deadline was serviced");
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "earliest DMA did not wake the idle IOP");
|
||||
f.advanceTo(f.deadlines[0]);
|
||||
require(f.word(0x10800) == 1, "later DMA dispatched early");
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24, "later DMA was lost");
|
||||
f.iop.runEeCycles(8000);
|
||||
require(f.word(0x10800) == 2, "DMA completion dispatched twice");
|
||||
}
|
||||
|
||||
void dmaReplacementAndReset()
|
||||
{
|
||||
DmaFixture f;
|
||||
f.start(0, 200);
|
||||
f.start(1, 400);
|
||||
f.start(0, 1000); // replace the earliest event with a later one
|
||||
f.load();
|
||||
f.advanceTo(f.deadlines[1]);
|
||||
require(f.word(0x10800) == 0, "replaced DMA deadline survived");
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "replacement hid the other channel");
|
||||
f.iop.reset();
|
||||
f.iop.runEeCycles(16000);
|
||||
require(f.iop.instructions() == 0, "reset retained a DMA callback");
|
||||
f.load();
|
||||
f.advanceTo(f.deadlines[1]);
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 1, "DMA scheduling did not recover after reset");
|
||||
}
|
||||
|
||||
void dmaReentrantHandler()
|
||||
{
|
||||
DmaFixture f;
|
||||
f.start(0, 200);
|
||||
f.image.words(0x804, {1});
|
||||
f.load();
|
||||
f.advanceTo(f.deadlines[0]);
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 1, "DMA handler recursively dispatched a new completion");
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24,
|
||||
"DMA started inside its handler was lost");
|
||||
}
|
||||
|
||||
void dmaEqualDeadlines()
|
||||
{
|
||||
DmaFixture f;
|
||||
f.start(1, 206);
|
||||
f.start(0, 200); // Six instructions later: same completion cycle.
|
||||
f.load();
|
||||
require(f.deadlines[0] == f.deadlines[1], "fixture deadlines differ");
|
||||
f.advanceTo(f.deadlines[0]);
|
||||
f.iop.runEeCycles(8);
|
||||
require(f.word(0x10800) == 2 && f.word(0x10810) == 0x24 && f.word(0x10814) == 0x28,
|
||||
"simultaneous DMA callbacks changed IRQ order");
|
||||
}
|
||||
|
||||
void dmaWhileExecuting()
|
||||
{
|
||||
DmaFixture f;
|
||||
f.start(0, 200);
|
||||
// Keep guest execution active beyond completion; no idle path is involved.
|
||||
f.emit({0x24100050, 0x2610ffff, 0x1600fffe, 0});
|
||||
f.load();
|
||||
require(f.word(0x10800) == 1 && f.word(0x10810) == 0x24,
|
||||
"DMA did not dispatch during guest execution");
|
||||
}
|
||||
|
||||
void instructionChanges()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopCpuCore core(memory);
|
||||
IopImportRegistry imports(memory);
|
||||
IopCpuState cpu{};
|
||||
const auto step = [&]()
|
||||
{
|
||||
const uint32_t instruction = memory.read32(cpu.pc);
|
||||
require(!imports.decode(cpu.pc, instruction), "ordinary instruction mistaken for an import");
|
||||
return core.executeInstruction(cpu, instruction);
|
||||
};
|
||||
cpu.pc = 0x80001000;
|
||||
memory.write32(0x1000, 0x24020011); // addiu v0, zero, 0x11
|
||||
require(step() && cpu.gpr[2] == 0x11, "first instruction failed");
|
||||
cpu.pc = 0x80001000;
|
||||
memory.write32(0x1000, 0x24020022);
|
||||
require(step() && cpu.gpr[2] == 0x22, "modified instruction was not refetched");
|
||||
cpu.pc = 0x1000;
|
||||
memory.write32(0x1000, 0x8c020800); // lw v0, 0x800(zero)
|
||||
memory.write32(0x1004, 0x24430001); // addiu v1, v0, 1 (sees old v0)
|
||||
memory.write32(0x800, 0x44);
|
||||
require(step() && cpu.gpr[2] == 0x22, "load delay changed");
|
||||
require(step() && cpu.gpr[2] == 0x44 && cpu.gpr[3] == 0x23,
|
||||
"instruction sharing changed load delay semantics");
|
||||
cpu.pc = 0x1000;
|
||||
memory.write32(0x1000, 0x10000001); // beq zero, zero, +1
|
||||
memory.write32(0x1004, 0x24020033);
|
||||
require(step() && cpu.pc == 0x1004 && cpu.branchPending, "branch delay slot was skipped");
|
||||
require(step() && cpu.pc == 0x1008 && cpu.gpr[2] == 0x33, "branch delay slot did not execute");
|
||||
}
|
||||
|
||||
void modifiedImportStub()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
memory.write32(0x1000, 0x41e00000);
|
||||
memory.write16(0x1008, 0x0101);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(0x100c, name, sizeof(name)), "import name write failed");
|
||||
memory.write32(0x1014, 0x03e00008);
|
||||
memory.write32(0x1018, 0x24000003);
|
||||
const auto decode = [&]() { return imports.decode(0x80001014, memory.read32(0x80001014)); };
|
||||
auto call = decode();
|
||||
require(call && call->ordinal == 3 && call->version == 0x0101, "shared fetch lost import metadata");
|
||||
memory.write16(0x1008, 0x0102);
|
||||
memory.write32(0x1018, 0x24000007);
|
||||
call = decode();
|
||||
require(call && call->ordinal == 7 && call->version == 0x0102, "import metadata became stale");
|
||||
memory.write32(0x1014, 0x24020011);
|
||||
require(!decode(), "patched import stub still dispatched as an import");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"DMA deadlines and idle wakeup", dmaDeadlines},
|
||||
{"DMA replacement and reset", dmaReplacementAndReset},
|
||||
{"DMA scheduled from a running IRQ handler", dmaReentrantHandler},
|
||||
{"Simultaneous DMA IRQ order", dmaEqualDeadlines},
|
||||
{"DMA dispatch while guest instructions execute", dmaWhileExecuting},
|
||||
{"Modified instructions and load delay", instructionChanges},
|
||||
{"Modified import stub and metadata", modifiedImportStub},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_kernel.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_cdvd.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
#include "emulator/imports/iop_loadcore.h"
|
||||
#include "emulator/imports/iop_timrman.h"
|
||||
#include "emulator/services/iop_rpc.h"
|
||||
#include "ps2x/iop/iop_host.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace ps2x::iop;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
constexpr uint32_t kExportMagic = 0x41C00000u;
|
||||
constexpr int32_t kLibraryNotFound = -213;
|
||||
constexpr int32_t kIllegalLibrary = -214;
|
||||
|
||||
class NullHost : public IopHost
|
||||
{
|
||||
public:
|
||||
bool readGuest(uint32_t, void *, size_t) const override { return false; }
|
||||
bool writeGuest(uint32_t, const void *, size_t) override { return false; }
|
||||
bool zeroGuest(uint32_t, size_t) override { return false; }
|
||||
bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; }
|
||||
uint32_t allocateIopHandle(IopHandleKind) override { return 1u; }
|
||||
uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; }
|
||||
void freeGuest(uint32_t) override {}
|
||||
void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {}
|
||||
std::string hostPath(HostPathKind) const override { return {}; }
|
||||
std::string translateGuestPath(std::string_view path) const override { return std::string(path); }
|
||||
uint64_t openHostFile(std::string_view) override { return 0u; }
|
||||
bool hostFileSize(uint64_t, uint64_t &) const override { return false; }
|
||||
bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; }
|
||||
void closeHostFile(uint64_t) override {}
|
||||
int32_t memoryCard(const MemoryCardRequest &) override { return 0; }
|
||||
bool hasGuestFunction(uint32_t) const override { return false; }
|
||||
bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; }
|
||||
void log(LogLevel, std::string_view) override {}
|
||||
};
|
||||
|
||||
class CdRootHost final : public NullHost
|
||||
{
|
||||
public:
|
||||
explicit CdRootHost(std::filesystem::path rootPath)
|
||||
: root(std::move(rootPath))
|
||||
{
|
||||
}
|
||||
|
||||
std::string hostPath(HostPathKind kind) const override
|
||||
{
|
||||
return kind == HostPathKind::CdRoot ? root.string() : std::string{};
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path root;
|
||||
};
|
||||
|
||||
class RecordingExecutor final : public IopGuestExecutor
|
||||
{
|
||||
public:
|
||||
uint32_t executeGuestFunction(uint32_t address,
|
||||
uint32_t a0,
|
||||
uint32_t,
|
||||
uint32_t,
|
||||
uint32_t,
|
||||
uint32_t gp) override
|
||||
{
|
||||
++calls;
|
||||
lastAddress = address;
|
||||
lastArgument = a0;
|
||||
lastGp = gp;
|
||||
return callbackResult;
|
||||
}
|
||||
|
||||
uint32_t callbackResult = 0u;
|
||||
uint32_t calls = 0u;
|
||||
uint32_t lastAddress = 0u;
|
||||
uint32_t lastArgument = 0u;
|
||||
uint32_t lastGp = 0u;
|
||||
};
|
||||
|
||||
bool expect(bool condition, std::string_view message)
|
||||
{
|
||||
if (condition)
|
||||
return true;
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
bool testLoadcoreRebootLibraryMode()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
IopLoadcore loadcore(memory, imports);
|
||||
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = 0u;
|
||||
cpu.gpr[5] = 2u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kIllegalLibrary,
|
||||
"loadcore:27 did not reject a null export table"))
|
||||
return false;
|
||||
|
||||
constexpr uint32_t table = 0x1000u;
|
||||
memory.write32(table, kExportMagic);
|
||||
memory.write16(table + 8u, 0x0101u);
|
||||
memory.write16(table + 10u, 0x1234u);
|
||||
const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'};
|
||||
(void)memory.writeRam(table + 12u, name, sizeof(name));
|
||||
memory.write32(table + 20u, 0u);
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = table;
|
||||
cpu.gpr[5] = 2u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") ||
|
||||
!expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") ||
|
||||
!expect(memory.read16(table + 10u) == 0x1232u,
|
||||
"loadcore:27 did not replace only export mode bits 1 and 2"))
|
||||
return false;
|
||||
|
||||
constexpr uint32_t invalidTable = 0x1100u;
|
||||
memory.write32(invalidTable, 0xDEADBEEFu);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = invalidTable;
|
||||
cpu.gpr[5] = 6u;
|
||||
if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) == kLibraryNotFound,
|
||||
"loadcore:27 returned the wrong invalid-table error"))
|
||||
return false;
|
||||
|
||||
if (!expect(imports.registerExportTable(table), "test export table did not register"))
|
||||
return false;
|
||||
memory.write32(table, 0u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = table;
|
||||
cpu.gpr[5] = 6u;
|
||||
return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") &&
|
||||
expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") &&
|
||||
expect(memory.read16(table + 10u) == 0x1236u,
|
||||
"loadcore:27 did not update a registered table's mode");
|
||||
}
|
||||
|
||||
bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected)
|
||||
{
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(eventId);
|
||||
cpu.gpr[5] = bits;
|
||||
cpu.gpr[6] = 0u; // WEF_AND
|
||||
cpu.gpr[7] = resultAddress;
|
||||
return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") &&
|
||||
expect(static_cast<int32_t>(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result");
|
||||
}
|
||||
|
||||
bool testCdvdSpecialControl()
|
||||
{
|
||||
NullHost host;
|
||||
IopMemory memory;
|
||||
IopKernel kernel(memory);
|
||||
kernel.reset();
|
||||
IopCdvd cdvd(host, memory, kernel);
|
||||
cdvd.reset();
|
||||
|
||||
constexpr uint32_t param = 0x2000u;
|
||||
constexpr uint32_t eventResult = 0x2010u;
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-11); // sceCdSC: return cdvdman interrupt event flag
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag"))
|
||||
return false;
|
||||
const int eventId = static_cast<int>(cpu.gpr[2]);
|
||||
|
||||
if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) ||
|
||||
!expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29"))
|
||||
return false;
|
||||
|
||||
IopCpuState clear{};
|
||||
clear.gpr[4] = static_cast<uint32_t>(eventId);
|
||||
clear.gpr[5] = ~0x29u;
|
||||
if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") ||
|
||||
!pollEvent(kernel, eventId, 0x29u, eventResult, -418))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = 0x12345u;
|
||||
if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") ||
|
||||
!pollEvent(kernel, eventId, 0x29u, eventResult, 0))
|
||||
return false;
|
||||
|
||||
memory.write8(param, 0x30u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-2);
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error"))
|
||||
return false;
|
||||
|
||||
memory.write32(param, 0u);
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-1);
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") ||
|
||||
!expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = 2u;
|
||||
cpu.gpr[5] = param;
|
||||
if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") ||
|
||||
!expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = static_cast<uint32_t>(-1);
|
||||
cpu.gpr[5] = param;
|
||||
return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") &&
|
||||
expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state");
|
||||
}
|
||||
|
||||
bool testCdvdSearchFile()
|
||||
{
|
||||
const auto suffix = std::to_string(
|
||||
static_cast<unsigned long long>(std::chrono::steady_clock::now().time_since_epoch().count()));
|
||||
const std::filesystem::path root =
|
||||
std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix);
|
||||
const std::filesystem::path movieDirectory = root / "MOVIE";
|
||||
const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS";
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(movieDirectory, error);
|
||||
if (!expect(!error, "could not create the temporary CD root"))
|
||||
return false;
|
||||
{
|
||||
std::ofstream movie(moviePath, std::ios::binary);
|
||||
movie.write("PSS!", 4);
|
||||
}
|
||||
|
||||
CdRootHost host(root);
|
||||
IopMemory memory;
|
||||
IopKernel kernel(memory);
|
||||
kernel.reset();
|
||||
IopCdvd cdvd(host, memory, kernel);
|
||||
cdvd.reset();
|
||||
|
||||
constexpr uint32_t resultAddress = 0x2400u;
|
||||
constexpr uint32_t pathAddress = 0x2480u;
|
||||
const char path[] = "cdrom0:\\movie\\opening.pss;1";
|
||||
(void)memory.writeRam(pathAddress, path, sizeof(path));
|
||||
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = resultAddress;
|
||||
cpu.gpr[5] = pathAddress;
|
||||
const bool handled = cdvd.dispatchImport(10u, cpu);
|
||||
const bool passed =
|
||||
expect(handled, "cdvdman:10 was not handled") &&
|
||||
expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") &&
|
||||
expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") &&
|
||||
expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") &&
|
||||
expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS",
|
||||
"sceCdSearchFile returned the wrong file name");
|
||||
|
||||
std::filesystem::remove_all(root, error);
|
||||
return passed;
|
||||
}
|
||||
|
||||
bool testKernelSchedulingTransitions()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopKernel kernel(memory);
|
||||
IopCpuState caller{};
|
||||
const auto createThread = [&](uint32_t entry, uint32_t priority)
|
||||
{
|
||||
memory.write32(0x2008, entry);
|
||||
memory.write32(0x200c, 0x100);
|
||||
memory.write32(0x2010, priority);
|
||||
caller.gpr[4] = 0x2000;
|
||||
(void)kernel.dispatchThreadImport(4, caller, 0);
|
||||
const int id = static_cast<int>(caller.gpr[2]);
|
||||
caller.gpr[4] = static_cast<uint32_t>(id);
|
||||
(void)kernel.dispatchThreadImport(6, caller, 0);
|
||||
return id;
|
||||
};
|
||||
const auto select = [&](int id, uint64_t cycle)
|
||||
{
|
||||
IopThread *thread = kernel.beginNextReady(cycle);
|
||||
if (!expect(thread && thread->id == id, "scheduler selected the wrong thread"))
|
||||
return static_cast<IopThread *>(nullptr);
|
||||
return thread;
|
||||
};
|
||||
const auto threadImport = [&](uint16_t ordinal, int id, uint32_t value = 0)
|
||||
{
|
||||
caller.gpr[4] = static_cast<uint32_t>(id);
|
||||
caller.gpr[5] = value;
|
||||
return kernel.dispatchThreadImport(ordinal, caller, 0) && caller.gpr[2] == 0;
|
||||
};
|
||||
constexpr uint32_t sentinel = 0xfffffffcu;
|
||||
const int first = createThread(0x10000, 32);
|
||||
const int second = createThread(0x20000, 64);
|
||||
for (int i = 0; i < 128; ++i)
|
||||
{
|
||||
auto *thread = select(first, 0);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
}
|
||||
if (!threadImport(14, second, 16)) return false;
|
||||
auto *thread = select(second, 0);
|
||||
if (!thread) return false;
|
||||
kernel.delayCurrentUntil(100, thread->cpu);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!expect(kernel.nextWakeCycle(1000) == 100, "delay deadline lost after blocking")) return false;
|
||||
thread = select(first, 99);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
thread = select(second, 100);
|
||||
if (!thread) return false;
|
||||
kernel.sleepCurrent(thread->cpu);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
thread = select(first, 101);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!threadImport(25, second)) return false;
|
||||
thread = select(second, 101);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!threadImport(29, second)) return false;
|
||||
thread = select(first, 101);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!threadImport(31, second)) return false;
|
||||
|
||||
memory.write32(0x202c, 1); // semaphore maximum, initially empty
|
||||
caller.gpr[4] = 0x2020;
|
||||
(void)kernel.dispatchSemaphoreImport(4, caller);
|
||||
const uint32_t semaphore = caller.gpr[2];
|
||||
thread = select(second, 101);
|
||||
if (!thread) return false;
|
||||
thread->cpu.gpr[4] = semaphore;
|
||||
(void)kernel.dispatchSemaphoreImport(8, thread->cpu);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
thread = select(first, 101);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
caller.gpr[4] = semaphore;
|
||||
(void)kernel.dispatchSemaphoreImport(6, caller);
|
||||
thread = select(second, 101);
|
||||
if (!thread) return false;
|
||||
|
||||
const int event = kernel.createInternalEventFlag(0, 0, 0);
|
||||
thread->cpu.gpr[4] = static_cast<uint32_t>(event);
|
||||
thread->cpu.gpr[5] = 1;
|
||||
thread->cpu.gpr[6] = 0;
|
||||
thread->cpu.gpr[7] = 0;
|
||||
(void)kernel.dispatchEventImport(10, thread->cpu);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
thread = select(first, 101);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!kernel.setInternalEventFlag(event, 1)) return false;
|
||||
const int third = createThread(0x30000, 16);
|
||||
thread = select(second, 101); // Equal priorities retain ascending ID order.
|
||||
if (!thread) return false;
|
||||
(void)kernel.dispatchThreadImport(9, thread->cpu, 101);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!expect(kernel.threadCount() == 2, "ExitDeleteThread must not turn Dead into Dormant")) return false;
|
||||
thread = select(third, 101);
|
||||
if (!thread) return false;
|
||||
kernel.terminateThreadsInRange(0x30000, 0x100);
|
||||
kernel.cleanupDeadThreads();
|
||||
if (!expect(kernel.threadCount() == 2 && thread->cpu.stopped, "active CPU reference must survive module unload")) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!expect(kernel.threadCount() == 1, "unloaded thread must retire after its timeslice")) return false;
|
||||
thread = select(first, 101);
|
||||
if (!thread) return false;
|
||||
kernel.delayCurrentUntil(1000, thread->cpu);
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!expect(kernel.beginNextReady(999) == nullptr && kernel.nextWakeCycle(2000) == 1000,
|
||||
"idle scheduling must preserve the next wake deadline")) return false;
|
||||
thread = select(first, 1000);
|
||||
if (!thread) return false;
|
||||
kernel.endTimeslice(*thread, sentinel);
|
||||
if (!threadImport(5, first)) return false;
|
||||
if (!expect(kernel.beginNextReady(1000) == nullptr, "deletion must invalidate the cached ready thread")) return false;
|
||||
kernel.reset();
|
||||
return expect(kernel.beginNextReady(0) == nullptr && kernel.threadCount() == 0,
|
||||
"reset must discard scheduling state");
|
||||
}
|
||||
|
||||
bool testTimrmanPeriodicCallback()
|
||||
{
|
||||
IopTimrman timrman;
|
||||
timrman.reset();
|
||||
IopCpuState cpu{};
|
||||
|
||||
cpu.gpr[4] = 1u; // SYSCLK
|
||||
cpu.gpr[5] = 32u;
|
||||
cpu.gpr[6] = 1u;
|
||||
if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") ||
|
||||
!expect(static_cast<int32_t>(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer"))
|
||||
return false;
|
||||
const uint32_t timerId = cpu.gpr[2];
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
cpu.gpr[5] = 100u;
|
||||
cpu.gpr[6] = 0x12340u;
|
||||
cpu.gpr[7] = 0x45670u;
|
||||
cpu.gpr[28] = 0x89AB0u;
|
||||
if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "SetTimerHandler failed"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
cpu.gpr[5] = 1u;
|
||||
cpu.gpr[6] = 0u;
|
||||
cpu.gpr[7] = 1u;
|
||||
if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "SetupHardTimer failed"))
|
||||
return false;
|
||||
|
||||
cpu = {};
|
||||
cpu.gpr[4] = timerId;
|
||||
if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") ||
|
||||
!expect(cpu.gpr[2] == 0u, "StartHardTimer failed") ||
|
||||
!expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle"))
|
||||
return false;
|
||||
|
||||
RecordingExecutor executor;
|
||||
executor.callbackResult = 100u;
|
||||
timrman.serviceDue(199u, executor);
|
||||
if (!expect(executor.calls == 0u, "timer callback ran too early"))
|
||||
return false;
|
||||
timrman.serviceDue(200u, executor);
|
||||
return expect(executor.calls == 1u, "timer callback did not run") &&
|
||||
expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") &&
|
||||
expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") &&
|
||||
expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") &&
|
||||
expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() ||
|
||||
!testTimrmanPeriodicCallback() || !testKernelSchedulingTransitions())
|
||||
return 1;
|
||||
std::cout << "ps2xIOP import tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "iop_compat_test_support.h"
|
||||
|
||||
#include "emulator/core/iop_cpu.h"
|
||||
#include "emulator/core/iop_memory.h"
|
||||
#include "emulator/imports/iop_imports.h"
|
||||
#include "emulator/imports/iop_loadcore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace iop_test;
|
||||
using namespace ps2x::iop::detail;
|
||||
|
||||
void addExport(IopMemory &memory, IopImportRegistry &imports, uint32_t address,
|
||||
uint16_t version, uint32_t target, uint32_t count = 4u)
|
||||
{
|
||||
require(memory.zeroRam(address, 128u), "export table does not fit");
|
||||
memory.write32(address, 0x41C00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "export name does not fit");
|
||||
for (uint32_t i = 0u; i < count; ++i)
|
||||
memory.write32(address + 20u + 4u * i, target);
|
||||
require(imports.registerExportTable(address), "export registration failed");
|
||||
}
|
||||
|
||||
void importTable(IopMemory &memory, uint32_t address, uint16_t version)
|
||||
{
|
||||
require(memory.zeroRam(address, 64u), "import table does not fit");
|
||||
memory.write32(address, 0x41E00000u);
|
||||
memory.write16(address + 8u, version);
|
||||
constexpr char name[8] = "tstlib";
|
||||
require(memory.writeRam(address + 12u, name, sizeof(name)), "import name does not fit");
|
||||
memory.write32(address + 20u, 0x03E00008u);
|
||||
memory.write32(address + 24u, 0x24000003u);
|
||||
}
|
||||
|
||||
void decodeVersion()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
importTable(memory, 0x1000u, 0x0310u);
|
||||
const auto call = imports.decode(0x1014u);
|
||||
require(call && call->library == "tstlib" && call->ordinal == 3u && call->version == 0x0310u,
|
||||
"decoder dropped the import library version");
|
||||
const auto alias = imports.decode(0x80001014u);
|
||||
require(alias && alias->version == 0x0310u, "cached alias lost import version");
|
||||
}
|
||||
|
||||
void majorIsolation()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "linked to wrong library major");
|
||||
require(imports.resolve("tstlib", 3u, 0x0201u) == 0x2100u, "second major unavailable");
|
||||
require(imports.resolve("tstlib", 3u, 0x0300u) == 0u, "incompatible major silently linked");
|
||||
require(imports.findTable("tstlib", 0x0300u) == 0u, "query ignored requested major");
|
||||
}
|
||||
|
||||
void newestMinor()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0104u, 0x3100u);
|
||||
addExport(memory, imports, 0x1400u, 0x0103u, 0x4100u);
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "selected lowest address, not newest minor");
|
||||
require(imports.resolve("tstlib", 3u, 0x017Fu) == 0x3100u,
|
||||
"invented a minimum-minor rule absent from LOADCORE linking");
|
||||
require(imports.releaseExportTable(0x1800u), "unregister failed");
|
||||
require(imports.resolve("tstlib", 3u, 0x0101u) == 0x4100u, "unregistered library remained selected");
|
||||
}
|
||||
|
||||
void missingOrdinal()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u, 8u);
|
||||
addExport(memory, imports, 0x1800u, 0x0102u, 0x3100u, 4u);
|
||||
require(imports.resolve("tstlib", 7u, 0x0101u) == 0u,
|
||||
"missing ordinal fell back to a different export table");
|
||||
require(imports.resolve("missing", 0u, 0x0101u) == 0u, "missing library resolved");
|
||||
imports.reset();
|
||||
require(imports.resolve("tstlib", 0u, 0x0101u) == 0u, "registry reset left exports");
|
||||
}
|
||||
|
||||
void queryFunctionArray()
|
||||
{
|
||||
IopMemory memory;
|
||||
IopImportRegistry imports(memory);
|
||||
IopLoadcore loadcore(memory, imports);
|
||||
addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u);
|
||||
addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u);
|
||||
importTable(memory, 0x800u, 0x0102u);
|
||||
IopCpuState cpu{};
|
||||
cpu.gpr[4] = 0x800u;
|
||||
require(loadcore.dispatchImport(11u, cpu), "QueryLibraryEntryTable unhandled");
|
||||
require(cpu.gpr[2] == 0x1814u && memory.read32(cpu.gpr[2]) == 0x3100u,
|
||||
"query returned an export header instead of function array");
|
||||
memory.write16(0x808u, 0x0300u);
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "query accepted wrong major");
|
||||
for (uint32_t address : {0u, 0xFFFFFFF8u, IopMemory::RamSize - 4u})
|
||||
{
|
||||
cpu.gpr[4] = address;
|
||||
require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "invalid query pointer accepted");
|
||||
}
|
||||
}
|
||||
|
||||
Irx provider(uint32_t base, uint16_t version, uint32_t result)
|
||||
{
|
||||
Irx image(base);
|
||||
const uint32_t table = base + 0x80u;
|
||||
const uint32_t importStub = base + 0xC0u + 20u;
|
||||
image.words(0u, {0x27BDFFE0u, 0xAFBF001Cu,
|
||||
0x3C040000u | (table >> 16u), 0x34840000u | (table & 0xFFFFu),
|
||||
0x0C000000u | (importStub >> 2u), 0u,
|
||||
0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u});
|
||||
image.words(0x60u, {0x03E00008u, 0x24020000u | result});
|
||||
image.words(0x80u, {0x41C00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
base, base, base, base + 0x60u, 0u});
|
||||
image.words(0xC0u, {0x41E00000u, 0u, 0x0101u, 0x64616F6Cu, 0x65726F63u,
|
||||
0x03E00008u, 0x24000006u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
Irx consumer(uint16_t version)
|
||||
{
|
||||
constexpr uint32_t base = 0x13000u;
|
||||
Irx image(base);
|
||||
image.words(0u, {0x27BDFFF0u, 0xAFBF000Cu,
|
||||
0x0C000000u | ((base + 0x54u) >> 2u), 0u,
|
||||
0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0u});
|
||||
image.words(0x40u, {0x41E00000u, 0u, version, 0x6C747374u, 0x00006269u,
|
||||
0x03E00008u, 0x24000003u, 0u, 0u});
|
||||
return image;
|
||||
}
|
||||
|
||||
void physicalImportsEndToEnd()
|
||||
{
|
||||
Host host;
|
||||
IopSubsystem iop(host);
|
||||
auto wrongMajor = provider(0x10000u, 0x0201u, 0x22u);
|
||||
wrongMajor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 2 failed");
|
||||
auto oldMinor = provider(0x11000u, 0x0101u, 0x11u);
|
||||
oldMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1 failed");
|
||||
auto newMinor = provider(0x12000u, 0x0103u, 0x13u);
|
||||
newMinor.install(host);
|
||||
require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1.3 failed");
|
||||
auto client = consumer(0x0101u);
|
||||
client.install(host);
|
||||
const auto result = iop.loadModuleBuffer(0x1000u);
|
||||
require(result.moduleId > 0 && result.startResult == 0x13, "R3000A called wrong export version");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
const Test tests[] = {
|
||||
{"Import decoder preserves library ABI version", decodeVersion},
|
||||
{"Different major versions cannot cross-link", majorIsolation},
|
||||
{"Newest registered minor wins within the requested major", newestMinor},
|
||||
{"Ordinal lookup stays in the selected table", missingOrdinal},
|
||||
{"LOADCORE query returns function array and honors major", queryFunctionArray},
|
||||
{"Physical IRX consumer links correct version end to end", physicalImportsEndToEnd},
|
||||
};
|
||||
return run(tests);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -136,3 +129,10 @@ install(TARGETS ps2_recomp ps2_recomp_lib
|
||||
install(DIRECTORY include/
|
||||
DESTINATION include
|
||||
)
|
||||
|
||||
include("${CMAKE_SOURCE_DIR}/ps2xRuntime/cmake/ReleaseMode.cmake")
|
||||
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")
|
||||
EnableFastReleaseMode(ps2_recomp_lib)
|
||||
EnableFastReleaseMode(ps2_recomp)
|
||||
endif()
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
[general]
|
||||
# Path to input ELF file
|
||||
input = "path/to/your/ps2_game.elf"
|
||||
|
||||
# Path to output directory
|
||||
output = "output/"
|
||||
|
||||
# Single file output mode (false for one file per function)
|
||||
single_file_output = false
|
||||
|
||||
# Path to runtime header (optional)
|
||||
runtime_header = "include/ps2_runtime.h"
|
||||
|
||||
# Functions to stub (these will generate wrappers to runtime syscall/stub handlers when names match)
|
||||
# You can also bind stripped functions by address with "handler@0xADDRESS".
|
||||
# Generic temporary handlers are available: ret0, ret1, reta0.
|
||||
stubs = [
|
||||
"printf",
|
||||
"malloc",
|
||||
"free",
|
||||
"memcpy",
|
||||
"memset",
|
||||
"strncpy",
|
||||
"sprintf",
|
||||
# "sceCdRead@0x00123456",
|
||||
# "SifLoadModule@0x00127890",
|
||||
# "ret0@0x001D9410",
|
||||
]
|
||||
|
||||
# Functions to skip (these will not be recompiled)
|
||||
skip = ["abort", "exit", "_exit"]
|
||||
|
||||
# Patches to apply during recompilation
|
||||
[patches]
|
||||
# Individual instruction patches
|
||||
instructions = [
|
||||
{ address = "0x100004", value = "0x00000000" }, # NOP an instruction
|
||||
{ address = "0x100104", value = "0x24040000" }, # Change an immediate value
|
||||
]
|
||||
|
||||
# Function hook patches (not yet implemented)
|
||||
[[patches.hook]]
|
||||
function = "printf"
|
||||
code = '''
|
||||
// Custom printf implementation
|
||||
void printf(uint8_t* rdram, R5900Context* ctx) {
|
||||
// Implementation here
|
||||
}
|
||||
'''
|
||||
|
||||
# Function replacement patches (not yet implemented)
|
||||
[[patches.func]]
|
||||
address = "0x100000"
|
||||
code = '''
|
||||
// Custom implementation for function at 0x100000
|
||||
void func_00100000(uint8_t* rdram, R5900Context* ctx) {
|
||||
// Implementation here
|
||||
}
|
||||
'''
|
||||
@@ -0,0 +1,86 @@
|
||||
#ifndef PS2RECOMP_CONTROL_FLOW_EMITTER_H
|
||||
#define PS2RECOMP_CONTROL_FLOW_EMITTER_H
|
||||
|
||||
#include "ps2recomp/code_generator.h"
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
class ControlFlowEmitter
|
||||
{
|
||||
public:
|
||||
ControlFlowEmitter(CodeGenerator &generator,
|
||||
const Instruction &branchInst,
|
||||
const Instruction &delaySlot,
|
||||
const Function &function,
|
||||
const CodeGenerator::AnalysisResult &analysisResult,
|
||||
std::string delaySlotOverride = {});
|
||||
|
||||
std::string emit();
|
||||
|
||||
private:
|
||||
enum class StaticBranchKind
|
||||
{
|
||||
Jump,
|
||||
Call,
|
||||
};
|
||||
|
||||
enum class RegisterBranchKind
|
||||
{
|
||||
Jump,
|
||||
Call,
|
||||
};
|
||||
|
||||
CodeGenerator &m_gen;
|
||||
const Instruction &m_branchInst;
|
||||
const Instruction &m_delaySlot;
|
||||
const Function &m_function;
|
||||
const CodeGenerator::AnalysisResult &m_analysisResult;
|
||||
std::string m_delaySlotOverride;
|
||||
std::stringstream m_ss;
|
||||
|
||||
uint32_t branchPc() const;
|
||||
uint32_t delayPc() const;
|
||||
uint32_t fallthroughPc() const;
|
||||
bool hasRealDelaySlot() const;
|
||||
bool isLikelyBranch() const;
|
||||
bool isCallLikeEdge() const;
|
||||
bool isInternalTarget(uint32_t target) const;
|
||||
std::vector<uint32_t> resolvedLocalIndirectTargets() const;
|
||||
|
||||
std::string delaySlotCode() const;
|
||||
void emitDelaySlot(std::string_view indent);
|
||||
void emitResumeFromDelaySlotEntry();
|
||||
void emitInternalTarget(uint32_t target, uint32_t sourcePc, std::string_view indent);
|
||||
void emitFallthroughLabelIfNeeded();
|
||||
void emitFinalFallthrough();
|
||||
|
||||
void emitStaticJump(StaticBranchKind kind);
|
||||
void emitRegisterJump(RegisterBranchKind kind);
|
||||
void emitConditionalBranch();
|
||||
void emitFallbackInstruction();
|
||||
|
||||
bool emitDirectFunctionJumpIfAvailable(uint32_t target, StaticBranchKind kind, std::string_view indent);
|
||||
void emitExternalJumpDispatch(uint32_t target, StaticBranchKind kind, std::string_view indent);
|
||||
void emitExternalRegisterCallDispatch(std::string_view jumpTargetExpression, std::string_view indent);
|
||||
void emitExternalRegisterJumpDispatch(std::string_view jumpTargetExpression, RegisterBranchKind kind, uint8_t rsReg, std::string_view indent);
|
||||
void emitRuntimeBranchDispatch(std::string_view targetExpression,
|
||||
uint32_t sourcePc,
|
||||
uint32_t returnPc,
|
||||
std::string_view runtimeKind,
|
||||
std::string_view debugName,
|
||||
std::string_view indent,
|
||||
bool returnOnTransfer);
|
||||
bool emitRelocationCallIfAvailable(StaticBranchKind kind, std::string_view indent);
|
||||
std::string conditionalBranchExpression() const;
|
||||
uint32_t conditionalBranchTarget() const;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_CONTROL_FLOW_EMITTER_H
|
||||
@@ -0,0 +1,25 @@
|
||||
#ifndef PS2RECOMP_FUNCTION_EMITTER_H
|
||||
#define PS2RECOMP_FUNCTION_EMITTER_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Function;
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class FunctionEmitter
|
||||
{
|
||||
public:
|
||||
explicit FunctionEmitter(CodeGenerator &codeGenerator);
|
||||
|
||||
std::string emit(const Function &function, const std::vector<Instruction> &instructions, bool useHeaders);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_FUNCTION_EMITTER_H
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef PS2RECOMP_FUNCTION_TABLE_EMITTER_H
|
||||
#define PS2RECOMP_FUNCTION_TABLE_EMITTER_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Function;
|
||||
class CodeGenerator;
|
||||
|
||||
class FunctionTableEmitter
|
||||
{
|
||||
public:
|
||||
explicit FunctionTableEmitter(CodeGenerator &codeGenerator);
|
||||
|
||||
std::string emit(const std::vector<Function> &functions, const std::map<uint32_t, std::string> &stubs);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_FUNCTION_TABLE_EMITTER_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_COP0_TRANSLATOR_H
|
||||
#define PS2RECOMP_COP0_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class Cop0Translator
|
||||
{
|
||||
public:
|
||||
explicit Cop0Translator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_COP0_TRANSLATOR_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_FPU_TRANSLATOR_H
|
||||
#define PS2RECOMP_FPU_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class FpuTranslator
|
||||
{
|
||||
public:
|
||||
explicit FpuTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_FPU_TRANSLATOR_H
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef PS2RECOMP_INSTRUCTION_TRANSLATOR_H
|
||||
#define PS2RECOMP_INSTRUCTION_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ps2recomp/types.h"
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class InstructionTranslator
|
||||
{
|
||||
public:
|
||||
explicit InstructionTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst, const MemoryAccessHint &memoryHint);
|
||||
|
||||
private:
|
||||
MemoryAccessHint effectiveMemoryHintFor(const Instruction &inst, const MemoryAccessHint &memoryHint) const;
|
||||
std::string translateMemoryRead(const Instruction &inst,
|
||||
const MemoryAccessHint &memoryHint,
|
||||
int width,
|
||||
const std::string &addr) const;
|
||||
std::string translateMemoryWrite(const Instruction &inst,
|
||||
const MemoryAccessHint &memoryHint,
|
||||
int width,
|
||||
const std::string &addr,
|
||||
const std::string &value) const;
|
||||
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_INSTRUCTION_TRANSLATOR_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_MMI_TRANSLATOR_H
|
||||
#define PS2RECOMP_MMI_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class MmiTranslator
|
||||
{
|
||||
public:
|
||||
explicit MmiTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_MMI_TRANSLATOR_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_REGIMM_TRANSLATOR_H
|
||||
#define PS2RECOMP_REGIMM_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class RegimmTranslator
|
||||
{
|
||||
public:
|
||||
explicit RegimmTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_REGIMM_TRANSLATOR_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_SPECIAL_TRANSLATOR_H
|
||||
#define PS2RECOMP_SPECIAL_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class SpecialTranslator
|
||||
{
|
||||
public:
|
||||
explicit SpecialTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_SPECIAL_TRANSLATOR_H
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef PS2RECOMP_VU_TRANSLATOR_H
|
||||
#define PS2RECOMP_VU_TRANSLATOR_H
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct Instruction;
|
||||
class CodeGenerator;
|
||||
|
||||
class VuTranslator
|
||||
{
|
||||
public:
|
||||
explicit VuTranslator(CodeGenerator &codeGenerator);
|
||||
std::string translate(const Instruction &inst);
|
||||
|
||||
private:
|
||||
CodeGenerator &m_codeGenerator;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_VU_TRANSLATOR_H
|
||||
@@ -7,15 +7,18 @@
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include "ps2recomp/control_flow_analyzer.h"
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
struct JumpTableEntry;
|
||||
struct JumpTable;
|
||||
struct Instruction;
|
||||
struct MemoryAccessHint;
|
||||
struct Function;
|
||||
struct Symbol;
|
||||
struct Section;
|
||||
class RecompilerReporter;
|
||||
|
||||
extern const std::unordered_set<std::string> kKeywords;
|
||||
|
||||
@@ -35,33 +38,43 @@ namespace ps2recomp
|
||||
std::string entryName;
|
||||
};
|
||||
|
||||
struct AnalysisResult {
|
||||
std::unordered_set<uint32_t> entryPoints;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> jumpTableTargets;
|
||||
};
|
||||
using AnalysisResult = ControlFlowAnalysisResult;
|
||||
|
||||
std::string generateFunction(const Function &function, const std::vector<Instruction> &instructions, const bool &useHeaders);
|
||||
std::string generateFunctionRegistration(const std::vector<Function> &functions, const std::map<uint32_t, std::string> &stubs);
|
||||
std::string handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot,
|
||||
const Function &function, const AnalysisResult &analysisResult);
|
||||
std::string handleBranchDelaySlots(const Instruction &branchInst, const Instruction &delaySlot,
|
||||
const Function &function, const AnalysisResult &analysisResult,
|
||||
std::string delaySlotOverride);
|
||||
|
||||
void setRenamedFunctions(const std::unordered_map<uint32_t, std::string> &renames);
|
||||
void setBootstrapInfo(const BootstrapInfo &info);
|
||||
void setRelocationCallNames(const std::unordered_map<uint32_t, std::string> &callNames);
|
||||
void setConfiguredJumpTables(const std::vector<JumpTable> &jumpTables);
|
||||
void setResumeEntryTargets(const std::unordered_map<uint32_t, std::vector<uint32_t>> &resumeTargetsByOwner);
|
||||
void setEmitInstructionComments(bool emitInstructionComments);
|
||||
void setReporter(RecompilerReporter *reporter);
|
||||
|
||||
AnalysisResult collectInternalBranchTargets(const Function &function,
|
||||
const std::vector<Instruction> &instructions);
|
||||
const std::vector<Instruction> &instructions,
|
||||
const std::vector<Function> *allFunctions = nullptr);
|
||||
|
||||
public:
|
||||
std::unordered_map<uint32_t, Symbol> m_symbols;
|
||||
std::unordered_map<uint32_t, std::string> m_renamedFunctions;
|
||||
std::unordered_map<uint32_t, std::string> m_relocationCallNames;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> m_configJumpTableTargetsByAddress;
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> m_resumeEntryTargetsByOwner;
|
||||
const std::vector<Section>& m_sections;
|
||||
BootstrapInfo m_bootstrapInfo;
|
||||
bool m_emitInstructionComments = true;
|
||||
RecompilerReporter *m_reporter = nullptr;
|
||||
std::string m_currentFunctionName;
|
||||
|
||||
std::string translateInstruction(const Instruction &inst);
|
||||
std::string translateInstruction(const Instruction &inst, const MemoryAccessHint &memoryHint);
|
||||
std::string emitUnhandledInstruction(const Instruction &inst, const std::string &message);
|
||||
std::string translateMMIInstruction(const Instruction &inst);
|
||||
std::string translateVUInstruction(const Instruction &inst);
|
||||
std::string translateFPUInstruction(const Instruction &inst);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#ifndef PS2RECOMP_CODEGEN_HELPERS_H
|
||||
#define PS2RECOMP_CODEGEN_HELPERS_H
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace ps2recomp::codegen
|
||||
{
|
||||
inline std::string formatFloatLiteral(float value)
|
||||
{
|
||||
if (!std::isfinite(value))
|
||||
{
|
||||
return (value < 0.0f) ? "-INFINITY" : "INFINITY";
|
||||
}
|
||||
|
||||
std::string literal = fmt::format("{:.9g}", value);
|
||||
if (literal.find_first_of(".eE") == std::string::npos)
|
||||
{
|
||||
literal += ".0";
|
||||
}
|
||||
literal += 'f';
|
||||
return literal;
|
||||
}
|
||||
|
||||
inline std::string vuMaskExpr(uint8_t dest_mask)
|
||||
{
|
||||
return fmt::format("_mm_castsi128_ps(_mm_set_epi32({}, {}, {}, {}))",
|
||||
(dest_mask & 0x1) ? -1 : 0,
|
||||
(dest_mask & 0x2) ? -1 : 0,
|
||||
(dest_mask & 0x4) ? -1 : 0,
|
||||
(dest_mask & 0x8) ? -1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // PS2RECOMP_CODEGEN_HELPERS_H
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
namespace ps2recomp
|
||||
{
|
||||
class RecompilerReporter;
|
||||
|
||||
class ConfigManager
|
||||
{
|
||||
public:
|
||||
@@ -14,9 +16,11 @@ namespace ps2recomp
|
||||
|
||||
RecompilerConfig loadConfig() const;
|
||||
void saveConfig(const RecompilerConfig &config) const;
|
||||
void setReporter(RecompilerReporter *reporter);
|
||||
|
||||
private:
|
||||
std::string m_configPath;
|
||||
RecompilerReporter *m_reporter = nullptr;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user